diff --git a/CONNECTIVITY.md b/CONNECTIVITY.md index 1cf989e3a..174948377 100644 --- a/CONNECTIVITY.md +++ b/CONNECTIVITY.md @@ -306,8 +306,8 @@ The app is available for a new session when hub presence is not busy backend not yet ready for play) and there is no consent prompt, reserved peer id, buffered handshake, or live message handler (`isAvailableForNewSessionPrompt()`). Backend readiness is owned by `InternalBlockchainInterface.isReadyForPlay()` -(simulator: connected; WalletConnect: a verified full-node peer, checked -privately). The app still connects to the hub normally while a backend is not +(simulator and Cloud Wallet: connected; WalletConnect: a verified full-node +peer, checked privately). The app still connects to the hub normally while a backend is not ready; it simply advertises busy, and inbound `advisory_start` / `session_proposal` must still be declined even if the game WebSocket is live. A consent prompt is a temporary unavailable state for inbound matchmaking even diff --git a/FRONTEND_ARCHITECTURE.md b/FRONTEND_ARCHITECTURE.md index 02eab4a29..5d1c01675 100644 --- a/FRONTEND_ARCHITECTURE.md +++ b/FRONTEND_ARCHITECTURE.md @@ -137,9 +137,9 @@ means session obligation, walletless (`shouldReportHubBusy`), or that the active blockchain backend is not yet ready for play (`blockchainReady === false`, folded into `shouldReportHubBusy` / `shouldReportHubBusyPresence`). Readiness is owned by the backend behind `InternalBlockchainInterface.isReadyForPlay()` / -`onPlayReadinessChange()`: the simulator is ready whenever connected, while -WalletConnect polls privately for a verified full-node peer (peer count never -leaves the backend). The app still connects to the hub normally while a backend +`onPlayReadinessChange()`: the simulator and Cloud Wallet are ready whenever +connected, while WalletConnect polls privately for a verified full-node peer +(peer count never leaves the backend). The app still connects to the hub normally while a backend is not ready; it just advertises busy. Shell mirrors the backend's readiness into `blockchainReadyRef` via `onPlayReadinessChange`, and a wallet disconnect clears it (the backend can no longer vouch for readiness). The `HubConnection` uses a @@ -455,7 +455,7 @@ are grouped under those phase-owned payloads: | `unreadGame` | `boolean?` | Whether the Game tab has unread activity. | | `walletAlert` | `boolean?` | Whether the Wallet tab should show an alert dot. | | `hubAlert` | `boolean?` | Whether the Hub tab should show an alert dot. | -| `blockchainType` | `'simulator' \| 'walletconnect'?` | Which wallet backend is active or should be reconnected. | +| `blockchainType` | `'simulator' \| 'walletconnect' \| 'cloud'?` | Which wallet backend is active or should be reconnected. | | `serializedGameSession` | `Uint8Array?` | Raw binary WASM game-session state via `serialize()`. | | `gameSessionSchemaVersion` | `bigint?` | Rust-owned schema ID for `serializedGameSession`; currently `6`. Missing or mismatched IDs are unsupported and cleared before deserialization. | | `pairingToken` | `string?` | Locally generated identity for the current peer-session/controller instance. It is persisted so pre-cradle setup or a full session resumes into the same instance, and it correlates Shell transition completion with that instance; it is not protocol authority. | @@ -1127,13 +1127,49 @@ Shell manages wallet connections through two abstractions defined in - **`InternalBlockchainInterface`** — the backend-specific implementation (`RealBlockchainInterface` for WalletConnect, `FakeBlockchainInterface` for - the simulator). Each exposes `beginConnect()`, `disconnect()`, - `isConnected()`, `spend()`, etc. + the simulator, `CloudBlockchainInterface` for Cloud Wallet OAuth). Each + exposes `beginConnect()`, `disconnect()`, `isConnected()`, `spend()`, etc. - **`ConnectionSetup`** — returned by `beginConnect()`. Contains a `uri` for - the QR code and a `finalize()` promise that resolves when the wallet is - paired. Optionally contains `fields` (a map of input descriptors) indicating - the backend needs extra user input before connecting (e.g. the simulator's - initial balance). + the QR code and a `finalize(values?)` promise that resolves when the wallet is + paired. Optionally contains `fields` (a `Record` of typed input descriptors, + each `{ type: 'string' | 'bigint', label, default }`) indicating the backend + needs extra user input before connecting, plus an optional `title`/ + `description` for the setup modal. Examples: the simulator's initial balance + (`bigint`), and Cloud Wallet's OAuth `clientId` / API URL / UI URL (`string`) + plus a transaction fee (`bigint`, in mojos). Cloud Wallet sets `skipQr: true` + and completes OAuth inside `finalize()` after persisting the entered config via + `cloudWalletConfig.ts` (kept separate from the OAuth tokens in + `cloudWalletAuth.ts`). The fee field is not part of `cloudWalletConfig`: it + writes through to the global `defaultFee` preference (`setDefaultFee`), the + same value the Wallet tab edits, so `CloudBlockchainInterface.getFee()` reads + one source of truth. All OAuth/GraphQL calls resolve the client id and + endpoints at call time through `getCloudWallet*` getters, so UI-entered config + takes effect without a rebuild. + + Cloud Wallet carries the fee inside the funding spend itself: `createOfferForIds` + passes `fee` to the `createSpendWithExtraConditions` mutation (which adds + `RESERVE_FEE` and selects coins for `amount + fee`), and `selectCoins` requests + `amount + fee` so the pinned launcher-parent coin can cover both. It therefore + implements no `createFeeSpend`; `submitTransactionNow` skips the separate fee + spend for backends lacking that method and broadcasts with no fee parameter. + + **Fee floor.** Chia's mempool treats a fee below 5 mojos per cost unit as zero + (`nonzero_fee_minimum_fpc`), so a small nonzero fee is strictly worse than no + fee: it buys no inclusion, and on a full mempool the node rejects the bundle + with `INVALID_FEE_TOO_CLOSE_TO_ZERO` instead of admitting it as free. The front + end therefore forbids the in-between values: `front-end/src/constants/fees.ts` + defines `MIN_NONZERO_FEE_MOJOS` (100M mojos, derived from 5 mojo/cost times a + conservative bundle cost) and `isEffectivelyZeroFee`, and both fee entry points + (the Wallet-tab editor in `Shell.tsx` and the Cloud Wallet connect modal's fee + field) reject a nonzero fee below it. Zero (a free transaction) and + floor-or-above are allowed. This is a floor below which a fee definitely cannot + work, not a guarantee of inclusion. Cloud-vs-Cloud is the pairing that most + needs it: neither peer has a wallet-built fee spend, so the entered fee is the + whole story, whereas a WalletConnect peer aggregates a real `createFeeSpend` + bundle. Both peers push the byte-identical funding bundle, so the node de-dups + the second arrival; `isBenignTransactionSubmitError` recognizes that + duplicate/`ALREADY_INCLUDING_TRANSACTION` as harmless, and a fee-rate rejection + is rewritten by `rewriteFeeRateRejection` into an actionable message. **Design principle:** Shell must not branch on `blockchainType` for connection logic. All differences between backends live behind the interface. A single @@ -1142,16 +1178,34 @@ and poll interval; the rest of the flow is generic. **Connection lifecycle:** -1. User picks "Simulator" or "Link Wallet" → `handleConnect(bcType)`. +1. User picks "Simulator", "Link Wallet", or "Cloud Wallet" → + `handleConnect(bcType)`. 2. `handleConnect` calls `iface.beginConnect(uniqueId)`, which returns a `ConnectionSetup`. -3. If `setup.fields` is present, Shell shows the `SimulatorSetupModal` overlay - so the user can provide the required values, then `handleFinalize()` calls - `setup.finalize()`. -4. If `setup.fields` is absent (WalletConnect), Shell renders the QR code and - immediately awaits `setup.finalize()`, which resolves when the wallet scans. -5. After finalize resolves, `completeConnection()` activates polling and - switches to the Hub tab. +3. If `setup.fields` is present, Shell shows the generic `ConnectionSetupModal` + overlay so the user can provide the required values, then `handleFinalize(values)` + calls `setup.finalize(values)`. This path is used by both the simulator and + Cloud Wallet (the latter is `skipQr` yet still collects OAuth config first). +4. If `setup.skipQr` is set with no fields (a restored WC/Cloud session), Shell + awaits `setup.finalize()` without showing a QR panel or modal. A failed + restore discards the stored Cloud Wallet tokens only for + `CloudWalletAuthError` — a revoked or expired grant, signalled by an + `invalid_grant`/`invalid_client` token response or a 401 that survives a + forced refresh. Network and server errors leave the refresh token in place so + a retry can resume, rather than demoting a momentary outage into a full popup + login. +5. If `setup.skipQr` is set *with* fields (Cloud Wallet, no stored auth), Shell + shows `ConnectionSetupModal` and does **not** call `finalize()` from silent + `handleConnect` or `performResume`. Auto-finalize would open an OAuth popup + or fail when no client id is configured; the user must submit the form (or + use an explicit Reconnect). +6. If `setup.fields` is absent and QR is required (WalletConnect pairing), Shell + renders the QR code and awaits `setup.finalize()`, which resolves when the + wallet scans. +7. After finalize resolves, `completeConnection()` activates polling and + switches to the Hub tab. Connect/finalize failures are surfaced on the Choose + Connection screen and inside the setup modal via `connectError`, rather than + silently resetting the chooser. **Auto-reconnect:** Both backends implement their own WebSocket reconnect following the shared connection discipline described in @@ -1160,8 +1214,12 @@ Shell's `onConnectionChange` callback handles UI state transitions (connected ↔ disconnected) generically. On page load, if the user chooses to resume a pre-game save (one with `blockchainType` but no `serializedGameSession`), Shell calls `handleConnect(bcType, true)` (silent mode) -to re-establish the connection automatically — no modals or QR codes are shown, -consistent with the principle that a reload should be invisible to the user. +to re-establish the connection automatically — no QR codes are shown, and the +simulator balance modal is skipped, consistent with the principle that a reload +should be invisible to the user. Cloud Wallet without stored auth is the +exception: `beginConnect` returns `skipQr` plus `fields`, so silent reconnect +and `performResume` keep `ConnectionSetupModal` (with a wallet alert) rather +than calling `finalize()` with no values. **Session persistence:** `blockchainType` is written via `saveSession({ blockchainType })` as soon as the wallet connection completes, diff --git a/INTERNALS.md b/INTERNALS.md index 29ff2eb93..7fca043b2 100644 --- a/INTERNALS.md +++ b/INTERNALS.md @@ -163,7 +163,11 @@ conflicting spend won — it is never rebroadcast again. Rebroadcasting an *identical* bundle is harmless (the mempool de-duplicates by fingerprint), and a cross-party conflict (the opponent spending the same coin with a *different* bundle) is expected on a real chain and resolves naturally, since only one spend -of a coin can confirm. Eager timeout claims are deliberately excluded from this +of a coin can confirm. On the browser side this harmlessness is enforced by +`isBenignTransactionSubmitError`, which classifies the node's +duplicate/`ALREADY_INCLUDING_TRANSACTION` verdict as benign: both peers push the +byte-identical funding bundle at channel creation, so the second arrival is +always de-duplicated and must not surface as an error. Eager timeout claims are deliberately excluded from this path (they carry a relative timelock) because the ripeness logic above already resubmits them in a reorg-aware way. Coverage: `auto_resubmits_dropped_output_bearing_spend_until_it_lands`, diff --git a/desktop/README.md b/desktop/README.md index f2b558441..49afbe2d4 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -40,12 +40,12 @@ browser at `:3002`. ## Connection modes -The desktop build is **WalletConnect only**. The preload sets +The desktop build hides the local simulator. The preload sets `window.__chiaDistribution = 'electron'`, and `front-end/src/util/distribution.ts` -uses it to hide the "Continue with Simulator" button and the simulator setup -modal, and to resume a saved session with no recorded `blockchainType` as -WalletConnect rather than simulator. The simulator remains available in the web -build. +uses it to hide the "Continue with Simulator" button and to resume a saved +session with no recorded `blockchainType` as WalletConnect rather than +simulator. WalletConnect and Cloud Wallet remain available. The simulator +stays in the web build. The same flag suppresses the front end's multi-tab lease. That lease records its owner in `localStorage` but identifies itself from `sessionStorage`, so a quit @@ -59,15 +59,21 @@ public internet, `front-end/src/util/walletConnectMetadata.ts` substitutes a public https identity when the page origin is not http(s) — the renderer origin here is `chiagaming://app`, which no wallet can open or fetch. +Cloud Wallet OAuth uses that same custom-scheme origin as `redirect_uri` +(`chiagaming://app/oauth/callback`). The protocol handler serves the player +document at that path so the callback page can `postMessage` the authorization +code to the opener. The Cloud Wallet OAuth client must allow that redirect URI. + ## Configuration Optional JSON file at `/config.json`, where `` is `~/Library/Application Support/Chia Gaming` on macOS, `%APPDATA%\Chia Gaming` on Windows, and `~/.config/Chia Gaming` on Linux. -| Key | Default | Meaning | -| ------------ | ---------------------------------------------------- | ------------------------------------------- | -| `hubOrigins` | `["http://localhost:3003", "http://127.0.0.1:3003"]` | Hub origins the app may load and connect to | +| Key | Default | Meaning | +| -------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | +| `hubOrigins` | `["http://localhost:3003", "http://127.0.0.1:3003"]` | Hub origins the app may load and connect to | +| `cloudWalletOrigins` | `["http://127.0.0.1:3000", "http://127.0.0.1:3001", "http://localhost:3000", "http://localhost:3001"]` | Cloud Wallet API and UI origins for OAuth, GraphQL, and approval popups | Anything invalid is reported in an error dialog and the app exits rather than starting with a half-applied policy. @@ -77,6 +83,11 @@ is a starting point rather than a fixed set: a hub typed into the in-app picker is added to it at runtime and written back to the file. See [Hub trust](#hub-trust). +`cloudWalletOrigins` feeds `connect-src` and the popup allowlist. They are not +framed. A production Cloud Wallet is added here (and the OAuth client must +allow `chiagaming://app/oauth/callback` as a redirect URI). A hub grant writes +`hubOrigins` without dropping a `cloudWalletOrigins` key already in the file. + ## Security posture ### Process isolation @@ -84,19 +95,19 @@ is added to it at runtime and written back to the file. See The renderer has no Node.js reachable from it at all, and the IPC surface is a single channel described under [Hub trust](#hub-trust). -| Setting | Value | -| ----------------------------- | ------- | +| Setting | Value | +| ----------------------------- | ------------------------------------------------------------------------- | | `sandbox` | `true` (also `app.enableSandbox()`, which covers renderers created later) | -| `contextIsolation` | `true` | -| `nodeIntegration` | `false` | -| `nodeIntegrationInWorker` | `false` | -| `nodeIntegrationInSubFrames` | `false` | -| `webSecurity` | `true` | -| `allowRunningInsecureContent` | `false` | -| `experimentalFeatures` | `false` | -| `webviewTag` | `false` | -| `navigateOnDragDrop` | `false` | -| `devTools` | only in unpackaged builds | +| `contextIsolation` | `true` | +| `nodeIntegration` | `false` | +| `nodeIntegrationInWorker` | `false` | +| `nodeIntegrationInSubFrames` | `false` | +| `webSecurity` | `true` | +| `allowRunningInsecureContent` | `false` | +| `experimentalFeatures` | `false` | +| `webviewTag` | `false` | +| `navigateOnDragDrop` | `false` | +| `devTools` | only in unpackaged builds | `src/preload/index.ts` exposes two things and nothing else: `__chiaDistribution`, a string the front end reads during the first render to drop web-only @@ -144,17 +155,17 @@ Three of those need explanation: instead of reusing `front-end/public/index.html` — the browser entry point bootstraps through an inline `