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.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// THE DEPTH CHART (operator, 2026-09-11, with a picture of bitcoinity's: "we are totally missing
|
|
2
|
+
// this view in the market info").
|
|
3
|
+
//
|
|
4
|
+
// x is price. The left axis is cumulative BTC: bids (green) summed downward from the best bid,
|
|
5
|
+
// asks (red) summed upward from the best ask -- every exchange's book a faint line, all of them
|
|
6
|
+
// together the bright line, and that total as it stood N minutes ago the dashed line. The right
|
|
7
|
+
// axis is a symmetric log scale, as on bitcoinity, for the change bars: how much was added (+) or
|
|
8
|
+
// pulled (-) AT each price since then. The books come from server/collect/markets.js, read every
|
|
9
|
+
// 30 s while the tab is open, as cumulative depth on fixed $50 levels. A shallow book (Kraken
|
|
10
|
+
// gives 500 levels, OKX 5000 -- a percent or two either side) is not extended: its line ends,
|
|
11
|
+
// the end is marked, and the total past it counts only the books that reach.
|
|
12
|
+
import { niceTicks } from './charts.js';
|
|
13
|
+
import { INK } from './theme.js'; // the axis, grid and tooltip colours of the chosen theme
|
|
14
|
+
import { prep } from './pricechart.js';
|
|
15
|
+
|
|
16
|
+
export const AGOS = [[60, '1m'], [300, '5m'], [600, '10m'], [1800, '30m'], [3600, '1h']];
|
|
17
|
+
export const ZOOMS = [[0.01, '±1%'], [0.025, '±2.5%'], [0.05, '±5%'], [0.1, '±10%']];
|
|
18
|
+
export const DEPTH_MS = 30_000;
|
|
19
|
+
const D = { data: null, at: 0, busy: false, key: null, ago: 600, zoom: 0.05, hover: null, bound: false, error: null };
|
|
20
|
+
// the two sides' colours come from the theme's ink (theme.js, 2026-09-16): neon green and red on a
|
|
21
|
+
// dark ground, the theme's own good and bad on a light one, where neon has no contrast
|
|
22
|
+
const PAD_FULL = { top: 28, right: 62, bottom: 22, left: 56 };
|
|
23
|
+
// COMPACT, for the kiosk. The full chart spends 62 px on the right purely to label the change
|
|
24
|
+
// bars' axis and 56 on the left for the cumulative-BTC labels plus a rotated caption -- on a wall
|
|
25
|
+
// panel a couple of hundred pixels tall that is most of the picture. Compact keeps the curves and
|
|
26
|
+
// the price axis, which are what carry the meaning across a room, and drops the annotation.
|
|
27
|
+
const PAD_COMPACT = { top: 15, right: 10, bottom: 17, left: 42 };
|
|
28
|
+
|
|
29
|
+
const money = (v, dp = 0) => (v == null ? '–' : v.toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp }));
|
|
30
|
+
const btc = (v) => (v == null ? '–' : Math.abs(v) >= 100 ? Math.round(v).toLocaleString('en-US') : v.toFixed(Math.abs(v) >= 10 ? 1 : 2));
|
|
31
|
+
const kUsd = (v) => `${(v / 1000).toFixed(v % 1000 ? 1 : 0)}k`;
|
|
32
|
+
const shortN = (v) => (v >= 1000 ? `${v / 1000}k` : String(v));
|
|
33
|
+
const clock = (t) => new Date(t).toISOString().slice(11, 19);
|
|
34
|
+
|
|
35
|
+
// Pure: the /api/markets/depth reply in, the curves the chart draws out.
|
|
36
|
+
export function depthSeries(d) {
|
|
37
|
+
if (!d?.ok || !d.n || !(d.exchanges ?? []).some((e) => e.bids)) return null;
|
|
38
|
+
const n = d.n;
|
|
39
|
+
const prices = Array.from({ length: n }, (_, i) => d.p0 + i * d.step);
|
|
40
|
+
// A BOOK THAT ENDS STILL COUNTS (2026-09-11: with Kraken's 500 levels ending at -2%, the total
|
|
41
|
+
// DROPPED there, and "within 5%" printed smaller than "within 1%"). Cumulative depth past the
|
|
42
|
+
// last level a book gave is at least what it had reached, so that value is carried on outward
|
|
43
|
+
// as a lower bound -- not an extrapolation -- and those levels are flagged partial: the chart
|
|
44
|
+
// draws the total there as "at least", and the readout says so.
|
|
45
|
+
const carry = (arr, side) => {
|
|
46
|
+
if (!arr) return null;
|
|
47
|
+
const o = arr.slice();
|
|
48
|
+
if (side === 'bid') { const lo = o.findIndex((v) => v != null); if (lo > 0) for (let i = lo - 1; i >= 0; i--) o[i] = o[lo]; }
|
|
49
|
+
else { let hi = -1; for (let i = n - 1; i >= 0; i--) if (o[i] != null) { hi = i; break; } if (hi >= 0) for (let i = hi + 1; i < n; i++) o[i] = o[hi]; }
|
|
50
|
+
return o;
|
|
51
|
+
};
|
|
52
|
+
const sum = (key, side) => {
|
|
53
|
+
const tot = new Array(n).fill(null), part = new Array(n).fill(false);
|
|
54
|
+
for (const e of d.exchanges) {
|
|
55
|
+
const raw = e[key];
|
|
56
|
+
const full = carry(raw, side);
|
|
57
|
+
if (!full) continue;
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
if (full[i] == null) continue;
|
|
60
|
+
tot[i] = (tot[i] ?? 0) + full[i];
|
|
61
|
+
if (raw[i] == null) part[i] = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { tot, part };
|
|
65
|
+
};
|
|
66
|
+
const B = sum('bids', 'bid'), A = sum('asks', 'ask'), TB = sum('thenBids', 'bid'), TA = sum('thenAsks', 'ask');
|
|
67
|
+
const bid = B.tot, ask = A.tot, thenBid = TB.tot, thenAsk = TA.tot;
|
|
68
|
+
const bidPart = B.part, askPart = A.part;
|
|
69
|
+
const hasThen = d.exchanges.some((e) => e.thenBids);
|
|
70
|
+
// the amount AT a level (not cumulative): bids accumulate downward, asks upward
|
|
71
|
+
const amt = (cum, i, side) => (cum[i] == null ? null : cum[i] - ((side === 'bid' ? cum[i + 1] : cum[i - 1]) ?? 0));
|
|
72
|
+
const change = hasThen ? prices.map((_, i) => {
|
|
73
|
+
const nb = amt(bid, i, 'bid'), tb = amt(thenBid, i, 'bid'), na = amt(ask, i, 'ask'), ta = amt(thenAsk, i, 'ask');
|
|
74
|
+
const v = (nb != null && tb != null && !bidPart[i] ? nb - tb : 0) + (na != null && ta != null && !askPart[i] ? na - ta : 0);
|
|
75
|
+
return Math.abs(v) < 1e-6 ? 0 : v;
|
|
76
|
+
}) : null;
|
|
77
|
+
const reach = d.exchanges.filter((e) => e.bids).map((e) => {
|
|
78
|
+
const lo = e.bids.findIndex((v) => v != null);
|
|
79
|
+
let hi = -1;
|
|
80
|
+
for (let i = n - 1; i >= 0; i--) if (e.asks?.[i] != null) { hi = i; break; }
|
|
81
|
+
return { id: e.id, name: e.name, low: lo > 0 ? prices[lo] : null, high: hi >= 0 && hi < n - 1 ? prices[hi] : null };
|
|
82
|
+
});
|
|
83
|
+
return { prices, step: d.step, bid, ask, bidPart, askPart, thenBid, thenAsk, change, mid: d.mid, exchanges: d.exchanges.filter((e) => e.bids), reach };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cumulative BTC bid within pct below the mid, and asked within pct above it.
|
|
87
|
+
export function depthSums(ser, pct) {
|
|
88
|
+
const idx = (p) => Math.round((p - ser.prices[0]) / ser.step);
|
|
89
|
+
const ib = idx(ser.mid * (1 - pct)), ia = idx(ser.mid * (1 + pct));
|
|
90
|
+
return { bids: ser.bid[ib] ?? null, asks: ser.ask[ia] ?? null, atLeast: Boolean(ser.bidPart?.[ib] || ser.askPart?.[ia]) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function drawDepth(canvas, ser, { zoom = 0.05, hover = null, compact = false, empty = null } = {}) {
|
|
94
|
+
if (!canvas?.getContext) return null;
|
|
95
|
+
const PAD = compact ? PAD_COMPACT : PAD_FULL;
|
|
96
|
+
const { ctx, w, h } = prep(canvas);
|
|
97
|
+
if (!ser) {
|
|
98
|
+
ctx.fillStyle = INK.text; ctx.textAlign = 'center';
|
|
99
|
+
ctx.fillText(empty ?? 'asking the exchanges for their order books…', w / 2, h / 2);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const plotW = Math.max(10, w - PAD.left - PAD.right), plotH = Math.max(40, h - PAD.top - PAD.bottom);
|
|
103
|
+
const pmin = ser.mid * (1 - zoom), pmax = ser.mid * (1 + zoom);
|
|
104
|
+
const X = (p) => PAD.left + ((p - pmin) / (pmax - pmin)) * plotW;
|
|
105
|
+
const inWin = (i) => ser.prices[i] >= pmin && ser.prices[i] <= pmax;
|
|
106
|
+
const line = (x0, y0, x1, y1) => { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); };
|
|
107
|
+
let vmax = 1;
|
|
108
|
+
ser.prices.forEach((_, i) => { if (inWin(i)) for (const a of [ser.bid, ser.ask, ser.thenBid, ser.thenAsk]) if (a[i] != null && a[i] > vmax) vmax = a[i]; });
|
|
109
|
+
vmax *= 1.06;
|
|
110
|
+
const Y = (v) => PAD.top + plotH - (v / vmax) * plotH;
|
|
111
|
+
let cmax = 1;
|
|
112
|
+
if (ser.change) ser.change.forEach((v, i) => { if (inWin(i) && Math.abs(v) > cmax) cmax = Math.abs(v); });
|
|
113
|
+
const F = Math.max(1, Math.ceil(Math.log10(1 + cmax)));
|
|
114
|
+
const sl = (v) => Math.sign(v) * Math.log10(1 + Math.abs(v));
|
|
115
|
+
const YC = (v) => PAD.top + plotH / 2 - (sl(v) / F) * (plotH / 2);
|
|
116
|
+
|
|
117
|
+
// left axis: cumulative BTC
|
|
118
|
+
ctx.lineWidth = 1;
|
|
119
|
+
ctx.textAlign = 'right';
|
|
120
|
+
for (const v of niceTicks(0, vmax, Math.max(3, Math.floor(plotH / 44)))) {
|
|
121
|
+
const y = Math.round(Y(v)) + 0.5;
|
|
122
|
+
if (y < PAD.top) continue;
|
|
123
|
+
ctx.strokeStyle = INK.grid; line(PAD.left, y, w - PAD.right, y);
|
|
124
|
+
ctx.fillStyle = INK.axisText; ctx.fillText(btc(v), PAD.left - 6, y);
|
|
125
|
+
}
|
|
126
|
+
if (!compact) {
|
|
127
|
+
ctx.save?.();
|
|
128
|
+
ctx.translate(12, PAD.top + plotH / 2); ctx.rotate(-Math.PI / 2);
|
|
129
|
+
ctx.textAlign = 'center'; ctx.fillStyle = INK.textDim; ctx.fillText('BTC, cumulative', 0, 0);
|
|
130
|
+
ctx.restore?.();
|
|
131
|
+
}
|
|
132
|
+
ctx.setTransform(Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0, Math.min(globalThis.devicePixelRatio || 1, 2), 0, 0);
|
|
133
|
+
// right axis: the change bars' symmetric log scale
|
|
134
|
+
if (ser.change) {
|
|
135
|
+
ctx.textAlign = 'left';
|
|
136
|
+
for (let e = -F; e <= F; e++) {
|
|
137
|
+
const v = e === 0 ? 0 : Math.sign(e) * 10 ** Math.abs(e);
|
|
138
|
+
const y = Math.round(YC(v)) + 0.5;
|
|
139
|
+
if (!compact) {
|
|
140
|
+
ctx.fillStyle = INK.axisText;
|
|
141
|
+
ctx.fillText(v === 0 ? '0' : `${v > 0 ? '' : '-'}${shortN(Math.abs(v))}`, w - PAD.right + 6, y);
|
|
142
|
+
}
|
|
143
|
+
if (v === 0) { ctx.strokeStyle = INK.axis; line(PAD.left, y, w - PAD.right, y); }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// price axis
|
|
147
|
+
ctx.textAlign = 'center';
|
|
148
|
+
for (const p of niceTicks(pmin, pmax, Math.max(3, Math.floor(plotW / 90)))) {
|
|
149
|
+
const x = Math.round(X(p)) + 0.5;
|
|
150
|
+
if (x < PAD.left || x > w - PAD.right) continue;
|
|
151
|
+
ctx.strokeStyle = INK.grid; line(x, PAD.top, x, PAD.top + plotH);
|
|
152
|
+
ctx.fillStyle = INK.axisText; ctx.fillText(zoom <= 0.01 ? money(p) : kUsd(p), x, h - PAD.bottom / 2);
|
|
153
|
+
}
|
|
154
|
+
ctx.strokeStyle = INK.axis;
|
|
155
|
+
line(PAD.left + 0.5, PAD.top, PAD.left + 0.5, PAD.top + plotH);
|
|
156
|
+
line(w - PAD.right + 0.5, PAD.top, w - PAD.right + 0.5, PAD.top + plotH);
|
|
157
|
+
line(PAD.left, PAD.top + plotH + 0.5, w - PAD.right, PAD.top + plotH + 0.5);
|
|
158
|
+
|
|
159
|
+
// change bars
|
|
160
|
+
if (ser.change) {
|
|
161
|
+
const bw = Math.max(1, (plotW / ((pmax - pmin) / ser.step)) * 0.8);
|
|
162
|
+
ser.change.forEach((v, i) => {
|
|
163
|
+
if (!v || !inWin(i)) return;
|
|
164
|
+
const x = X(ser.prices[i]), y0 = YC(0), y1 = YC(v);
|
|
165
|
+
ctx.fillStyle = v > 0 ? 'rgba(110,180,255,0.6)' : 'rgba(255,170,70,0.6)';
|
|
166
|
+
ctx.fillRect(x - bw / 2, Math.min(y0, y1), bw, Math.max(1, Math.abs(y1 - y0)));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// the curves: each book faint, the total then (dashed), the total now (bright)
|
|
170
|
+
const curve = (arr, col, lw, dash = []) => {
|
|
171
|
+
if (!arr) return;
|
|
172
|
+
ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.setLineDash(dash);
|
|
173
|
+
ctx.beginPath();
|
|
174
|
+
let on = false;
|
|
175
|
+
ser.prices.forEach((p, i) => {
|
|
176
|
+
const v = arr[i];
|
|
177
|
+
if (v == null || !inWin(i)) { on = false; return; }
|
|
178
|
+
if (on) ctx.lineTo(X(p), Y(v)); else { ctx.moveTo(X(p), Y(v)); on = true; }
|
|
179
|
+
});
|
|
180
|
+
ctx.stroke();
|
|
181
|
+
ctx.setLineDash([]); ctx.lineWidth = 1;
|
|
182
|
+
};
|
|
183
|
+
for (const e of ser.exchanges) { curve(e.bids, INK.bidT, 1); curve(e.asks, INK.askT, 1); }
|
|
184
|
+
if (ser.change) { curve(ser.thenBid, INK.bidThen, 1.4, [5, 4]); curve(ser.thenAsk, INK.askThen, 1.4, [5, 4]); }
|
|
185
|
+
const only = (arr, part, want) => arr.map((v, i) => (v != null && (part[i] === want || (i > 0 && part[i - 1] === want) || (i < arr.length - 1 && part[i + 1] === want)) ? v : null));
|
|
186
|
+
curve(only(ser.bid, ser.bidPart, true), INK.bidHalf, 2.2, [2, 3]);
|
|
187
|
+
curve(only(ser.ask, ser.askPart, true), INK.askHalf, 2.2, [2, 3]);
|
|
188
|
+
curve(only(ser.bid, ser.bidPart, false), INK.bid, 2.2);
|
|
189
|
+
curve(only(ser.ask, ser.askPart, false), INK.ask, 2.2);
|
|
190
|
+
// the mid, and where each shallow book ends
|
|
191
|
+
ctx.strokeStyle = INK.dash;
|
|
192
|
+
const xm = Math.round(X(ser.mid)) + 0.5;
|
|
193
|
+
line(xm, PAD.top, xm, PAD.top + plotH);
|
|
194
|
+
ctx.font = '10px ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
195
|
+
let lane = 0;
|
|
196
|
+
for (const r of ser.reach) {
|
|
197
|
+
for (const p of [r.low, r.high]) {
|
|
198
|
+
if (p == null || p < pmin || p > pmax) continue;
|
|
199
|
+
const x = Math.round(X(p)) + 0.5;
|
|
200
|
+
ctx.strokeStyle = INK.dash; line(x, PAD.top + plotH - 8, x, PAD.top + plotH);
|
|
201
|
+
if (!compact) {
|
|
202
|
+
ctx.fillStyle = INK.label; ctx.textAlign = 'center';
|
|
203
|
+
ctx.fillText(`${r.name} ends`, x, PAD.top + plotH - 14 - 11 * (lane++ % 3));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
ctx.font = '11px ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
208
|
+
|
|
209
|
+
// readout: the pointer's price, or the sums near the mid
|
|
210
|
+
ctx.textAlign = 'left'; ctx.fillStyle = INK.bright;
|
|
211
|
+
if (hover && hover.x >= PAD.left && hover.x <= w - PAD.right) {
|
|
212
|
+
const p = pmin + ((hover.x - PAD.left) / plotW) * (pmax - pmin);
|
|
213
|
+
const i = Math.max(0, Math.min(ser.prices.length - 1, Math.round((p - ser.prices[0]) / ser.step)));
|
|
214
|
+
const bidSide = ser.prices[i] <= ser.mid;
|
|
215
|
+
const cum = bidSide ? ser.bid[i] : ser.ask[i];
|
|
216
|
+
const per = ser.exchanges.map((e) => [e.name, bidSide ? e.bids[i] : e.asks?.[i]]).filter(([, v]) => v != null).map(([nm, v]) => `${nm} ${btc(v)}`).join(' · ');
|
|
217
|
+
ctx.strokeStyle = INK.dash; ctx.setLineDash([3, 3]);
|
|
218
|
+
const x = Math.round(X(ser.prices[i])) + 0.5;
|
|
219
|
+
line(x, PAD.top, x, PAD.top + plotH);
|
|
220
|
+
ctx.setLineDash([]);
|
|
221
|
+
const part = bidSide ? ser.bidPart[i] : ser.askPart[i];
|
|
222
|
+
ctx.fillText(`$${money(ser.prices[i])} · ${bidSide ? 'bid at or above' : 'asked at or below'}: ${part ? 'at least ' : ''}${btc(cum)} BTC${ser.change ? ` · change here ${ser.change[i] > 0 ? '+' : ''}${btc(ser.change[i])} BTC` : ''}${per ? ` (${per})` : ''}`, PAD.left + 2, 12);
|
|
223
|
+
} else {
|
|
224
|
+
const s1 = depthSums(ser, 0.01), s5 = depthSums(ser, 0.05);
|
|
225
|
+
const al = (s) => (s.atLeast ? '≥ ' : '');
|
|
226
|
+
ctx.fillText(compact
|
|
227
|
+
? `mid $${money(ser.mid)} · 1%: ${al(s1)}${btc(s1.bids)} / ${btc(s1.asks)} BTC`
|
|
228
|
+
: `mid $${money(ser.mid)} · within 1%: ${al(s1)}bids ${btc(s1.bids)} / asks ${btc(s1.asks)} BTC · within 5%: ${al(s5)}bids ${btc(s5.bids)} / asks ${btc(s5.asks)} BTC (dotted: a book has ended, so at least)`,
|
|
229
|
+
PAD.left + 2, compact ? 8 : 12);
|
|
230
|
+
}
|
|
231
|
+
return { pmin, pmax, vmax, F };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function depthBarHtml(ago = D.ago, zoom = D.zoom) {
|
|
235
|
+
const b = (attr, v, label, on) => `<button type="button" class="mkbtn${on ? ' on' : ''}" ${attr}="${v}">${label}</button>`;
|
|
236
|
+
return `<div class="mkgrp"><span class="mklab">change bars for the last</span>${AGOS.map(([s, l]) => b('data-ago', s, l, s === ago)).join('')}</div>
|
|
237
|
+
<div class="mkgrp"><span class="mklab">price window</span>${ZOOMS.map(([z, l]) => b('data-zoom', z, l, z === zoom)).join('')}</div>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// what the empty chart says while the feed is off: the switch, or the server's hard off
|
|
241
|
+
const offText = (d) => (d?.enabled === false ? (d.polling === false ? 'market polling is off — Display settings → Markets & Price → Enable market polling' : 'market data is off on this server') : null);
|
|
242
|
+
|
|
243
|
+
export function depthNote(d, fmt) {
|
|
244
|
+
if (!d) return 'asking the exchanges for their order books…';
|
|
245
|
+
if (d.enabled === false) return d.note ?? 'market data is off on this monitor';
|
|
246
|
+
if (d.warming) return 'reading the order books for the first time…';
|
|
247
|
+
const agoL = AGOS.find(([s]) => s === d.ago)?.[1] ?? `${d.ago} s`;
|
|
248
|
+
const parts = [`books read at ${clock(d.at)} UTC, every ${Math.round((d.bookMs ?? DEPTH_MS) / 1000)} s while this tab is open`];
|
|
249
|
+
parts.push(d.thenAt ? `the dashed line and the change bars compare with ${clock(d.thenAt)} UTC (${agoL} before)` : `no snapshot ${agoL} old yet -- the books have been read since ${clock(d.firstAt)} UTC, so the change bars wait`);
|
|
250
|
+
const bad = (d.exchanges ?? []).filter((e) => e.error);
|
|
251
|
+
if (bad.length) parts.push(`not read: ${bad.map((e) => `${fmt.esc(e.name)} (${fmt.esc(e.error)})`).join(', ')}`);
|
|
252
|
+
parts.push('OKX quotes USDT; it is in the total');
|
|
253
|
+
return parts.join(' · ');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function draw() {
|
|
257
|
+
const canvas = document.getElementById('mkDepth');
|
|
258
|
+
if (!canvas) return;
|
|
259
|
+
drawDepth(canvas, depthSeries(D.data), { zoom: D.zoom, hover: D.hover, empty: offText(D.data) });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* ONE FETCH, HOWEVER MANY VIEWS. The Markets tab and the Kiosk both want the books; this is the
|
|
264
|
+
* shared poll, so opening both does not double the traffic to five exchanges. The server's route
|
|
265
|
+
* calls markets.touch(), so a Kiosk polling on its own keeps the collector out of its idle park --
|
|
266
|
+
* a wall display needs no special handling.
|
|
267
|
+
*/
|
|
268
|
+
function ensureDepth(h) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const key = String(D.ago);
|
|
271
|
+
if (!D.busy && (D.key !== key || now - D.at >= (D.data && !D.data.warming ? DEPTH_MS : 4000))) {
|
|
272
|
+
D.busy = true; D.at = now; D.key = key;
|
|
273
|
+
h.api(`/api/markets/depth?ago=${D.ago}`)
|
|
274
|
+
.then((d) => { if (D.key === key) { D.data = d; D.error = null; } })
|
|
275
|
+
.catch((err) => { D.error = err.message; })
|
|
276
|
+
.finally(() => { D.busy = false; h.render(); });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The chart into a canvas of the caller's choosing, compact and with no toolbar -- for the Kiosk.
|
|
282
|
+
* It deliberately does NOT touch D.hover or D.zoom: those belong to the Markets tab's chart, and
|
|
283
|
+
* sharing them would make a pointer on one view redraw the other.
|
|
284
|
+
*/
|
|
285
|
+
export function renderDepthInto(canvasId, h, { zoom = 0.025, compact = true } = {}) {
|
|
286
|
+
ensureDepth(h);
|
|
287
|
+
const canvas = document.getElementById(canvasId);
|
|
288
|
+
if (!canvas) return null;
|
|
289
|
+
return drawDepth(canvas, depthSeries(D.data), { zoom, compact, empty: offText(D.data) });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function renderDepth(h) {
|
|
293
|
+
ensureDepth(h);
|
|
294
|
+
const bar = document.getElementById('mkDepthBar'), canvas = document.getElementById('mkDepth'), note = document.getElementById('mkDepthNote');
|
|
295
|
+
if (!bar || !canvas) return;
|
|
296
|
+
const html = depthBarHtml();
|
|
297
|
+
if (bar.__html !== html) { bar.innerHTML = html; bar.__html = html; }
|
|
298
|
+
if (!D.bound) {
|
|
299
|
+
D.bound = true;
|
|
300
|
+
bar.addEventListener('click', (e) => {
|
|
301
|
+
const btn = e.target.closest('button');
|
|
302
|
+
if (!btn) return;
|
|
303
|
+
if (btn.dataset.ago) D.ago = Number(btn.dataset.ago);
|
|
304
|
+
if (btn.dataset.zoom) D.zoom = Number(btn.dataset.zoom);
|
|
305
|
+
h.render();
|
|
306
|
+
});
|
|
307
|
+
canvas.addEventListener('pointermove', (e) => { const r = canvas.getBoundingClientRect(); D.hover = { x: e.clientX - r.left, y: e.clientY - r.top }; draw(); });
|
|
308
|
+
canvas.addEventListener('pointerleave', () => { D.hover = null; draw(); });
|
|
309
|
+
}
|
|
310
|
+
draw();
|
|
311
|
+
if (note) {
|
|
312
|
+
const t = D.error ? h.fmt.esc(D.error) : depthNote(D.data, h.fmt);
|
|
313
|
+
if (note.__html !== t) { note.innerHTML = t; note.__html = t; }
|
|
314
|
+
}
|
|
315
|
+
}
|