Skip to content

feat(flutter): wallet connection with Freighter and a mock wallet - #113

Open
Pericena wants to merge 4 commits into
mainfrom
feat/25-wallet-connection
Open

Pericena wants to merge 4 commits into
mainfrom
feat/25-wallet-connection

Conversation

@Pericena

@Pericena Pericena commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Refs #25 — stacked on #112 (base feat/27-deploy-flow). Review after #111 and #112.

What

  • WalletPort is the only signing boundary: connect, disconnect, address, network, signTransaction (returns the signed XDR; the server submits, contract docs: add API contract between Flutter app and Serverpod #77 Decision A) and signAuthEntry.
  • FreighterWallet over a small JS bridge (@stellar/freighter-api 6.0.1), from the feat(spike): prove Flutter Web Freighter signing and Testnet SAC payments #70 spike. Before any prompt it applies the chore(frontend): enforce pre-prompt checks in the #68 wallet spike adapter #69 checks: Testnet passphrase, no fee-bump envelopes, no trailing bytes, transactions only for the connected account, auth entries with non-zero expiration. After signing it verifies the transaction is unchanged and signed by the connected account.
  • WalletController exposes one explicit WalletStatus: disconnected, connecting, connected, signing, signed, rejected, wrongNetwork, error. It blocks signing before any prompt when the wallet is not on Testnet (any adapter); a network/account switch forgets the session.
  • MockWallet for tests and the demo console (--dart-define=WALLET=mock); Freighter is the default on the web.
  • Wallet sheet (S09): connect, waiting, connected (Testnet badge, copyable address, disconnect), signing, signed, signature rejected, wrong network, not installed (install link), locked, account changed — each with a retry.
  • Demo flows (deploy, hire) sign a harmless, never-submitted Testnet envelope so Freighter shows its prompt; they stay labelled as demos.
  • README: architecture, how to test with Freighter, build and test commands.

Security

The app only handles the public address, network, unsigned XDR and signed XDR. No secret keys are stored or logged. Mainnet is not supported.

Tests

  • 160 passing: Freighter adapter checks, connection and signing states (signing, signed, rejected, wrong network, error, disconnect).
  • flutter analyze: no issues.

Pending (why Refs, not Closes)

Pericena and others added 4 commits October 6, 2026 23:56
Part A of the #109 split (theme and shell). No screen content changes.

- Puls3Breakpoints: one set of named layout breakpoints instead of raw
  600/640/700/900/960/1000 numbers across screens and widgets, plus
  isCompactLayout().
- Phone type scale below 640 px (14 px body, 18-28 px titles), set by
  the app root; desktop keeps the brand sizes.
- Compact visual density and denser inputs on phones; PrimaryButton is
  44 px tall there (52 on desktop) inside a 48 px tap target.
- Bottom navigation bar on phones (Studio, Marketplace); the top bar
  keeps the logo and a wallet chip that shrinks instead of overflowing.
- ScreenHeader and SkeletonBox building blocks for the following parts.

Tests: bottom bar on phones only, compact type and button size, and no
overflow on every screen at 320, 360, 390 and 414 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part B of the #109 split. Refs #27: the testnet end-to-end criterion
waits for the deploy endpoint (#18) and the relay (#96).

- DeployFlow + DeployFlowController: preparing -> waiting for signature
  -> registering on-chain -> activating -> live. Never runs twice,
  resumes from the failed step, keeps the prepared and signed
  transaction across a lost response, verifies every backend answer,
  and stops if the flow is closed mid-run.
- Presentational widgets: DeployStepper, phase card, error panel and
  success, with skeletons until values arrive.
- Errors with a recovery action and no reload: signature rejected,
  transaction failed, backend error, plus wallet unavailable, wrong
  network, account changed, connection lost/timeout, invalid response,
  expired preparation (prepares again) and a wallet that never answers
  (times out; the sheet can be closed while the wallet prompt is open).

Review fixes:
- The demo gateway is labelled: a banner says nothing is registered on
  Stellar, and the result reads "Demo deploy only" with no explorer
  link, no agent page and no made-up hash. Demo agents are not listed.
- WalletPort.signTransaction only signs and returns the signed XDR; the
  server submits (API contract #77, Decision A). MockWallet returns a
  marked signed placeholder instead of a hash.
- Real agents start at a 0.0 rating; explorer links use
  stellarExpertTxUrl; the unused SuccessPanel, DeployProgressView and
  AppScope.ids are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #25. Built on the deploy flow (#112). The real-wallet acceptance
on Testnet (screenshot) and the server relay (#96/#18) are pending.

- WalletPort keeps one signing boundary: connect, disconnect, address,
  network, signTransaction (returns the signed XDR, the server submits)
  and signAuthEntry. Typed errors: rejected, not installed, locked/
  unavailable, wrong network, account changed, invalid payload.
- FreighterWallet over a small JS bridge (freighter-api 6.0.1). Before
  any prompt it checks the payload (#69): Testnet passphrase, no
  fee-bump envelopes, no trailing bytes, auth entries with a non-zero
  expiration and address credentials only. After signing it checks the
  account did not change.
- MockWallet for tests and the demo console (--dart-define=WALLET=mock);
  Freighter is the default on the web.
- Wallet sheet (S09): disconnected, connecting, connected (Testnet
  badge, copyable address, disconnect), cancelled, wrong network, not
  installed (install link), locked, with a retry in every error.
- Demo flows (deploy, hire) sign a harmless, never-submitted Testnet
  envelope so a real wallet shows its prompt; they stay labelled as
  demos. Wallet errors show human messages.

Tests: 19 for the Freighter adapter checks, 11 for the connection
states, chip and phone layout. 151 passing, analyze clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t guide

Refs #25.

- WalletStatus is one explicit state: disconnected, connecting,
  connected, signing, signed, rejected, wrongNetwork, error. No mix of
  flags.
- WalletController blocks signing before any prompt when the wallet is
  not on Testnet, for every adapter. A network or account switch during
  signing forgets the session, so the next attempt connects again; the
  deploy flow reconnects before re-signing.
- Wallet sheet: "Waiting for wallet confirmation…", "Transaction
  signed", "Signature rejected", "Wrong network", "Account changed",
  "Transaction refused" and "Wallet connection failed", each with a
  retry.
- MockWallet keeps its account across a disconnect, like a real wallet.
- README: wallet architecture, how to test with Freighter, and the
  commands to build and test.

Tests: signing, signed, rejected signature, signing error, wrong network
blocks signing, disconnect, and the panel states. 160 passing, analyze
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying puls3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: f58652e
Status: ✅  Deploy successful!
Preview URL: https://17777abf.puls3-4lw.pages.dev
Branch Preview URL: https://feat-25-wallet-connection.puls3-4lw.pages.dev

View logs

@Pericena Pericena self-assigned this Oct 7, 2026
@TOMOKI977 TOMOKI977 mentioned this pull request Oct 7, 2026

@TOMOKI977 TOMOKI977 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really solid security work here. I checked every claim in the PR body against the code, and each check is actually enforced:

  • Testnet only, at connect() and again before each prompt.
  • Fee-bump envelopes refused.
  • Canonical re-encode, so trailing bytes are rejected.
  • Transaction and operation sources bound to the connected account.
  • Auth entries with zero expiration refused.
  • After signing, the body is compared unchanged and the signature is verified against the connected key with Dart's hard-coded Testnet hash.

The demo envelope is a single manage_data that can't move value. Nothing sensitive is logged. The WalletPort seam, the sealed exceptions and the single WalletStatus make it easy to follow.

I'm requesting changes for one issue, the same one I raised on #112. It is now reachable with a real wallet:

Starting a deploy with no connected wallet can trap the user. The preparing step awaits _wallet.connect() without a timeout (deploy_flow_controller.dart:189). With Freighter that call is an untimed requestAccess() (freighter_bridge.js:32, freighter_bridge_web.dart:105). While preparing, the sheet hides Close and blocks back (deploy_flow.dart:109-114), and it can't be dismissed (deploy_sheet.dart:20-21). If Freighter never settles the promise (popup ignored, blocked, or stuck on the lock screen), the only way out is reloading the page. An already-connected wallet avoids it (wallet_controller.dart:74-75), but Studio doesn't require connecting first. A timeout on wallet calls in the bridge or controller fixes it for both flows, as would letting the user close while the wallet step is pending. Please add a test with a connect() that never completes.

Also please fix two tests that can't fail as written. freighter_wallet_test.dart:130-131 and :184-188 check wallet.address / bridge.signCalls before the future settles. Use await expectLater(...) first, as you already do at 134-142.

Not blocking, worth a follow-up:

  • Unknown errors read as "you rejected". _translate maps anything without a known code to WalletSignatureRejected (freighter_bridge_web.dart:123-124). A JS TypeError or an extension crash tells the user "You rejected the transaction" with nothing logged. A generic WalletUnavailable (or an error state) plus a log would be more honest.
  • The bridge loads @stellar/freighter-api from esm.sh at runtime (freighter_bridge.js:6), with no integrity pin and no CSP, and index.html blocks app startup on that import. A slow CDN delays the app for everyone, and a CDN that is down shows "Freighter not found" even when it is installed. Your comment already notes that production should bundle it. Let's track that as an issue before the public demo.
  • main.dart:19-26 falls back to MockWallet for any WALLET value other than exactly freighter. An assert or a log would catch a typo.
  • WalletController has no in-flight guard on connect/_sign/disconnect. For example, disconnecting during a pending connect ends up connected again (freighter_wallet.dart:43).
  • Untested branches: the signAuthEntry session checks and its post-sign checks (signer mismatch, signature length, bad base64), ADDRESS_V2 credentials, a fee-bump or unsigned envelope returned by the wallet, and the WalletAccountChanged path through the controller.
  • Smaller notes: wallet_sheet.dart:28 uses a raw 600 instead of Puls3Breakpoints.narrowGutter. _Connecting and _Signing duplicate the progress bar block. The README port list is missing signAuthEntry.

@TOMOKI977

Copy link
Copy Markdown
Contributor

Opened #116 to track bundling @stellar/freighter-api locally and adding a CSP before the app is public (#31). It doesn't need to block this PR.

@TOMOKI977

Copy link
Copy Markdown
Contributor

@Pericena Heads-up: CI changes when #114 (Serverpod 4.0.4) merges. Once it lands on main:

  1. Upgrade locally to Flutter 3.44.4 (Dart 3.12.2) and serverpod_cli 4.0.4. The steps are in docs/operations/serverpod-4-migration.md.
  2. Update your branch from main and run dart pub get again. The lockfile moves to Serverpod 4.0.4, and checks run with the new toolchain.
  3. Generated code and migrations: if you touch a *.spy.yaml or an endpoint, run serverpod generate with 4.0.4 and commit the output, since CI fails on any generated diff. Create new migrations with 4.0.4 too, so they sort after upgrade-4-0.
  4. Two new required CI jobs:
    • docker builds the server image.
    • scripts runs the offline tests in scripts/tests/, including the env inventory: every PULS3_* variable read in code must be listed in .env.example. Secrets stay commented out there, with an entry in docs/infra/secrets.md.
  5. Cloudflare Pages builds with scripts/cloudflare-pages-build.sh, which pins the same Flutter version as CI.

Specific to this PR: after rebasing #112, rebase this one onto it (or onto main once #112 merges). It currently has conflicts with main. The new stellar_flutter_sdk dependency will resolve against the Serverpod 4 lockfile, so run flutter pub get and commit the lockfile change.

@TOMOKI977 TOMOKI977 mentioned this pull request Oct 7, 2026
4 of 5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants