wasm-smoke.cjs raw
1 // Smoke test for the browser build: load the wasm light client, sync a
2 // regtest chain through a real relay and bridge, then match a script locally.
3 //
4 // node --experimental-websocket wasm-smoke.cjs <nostrino.wasm> <gateway.uri> <script-hex>
5 //
6 // Node 21+ exposes WebSocket globally; Node 20 needs --experimental-websocket.
7 'use strict';
8
9 const fs = require('fs');
10 const path = require('path');
11
12 const [wasmPath, uriFile, scriptHex] = process.argv.slice(2);
13 if (!wasmPath || !uriFile || !scriptHex) {
14 console.error('usage: wasm-smoke.cjs <nostrino.wasm> <gateway.uri> <script-hex>');
15 process.exit(2);
16 }
17 if (typeof globalThis.WebSocket === 'undefined') {
18 console.error('this Node has no global WebSocket; run with --experimental-websocket');
19 process.exit(2);
20 }
21
22 const execPath = process.env.TINYGO_WASM_EXEC || '/usr/local/lib/tinygo/targets/wasm_exec.js';
23 if (!fs.existsSync(execPath)) {
24 console.error('wasm_exec.js not found at ' + execPath + '; set TINYGO_WASM_EXEC');
25 process.exit(2);
26 }
27
28 globalThis.require = require;
29 globalThis.fs = fs;
30 globalThis.performance = {
31 now() {
32 const [sec, nsec] = process.hrtime();
33 return sec * 1000 + nsec / 1e6;
34 },
35 };
36 require(execPath);
37
38 const uri = fs.readFileSync(uriFile, 'utf8').trim();
39 const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
40
41 (async () => {
42 const go = new Go();
43 const result = await WebAssembly.instantiate(fs.readFileSync(wasmPath), go.importObject);
44 go.run(result.instance);
45
46 for (let i = 0; i < 500 && !globalThis.nostrino; i++) await sleep(10);
47 if (!globalThis.nostrino) {
48 console.error('FAIL: the wasm module did not install the nostrino global');
49 process.exit(1);
50 }
51 // First prove cancellation: start a sync, cancel it, and require the module
52 // to settle as cancelled rather than hanging on a read.
53 globalThis.nostrino.sync(uri, 'regtest');
54 // Cancel in the same turn: the sync goroutine either has not started, is
55 // connecting, or is mid-sync. All three must settle as cancelled.
56 globalThis.nostrino.cancel();
57 let cancelled = false;
58 let lastCancel = '';
59 for (let i = 0; i < 100; i++) {
60 lastCancel = globalThis.nostrino.status();
61 const st = JSON.parse(lastCancel);
62 if (!st.running) {
63 cancelled = st.status === 'cancelled';
64 break;
65 }
66 await sleep(50);
67 }
68 if (!cancelled) {
69 console.error('FAIL: cancel did not settle as cancelled; last status: ' + lastCancel);
70 process.exit(1);
71 }
72 console.log('cancel: ok');
73
74 // Then a full sync.
75 globalThis.nostrino.sync(uri, 'regtest');
76 let last = '';
77 for (let i = 0; i < 900; i++) {
78 const raw = globalThis.nostrino.status();
79 if (raw !== last) {
80 console.log(raw);
81 last = raw;
82 }
83 const st = JSON.parse(raw);
84 if (!st.running) {
85 if (st.status !== 'done') {
86 console.error('FAIL: sync status ' + st.status + ': ' + st.error);
87 process.exit(1);
88 }
89 if (st.transport !== 'websocket') {
90 console.error('FAIL: expected the websocket data plane, got ' + st.transport);
91 process.exit(1);
92 }
93 if (st.tip === 0 || st.filters === 0) {
94 console.error('FAIL: sync produced no tip or filters');
95 process.exit(1);
96 }
97 const matches = JSON.parse(globalThis.nostrino.match(scriptHex));
98 console.log('matches: ' + JSON.stringify(matches));
99 if (!Array.isArray(matches) || matches.length !== 1 || matches[0] !== 7) {
100 console.error('FAIL: expected the watched script at height 7');
101 process.exit(1);
102 }
103 console.log('PASS: wasm synced tip ' + st.tip + ' with ' + st.filters + ' filters in ' + st.elapsed);
104 process.exit(0);
105 }
106 await sleep(100);
107 }
108 console.error('FAIL: timeout waiting for sync');
109 process.exit(1);
110 })().catch((err) => {
111 console.error('FAIL:', err);
112 process.exit(1);
113 });
114