Skip to content

feat(flutter): SEP-10 wallet sign-in before a real hire (signChallenge with challenge checks, session on the Serverpod client) - #149

Merged
moises-cisneros merged 4 commits into
mainfrom
feat/136-app-wallet-sign-in
Oct 10, 2026
Merged

moises-cisneros merged 4 commits into
mainfrom
feat/136-app-wallet-sign-in

Conversation

@XxHugheadxX

Copy link
Copy Markdown
Contributor

Refs #136

The app half of wallet-authenticated sessions. The server half is #147.

Summary

Since #147, the server accepts hire calls only from a wallet-bound session. The app connected Freighter and read the address, but never signed in, so with HIRE=server every hire ended in "Sign-in with your wallet is not available yet". It also could not sign the challenge: FreighterWallet.signTransaction requires every source to be the connected account (#69), and a SEP-10 challenge's source is the server.

Wallet

  • WalletPort.signChallenge(SignInChallenge). The Freighter adapter validates the challenge with assertSep10Challenge before any prompt:
    • sequence number 0, so the challenge can never be submitted;
    • only manage_data operations;
    • the first one is <home domain> auth, from the connected account;
    • every other operation comes from the server (the transaction source), which is never the connected account;
    • time bounds that include now;
    • when known (config.json auth), the server signing key and the home domain.
  • After signing, the body must be unchanged and carry a new signature from the connected account. signTransaction uses the same check.
  • MockWallet and WalletController implement it.

Session

  • WalletSession runs createChallenge, then the wallet signature, then verifyChallenge. The AuthSuccess is stored by the client's FlutterAuthSessionManager, which sends it on every call.
  • A session is reused only for the wallet that opened it. A challenge issued for another wallet is never signed.
  • main.dart sets client.authSessionManager. It reads serverSigningKey and homeDomain from config.json auth when the server publishes them. The bundled config of the Pages build has no auth; the wallet checks still hold without it.

Hire flow

  • HireGateway.ensureSignedIn / forgetSession. Both do nothing in the demo.
  • When it signs in: after connecting and before createHire, in the new step signingIn ("Sign in with your wallet: it only proves the account is yours").
  • Refused session: when the server rejects the session (NotAuthenticated, AuthenticationUnavailable), or rejects the sign-in (ChallengeExpired, ChallengeConsumed, ChallengeNotFound, InvalidWalletSignature → HireSignInFailed), the session is dropped, so Try again signs in afresh.
  • ChallengeRateLimited: "too many sign-in attempts".

Acceptance criteria (#136, app half)

  • The app signs in through walletAuth.createChallenge / verifyChallenge, and the session is sent on every call.
  • The wallet signs only a valid SEP-10 challenge for its own account (10 guards, each tested).
  • A refused or expired session is dropped and redone on retry.
  • Live run with Freighter against the deployed server. Blocked: the deployed API (1.0.0+4df3ba4) returns 404 for walletAuth and hire until it is redeployed from main (chore: deploy server to Serverpod Cloud and Flutter web app #31).

Verification evidence

$ cd puls3_flutter && flutter analyze --fatal-infos
No issues found!
$ flutter test
00:32 +267: All tests passed!
$ flutter build web --release
√ Built build\web

New tests:

  • test/wallet/freighter_wallet_test.dart, group signChallenge:
    • challenges are built like the server's StellarSep10Codec and signed by a real key through the fake Freighter bridge;
    • a valid challenge, with and without the server key;
    • signTransaction refuses it, which is why this path exists;
    • every refusal: sequence ≠ 0, the wallet as source, a non-manage_data operation, another account, another operation source, a wrong home domain, another server key, no or expired time bounds, the wrong network, a changed answer, no new signature.
  • test/hire/wallet_session_test.dart:
    • sign-in order;
    • reuse for the same wallet;
    • a wallet switch;
    • a restored session for an unknown wallet;
    • a challenge for another wallet;
    • a declined signature;
    • a refused verification;
    • forget;
    • the gateway's error mapping.
  • test/screens/hire_sheet_test.dart:
    • sign-in happens before createHire;
    • a declined sign-in creates no hire, and a retry works;
    • a refused sign-in is dropped and redone.
  • test/data/app_config_test.dart: readAuthConfig.

I broke each guard one at a time to check that the tests catch it. All 10 changes made a test fail:

  • sequence 0;
  • only manage_data;
  • the challenge's account;
  • the source of the second operation;
  • expiry;
  • the server key;
  • a challenge for another wallet;
  • reusing another wallet's session;
  • no sign-in before the hire;
  • not dropping a refused session.

Notes for reviewers

Since #147 the server only accepts hire calls from a wallet-bound session.
The app only read the Freighter address, so with HIRE=server every hire
ended in "Sign-in with your wallet is not available yet". It also could not
sign the challenge: FreighterWallet.signTransaction requires every source to
be the connected account (#69), but a SEP-10 challenge's source is the
server.

Wallet:
- WalletPort.signChallenge(SignInChallenge), with assertSep10Challenge:
  sequence 0 (never submittable), only manage_data operations, the first one
  "<home domain> auth" from the connected account, every other one from the
  server (the transaction source, never the account), time bounds that
  include now, and the server key and home domain when the app knows them.
  After signing, the body must be unchanged and carry a new signature from
  the connected account (shared with signTransaction).
- MockWallet and WalletController implement it.

Session:
- WalletSession: createChallenge, wallet signature, verifyChallenge, then
  the AuthSuccess is stored by the client's FlutterAuthSessionManager, which
  sends it on every call. A session is reused only for the wallet that
  opened it; a challenge issued for another wallet is never signed.
- main.dart sets client.authSessionManager and reads serverSigningKey and
  homeDomain from config.json auth when the server publishes them.

Hire flow:
- HireGateway.ensureSignedIn / forgetSession (no-op in the demo). The flow
  signs in after connecting and before createHire (new step signingIn). A
  session the server refuses (NotAuthenticated, AuthenticationUnavailable)
  or a refused sign-in (ChallengeExpired, ChallengeConsumed,
  ChallengeNotFound, InvalidWalletSignature: HireSignInFailed) is dropped,
  so Try again signs in afresh. ChallengeRateLimited maps to "too many
  sign-in attempts".

Tests: SEP-10 challenges built like the server's codec, signed by a real
key through the fake Freighter bridge (valid, and every refusal); session
reuse, wallet switch, wrong-wallet challenge, declined and refused
sign-ins; the sheet signs in before createHire and recovers after a refusal;
readAuthConfig. 267 app tests pass, web release build succeeds, and
breaking each of the 10 guards fails a test.

Refs #136
@XxHugheadxX XxHugheadxX added this to the Serverpod (Oct 14) milestone Oct 10, 2026
@XxHugheadxX XxHugheadxX added area: frontend Flutter implementation type: feat New functionality P0 Blocks a deadline deliverable labels Oct 10, 2026
@XxHugheadxX XxHugheadxX self-assigned this Oct 10, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying puls3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: f07f76e
Status: ✅  Deploy successful!
Preview URL: https://03337bbc.puls3-4lw.pages.dev
Branch Preview URL: https://feat-136-app-wallet-sign-in.puls3-4lw.pages.dev

View logs

@moises-cisneros

Copy link
Copy Markdown
Contributor

Note for testing this end to end: with a real funded testnet account, sign-in will fail on the server, independent of this PR. The signer lookup cannot decode a real account entry, so verifyChallenge answers AuthenticationUnavailable and the app shows "Sign-in with your wallet is not available yet".

I reproduced it by signing a challenge directly against the server with a funded account, with no Flutter code involved. The fix is in #158 (server only). Once it is merged, this PR should work with a real Freighter account.

XxHugheadxX and others added 3 commits October 10, 2026 08:37
…the agent's result (#150)

- HireGateway.getHire returns HireProgress (from HireDetail): escrow
  status, run status, failure reason, result and the fund tx. HireStage
  derives Funded/Queued/Running/Result ready/Run failed/Submitted/
  Completed/Rejected (Cancelled from open)/Expired; a succeeded run that
  is still `running` with a result reads as Result ready.
- ServerHireGateway maps HireNotFound/HireNotOwned/InvalidHireId to
  HireNotFound and takes the payment link from the fund submission only.
- /hires/:id (HireDetailScreen + HireDetailView): polls every 4 s until
  the hire is final, keeps the last status with a Retry banner when a
  refresh fails, asks to connect or sign in (SEP-10) when needed, copies
  the result, opens the payment on StellarExpert.
- The hire sheet's confirmed state gets "View hire".
- Pages build: PULS3_HIRE sets the HIRE dart-define (server or demo).
- Approve and Reject are not offered: they need the server-signed
  submit (#97).

Refs #28
@moises-cisneros
moises-cisneros self-requested a review October 10, 2026 12:49
@moises-cisneros
moises-cisneros merged commit e55a0a9 into main Oct 10, 2026
10 checks passed
@moises-cisneros
moises-cisneros deleted the feat/136-app-wallet-sign-in branch October 10, 2026 12:50
TOMOKI977 added a commit that referenced this pull request Oct 11, 2026
…ory and add a blueprint check (#167)

Refs #23.

- S04, S07 and S08 wireframes no longer matched the app: they predated
  the hire relay (#144), wallet sign-in (#149), the funds check (#159)
  and the Studio builder (#145). They are regenerated from the app at
  1440 and 390 px; the other 30 generated wireframes still match and are
  unchanged.
- screens.md: S04 states now cover the wallet sign-in step, the funds
  check before any signature, resuming an unfinished hire, and the
  confirming/held result. S06 lists HireDetailView. The inventory adds
  HireDetailView and Puls3Mark and explains the (new) planned components.
- The wireframe tool fills a valid manifest before opening the deploy
  sheet, and seeds the demo wallet so the S08 PNG is the same on every
  run.
- scripts/tests/blueprints.test.sh checks offline that every screen ID
  has a 1440 and a 390 px PNG, every wireframe is linked and every link
  resolves, every screen documents its four states, data, actions and
  components, and every Entity.field exists in docs/domain/model.md.

Co-authored-by: Julio Cesar Severiche Orellana <39506930+TOMOKI977@users.noreply.github.com>
moises-cisneros pushed a commit that referenced this pull request Oct 14, 2026
…ore the wallet prompt (#136) (#176)

Ports what the #174 draft added over main's sign-in (#149), onto main's
WalletPort.signChallenge(SignInChallenge):

- assertSep10Challenge now also requires a web_auth_domain operation from
  the server, checks its value when the app knows it, and verifies the
  server's own signature (the transaction source, on Testnet) before
  Freighter is prompted. Before, a challenge with no server signature was
  signed.
- SignInChallenge, WalletSession and readAuthConfig carry webAuthDomain.
  readAuthConfig takes build-time pins (PULS3_AUTH_SERVER_SIGNING_KEY,
  PULS3_AUTH_HOME_DOMAIN, PULS3_AUTH_WEB_AUTH_DOMAIN) that win over
  config.json, because the static Pages build has no server-published
  auth block, so today it pins nothing.
- cloudflare-pages-build.sh passes the pins as dart-defines, fails when
  PULS3_HIRE=server is missing any of them, and refuses a server key that
  is not a public G... address without echoing it.
- README (Deploy) and .env.example list the three Pages variables and the
  values of the deployed server.

The #174 config.json pinned GDVEU3..., puls3.app and auth.puls3.app; the
live server signs with GBEEL... and uses
puls3-hub-on-stellar.api.serverpod.space for both domains, so those pins
would refuse every real challenge. They are not carried over.

Tests: signed challenge with the pinned web_auth_domain; refused without
web_auth_domain, with another web_auth_domain, unsigned, and signed by
another key; pins over config.json; Pages build pins and fail-closed cases.

Refs #136
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: frontend Flutter implementation P0 Blocks a deadline deliverable type: feat New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants