miniledger 0.1.0
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/LICENSE +15 -0
- package/README.md +191 -0
- package/dashboard/app.js +262 -0
- package/dashboard/index.html +102 -0
- package/dashboard/style.css +286 -0
- package/dist/bin/miniledger.cjs +3268 -0
- package/dist/bin/miniledger.cjs.map +1 -0
- package/dist/index.cjs +2778 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +702 -0
- package/dist/index.d.ts +702 -0
- package/dist/index.js +2721 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# MiniLedger
|
|
2
|
+
|
|
3
|
+
**The SQLite of private blockchains.** Zero-config, embeddable, SQL-queryable.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install miniledger
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
MiniLedger is a private/permissioned blockchain that runs in a single Node.js process. No Docker. No Kubernetes. No certificate authorities. Just `npm install` and go.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Initialize and start a node
|
|
15
|
+
npx miniledger init
|
|
16
|
+
npx miniledger start
|
|
17
|
+
|
|
18
|
+
# Submit a transaction
|
|
19
|
+
curl -X POST http://localhost:4441/tx \
|
|
20
|
+
-H "Content-Type: application/json" \
|
|
21
|
+
-d '{"key": "account:alice", "value": {"balance": 1000}}'
|
|
22
|
+
|
|
23
|
+
# Query state with SQL (!)
|
|
24
|
+
curl -X POST http://localhost:4441/state/query \
|
|
25
|
+
-H "Content-Type: application/json" \
|
|
26
|
+
-d '{"sql": "SELECT * FROM world_state"}'
|
|
27
|
+
|
|
28
|
+
# Open the dashboard
|
|
29
|
+
open http://localhost:4441/dashboard
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 30-Second Demo
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx miniledger demo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Spins up a 3-node Raft cluster, deploys contracts, submits sample data, and opens a web dashboard.
|
|
39
|
+
|
|
40
|
+
## Programmatic API
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { MiniLedger } from 'miniledger';
|
|
44
|
+
|
|
45
|
+
const node = await MiniLedger.create({ dataDir: './my-ledger' });
|
|
46
|
+
await node.init();
|
|
47
|
+
await node.start();
|
|
48
|
+
|
|
49
|
+
// Submit a transaction
|
|
50
|
+
await node.submit({ key: 'account:alice', value: { balance: 1000 } });
|
|
51
|
+
|
|
52
|
+
// Query state with SQL
|
|
53
|
+
const results = await node.query(
|
|
54
|
+
'SELECT * FROM world_state WHERE key LIKE ?',
|
|
55
|
+
['account:%']
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Deploy a smart contract
|
|
59
|
+
await node.submit({
|
|
60
|
+
type: 'contract:deploy',
|
|
61
|
+
payload: {
|
|
62
|
+
kind: 'contract:deploy',
|
|
63
|
+
name: 'token',
|
|
64
|
+
version: '1.0',
|
|
65
|
+
code: `return {
|
|
66
|
+
mint(ctx, amount) {
|
|
67
|
+
const bal = ctx.get("balance:" + ctx.sender) || 0;
|
|
68
|
+
ctx.set("balance:" + ctx.sender, bal + amount);
|
|
69
|
+
}
|
|
70
|
+
}`
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
| Feature | Description |
|
|
78
|
+
|---------|-------------|
|
|
79
|
+
| **Zero config** | No Docker, no K8s, no certificate authorities. Single process. |
|
|
80
|
+
| **SQL queryable** | State stored in SQLite. Query with `SELECT * FROM world_state`. |
|
|
81
|
+
| **Raft consensus** | Leader election, log replication, fault tolerance. |
|
|
82
|
+
| **Smart contracts** | Write contracts in JavaScript. Deploy via transactions. |
|
|
83
|
+
| **Per-record privacy** | AES-256-GCM field encryption with ACLs. No channels. |
|
|
84
|
+
| **On-chain governance** | Propose and vote on network changes. Quorum-based. |
|
|
85
|
+
| **Web dashboard** | Built-in block explorer, state browser, SQL console. |
|
|
86
|
+
| **P2P networking** | WebSocket mesh with auto-reconnect and peer discovery. |
|
|
87
|
+
| **Ed25519 identity** | Audited crypto. No PKI setup required. |
|
|
88
|
+
| **TypeScript native** | Full type safety. Dual CJS/ESM package. |
|
|
89
|
+
|
|
90
|
+
## Architecture
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
┌───────────┐
|
|
94
|
+
│ CLI │
|
|
95
|
+
└─────┬─────┘
|
|
96
|
+
│
|
|
97
|
+
┌─────▼─────┐
|
|
98
|
+
│ Node │ (orchestrator)
|
|
99
|
+
└─────┬─────┘
|
|
100
|
+
│
|
|
101
|
+
┌───────┬───────┬───┴───┬───────┬───────┐
|
|
102
|
+
│ │ │ │ │ │
|
|
103
|
+
┌──▼──┐ ┌─▼───┐ ┌─▼────┐ ┌▼─────┐ ┌▼────┐ ┌▼───────┐
|
|
104
|
+
│ API │ │Raft │ │ P2P │ │Contr.│ │Gov. │ │Privacy │
|
|
105
|
+
└──┬──┘ └──┬──┘ └──┬───┘ └──┬───┘ └──┬──┘ └───┬────┘
|
|
106
|
+
└───────┴───────┴────┬───┴────────┴────────┘
|
|
107
|
+
┌──────▼──────┐
|
|
108
|
+
│ Core │ (blocks, transactions, merkle)
|
|
109
|
+
└──────┬──────┘
|
|
110
|
+
┌────────────┼────────────┐
|
|
111
|
+
┌─────▼─────┐ ┌──────▼─────┐
|
|
112
|
+
│ SQLite │ │ Ed25519 │
|
|
113
|
+
└────────────┘ └─────────────┘
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Multi-Node Cluster
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Node 1 (bootstrap)
|
|
120
|
+
miniledger init -d ./node1
|
|
121
|
+
miniledger start -d ./node1 --consensus raft --p2p-port 4440 --api-port 4441
|
|
122
|
+
|
|
123
|
+
# Node 2
|
|
124
|
+
miniledger init -d ./node2
|
|
125
|
+
miniledger join ws://localhost:4440 -d ./node2 --p2p-port 4442 --api-port 4443
|
|
126
|
+
|
|
127
|
+
# Node 3
|
|
128
|
+
miniledger init -d ./node3
|
|
129
|
+
miniledger join ws://localhost:4440 -d ./node3 --p2p-port 4444 --api-port 4445
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## CLI Commands
|
|
133
|
+
|
|
134
|
+
| Command | Description |
|
|
135
|
+
|---------|-------------|
|
|
136
|
+
| `miniledger init` | Initialize a new node (create keys, genesis block) |
|
|
137
|
+
| `miniledger start` | Start the node |
|
|
138
|
+
| `miniledger join <addr>` | Join an existing network |
|
|
139
|
+
| `miniledger demo` | Run a 3-node demo cluster |
|
|
140
|
+
| `miniledger status` | Show node status |
|
|
141
|
+
| `miniledger tx submit <json>` | Submit a transaction |
|
|
142
|
+
| `miniledger query <sql>` | Query state with SQL |
|
|
143
|
+
| `miniledger keys show` | Show node's public key |
|
|
144
|
+
| `miniledger peers list` | List connected peers |
|
|
145
|
+
|
|
146
|
+
## REST API
|
|
147
|
+
|
|
148
|
+
| Endpoint | Method | Description |
|
|
149
|
+
|----------|--------|-------------|
|
|
150
|
+
| `/status` | GET | Node status (height, peers, uptime) |
|
|
151
|
+
| `/blocks` | GET | Recent blocks |
|
|
152
|
+
| `/blocks/:height` | GET | Block by height |
|
|
153
|
+
| `/blocks/latest` | GET | Latest block |
|
|
154
|
+
| `/tx` | POST | Submit transaction |
|
|
155
|
+
| `/tx/:hash` | GET | Transaction by hash |
|
|
156
|
+
| `/state/:key` | GET | State entry by key |
|
|
157
|
+
| `/state/query` | POST | SQL query (`{sql: "SELECT ..."}`) |
|
|
158
|
+
| `/peers` | GET | Connected peers |
|
|
159
|
+
| `/consensus` | GET | Consensus state |
|
|
160
|
+
| `/proposals` | GET | Governance proposals |
|
|
161
|
+
| `/contracts` | GET | Deployed contracts |
|
|
162
|
+
| `/dashboard` | GET | Web dashboard |
|
|
163
|
+
|
|
164
|
+
## Comparison
|
|
165
|
+
|
|
166
|
+
| | MiniLedger | Hyperledger Fabric | R3 Corda |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| **Setup time** | 10 seconds | Hours/days | Hours |
|
|
169
|
+
| **Dependencies** | `npm install` | Docker, K8s, CAs | JVM, Corda node |
|
|
170
|
+
| **Config files** | 0 (auto) | Dozens of YAML | Multiple configs |
|
|
171
|
+
| **Consensus** | Raft (built-in) | Raft (separate orderer) | Notary service |
|
|
172
|
+
| **Smart contracts** | JavaScript | Go/Java/Node | Kotlin/Java |
|
|
173
|
+
| **State queries** | SQL | CouchDB queries | JPA/Vault |
|
|
174
|
+
| **Privacy** | Per-record ACLs | Channels (complex) | Point-to-point |
|
|
175
|
+
| **Governance** | On-chain voting | Off-chain manual | Off-chain |
|
|
176
|
+
| **Dashboard** | Built-in | None (3rd party) | None |
|
|
177
|
+
|
|
178
|
+
## Tech Stack
|
|
179
|
+
|
|
180
|
+
- **Runtime:** Node.js >= 22
|
|
181
|
+
- **State:** SQLite (better-sqlite3, WAL mode)
|
|
182
|
+
- **Crypto:** @noble/ed25519 + @noble/hashes (audited, pure JS)
|
|
183
|
+
- **P2P:** WebSocket mesh (ws)
|
|
184
|
+
- **HTTP:** Hono
|
|
185
|
+
- **CLI:** Commander
|
|
186
|
+
- **Build:** tsup (dual CJS/ESM)
|
|
187
|
+
- **Tests:** Vitest
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
Apache-2.0
|
package/dashboard/app.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// MiniLedger Dashboard — vanilla JS, no build step
|
|
2
|
+
const API = window.location.origin;
|
|
3
|
+
let refreshTimer = null;
|
|
4
|
+
|
|
5
|
+
// ── Fetch helpers ─────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
async function api(path) {
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(API + path);
|
|
10
|
+
if (!res.ok) return null;
|
|
11
|
+
return await res.json();
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function apiPost(path, body) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(API + path, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
return await res.json();
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Formatters ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function shortHash(h) {
|
|
33
|
+
if (!h) return '—';
|
|
34
|
+
return h.substring(0, 8) + '...' + h.substring(h.length - 6);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function timeAgo(ts) {
|
|
38
|
+
if (!ts) return '—';
|
|
39
|
+
const diff = Date.now() - ts;
|
|
40
|
+
if (diff < 1000) return 'just now';
|
|
41
|
+
if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
|
|
42
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
43
|
+
return Math.floor(diff / 3600000) + 'h ago';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatUptime(ms) {
|
|
47
|
+
if (!ms) return '—';
|
|
48
|
+
const s = Math.floor(ms / 1000);
|
|
49
|
+
if (s < 60) return s + 's';
|
|
50
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
|
|
51
|
+
return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Update functions ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function updateStatus() {
|
|
57
|
+
const data = await api('/status');
|
|
58
|
+
if (!data) {
|
|
59
|
+
document.getElementById('status-dot').className = 'dot red';
|
|
60
|
+
document.getElementById('status-text').textContent = 'Disconnected';
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
document.getElementById('status-dot').className = 'dot green';
|
|
65
|
+
document.getElementById('status-text').textContent = 'Running';
|
|
66
|
+
document.getElementById('node-id').textContent = data.nodeId;
|
|
67
|
+
document.getElementById('uptime').textContent = formatUptime(data.uptime);
|
|
68
|
+
document.getElementById('stat-height').textContent = data.chainHeight;
|
|
69
|
+
document.getElementById('stat-txpool').textContent = data.txPoolSize;
|
|
70
|
+
document.getElementById('stat-peers').textContent = data.peerCount;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function updateBlocks() {
|
|
74
|
+
const data = await api('/blocks');
|
|
75
|
+
if (!data) return;
|
|
76
|
+
|
|
77
|
+
const el = document.getElementById('blocks-list');
|
|
78
|
+
document.getElementById('block-count').textContent = data.height + 1;
|
|
79
|
+
|
|
80
|
+
if (!data.blocks || data.blocks.length === 0) {
|
|
81
|
+
el.innerHTML = '<div class="empty-state">No blocks yet</div>';
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Show newest first
|
|
86
|
+
const blocks = [...data.blocks].reverse();
|
|
87
|
+
el.innerHTML = blocks.map(b => `
|
|
88
|
+
<div class="block-item">
|
|
89
|
+
<span class="block-num">#${b.height}</span>
|
|
90
|
+
<span class="block-hash">${shortHash(b.hash)}</span>
|
|
91
|
+
<span class="block-meta">${b.transactions.length} tx · ${timeAgo(b.timestamp)}</span>
|
|
92
|
+
</div>
|
|
93
|
+
`).join('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function updateTransactions() {
|
|
97
|
+
// Get transactions from recent blocks
|
|
98
|
+
const data = await api('/blocks');
|
|
99
|
+
if (!data || !data.blocks) return;
|
|
100
|
+
|
|
101
|
+
const el = document.getElementById('tx-list');
|
|
102
|
+
const allTx = [];
|
|
103
|
+
|
|
104
|
+
for (const block of data.blocks) {
|
|
105
|
+
for (const tx of block.transactions) {
|
|
106
|
+
allTx.push({ ...tx, blockHeight: block.height });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
allTx.sort((a, b) => b.timestamp - a.timestamp);
|
|
111
|
+
const recent = allTx.slice(0, 20);
|
|
112
|
+
|
|
113
|
+
document.getElementById('tx-count').textContent = allTx.length;
|
|
114
|
+
|
|
115
|
+
if (recent.length === 0) {
|
|
116
|
+
el.innerHTML = '<div class="empty-state">No transactions yet</div>';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
el.innerHTML = recent.map(tx => `
|
|
121
|
+
<div class="tx-item">
|
|
122
|
+
<span class="tx-hash">${shortHash(tx.hash)}</span>
|
|
123
|
+
<span class="tx-type">${tx.type}</span>
|
|
124
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:2px">
|
|
125
|
+
Block #${tx.blockHeight} · ${timeAgo(tx.timestamp)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
`).join('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function updatePeers() {
|
|
132
|
+
const data = await api('/peers');
|
|
133
|
+
if (!data) return;
|
|
134
|
+
|
|
135
|
+
const el = document.getElementById('peers-list');
|
|
136
|
+
document.getElementById('peer-badge').textContent = data.count;
|
|
137
|
+
|
|
138
|
+
if (!data.peers || data.peers.length === 0) {
|
|
139
|
+
el.innerHTML = '<div class="empty-state">No peers connected (solo mode)</div>';
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
el.innerHTML = data.peers.map(p => `
|
|
144
|
+
<div class="peer-item">
|
|
145
|
+
<span class="dot ${p.status === 'connected' ? 'green' : 'red'}"></span>
|
|
146
|
+
<span class="peer-id">${p.nodeId}</span>
|
|
147
|
+
<span style="color:var(--text-dim);font-size:12px">${p.address || '—'}</span>
|
|
148
|
+
<span style="margin-left:auto;font-size:12px;color:var(--text-dim)">H:${p.chainHeight}</span>
|
|
149
|
+
</div>
|
|
150
|
+
`).join('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function updateContractsGov() {
|
|
154
|
+
const [contracts, proposals] = await Promise.all([
|
|
155
|
+
api('/contracts'),
|
|
156
|
+
api('/proposals'),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
const el = document.getElementById('contracts-gov');
|
|
160
|
+
let html = '';
|
|
161
|
+
|
|
162
|
+
if (contracts && contracts.contracts && contracts.contracts.length > 0) {
|
|
163
|
+
html += '<div style="margin-bottom:12px;font-size:12px;color:var(--text-dim);font-weight:600">CONTRACTS</div>';
|
|
164
|
+
html += contracts.contracts.map(c => `
|
|
165
|
+
<div class="tx-item">
|
|
166
|
+
<strong>${c.name}</strong> <span class="tx-type">v${c.version}</span>
|
|
167
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:2px">by ${shortHash(c.deployedBy)}</div>
|
|
168
|
+
</div>
|
|
169
|
+
`).join('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (proposals && proposals.proposals && proposals.proposals.length > 0) {
|
|
173
|
+
html += '<div style="margin-top:12px;margin-bottom:12px;font-size:12px;color:var(--text-dim);font-weight:600">PROPOSALS</div>';
|
|
174
|
+
html += proposals.proposals.map(p => `
|
|
175
|
+
<div class="tx-item">
|
|
176
|
+
<strong>${p.title}</strong>
|
|
177
|
+
<span class="tx-type">${p.status}</span>
|
|
178
|
+
<div style="font-size:12px;color:var(--text-dim);margin-top:2px">
|
|
179
|
+
${Object.keys(p.votes).length} votes · by ${shortHash(p.proposer)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
`).join('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!html) {
|
|
186
|
+
html = '<div class="empty-state">No contracts or proposals</div>';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
el.innerHTML = html;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function updateStateCount() {
|
|
193
|
+
const data = await apiPost('/state/query', {
|
|
194
|
+
sql: 'SELECT COUNT(*) as count FROM world_state WHERE key NOT LIKE \'_%\'',
|
|
195
|
+
});
|
|
196
|
+
if (data && data.results && data.results.length > 0) {
|
|
197
|
+
document.getElementById('stat-state').textContent = data.results[0].count || 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── SQL Query ─────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
async function runQuery() {
|
|
204
|
+
const sql = document.getElementById('sql-input').value.trim();
|
|
205
|
+
if (!sql) return;
|
|
206
|
+
|
|
207
|
+
const el = document.getElementById('query-results');
|
|
208
|
+
el.innerHTML = '<div class="empty-state">Running...</div>';
|
|
209
|
+
|
|
210
|
+
const data = await apiPost('/state/query', { sql });
|
|
211
|
+
if (!data) {
|
|
212
|
+
el.innerHTML = '<div class="empty-state" style="color:var(--red)">Query failed</div>';
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (data.error) {
|
|
217
|
+
el.innerHTML = `<div class="empty-state" style="color:var(--red)">${data.error}</div>`;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!data.results || data.results.length === 0) {
|
|
222
|
+
el.innerHTML = '<div class="empty-state">No results</div>';
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cols = Object.keys(data.results[0]);
|
|
227
|
+
const headerRow = cols.map(c => `<th>${c}</th>`).join('');
|
|
228
|
+
const rows = data.results.map(r =>
|
|
229
|
+
'<tr>' + cols.map(c => `<td title="${String(r[c] ?? '')}">${String(r[c] ?? '')}</td>`).join('') + '</tr>'
|
|
230
|
+
).join('');
|
|
231
|
+
|
|
232
|
+
el.innerHTML = `
|
|
233
|
+
<table class="result-table">
|
|
234
|
+
<thead><tr>${headerRow}</tr></thead>
|
|
235
|
+
<tbody>${rows}</tbody>
|
|
236
|
+
</table>
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Enter key runs query
|
|
241
|
+
document.getElementById('sql-input').addEventListener('keydown', (e) => {
|
|
242
|
+
if (e.key === 'Enter') runQuery();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ── Refresh loop ──────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async function refreshAll() {
|
|
248
|
+
await Promise.all([
|
|
249
|
+
updateStatus(),
|
|
250
|
+
updateBlocks(),
|
|
251
|
+
updateTransactions(),
|
|
252
|
+
updatePeers(),
|
|
253
|
+
updateContractsGov(),
|
|
254
|
+
updateStateCount(),
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Initial load
|
|
259
|
+
refreshAll();
|
|
260
|
+
|
|
261
|
+
// Auto-refresh every 2 seconds
|
|
262
|
+
refreshTimer = setInterval(refreshAll, 2000);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>MiniLedger Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/dashboard/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<h1><span>Mini</span>Ledger</h1>
|
|
12
|
+
<div class="status-bar">
|
|
13
|
+
<span><span class="dot green" id="status-dot"></span><span id="status-text">Connecting...</span></span>
|
|
14
|
+
<span>Node: <strong id="node-id">—</strong></span>
|
|
15
|
+
<span>Uptime: <strong id="uptime">—</strong></span>
|
|
16
|
+
</div>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<div class="stats">
|
|
20
|
+
<div class="stat">
|
|
21
|
+
<div class="stat-value" id="stat-height">0</div>
|
|
22
|
+
<div class="stat-label">Block Height</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="stat">
|
|
25
|
+
<div class="stat-value" id="stat-txpool">0</div>
|
|
26
|
+
<div class="stat-label">TX Pool</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="stat">
|
|
29
|
+
<div class="stat-value" id="stat-peers">0</div>
|
|
30
|
+
<div class="stat-label">Peers</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="stat">
|
|
33
|
+
<div class="stat-value" id="stat-state">0</div>
|
|
34
|
+
<div class="stat-label">State Keys</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="grid">
|
|
39
|
+
<!-- Blocks -->
|
|
40
|
+
<div class="card">
|
|
41
|
+
<div class="card-header">
|
|
42
|
+
Recent Blocks
|
|
43
|
+
<span class="badge" id="block-count">0</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card-body" id="blocks-list">
|
|
46
|
+
<div class="empty-state">Loading blocks...</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Transactions -->
|
|
51
|
+
<div class="card">
|
|
52
|
+
<div class="card-header">
|
|
53
|
+
Recent Transactions
|
|
54
|
+
<span class="badge" id="tx-count">0</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="card-body" id="tx-list">
|
|
57
|
+
<div class="empty-state">No transactions yet</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- SQL Query -->
|
|
62
|
+
<div class="card full-width">
|
|
63
|
+
<div class="card-header">
|
|
64
|
+
State Explorer
|
|
65
|
+
<span class="badge">SQL</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="card-body">
|
|
68
|
+
<div class="query-area">
|
|
69
|
+
<input type="text" id="sql-input" placeholder="SELECT * FROM world_state LIMIT 20" value="SELECT * FROM world_state ORDER BY updated_at DESC LIMIT 20">
|
|
70
|
+
<button onclick="runQuery()">Run Query</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="query-results">
|
|
73
|
+
<div class="empty-state">Run a query to see results</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Peers -->
|
|
79
|
+
<div class="card">
|
|
80
|
+
<div class="card-header">
|
|
81
|
+
Network Peers
|
|
82
|
+
<span class="badge" id="peer-badge">0</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="card-body" id="peers-list">
|
|
85
|
+
<div class="empty-state">No peers connected</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Contracts & Governance -->
|
|
90
|
+
<div class="card">
|
|
91
|
+
<div class="card-header">
|
|
92
|
+
Contracts & Governance
|
|
93
|
+
</div>
|
|
94
|
+
<div class="card-body" id="contracts-gov">
|
|
95
|
+
<div class="empty-state">Loading...</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script src="/dashboard/app.js"></script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|