blockyard 0.0.1 → 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.
Files changed (122) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +191 -4
  5. package/SECURITY.md +38 -0
  6. package/bin/blockyard.js +41 -0
  7. package/config/pool-map.json +2620 -0
  8. package/docs/API.md +1577 -0
  9. package/docs/ARCHITECTURE.md +1394 -0
  10. package/docs/AUTO-UPDATE.md +269 -0
  11. package/docs/CONFIGURATION.md +847 -0
  12. package/docs/DEFECTS.md +813 -0
  13. package/docs/EFFECTS-AGENTS.md +448 -0
  14. package/docs/GETTING-STARTED.md +205 -0
  15. package/docs/INSTALL.md +547 -0
  16. package/docs/MEASUREMENTS.md +1401 -0
  17. package/docs/RULES.md +681 -0
  18. package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
  19. package/docs/SECURITY-AUDIT.md +258 -0
  20. package/docs/SECURITY.md +212 -0
  21. package/docs/TROUBLESHOOTING.md +332 -0
  22. package/docs/USER-GUIDE.md +1262 -0
  23. package/package.json +53 -5
  24. package/public/404.html +9 -0
  25. package/public/css/app.css +2009 -0
  26. package/public/donate-qr.png +0 -0
  27. package/public/index.html +1085 -0
  28. package/public/js/about.js +112 -0
  29. package/public/js/agents.js +1141 -0
  30. package/public/js/app.js +1386 -0
  31. package/public/js/arkanoid.js +806 -0
  32. package/public/js/blockanoid.js +347 -0
  33. package/public/js/blockout.js +347 -0
  34. package/public/js/blockpack.js +428 -0
  35. package/public/js/blockscene3d.js +2830 -0
  36. package/public/js/breakout.js +224 -0
  37. package/public/js/charts.js +635 -0
  38. package/public/js/depthchart.js +315 -0
  39. package/public/js/details3d.js +4342 -0
  40. package/public/js/doom.js +31 -0
  41. package/public/js/dosaudio.js +48 -0
  42. package/public/js/dosgame.js +389 -0
  43. package/public/js/dosio.js +186 -0
  44. package/public/js/dospc.js +1353 -0
  45. package/public/js/dosworker.js +196 -0
  46. package/public/js/explorer.js +405 -0
  47. package/public/js/feepalette.js +149 -0
  48. package/public/js/fmt.js +162 -0
  49. package/public/js/goggles.js +886 -0
  50. package/public/js/kiosk.js +41 -0
  51. package/public/js/login.js +88 -0
  52. package/public/js/markets.js +395 -0
  53. package/public/js/mining.js +1416 -0
  54. package/public/js/panels.js +970 -0
  55. package/public/js/pricechart.js +189 -0
  56. package/public/js/quake.js +20 -0
  57. package/public/js/settings.js +1096 -0
  58. package/public/js/soundcard.js +459 -0
  59. package/public/js/tetris.js +226 -0
  60. package/public/js/tetrust.js +356 -0
  61. package/public/js/tetsound.js +175 -0
  62. package/public/js/theme.js +235 -0
  63. package/public/js/wolf3d.js +22 -0
  64. package/public/js/x86.js +1978 -0
  65. package/public/login.html +33 -0
  66. package/scripts/blockfile-measure.js +156 -0
  67. package/scripts/browser-check.mjs +286 -0
  68. package/scripts/check.js +173 -0
  69. package/scripts/decode-check.js +81 -0
  70. package/scripts/doc-counts.js +109 -0
  71. package/scripts/donate-qr.py +23 -0
  72. package/scripts/dos-bench.js +56 -0
  73. package/scripts/fake-node.js +534 -0
  74. package/scripts/index-bench.js +216 -0
  75. package/scripts/index-benchmark.js +117 -0
  76. package/scripts/index-build.js +40 -0
  77. package/scripts/live-render-check.mjs +89 -0
  78. package/scripts/manage-users.js +132 -0
  79. package/scripts/motion-check.mjs +138 -0
  80. package/scripts/pool-map.js +157 -0
  81. package/scripts/setup.js +432 -0
  82. package/scripts/shots.mjs +278 -0
  83. package/scripts/smoke.sh +327 -0
  84. package/scripts/tls.js +31 -0
  85. package/scripts/ui.js +174 -0
  86. package/server/auth/sessions.js +221 -0
  87. package/server/auth/users.js +243 -0
  88. package/server/chain/blockfile.js +234 -0
  89. package/server/chain/index/build.js +210 -0
  90. package/server/chain/index/heights.js +36 -0
  91. package/server/chain/index/live.js +276 -0
  92. package/server/chain/index/rows.js +145 -0
  93. package/server/chain/index/store.js +154 -0
  94. package/server/chain/index/worker.js +109 -0
  95. package/server/chain/tx.js +310 -0
  96. package/server/collect/gbt.js +229 -0
  97. package/server/collect/logparse.js +765 -0
  98. package/server/collect/logtail.js +189 -0
  99. package/server/collect/markets.js +333 -0
  100. package/server/collect/mining.js +333 -0
  101. package/server/collect/monitor.js +2545 -0
  102. package/server/collect/network.js +295 -0
  103. package/server/collect/nextblock.js +275 -0
  104. package/server/collect/sync.js +386 -0
  105. package/server/config.js +644 -0
  106. package/server/http/api.js +1319 -0
  107. package/server/http/explorer.js +418 -0
  108. package/server/http/games.js +77 -0
  109. package/server/http/server.js +420 -0
  110. package/server/http/sse.js +176 -0
  111. package/server/http/static.js +212 -0
  112. package/server/main.js +673 -0
  113. package/server/netinfo.js +253 -0
  114. package/server/rpc/allowlist.js +130 -0
  115. package/server/rpc/client.js +414 -0
  116. package/server/store/audit.js +148 -0
  117. package/server/store/history.js +220 -0
  118. package/server/store/ledger.js +290 -0
  119. package/server/store/ring.js +173 -0
  120. package/server/tls/selfsigned.js +160 -0
  121. package/server/util/fmt.js +29 -0
  122. package/systemd/blockyard.service +102 -0
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // feepalette.js -- feerate bands and their colours, for the block-space
4
+ // viewer, the block meter and the feerate legend.
5
+ //
6
+ // THE BANDS are a geometric series: from FLOOR_RATE to TOP_RATE in equal
7
+ // ratios, each threshold rounded to two significant figures so the legend
8
+ // reads in plain numbers. Everything below FLOOR_RATE is band 0. A geometric
9
+ // series is what a feerate scale wants: the difference between 1 and 2 sat/vB
10
+ // matters as much as the difference between 500 and 1000, so the bands sit
11
+ // close together near the floor and far apart near the ceiling.
12
+ //
13
+ // 128 BANDS FROM 0.1 sat/vB (operator, 2026-09-11: "we really need more color
14
+ // separation. at least 64 different colors and shades", then "we should try
15
+ // 128 colors"). The first palette began at 1 sat/vB with 36 bands, and this
16
+ // chain's blocks run from 0.1 to a few tens of sat/vB -- so most of a block
17
+ // fell into band 0 and the board was one green. Starting at 0.1 puts
18
+ // 0.1..30 sat/vB across some 73 bands, each step about 8% up the scale.
19
+ // Thresholds are two significant figures, three where two would repeat the
20
+ // band below (an 8% step is finer than two figures near the start of a decade).
21
+ //
22
+ // THE COLOURS are a ramp through a handful of anchors, interpolated in HSL
23
+ // with the hue unwrapped so it only ever travels one way: sky blue for the
24
+ // cheapest, through teal, green, yellow-green, yellow, amber, orange, red and
25
+ // crimson, to purple for the most expensive -- 280 degrees of hue. Band i takes
26
+ // the colour at i / (bands - 1) along the ramp, and its lightness steps
27
+ // through a three-band pattern (LIGHT_STEP), so two neighbouring bands differ
28
+ // in tone as well as hue and read as separate colours side by side.
29
+ //
30
+ // feeShade() then gives each tile its own tone of its band's colour: darker
31
+ // at the bottom of the band, brighter toward its top, plus a small fixed
32
+ // variation per transaction id so a field of equal feerates reads as a field
33
+ // of separate stones rather than one flat slab. Only the brightness moves;
34
+ // the hue is the band's.
35
+
36
+ const FLOOR_RATE = 0.1; // sat/vB: the start of band 1 (below it is band 0)
37
+ const TOP_RATE = 2000; // sat/vB: the start of the last band
38
+ const STEPS = 126; // ratios between FLOOR_RATE and TOP_RATE: 128 bands in all
39
+
40
+ // n significant figures: figures(1.2505, 2) -> 1.3, figures(14.6, 2) -> 15, figures(1.049, 3) -> 1.05
41
+ function figures(v, n) {
42
+ const mag = 10 ** (Math.floor(Math.log10(v)) - (n - 1));
43
+ return Math.round(v / mag) * mag;
44
+ }
45
+ const tidy = (v) => Number(v.toPrecision(12)); // drop float dust (1.3000000000000003)
46
+
47
+ export const FEE_BANDS = Object.freeze((() => {
48
+ const bands = [0];
49
+ for (let i = 0; i <= STEPS; i++) {
50
+ const v = FLOOR_RATE * (TOP_RATE / FLOOR_RATE) ** (i / STEPS);
51
+ const two = tidy(figures(v, 2));
52
+ bands.push(two > bands.at(-1) ? two : tidy(figures(v, 3)));
53
+ }
54
+ return bands;
55
+ })());
56
+
57
+ // [position along the ramp, hue (deg), saturation, lightness], with the rate
58
+ // that lands there. Hues are unwrapped (they fall from blue past 0 into
59
+ // negative = crimson/purple), so linear interpolation moves the hue one way.
60
+ const ANCHORS = [
61
+ [0.00, 205, 0.62, 0.42], // sky blue under 0.1 sat/vB
62
+ [0.12, 172, 0.60, 0.40], // teal ~0.3
63
+ [0.24, 138, 0.58, 0.40], // green ~1
64
+ [0.35, 96, 0.58, 0.45], // yellow-green ~3
65
+ [0.47, 58, 0.72, 0.50], // yellow ~10
66
+ [0.56, 40, 0.76, 0.51], // amber ~25
67
+ [0.65, 24, 0.78, 0.51], // orange ~60
68
+ [0.78, 2, 0.72, 0.50], // red ~250
69
+ [0.89, -28, 0.70, 0.46], // crimson ~700
70
+ [1.00, -75, 0.60, 0.46], // purple 2000 and up
71
+ ];
72
+ // lightness added band by band, in a repeating three-step pattern
73
+ const LIGHT_STEP = [0, 0.075, -0.06];
74
+
75
+ function hslToRgb(h, s, l) {
76
+ const hh = ((h % 360) + 360) % 360;
77
+ const c = (1 - Math.abs(2 * l - 1)) * s;
78
+ const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
79
+ const m = l - c / 2;
80
+ const [r, g, b] = hh < 60 ? [c, x, 0] : hh < 120 ? [x, c, 0] : hh < 180 ? [0, c, x]
81
+ : hh < 240 ? [0, x, c] : hh < 300 ? [x, 0, c] : [c, 0, x];
82
+ return [r + m, g + m, b + m].map((v) => Math.round(v * 255));
83
+ }
84
+
85
+ function rampAt(t, dl = 0) {
86
+ let i = 0;
87
+ while (i < ANCHORS.length - 2 && t > ANCHORS[i + 1][0]) i++;
88
+ const [t0, h0, s0, l0] = ANCHORS[i];
89
+ const [t1, h1, s1, l1] = ANCHORS[i + 1];
90
+ const f = Math.min(1, Math.max(0, (t - t0) / (t1 - t0)));
91
+ return hslToRgb(h0 + (h1 - h0) * f, s0 + (s1 - s0) * f, l0 + (l1 - l0) * f + dl);
92
+ }
93
+
94
+ const hex = (rgb) => '#' + rgb.map((v) => Math.min(255, Math.max(0, Math.round(v))).toString(16).padStart(2, '0')).join('');
95
+
96
+ const BAND_RGB = FEE_BANDS.map((_, i) => rampAt(i / (FEE_BANDS.length - 1), LIGHT_STEP[i % LIGHT_STEP.length]));
97
+ const BAND_HEX = BAND_RGB.map(hex);
98
+
99
+ export function feeBandIndex(rate) {
100
+ const r = Number(rate);
101
+ if (!Number.isFinite(r) || r <= 0) return 0;
102
+ // binary search: the highest band whose threshold is <= r
103
+ let lo = 0, hi = FEE_BANDS.length - 1;
104
+ while (lo < hi) {
105
+ const mid = (lo + hi + 1) >> 1;
106
+ if (FEE_BANDS[mid] <= r) lo = mid; else hi = mid - 1;
107
+ }
108
+ return lo;
109
+ }
110
+
111
+ export function feeColor(rate) {
112
+ return BAND_HEX[feeBandIndex(rate)];
113
+ }
114
+
115
+ // Where a rate sits inside its band, 0 at the threshold and 1 at the next.
116
+ // The last band has no upper threshold; it is given one as wide as itself.
117
+ function inBand(rate, i) {
118
+ const r = Number(rate);
119
+ if (!Number.isFinite(r) || r <= 0) return 0;
120
+ const lo = FEE_BANDS[i];
121
+ const hi = i + 1 < FEE_BANDS.length ? FEE_BANDS[i + 1] : lo * 2;
122
+ return Math.min(1, Math.max(0, (r - lo) / (hi - lo)));
123
+ }
124
+
125
+ // FNV-1a, 32 bits: a stable number per key, the same in every browser
126
+ function hashKey(key) {
127
+ let h = 0x811c9dc5;
128
+ for (let i = 0; i < key.length; i++) {
129
+ h ^= key.charCodeAt(i);
130
+ h = Math.imul(h, 0x01000193);
131
+ }
132
+ return h >>> 0;
133
+ }
134
+
135
+ const SHADE_LOW = 0.84; // brightness at the bottom of a band
136
+ const SHADE_SPAN = 0.26; // added across the band (so 1.10 at its top)
137
+ const KEY_SPREAD = 0.06; // +/- per-key variation
138
+
139
+ export function feeShade(rate, key = '') {
140
+ const i = feeBandIndex(rate);
141
+ const rgb = BAND_RGB[i];
142
+ let k = SHADE_LOW + SHADE_SPAN * inBand(rate, i);
143
+ const id = key == null ? '' : String(key);
144
+ if (id) k += KEY_SPREAD * ((hashKey(id) % 1001) / 500 - 1);
145
+ // never past full brightness in any channel: clipping one channel would
146
+ // change the hue, and the hue is the band's
147
+ k = Math.min(k, 255 / Math.max(1, ...rgb));
148
+ return hex(rgb.map((v) => v * k));
149
+ }
@@ -0,0 +1,162 @@
1
+ // Display formatters. Units are decimal (1 KB = 1000 B) to match how the node
2
+ // itself prints sizes -- its log renders 4096 bytes as "4.0KB", so the monitor
3
+ // must not be the one place on the box that says 4.1 KB.
4
+ const U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
5
+
6
+ export function bytes(n, dp = 1) {
7
+ if (n == null || !Number.isFinite(n)) return '–';
8
+ let v = Math.abs(n);
9
+ let i = 0;
10
+ while (v >= 1000 && i < U.length - 1) { v /= 1000; i += 1; }
11
+ return `${n < 0 ? '-' : ''}${v.toFixed(i === 0 ? 0 : dp)} ${U[i]}`;
12
+ }
13
+
14
+ export function rate(bps, unit = 'B/s') {
15
+ if (bps == null || !Number.isFinite(bps)) return '–';
16
+ let v = Math.abs(bps);
17
+ let i = 0;
18
+ while (v >= 1000 && i < U.length - 1) { v /= 1000; i += 1; }
19
+ return `${(bps < 0 ? '-' : '')}${v.toFixed(i === 0 ? 0 : 1)} ${U[i]}/${unit === 'B/s' ? 's' : unit}`;
20
+ }
21
+
22
+ export function num(n, dp = 0) {
23
+ if (n == null || !Number.isFinite(n)) return '–';
24
+ return n.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp });
25
+ }
26
+
27
+ export function pct(n, dp = 2) {
28
+ if (n == null || !Number.isFinite(n)) return '–';
29
+ return `${n.toFixed(dp)}%`;
30
+ }
31
+
32
+ // DD:HH:MM:SS, matching the node's own ETA and heartbeat format.
33
+ export function eta(sec) {
34
+ if (sec == null || !Number.isFinite(sec) || sec < 0) return '–';
35
+ const s = Math.floor(sec);
36
+ const p = (x, w = 2) => String(x).padStart(w, '0');
37
+ return `${p(Math.floor(s / 86400), 2)}:${p(Math.floor((s % 86400) / 3600))}:${p(Math.floor((s % 3600) / 60))}:${p(s % 60)}`;
38
+ }
39
+
40
+ export function ago(ms, now = Date.now()) {
41
+ if (ms == null || !Number.isFinite(ms)) return '–';
42
+ const s = Math.max(0, (now - ms) / 1000);
43
+ if (s < 45) return `${Math.round(s)}s ago`;
44
+ if (s < 5400) return `${Math.round(s / 60)}m ago`;
45
+ if (s < 172800) return `${(s / 3600).toFixed(1)}h ago`;
46
+ return `${(s / 86400).toFixed(1)}d ago`;
47
+ }
48
+
49
+ export function ageSec(sec) {
50
+ if (sec == null || !Number.isFinite(sec)) return '–';
51
+ if (sec < 60) return `${Math.round(sec)}s`;
52
+ if (sec < 5400) return `${(sec / 60).toFixed(1)}m`;
53
+ if (sec < 172800) return `${(sec / 3600).toFixed(1)}h`;
54
+ return `${(sec / 86400).toFixed(1)}d`;
55
+ }
56
+
57
+ export function sats(n) {
58
+ if (n == null || !Number.isFinite(n)) return '–';
59
+ return n.toLocaleString('en-US');
60
+ }
61
+
62
+ export function btc(sat) {
63
+ if (sat == null || !Number.isFinite(sat)) return '–';
64
+ return (sat / 1e8).toFixed(8);
65
+ }
66
+
67
+ // Core reports feerates in BTC/kvB; operators think in sat/vB. Convert, and show
68
+ // the unit every time so nobody compares two numbers in different currencies.
69
+ export function satPerVb(btcPerKvB, dp = 2) {
70
+ if (btcPerKvB == null || !Number.isFinite(btcPerKvB)) return '–';
71
+ return (btcPerKvB * 1e8 / 1000).toFixed(dp);
72
+ }
73
+
74
+ // `n` is already in EH/s, so each step up is a THOUSAND of them: 1e3 EH/s is a zettahash and 1e6
75
+ // is a yottahash. Both labels were one prefix too low (2026-09-12) -- 1112 EH/s printed as
76
+ // "1.11 EH/s" -- which stayed hidden while the estimator that feeds this was itself out by 2^32
77
+ // and never produced a number above 1.
78
+ export function eh(n) {
79
+ if (n == null || !Number.isFinite(n)) return '–';
80
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)} YH/s`;
81
+ if (n >= 1e3) return `${(n / 1e3).toFixed(2)} ZH/s`;
82
+ return `${n.toFixed(1)} EH/s`;
83
+ }
84
+
85
+ export function clock(ms) {
86
+ const d = new Date(ms);
87
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
88
+ }
89
+
90
+
91
+ export function short(n) {
92
+ if (n == null || !Number.isFinite(n)) return '–';
93
+ const a = Math.abs(n);
94
+ if (a >= 1e12) return (n / 1e12).toFixed(1) + 'T';
95
+ if (a >= 1e9) return (n / 1e9).toFixed(1) + 'G';
96
+ if (a >= 1e6) return (n / 1e6).toFixed(1) + 'M';
97
+ if (a >= 1e3) return (n / 1e3).toFixed(a >= 1e4 ? 0 : 1) + 'k';
98
+ if (a >= 100) return n.toFixed(0);
99
+ if (a >= 1) return (+n.toFixed(1)).toString();
100
+ return (+n.toFixed(4)).toString();
101
+ }
102
+
103
+ export function hash(x, keep = 8) {
104
+ if (!x || typeof x !== 'string') return '–';
105
+ if (x.length <= keep * 2 + 3) return x;
106
+ return `${x.slice(0, keep)}…${x.slice(-keep)}`;
107
+ }
108
+
109
+ export function esc(s) {
110
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
111
+ };
112
+
113
+ export function uptime(ms) {
114
+ if (!Number.isFinite(ms)) return '–';
115
+ const s = Math.floor(ms / 1000);
116
+ const d = Math.floor(s / 86400);
117
+ const h = Math.floor((s % 86400) / 3600);
118
+ const m = Math.floor((s % 3600) / 60);
119
+ return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
120
+ }
121
+
122
+ // THE ONE WARNING THE BANNER DOES NOT SHOW (operator, 2026-09-13, getting ready for release:
123
+ // "how do we get rid of the node warning?" -- "This is a pre-release test build - use at your
124
+ // own risk - do not use for mining or merchant applications").
125
+ //
126
+ // It is TRUE: a node built from source reports itself pre-release on every poll, which means a red
127
+ // caveat sits across the hero permanently and stops meaning anything. The fix is deliberately
128
+ // the narrowest one available -- this exact notice, matched on the phrase bitcoind has always
129
+ // used for it, and nothing else. "unknown new rules activated", a chain reorganisation, an
130
+ // unsupported chainstate: all still land in the banner, in red.
131
+ //
132
+ // WHERE THIS DOES NOT HAPPEN: the server. monitor.js and sync.js keep passing the node's exact
133
+ // words through, so /api/state still reports what the node said and the filter cannot become a
134
+ // way for the monitor to conceal it. This is a presentation choice, applied at the banner.
135
+ const PRE_RELEASE = /pre-release test build/i;
136
+
137
+ /** The node's warnings, minus the permanent pre-release notice. Never throws on odd input. */
138
+ export function nodeWarnings(list) {
139
+ if (!Array.isArray(list)) return [];
140
+ return list.filter((w) => typeof w === 'string' && w.trim() && !PRE_RELEASE.test(w));
141
+ }
142
+
143
+ /**
144
+ * Copy text to the clipboard. The monitor is served over plain http on the LAN, where
145
+ * navigator.clipboard does not exist (it needs a secure context), so a select-and-copy fallback is
146
+ * kept. Resolves when copied, rejects when the browser refused. (public/js/explorer.js keeps its
147
+ * own copy of this: that file imports nothing by design.)
148
+ */
149
+ export function copyText(text) {
150
+ if (globalThis.navigator?.clipboard && globalThis.isSecureContext) return navigator.clipboard.writeText(text);
151
+ return new Promise((resolve, reject) => {
152
+ const ta = document.createElement('textarea');
153
+ ta.value = text;
154
+ ta.setAttribute('readonly', '');
155
+ ta.className = 'xclip';
156
+ document.body.appendChild(ta);
157
+ ta.select();
158
+ const ok = document.execCommand?.('copy');
159
+ ta.remove();
160
+ if (ok) resolve(); else reject(new Error('copy refused'));
161
+ });
162
+ }