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 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
@@ -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>