Back to list

Development Update — August 12

A very large day, on two fronts that rarely land together. The manager UI — the Angular hypervisor front-end — was brought to the same engineering footing as skycoin’s three other front-ends in a single coordinated stack: explicit change detection, strict TypeScript with strict templates, correctness-matched ESLint, the esbuild application builder, Angular 22, and a browser smoke check that catches the class of migration bugs a build and a lint and a spec all miss. In parallel the cascade route-setup verification campaign gained the observability and the leg-level reclamation it needs to prove its own correctness, dmsg carrier selection learned to converge and self-heal, and the CLI got several honesty fixes. Underneath it all, the manager UI was also made null-safe end to end. And on the Skycoin side the same wasm-cipher work reached its point: skycoin-lite now stamps both its Go and TinyGo artifacts, returns errors instead of trapping the whole module on a mistyped address, and — the payoff — was lifted out of a main into a Register any wasm program can call, so a skywire visor can carry the skycoin cipher, with a transport seam now under the wallet’s ApiService for the node API calls to follow.

Skywire: The Manager UI Toolchain, Modernized

The stack begins with change detection, because it has to. 3845 ui: explicit change detection before Angular 22 addresses the one thing that must happen before the version bump: Angular 22 makes OnPush the framework default, and this project was on 21 with only 2 of its 90 components declaring a strategy — so the upgrade would silently flip the other 88 to OnPush, under which a field assigned from a subscription or a timer repaints nothing on its own. No error, no warning, just a transport list that stops updating or a status stuck green. Doing it now, on 21, makes that a change with a diff and a test run behind it: every component declares OnPush and every asynchronous callback that can touch view state marks it (generously — marking a view that didn’t change costs one check; missing one that did is the bug), with make check-onpush sweeping for any that a later subscription forgets. The codemods caught three real bugs before they shipped.

3846 ui: compile the manager UI under strict TypeScript and strictTemplates is the substantial one — the project had no strictness at all, no strict, no strictNullChecks, no angularCompilerOptions and therefore no strictTemplates, while skycoin’s three front-ends all compile strict. Turning it on surfaced 704 errors, most mechanical (373 definite-assignment assertions for properties set in ngOnInit, 31 override markers, 45 typed-object string-index casts), but strictTemplates is the point and it found real defects: three charts passing the string "140" to a number height input, a template calling a private method, a boolean pushed into a string[] router link, a string compared against a number by coercion, a (keydown.enter) handler reading ctrlKey off a value Angular types as Event not KeyboardEvent, and a route list rendering fields the Route interface never declared. strictNullChecks — another 281 errors — was deliberately left for its own pass, because each is a decision about whether a null is real and blanket ! assertions would hide the next real bug.

3848 ui: match skycoin’s ESLint correctness rules brings the ruleset to skycoin’s shared base — prefer-const, curly, eqeqeq, valid-typeof, and the component/directive selector prefixes — kept in parity by matching the rules rather than sharing the file (separate npm projects in separate repos; skywire depends on skycoin as a Go module, not an npm one), which required only two brace fixes and renaming the clipboard directive to carry the app prefix. 3849 ui: build the manager UI with @angular/build moves build, serve and karma off the older webpack @angular-devkit/build-angular:browser target onto the esbuild application builder skycoin’s front-ends use, dropping vendorChunk and buildOptimizer (esbuild rejects both). This is the migration that broke skycoin three ways — a blank page from JSON-import resolution, translations that never loaded, and a downlevel the browser rejected, none caught by a build, lint or spec — and two of the three can’t happen here because this project’s translate loader already fetch()es i18n JSON as a runtime asset; the third is exactly what the new browser smoke check is for, and pkg/wasmhv, which inlines this bundle into the single-file hypervisor, passes its Go tests against the esbuild output.

3852 ui: upgrade to Angular 22 and ngx-translate 18 is the last of the parity stack, and because every component already declares a strategy, the OnPush-default bump changes nothing about how any view updates. ngx-translate 18 made the pipe and directive standalone (provideTranslateService, setDefaultLangsetFallbackLang); the loader stays an explicit class provider rather than provideTranslateLoader() because that helper matches Function.prototype.toString() against /^class\s/ and esbuild minifies class TranslationModuleLoader { to class{, leaving no space to match — the same trap skycoin hit. It also fixes karma, which #3849 broke by removing a package karma.conf.js still required (the 73 specs passed there only because node_modules still held it until the clean install afterward — CI caught it). 3854 ui: drop baseUrl, and make the root tsconfig checkable is the correction to a footnote: #3852 had silenced a TypeScript 6 baseUrl deprecation with ignoreDeprecations believing removal was a 206-error job, but those 206 errors turned out to be 204 Cannot find name 'describe' in spec files plus relatives — the root tsconfig declared no types, so tsc picked up every spec without jasmine’s, and TS5101 was aborting the check and hiding them. The real fix is a paths mapping ("src/*": ["./src/*"]) that survives TypeScript 7, plus the jasmine/node types, taking the root config from 204 errors to zero and making tsc -p tsconfig.json worth running.

Skywire: Null-Safety, End to End

With the strict-template pass done, strictNullChecks got its own dedicated series, each PR making a declaration say what the code already does rather than papering over it. 3855 ui: make the services layer honest about null starts it — Node.ip was declared string but assigned null whenever a visor reports no address, getDataForHv returned whatever localStorage held (i.e. null) under a string signature, getLabelInfo had a comment saying it returns null under a non-null signature, and several subjects typed as always-full started empty. 3857 and 3858 clean the VPN services and node components the same way, and 3859 ui: turn on strictNullChecks is the last of the series: the 58 remaining sites are either widened where the code already puts nothing there (route params and localStorage reads, chart min/max, filterList’s null-when-nothing-filters return whose consumers already declared | null) or guarded where the fix changes behaviour and carries a reason. The manager UI now compiles with every strict flag on.

Skywire: Cascade Route-Setup Verification

The route-setup campaign advanced with the instrumentation that makes its later fixes provable. 3844 feat(router): routing-table observability is Phase 0 — routing.Table.Stats() returns a rule count, the monotonic route-ID high-water (never reused, so it surfaces reserved-ID leaks), and a per-type breakdown, exposed through a RoutingStats RPC and skywire cli route table-stats [--json] so a soak harness can sample each relay’s rule-count and high-water remotely — that’s how the “no leak” acceptance gate is measured across intermediates. 3847 feat(router): reclaim retired mux legs immediately via CloseLegRetired is Phase 1a and fixes a leak that affects legacy route setup today: when a mux leg is rotated out the source just closes its local transport and sends no routing close down the leg, so every transited relay keeps that leg’s intermediary rules until the ~10-minute idle GC and the reserved route IDs are never reclaimed — under a rotating policy this piles up dozens of stale rule-sets per relay before the first GC even fires. The naive fix is wrong because all mux legs share one RouteDescriptor, so a close at the destination endpoint would tear down every leg; the new CloseLegRetired close code is handled identically to CloseRequested at intermediaries (delete and forward, immediate per-hop reclamation) but at the destination prunes only the leg it arrived on and keeps the group live. It’s receive-side only and inert until a source emits it, so the fleet saturates the safe half first.

3850 fix(transport): recover all transport types from a TPID stops ping --show-route and the HV UI route display from rendering a blank type for any hop riding a newer transport (squicr, webrtc, swsr, swtr) — a TPID is a one-way hash so the type isn’t stored recoverably, and the reverse-map now iterates types.Known() (the canonical source of truth) plus the legacy pre-rename wire names instead of a hard-coded {STCPR, SUDPH, STCP, DMSG} that drifted. 3851 feat(cli): pin an explicit route for ping via –route adds a --route flag that accepts the JSON route find/route calc emit and pins an explicit forward+reverse hop set instead of running route calculation — the plumbing already existed end to end, only the CLI surface was missing — which is the only way to make a controlled A/B valid across a config change that forces a restart, since the same intermediates then carry both arms and any observed difference is real rather than route-selection noise.

Skywire: dmsg Carrier Convergence and Self-Heal

3835 dmsg: ordered carrier preference + convergence, per-carrier server health probe makes Config.Carriers an ordered preference (e.g. wt,quic,ws,tcp), with ConvergeCarriers() re-dialing each dmsg-server session onto the most-preferred carrier the server currently advertises — strictly upgrade-only, replace-in-place so the session count never grows, with per-server backoff — exposed as skywire cli dmsg converge; and it adds a direct per-carrier health probe (svc health --dmsg-server <pk> --carriers wt,quic,ws,tcp) that spins up a clean single-carrier client per protocol and dials the server directly, cleanly separating a genuinely UDP-unreachable server from one reachable by a clean client but failing in a specific process. 3839 dmsg-disc: reconcile server sessions against the live registry closes a self-healing gap: the discovery is a direct client that must hold a session to every dmsg server or a client relaying through a missed one gets 202 - cannot connect to delegated server, but it maintained sessions from two loops — a tight one dialing the static configured set and a slow one dialing the live registry — and when a server re-registered at a new address the static entry went stale and the tight loop dialed the dead address forever (observed live: 8 of 9 servers held because one had drifted :30081:30088). The two loops fold into one that dials the deduped union each tick with the registered address winning, self-healing drift within one cycle. 3836 visor: refuse to start a second visor on the same cli_addr adds a fast preflight that probes the configured cli_addr and aborts with the running visor’s PK and the fix (skywire cli visor halt) instead of racing it and dying deep in init with an opaque bind error, bypassable via SKYWIRE_ALLOW_DUPLICATE=1 for the supported multi-visor-per-host setup.

Skywire: CLI, Build, and Housekeeping

3860 cli: visor info reports WT registration corrects three things visor info was saying wrongly — it now lists the visor’s WebTransport AR registration with its certificate hash (abbreviated in the summary, full in --json; the hash was already on the resolved data, nothing consumed it), distinguishes “checked and not registered” from “predates that type and never checked” instead of inventing an answer, and stops reporting invented latency. 3861 cli/proxy: show route in proxy status + nest mux ops under proxy mux, motivated by debugging intermittent skysocks-client route drops, makes each running client print its live route — destination plus each mux leg’s first-hop transport (type, remote PK, latency) — and flags a single-leg route because a first-hop flap on a lone leg drops the whole session, the common cause of those intermittent drops; it also regroups the flat mux-* verbs under a proxy mux <sub> parent (hidden back-compat aliases kept), with 3864 sweeping the docs to the nested form. On the build side, 3832 drops the unused goimports-reviser from make format (not enforced anywhere, redundant with the goimports step before it, and the sole cause of spurious import churn in the vendored third_party/0magnet/* copies), 3840 is a Makefile audit fixing several broken targets (a stray 8 in build-static, an unbuildable install-generate, a tool-install target that rewrote go.mod) and pruning dead entries, and 3841/3842 fix a test-build break and refresh the dependency graph including skycoin to latest develop. 3834 tpviz: a Go/wasm WebGL view alongside the JavaScript one adds a fourth view drawing the same graph through a Go port of cosmos.gl 2.6.3, built with stock TinyGo (~630 KB), running beside the existing WebGL view — motivated both by the current cosmos 1.6.1’s CC-BY-NC-4.0 license (the only non-permissive dependency in tpviz’s tree) and by its hard requirement on the OES_texture_float extension, whose absence throws and takes the whole page blank rather than just the graph. On mobile, 3833 lands a batch of Telegram-reported fixes (skychat keyboard/scroll and alert-replay, microphone release on call end, lock-screen behaviour) and 3853 adds an in-app language picker with Simplified Chinese.

Skycoin: The wasm Cipher, Hardened

3024 skycoin-lite: version the TinyGo wasm too, and require both to say what built them extends yesterday’s build stamp to the TinyGo artifact, which had gone unversioned because upstream tinygo 0.41.1 records no build information at all; built with the 0magnet/tinygo fork — which writes vcs.revision/vcs.time/vcs.modified and implements runtime/debug.ReadBuildInfo — both artifacts now report their commit, and check-wasm requires a stamp on both rather than skipping an unstamped one, a file nothing can identify being no better than a dirty one. Both toolchains compile to a scratch directory before either is written back, because writing the Go artifact first left the tree modified and made TinyGo stamp its own output vcs.modified=true. Running the browser suite against the TinyGo cipher for the first time surfaced the finding acted on next: all 2212 address and signature vectors pass, but every error-handling case fails with unreachable executedrecover() does not fire under TinyGo, so a bad seed traps the whole module rather than returning an error. 3025 skycoin-lite: return errors instead of panicking is that fix, and it matters on the standard build too: buildTransaction decoded the destination address — typed by the user — with a function that panicked on a bad base58, so a typo in a recipient took out the wallet’s cipher for the rest of the page. Every exported liteclient function returns an error now and nothing in the package panics; signing no longer routes through the log.Panic-ing coin.Transaction.SignInputs, two callers with their own recover/panic bugs are fixed, and the demo main.go — which had been importing an external module last published in 2019 rather than the package beside it — is repointed locally, dropping that dependency and its vendored copy from every build. The package went from zero tests to 16 covering every failure path, and the five error specs that trapped the module under TinyGo now pass because nothing panics for them to trap on (2217/2217 on both toolchains). 3026 skycoin-lite: embed the wasm cipher gzipped then stops //go:embed adding 3.1 MB of uncompressed wasm to every binary that carries it — it is committed and embedded gzipped, as skywire keeps its wasm-visor blobs, and cmd/skycoin-web serves the bytes straight to the browser with Content-Encoding: gzip (1.8 MB on the wire, byte-identical to the committed .gz), inflating only for a client that will not accept it; the PR is careful to correct its own first pass and note the repository gains nothing (git already compresses raw wasm to the same packed size), so the .gitignore exception is annotated as a binary-size lever, not a repo-size one.

Skycoin: The Cipher, Extracted for the Visor

3027 skycoin-lite: make the wasm cipher registrable from any wasm program is the pivotal one: the registration lived in a main, so the only way to get the cipher into a browser was to serve the binary built from src/skycoin-lite/wasm — but nothing about it needed to be a program, it just publishes two objects on the JavaScript global and returns. It moves to a wasmcipher package exported as Register (the main shrinks from 181 lines to 25, and Register itself does not block, since a host carrying the cipher alongside other things has its own reason to stay alive), and because the wallet front end reaches the cipher only through window.SkycoinCipher, any wasm that calls Register is a drop-in the page cannot tell apart — verified not by assertion but by building a throwaway wasm program that publishes its own globals and then calls wasmcipher.Register(), pointing the wallet’s cipher suite at it, and getting 2217/2217. Returning errors instead of panicking (#3025) was the real precondition, since a library that reports failure by panicking is not safe to link into a host doing other work — under TinyGo a mistyped address would have trapped the host, not just the cipher. 3028 buildinfo: report skycoin’s version when skycoin is not the program closes the loose end that leaves: skywire skycoin printed skywire’s version under a skycoin banner, because the banners read debug.BuildInfo.Main.Version — the module that was built — and skywire is the main module. The skycoin version was already reachable as a dependency entry (exactly what skywire -d | grep skycoin prints) that nothing consumed; SelfVersion uses it now, falling back to the main module for a host that does not record skycoin and keeping (devel) for a versionless local build, with six new tests where the package had none — and the same treatment flows to SkycoinCipher.version, so a visor carrying the cipher can report both its own build and the cipher’s. The fix lives in skycoin’s buildinfo, not skywire’s, because a host cannot get it right without knowing which dependency is mounted, and a downstream fix is one any change here can undo. 3029 skycoin-web: put a transport seam under ApiService is the integration seam itself: ApiService called HttpClient directly, which is why cmd/skycoin-web has to proxy /api at all — a browser cannot call a node itself, both because of CORS and because the node checks Host, Origin and Referer, but a skywire visor on the same page makes the request outside the browser’s fetch stack, where none of that applies. A new ApiTransportService is the seam, and the target decides the path with no setting to configure: a <pk>.dmsg address (which a browser cannot fetch at all) goes via skywireVisor.fetchDmsg, anything else via HttpClient exactly as before, so a wallet with no visor behaves identically down to the parameters, headers and POST body its specs pin. It switches nothing over yet — nothing configures a dmsg node URL — it removes the reason the proxy has to exist; found through the published global rather than an import (skycoin-web does not depend on skywire), it pairs with skywire 3862, which puts the cipher in the visor.