This tutorial covers how wallet_connect.html authenticates a Stellar
account, on both desktop and mobile, and how to test each path locally and
in CI.
- Desktop: Freighter browser extension —
unchanged, see the
window.freighterbranch inwallet_connect.html. - Mobile: no browser extension exists on iOS/Android, so
wallet_connect.jsroutes to a wallet picker (Lobstr, Bitnovo, or any other SEP-0007-compatible wallet) and completes a SEP-0010 web-authentication handshake via deep link.
Tracked under the mobile wallet deep-link issue, part of the umbrella tracker issue #37; depends on the CLI/contract stabilization work in #54 and #55.
window.freighter only exists because Freighter injects itself into the
page as a desktop browser extension. Mobile browsers have no equivalent
injection point, and the Stellar mobile wallets that matter here — Lobstr
and Bitnovo — are apps, not extensions. The bridge between "web page" and
"wallet app" on mobile is a deep link: the page redirects the OS to a
URI the wallet app has registered itself to handle
(web+stellar:tx?xdr=..., per SEP-0007), the wallet shows the user what
they're signing, and it redirects back to a callback URL with the result.
That means the mobile flow is two round trips instead of one function call:
- Identify — the browser has no way to ask an app "what's your public
key" up front, so the user supplies the Stellar public key they intend to
authenticate with (copied from the wallet's "Receive" screen). This is
the same manual-entry pattern
wallet_connect.htmlalready used as its Freighter-not-found fallback; the mobile flow reuses it deliberately instead of inventing a second UX for the same problem. - Challenge / sign / verify — once the account is known, the page can request a real SEP-10 challenge, hand it to the wallet app via deep link, and verify the signature when the wallet redirects back.
wallet_connect.js checks navigator.userAgent against a conservative
regex (Android|iPhone|iPad|iPod|Mobile|IEMobile|BlackBerry|webOS). Desktop
user agents never match, so the existing Freighter path is completely
unaffected — this is additive, not a rewrite. See isMobile() in
wallet_connect.js.
When connectWallet() detects a mobile UA, it opens the #wallet-picker
modal instead of calling window.freighter. The wallet registry
(WALLETS in wallet_connect.js) currently lists:
| Wallet | Deep link scheme | Notes |
|---|---|---|
| Lobstr | web+stellar:tx?xdr=...&callback=url:... |
SEP-0007 tx request; Lobstr SEP-0007 docs |
| Bitnovo Wallet | web+stellar:tx?xdr=...&callback=url:... |
SEP-0007 tx request; fallback custom scheme bitnovowallet://sep0007?... if the universal link doesn't resolve to an installed app |
| Other SEP-0007 wallet | web+stellar:tx?xdr=...&callback=url:... |
Generic entry for any wallet that implements the spec (xBull, Rabet mobile, etc.) |
Both buildDeepLink scheme strings are the standards-track SEP-0007 URI.
The vendor-specific fallbackScheme entries are best-effort — confirm
against each wallet's current developer docs before depending on them in
production, since undocumented custom schemes can change without notice.
Browser (mobile UA) Auth server Wallet app (Lobstr/Bitnovo)
| | |
|-- 1. GET /auth?account=G... --->| |
|<--------- challenge XDR ---------| |
| | |
|-- 2. redirect: web+stellar:tx?xdr=<challenge>&callback=url:<returnUrl> ---------->|
| |
| user reviews & signs |
|<----------------- redirect: <returnUrl>?xdr=<signed> ----------|
| | |
|-- 3. POST /auth {transaction} -->| |
|<---------- { token } ------------| |
Sep10.fetchChallenge(account)—GET {AUTH_SERVER}/auth?account=...&home_domain=...returns{ transaction, network_passphrase }(aManageData-based challenge transaction per SEP-10, signed by the server so the client can trust its origin).startMobileConnect()builds the SEP-0007 deep link for the chosen wallet, embedding the challenge XDR and a callback URL that points back atwallet_connect.htmlwith a marker query param (?orbitchain_xdr=1), then doeswindow.location.href = deepLink. The mobile OS switches to the wallet app; JS execution on the page pauses here.- The wallet app shows the transaction (a zero-amount, zero-effect
ManageDataop — nothing is transferred), the user approves, and the wallet redirects back to the callback URL with?xdr=<signed-xdr>appended, per the SEP-0007callback=url:response convention. - On page load,
handleMobileReturn()detectsorbitchain_xdr=1in the URL,POSTs the signed transaction to{AUTH_SERVER}/auth, and the server verifies the signature belongs to the claimed account (WebAuth.verifyChallengeTxSigners) before issuing a JWT session token. The page then decodes the token'ssubclaim to show the connected address, mirroringshowConnected()on the desktop path.
AUTH_SERVER defaults to http://localhost:4000 and is overridable via
window.ORBITCHAIN_AUTH_SERVER or <script data-auth-server="..."> on the
wallet_connect.js tag — point it at your real SEP-10 endpoint in
production.
The issue references SEP-45-style redirects. SEP-45 is Stellar's
web-authentication spec for smart-wallet / contract accounts
(passkeys, Soroban Address accounts). OrbitChain's campaign contract
currently operates against classic Stellar accounts (G... keys), so
SEP-10 is the correct spec today. SEP-45 support is a natural follow-up
once campaign donations accept Soroban smart-wallet signers — tracked
against the same umbrella issue (#37) rather than bundled here.
This repo has no production auth backend yet, so
scripts/sep10-server.mjs provides a
minimal, spec-correct SEP-10 challenge/verify server for local development,
the curl smoke test, and the Playwright CI job. It is not meant for
production — swap AUTH_SERVER to a real endpoint before deploying.
npm install
npm run sep10:server
# [sep10-server] listening on http://localhost:4000
# [sep10-server] server account: GDTXR3JTIVAWQRBCR35PMRZAFT2ISBBJXEWRW2NHBY7SH4S75AUDVA7B
# [sep10-server] home_domain: localhost
# [sep10-server] network: Test SDF Network ; September 2015Then open wallet_connect.html (e.g. python3 -m http.server 8055) from a
mobile device or a mobile emulation profile in your browser's dev tools.
scripts/smoke-test-sep10.sh
reproduces the exact handshake wallet_connect.js performs over HTTP, using
scripts/sep10-sign-challenge.mjs
to stand in for the wallet's signature step (Lobstr/Bitnovo would show a
signing prompt here; this script signs with a well-known test keypair
instead so the loop can run headless):
npm run sep10:server &
bash scripts/smoke-test-sep10.sh── [1/4] Wallet picker + deep-link scheme sanity ─────────────────────────
✅ Lobstr and Bitnovo both route through the SEP-0007 'web+stellar:tx' scheme
(see WALLETS registry in wallet_connect.js)
── [2/4] GET http://localhost:4000/auth — request SEP-10 challenge ─────────────────────
{
"transaction": "AAAAAgAAAAD...",
"network_passphrase": "Test SDF Network ; September 2015"
}
✅ received challenge transaction (508 bytes, base64 XDR)
── [3/4] Sign challenge as the wallet would (Lobstr/Bitnovo SEP-0007) ────
✅ signed challenge with client keypair GATOACHAPPG72R2KKG5K47ORQVZKGBQ4UYVWLIYITEKMNFXQLNPJFJI3
── [4/4] POST http://localhost:4000/auth — exchange signed challenge for a session ─────
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."
}
✅ session token issued for GATOACHAPPG72R2KKG5K47ORQVZKGBQ4UYVWLIYITEKMNFXQLNPJFJI3
✅ SEP-10 handshake round-trip OK (Lobstr/Bitnovo deep-link path validated)
A non-zero exit code means the handshake broke somewhere (bad challenge,
signature verification failure, token mismatch) — safe to wire into CI as a
hard gate, and it is (mobile-wallet-e2e job in
.github/workflows/ci.yml).
tests/e2e/wallet-connect-mobile.spec.js
drives wallet_connect.html in real mobile browser contexts
(devices['iPhone 13'] and devices['Pixel 7'], via
playwright.config.js), plus a
desktop-chromium project for the regression check. It covers:
- the wallet picker appearing on mobile UAs and listing Lobstr + Bitnovo;
- picking either wallet firing a real
GET /auth?account=...SEP-10 challenge request for the entered account; - invalid public keys being rejected client-side before any network call;
- the desktop UA never seeing the picker (Freighter path unaffected);
- the full return handshake: a pre-signed challenge is appended to the URL exactly as a wallet's callback redirect would, and the page is expected to show "Wallet Connected" with the matching address.
npm install
npx playwright install --with-deps chromium webkit
npm run test:e2eplaywright.config.js starts both the static file server (python3 -m http.server) and the reference SEP-10 server automatically, so no manual
setup is needed beyond npm install + browser install.
- Serve the repo root over HTTPS or on the same LAN as your phone (Lobstr
and Bitnovo both require the deep link to be triggered from an actual
page load, not
file://). - Start
npm run sep10:serveron a host reachable from your phone, and setwindow.ORBITCHAIN_AUTH_SERVERinwallet_connect.htmlaccordingly. - Open the page on your phone, tap Connect Wallet, pick Lobstr or
Bitnovo, paste your account's public key, and confirm the app opens with
a signing prompt for a
manage_dataoperation (not a payment — SEP-10 challenges never move funds). - Approve in the wallet app and confirm you're redirected back to
wallet_connect.htmlshowing Wallet Connected.