Skip to content

Commit ba1c410

Browse files
committed
Status lights and block heights for L1
- Show status light for bitcoin, enforcer, bitwindow - Show current block height of bitcoin, enforcer - Import enfocer grpc proto files
1 parent 78fafca commit ba1c410

15 files changed

+1541
-69
lines changed

package-lock.json

+662-60
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+18-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"@fortawesome/free-regular-svg-icons": "^6.6.0",
1616
"@fortawesome/free-solid-svg-icons": "^6.6.0",
1717
"@fortawesome/react-fontawesome": "^0.2.2",
18+
"@grpc/grpc-js": "^1.12.5",
19+
"@grpc/proto-loader": "^0.7.13",
1820
"@reduxjs/toolkit": "^2.2.6",
1921
"@testing-library/jest-dom": "^5.17.0",
2022
"@testing-library/react": "^13.4.0",
@@ -88,12 +90,19 @@
8890
"buildResources": "public"
8991
},
9092
"linux": {
91-
"target": ["deb", "tar.gz"],
93+
"target": [
94+
"deb",
95+
"tar.gz"
96+
],
9297
"category": "Development",
9398
"maintainer": "Drivechain <support@drivechain.info>",
94-
"asarUnpack": ["**/chrome-sandbox"],
99+
"asarUnpack": [
100+
"**/chrome-sandbox"
101+
],
95102
"executableName": "drivechain-launcher",
96-
"executableArgs": ["--no-sandbox"],
103+
"executableArgs": [
104+
"--no-sandbox"
105+
],
97106
"artifactName": "Drivechain-Launcher-${version}-${arch}.${ext}"
98107
},
99108
"win": {
@@ -104,20 +113,23 @@
104113
"target": [
105114
{
106115
"target": "dmg",
107-
"arch": ["x64", "arm64"]
116+
"arch": [
117+
"x64",
118+
"arm64"
119+
]
108120
}
109121
],
110122
"category": "public.app-category.developer-tools",
111123
"artifactName": "Drivechain-Launcher-${version}-${arch}.${ext}"
112124
}
113125
},
114126
"devDependencies": {
115-
"electron": "^28.1.0",
116-
"electron-builder": "^24.9.1",
117127
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
118128
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
119129
"@babel/preset-env": "^7.26.0",
120130
"babel-jest": "^29.7.0",
131+
"electron": "^28.1.0",
132+
"electron-builder": "^24.9.1",
121133
"prettier": "^3.3.3"
122134
}
123135
}

public/electron.js

+9
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ function setupIPCHandlers() {
207207
}
208208
});
209209

210+
ipcMain.handle("get-chain-block-count", async (event, chainId) => {
211+
try {
212+
return await chainManager.getChainBlockCount(chainId);
213+
} catch (error) {
214+
console.error("Failed to get chain block count:", error);
215+
return 0;
216+
}
217+
});
218+
210219
ipcMain.handle("reset-chain", async (event, chainId) => {
211220
try {
212221
return await chainManager.resetChain(chainId);

public/modules/chainManager.js

+25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require("path");
44
const { spawn } = require("child_process");
55
const BitcoinMonitor = require("./bitcoinMonitor");
66
const BitWindowClient = require("./bitWindowClient");
7+
const EnforcerClient = require("./enforcerClient");
78

89
class ChainManager {
910
constructor(mainWindow, config) {
@@ -16,6 +17,7 @@ class ChainManager {
1617
this.bitWindowClient = new BitWindowClient();
1718
this.logProcesses = new Map(); // Track log streaming processes
1819
this.processCheckers = new Map(); // Track process check intervals
20+
this.enforcerClient = new EnforcerClient(); // Connect to enfocer gRPC
1921
}
2022

2123
async isChainReady(chainId) {
@@ -643,6 +645,29 @@ class ChainManager {
643645
const basePath = path.join(downloadsDir, extractDir);
644646
return path.join(basePath, path.dirname(binaryPath));
645647
}
648+
649+
async getChainBlockCount(chainId) {
650+
const status = this.chainStatuses.get(chainId);
651+
if (status !== 'running') return -1;
652+
653+
if (chainId === 'bitcoin') {
654+
try {
655+
return await this.bitcoinMonitor.makeRpcCall('getblockcount', [], true);
656+
} catch (error) {
657+
return -1;
658+
}
659+
}
660+
else
661+
if (chainId == "enforcer") {
662+
try {
663+
return await this.enforcerClient.getBlockCount();
664+
} catch (error) {
665+
return -1;
666+
}
667+
}
668+
669+
return -1;
670+
}
646671
}
647672

648673
module.exports = ChainManager;

public/modules/enforcerClient.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const grpc = require('@grpc/grpc-js');
2+
const protoLoader = require('@grpc/proto-loader');
3+
const path = require('path');
4+
5+
class EnforcerClient {
6+
constructor() {
7+
// Load the proto file
8+
const protoPath = path.join(__dirname, '../..', 'src/data/proto/cusf/mainchain/v1/validator.proto');
9+
const packageDefinition = protoLoader.loadSync(protoPath, {
10+
keepCase: true,
11+
longs: String,
12+
enums: String,
13+
defaults: true,
14+
oneofs: true,
15+
includeDirs: [path.join(__dirname, '../..', 'src/data/proto')]
16+
});
17+
18+
// Load the ValidatorService package
19+
const proto = grpc.loadPackageDefinition(packageDefinition);
20+
const validatorService = proto.cusf.mainchain.v1.ValidatorService;
21+
22+
// Create the client
23+
this.client = new validatorService(
24+
'localhost:50051', // Default gRPC port, adjust if needed
25+
grpc.credentials.createInsecure()
26+
);
27+
}
28+
29+
async getBlockCount() {
30+
return new Promise((resolve, reject) => {
31+
this.client.getChainTip({}, (error, response) => {
32+
if (error) {
33+
reject(error);
34+
return;
35+
}
36+
37+
// Response contains BlockHeaderInfo with height field
38+
resolve(response.block_header_info.height);
39+
});
40+
});
41+
}
42+
}
43+
44+
module.exports = EnforcerClient;

public/modules/validator.proto

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
syntax = "proto3";
2+
package cusf.mainchain.v1;
3+
4+
message GetChainTipRequest {}
5+
6+
message Hash {
7+
string hex = 1;
8+
}
9+
10+
message Work {
11+
string hex = 1;
12+
}
13+
14+
message BlockHeaderInfo {
15+
Hash blockHash = 1;
16+
Hash prevBlockHash = 2;
17+
uint64 height = 3;
18+
Work work = 4;
19+
}
20+
21+
message GetChainTipResponse {
22+
BlockHeaderInfo blockHeaderInfo = 1;
23+
}
24+
25+
service ValidatorService {
26+
rpc GetChainTip(GetChainTipRequest) returns (GetChainTipResponse);
27+
}

public/preload.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
1212
getConfig: () => ipcRenderer.invoke("get-config"),
1313
downloadChain: (chainId) => ipcRenderer.invoke("download-chain", chainId),
1414
startChain: (chainId, args) => ipcRenderer.invoke("start-chain", chainId, args),
15+
getChainBlockCount: (chainId) => ipcRenderer.invoke("get-chain-block-count", chainId),
1516
getMnemonicPath: (chainId) => ipcRenderer.invoke("get-mnemonic-path", chainId),
1617
stopChain: (chainId) => ipcRenderer.invoke("stop-chain", chainId),
1718
getChainStatus: (chainId) => ipcRenderer.invoke("get-chain-status", chainId),

src/components/Card.js

+69-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ChainSettingsModal from './ChainSettingsModal';
44
import ForceStopModal from './ForceStopModal';
55
import SettingsIcon from './SettingsIcon';
66
import Tooltip from './Tooltip';
7+
import './StatusLight.css';
78

89
const Card = ({
910
chain,
@@ -23,8 +24,62 @@ const Card = ({
2324
const [lastActionTime, setLastActionTime] = useState(0);
2425
const [tooltipVisible, setTooltipVisible] = useState(false);
2526
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
27+
const [processHealth, setProcessHealth] = useState('offline'); // 'healthy', 'warning', 'error', 'offline'
28+
const [blockCount, setBlockCount] = useState(-1);
2629
const buttonRef = useRef(null);
2730

31+
// Periodic chain status / health check
32+
useEffect(() => {
33+
const fetchBlockCount = async () => {
34+
console.log("chain name: ", chain)
35+
try {
36+
const count = await window.electronAPI.getChainBlockCount(chain.id);
37+
console.log("new count: ", count)
38+
setBlockCount(count);
39+
} catch (error) {
40+
setBlockCount(-1)
41+
console.error('Failed to fetch block count:', error);
42+
}
43+
};
44+
45+
const interval = setInterval(() => {
46+
// Check on the health and status of chains
47+
if (chain.id === "bitwindow") {
48+
// BitWindow does not return a block count, so just check if it is running
49+
if (chain.status === 'stopping' || chain.status === 'stopped') {
50+
setProcessHealth('offline');
51+
}
52+
else
53+
if (chain.status === 'running') {
54+
setProcessHealth('healthy');
55+
}
56+
else {
57+
setProcessHealth('warning');
58+
}
59+
} else {
60+
// Other chains can tell us their current block height
61+
fetchBlockCount();
62+
if (chain.status === 'stopping' || chain.status === 'stopped') {
63+
setProcessHealth('offline');
64+
}
65+
else
66+
if (blockCount === -1) {
67+
setProcessHealth('offline');
68+
}
69+
else
70+
if (blockCount === 0) {
71+
setProcessHealth('warning');
72+
}
73+
else
74+
if (blockCount > 0) {
75+
setProcessHealth('healthy');
76+
}
77+
}
78+
}, 1000);
79+
80+
return () => clearInterval(interval);
81+
}, [chain.id, chain.status, blockCount]);
82+
2883
const checkDependencies = () => {
2984
if (!chain.dependencies || chain.dependencies.length === 0) return true;
3085
return chain.dependencies.every(dep => runningNodes.includes(dep));
@@ -132,6 +187,8 @@ const Card = ({
132187
return;
133188
}
134189

190+
setProcessHealth('offline');
191+
135192
console.log(`Stopping chain ${chain.id}`);
136193
// Update UI immediately to show stopping state
137194
onUpdateChain(chain.id, { status: 'stopping' });
@@ -237,8 +294,17 @@ const Card = ({
237294
<>
238295
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minWidth: '300px' }}>
239296
<div className={`card ${isDarkMode ? 'dark' : 'light'}`}>
240-
<div className="card-header">
241-
<h2>{chain.display_name}</h2>
297+
<div className="card-header" style={{ position: 'relative' }}>
298+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
299+
<h2 style={{ margin: 0, lineHeight: 1.2, textAlign: 'left' }}>{chain.display_name}</h2>
300+
<div className={`status-light ${processHealth}`} title={`Process Status: ${processHealth}`} />
301+
</div>
302+
<div style={{ fontSize: '0.9em', color: isDarkMode ? 'rgba(255, 255, 255, 0.7)' : 'rgba(51, 51, 51, 0.7)', marginTop: '4px', fontWeight: 400 }}>
303+
{chain.id === 'bitwindow' ?
304+
(processHealth === 'healthy' ? 'Running' : 'Not started') :
305+
(processHealth === 'healthy' && blockCount >= 0 ? `#Blocks: ${blockCount}` : 'Not started')}
306+
</div>
307+
242308
</div>
243309
<div className="card-content">
244310
<p>{chain.description}</p>
@@ -294,4 +360,4 @@ const Card = ({
294360
);
295361
};
296362

297-
export default Card;
363+
export default Card;

src/components/StatusLight.css

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.status-light {
2+
width: 12px;
3+
height: 12px;
4+
border-radius: 50%;
5+
position: relative;
6+
display: inline-block;
7+
margin-right: 10px;
8+
transition: background-color 0.3s ease;
9+
}
10+
11+
/* Add z-index to ensure lights are visible */
12+
.status-light.healthy {
13+
background-color: #2ecc71; /* Green */
14+
box-shadow: 0 0 8px rgba(46, 204, 113, 0.5);
15+
}
16+
17+
.status-light.warning {
18+
background-color: #f1c40f; /* Yellow */
19+
box-shadow: 0 0 8px rgba(241, 196, 15, 0.5);
20+
}
21+
22+
.status-light.error {
23+
background-color: #e74c3c; /* Red */
24+
box-shadow: 0 0 8px rgba(231, 76, 60, 0.5);
25+
}
26+
27+
.status-light.offline {
28+
background-color: #95a5a6; /* Gray */
29+
box-shadow: 0 0 8px rgba(149, 165, 166, 0.5);
30+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* Common message types */
2+
3+
syntax = "proto3";
4+
package cusf.common.v1;
5+
6+
import "google/protobuf/wrappers.proto";
7+
8+
/// Consensus-encoded hex.
9+
/// Variable length data uses a length prefix.
10+
message ConsensusHex {
11+
google.protobuf.StringValue hex = 1;
12+
}
13+
14+
/// Hex encoding of byte arrays/vectors.
15+
/// Length prefixes are not used.
16+
message Hex {
17+
google.protobuf.StringValue hex = 1;
18+
}
19+
20+
/// Reverse consensus-encoded hex
21+
message ReverseHex {
22+
google.protobuf.StringValue hex = 1;
23+
}

0 commit comments

Comments
 (0)