@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 +671 -126
- 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
|
// ============================================================
|
|
@@ -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">📋</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">🔗</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">🔑</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">🔍</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">💸</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">🤖</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">🚰</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
|
+
⚠️ <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
|
-
|
|
1245
|
+
// ── UI Server (standalone script, forked as background process) ──
|
|
927
1246
|
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
934
|
-
const
|
|
1259
|
+
async function fetchJson(url) {
|
|
1260
|
+
const r = await fetch(url);
|
|
1261
|
+
return r.json();
|
|
1262
|
+
}
|
|
935
1263
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
976
|
-
|
|
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
|
-
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
|
1018
|
-
json(200, { message: '
|
|
1019
|
-
} catch
|
|
1020
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
2068
|
+
// Step 6: Start UI dashboard
|
|
1528
2069
|
startUiServer(cfg, api)
|
|
1529
2070
|
|
|
1530
|
-
// Step
|
|
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
|
},
|
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.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": {
|