feat(gnoweb): GnoConnect external-wallet transport — registry + chooser + launch-link#5970
feat(gnoweb): GnoConnect external-wallet transport — registry + chooser + launch-link#5970D4ryl00 wants to merge 12 commits into
Conversation
Phase B of the GnoConnect external-wallet transport: add a server-side embedded wallet registry so gnoweb can render a wallet chooser offline, with no fetch. - components/wallets.json: registry array, single Gnokey entry (bare scheme land.gno.gnokey, self-contained data: URI icon). - components/wallet_registry.go: Wallet type + //go:embed loader, parsed once at init; Wallets() accessor. - HelpData gains Wallets []Wallet, populated in HelpView. - Tests cover the embedded registry and HelpView population.
Phase C of the GnoConnect external-wallet transport. On Execute, route to a native wallet via a deep link on touch devices; otherwise fall through to today's TxLink navigation unchanged. - controller-wallet-launch.ts: intercepts the Execute submit per b-action-function. Builds <scheme>://tx?path=&func=&args=&send=&rpc= &chainid=&callback= from the live form (args in declaration order), reading rpc/chainid from the gnoconnect meta tags. Strictly additive: no-op (native submit) on desktop, with no registered wallet, or when an in-page provider (extension) is present. 1 wallet opens directly; >1 shows a chooser dialog. - view_action.go: HelpData.WalletsJSON (template.JS), pre-marshaled once in HelpView; attach the wallet-launch controller and embed the registry as <script type=application/json> + a hidden chooser <dialog>. - events.d.ts: declare the params:changed CustomEvent. - 06-blocks.css: .b-wallet-chooser block. - Rebuilt public/js + public/main.css artifacts. - Tests cover WalletsJSON round-trip and verbatim script-tag rendering.
The wallet chooser never intercepted Execute: the controller threw during construction and the loader swallowed it. Root cause — the article carried two controllers (data-controller="action-function wallet-launch"), but BaseController derives its target-attribute prefix from the *entire* data-controller value. With two names it built the invalid CSS selector [data-action-function wallet-launch-target=...], so getGlobalTarget threw in connect() (and degraded action-function on that article too). The framework assumes one controller per element. Move data-controller="wallet-launch" onto the <form> (a single-controller element): the controller now reads the func/pkgpath from the enclosing article via closest(), scopes params to the form, attaches the submit listener to the form directly, and scopes the chooser dialog lookups to the article. Also drop the unused default export (all other controllers use the named export only) and wire the chooser Cancel button in JS (it lives outside the form, so setupActions can't reach it via data-action). Verified end-to-end on the iOS simulator: Execute -> Gnokey opens prefilled -> sign & broadcast -> counter increments -> callback returns to Safari.
🛠 PR Checks Summary🔴 Changes related to gnoweb must be reviewed by its codeowners Manual Checks (for Reviewers):
Read More🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. ✅ Automated Checks (for Contributors):🟢 Maintainers must be able to edit this pull request (more info) ☑️ Contributor Actions:
☑️ Reviewer Actions:
📚 Resources:Debug
|
… ADR - Gate external-wallet routing on (pointer: coarse) only; maxTouchPoints also matches touchscreen laptops, where a failed custom-scheme launch left Execute dead. - Strip status/hash from the callback URL so wallet round trips don't accumulate stale result params. - Read params/send from the DOM at submit time and drop the params:changed subscription (debounced dispatch could be stale). - Marshal the registry once at package init; drop the unused HelpData.Wallets field and the per-request marshal fallback. - Render one page-level chooser dialog instead of one per function; parse the registry lazily with a shared cache. - Trim comments; move design rationale into an ADR that also documents the launch-link protocol. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
Same pattern as feature/state/page.go: the value is the repo-embedded registry marshaled by json.Marshal (HTML-escaped), destined for a <script type="application/json"> tag. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
Aligns the external-wallet launch link with TxLink's named-argument encoding, namespaced under arg. so realm parameter names cannot collide with the link's reserved keys. Documents the launch-link protocol in docs/resources/gnoconnect.md instead of only the PR ADR. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
The previous test-only check (no "://" substring) missed schemes like javascript:alert(1)//, which would execute once concatenated into the launch link and assigned to window.location. Enforce the RFC 3986 scheme grammar (which cannot contain ":"), unique ids/schemes, and data:image/ icons at init; a bad registry panics. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
…back A failed custom-scheme launch is silent, so a mobile user without the wallet installed had no way back to the native submit once a single registered wallet triggered a direct launch. The chooser now always appears and carries a "Continue in browser" action that submits the form natively (form.submit() bypasses submit listeners, so there is no re-interception). Also restores action.html's trailing newline and reverts an unrelated attribute reflow. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
Replace the placeholder SVG with the icon from gnolang/gnokey-mobile (mobile/assets/images/icon.png), downscaled to 96x96 PNG. Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
The preflight reset (* { margin: 0 }) zeroed the UA's dialog margin:
auto, pinning the wallet chooser to the top-left corner. Re-assert it
at the element level so any dialog centers correctly.
On top of that, iOS auto-zooms when focusing sub-16px inputs and
showModal() centers in the layout viewport, so the dialog could land
outside the zoomed visual viewport. The controller now re-centers the
open dialog into the visual viewport and caps its size to the visible
area, tracking zoom/scroll while open.
Signed-off-by: D4ryl00 <d4ryl00@gmail.com>
|
Independent local validation report for head
I also independently confirmed the integration gap already called out in the PR description: the referenced From this pass, the gnoweb transport/chooser behavior looks good. The remaining end-to-end blocker is updating the wallet parser to collect |
|
Thanks for the review! You're right about the named args — I forgot to push the fix, now on the companion branch: D4ryl00/gnokey-mobile@7b28cf0c326878b181afb57ce58e5f3fd72440d6 (note: the repo is now private). |
jefft0
left a comment
There was a problem hiding this comment.
This PR resolves part of an issue. See the description. Has tests for this solution. CI checks pass. Ready for core dev review.
feat(gnoweb): GnoConnect external-wallet transport — registry + chooser + launch-link
Closes the mobile/standalone half of #2799 (see my proposal there:
#2799 (comment)).
Design rationale, alternatives, and the launch-link protocol are documented in
the ADR:
gno.land/adr/pr5970_gnoconnect_external_wallets.md. The wire formatitself is specified in
docs/resources/gnoconnect.md§ Launch Links.The problem
gnoconnect:*meta + TxLinks (#3609/#3615) already carry the network + txintent. The in-page JS draft (
registerWallet/getWallets) handles browserextensions — wallets that inject JS into the page. Neither reaches a wallet
that can't run code in the page: a mobile app or a standalone desktop signer.
In-page discovery is structurally extension-only (a native app can't announce
itself on
window), and a same-domain Execute submit can't hand off to anexternal app.
This PR adds the missing transport + discovery path, and it composes with the
in-page layer rather than replacing it.
The solution (and why these are the right calls)
This is an opinionated design. Each decision below is a position I'll defend;
push back with a concrete counter-case, not a preference.
1. Launch link, host
tx. gnoweb opens the wallet with:Named args, like TxLinks (
¶m=value), but namespaced underarg.so arealm parameter named
func,send,rpc,chainid, orcallbackcan'tcollide with the link's own keys. The wallet resolves declaration order via
vm/qdocbefore building theMsgCall— no new connectivity requirement,since it already needs the RPC node for the account sequence and the broadcast.
rpc/chainidcome from the existing meta so the wallet targets the rightnode (incl. localnet). Host is
tx, notcall: it names the intent,leaving room for future hosts (
run,sign) under the same scheme. Walletsshould accept
callas a silent back-compat alias but emit/document onlytx.2. Args are built client-side from live form values. Not read back from the
address bar (which goes stale between edits and submit). One
arg.<name>pairper parameter, every name and value
encodeURIComponentd. Values are readstraight from the inputs at submit time — always fresh, no event-driven state
to go stale.
3.
callback= the current page URL. The wallet reopens exactly where theuser was, with
status+ txhashappended. Anystatus/hashleft by aprevious round trip is stripped before composing the link, so repeated round
trips don't accumulate stale result params. No new return-page concept, and it
works on localhost.
4. Registry stores the bare scheme; gnoweb composes the prefix. Entry
shape:
{ "name": "Gnokey", "id": "gnokey", "icon": "data:image/svg+xml;base64,…", "scheme": "land.gno.gnokey", "platforms": ["ios","android"], "install_url": "https://github.com/gnolang/gnokey-mobile" }Storing
"land.gno.gnokey"(not"land.gno.gnokey://tx") keeps the registryreusable when the standard grows other hosts, and keeps gnoweb the single owner
of link composition. Entries are validated at package init — RFC 3986 scheme
grammar (which can't contain
:, so an entry can never smuggle ajavascript:payload intowindow.location), unique ids/schemes,data:image/icons; a bad registry panics.platformsandinstall_urlareinformational for now (no platform filtering or "not installed" auto-fallback
yet).
5. The registry lives in-repo, server-embedded — not a served endpoint.
//go:embed wallets.json, parsed, validated, and pre-marshaled once at packageinit, then served into a
<script type="application/json">the controllerreads. This is the decisive call for gnoweb specifically: it renders
offline, needs no fetch, and is cache-safe — exactly what the
localnet requirement demands. A served/on-chain catalog can come later without
changing the entry shape. Wallets register by PR.
6. Selection lives in gnoweb, never the OS. A duplicated custom scheme on
iOS routes to one arbitrary app with no system chooser, so the OS cannot do the
picking. gnoweb merges candidates and shows the chooser (one page-level
<dialog>, populated on demand) — which also makes desktop and mobile behaveidentically. The chooser appears whenever at least one wallet is registered —
even for a single one — because it carries a "Continue in browser" action
that performs the native submit: a failed custom-scheme launch is silent, so
without that escape hatch a mobile user without the wallet installed would find
Execute dead-ended. No registered wallet → today's copy-paste TxLink,
untouched.
7. RPC normalization is wallet-side. gnoweb forwards the
gnoconnect:rpcmeta verbatim (it may be scheme-less
127.0.0.1:26657or a full URL). The walletassumes
http://when the scheme is missing. Keeping gnoweb dumb here avoidsbaking transport assumptions into the provider.
8. Custom schemes over Universal/App Links. Origin-independent, work
offline/localhost, need no AASA/assetlinks/CDN. UL/AL are at most an optional
"not installed" fallback — never the primary path.
9. Additive and fail-safe by construction.
preventDefault()fires onlywhen routing to an external wallet. No registry, in-page provider present, or
desktop-without-QR → fall straight through to the native submit; from the
chooser, "Continue in browser" gets back to it explicitly. The Adena/extension
path is never intercepted. Mobile detection is
(pointer: coarse)only —maxTouchPointswould also match touchscreen laptops, where a failedcustom-scheme launch would leave Execute dead.
What's in this PR
components/wallets.json+wallet_registry.go(//go:embed, parsed,validated, and pre-marshaled at init) +
wallet_registry_test.go.HelpData.WalletsJSON, populated inHelpView.views/action.html: serialized registry + one page-level chooser<dialog>with "Continue in browser" and Cancel actions.
frontend/js/controller-wallet-launch.ts: submit-time param collection,launch-link builder (named
arg.<name>params),(pointer: coarse)mobilegate, fail-open behavior, lazily parsed shared registry.
frontend/css/06-blocks.css:.b-wallet-chooser.docs/resources/gnoconnect.md: new "Launch Links" section — the wire formatwallet authors must implement, in the public standard, not just the ADR.
gno.land/adr/pr5970_gnoconnect_external_wallets.md: design rationale andalternatives considered.
public/artifacts (controller-wallet-launch.js,main.css).Validated end-to-end (localhost, no internet)
Full round-trip against a local
gnodev, wallet = gnokey-mobile(companion branch:
D4ryl00/gnokey-mobile@feat/deeplink-tx—https://github.com/D4ryl00/gnokey-mobile/tree/feat/deeplink-tx):
prefilled → Sign & Broadcast against local gnodev →
callbackreopens gnowebwith
?status=success&hash=….r/demo/profile.SetStringField(field, value)under a scoped session allow-pathvm/exec:gno.land/r/demo/profile— confirmed on-chain (
GetStringField(master,"DisplayName") == "AndroidE2E").Deliberately out of scope (follow-ups, not gaps in the design)
only (mobile browser). Clean follow-up; the launch link is already the payload.
Blocked on Onbloc's
registerWallet/getWalletsdraft; the chooser is builtto absorb them.
contribs/gnodev/setup_web.goassignsRemoteHelp = remoteAddrwithoutnormalizeRemoteURL, so gnodev-embedded webemits a scheme-less
gnoconnect:rpc. Harmless with §7, but worth aligning.The thing I'd still take input on
Registry location — in-repo embed now; served/on-chain catalog later. (The
launch-link shape was the other open question; it's now settled as named
arg.<name>params and specified indocs/resources/gnoconnect.md.)Post-review updates (2026-07-16)
Applied from an AI-assisted review pass (fixes reviewed and pushed by the
author; ADR added per AGENTS.md):
(pointer: coarse)only — the previousmaxTouchPoints > 0clause broke Execute on touchscreen laptops.callbackstripsstatus/hashfrom the page URL so repeatedwallet round trips don't accumulate stale result params.
params:changedsubscription (debounced, so potentially stale at submit) andits
events.d.tsdeclaration are gone.HelpData.Walletsfield and per-request marshal dropped.<dialog>instead of one per function;registry parsed lazily into a cache shared across controller instances.
moved into the ADR.
Post-review updates, round 2 (2026-07-16)
Applied from a second review pass (two external findings + follow-ups from the
first pass):
args=to namedarg.<name>=— aligns with TxLink's named encoding (the old PR text claimedparity it didn't have), is safe against positional mis-binding by third-party
producers, and supports partial prefill. The
arg.namespace preventscollisions with the link's reserved keys. Requires the wallet to resolve
declaration order via
vm/qdoc— free, since it's online for thesequence/broadcast anyway. Documented in
docs/resources/gnoconnect.md§ Launch Links.
check (
no "://") missed schemes likejavascript:alert(1)//, which wouldexecute once concatenated and assigned to
window.location. The RFC 3986scheme grammar structurally excludes
:; also enforces unique ids/schemesand
data:image/icons."Continue in browser" action that performs the native submit — previously a
mobile user without the wallet installed hit a silent dead end with no way
back.
feat/deeplink-txstill parses positional args;it needs the named-params update, then the e2e validation will be re-run.