diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index af3ad1281..000000000 --- a/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -/.yarn/** linguist-vendored -/.yarn/releases/* binary -/.yarn/plugins/**/* binary -/.pnp.* binary linguist-generated diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89370adf4..8d16b274e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,11 +4,13 @@ on: push: branches: - main + workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: release: + if: github.event_name == 'push' name: Release runs-on: ubuntu-latest permissions: @@ -83,3 +85,117 @@ jobs: pnpm install --frozen-lockfile pnpm exec turbo --filter=@stakekit/widget build npm publish "./$PACKAGE_DIR" + + canary: + if: github.event_name == 'workflow_dispatch' + name: Publish canary + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + id-token: write + concurrency: + group: npm-canary-publish + cancel-in-progress: false + env: + PACKAGE_DIR: packages/widget + + steps: + # actions/checkout@v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.sha }} + fetch-depth: 0 + + - name: Install mise + run: | + curl https://mise.run | MISE_VERSION=v2026.5.6 sh + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH" + mise install + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Read npm release state + id: registry + shell: bash + run: | + package_name=$(node -p "require('./${PACKAGE_DIR}/package.json').name") + latest_version=$(npm view "${package_name}@latest" version) + + { + echo "package_name=${package_name}" + echo "latest_version=${latest_version}" + } >> "$GITHUB_OUTPUT" + + - name: Prepare canary version + id: canary_version + env: + LATEST_VERSION: ${{ steps.registry.outputs.latest_version }} + run: pnpm exec tsx packages/widget/scripts/prepare-canary-release.ts + + - name: Build widget + run: pnpm exec turbo --filter=@stakekit/widget build + + - name: Test widget + run: | + pnpm --filter @stakekit/widget test:unit + pnpm --filter @stakekit/widget test:dom + + - name: Check canary publish status + id: publish + shell: bash + env: + PACKAGE_NAME: ${{ steps.registry.outputs.package_name }} + VERSION: ${{ steps.canary_version.outputs.version }} + run: | + if npm view "${PACKAGE_NAME}@${VERSION}" version >/dev/null 2>&1; then + echo "${PACKAGE_NAME}@${VERSION} is already published; skipping." + echo "should_publish=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "should_publish=true" >> "$GITHUB_OUTPUT" + + - name: Inspect package contents + if: steps.publish.outputs.should_publish == 'true' + run: npm pack "./$PACKAGE_DIR" --dry-run + + - name: Publish canary to npm + if: steps.publish.outputs.should_publish == 'true' + run: npm publish "./$PACKAGE_DIR" --access public --tag canary + + - name: Write release summary + env: + PACKAGE_NAME: ${{ steps.registry.outputs.package_name }} + VERSION: ${{ steps.canary_version.outputs.version }} + SHOULD_PUBLISH: ${{ steps.publish.outputs.should_publish }} + run: | + if [[ "$SHOULD_PUBLISH" == "true" ]]; then + release_status="Published" + else + release_status="Already published" + fi + + { + echo "## Widget canary release" + echo + echo "| Field | Value |" + echo "| --- | --- |" + echo "| Status | ${release_status} |" + echo "| Package | ${PACKAGE_NAME} |" + echo "| Version | ${VERSION} |" + echo "| npm tag | canary |" + echo "| Branch | ${{ github.ref_name }} |" + echo "| Commit | ${{ github.sha }} |" + echo + echo "Install the moving canary channel:" + echo + echo " pnpm add ${PACKAGE_NAME}@canary" + echo + echo "Install this exact build:" + echo + echo " pnpm add ${PACKAGE_NAME}@${VERSION}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 7eec523be..462414e08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,268 +1,50 @@ -# production -/build - -# misc -.DS_Store -*.pem -.idea - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local +# Dependencies and package-manager state +node_modules/ +.pnpm-store/ +*.tgz -# typescript +# Build output and caches +build/ +dist/ +.next/ +out/ +.turbo/ +.cache/ +coverage/ +.nyc_output/ +playwright-report/ +test-results/ +*.lcov *.tsbuildinfo next-env.d.ts +# Environment and local configuration +.env +.env.* +!.env.example +*.local -# compiled output -lib -**/lib - -# OS -.DS_Store - -# Tests -coverage -**/coverage -.nyc_output - -.data -ormconfig.json - -## Terraform -**/.terraform/**/ - -.terraform.lock.hcl -.terraform -# .tfstate files -*.tfstate -*.tfstate.* -*.tfvars - -# Logs -logs +# Logs and diagnostics +logs/ *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) +*-debug.log* report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -*.lcov - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Swap the comments on the following lines if you don't wish to use zero-installs -# Documentation here: https://yarnpkg.com/features/zero-installs -!.yarn/cache -#.pnp.* - -# Yarn Not-Zero-Installs -# https://yarnpkg.com/getting-started/qa/\#which-files-should-be-gitignored -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions -.turbo +# Local vendored reference repositories +@repos/ +# Editors and operating systems .DS_Store - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files +.idea/ .vscode/* !.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln +!.vscode/settings.json *.sw? - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - +# Credentials and certificates *.key *.pem *.p12 *.pfx *.crt -*.cer - -.pnpm-store \ No newline at end of file +*.cer \ No newline at end of file diff --git a/.rev-dep.config.jsonc b/.rev-dep.config.jsonc index 5691d0bec..69494ce0d 100644 --- a/.rev-dep.config.jsonc +++ b/.rev-dep.config.jsonc @@ -1,15 +1,16 @@ { - "$schema": "https://github.com/jayu/rev-dep/blob/master/config-schema/1.7.schema.json?raw=true", - "configVersion": "1.7", + "configVersion": "1.11", + "$schema": "https://raw.githubusercontent.com/jayu/rev-dep/master/config-schema/1.11.schema.json", "rules": [ { "path": "packages/widget", "prodEntryPoints": [ "src/index.package.ts", "src/index.bundle.ts", + "src/public-api/index.package.ts", + "src/public-api/index.bundle.ts", "src/main.tsx", "src/translation/i18next.d.ts", - "src/types/purify-extend.d.ts", "src/types/window.d.ts", "src/vite-env.d.ts", "postcss.config.js", @@ -18,16 +19,443 @@ ], "devEntryPoints": [ "scripts/generate-effect-openapi.ts", - "tests/utils/setup.ts", + "scripts/measure-mount-jank.mts", + "scripts/prepare-canary-release.ts", + "scripts/*.test.ts", + "tests/utils/setup.browser.ts", + "tests/utils/setup.dom.ts", "tests/**/*.test.ts", "tests/**/*.test.tsx" ], - "orphanFilesDetection": { - "enabled": true - }, + "moduleBoundaries": [ + { + "name": "app", + "pattern": "src/app/**", + "allow": [ + "src/app/**", + "src/features/**", + "src/resources/**", + "src/services/**", + "src/domain/**", + "src/shared/**", + "src/public-api/**", + "src/translation/**" + ] + }, + { + "name": "features", + "pattern": "src/features/**", + "allow": [ + "src/features/**", + "src/app/config/**", + "src/app/runtime/**", + "src/resources/**", + "src/services/**", + "src/domain/**", + "src/shared/**", + "src/public-api/**" + ], + "deny": [ + "src/generated/**", + "src/services/api/*-resource-source.ts", + "src/services/api/transport.ts" + ] + }, + { + "name": "resources", + "pattern": "src/resources/**", + "allow": [ + "src/resources/**", + "src/app/runtime/**", + "src/services/**", + "src/domain/**", + "src/shared/**", + "src/public-api/**" + ], + "deny": [ + "src/features/**", + "src/generated/**", + "src/services/api/*-operations.ts", + "src/services/api/transport.ts" + ] + }, + { + "name": "services", + "pattern": "src/services/**", + "allow": [ + "src/services/**", + "src/domain/**", + "src/shared/**", + "src/generated/**", + "src/public-api/**" + ] + }, + { + // `domain` owns framework-independent rules and must not reach into + // `shared`; the documented direction is shared -> domain, not both + // ways. + "name": "domain", + "pattern": "src/domain/**", + "allow": ["src/domain/**", "src/generated/**", "src/public-api/**"] + }, + { + "name": "shared", + "pattern": "src/shared/**", + "allow": ["src/shared/**", "src/domain/**"] + }, + { + "name": "generated", + "pattern": "src/generated/**", + "allow": ["src/generated/**"] + }, + { + "name": "translation", + "pattern": "src/translation/**", + "allow": ["src/translation/**", "src/shared/**"] + }, + { + "name": "public-api", + "pattern": "src/public-api/**", + "allow": ["src/public-api/**"] + } + ], + "restrictedDirectImportersDetection": [ + { + "enabled": true, + "files": ["src/generated/api/**"], + "allowImporters": [ + "src/domain/borrow/**", + "src/domain/schema/**", + "src/services/api/**", + "scripts/**", + "tests/generated/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/yield-resource-source.ts"], + "allowImporters": [ + "src/app/runtime/**", + "src/resources/**", + "src/services/wallet/bootstrap.ts", + "tests/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/legacy-resource-source.ts"], + "allowImporters": [ + "src/app/runtime/**", + "src/resources/**", + "src/services/wallet/bootstrap.ts", + "tests/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/borrow-resource-source.ts"], + "allowImporters": [ + "src/app/runtime/**", + "src/resources/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/yield-operations.ts"], + "allowImporters": [ + "src/app/runtime/**", + "src/features/classic-transaction-flow/state/classic-flow-session-facade.ts", + "src/services/workflow/transaction-workflow-operations-service.ts", + "tests/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/borrow-operations.ts"], + "allowImporters": [ + "src/app/runtime/**", + "src/features/borrow-transaction-flow/state/borrow-flow-session-facade.ts", + "src/services/workflow/transaction-workflow-operations-service.ts", + "tests/**" + ] + }, + { + "enabled": true, + "files": ["src/services/api/transport.ts"], + "allowImporters": [ + "src/app/runtime/app-runtime.ts", + "src/services/api/*-operations.ts", + "src/services/api/*-resource-source.ts", + "tests/**" + ] + }, + // Feature internals are private, root files included. Cross-feature and + // app imports must go through the feature's `state.ts`, `ui.ts`, or + // `components.ts` public entry; those three filenames are the only + // importable modules in a feature and are subtracted from the private + // set below. Generic UI kit components are not feature internals; they + // live in `src/shared/ui/components` and are imported directly. + { + "enabled": true, + "files": [ + "src/features/activity/**", + "!src/features/activity/state.ts", + "!src/features/activity/ui.ts", + "!src/features/activity/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/activity/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/borrow/**", + "!src/features/borrow/state.ts", + "!src/features/borrow/ui.ts", + "!src/features/borrow/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/borrow/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/borrow-transaction-flow/**", + "!src/features/borrow-transaction-flow/state.ts", + "!src/features/borrow-transaction-flow/ui.ts", + "!src/features/borrow-transaction-flow/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/borrow-transaction-flow/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/classic-transaction-flow/**", + "!src/features/classic-transaction-flow/state.ts", + "!src/features/classic-transaction-flow/ui.ts", + "!src/features/classic-transaction-flow/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/classic-transaction-flow/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/earn/**", + "!src/features/earn/state.ts", + "!src/features/earn/ui.ts", + "!src/features/earn/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/earn/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/init-params/**", + "!src/features/init-params/state.ts", + "!src/features/init-params/ui.ts", + "!src/features/init-params/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/init-params/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/mount-animation/**", + "!src/features/mount-animation/state.ts", + "!src/features/mount-animation/ui.ts", + "!src/features/mount-animation/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/mount-animation/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/portfolio/**", + "!src/features/portfolio/state.ts", + "!src/features/portfolio/ui.ts", + "!src/features/portfolio/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/portfolio/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/position-details/**", + "!src/features/position-details/state.ts", + "!src/features/position-details/ui.ts", + "!src/features/position-details/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/position-details/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/preferences/**", + "!src/features/preferences/state.ts", + "!src/features/preferences/ui.ts", + "!src/features/preferences/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/preferences/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/tracking/**", + "!src/features/tracking/state.ts", + "!src/features/tracking/ui.ts", + "!src/features/tracking/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/tracking/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/transaction-workflow/**", + "!src/features/transaction-workflow/state.ts", + "!src/features/transaction-workflow/ui.ts", + "!src/features/transaction-workflow/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/transaction-workflow/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/wallet/**", + "!src/features/wallet/state.ts", + "!src/features/wallet/ui.ts", + "!src/features/wallet/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/wallet/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/widget-shell/**", + "!src/features/widget-shell/state.ts", + "!src/features/widget-shell/ui.ts", + "!src/features/widget-shell/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/widget-shell/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/yield-entry/**", + "!src/features/yield-entry/state.ts", + "!src/features/yield-entry/ui.ts", + "!src/features/yield-entry/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/yield-entry/**", + "tests/**" + ] + }, + { + "enabled": true, + "files": [ + "src/features/yield-summary/**", + "!src/features/yield-summary/state.ts", + "!src/features/yield-summary/ui.ts", + "!src/features/yield-summary/components.ts" + ], + "allowImporters": [ + "**/*.css.ts", + "src/features/yield-summary/**", + "tests/**" + ] + } + ], + "orphanFilesDetection": [ + { + "enabled": true + }, + { + // check for runtime files that are only reachable from tests + "enabled": true, + "validEntryPoints": [ + "src/index.package.ts", + "src/index.bundle.ts", + "src/public-api/index.package.ts", + "src/public-api/index.bundle.ts", + "src/main.tsx", + "src/translation/i18next.d.ts", + "src/types/window.d.ts", + "src/vite-env.d.ts", + "postcss.config.js", + "public/mockServiceWorker.js", + "vite/*.ts", + "scripts/generate-effect-openapi.ts", + "scripts/measure-mount-jank.mts", + "scripts/prepare-canary-release.ts", + "scripts/*.test.ts", + "tests/**/*.ts", + "tests/**/*.tsx" + ], + "graphExclude": [ + "scripts/*.test.ts", + "tests/**/*.ts", + "tests/**/*.tsx" + ] + } + ], "unusedExportsDetection": { - "enabled": true, - "ignoreFiles": ["src/types/yield-api-schema.d.ts"] + "enabled": true }, "unusedNodeModulesDetection": { "enabled": true, @@ -35,10 +463,12 @@ "@effect/openapi-generator", "@effect/platform-node", "babel-plugin-react-compiler", + "jsdom", "playwright", "postcss", "swagger2openapi", "tsx", + "typescript", "yaml" ], "pkgJsonFieldsWithBinaries": ["scripts"] diff --git a/.review/core-app/FIX_PLAN.md b/.review/core-app/FIX_PLAN.md new file mode 100644 index 000000000..2c6c7a73c --- /dev/null +++ b/.review/core-app/FIX_PLAN.md @@ -0,0 +1,43 @@ +# Core application high-risk fix plan + +## Review contract + +- Treat the current application and verified user flows as the behavioral source + of truth. OpenSpec artifacts and existing tests are not specifications. +- Use `main` only as a behavioral comparison for regressions. +- Prove each fix at a user-observable seam before accepting it. +- Keep fix verification and the subsequent discovery review independent. + +## Agreed test seams + +| Finding | Observable seam | Acceptance criterion | Status | +| --- | --- | --- | --- | +| H1 | Host configuration rerender during transaction execution | Replacing only live callback identities preserves the registry, runtime, workflow identity, and exactly-once signing/submission behavior. | Fixed and verified | +| H2 | Leaving or cancelling the classic steps route | Unmounting the workflow route interrupts deferred signing/confirmation and cannot restart it through history navigation. | Fixed and verified | +| H3 | Wallet provider after best-effort initialization failure | Reconnect, mobile fallback, or initial-chain-switch failure preserves configured connectors and permits manual recovery. | Not fixed: atom seams pass; automatic reconnect browser integration fails | +| H4 | Position details across wallet changes | Data and staged actions captured for wallet A are unavailable immediately after switching to wallet B. | Fixed and verified | +| H5 | Resolved force-max Earn form | A force-max yield resolves to the available balance, never the `-1` sentinel, and stays disabled while that balance is unknown. | Fixed and verified | +| H6 | Screens after successful transaction completion | Earn, Activity, and Borrow invalidate and refetch the exact wallet-scoped resources consumed by their visible screens. | Fixed and verified | + +## Execution waves + +1. Independently verify the staged candidate across runtime/wallet, + Earn/Activity, and position/Borrow seams. +2. Patch confirmed gaps one vertical red-green slice at a time. +3. Run focused unit, DOM, and browser tests plus package lint/type checks. +4. Assign fresh agents to complete flow-based review lanes, explicitly searching + for regressions and previously unknown lifecycle/state bugs. + +## Evidence log + +- Lint, Biome, and TypeScript passed. +- Unit: 86 files / 323 tests passed. +- DOM: 20 files / 48 tests passed. +- Focused Chromium: classic workflow 8/8, Borrow execution 6/6, Borrow position + flow 2/2, and dashboard rendering 8/8 passed. +- Full Chromium: 12 files / 55 tests passed; 9 tests failed across the known + Wagmi reconnect regression and full-suite gas, staking, deep-link, and + dashboard timeouts. Dashboard rendering passed in isolation. +- Hygiene checks passed. +- Detailed verdicts, additional findings, and report-only medium issues are in + `REPORT.md`. diff --git a/.review/core-app/REPORT.md b/.review/core-app/REPORT.md new file mode 100644 index 000000000..53375bb93 --- /dev/null +++ b/.review/core-app/REPORT.md @@ -0,0 +1,465 @@ +# Core application logic review report + +## Scope and method + +The current branch was reviewed as a complete application, with equivalent +flows on `main` used as the behavioral regression baseline. Diffs were used only +to locate moved or replaced code. OpenSpec artifacts, commit history, and +existing test assertions were not treated as specifications. + +The review covered application/runtime lifecycle, wallet initialization, +classic and dashboard routing, Earn and stake, portfolio and position details, +activity retry, Borrow, shared transaction execution, atom identity, and +resource invalidation. + +## Executive summary + +The initial review found six high-risk correctness issues and eight medium-risk +flow issues. The dominant failure patterns were: + +1. unrelated configuration identity owns lifecycle-sensitive runtime state; +2. route unmount no longer owns the lifetime of side-effecting workflows; +3. long-lived atoms and refs are not consistently scoped to wallet, route, or + position identity; +4. mutation completion refreshes parallel resource graphs instead of every + resource actually used by the affected screens. + +The high-risk findings were subsequently repaired and independently reviewed. +The round-two results below supersede the original implementation status while +preserving the initial findings as the audit trail. + +## Round-two verification (2026-07-17) + +### Outcome + +Five of the six original high-risk findings are fixed for their reported +failure modes. H3's failure-isolation behavior works at the atom seam, but H3 +is **not fixed** because automatic reconnect no longer completes after the +controller becomes ready. The second flow-based +review found four additional high-risk lifecycle or ownership defects; those +were fixed with regression tests before the scope was closed. No medium-risk +finding was fixed in this review round. + +| Finding | Verdict | Verification evidence | +| --- | --- | --- | +| H1 runtime rebuild on callback rerender | Fixed | Callback-only host rerenders preserve runtime generation and active workflow identity; signing and submission remain exactly once. | +| H2 classic workflow survives route exit | Fixed | The route guard explicitly mounts the TTL-zero workflow atom. Unmount closes the atom scope and eventually interrupts the processor; no separate route-active flag or per-operation ownership checks remain. | +| H3 wallet initialization loses topology | Not fixed | The built Wagmi configuration is exposed and transient enabled-network failures recover, but the isolated Wagmi provider browser test consistently reaches `ready: true` while remaining `connected: false`. Automatic reconnect no longer completes within 10 seconds. Read-only diagnosis confirmed the scoped fiber remains alive until atom disposal; the likely failure class is overlapping reconnect attempts interacting with Wagmi's global reconnect guard, not immediate fiber cancellation. | +| H4 position state crosses wallets | Fixed | Position data, staged action state, validator selection, and position action forms are cleared or remounted when the normalized network/address owner changes. Resource keys continue using the complete, stable wallet scope. | +| H5 force-max resolves to `-1` | Fixed | Force-max forms resolve to the available balance and remain unavailable while that balance is unknown. | +| H6 completion leaves visible resources stale | Fixed | Completion invalidates wallet-scoped Earn balances/positions, Activity, and Borrow resources. Submission invalidation runs immediately after successful broadcast and before confirmation begins. | + +### Architecture alignment follow-up + +- Removed the transaction workflow service's `routeActive` flag. Classic guards + mount the workflow atom directly; its TTL-zero scope owns eventual processor + interruption. +- Removed `WalletIdentityKey`. Ownership comparisons now use + `sameWalletScopeOwner` over normalized network/address, while cache and + invalidation keys retain the complete `WalletScopeKey`. +- Aligned Borrow execution with classic execution. Review stores one execution + input, a selector derives `BorrowTransactionWorkflowKey`, and a lifecycle + guard owns Steps and Complete. Executable router state, the action-form + execution variant, and history tombstones were removed. + +### Additional high-risk defects found and fixed + +1. **Case-sensitive wallet identities were compared as EVM addresses.** The + workflow guard and signing validation lowercased every network address, so + distinct Solana accounts differing by case could be treated as the same + signer. Identity comparison is now network-aware; EVM addresses remain + case-insensitive and non-EVM addresses retain their native casing. +2. **A successful broadcast could miss completion invalidation after route + exit.** Successful submission now records its semantic invalidations before + confirmation begins. Route exit is handled by scope interruption rather than + a separate route-active branch. +3. **Borrow execution could restart through browser history.** Returning Back + from active Borrow steps and then navigating Forward restored executable + history state and recreated the workflow. Execution is now resolved only + from the guarded workflow input/key. Leaving the guard clears that input, so + Forward redirects instead of reconstructing execution. +4. **Borrow position refresh could unmount an active nested execution route.** + Semantic invalidation temporarily made the routed parent render only its + loading/empty pane, removing Steps or Complete. Nested route ownership is + now independent of position loading and empty states, while the position + pane refreshes separately. + +### Remaining findings (report only) + +Per review scope, the following medium-risk findings remain unfixed: + +| ID | Finding | Current round-two status | +| --- | --- | --- | +| M1 | Earn intent survives implicit wallet/token/yield fallback | Still present. | +| M2 | Borrow review state is not scoped to wallet and route market | Confirmed reachable; backend action creation can occur before signing rejects the stale wallet. | +| M3 | Dashboard Activity details retain the previous wallet selection | UI can remain stale; the wallet-scoped execution guard prevents cross-wallet signing. | +| M4 | Position workflow inputs survive route/position changes | Still present. | +| M5 | Successful preview retry requires a second Confirm | Still present. | +| M6 | Dashboard Activity completion route is unregistered | Still present. | +| M7 | Dashboard validator-selection route mounts the wrong owner | Confirmed reachable. | +| M8 | Classic-to-dashboard variant switch can leave an unmatched route | Still present. | +| M9 | Duplicate queued Retry commands can repeat a wallet prompt | Confirmed. Two retries from one failure generation can make the second queued command valid again after the first retry returns to the same phase. Submission and advance commands have the analogous risk. | +| M10 | Permanent enabled-network errors retry every five seconds | The retry is scoped and stops on widget disposal, but errors such as invalid credentials are not classified as terminal and have no jitter. | +| M11 | Withdraw token selection can become stale after same-wallet position refresh | A removed token or reduced balance can remain captured by the local form and be submitted with old arguments. | +| M12 | Borrow form fallback can retarget preserved amounts | If selected market/collateral IDs disappear, fallback selection can change while the entered amount survives. | +| M13 | Dashboard stake-position completion outlet can disappear after an empty refresh | If successful completion removes the final position and the integration is not enterable, the parent can remove the active nested Complete outlet. | + +One lower-confidence race remains for future testing: because wallet +initialization is intentionally non-blocking, a delayed reconnect or initial +switch may overlap a manual connection initiated immediately after the +controller becomes available. No failure was reproduced in this review. + +### Open high-risk regression + +**H3-R1. Automatic wallet reconnect no longer completes after controller +readiness.** The browser contract reaches the authoritative built Wagmi config, +but `useAccount()` remains disconnected. This reproduced both in the complete +Chromium run and in an isolated two-file rerun. A mounted-atom probe confirmed +the `forkScoped` child starts, remains live after the parent publishes Success, +and stops only on registry disposal. The likely failure class is overlapping +background reconnects: Wagmi can return an empty result while a module-global +reconnect guard is active, and this initialization path does not retry that +result. The exact browser ordering still needs instrumentation. Per the +instruction to stop fixing, no production change was made after this regression +was confirmed. + +### Validation + +- Widget lint, Biome, and TypeScript: passed. +- Unit: 86 files, 323 tests passed. +- DOM: 20 files, 48 tests passed. +- Hygiene: dependency, cycle, orphan, boundary, unresolved-import, unused + export, and test-only export checks passed. +- Focused wallet-bootstrap atom verification: passed, including injected scope + cleanup and rejection behavior. Browser integration does not pass automatic + reconnect and therefore overrides the atom-only H3 acceptance verdict. +- Focused classic workflow, semantic invalidation, wallet ownership, Borrow + execution history, and Borrow routed-parent regressions: passed. +- Focused Chromium after the architecture alignment: classic workflow 8/8, + Borrow execution 6/6, Borrow position flow 2/2, and dashboard rendering 8/8 + passed. +- Full Chromium run: 12 files / 55 tests passed; 5 files / 9 tests failed. The + known Wagmi reconnect regression reproduced. Dashboard rendering passed when + isolated; the remaining failures were full-suite gas, staking, and deep-link + timeouts outside the changed seams. + +## High-risk findings + +### H1. Callback-only host rerenders rebuild the runtime and restart active workflows + +**Confidence:** High. **Baseline:** Branch regression. + +A normal host rerender with a newly created `tracking.trackEvent` or +`tracking.trackPageView` function changes `widgetBootstrapConfigAtom`. The +entire `Layer.fresh` application runtime is built from that value, so every +mounted `appRuntime.atom` is restarted, including active transaction machines. +The old machine loses its accumulated submissions and the replacement starts +from the original workflow key in `Signing` or `Confirming`. This can request a +second signature or rebroadcast a transaction already sent by the disposed +machine. + +References: + +- `packages/widget/src/app/config/widget-config.ts:19-22,41-48` +- `packages/widget/src/app/runtime/app-runtime.ts:21-58` +- `packages/widget/src/features/transaction-flow/state/transaction-workflow-atoms.ts:32-62` +- `packages/widget/src/services/workflow/transaction-workflow-service.ts:37-67` +- `packages/widget/src/services/workflow/transaction-workflow-model.ts:332-355` + +Regression test: start a deferred workflow, update only the tracking callback +identity through `widgetConfigAtom`, and assert machine identity, state, and +sign/submit call counts remain unchanged. + +### H2. Leaving the classic steps route does not stop transaction execution + +**Confidence:** High. **Baseline:** Branch regression. + +Cancel only navigates away. The workflow state, completion listener, and machine +atoms retain a five-minute idle TTL, so Effect AtomRegistry delays finalization +and the `forkScoped` processor remains active. A wallet request resolved after +the route is gone can still be submitted, confirmed, and followed by later +transactions and wallet prompts. `main` used a component-owned XState actor; +unmount stopped the actor and explicitly cleared confirmation timeouts. + +References: + +- `packages/widget/src/features/transaction-flow/ui/steps/hooks/use-steps.hook.ts:53-58` +- `packages/widget/src/features/transaction-flow/state/transaction-workflow-atoms.ts:32-105` +- `packages/widget/src/shared/config/widget-defaults.ts:8-10` +- `packages/widget/src/services/workflow/transaction-workflow-service.ts:37-67` +- `packages/widget/src/services/workflow/transaction-workflow-runtime/processor.ts:100-170` + +Regression test: defer signing or confirmation, unmount the steps route, and +assert that the TTL-zero workflow scope is eventually interrupted and cannot be +restarted through Forward navigation. + +### H3. Recoverable wallet initialization failures discard the usable wallet topology + +**Confidence:** High. **Baseline:** Branch regression. + +The branch constructs the Wagmi configuration and then runs reconnect, mobile +fallback connect, and initial chain switching as part of the same failing atom. +If any best-effort operation rejects, `walletControllerAtom` fails and +`WagmiConfigProvider` replaces the already-built configuration with the empty +fallback configuration. The widget then has no usable connectors or recovery +path until remount or a topology-changing config update. `main` retained the +configuration when these initialization attempts failed. + +References: + +- `packages/widget/src/features/wallet/wagmi/initialization.ts:123-171` +- `packages/widget/src/features/wallet/wagmi/controller.ts:42-76` +- `packages/widget/src/features/wallet/runtime/root-atom.ts:117-131` +- `packages/widget/src/features/wallet/react/provider.tsx:6-14` +- `packages/widget/src/services/wallet/default-wagmi-config.ts:17-25` + +Regression test: reject `switchChain` after configuration construction and +assert the configured connector list is still exposed and manual connect works. + +### H4. Dashboard position state can cross wallet identities + +**Confidence:** High. **Baseline:** Invariant defect also present on `main`. + +Dashboard position details retain the last non-null position balances in refs. +When wallet A changes to wallet B and B has no matching position, the new null +result is ignored and A's position remains rendered indefinitely. Exit and +pending-action builders then combine A's retained balances and pending actions +with B's current address. The request can pass signing validation because both +the newly created action and current wallet use B, even though its values came +from A. + +References: + +- `packages/widget/src/features/position-details/ui/classic/state/index.tsx:59-120` +- `packages/widget/src/features/position-details/ui/classic/hooks/use-stake-exit-request-dto.ts:20-102` +- `packages/widget/src/features/position-details/ui/classic/hooks/use-pending-actions.ts:146-185` +- `packages/widget/src/app/routes/dashboard-routes.tsx:125-183` + +Regression test: render A's position, switch to B with an empty positions +response, and require the position/action panel to clear before any request can +be created. + +### H5. Force-max Earn integrations initialize the amount to `-1` + +**Confidence:** High. **Baseline:** Branch regression. + +The force-max contract is represented by `minimum === -1 && maximum === -1`. +The current form resolver uses the raw minimum as its default amount, producing +`-1`, while validation independently maps the minimum and maximum to the wallet +balance. The flow is invalid until the user explicitly presses Max, and can be +blocked where that action is unavailable. `main` initialized force-max forms to +the available balance. + +References: + +- `packages/widget/src/domain/types/stake.ts:69-105` +- `packages/widget/src/features/earn/react/use-max-min-yield-amount.ts:28-64` +- `packages/widget/src/features/earn/state/atoms-state/resolver/form.ts:46-60` +- `packages/widget/src/features/earn/ui/classic/earn-page/state/earn-page-model.tsx:534-570` + +Regression test: resolve a force-max yield with available balance `10` and +assert `view.form.stakeAmount === "10"`, not `"-1"`. + +### H6. Successful mutations leave the resources used by affected screens stale + +**Confidence:** High. **Baseline:** Mixed branch regressions and new invariant +defects. + +The completion handlers refresh parallel or list resources, but omit several +feature-owned resources used by the visible screens: + +- Earn completion refreshes portfolio balance scans, but Earn derives its form + from separate private token-balance and catalog-position atoms with five-minute + SWR. A quick return can show pre-stake balance and minimum values. +- Activity pages and filter counts are never refreshed after completion. + Remounting their `Atom.pull` within the five-minute idle TTL reuses the old + accumulated pages without another API call. +- Borrow completion refreshes `borrowPositionsAtom`, but the routed position + page reads the distinct `borrowPositionAtom`. The parent details route remains + mounted through review, steps, and completion, so returning from a successful + repay/withdraw/toggle continues to show the old position and pending actions. + +References: + +- `packages/widget/src/features/transaction-flow/state/transaction-workflow-atoms.ts:15-30,80-96` +- `packages/widget/src/features/earn/state/atoms-state/catalog/atoms.ts:55-59,172-190,367-386,520-534` +- `packages/widget/src/features/activity/react/use-activity-actions.ts:45-107,282-300` +- `packages/widget/src/features/borrow/atoms/refresh.ts:49-83` +- `packages/widget/src/features/borrow/atoms/resources.ts:126-168` +- `packages/widget/src/features/borrow/ui/use-borrow-positions.ts:38-54` +- `packages/widget/src/features/borrow/ui/position-details.tsx:1178-1299` + +Regression tests should drive each mutation to completion and require the exact +resources consumed by Earn, Activity, and Borrow position details to issue a +new request and display the new state. + +## Medium-risk findings + +### M1. Earn form intent survives implicit wallet/token/yield scope changes + +**Confidence:** High. **Baseline:** Branch regression. + +Network change or disconnect/reconnect can make token and yield resolution fall +back to a new scope while retaining the previous amount, provider ID, Tron +resource, and max-amount flag. Explicit token/yield actions reset these fields, +but resolver-driven selection changes do not. A request for the new yield can +therefore include a provider belonging to the previous yield. + +References: + +- `packages/widget/src/features/earn/state/atoms-state/machine/atoms.ts:16-60` +- `packages/widget/src/features/earn/state/atoms-state/machine/reducer.ts:6-49` +- `packages/widget/src/features/earn/state/atoms-state/resolver/token.ts:16-60` +- `packages/widget/src/features/earn/state/atoms-state/resolver/yield.ts:53-105` +- `packages/widget/src/features/earn/state/atoms-state/resolver/form.ts:18-27,55-59` +- `packages/widget/src/features/earn/ui/classic/earn-page/state/use-stake-enter-request-dto.ts:96-110` + +### M2. Borrow review/execution state is not scoped to the current wallet or route + +**Confidence:** High. **Baseline:** New invariant defect. + +`borrowActionFormAtom` is a single registry-wide state value. The Borrow route +guard verifies only that some supported wallet is connected, and review prefers +history state or the global staged state without comparing its address/network +or market with the current wallet and route. Switching from wallet A to B on +Review lets Confirm call the Borrow API with A's staged request; wallet mismatch +is detected only later during transaction signing, after the backend action has +already been created. A stale market review can likewise be rendered under a +different position route. + +References: + +- `packages/widget/src/features/borrow/atoms/action-form.ts:107-140` +- `packages/widget/src/features/borrow/ui/connected-wallet.tsx:10-17` +- `packages/widget/src/features/borrow/ui/review.tsx:52-70,130-143` +- `packages/widget/src/services/workflow/transaction-workflow-runtime/signing.ts:28-73` + +### M3. Dashboard activity retry remains bound to the previous wallet + +**Confidence:** High. **Baseline:** Invariant defect also present on `main`. + +Selecting activity writes a keep-alive selection and unmounts the Activity page +that owns wallet-change cleanup. Account/network changes while details are open +therefore leave the old action retryable through the new `WalletService`. +Signing normally fails late on identity validation rather than clearing the +invalid flow immediately. + +References: + +- `packages/widget/src/features/activity/state/selection.ts:24-55` +- `packages/widget/src/features/activity/ui/dashboard/activity/index.tsx:20-54` +- `packages/widget/src/features/activity/ui/dashboard/activity/activity.page.tsx:93-108` + +### M4. Position workflow inputs leak across route unmounts and positions + +**Confidence:** High. **Baseline:** Branch regression. + +Unstake amount, max-amount selection, and pending-action input live in one +keep-alive `positionDetailsWorkflowAtom`. A newly mounted hook initializes its +`previousWorkflowKey` to the new current key, so its reset effect sees no +transition and preserves state inherited from the previously unmounted +position. `main` used provider-local reducer state that was discarded on +unmount. + +References: + +- `packages/widget/src/features/position-details/state/workflow.ts:35-40,75-78` +- `packages/widget/src/features/position-details/ui/classic/state/index.tsx:238-247` + +### M5. A successful stake-preview retry requires a second Confirm click + +**Confidence:** High. **Baseline:** Branch regression. + +When initial preview fails, Confirm calls `refetch()` and returns. A successful +refetch has no continuation that navigates to steps; the user must click Confirm +again. `main` awaited the retry and continued on the same click. + +References: + +- `packages/widget/src/features/transaction-flow/ui/review/hooks/use-stake-review.hook.ts:162-203` + +### M6. Dashboard activity completion navigates to an unregistered route + +**Confidence:** High. **Baseline:** Invariant defect also present on `main`. + +Successful retry steps navigate relatively to +`/activity/:pendingActionType/complete`, but dashboard routes register only the +corresponding `steps` child. The details shell remains with an empty outlet +instead of showing completion. + +References: + +- `packages/widget/src/features/transaction-flow/ui/review/hooks/use-action-review.hook.ts:90-124` +- `packages/widget/src/features/transaction-flow/ui/steps/hooks/use-steps.hook.ts:33-51` +- `packages/widget/src/app/routes/dashboard-routes.tsx:188-203` + +### M7. The dashboard validator-selection child route mounts the wrong page + +**Confidence:** High. **Baseline:** Invariant defect also present on `main`. + +The outer position route already renders `DashboardPositionDetailsPage`, but +its validator-selection child renders a second full +`DashboardPositionDetailsPage`. The component that owns and renders the +`SelectValidator` modal is absent, so the registered route cannot present the +selection UI or continue the pending action. + +References: + +- `packages/widget/src/app/routes/dashboard-routes.tsx:125-151` +- `packages/widget/src/features/position-details/ui/dashboard/index.tsx:54-104` +- `packages/widget/src/features/position-details/ui/classic/hooks/use-pending-actions.ts:120-144` +- `packages/widget/src/features/position-details/ui/dashboard/components/position-details-actions.tsx:225-260` + +### M8. Switching classic Positions to dashboard through rerender blanks the widget + +**Confidence:** High. **Baseline:** Invariant defect also present on `main`. + +The memory router survives supported prop rerenders. If classic is at +`/positions` and `dashboardVariant` becomes true, dashboard routes have neither +a `/positions` route nor a catch-all redirect, so no shell or page matches. + +References: + +- `packages/widget/src/App.tsx:25-31,41-64,68-97` +- `packages/widget/src/app/routes/dashboard-routes.tsx:95-210` + +## Hardening issue not promoted to the main list + +Unstake and pending-action review hooks dereference nullable request atoms with +non-null assertions, while their review routes are outside workflow-state +guards. A custom/direct memory-router entry can crash. The public widget creates +its own memory router at `/`, so a fresh-registry direct review entry was not +confirmed through the supported external API. The routes should still guard +ephemeral request state for consistency with steps and completion. + +References: + +- `packages/widget/src/app/routes/classic-routes.tsx:179-209` +- `packages/widget/src/app/routes/dashboard-routes.tsx:153-180` +- `packages/widget/src/features/transaction-flow/ui/review/hooks/use-unstake-review.hook.ts:33-45` +- `packages/widget/src/features/transaction-flow/ui/review/hooks/use-pending-review.hook.ts:25-48` + +## Validation performed + +- TypeScript no-emit check passed. +- Focused unit suites passed: 13 files, 48 tests covering transaction workflow + model/runtime/atoms and Borrow domain/atoms. +- Focused browser suites passed: 3 files, 8 tests covering classic transaction + execution, Borrow execution, and Borrow position details. +- The focused wallet-atom unit suite passed. + +These green suites do not exercise the state-transition sequences above; each +finding includes the missing regression scenario that should be added before or +with its fix. + +## Recommended fix order + +1. Decouple runtime identity from dynamic tracking callbacks and make workflow + ownership explicit. +2. Stop side-effecting workflow processors immediately when their route owner + exits. +3. Preserve usable wallet topology across initialization failures. +4. Introduce wallet/route/position-scoped reset rules for long-lived flow state. +5. Centralize mutation-to-resource invalidation and refresh every resource + actually consumed by affected screens. +6. Fix the force-max resolver and the isolated routing/retry defects. diff --git a/.review/core-app/REVIEW_PLAN.md b/.review/core-app/REVIEW_PLAN.md new file mode 100644 index 000000000..365be2177 --- /dev/null +++ b/.review/core-app/REVIEW_PLAN.md @@ -0,0 +1,118 @@ +# Core application logic review + +## Goal + +Review the current branch as a coherent application and identify concrete logic, +state-management, lifecycle, and flow bugs. The review is primarily behavioral: +understand how each end-to-end flow works on `main`, understand how the same flow +works on the current branch, and compare their observable behavior and required +state invariants. + +This is not a review of individual commits, migration steps, naming, formatting, +or architectural taste. Architectural concerns are findings only when they create +a concrete correctness, lifecycle, isolation, or maintainability defect with a +realistic failure sequence. + +## Sources of truth + +1. The complete implementation and observable behavior on `main` is the + regression baseline. +2. The complete implementation on the current branch is the review target. +3. Public package behavior and integration examples may clarify externally + observable contracts when the two implementations are ambiguous. + +Do not use OpenSpec artifacts or commit history to establish intended behavior. +Existing tests are incomplete and may be stale. They may be used to learn how to +exercise a flow, detect regressions, or verify a finding, but their assertions +are not authoritative specifications. + +Use `git diff` only as a navigation aid for locating moved, removed, or replaced +code. Do not review the branch commit-by-commit or treat each changed hunk as the +unit of review. + +`main` is a regression baseline, not proof that behavior is correct. Report a +current-branch logic bug even if there is no direct divergence from `main`, but +clearly distinguish an invariant-based defect from a demonstrated regression. + +## Review method + +For each owned flow: + +1. Map the entry points, routes, state owners, service boundaries, persistence, + and external inputs on `main` and on the current branch. +2. Trace the happy path end to end on both implementations. +3. Trace disruptive transitions and boundary conditions: + - wallet connection and disconnection; + - account, address, or chain changes; + - route exit, back navigation, deep links, and direct route entry; + - host configuration and initialization changes; + - overlapping requests and out-of-order async completion; + - transaction rejection, cancellation, retry, partial completion, and resume; + - component unmount and widget remount; + - resource invalidation and refresh after successful mutations; + - stale selection, workflow, or cached state crossing between identities. +4. State the behavioral invariants that should remain true and verify whether + the current implementation preserves them. +5. Follow state through UI adapters, atoms, services, workflow execution, and + resource refresh. Do not stop at a single folder boundary. +6. Reproduce credible issues with a focused test, command, or fully specified + execution sequence where practical. + +The initial review pass is read-only. Do not modify production code or tests. + +## Review lanes + +### Application lifecycle and navigation + +Runtime creation and disposal, provider composition, configuration and root +inputs, routing, route guards, initial tabs, deep links, wallet/account/chain +transitions, and widget unmount/remount behavior. + +### Earn and stake + +Earn catalog and selection resolution, token/yield/validator state, form and +amount validation, stake request creation, review, execution steps, completion, +retry/cancellation, and post-transaction refresh. + +### Portfolio, position details, and activity + +Portfolio resources and summaries, position selection/details, unstake and +pending-action creation, activity selection and reconstruction, execution, +navigation, and resource invalidation after mutations. + +### Borrow and horizontal state architecture + +Borrow form, market/position identity, wallet projection, action creation, +review/execution/completion, resource refresh, plus shared atom identity and +keys, service lifetimes, workflow isolation, cancellation, and transaction +runtime behavior used across features. + +Reviewers own their end-to-end lane but should inspect shared dependencies when +needed. Cross-lane concerns should be reported rather than silently assumed to +belong to another reviewer. + +## Finding standard + +Only report actionable correctness findings. Each finding must include: + +- severity and confidence; +- user-visible or integration impact; +- preconditions; +- the exact state/event sequence that triggers the issue; +- expected behavior, identified as either `main` behavior or an explicit + correctness invariant; +- current-branch behavior; +- precise file and line references; +- reproduction evidence or a concrete proposed regression test; +- why the behavior is not merely an intentional implementation difference. + +Keep unverified suspicions, ambiguities, and follow-up questions separate from +confirmed findings. Do not inflate the report with style feedback, broad praise, +or descriptions of code that is working correctly. + +## Coordination + +Each reviewer returns findings independently to the coordinating reviewer and +does not edit a shared findings file. The coordinator deduplicates cross-lane +findings, validates high-severity claims, and produces the final prioritized +report. No fixes are made until the review report is agreed upon. diff --git a/.scratch/atom-owned-feature-facades/issues/01-runtime-navigation-pending-actions.md b/.scratch/atom-owned-feature-facades/issues/01-runtime-navigation-pending-actions.md new file mode 100644 index 000000000..2fad64d60 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/01-runtime-navigation-pending-actions.md @@ -0,0 +1,12 @@ +# 01 — Add runtime navigation through pending-action deep links + +**What to build:** Application-owned commands can navigate through the Widget Instance's memory router without publishing a navigation outcome for React to consume. Pending-action deep links are the first complete journey to use that capability. + +**Blocked by:** None — can start immediately. + +**Status:** complete + +- [x] A runtime-scoped navigation capability supports canonical absolute push, replace, back, and scroll-reset decisions. +- [x] Pending-action deep-link commands navigate directly and no React effect coordinates their delivery. +- [x] Production and in-memory test adapters demonstrate isolation between sequential Widget Instances. +- [x] Declarative route guards and view-local navigation remain React-owned. diff --git a/.scratch/atom-owned-feature-facades/issues/02-flow-navigation.md b/.scratch/atom-owned-feature-facades/issues/02-flow-navigation.md new file mode 100644 index 000000000..990e2fa04 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/02-flow-navigation.md @@ -0,0 +1,12 @@ +# 02 — Move Classic and Borrow workflow navigation into atoms + +**What to build:** Classic and Borrow Transaction Flow commands navigate directly after their ownership and stale-result checks, so Review, Steps, and Complete transitions no longer depend on React navigation outcomes. + +**Blocked by:** 01 — Add runtime navigation through pending-action deep links. + +**Status:** complete + +- [x] Classic Transaction Flow transitions navigate through the runtime capability. +- [x] Borrow Transaction Flow transitions navigate through the runtime capability. +- [x] React navigation outcome effects and rendering adapters are removed. +- [x] Flow Session and Execution Attempt lifecycle guarantees remain covered by registry-level tests. diff --git a/.scratch/atom-owned-feature-facades/issues/03-yield-summary.md b/.scratch/atom-owned-feature-facades/issues/03-yield-summary.md new file mode 100644 index 000000000..78c64155f --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/03-yield-summary.md @@ -0,0 +1,12 @@ +# 03 — Introduce Yield Summary across read-only journeys + +**What to build:** Users see the same provider, reward-token, and semantic yield information across activity, portfolio, Review, and Complete, supplied by one shared Yield Summary capability with normalized loading and failure states. + +**Blocked by:** None — can start immediately. + +**Status:** complete + +- [x] Yield Summary exposes stable read-only view Atoms without nested Atoms, factories, or callbacks. +- [x] Read-only activity, portfolio, Review, and Complete consumers use Yield Summary. +- [x] Loading, unavailable data, and typed failures have consistent projections. +- [x] Existing visible behavior and copy remain unchanged. diff --git a/.scratch/atom-owned-feature-facades/issues/04-yield-entry-position-details.md b/.scratch/atom-owned-feature-facades/issues/04-yield-entry-position-details.md new file mode 100644 index 000000000..453ccd08c --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/04-yield-entry-position-details.md @@ -0,0 +1,13 @@ +# 04 — Introduce Yield Entry through position details + +**What to build:** A user entering or exiting a position receives shared amount constraints, validation, KYC, rewards, Action Command preparation, submission, tracking, flow start, and navigation behavior owned outside React. + +**Blocked by:** 01 — Add runtime navigation through pending-action deep links; 03 — Introduce Yield Summary across read-only journeys. + +**Status:** complete + +- [x] Position details consumes one Yield Entry facade for its complete entry journey. +- [x] React event handlers only normalize events and dispatch synchronous commands. +- [x] RainbowKit modal commands are installed through one runtime-scoped boundary adapter. +- [x] The module-global Ledger modal callback and callback-bearing Ledger command are removed. +- [x] Yield Entry behavior is covered at its public facade seam. diff --git a/.scratch/atom-owned-feature-facades/issues/05-earn-selection-facades.md b/.scratch/atom-owned-feature-facades/issues/05-earn-selection-facades.md new file mode 100644 index 000000000..69f619555 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/05-earn-selection-facades.md @@ -0,0 +1,12 @@ +# 05 — Replace the Earn browsing bridge with capability facades + +**What to build:** Users can select tokens, yields, and validators and can search, filter, paginate, and retry through stable Earn capability facades instead of an aggregate React-owned page model. + +**Blocked by:** None — can start immediately. + +**Status:** complete + +- [x] Token, yield, and validator views publish plain values through stable read-only Atoms. +- [x] Search, debounce, filtering, pagination, retry routing, and selection are stable command Atoms. +- [x] Classic and dashboard browsing surfaces consume the capability facades. +- [x] Earn Selection remains the sole source of selection, readiness, and intent decisions. diff --git a/.scratch/atom-owned-feature-facades/issues/06-earn-yield-entry.md b/.scratch/atom-owned-feature-facades/issues/06-earn-yield-entry.md new file mode 100644 index 000000000..eeca72df8 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/06-earn-yield-entry.md @@ -0,0 +1,12 @@ +# 06 — Complete Earn Yield Entry and remove the aggregate page model + +**What to build:** Both Earn variants use Yield Entry and Yield Summary for amount, quote, readiness, CTA, failures, and submission, eliminating the Atom-to-React page-model bridge. + +**Blocked by:** 04 — Introduce Yield Entry through position details; 05 — Replace the Earn browsing bridge with capability facades. + +**Status:** complete + +- [x] Classic and dashboard Earn entry surfaces consume stable feature facades. +- [x] Published view values contain no nested Atoms, Atom factories, or command callbacks. +- [x] The aggregate Earn page model, binding Atom, and bridge hook are deleted. +- [x] Earn initialization, retry, KYC, tracking, and transaction-start behavior remain unchanged. diff --git a/.scratch/atom-owned-feature-facades/issues/07-contract-and-verify.md b/.scratch/atom-owned-feature-facades/issues/07-contract-and-verify.md new file mode 100644 index 000000000..94d2f6e81 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/07-contract-and-verify.md @@ -0,0 +1,12 @@ +# 07 — Contract legacy hooks and verify the final architecture + +**What to build:** Every migrated journey uses the new facades directly, obsolete compatibility surfaces are removed, and architecture checks prove that React remains a thin view layer. + +**Blocked by:** 02 — Move Classic and Borrow workflow navigation into atoms; 06 — Complete Earn Yield Entry and remove the aggregate page model. + +**Status:** complete + +- [x] Remaining legacy consumers migrate and obsolete hooks and compatibility adapters are deleted. +- [x] Application-logic modules do not import React and touched view adapters do not synchronize domain state with effects. +- [x] Static checks reject nested public Atoms and React-owned application navigation bridges. +- [x] Focused seam tests, lint and typechecking, and the complete suite pass without public API or copy changes. diff --git a/.scratch/atom-owned-feature-facades/issues/08-application-runtime-router.md b/.scratch/atom-owned-feature-facades/issues/08-application-runtime-router.md new file mode 100644 index 000000000..9ca311975 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/issues/08-application-runtime-router.md @@ -0,0 +1,15 @@ +# 08 — Move memory-router ownership into the Application Runtime Generation + +**What to build:** A scoped synchronous Application Router runtime owns and disposes the memory router, exposes it only to the React composition boundary, and supplies its Effect Context to application navigation without a React or adapter bridge. + +**Blocked by:** 07 — Contract legacy hooks and verify the final architecture. + +**Status:** complete + +- [x] The first Application Router Atom read synchronously returns the existing memory-router type without a loading or fallback state. +- [x] `WidgetNavigation` is constructed directly from `ApplicationRouter` and preserves canonical paths, history operations, typed failures, and scroll-reset policy. +- [x] React only reads the runtime-owned router and passes it to the DOM `RouterProvider`. +- [x] Live settings changes preserve router identity while API-identity replacement and sequential remount receive fresh memory history. +- [x] Closing the Application Runtime Generation disposes the router exactly once. +- [x] The navigation adapter type, adapter Atom, provider input, React forwarding object, and adapter-specific test utilities are removed. +- [x] Classic and Dashboard route definitions remain declarative, and the router remains a memory router. diff --git a/.scratch/atom-owned-feature-facades/spec.md b/.scratch/atom-owned-feature-facades/spec.md new file mode 100644 index 000000000..4e17f25c2 --- /dev/null +++ b/.scratch/atom-owned-feature-facades/spec.md @@ -0,0 +1,247 @@ +# Atom-owned feature facades, Yield Entry, and application navigation + +Status: implemented + +## Problem Statement + +The Earn page currently concentrates a large amount of view-model and workflow logic in one React-owned model. That model subscribes to state, performs filtering, grouping, formatting, validation, request construction, pagination routing, CTA decisions, navigation, tracking, and third-party integration, then republishes the aggregate through a layout-effect-backed Atom for descendant consumers. Some values published by the underlying Earn state are themselves Atom identities or factories, so React must read one Atom to discover and subscribe to another. This Atom-to-React-to-Atom binding obscures ownership, causes broad subscriptions, and creates a second presentation-owned state machine alongside the existing Earn machine. + +The same deterministic calculations and resource projections are packaged as React hooks and imported by position details, transaction review and completion, activity, and portfolio. Those consumers therefore depend backwards on Earn's React layer, and fixes to amount constraints, provider details, reward projections, KYC state, yield type, or Action Command construction remain distributed across hook call sites. + +Application-owned navigation has a similar bridge. Classic and Borrow Transaction Flows and pending-action deep links publish Atom outcomes that React route adapters observe and convert into React Router navigation. This adds outcome-delivery state and lifecycle coordination even though the Widget Instance already owns an explicit memory router. Submission handlers in Earn and position details also combine workflow decisions, session start, analytics, and navigation inside React. + +After moving navigation decisions into `WidgetNavigation`, router construction still crosses the boundary in the opposite direction. React creates and retains the memory router, wraps it in a forwarding adapter, seeds that adapter into the Atom registry, and the application runtime reads it back to construct its navigation Layer. This models a construction-time dependency as mutable reactive input, leaves router disposal implicit, and conflicts with React Router guidance that data routers be created outside the React tree. + +The result is difficult to test at a stable interface, hard to navigate for maintainers and agents, and inconsistent with the established rule that Effect and Effect Atom own application logic. The architecture needs stable feature facades, shared Yield Entry and yield-summary capabilities, and a headless navigation seam without changing visible product behavior or public embedding interfaces. + +## Solution + +Replace callback-rich aggregate React models with capability-oriented feature facades. Each facade exposes stable read-only view Atoms and writable command Atoms while keeping mutable state, dynamic Authoritative Resource identities, pagination, retry routing, and command implementations private. React reads the smallest relevant capability and synchronously dispatches user intent. Public view values never contain nested Atoms, Atom factories, or callback functions. + +Keep the existing Earn machine as the sole source of truth for Earn Selection, readiness, failure, and user intent. Build the Earn page facade as derived capabilities for token options, yield options, validators, amount and quote, rewards and providers, submission, validation, CTA state, and page failure. Move search normalization, filtering, grouping, validator request debounce, dynamic pagination routing, and loading projections into Atom state. Leave translation and genuinely local presentation state in React. + +Introduce two cross-feature deep modules. `YieldSummary` owns read-only semantic projections such as provider details, reward-token details, yield type, and their normalized resource state. `YieldEntry` owns the pre-execution attempt to add tokens to an Earn Selection: amount constraints, validation, KYC projection, estimated rewards, Enter Action Command construction, submission eligibility, CTA decisions, command effects, analytics, transaction-session start, and navigation. Each feature supplies an input Atom and consumes the module's stable view and command interface, preserving the feature's own route or session lifetime. + +Migrate every consumer of the legacy shared React hooks. Follow any required dependency upstream until it reaches an immutable route/session input, an existing feature Atom, or an Authoritative Resource; do not publish changing hook results into writable Atoms as a replacement bridge. Retain unrelated logic in surrounding features, but delete the migrated hooks and the Earn aggregate model once no callers remain. + +Add `WidgetNavigation` to the application runtime. A separate synchronous base runtime constructs a scoped `ApplicationRouter` Layer around the existing memory router and provides its Effect Context to the application runtime, which constructs `WidgetNavigation` directly from that service. An internal Atom synchronously exposes the runtime-owned router only to the React composition boundary for `RouterProvider`. The router is disposed with its Application Runtime Generation, and a new API identity receives fresh memory history. Workflow commands and transition events execute canonical absolute destinations through `WidgetNavigation` after their ownership and stale-result checks. Declarative route guards, route reads used only for presentation, and view-local tabs, breadcrumbs, and back controls remain React concerns. + +Keep one named React-only integration seam for RainbowKit modal commands. A provider adapter installs connect- and chain-modal commands into a runtime-scoped `WalletModal` interface and releases them with the provider lifetime. Feature state and commands do not carry modal callbacks. Remove the existing module-global latest-chain-modal callback mechanism. + +The completed migration is behavior-preserving. Classic and dashboard Earn, position details, transaction flows, activity, portfolio, deep links, loading, pagination, retry, KYC, analytics, and routing retain their visible semantics. The final tree contains no compatibility aggregate page-model adapter and no dual ownership of the migrated behavior. + +## User Stories + +1. As a widget user, I want the Earn page to preserve its current selections and interactions, so an internal architecture change does not alter how I stake. +2. As a widget user, I want classic and dashboard Earn variants to show the same token, yield, validator, amount, and provider information as before, so the two experiences remain consistent. +3. As a widget user, I want token searching to filter the available token options correctly, so I can quickly find the asset I intend to use. +4. As a widget user, I want yield searching, ranking, grouping, and category filtering to remain correct, so I can find an appropriate yield. +5. As a widget user, I want validator search requests to wait for the established debounce interval, so typing does not issue a request for every keystroke. +6. As a widget user, I want the UI to show that validator search is still debouncing, so loading feedback remains accurate. +7. As a widget user, I want obsolete validator-search results ignored after I change the query, so stale results cannot replace the active search. +8. As a widget user, I want loading more tokens to continue from the active token resource, so pagination remains coherent after selection or wallet changes. +9. As a widget user, I want loading more validators to target the active yield and normalized search, so pages cannot be appended to the wrong list. +10. As a widget user, I want accumulated pagination results preserved during later-page loading and retry, so lists do not disappear unnecessarily. +11. As a widget user, I want resource failures represented consistently, so I can distinguish loading, ready, empty, and recoverable failure states. +12. As a widget user, I want Retry to refresh the resource responsible for the current failure, so recovery does not reset unrelated state. +13. As a widget user, I want later refresh failures to retain usable prior data, so an auxiliary failure does not replace a coherent page with a blocking error. +14. As a widget user, I want my available amount, minimum, maximum, and force-max behavior to remain unchanged, so amount entry remains trustworthy. +15. As a widget user, I want amount validation to use one definition across Earn and position details, so the same Yield Entry is never accepted in one place and rejected in another. +16. As a widget user, I want provider selection and provider details to remain accurate, so the prepared Action Command uses the provider I selected. +17. As a widget user, I want reward estimates and reward-token details to remain accurate across Earn, position details, and review, so I can understand the expected result. +18. As a widget user, I want validator selection requirements to remain accurate, so a Yield Entry cannot proceed with missing or invalid validators. +19. As a widget user, I want KYC status and refresh behavior to remain consistent across Earn and position details, so eligibility is checked in the same way. +20. As a widget user, I want a blocked KYC gate to prevent submission without losing my form state, so I can resume after verification. +21. As a widget user, I want the generated Enter Action Command to retain address, token, validator, provider, subnet, Tron resource, Ledger, and max-amount arguments, so execution remains correct. +22. As a disconnected widget user, I want the Earn CTA to open the wallet connection modal, so I can connect and continue. +23. As a Ledger Live user, I want the add-account CTA to request and switch the account and close the chain modal, so Ledger account setup continues to work. +24. As a connected widget user, I want submitting a valid Yield Entry to start one fresh Classic Transaction Flow and navigate to Review, so the journey begins exactly once. +25. As a widget user, I want invalid submission to mark validation state without starting a transaction or navigating, so errors are visible and safe. +26. As a widget user, I want deep-link initialization to preserve its current readiness and selection behavior, so external links still land on the intended journey. +27. As a widget user, I want pending-action deep links to navigate only after their existing readiness conditions settle, so routes do not advance prematurely. +28. As a widget user, I want Classic Transaction Flow Continue, Back, and completion navigation to retain their existing history behavior, so Review, Steps, and Complete remain coherent. +29. As a widget user, I want Borrow Transaction Flow navigation to retain its existing session and completion behavior, so browser history cannot revive a disposed execution. +30. As a widget user, I want declarative route guards to keep redirecting invalid URLs safely, so missing or stale sessions do not render protected pages. +31. As a widget user, I want view-local tabs and back controls to behave as before, so ordinary interface navigation is not changed by workflow refactoring. +32. As a widget user, I want configured scroll-to-top behavior preserved on navigation that currently requests it, so page transitions retain their established presentation. +33. As a widget user, I want browser Back and replacement navigation to preserve their current semantics, so the history stack remains predictable. +34. As a widget user, I want stale Flow Sessions or Execution Attempts unable to navigate after replacement, so an obsolete workflow cannot take over the current screen. +35. As a widget user, I want analytics events for Max, Connect, Ledger account setup, submission, and workflow transitions to remain accurate, so product telemetry is not lost. +36. As a widget host, I want the React component export to remain compatible, so adopting this internal refactor requires no host changes. +37. As a widget host, I want the bundled renderer and its unmount/remount behavior to remain compatible, so imperative and CDN integrations continue working. +38. As a widget host, I want one memory router scoped to each Application Runtime Generation, so state from a prior API identity or sequential mount cannot leak into a later generation. +39. As a feature developer, I want stable capability view Atoms, so components subscribe only to the state they actually render. +40. As a feature developer, I want separate typed command Atoms, so UI events dispatch intent without carrying implementation callbacks. +41. As a feature developer, I want dynamic resource identity hidden inside the facade, so React never has to read one Atom to discover another Atom. +42. As a feature developer, I want pagination and retry exposed as semantic commands, so callers do not know which resource Atom is active. +43. As a feature developer, I want deterministic calculations implemented as pure TypeScript, so they are reusable and testable without ceremonial Atom wrappers. +44. As a feature developer, I want translation performed from semantic data in React, so application-logic modules remain React-free. +45. As a feature developer, I want local focus, disclosure, hover, animation, and element-reference state to remain in React, so the facade does not absorb presentation-only concerns. +46. As a feature developer, I want one `YieldSummary` interface for provider and reward projections, so activity, portfolio, review, completion, Earn, and position details stop importing Earn hooks. +47. As a feature developer, I want one `YieldEntry` interface for entry preparation and submission, so Earn and position details share the same behavior without depending on one another. +48. As a feature developer, I want each feature to supply its own input Atom to shared modules, so route, item, and Flow Session lifetimes remain owned by the caller. +49. As a feature developer, I want shared modules to consume Authoritative Resources rather than hook-owned fetches, so canonical remote read ownership remains intact. +50. As a feature developer, I want explicit normalized loading and failure states instead of nullable hook results, so callers do not infer why data is absent. +51. As a feature developer, I want application-owned navigation available as an Effect-backed runtime command, so workflow code does not publish outcomes for React to apply. +52. As a feature developer, I want navigation commands to accept canonical absolute destinations, so behavior is independent of React route context. +53. As a feature developer, I want router promises and failures owned by the runtime module, so event handlers never discard asynchronous navigation work. +54. As a feature developer, I want RainbowKit modal callbacks isolated in one named provider adapter, so third-party React constraints do not spread into feature state. +55. As a feature developer, I want command-related tracking performed with the command, so analytics follows the same ownership and stale-result checks as the behavior it records. +56. As a maintainer, I want the Earn machine to remain the sole source of Earn Selection and readiness, so a second writable page model cannot diverge from it. +57. As a maintainer, I want the aggregate Earn binding, aggregate page-model Atom, aggregate model type, and model hook removed, so the old architecture cannot remain as a compatibility layer. +58. As a maintainer, I want every consumer of the migrated shared hooks moved to feature or shared-module Atoms, so two architectures do not persist indefinitely. +59. As a maintainer, I want legacy calculation hooks deleted after migration, so ownership can be found from the module graph. +60. As a maintainer, I want unrelated surrounding feature logic left alone unless it blocks a headless dependency chain, so the migration remains bounded. +61. As a maintainer, I want dependency migration to stop at stable Atoms, Authoritative Resources, or immutable route/session input, so React values are not republished merely to satisfy a new facade. +62. As a maintainer, I want Classic and Borrow Flow scopes to retain their established ownership, so shared projections do not become application-global state. +63. As a maintainer, I want application navigation executed only from explicit commands or transition events, so reading a derived Atom never causes a side effect. +64. As a maintainer, I want route guards distinguished from workflow navigation commands, so authorization and route validity remain declarative. +65. As a maintainer, I want the module-global chain-modal callback holder removed, so sequential mounts and provider lifetimes do not depend on ambient mutable state. +66. As a maintainer, I want the final architecture to conform to the accepted facade and navigation ADRs, so code and documentation tell the same story. +67. As a test author, I want public facade and deep-module interfaces to be the primary seams, so tests survive internal file and helper refactors. +68. As a test author, I want to inject deterministic resource results, clocks, `WidgetNavigation` services, and WalletModal adapters, so asynchronous and external behavior can be tested without React orchestration. +69. As a test author, I want command branch tables for disconnected, connected, invalid, KYC-blocked, Ledger-placeholder, and valid Yield Entry states, so every submission decision is explicit. +70. As a test author, I want navigation integration tests to inspect the runtime-owned memory router, so Classic, Borrow, deep-link, and Yield Entry navigation can be verified without mounting React adapters. +71. As a test author, I want debounce tests to use deterministic time, so validator-search behavior is fast and reliable. +72. As a test author, I want existing browser journeys retained as end-to-end evidence, so internal refactoring cannot silently change user-visible behavior. +73. As a reviewer, I want deletion of old hooks, aggregate bindings, nested Atom view fields, and the router adapter bridge demonstrated in the final diff, so the migration proves replacement rather than layering. +74. As an AI coding agent, I want narrow named modules and stable public entries, so ownership and safe change surfaces are discoverable without tracing a monolithic React model. +75. As a widget host, I want ordinary live settings changes to preserve the current in-memory route, so configuration callbacks and presentation updates do not restart my journey. +76. As a widget host, I want an API-identity change to reset in-memory history with the rest of application state, so a fresh generation cannot open on a stale Review, Steps, or Details route. +77. As a feature developer, I want router construction and disposal owned by one scoped Layer, so React does not act as a dependency-injection bridge. +78. As a feature developer, I want React to obtain the runtime-owned router synchronously through an internal Atom, so `RouterProvider` needs no loading state, fallback router, or synchronization effect. +79. As a maintainer, I want `ApplicationRouter` to be the only service in the base runtime, so runtime composition stays explicit and minimal. +80. As a maintainer, I want `WidgetNavigation` constructed directly from `ApplicationRouter`, so a forwarding adapter does not duplicate the service boundary. +81. As a maintainer, I want the complete React Router instance restricted to top-level composition, so feature modules cannot bypass canonical navigation commands. +82. As a maintainer, I want the router explicitly disposed when its Application Runtime Generation closes, so pending navigation, loaders, blockers, and subscriptions cannot outlive their owner. + +## Implementation Decisions + +- ADR-0004 remains the base application-logic rule: Effect and Effect Atom own business state, asynchronous work, failure normalization, retries, concurrency, resource lifetimes, and command effects; React is a synchronous view adapter. +- ADR-0008 remains authoritative for canonical remote reads. Shared modules and feature facades derive from Authoritative Resources and do not create feature-owned remote caches or alternate retry and pagination policy. +- ADR-0009 remains authoritative for Earn Selection, Earn Readiness, failure precedence, initialization, and machine reconciliation. The new facade changes how that state is projected and consumed, not the machine's domain authority. +- ADR-0011 governs facade interfaces. Public view values contain no nested Atoms, Atom factories, or retry, refresh, pagination, or command callbacks. +- ADR-0012 governs application-owned navigation. An Application Runtime Generation owns the memory router and workflow decisions use the application-runtime navigation interface; React remains responsible for route guards and view-local navigation. +- The Earn machine remains the sole writable source of Earn Selection and entry-form intent. The facade derives capability views and forwards typed commands to the machine and active resources. +- Earn facade capabilities are organized by stable behavior rather than React component names. They cover token options, yield options, validators, amount and quote, yield summary, Yield Entry, submission and CTA, and page-level failure. +- The final Earn interface does not expose one aggregate page model. Consumers subscribe to the capability they render. +- Dynamic Authoritative Resource selection occurs inside derived or command Atoms. Token and validator Pull, loaded-validator memory, retry targets, and resource-family selection do not cross the facade. +- Token and yield search state is Atom-owned. Their filters are direct derived projections; React deferred values are removed unless later profiling establishes a separate presentation need. +- Validator search normalization and the established debounce interval are Atom-owned using Effect time. The active view exposes whether it is debouncing. +- Search, selection, pagination, loading, empty, and failure projections are deterministic and testable without React. +- Deterministic filtering, grouping, sorting, formatting, amount calculations, validation, reward calculations, provider projection, reward-token projection, yield-type projection, and Action Command construction remain pure TypeScript functions composed by Atoms. +- View Atoms expose semantic categories or translation keys rather than invoking React i18n. React performs translation and renders JSX without domain branching. +- Locale-independent deterministic formatting may occur in pure projections. Locale and translation-dependent presentation remains at the view seam. +- Synchronous local presentation state with no workflow meaning, persistence, asynchronous behavior, route lifetime, or cross-view coordination remains in React. +- `YieldSummary` is a top-level feature module with a narrow public entry. It accepts feature-owned input through an Atom and exposes a stable read-only view Atom. +- `YieldSummary` owns provider-yield lookup composition, provider details, reward-token details, semantic yield type, and normalized loading, ready, and failed states. +- `YieldSummary` does not own translation, route state, selection intent, transaction preparation, or feature-specific layout. +- `YieldEntry` is a top-level feature module with a narrow public entry. It accepts a feature-owned input Atom and exposes stable view and command Atoms. +- `YieldEntry` owns entry amount constraints, force-max projection, amount validation, KYC projection and refresh, estimated rewards, Enter Action Command construction, submission eligibility, CTA decisions, command-related analytics, transaction-session start, and application navigation. +- `YieldEntry` composes `YieldSummary` rather than duplicating its provider and reward projections. +- A feature input supplies selected yield, selected token, selected validators, selected provider, amount intent, relevant positions and balances, Wallet Scope facts, required configuration, and destination policy. Inputs express domain policy rather than caller names such as classic or dashboard. +- Earn and dashboard position-details entry compose `YieldEntry` with their own state and lifetime. The shared module does not import either caller. +- Classic position-details exit behavior may reuse extracted amount calculations without being forced into the entry module. +- Classic Transaction Flow review and completion compose `YieldSummary` inside their existing Session, Review, and Execution scopes. +- Activity and portfolio item projections use item-keyed feature Atoms and `YieldSummary`; they do not become global Earn state. +- When a required input is still React-hook-owned, its dependency chain is migrated only until it reaches a stable Atom, Authoritative Resource, or immutable route/session input. A React effect must not republish changing hook state into the new module. +- All current consumers of estimated rewards, amount limits, amount validation, provider details, reward-token details, yield KYC, yield type, stake-enter request construction, and pending-action deep-link adaptation are migrated. +- Legacy shared hooks are deleted when their final consumer moves. Thin wrappers are not retained in the finished change merely for compatibility. +- The aggregate Earn binding, aggregate page-model Atom, aggregate model type, and aggregate hook are deleted after classic and dashboard consumers migrate. +- The facade does not require a React provider or root binding. Derived capabilities live in the existing Widget Instance Atom registry, and consumers mount only what they read. +- Lifecycle Atoms are introduced only when an effectful resource genuinely follows route or view visibility. They do not assemble or publish aggregate view models. +- `WidgetNavigation` is a headless Effect module in the application runtime. Feature and workflow code depends on its narrow command interface, never on the React Router instance. +- A separate synchronous Atom runtime contains only `ApplicationRouter`. Its static scoped Layer creates the existing memory router and registers deterministic router disposal. +- The internal router Atom uses synchronous service projection. Router construction is treated as an invariant and does not add loading, retry, fallback-router, or recoverable failure UI. +- The React composition boundary reads the router Atom and passes the value to the DOM `RouterProvider`. React does not construct, retain, synchronize, or dispose the router. +- The complete router service remains internal to runtime composition. Feature modules receive `WidgetNavigation`, not the raw React Router instance. +- The Application Router runtime provides its Effect Context to the application runtime, following the existing application-runtime-to-wallet-runtime composition pattern. +- `WidgetNavigation` is constructed directly from `ApplicationRouter`. The production and test navigation adapter types, adapter Atom, registry-provider adapter input, and React forwarding object are removed. +- Tests of feature and workflow behavior provide `WidgetNavigation` directly. Focused integration tests use the real runtime-owned memory router. +- Application navigation commands use canonical absolute widget paths. Route helpers own destination construction. +- Navigation distinguishes push, replace, and back operations and carries a semantic scroll-reset or preserve policy. `WidgetNavigation` retains the existing widget configuration that can disable automatic scroll reset. +- The static root route configuration is assembled at the top-level React composition seam and supplied to the Application Router Layer. Application/service modules do not import React. +- The router remains a memory router. Browser and hash routers are not introduced. +- API-identity replacement closes the current Application Runtime Generation and creates fresh memory history. Live settings changes that retain the generation preserve the router. +- `RouterProvider` is imported from the documented DOM entrypoint. Router creation and route APIs continue to use the base React Router entrypoint. +- Workflow modules execute navigation only from explicit commands or transition events after checking current Flow Session, Execution Attempt, or intent ownership. +- Derived view Atoms never navigate when read, mounted, refreshed, or recomputed. +- Pending-action deep-link routing invokes `WidgetNavigation` directly once readiness and intent-claim rules pass; its React outcome bridge and delivery state are removed. +- Classic Transaction Flow forward, cancellation, and completion navigation invokes `WidgetNavigation` from its scoped commands or transition events. Existing stale-session suppression remains. +- Borrow Transaction Flow base, forward, and completion navigation invokes `WidgetNavigation` from its scoped commands or transition events. Existing epoch and execution checks remain. +- Declarative redirects that guard route validity remain React `` behavior. +- Tabs, breadcrumbs, view-local back controls, and external URL navigation remain outside `WidgetNavigation` unless separately redesigned. +- RainbowKit modal integration remains the one required React-only command seam for this effort. +- A named provider adapter installs connect- and chain-modal commands into a runtime-scoped `WalletModal` port and releases them on provider teardown. +- `WalletModal` callbacks are private adapter implementation. They do not appear in facade views, Yield Entry inputs, or command payloads. +- Ledger add-account behavior depends on `WalletModal` through the runtime and no longer accepts a close-modal callback as command data. +- The module-global latest-chain-modal callback holder is removed. +- Analytics caused by commands runs in the owning Atom/Effect command through the tracking runtime. Page-impression analytics may remain a route visibility adapter. +- The migration preserves public React and bundled-renderer interfaces. +- The migration preserves existing user-facing copy; no translation resource changes are required unless implementation reveals an accidental behavior dependency. +- The migration preserves single-Widget-Instance and sequential-remount constraints and introduces no machinery for concurrent Widget Instances. +- The final change contains one authority for each migrated behavior. Temporary compatibility used between local implementation stages is deleted before completion. +- Implementation proceeds in green stages: pure projections, shared resource selectors, runtime navigation and modal seams, shared modules, Earn facade and consumers, position details, transaction flows, activity and portfolio, legacy deletion, documentation alignment, and full verification. +- Existing unrelated React Query, direct `useNavigate`, and legacy hooks are not migrated unless they are in the required dependency chain or implement an Atom-generated navigation outcome covered by this spec. + +## Testing Decisions + +- Tests assert observable behavior through the highest stable interface: a feature facade, `YieldEntry`, `YieldSummary`, or `WidgetNavigation`. Tests do not inspect private mutable Atoms, private resource selection, helper call counts, React memoization, or file organization. +- Pure calculation tests cover filtering, grouping, sorting, amount limits, force-max behavior, validation, provider projection, reward projection, reward-token projection, semantic yield type, CTA projection, and Enter Action Command construction. +- `YieldSummary` interface tests use controllable Authoritative Resource results and assert normalized loading, ready, failed, refresh-with-value, provider, reward-token, and semantic type views. +- `YieldEntry` interface tests use controllable input Atoms and injected adapters. They assert amount constraints, KYC states and refresh, rewards, validation, CTA states, and command effects. +- Yield Entry command tests use a decision table covering disconnected, external-provider-hidden, connecting, invalid, submitted-invalid, missing request input, KYC-blocked, Ledger-placeholder, valid connected, and stale-owner cases. +- Yield Entry command tests assert transaction-session start, analytics, WalletModal commands, navigation commands, and non-occurrence of forbidden effects. +- Earn facade registry tests assert capability projections and commands without mounting React. Existing Earn machine registry tests remain the prior art for controlled machine, resource, and race behavior. +- Search tests use deterministic Effect time to assert normalization, the established validator debounce interval, the debouncing flag, query-key changes, and stale-result suppression. +- Pagination tests assert that stable load-more commands route to the active token or validator resource, ignore duplicate pulls while waiting, preserve accumulated values, and do not expose resource Atoms to callers. +- Retry tests assert that stable retry commands refresh the current responsible resource and do not retain a stale resource identity after selection or key changes. +- `WidgetNavigation` capability tests assert push, replace, back, absolute destinations, scroll policy, successful completion, and normalized failure without depending on React. +- Application Router integration tests assert that the first Atom read synchronously returns the memory router, real commands update its route state, one runtime generation preserves router identity, API-identity replacement creates fresh history, and registry disposal disposes the router exactly once. +- Pending-action deep-link registry tests assert readiness gating, intent claiming, Classic Flow start where required, and direct navigation without a mounted React adapter. +- Classic Flow facade tests assert Review-to-Steps, execution cancellation, completion, current-session checks, browser-history semantics, and suppression of stale navigation. +- Borrow Flow facade tests assert Base, Steps, Complete, session epoch checks, execution lifetime, and suppression of stale navigation. +- Route-level DOM/browser tests continue asserting declarative invalid-session guards because those guards intentionally remain React-owned. +- WalletModal adapter tests assert provider acquisition, replacement, cleanup, and unavailability behavior. Feature tests inject a fake port rather than mounting RainbowKit. +- Ledger account tests assert that successful account switching closes the chain modal through the port and that missing or invalid Ledger connectors retain typed failure behavior. +- Consumer migration tests replace mocked aggregate page-model hooks with Atom registry inputs or public module fakes. +- Existing provider-selection, validator-selection, Earn workflow, position-details, Classic Flow, Borrow Flow, deep-link, and staking browser tests are retained as behavior-level prior art. +- Browser tests cover at least one classic Earn journey and one dashboard/position-details Yield Entry journey through Review, proving that the shared module and runtime navigation integrate with real UI adapters. +- Regression tests assert that mounting and unmounting a Widget Instance releases modal adapters, router/runtime work, and scoped flow modules before sequential remount. +- Static verification rejects React-owned data-router construction, the removed navigation-adapter bridge, and raw router imports from feature modules. +- Static verification asserts no remaining imports of deleted shared hooks, aggregate Earn model interfaces, nested operational Atom fields in public view types, or React adapters for Atom-generated navigation outcomes. +- Lint and type checking are required after every materially complete stage. Focused unit and DOM tests run during slices; changed browser tests run for affected slices; full package verification runs before completion. +- Tests added for the new deep modules replace tests that only exercised deleted shallow hooks. Existing machine, schema, Authoritative Resource, and workflow tests remain when they still describe public behavior. +- A good test should survive moving helpers, splitting files, or changing private Atom composition. If a test changes solely because implementation internals move while interface behavior is unchanged, it is testing below the intended seam. + +## Out of Scope + +- Changing visible Earn, position-details, activity, portfolio, review, completion, KYC, or transaction-flow product behavior. +- Redesigning the UI, adding new controls, changing copy, or changing translations. +- Changing backend API contracts, generated schemas, Action Command semantics, wallet protocols, or transaction execution mechanics. +- Replacing or redesigning the existing Earn machine's selection, readiness, initialization, reconciliation, and failure precedence. +- Rewriting every surrounding activity, portfolio, position-details, or transaction-flow model. Only migrated capabilities and dependencies required to make them headless are included. +- Migrating every direct React Router `useNavigate` call. Declarative guards and view-local navigation remain React-owned. +- Converting Classic or Dashboard declarative routes into data-router route objects. +- Replacing the memory router with a browser, hash, or host-URL router. +- Mirroring route state into Atom before application logic has a concrete route-read requirement. +- Moving external URL navigation into the widget navigation module. +- Replacing RainbowKit or modifying its package interface. +- Migrating unrelated React Query resources or hook-owned logic outside the required dependency chains. +- Adding concurrency support for more than one mounted Widget Instance per browser document. +- Adding a second Atom registry, ad hoc Effect runtime, global router singleton, global navigation queue, or global normalized feature-state cache. +- Retaining a permanent compatibility aggregate page-model hook, callback-rich facade, or dual shared-hook implementation. +- Creating new module-specific architecture documents or storing the temporary implementation checklist in permanent architecture documentation. +- Prototyping alternative UI or state behavior; the design questions were resolved in conversation and ADRs. + +## Further Notes + +- The canonical term **Yield Entry** is defined in the project glossary. Avoid “Enter Action” for the pre-execution attempt because **Yield Action** refers to the created action and **Action Command** refers to its prepared instruction. +- `YieldSummary`, `WidgetNavigation`, facade, and port are implementation vocabulary and do not belong in the domain glossary. +- **Application Runtime Generation** is the canonical lifecycle term for application state under one stable API identity. Avoid the ambiguous term “Widget Runtime.” +- The accepted facade ADR supersedes only the part of the Earn-state ADR that allowed operational pagination and retry-target Atoms to cross the published view. The rest of the Earn-state decision remains authoritative. +- The accepted navigation ADR replaces the earlier living-architecture rule that React route adapters apply Atom navigation outcomes. +- Existing Classic and Borrow architecture documents and the widget-wide architecture document have already been aligned with the accepted target. +- The migration is complete across all eight tracer-bullet issues, including + Application Runtime Generation ownership of the memory router. +- No runnable prototype is required before ticketing. The principal risks are migration breadth, lifecycle preservation, and behavioral regression, all of which have established registry and browser test seams. diff --git a/.scratch/authoritative-resources/issues/01-share-yield-positions.md b/.scratch/authoritative-resources/issues/01-share-yield-positions.md new file mode 100644 index 000000000..a58fa3dd2 --- /dev/null +++ b/.scratch/authoritative-resources/issues/01-share-yield-positions.md @@ -0,0 +1,16 @@ +# 01 — Share Yield positions through the first Authoritative Resource + +**What to build:** Establish the Authoritative Resource pattern through a complete Yield positions slice. Earn and Portfolio must read one canonical Wallet Scope-keyed position fact, share one acquisition and cache policy, and keep historical positions available before feature visibility projections. + +**Blocked by:** None — can start immediately. + +**Status:** implemented + +- [x] A narrow `YieldResourceSource` capability provides the aggregate-position read without exposing the broad Yield backend service. +- [x] One named Yield Positions resource owns explicit request identity, acquisition, typed failures, freshness, retry, stale-result suppression, and semantic position invalidation. +- [x] Equivalent Earn and Portfolio requests within one Widget Instance share one acquisition and cached result. +- [x] Earn and Portfolio consume canonical positions through read-only projections and no replaced feature-local fetch owner remains. +- [x] Portfolio totals and grouping retain historical positions even when their yields are not currently selected or visible. +- [x] Resource-contract tests cover exact request sharing, distinct Wallet Scopes, typed failure, invalidation, and registry lifetime. +- [x] A generated-client adapter test proves aggregate-position request and response mapping at the capability seam. +- [x] Focused feature tests and widget lint/type checking pass. diff --git a/.scratch/authoritative-resources/issues/02-share-token-balance-scans.md b/.scratch/authoritative-resources/issues/02-share-token-balance-scans.md new file mode 100644 index 000000000..948705924 --- /dev/null +++ b/.scratch/authoritative-resources/issues/02-share-token-balance-scans.md @@ -0,0 +1,15 @@ +# 02 — Share wallet token-balance scans + +**What to build:** Replace the separate Earn and Portfolio token scans with one canonical Token Balances resource keyed by complete Wallet Scope and token-scan identity, while preserving each feature's presentation model. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] A narrow `LegacyResourceSource` capability exposes the semantically read-only token scan without exposing the broad Legacy backend service. +- [x] One Token Balances resource owns canonical input normalization, empty-input behavior, typed failures, freshness, retry, interruption, and wallet-balance invalidation. +- [x] Equivalent Earn and Portfolio scans share one backend acquisition and canonical result. +- [x] Feature-specific token models are pure projections and cannot create a second request authority. +- [x] Complete Wallet Scope identity prevents results from one wallet or network appearing in another. +- [x] Contract tests cover sharing, empty inputs, duplicate identifiers, distinct scopes, invalidation, and Widget Instance remount. +- [x] Adapter, affected feature, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/03-share-yield-opportunity-provider-facts.md b/.scratch/authoritative-resources/issues/03-share-yield-opportunity-provider-facts.md new file mode 100644 index 000000000..03ac9fce9 --- /dev/null +++ b/.scratch/authoritative-resources/issues/03-share-yield-opportunity-provider-facts.md @@ -0,0 +1,15 @@ +# 03 — Share Yield opportunity and provider facts + +**What to build:** Give individual Yield opportunities and providers one authoritative cache owner so ordinary lookup, initialization, details, and enrichment consumers reuse the same canonical decoded facts. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] Ordinary and initial Yield lookup paths use one request identity when they represent the same remote fact. +- [x] Provider lookups are named resources with explicit provider identity and typed missing-provider behavior. +- [x] Initialization and feature-specific interpretation remain projections outside canonical resource storage. +- [x] Equivalent concurrent and sequential consumers share acquisition, fresh state, retry, and failure state. +- [x] Raw generated-client response types and errors do not cross either resource interface. +- [x] Replaced feature-local Yield and provider fetch owners are removed. +- [x] Contract, adapter, feature integration, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/04-share-complete-yield-directory.md b/.scratch/authoritative-resources/issues/04-share-complete-yield-directory.md new file mode 100644 index 000000000..c5665c1df --- /dev/null +++ b/.scratch/authoritative-resources/issues/04-share-complete-yield-directory.md @@ -0,0 +1,16 @@ +# 04 — Share bounded Yield Directory reads + +**What to build:** Replace separate Yield-list fetchers with a canonical directory that completely resolves explicit requested Yield IDs, plus a shared bounded category-summary resource that performs one maximum-size request per category. Provider enrichment remains a projection over the explicit-ID directory. + +**Blocked by:** 03 — Share Yield opportunity and provider facts. + +**Status:** implemented + +- [x] Endpoint-equivalent listing requests resolve through one named resource with complete explicit filter and sort identity. +- [x] Full-result consumers receive all applicable pages rather than an arbitrary first page. +- [x] Available categories use one `offset: 0`, API-maximum-size request per category and do not scan the complete unfiltered Yield catalog. +- [x] Provider enrichment reuses the provider resource, deduplicates provider identities, and has explicit failure and missing-provider semantics. +- [x] Availability, selected, visible, and token-scope behavior remains downstream projection logic. +- [x] No former listing or category atom retains independent acquisition or freshness policy. +- [x] Tests cover equivalent and distinct requests, explicit-ID pagination boundaries, bounded category requests, provider deduplication, failures, and stale-result suppression. +- [x] Adapter, feature integration, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/05-share-earn-token-discovery.md b/.scratch/authoritative-resources/issues/05-share-earn-token-discovery.md new file mode 100644 index 000000000..f382394db --- /dev/null +++ b/.scratch/authoritative-resources/issues/05-share-earn-token-discovery.md @@ -0,0 +1,21 @@ +# 05 — Share Earn token discovery + +**What to build:** Restore the two intentional token-discovery contracts: Yield API results use one shared demand-driven Pull per semantic query, while Legacy token options remain a complete non-paginated resource. The feature chooses the source and projects either result without owning transport pagination. + +**Blocked by:** 02 — Share wallet token-balance scans. + +**Status:** implemented + +- [x] Yield token discovery has complete network and Yield-type identity, requests only the first backend page initially, and advances by one backend continuation per accepted Pull. +- [x] Equivalent Yield token consumers share one Pull Atom and accumulated progress; the feature facade does not create a second stream or paginate a complete array in memory. +- [x] Legacy token options use `LegacyResourceSource` and preserve network-specific behavior. +- [x] Legacy token options remain a complete non-paginated resource and are exposed as immediately done, with no synthetic pages or fake continuation. +- [x] Empty network or filter inputs avoid meaningless I/O where the semantic result is empty. +- [x] Canonical token facts remain independent from selected-token and view presentation state. +- [x] Former feature-local token-option acquisition paths are removed. +- [x] Tests prove one initial Yield page, one request per Pull, shared progress, backend-derived continuation, refresh from page one, and complete Legacy behavior. +- [x] Focused adapter and token-selection tests, lint, and type-check validation pass. + +## Comments + +- Pagination audit against `77802a3c2849416602a0b20280a5b40acb7b6cf1` found that the Yield branch was incrementally paginated while the Legacy branch intentionally returned a complete list. Eagerly collecting all Yield pages was a regression. diff --git a/.scratch/authoritative-resources/issues/06-share-validator-discovery.md b/.scratch/authoritative-resources/issues/06-share-validator-discovery.md new file mode 100644 index 000000000..95b26ac9d --- /dev/null +++ b/.scratch/authoritative-resources/issues/06-share-validator-discovery.md @@ -0,0 +1,22 @@ +# 06 — Share validator discovery + +**What to build:** Restore the validator endpoint's distinct semantic contracts: ordinary and search discovery use shared demand-driven Pull resources, preferred validators use an explicit complete resource, and address resolution uses a bounded point resource. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] Each validator contract has a minimal complete identity: discovery includes Yield, status, and search inputs; preferred includes its applicable scope; address lookup includes Yield and address. Transport continuation is never part of caller identity. +- [x] Ordinary and search discovery request only the first backend page initially and advance by one backend continuation per accepted Pull. +- [x] Equivalent ordinary or search consumers share the same Pull Atom and accumulated progress; feature projections forward Pull and Refresh instead of creating another stream. +- [x] Validator search advances its name and address branches independently and concurrently, then merges and deduplicates their emitted batches deterministically. +- [x] Preferred validators acquire the complete applicable result explicitly; address resolution remains a bounded point lookup and neither contract is routed through the ordinary Pull. +- [x] Continuation, later-page failure, waiting, and refresh use native Atom and Stream behavior without page caches, manual offsets, locks, or custom refresh coordination. +- [x] Feature code receives semantic validator state rather than raw offsets or generated-client page types. +- [x] Replaced validator fetch implementations are removed. +- [x] Tests prove incremental ordinary/search acquisition, shared progress, independent search continuations, complete preferred acquisition, bounded address lookup, and refresh from page one. +- [x] Focused resource and UI integration tests, lint, and type-check validation pass. + +## Comments + +- Pagination audit against `77802a3c2849416602a0b20280a5b40acb7b6cf1` found that the endpoint intentionally served incremental, complete, and point consumers. Replacing those contracts with one eager complete directory was a regression. diff --git a/.scratch/authoritative-resources/issues/07-share-activity-history.md b/.scratch/authoritative-resources/issues/07-share-activity-history.md new file mode 100644 index 000000000..2b2a05c13 --- /dev/null +++ b/.scratch/authoritative-resources/issues/07-share-activity-history.md @@ -0,0 +1,24 @@ +# 07 — Share Activity history + +**What to build:** Give Activity history one shared demand-driven Pull owner. It emits semantic batches containing actions and the backend total; Activity filter counts use bounded summary requests, and feature enrichment projects the canonical Pull without introducing another pagination stream. + +**Blocked by:** 03 — Share Yield opportunity and provider facts. + +**Status:** implemented + +- [x] Activity request identity includes the semantic wallet owner scope, filters, and ordering; backend offset and continuation remain private stream state rather than caller-provided cache identity. +- [x] Initial Activity acquisition requests only the first backend page and each accepted Pull advances by one backend continuation derived from the response's offset, limit, and total. +- [x] Equivalent Activity consumers share one Pull Atom and accumulated progress; no complete-history resource or in-memory pagination remains. +- [x] Pull emissions carry both the action batch and backend total, replacing pagination side atoms. +- [x] Activity filter counts use bounded summary requests that read backend totals without collecting history. +- [x] Yield and validator enrichment reuse authoritative resources as a projection over the Activity Pull and do not create duplicate lookups or a second pagination stream. +- [x] Activity invalidation affects all relevant variants without eagerly fetching inactive variants. +- [x] Obsolete or disposed requests cannot publish into a newer Activity state. +- [x] Existing Activity display and Activity Resume behavior remain compatible. +- [x] Later-page failure preserves accumulated actions, waiting prevents repeated Pull dispatch, and Refresh restarts from page one using native Atom and Stream behavior. +- [x] Tests prove one initial request, one request per Pull, shared progress, bounded counts, backend-derived continuation, refresh behavior, and absence of eager full-history acquisition. +- [x] Focused contract, integration, and invalidation tests, lint, and type-check validation pass. + +## Comments + +- Pagination audit against `77802a3c2849416602a0b20280a5b40acb7b6cf1` found that Activity history was product-level incremental pagination and counts were bounded total probes. Eagerly collecting all history and slicing it in memory was a regression. diff --git a/.scratch/authoritative-resources/issues/08-share-flow-balance-facts.md b/.scratch/authoritative-resources/issues/08-share-flow-balance-facts.md new file mode 100644 index 000000000..047815f74 --- /dev/null +++ b/.scratch/authoritative-resources/issues/08-share-flow-balance-facts.md @@ -0,0 +1,15 @@ +# 08 — Share flow balance facts + +**What to build:** Make single-Yield balances and gas-token balance checks authoritative facts reused by deep links, Review, position details, and completion views without moving workflow ownership into Resources. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource; 02 — Share wallet token-balance scans. + +**Status:** implemented + +- [x] Single-Yield balances use complete Yield and wallet identity and one resource-owned freshness and invalidation policy. +- [x] Gas-token balance requests use explicit Action Command and Wallet Scope-derived identity without reading flow state internally. +- [x] Classic Transaction Flow remains the owner of Action Preview and execution; Resources own only the cacheable balance facts. +- [x] Existing deep-link, Review warning, position-detail, and completion behavior consumes read-only projections. +- [x] Replaced feature and flow-local fetch owners are removed. +- [x] Tests cover sharing across consumers, distinct commands and wallets, empty inputs, failures, invalidation, and execution-scope disposal. +- [x] Adapter, flow integration, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/09-share-prices-reward-summaries.md b/.scratch/authoritative-resources/issues/09-share-prices-reward-summaries.md new file mode 100644 index 000000000..85d1b40d5 --- /dev/null +++ b/.scratch/authoritative-resources/issues/09-share-prices-reward-summaries.md @@ -0,0 +1,15 @@ +# 09 — Share prices and reward summaries + +**What to build:** Give token prices and per-Yield reward summaries authoritative owners so Earn, Portfolio, and position details share canonical financial facts and bounded backend work. + +**Blocked by:** 02 — Share wallet token-balance scans. + +**Status:** implemented + +- [x] Price request identity is complete and semantically equivalent token requests share one acquisition. +- [x] Empty and duplicate token inputs avoid unnecessary backend work and preserve deterministic result ordering. +- [x] Reward-summary requests canonicalize Yield identities safely, apply bounded concurrency, and represent missing summaries explicitly. +- [x] Feature totals and formatted values remain downstream projections over canonical facts. +- [x] Typed transport, decode, partial, and invariant failures do not leak raw adapter errors. +- [x] Former price and reward fetch owners are removed. +- [x] Contract, feature integration, adapter, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/10-share-kyc-yield-history-insights.md b/.scratch/authoritative-resources/issues/10-share-kyc-yield-history-insights.md new file mode 100644 index 000000000..1155e8d33 --- /dev/null +++ b/.scratch/authoritative-resources/issues/10-share-kyc-yield-history-insights.md @@ -0,0 +1,15 @@ +# 10 — Share KYC and Yield history insights + +**What to build:** Make KYC status, reward-rate history, and TVL history named authoritative resources with complete identity and consistent freshness across classic and dashboard consumers. + +**Blocked by:** 03 — Share Yield opportunity and provider facts. + +**Status:** implemented + +- [x] KYC identity includes Yield and wallet address and cannot reuse status across owners. +- [x] Reward-rate and TVL histories include Yield, period, and interval in their resource identities. +- [x] Each resource owns typed failures, freshness, retry, interruption, and stale-result behavior. +- [x] History sorting and chart formatting remain feature projections rather than cached feature models. +- [x] Existing KYC gates and insight displays retain their behavior. +- [x] Former insight fetch owners are removed. +- [x] Contract, dashboard and classic integration, adapter, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/11-share-widget-health-status.md b/.scratch/authoritative-resources/issues/11-share-widget-health-status.md new file mode 100644 index 000000000..3e12386ba --- /dev/null +++ b/.scratch/authoritative-resources/issues/11-share-widget-health-status.md @@ -0,0 +1,14 @@ +# 11 — Share widget health status + +**What to build:** Give backend health one authoritative resource owner so maintenance detection observes one typed, policy-controlled fact rather than calling the broad Yield backend service directly. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] Health acquisition uses `YieldResourceSource` and exposes a stable typed resource state. +- [x] Polling, freshness, retry, and stale-result behavior are owned by the Health resource. +- [x] Maintenance presentation consumes a zero-logic read-only adapter. +- [x] The former direct health-service call is removed. +- [x] Tests cover healthy, maintenance, transport failure, retry, polling lifecycle, and Widget Instance disposal. +- [x] Focused integration, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/12-narrow-enabled-network-bootstrap.md b/.scratch/authoritative-resources/issues/12-narrow-enabled-network-bootstrap.md new file mode 100644 index 000000000..925acd1ad --- /dev/null +++ b/.scratch/authoritative-resources/issues/12-narrow-enabled-network-bootstrap.md @@ -0,0 +1,14 @@ +# 12 — Move enabled-network bootstrap behind the Legacy read capability + +**What to build:** Preserve Wallet Bootstrap network discovery while removing its dependency on the broad Legacy backend service and keeping bootstrap lifetime and failure behavior unchanged. + +**Blocked by:** 02 — Share wallet token-balance scans. + +**Status:** implemented + +- [x] Wallet Bootstrap obtains enabled networks through `LegacyResourceSource` with no broad-service dependency. +- [x] The capability exposes only the query required by bootstrap and returns canonical decoded network data. +- [x] Bootstrap Snapshot, readiness, fallback, and failure semantics remain unchanged. +- [x] No React adapter or feature atom becomes the owner of enabled-network acquisition. +- [x] Service and Wallet Bootstrap tests cover success, failure, runtime replacement, and sequential remount. +- [x] Lint and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/13-route-classic-operations.md b/.scratch/authoritative-resources/issues/13-route-classic-operations.md new file mode 100644 index 000000000..80cc5aab8 --- /dev/null +++ b/.scratch/authoritative-resources/issues/13-route-classic-operations.md @@ -0,0 +1,15 @@ +# 13 — Route classic transaction operations through YieldOperations + +**What to build:** Replace broad Yield backend access in Classic Transaction Flow and Transaction Workflow with a narrow operation capability covering Action Preview, submission, and status polling. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] `YieldOperations` exposes only the operation and workflow calls required by classic transaction intent owners. +- [x] Action Preview remains owned by the Review scope and transaction status remains owned by Transaction Workflow rather than becoming shared cache resources. +- [x] Submission and polling preserve their typed errors, retries, interruption, and scoped lifetime. +- [x] Successful operations publish semantic position, balance, and activity invalidation at the correct completion points. +- [x] Feature commands remain synchronous Atom dispatch boundaries and do not call an Effect runtime directly. +- [x] Classic flow and workflow code no longer imports the broad Yield backend service. +- [x] Operation adapter, flow, workflow, invalidation, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/14-share-borrow-catalogs-positions.md b/.scratch/authoritative-resources/issues/14-share-borrow-catalogs-positions.md new file mode 100644 index 000000000..6dc4441cc --- /dev/null +++ b/.scratch/authoritative-resources/issues/14-share-borrow-catalogs-positions.md @@ -0,0 +1,15 @@ +# 14 — Share Borrow catalogs and positions + +**What to build:** Introduce `BorrowResourceSource` and authoritative resources for integrations, markets, and Wallet Scope positions so all Borrow and Portfolio views share canonical catalog and position facts. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource. + +**Status:** implemented + +- [x] Borrow integration, market, and position reads are exposed through a narrow read-source capability. +- [x] Market pagination and position fan-out are hidden behind resource-owned policies with bounded concurrency. +- [x] Complete network, integration, and Wallet Scope identities prevent incorrect cache sharing. +- [x] Borrow and Portfolio consumers share resource state and retain existing projections and empty states. +- [x] Borrow-market and Borrow-position invalidations affect all relevant variants. +- [x] Former Borrow resource atoms no longer acquire through the broad backend service. +- [x] Contract, adapter, feature integration, invalidation, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/15-route-borrow-operations.md b/.scratch/authoritative-resources/issues/15-route-borrow-operations.md new file mode 100644 index 000000000..5dadf3919 --- /dev/null +++ b/.scratch/authoritative-resources/issues/15-route-borrow-operations.md @@ -0,0 +1,14 @@ +# 15 — Route Borrow workflows through BorrowOperations + +**What to build:** Replace broad Borrow backend access in Borrow Transaction Flow and Transaction Workflow with an operation capability for action creation, advancement, polling, and submission. + +**Blocked by:** 14 — Share Borrow catalogs and positions. + +**Status:** implemented + +- [x] `BorrowOperations` exposes only the commands and workflow queries required by Borrow intent owners. +- [x] Action creation remains owned by Borrow Review and status polling remains owned by Transaction Workflow. +- [x] Existing missing-configuration, action, transaction, retry, and interruption behavior remains typed and scoped. +- [x] Successful Borrow operations invalidate affected positions and markets without enumerating cache keys. +- [x] Borrow feature and workflow modules no longer import the broad Borrow backend service. +- [x] Operation adapter, transaction-flow, workflow, invalidation, lint, and type-check validation pass. diff --git a/.scratch/authoritative-resources/issues/16-contract-yield-backend-service.md b/.scratch/authoritative-resources/issues/16-contract-yield-backend-service.md new file mode 100644 index 000000000..72cc3f060 --- /dev/null +++ b/.scratch/authoritative-resources/issues/16-contract-yield-backend-service.md @@ -0,0 +1,14 @@ +# 16 — Contract the broad Yield backend service + +**What to build:** Remove the generated-client-shaped Yield service and its application-runtime wiring after every Yield fact and operation has moved to an Authoritative Resource or `YieldOperations`. + +**Blocked by:** 01 — Share Yield positions through the first Authoritative Resource; 03 — Share Yield opportunity and provider facts; 04 — Share the complete Yield Directory; 06 — Share validator discovery; 07 — Share Activity history; 08 — Share flow balance facts; 10 — Share KYC and Yield history insights; 11 — Share widget health status; 13 — Route classic transaction operations through YieldOperations. + +**Status:** implemented + +- [x] No production caller imports or resolves the broad Yield backend service. +- [x] Runtime composition provides the Yield read-source and operation capabilities from one private generated-client adapter layer. +- [x] Generated Yield client types remain private to transport and adapter infrastructure. +- [x] The broad service contract, constructor, layer, and dead compatibility helpers are removed. +- [x] Yield resource and operation contract suites pass against the final runtime composition. +- [x] Widget lint, type checking, focused suites, and hygiene checks pass. diff --git a/.scratch/authoritative-resources/issues/17-contract-legacy-backend-service.md b/.scratch/authoritative-resources/issues/17-contract-legacy-backend-service.md new file mode 100644 index 000000000..3810bf6c3 --- /dev/null +++ b/.scratch/authoritative-resources/issues/17-contract-legacy-backend-service.md @@ -0,0 +1,14 @@ +# 17 — Contract the broad Legacy backend service + +**What to build:** Remove the broad Legacy service after every scan, token, price, reward, gas-balance, and bootstrap query uses `LegacyResourceSource`. + +**Blocked by:** 02 — Share wallet token-balance scans; 05 — Share Earn token discovery; 08 — Share flow balance facts; 09 — Share prices and reward summaries; 12 — Move enabled-network bootstrap behind the Legacy read capability. + +**Status:** implemented + +- [x] No production caller imports or resolves the broad Legacy backend service. +- [x] Runtime composition provides one narrow Legacy read-source capability backed by the private generated client. +- [x] Generated Legacy client types remain private to transport and adapter infrastructure. +- [x] The broad service contract, constructor, layer, and duplicated response adaptations are removed. +- [x] Legacy-backed resource and Wallet Bootstrap tests pass against final composition. +- [x] Widget lint, type checking, focused suites, and hygiene checks pass. diff --git a/.scratch/authoritative-resources/issues/18-contract-borrow-backend-service.md b/.scratch/authoritative-resources/issues/18-contract-borrow-backend-service.md new file mode 100644 index 000000000..b26ed462b --- /dev/null +++ b/.scratch/authoritative-resources/issues/18-contract-borrow-backend-service.md @@ -0,0 +1,14 @@ +# 18 — Contract the broad Borrow backend service + +**What to build:** Remove the broad Borrow service after all Borrow reads and operations use their separate capability contracts while retaining one private generated-client-backed implementation. + +**Blocked by:** 14 — Share Borrow catalogs and positions; 15 — Route Borrow workflows through BorrowOperations. + +**Status:** implemented + +- [x] No production caller imports or resolves the broad Borrow backend service. +- [x] Runtime composition provides `BorrowResourceSource` and `BorrowOperations` from one private implementation layer. +- [x] Missing Borrow configuration remains a typed capability failure. +- [x] Generated Borrow client types remain private to transport and adapter infrastructure. +- [x] The broad service contract, constructor, layer, and dead compatibility helpers are removed. +- [x] Borrow resource, flow, workflow, lint, type-check, and hygiene validation pass. diff --git a/.scratch/authoritative-resources/issues/19-enforce-resource-dependency-graph.md b/.scratch/authoritative-resources/issues/19-enforce-resource-dependency-graph.md new file mode 100644 index 000000000..9d50f8f97 --- /dev/null +++ b/.scratch/authoritative-resources/issues/19-enforce-resource-dependency-graph.md @@ -0,0 +1,16 @@ +# 19 — Enforce and verify the final dependency graph + +**What to build:** Make Authoritative Resource ownership mechanically durable by rejecting cache-bypassing imports and verifying that the final module graph contains no broad backend-service escape hatch or duplicate remote-data owner. + +**Blocked by:** 16 — Contract the broad Yield backend service; 17 — Contract the broad Legacy backend service; 18 — Contract the broad Borrow backend service. + +**Status:** implemented + +- [x] Module boundaries represent application composition, features, Resources, runtime, services, and domain/shared foundations in the agreed dependency direction. +- [x] Features cannot import read-source capabilities, generated clients, or transport infrastructure. +- [x] Resources cannot import features, React, operation capabilities, generated clients, or transport infrastructure. +- [x] Operation capabilities are imported only by intent-owning command and workflow modules. +- [x] Generated clients are reachable only from private adapter and transport infrastructure. +- [x] Searches and hygiene checks prove there are no broad backend services, duplicate fetch atoms, module-global remote caches, or ad hoc runtimes. +- [x] Architecture documentation matches the enforced graph and names the permitted exceptions. +- [x] Full widget lint, type checking, test suites, build, and hygiene checks pass. diff --git a/.scratch/authoritative-resources/spec.md b/.scratch/authoritative-resources/spec.md new file mode 100644 index 000000000..263511cdd --- /dev/null +++ b/.scratch/authoritative-resources/spec.md @@ -0,0 +1,265 @@ +# Authoritative Resources for Shared Remote Data + +Status: ready-for-agent + +## Problem Statement + +Remote API reads are currently owned by feature-local atoms and broad backend services. Equivalent requests to the same endpoint are represented by different atoms in Earn, Portfolio, Activity, Borrow, and widget-shell code, so the widget can fetch the same canonical fact more than once, apply different freshness policies to it, decode it into incompatible feature shapes, and invalidate only some of its consumers. The current layout makes the feature that happens to issue a request look like the owner of remote data that is actually shared across the application. + +The duplication is already visible in yield details, yield listings, aggregate yield positions, token-balance scans, and validator pagination. Some duplicates call the same endpoint under different names; others differ only because a feature immediately projects the response into its own view model. This fragments the cache and obscures correctness problems. Selection-specific filtering can remove historical positions from portfolio totals, category availability checks can repeat equivalent bounded requests, and identical data may use different stale times depending on which feature requested it. + +The backend abstraction is also too broad. Yield, Legacy, and Borrow services currently expose large generated-client-shaped surfaces to application code. That permits features to bypass a shared cache owner, makes dependency boundaries difficult to enforce, and couples callers to transport concerns and unrelated endpoint families. Simply moving all API atoms into one global module would centralize filenames without establishing coherent ownership; it would create a shallow registry with broad dependencies and no semantic boundary between cacheable facts and state-changing operations. + +The widget needs one authoritative owner for every cacheable canonical remote fact, shared across all consuming features and scoped to one Widget Instance. That owner must define request identity, decoding, freshness, retry, pagination, concurrency, stale-result suppression, failure normalization, and invalidation. Features should bind current UI state to explicit resource inputs and derive feature views, while commands and multi-step workflows remain with the feature or workflow that owns the user intent. Backend access must be split into narrow read and operation capabilities so dependency rules can prevent feature code from reaching generated clients or transport infrastructure directly. + +## Solution + +Introduce a top-level Resources architectural tier composed of separate named deep modules. Each Authoritative Resource is the sole owner of one cacheable canonical remote fact and exposes a small typed Effect Atom interface. Equivalent requests made anywhere in the widget resolve to the same resource identity and therefore share acquisition, cache state, refresh work, and failures within the current Widget Instance registry. + +Resources accept complete explicit inputs and never read feature state, selected state, current wallet state, or route state. They cache canonical decoded facts rather than transport DTOs or feature-shaped read models. Features bind concepts such as current wallet, selected yield, visible items, and route-specific filters to resource inputs, then project the canonical result for presentation without starting a second fetch. + +The Resources tier owns cacheable reads only. User operations remain command atoms or workflow modules in the feature that owns the intent. Successful operations publish semantic invalidation events so every affected query variant becomes coherent. Direct refresh is reserved for user refresh and retry behavior. Pagination, request chunking, concurrency, stale-while-revalidate behavior, retry policy, and typed failure normalization stay behind each resource interface. + +Replace the broad Yield, Legacy, and Borrow backend service surfaces with coarse capability ports. Yield and Borrow each expose a read source and an operations capability. Legacy currently exposes a read source only because its POST-based scans are semantically queries. One shared backend layer may implement both capabilities for a backend, but each capability has its own importable service contract. Generated clients and the transport service remain private adapter infrastructure. + +Enforce the dependency direction from application composition through features, Resources, application runtime, capability services, and domain/shared foundations. Features may consume Authoritative Resources and operation capabilities, but may not import read-source capabilities, generated clients, or transport infrastructure. Resources may consume read-source capabilities but may not depend on feature state. Migrate in resource-sized vertical slices, deleting temporary adapters and duplicate fetch owners as each slice lands, then remove the broad backend services and enable the final strict dependency rules. + +## User Stories + +1. As a widget user, I want two screens requesting the same yield fact to share one fetch, so navigation and simultaneous rendering do not produce duplicate network work. +2. As a widget user, I want equivalent yield-list requests from different features to observe the same cached result, so Earn and Portfolio do not disagree because they refreshed independently. +3. As a widget user, I want aggregate yield positions requested by Earn and Portfolio to come from one authority, so balances and totals stay coherent. +4. As a widget user, I want token-balance scans requested by different features to share the same canonical result, so my wallet is not scanned repeatedly for the same scope. +5. As a widget user, I want validator lists to load on demand and equivalent screens to share their acquired progress, so opening a validator picker does not eagerly fetch the complete directory or duplicate page requests. +6. As a widget user, I want each available category checked with one shared request using the API maximum page size, so category discovery preserves the established bounded behavior without scanning the complete Yield catalog. +7. As a widget user, I want historical positions included in portfolio totals even when their yields are not currently selected or visible, so presentation filtering cannot corrupt accounting. +8. As a widget user, I want switching between Earn and Portfolio to reuse fresh data, so the widget feels responsive and avoids unnecessary loading states. +9. As a widget user, I want a stale cached value to remain usable according to one resource policy while revalidation occurs, so every consumer has consistent refresh behavior. +10. As a widget user, I want retrying a failed resource to refresh the shared authority, so all consumers recover together. +11. As a widget user, I want a manual refresh to update the shared resource, so I do not need to refresh the same fact separately on every screen. +12. As a widget user, I want an operation that changes positions to invalidate every relevant position query, so no screen continues displaying a cached pre-operation position. +13. As a widget user, I want an operation that changes balances to invalidate every relevant balance query, so dependent screens become coherent after the operation. +14. As a widget user, I want activity-producing operations to invalidate relevant activity resources, so new history appears without unrelated cache resets. +15. As a widget user, I want borrow operations to invalidate affected borrow positions and markets, so Borrow screens do not retain stale command results. +16. As a widget user, I want semantic invalidation to cover all query variants affected by a change, so a differently filtered or paginated view cannot remain stale. +17. As a widget user, I want an empty collection of requested identifiers to complete without network I/O, so empty UI states do not trigger meaningless requests. +18. As a widget user, I want requests containing repeated identifiers to avoid duplicate backend work, so enrichment and bulk loading are efficient. +19. As a widget user, I want a partial or missing identifier result represented explicitly, so missing remote facts are not mistaken for empty or successful data. +20. As a widget user, I want transport failures, decode failures, and violated response invariants distinguished, so the UI can present appropriate retry and error behavior. +21. As a widget user, I want a slower obsolete request unable to overwrite a newer result, so rapid wallet, network, or filter changes cannot publish stale data. +22. As a widget user, I want changing wallet or network identity to produce a different resource request, so cached data from one scope is never shown in another. +23. As a widget user, I want semantically equivalent input ordering to share a request where ordering is irrelevant, so harmless collection order changes do not fragment the cache. +24. As a widget user, I want semantically distinct requests to remain separate, so canonicalization never merges inputs that can produce different results. +25. As a widget user, I want large requests to be chunked or paginated behind the resource boundary, so transport limits do not leak into feature behavior. +26. As a widget user, I want a full-result resource to publish success only after its hidden pagination policy is satisfied, so callers do not accidentally treat a partial page as the complete fact. +27. As a widget user, I want a product-defined load-more experience to expose semantic continuation rather than raw transport page mechanics, so pagination behavior matches the UI intent. +28. As a widget user, I want provider enrichment to deduplicate provider lookups, so a list with many yields from one provider does not repeat the same work. +29. As a widget user, I want provider-enrichment failure behavior to be consistent and explicit, so one auxiliary failure cannot unpredictably change the whole catalog result. +30. As a widget user, I want prices, histories, rewards, KYC facts, and health data to follow the same ownership rules, so resource behavior is predictable beyond yields. +31. As a widget user, I want Activity reads to have one authoritative cache owner, so history summaries and detail surfaces do not independently fetch the same records. +32. As a widget user, I want Borrow catalog and position reads to share canonical resources, so distinct Borrow views do not create parallel caches. +33. As a widget user, I want remounting the widget after unmount to start with a fresh resource registry, so data and in-flight work from an old Widget Instance cannot leak into the new one. +34. As a widget user, I want unmounting the widget to interrupt or release resource work owned by that instance, so obsolete requests cannot continue publishing application state. +35. As a widget host, I want the public React component API to remain compatible, so this internal architecture change does not require integration changes. +36. As a widget host, I want the bundled renderer API to remain compatible, so CDN and imperative integrations continue to work. +37. As a feature developer, I want to request a named domain resource rather than choose an API method, stale time, retry count, or page size, so remote-data policy has one owner. +38. As a feature developer, I want resource inputs to be explicit and complete, so a resource can be understood and tested without hidden access to feature or wallet state. +39. As a feature developer, I want current-wallet and selected-yield binding to remain in the feature layer, so shared resources are reusable and do not import UI context. +40. As a feature developer, I want to derive view-specific models from canonical resource facts without creating another fetch atom, so presentation remains flexible without fragmenting the cache. +41. As a feature developer, I want read-only resource state exposed to React, so view code cannot mutate canonical storage or coordinate asynchronous fetching. +42. As a feature developer, I want command atoms to dispatch user intent and publish semantic invalidation after successful operations, so workflows remain near their domain behavior. +43. As a feature developer, I want simple operations to call an operation capability directly from their owning command atom, so the architecture does not require shallow pass-through workflow modules. +44. As a feature developer, I want complex operations to live in deep intent-owning workflow modules when they coordinate multiple steps, retries, state transitions, or cleanup, so that complexity is hidden behind a narrow command surface. +45. As a feature developer, I want Legacy POST scans treated as cacheable reads when they do not change server state, so HTTP verbs do not determine architectural ownership. +46. As a feature developer, I want Yield, Legacy, and Borrow read access represented by separate named capabilities, so a feature cannot receive an entire backend client merely to read one fact. +47. As a feature developer, I want Yield and Borrow operations represented separately from their read sources, so command access does not also grant cache-bypassing read access. +48. As a maintainer, I want every cacheable canonical remote fact to have exactly one Authoritative Resource, so ownership can be located without tracing feature-local fetches. +49. As a maintainer, I want Resources to be a tier of named modules rather than one global API atom registry, so each module remains deep, cohesive, and independently testable. +50. As a maintainer, I want exact semantic request sharing to be the default, so deduplication is correct without requiring a risky global normalized entity store. +51. As a maintainer, I want cross-request entity normalization enabled only for a resource whose merge semantics and identity invariants are explicitly proven, so partial responses cannot corrupt canonical facts. +52. As a maintainer, I want canonical decoded domain facts cached instead of generated transport DTOs, so transport schema changes have one adaptation seam. +53. As a maintainer, I want feature-shaped projections excluded from canonical storage, so one feature cannot silently redefine the data observed by another. +54. As a maintainer, I want each Authoritative Resource to normalize failures into a stable typed vocabulary, so raw generated-client and transport errors do not escape into features. +55. As a maintainer, I want freshness, polling, retry, concurrency, pagination, and stale-result policy owned by the resource, so callers cannot create divergent behavior for the same fact. +56. As a maintainer, I want cache state to use the existing Widget Instance Atom registry, so no module-global cache or ad hoc runtime undermines lifecycle and invalidation. +57. As a maintainer, I want generated backend clients hidden behind adapters, so application modules do not depend on generated client organization or transport details. +58. As a maintainer, I want the transport service to remain the single private owner of base URLs, API credentials, headers, retries, geo-block handling, and rich transport errors, so the refactor does not duplicate infrastructure. +59. As a maintainer, I want one implementation layer to be able to provide both read and operation capabilities for a backend, so capability separation does not require duplicate client construction. +60. As a maintainer, I want the broad Yield, Legacy, and Borrow API service interfaces removed after migration, so there is no permanent escape hatch around Authoritative Resources. +61. As a maintainer, I want dependency rules to reject feature imports of read sources, generated clients, and transport infrastructure, so cache ownership is mechanically enforced. +62. As a maintainer, I want dependency rules to reject Resource imports of feature state, so resources cannot gain hidden current-selection or current-wallet dependencies. +63. As a maintainer, I want operation capabilities available only to command and workflow owners that need them, so read-only view code cannot mutate remote state. +64. As a maintainer, I want reverse-dependency and hygiene checks to detect forbidden imports in continuous validation, so architectural erosion fails early. +65. As a maintainer, I want migration to proceed one resource-sized vertical slice at a time, so behavior can be proven and duplicate owners deleted incrementally. +66. As a maintainer, I want temporary adapters deleted inside the slice that introduces them, so the codebase never settles into permanent dual fetching or two authorities. +67. As a maintainer, I want each migrated resource to replace its old atoms and callers completely before the next slice depends on it, so cache behavior remains comprehensible during the transition. +68. As a maintainer, I want the clearest duplicated resources migrated first, so the architecture is validated against real shared-cache problems before lower-value endpoints move. +69. As a maintainer, I want the final dependency restriction enabled only after the legacy broad services are removed, so the migration can remain buildable without weakening the end-state rule. +70. As a test author, I want an Authoritative Resource's public contract to be the primary behavioral seam, so tests remain stable if pagination, caching, or adapter internals change. +71. As a test author, I want controllable in-memory read-source capabilities beneath resource tests, so request sharing, freshness, interruption, and failure behavior can be verified deterministically. +72. As a test author, I want backend adapter tests below the resource seam, so generated-client request mapping and transport error conversion are covered without duplicating resource behavior tests. +73. As an AI coding agent, I want named resources, explicit inputs, narrow capabilities, and enforced dependencies, so ownership and allowed change surfaces are discoverable from the module graph. +74. As a reviewer, I want a resource migration to show removal of duplicate fetch paths and broad-service imports, so centralization is demonstrated by deletion rather than only by adding a new abstraction. + +## Implementation Decisions + +### Resource ownership and vocabulary + +- An Authoritative Resource is the sole owner of one cacheable canonical remote fact. The term is architectural vocabulary and does not introduce a new product-domain concept. +- The Resources tier is a collection of separate named deep modules, not a single global registry API, generic endpoint wrapper, or file containing all atoms. +- Resource boundaries follow semantic facts rather than backend endpoint grouping or consuming feature structure. A resource may serve multiple features, and one feature may compose several resources. +- A backend endpoint may support several Authoritative Resources when consumers require different semantic contracts, such as demand-driven Pull, complete collection, bounded summary, or point lookup. Sharing is mandatory within an equivalent semantic contract, not across contracts with different completeness or continuation guarantees. +- Exact semantic request sharing is mandatory. Two calls that represent the same complete request identity use the same Effect Atom resource and share acquisition, cached state, revalidation, and failure state inside one Widget Instance. +- Cross-request entity normalization is opt-in per resource. It requires documented stable identity, field completeness, merge semantics, invalidation behavior, and evidence that combining responses cannot create a fact the backend never returned. +- Resources cache canonical decoded facts. Generated transport DTOs are adapted at the backend boundary, while feature-specific filtering, grouping, totals, selection, visibility, and presentation models remain downstream projections. +- Every resource exposes a named typed interface. Do not introduce a generic resource registry in which callers provide endpoint names, fetch functions, page policy, arbitrary cache keys, or retry options. +- A resource's internal mutable cache storage remains private. Consumers receive read-only state and narrowly defined refresh or retry commands where product behavior requires them. + +### Inputs, keys, and cache identity + +- Resource inputs contain the complete identity of the remote fact, including all wallet, network, protocol, yield, token, locale, filter, sort, and other request dimensions that can alter the result. +- Resources never read current Wallet Scope, route state, feature atoms, selected yield, visible collections, or configuration implicitly. Features snapshot or bind those values into explicit typed resource inputs. +- Key canonicalization is semantic and resource-specific. Irrelevant collection ordering and duplicate identifiers may be normalized; meaningful ordering, multiplicity, absence, default values, and filter distinctions must remain part of identity when they affect the response. +- Empty identifier collections and other semantically empty requests are handled before adapter I/O and return the resource-defined empty fact. +- Cache keys must not depend on unstable object identity, generated-client instances, timestamps, or caller-chosen cache labels. +- Cache state, request sharing, and in-flight fibers are scoped to the existing Atom registry owned by one Widget Instance. No module-global caches, Promise maps, secondary Atom registries, or ad hoc Effect runtimes are introduced. +- Sequential Widget unmount and remount creates fresh resource state. The design does not attempt to share caches between separate Widget Instances or browser documents. + +### Resource policy and failures + +- Each Authoritative Resource owns freshness duration, stale-while-revalidate behavior, polling eligibility and cadence, retry eligibility and backoff, request concurrency, chunking, pagination, stale-result suppression, and disposal behavior. +- Callers cannot override resource policy. A genuinely different product contract is modeled as a distinct semantic resource or a named operation on the same resource, not as caller-provided transport settings. +- Resources hide transport pagination. A remotely paginated product list defaults to demand-driven semantic Pull behavior, while a bounded canonical collection returns the complete applicable result only when a consumer explicitly requires completeness. Bounded summaries and point lookups remain separate contracts rather than scanning a complete collection. +- Demand-driven resources use one memoized Pull Atom per semantic query so equivalent consumers share acquisition and accumulated progress. A feature facade may map, filter, enrich, or otherwise project the Pull result, but it must forward the resource's Pull and Refresh commands instead of creating a second pagination stream. +- Pull resources rely on native Atom and Stream semantics for continuation, accumulation, waiting, failure, disposal, and refresh. Continuation is derived only from the backend response's offset, limit, and total. Refresh reconstructs the stream from its first page, and a later-page failure retains already accumulated values according to native Pull behavior. +- Repeated Pull while a request is waiting is prevented by the UI's published waiting state. Resources do not introduce private page caches, manual offset state, request locks, replay buffers, or custom refresh generations to coordinate pagination. +- Bulk and enrichment resources deduplicate identifiers and share repeated subrequests where semantics permit it. Provider enrichment explicitly defines whether auxiliary failures fail the whole fact, produce typed partial data, or use a documented fallback. +- Resource acquisition maps request, transport, decode, and invariant failures into stable typed resource failures. Raw generated-client exceptions and transport DTO error unions do not cross the resource interface. +- Missing requested entities are modeled explicitly according to the resource contract. They are not silently dropped when dropping them would make a partial response appear complete. +- Refresh generations, interruption, or equivalent Atom resource semantics prevent results from obsolete inputs or disposed scopes from replacing newer state. +- Retry repeats the resource-owned acquisition policy. User refresh and retry may trigger direct refresh; ordinary command completion uses semantic invalidation instead. + +### Commands, operations, and invalidation + +- The Resources tier owns cacheable reads only. It does not own transactions, mutation workflows, signing, submissions, multi-step commands, or user-intent state machines. +- A feature's command atom may call an operation capability when the action is simple and the atom already forms a deep intent boundary. Do not add a pass-through command module that only renames one operation call. +- A dedicated operation or workflow module is introduced when it hides meaningful coordination such as multiple backend calls, wallet interaction, retries, concurrency, state transitions, compensation, or scoped cleanup. +- Successful operations publish semantic invalidation events such as wallet balances changed, yield positions changed, activity changed, borrow positions changed, or borrow markets changed. Events describe changed facts rather than naming cache keys or feature screens. +- Each Authoritative Resource declares which semantic invalidations affect it and refreshes or marks stale every relevant query variant. Operation modules do not enumerate individual cached request keys. +- Semantic invalidation is the normal cross-module coherence mechanism. It can invalidate resources not currently mounted without forcing an immediate fetch; active resource policy determines revalidation timing. +- Direct refresh remains available only where a user explicitly refreshes or retries a resource. Features do not use direct refresh as a substitute for publishing the semantic effects of a command. + +### Backend capabilities and dependency boundaries + +- Replace broad backend services with coarse capability ports: `YieldResourceSource`, `YieldOperations`, `LegacyResourceSource`, `BorrowResourceSource`, and `BorrowOperations`. +- Read-source capabilities contain backend access needed to acquire canonical facts. Operation capabilities contain state-changing calls. They use distinct service contracts and import paths even when one shared implementation layer constructs and provides both. +- Legacy exposes no operations capability until it has a genuine state-changing use case. A POST request used for token scanning remains part of `LegacyResourceSource` because query versus command is determined by semantics, not HTTP method. +- Resource modules may depend on the corresponding read-source capability. Features, view adapters, and workflows do not import read-source capabilities directly. +- Feature command atoms and intent-owning workflow modules may depend on operation capabilities. Read-only resource modules do not depend on operation capabilities. +- Generated Yield, Legacy, and Borrow clients remain private implementation details of backend adapters. They are not imported by Resources, features, React views, or domain modules. +- The transport service remains private infrastructure responsible for client construction, base URLs, credentials, common headers, retry behavior, geo-block recognition, and transport-level error detail. +- Broad `YieldApiService`, `LegacyApiService`, and `BorrowApiService` contracts are removed after their callers migrate. No compatibility facade remains that exposes the full generated-client-shaped surface internally. +- The public widget component and bundled renderer remain compatible. Removing broad backend services is an internal architecture change and does not remove or rename the package's public bundle entry. +- The mechanically enforced dependency direction is application composition to features to Resources to application runtime to services to domain/shared foundations, with narrow exceptions for established foundational dependencies that do not bypass resource ownership. +- Boundary checks prohibit features from importing read sources, generated clients, or transport infrastructure; prohibit Resources from importing features or React; and prohibit application logic from using React-owned fetching. +- Operation-capability access is permitted only from owning command or workflow modules. The rule should be encoded with module-boundary and restricted-import checks rather than relying on filename convention or review memory alone. + +### Initial resource boundaries + +- Yield detail and initial-yield lookups become one authoritative semantic resource when their endpoint and identity are equivalent. Caller-specific initialization behavior remains a feature projection or workflow concern. +- General yield listing, available-yield listing, and token-scope yield listing share the same canonical listing resource where they are endpoint-equivalent. Availability and token-scope views are derived or expressed as explicit semantic inputs rather than separate fetch owners. +- Yield positions become one canonical resource used by Earn and Portfolio. Selection and visibility filtering occur after canonical position accounting so historical positions cannot disappear from totals. +- Token-balance scanning becomes one canonical resource. Earn- and Portfolio-specific schemas become projections from one decoded fact rather than separate requests. +- Validator discovery has separate semantic contracts over the same endpoint: ordinary and search results use one shared demand-driven Pull per query; preferred validators use an explicit complete resource; and address resolution uses a bounded point resource. Search may advance independent name and address branches concurrently, then merge and deduplicate them deterministically. +- Yield Directory and provider data use named resources with explicit enrichment and failure semantics. The complete directory contract is bounded to explicit requested Yield IDs. Category discovery is a separate bounded-summary contract that issues one first-page request per category using the API maximum page size and checks eligible results from that response. +- Yield-backed token discovery uses a shared demand-driven Pull. Legacy token options remain a complete non-paginated resource and are projected as immediately complete when the product surface supports both sources. +- Activity history uses a shared demand-driven Pull that emits semantic batches containing actions and the backend total. Filter counts use bounded summary requests; neither path scans a complete history. Activity enrichment is a projection over the canonical Pull and does not create a second pagination stream. +- Prices, histories, KYC facts, rewards, and health data each receive a named owner as their slices migrate; they are not placed into a miscellaneous shared API module. +- Yield Directory remains an explicit complete resource for a bounded set of requested Yield IDs, and Borrow Markets remains complete because market-position resolution requires the full applicable set. No Yield resource scans the unfiltered complete catalog; category availability uses the bounded per-category summary contract. +- Borrow catalogs and positions otherwise receive named resources with complete identity and semantic invalidation from Borrow operations. + +### Migration and delivery + +- Migrate by complete vertical resource slices rather than converting every backend service in one change. Each slice introduces the Authoritative Resource, capability access, canonical model, tests, caller migrations, invalidation behavior, and deletion of all replaced fetch owners. +- The first slice covers yield positions and token balances because they have clear cross-feature duplicates and exercise explicit Wallet Scope, canonical models, semantic invalidation, and shared cache behavior. +- The second slice covers the Yield Directory, yield listings, categories, and provider enrichment. The third covers validators. The fourth covers Activity. The fifth covers prices, histories, KYC, rewards, and health. The sixth covers Borrow catalogs and positions. +- Within each slice, temporary adapters may delegate to existing infrastructure only while callers are being moved. They are deleted before the slice is considered complete; there is no permanent dual-fetch period or two cache authorities. +- A migrated caller must consume the resource or a zero-logic feature projection over it. It must not retain a fallback path to the broad backend service. +- Existing freshness and retry behavior is inventoried before choosing the canonical resource policy. Where feature policies conflict, the resource adopts one explicit semantic policy and tests make the behavior change visible. +- Existing results are audited for endpoint equivalence before atoms are merged. Similar names alone do not prove equivalent request identity or response semantics. +- The broad backend services are removed only after all of their reads and operations have moved to the relevant capability ports. The final strict import rule is enabled in the same concluding slice so no unrestricted service path remains. +- Architecture documentation and the accepted decision record remain aligned with the implementation as each slice lands. + +## Testing Decisions + +- The primary behavioral test seam is each named Authoritative Resource interface. Tests construct it inside a fresh Atom registry with controllable in-memory read-source capabilities and observe the same read-only state and commands available to feature consumers. +- This seam is preferred over testing internal atom helpers, cache maps, generated-client calls, or feature hooks because it protects the architectural contract while allowing pagination, canonicalization, and adapter internals to change. +- Backend adapter tests form a lower seam. They verify capability-to-generated-client request mapping, authentication and transport integration boundaries, DTO decoding inputs, and transport failure conversion without repeating cache-policy tests. +- Operation-capability and intent-module tests form a separate lower seam for commands. They verify operation request mapping, typed failures, multi-step coordination where present, and publication of semantic invalidations. +- A contract suite proves that two consumers making exactly equivalent requests receive one resource identity and cause one acquisition while the result is fresh. +- Equivalent-request tests cover separate feature projections, simultaneous subscriptions, sequential subscriptions, reordered set-like identifiers, duplicate identifiers, and normalized defaults that are semantically equal. +- Distinct-request tests cover every identity dimension that can alter a response, including wallet owner, network, yield, token set, filter, sort, locale, protocol, and pagination mode where applicable. +- Empty-input tests prove a semantic empty result and zero read-source calls. +- Key-stability tests prove that cache identity does not rely on object reference, caller name, timestamps, or generated-client identity. +- Canonicalization tests prove that only semantically irrelevant ordering and duplicates are normalized. Counterexamples prove meaningful ordering or multiplicity is not collapsed. +- Lifecycle tests prove cache sharing within one Widget Instance registry, interruption on disposal, no publication from an obsolete generation, no module-global retention, and a fresh cache after sequential unmount and remount. +- Freshness tests use controllable time to cover fresh reuse, stale publication, revalidation, expiry, polling eligibility, and resource-specific retry policy without real timers or network calls. +- Concurrency tests prove multiple subscribers share one in-flight acquisition, a newer generation wins over a slower obsolete request, cancellation does not corrupt a replacement request, and configured request concurrency limits are respected. +- Failure tests independently cover request construction, transport, decoding, invariant, missing-entity, and unsupported-input failures. They assert stable resource failure types and prove raw adapter exceptions do not escape. +- Retry tests prove retry eligibility is owned by the resource, non-retryable failures do not loop, a later-page failure preserves accumulated values, and explicit Retry or Refresh restarts the shared Pull from its first page. +- Manual-refresh tests prove user refresh uses the resource interface and cannot create a feature-local second fetch path. +- Complete-resource pagination tests cover zero pages, one page, multiple pages, backend page-size boundaries, ordering, deduplication, and publication only after the full canonical collection is acquired. +- Semantic Pull tests prove initial acquisition requests only the first backend page, each accepted Pull advances by one backend continuation, no page is fetched eagerly, equivalent consumers share one Pull identity and accumulated progress, waiting disables repeated Pull, and the interface does not leak raw generated-client pagination structures. +- Pagination continuation tests prove the next request is derived from the backend response's offset, limit, and total rather than the requested limit or decoded item count. +- Chunking tests cover empty chunks, exact transport-limit boundaries, multiple chunks, duplicate identifiers across chunks, stable result assembly, bounded concurrency, partial failure, missing IDs, and deterministic canonical ordering. +- Provider-enrichment tests cover no providers, one provider referenced repeatedly, many providers, deduplicated lookup, auxiliary failure policy, missing provider records, and preservation of the base canonical result according to the declared contract. +- Yield-detail tests prove endpoint-equivalent initial and ordinary lookups share one request while feature-specific initialization decisions remain outside the resource. +- Yield-list tests prove endpoint-equivalent catalog views share acquisition and that selection, availability, and token-scope projections do not create new fetches. +- Category tests prove discovery issues exactly one request per category with `offset: 0` and the API maximum page size, applies the category's Yield types, and derives visibility from eligible results in that bounded response. +- Position tests prove Earn and Portfolio share aggregate position acquisition, all historical positions remain available to accounting projections, and visible-yield filtering cannot alter canonical totals. +- Token-balance tests prove Earn and Portfolio share scanning for the same Wallet Scope and token request, while their differing view schemas are pure projections. +- Validator tests prove ordinary and search queries acquire one page per Pull and share progress across equivalent consumers, preferred validators explicitly acquire the complete result, address lookup remains bounded, and search merges its independently paginated name and address branches deterministically. +- Activity tests prove history acquires one backend page per Pull, exposes action batches with the backend total, uses bounded total requests for filter counts, never completes history merely to paginate in memory, and receives command invalidation without eagerly fetching inactive variants. +- Token-discovery tests prove the Yield source acquires one page per Pull while the Legacy source publishes its complete non-paginated list immediately and exposes no fake continuation. +- Borrow tests prove catalogs, markets, and positions share within their exact identities and respond to the appropriate Borrow operation invalidations. +- Semantic-invalidation tests publish each supported event and assert that every affected resource variant becomes stale or revalidates according to its policy, while unrelated resources remain fresh. +- Multi-variant invalidation tests include different filters, Wallet Scopes, networks, pages, and mounted/unmounted states so invalidation cannot accidentally target only the command caller's current view. +- Operation tests prove successful commands publish invalidation only after the remote operation succeeds, failed commands do not claim nonexistent changes, and repeated invalidation is safe. +- Feature integration tests mount representative Earn, Portfolio, Activity, and Borrow adapters against one registry and prove their projections share resource acquisition without importing read-source capabilities. +- React adapter tests prove views only render resource state and synchronously dispatch commands; they do not own fetching, retry loops, duplicated loading state, or raw error normalization. +- Dependency tests and hygiene checks prove features cannot import read sources, generated clients, or transport infrastructure; Resources cannot import features or React; and generated clients are reachable only from private adapters. +- Migration-slice tests include a search or dependency assertion that no replaced broad-service call or old fetch atom remains for the migrated fact. +- Selective-normalization tests are required only for resources that opt into cross-request entity normalization. They prove entity identity, completeness, merge order independence, invalidation, deletion, and partial-response behavior; absence of these proofs means the resource remains exact-request cached. +- Regression coverage preserves public React and bundled entry behavior, one concurrently mounted Widget Instance, sequential remount, existing routes, and existing feature presentation unless a slice explicitly changes a previously inconsistent cache policy. +- The validation ladder for each slice includes focused resource contract tests, adapter or operation tests when touched, affected feature integration tests, widget lint and type checking, and dependency-hygiene enforcement. Representative browser tests are added where cache sharing, navigation, polling, or lifecycle behavior cannot be proven at a lower seam. + +## Out of Scope + +- Building one global API atom module, generic endpoint registry, generic query builder, or caller-configurable cache framework. +- Introducing a normalized entity store across all endpoints without resource-specific identity and merge proofs. +- Sharing cache state across concurrently mounted Widget Instances, browser documents, users, or sequential application-runtime generations. +- Adding support for multiple concurrently mounted Widget Instances. +- Replacing the existing Atom registry or application Effect runtime with a new caching runtime. +- Introducing page-level cache atoms, manual offset or accumulation atoms, pagination locks, replay buffers, or custom refresh state machines for semantic Pull resources. +- Introducing React Query, hook-owned fetches, Promise caches, module-global maps, or React effects for resource acquisition. +- Moving transactions, signing, submissions, user-intent workflows, or command state machines into the Resources tier. +- Creating shallow operation modules that only forward one call without hiding coordination or policy. +- Changing backend endpoint contracts, generated SDK behavior, authentication, base URL selection, geo-block policy, or transport retry infrastructure except where needed to expose the agreed capability ports. +- Changing public package exports, the React component contract, the bundled renderer contract, routes, UI design, translations, or product copy. +- Guaranteeing that similarly named endpoints are equivalent without auditing their complete request and response semantics. +- Preserving conflicting feature-specific stale times merely for internal behavioral compatibility; each resource must choose and document one canonical policy. +- Performing the migration as an all-at-once rewrite or retaining permanent compatibility facades around the broad backend services. +- Adding Authoritative Resource to the product domain glossary; it remains implementation architecture terminology. + +## Further Notes + +- The centralization goal is ownership, not physical colocation. A discoverable Resources tier provides one place in the architecture to look, while separate named modules preserve cohesion and information hiding. +- “Route commands through deep intent-owning operation modules” means a user intent with meaningful orchestration is represented by a module that owns that orchestration and consumes an operation capability. It does not mean every generated-client method receives a same-shaped command wrapper. +- `YieldResourceSource` and `YieldOperations`, and their Legacy and Borrow counterparts, are Effect service contracts. They are capability seams below Resources and commands, not resource caches themselves. +- The transport service and generated clients still exist as private implementation machinery. What disappears is the ability for arbitrary application code to receive the full backend client surface. +- POST-based reads demonstrate why semantic ownership matters more than HTTP conventions: a scan that only retrieves facts belongs to a read source and may be cached by an Authoritative Resource. +- Exact-request sharing is the safe baseline. It delivers the primary benefit—one cache and one in-flight acquisition for equivalent calls—without creating the correctness risks of merging partial entities returned by different endpoints. +- Pagination mode is part of the semantic contract. Two consumers share acquisition and progress when they use the same Pull contract; a complete, summary, or point contract over the same endpoint remains distinct because it promises a different fact. +- Historical behavior is intentionally preserved where it represented product semantics: Activity history, Yield token discovery, and ordinary/search validators remain demand-driven; preferred validators and Borrow Markets remain complete; Yield Directory is complete only for explicit requested IDs; category discovery remains one bounded maximum-size request per category; and Legacy token options remain a complete non-paginated list. +- The staged order is intentionally front-loaded with duplicated Yield and Legacy reads. Those slices validate the architecture's hardest requirements: cross-feature sharing, explicit Wallet Scope, feature-independent canonical models, pagination, and semantic invalidation. +- A slice is complete only when the new resource is authoritative in practice: all intended callers use it, duplicate fetch owners are deleted, invalidation is wired, its capability boundary is tested, and forbidden direct imports cannot reappear. diff --git a/.scratch/classic-transaction-flow/issues/01-enforce-one-widget-instance-lifecycle.md b/.scratch/classic-transaction-flow/issues/01-enforce-one-widget-instance-lifecycle.md new file mode 100644 index 000000000..c893b95b4 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/01-enforce-one-widget-instance-lifecycle.md @@ -0,0 +1,17 @@ +# 01 — Enforce one Widget Instance lifecycle + +**What to build:** Enforce the supported host behavior of at most one mounted Widget Instance per browser document. A second mount must fail deterministically without disturbing the active widget, while bundled hosts can explicitly unmount and later create a fresh instance. + +**Blocked by:** None — can start immediately. + +**Status:** ready-for-agent + +- [ ] The public embedding boundary acquires one document-level claim before wallet and application providers initialize. +- [ ] The claim uses a stable document-owned identity that detects conflicts across separately bundled widget copies or versions. +- [ ] A second concurrent mount fails immediately with a deterministic internal error name and message, without warning-only behavior, takeover, degradation, or changes to the active instance. +- [ ] The second-mount error is not added to the published package error surface. +- [ ] The bundled renderer returns additive `rerender` and `unmount` operations; rerender retains the claim and unmount releases it with the widget runtime. +- [ ] Sequential unmount and remount creates a clean Widget Instance and fresh runtime generation. +- [ ] Claim ownership prefers a scoped Effect exposed through Atom lifecycle. Any required React mount bridge is isolated as the named embedding boundary and contains no wallet or feature logic. +- [ ] Contract tests cover first mount, rejected second mount, preservation of the first instance, cross-bundle conflict, rerender, unmount, claim release, and sequential remount. +- [ ] Existing published React and bundled entry contracts remain compatible apart from additive bundled unmounting. diff --git a/.scratch/classic-transaction-flow/issues/02-remove-multi-instance-wallet-concurrency.md b/.scratch/classic-transaction-flow/issues/02-remove-multi-instance-wallet-concurrency.md new file mode 100644 index 000000000..41ef6c29f --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/02-remove-multi-instance-wallet-concurrency.md @@ -0,0 +1,15 @@ +# 02 — Remove unsupported multi-instance wallet concurrency + +**What to build:** Simplify wallet runtime code around the supported single Widget Instance contract. Remove synchronization and isolation behavior that exists only for simultaneous widgets while preserving every concurrency mechanism needed for concurrent work inside one widget and for clean sequential remounts. + +**Blocked by:** 01 — Enforce one Widget Instance lifecycle. + +**Status:** ready-for-agent + +- [ ] Audit wallet synchronization and isolation mechanisms by production purpose before removing them. +- [ ] Remove redundant reconnect-initialization serialization and its multiple-initializer contract when Wallet Bootstrap can invoke initialization only once per runtime generation. +- [ ] Remove or rewrite tests that claim simultaneous widget registries or runtimes are a supported production behavior. +- [ ] Retain connector-membership serialization, serialized event and command handling, queues, references, streams, resource deduplication, scoped fibers, and interruption required within one Widget Instance. +- [ ] Retain fresh runtime scopes and disposal behavior required for sequential unmount and remount. +- [ ] Wallet Bootstrap, reconnect, connector discovery, commands, and cleanup retain their existing supported behavior for one widget. +- [ ] Focused wallet tests distinguish removed multi-instance assumptions from retained intra-instance races and lifecycle guarantees. diff --git a/.scratch/classic-transaction-flow/issues/03-enforce-effect-atom-application-boundary.md b/.scratch/classic-transaction-flow/issues/03-enforce-effect-atom-application-boundary.md new file mode 100644 index 000000000..25c3fdf03 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/03-enforce-effect-atom-application-boundary.md @@ -0,0 +1,16 @@ +# 03 — Establish the Effect and Atom application boundary + +**What to build:** Establish React-as-view-layer as the documented design boundary for the Classic Flow effort and future materially refactored code. Application logic must live in deterministic functions, Effect, and Effect Atom, with React limited to rendering, synchronous intent dispatch, presentation-only local state, and explicitly named external boundaries. + +**Blocked by:** 02 — Remove unsupported multi-instance wallet concurrency. + +**Status:** ready-for-agent + +- [ ] Application-logic modules, including the future Classic Flow core and facade, do not import React. +- [ ] Touched view adapters do not use `useEffect` unless the use is isolated in an explicitly named and reviewed external boundary. +- [ ] The document-claim embedding bridge remains only if scoped Effect and Atom lifecycle cannot safely express the React mount contract. +- [ ] The rules permit local synchronous presentation state only when it has no domain meaning, asynchronous behavior, persistence, route lifetime, or cross-component coordination. +- [ ] React event handlers are constrained to normalizing UI input and synchronously dispatching Atom commands; Effect execution and Promise awaiting are not permitted there. +- [ ] Feature facades may expose read-only view Atoms, writable command Atoms, and zero-logic React convenience hooks while keeping mutable storage private. +- [ ] Existing unrelated React effects and React Query usage remain outside this effort's touched scope. +- [ ] The accepted architecture decision and agent guidance remain aligned with the implementation. diff --git a/.scratch/classic-transaction-flow/issues/04-establish-pure-classic-flow-core.md b/.scratch/classic-transaction-flow/issues/04-establish-pure-classic-flow-core.md new file mode 100644 index 000000000..40ce351f6 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/04-establish-pure-classic-flow-core.md @@ -0,0 +1,18 @@ +# 04 — Establish the pure Classic Flow core + +**What to build:** Create a deterministic Classic Transaction Flow model that makes intake identity, variants, phases, replacement, abandonment, and projections explicit without depending on React, Atom storage, browser globals, clocks, randomness, or asynchronous execution. + +**Blocked by:** 03 — Enforce the Effect and Atom application boundary. + +**Status:** ready-for-agent + +- [ ] The model is one tagged union with Enter, Exit, Manage, and Activity Resume variants rather than nullable variant fields. +- [ ] A branded Classic Transaction Flow Identity is injected when a flow starts; the core does not generate identity or read time. +- [ ] Starting Enter, Exit, or Manage creates a Reviewing flow with immutable intake facts; Activity Resume creates an Executable flow with its existing Yield Action. +- [ ] Starting a flow atomically replaces the previous active flow. +- [ ] A targeted Reviewing flow can attach one Yield Action exactly once and transition one-way to Executable. +- [ ] Attachment enforces active identity, phase, and exactly-once invariants without adding cross-field Yield Action content validation. +- [ ] Stale transition attempts return a typed stale-flow result; stale targeted abandonment is an idempotent no-op; other invariant failures leave state unchanged. +- [ ] Pure projections cover Action-preview input, review-pricing input, gas-warning input, Wallet Scope validity, narrow variant snapshots, and Executable Transaction Workflow handoff. +- [ ] Wallet Scope owner comparison remains network plus primary address, with case-insensitive EVM addresses and no invalidation for additional-address-only changes. +- [ ] Table-driven tests cover every variant, transition, replacement, stale case, immutable fact, Back identity rule, and deterministic projection without mounting React. diff --git a/.scratch/classic-transaction-flow/issues/05-build-effect-atom-classic-flow-facade.md b/.scratch/classic-transaction-flow/issues/05-build-effect-atom-classic-flow-facade.md new file mode 100644 index 000000000..8227444ca --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/05-build-effect-atom-classic-flow-facade.md @@ -0,0 +1,18 @@ +# 05 — Build the Effect and Atom Classic Flow facade + +**What to build:** Wrap the pure Classic Flow core in one Effect/Atom application interface that owns reactive state, commands, Action preparation, typed asynchronous state, concurrency, lifecycle resources, and declarative navigation outcomes while keeping mutable storage and Yield Action attachment private. + +**Blocked by:** 04 — Establish the pure Classic Flow core. + +**Status:** ready-for-agent + +- [ ] The facade exposes read-only active and narrow view Atoms plus writable start, Continue, Retry, and targeted-abandon command Atoms; mutable storage is private. +- [ ] Effects run through the appropriate existing scoped application or wallet Atom runtime with injected services; the feature creates no ad hoc runtime and calls neither `Effect.runPromise` nor React-owned asynchronous APIs. +- [ ] Action preview, pricing, gas checks, retries, and invalidation use Effect resources exposed through Atom rather than React Query, hook-owned fetching, or Promise caches. +- [ ] Continue owns preparation loading, typed failures, retry eligibility, exactly-once attachment, and a declarative navigation outcome. +- [ ] Repeated Continue intents for one flow coalesce into one in-flight Action preparation; Retry is enabled after failure rather than creating parallel work. +- [ ] Replacement or abandonment interrupts preparation when possible, and identity checks silently discard any stale completion that cannot be interrupted. +- [ ] Action-preview resource identity includes Classic Transaction Flow Identity; price and gas resources may preserve reusable domain keys across flows. +- [ ] Activity Resume can produce the same Continue navigation outcome without previewing or changing its already-Executable phase. +- [ ] Lifecycle Atoms own acquisition, interruption, abandonment, and finalization for route-scoped work; widget-runtime resources remain scoped to their runtime generation. +- [ ] Facade tests drive Atom commands and observations directly, covering loading, failure, retry, coalescing, interruption, stale suppression, cache identity, navigation outcomes, and scope closure without React effect flushing. diff --git a/.scratch/classic-transaction-flow/issues/06-migrate-enter-journey.md b/.scratch/classic-transaction-flow/issues/06-migrate-enter-journey.md new file mode 100644 index 000000000..700041385 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/06-migrate-enter-journey.md @@ -0,0 +1,19 @@ +# 06 — Migrate the Enter journey + +**What to build:** Move the complete Enter user journey onto the Classic Flow facade, from starting immutable intake through review, Continue, Back, execution, and completion. React must remain a view adapter and the slice must preserve current classic and dashboard behavior. + +**Blocked by:** 05 — Build the Effect and Atom Classic Flow facade. + +**Status:** ready-for-agent + +- [ ] Enter entry points start one Reviewing Classic Transaction Flow through the tagged start command. +- [ ] Enter review consumes a narrow read-only view Atom and normalized pricing and gas-warning projections rather than the legacy request authority. +- [ ] Continue handlers synchronously dispatch the Atom command; React does not await work, run Effects, infer loading locally, or use `useEffect` to navigate. +- [ ] The route adapter renders the declarative navigation outcome only when preparation succeeds for the still-active flow. +- [ ] Returning from Executable to review abandons the old Enter flow and starts a new Reviewing identity with the same immutable intake facts. +- [ ] Continuing the Back-created flow performs a fresh Action preview and attaches a fresh Yield Action. +- [ ] Wallet Scope disconnect or owner change follows existing fallback behavior; casing-only EVM and additional-address-only changes preserve the flow. +- [ ] Tracking and KYC work is initiated by Effect-backed commands at the existing intent or transition boundary with compatible timing and payloads. +- [ ] React code introduced or materially changed by this slice contains only rendering, synchronous intent dispatch, presentation-only local state, and named boundary integration. +- [ ] Unit, DOM, and representative browser tests cover Enter review, loading/failure/retry, Continue, Back, fresh action preparation, steps, completion, routing, and preserved copy. +- [ ] This migration remains on the shared integration branch until ticket 10 completes the atomic cutover. diff --git a/.scratch/classic-transaction-flow/issues/07-migrate-exit-manage-journeys.md b/.scratch/classic-transaction-flow/issues/07-migrate-exit-manage-journeys.md new file mode 100644 index 000000000..da55f65d0 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/07-migrate-exit-manage-journeys.md @@ -0,0 +1,18 @@ +# 07 — Migrate the Exit and Manage journeys + +**What to build:** Move complete Exit and pending-action Manage journeys onto the Classic Flow facade while preserving their distinct review inputs, warnings, routes, tracking, execution, and completion behavior. + +**Blocked by:** 05 — Build the Effect and Atom Classic Flow facade. + +**Status:** ready-for-agent + +- [ ] Exit and Manage entry points start tagged Reviewing flows with immutable variant-specific intake facts. +- [ ] Variant-specific views consume narrow read-only Atoms while shared preview, pricing, gas-warning, Wallet Scope, and lifecycle behavior use normalized facade projections. +- [ ] Continue, loading, typed failure, Retry, and navigation use writable command and view Atoms without React-owned asynchronous orchestration. +- [ ] Returning from Executable Exit or Manage to review creates a new Reviewing identity with the same intake facts and forces a fresh Action preview. +- [ ] The refactor does not inspect, block, compensate for, or model irreversible wallet or submission side effects that may have preceded Back. +- [ ] Tracking and KYC calls remain Effect-backed and preserve existing timing, ordering, payloads, and user-visible state. +- [ ] Existing classic and dashboard routes, Wallet Scope redirects, warnings, steps, completion behavior, and copy remain compatible. +- [ ] Touched React code contains no application logic or unreviewed `useEffect` boundaries. +- [ ] Unit, DOM, and representative browser tests cover both variants through review, Continue, failure/retry, Back, fresh preparation, execution, and completion. +- [ ] This migration remains on the shared integration branch until ticket 10 completes the atomic cutover. diff --git a/.scratch/classic-transaction-flow/issues/08-migrate-activity-resume-lifetime.md b/.scratch/classic-transaction-flow/issues/08-migrate-activity-resume-lifetime.md new file mode 100644 index 000000000..7c264c85d --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/08-migrate-activity-resume-lifetime.md @@ -0,0 +1,18 @@ +# 08 — Migrate Activity Resume and unified flow lifetime + +**What to build:** Replace Activity selection as a parallel transaction authority with the Activity Resume Classic Flow variant, and make every variant obey one route, Wallet Scope, runtime-generation, replacement, and unmount lifetime. + +**Blocked by:** 05 — Build the Effect and Atom Classic Flow facade. + +**Status:** ready-for-agent + +- [ ] Selecting an existing activity Yield Action starts an Executable Activity Resume flow with a new injected Classic Transaction Flow Identity. +- [ ] Activity Resume never requests a new Action preview for its existing Yield Action. +- [ ] Back retains the same Activity Resume identity and Yield Action; a later Continue produces declarative steps navigation for that same flow. +- [ ] Starting any Classic Flow atomically replaces the previous active variant, including replacement between Activity Resume and Enter, Exit, or Manage. +- [ ] The active flow survives ordinary props updates, live rerenders, and bundled rerender. +- [ ] Route exit, Wallet Scope disconnect or owner change, application-runtime generation replacement, and widget unmount abandon the targeted active flow through Atom/Effect lifecycle ownership. +- [ ] Stale lifecycle finalization cannot clear a newer flow, and additional-address-only changes do not invalidate Wallet Scope ownership. +- [ ] React route adapters mount lifecycle Atoms and render view state; they do not coordinate cleanup or workflow transitions through `useEffect`. +- [ ] Unit, DOM, and browser tests cover activity selection, review, Back/Continue identity retention, replacement by other variants, every lifecycle boundary, and existing activity behavior. +- [ ] This migration remains on the shared integration branch until ticket 10 completes the atomic cutover. diff --git a/.scratch/classic-transaction-flow/issues/09-isolate-transaction-workflow-handoff.md b/.scratch/classic-transaction-flow/issues/09-isolate-transaction-workflow-handoff.md new file mode 100644 index 000000000..f4c47d058 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/09-isolate-transaction-workflow-handoff.md @@ -0,0 +1,18 @@ +# 09 — Isolate the Transaction Workflow handoff + +**What to build:** Make Transaction Workflow consume one pure handoff projected from an Executable Classic Transaction Flow, with execution-machine lifetime separated by Classic Flow identity while preserving Yield Action identity and all existing execution semantics. + +**Blocked by:** 06 — Migrate the Enter journey; 07 — Migrate the Exit and Manage journeys; 08 — Migrate Activity Resume and unified flow lifetime. + +**Status:** ready-for-agent + +- [ ] Transaction Workflow handoff is projected from the active Executable flow and is not stored as a second authority. +- [ ] Classic Transaction Flow Identity participates in the local execution-machine generation key so distinct flows cannot share machine state through structural equality. +- [ ] Yield Action ID remains the workflow diagnostic, error, history, and API identity. +- [ ] Activity Resume Back and Continue reuse the same machine generation because the Classic Flow identity is retained. +- [ ] Enter, Exit, or Manage Back creates a new machine generation after the new flow prepares its fresh Yield Action. +- [ ] Submission, signing, confirmation, pending, retry, failure, and completion remain Transaction Workflow state and do not become Classic Flow phases. +- [ ] The Classic flow remains Executable through steps and completion and is abandoned only at its established lifecycle boundary. +- [ ] Execution state and commands remain Effect/Atom-owned; React consumes view Atoms and synchronously dispatches intents without workflow orchestration. +- [ ] Integration tests cover generation separation, Activity Resume reuse, preserved Yield Action diagnostics, completion/history effects, and unchanged execution behavior. +- [ ] This migration remains on the shared integration branch until ticket 10 completes the atomic cutover. diff --git a/.scratch/classic-transaction-flow/issues/10-contract-legacy-intake-and-verify-cutover.md b/.scratch/classic-transaction-flow/issues/10-contract-legacy-intake-and-verify-cutover.md new file mode 100644 index 000000000..cc9b10743 --- /dev/null +++ b/.scratch/classic-transaction-flow/issues/10-contract-legacy-intake-and-verify-cutover.md @@ -0,0 +1,19 @@ +# 10 — Contract legacy intake and verify the atomic cutover + +**What to build:** Complete the atomic Classic Flow cutover by deleting every superseded transaction-intake authority and proving that supported embedding, routing, wallet, review, execution, Effect/Atom ownership, and public API behavior remain coherent as one merge-ready change. + +**Blocked by:** 06 — Migrate the Enter journey; 07 — Migrate the Exit and Manage journeys; 08 — Migrate Activity Resume and unified flow lifetime; 09 — Isolate the Transaction Workflow handoff. + +**Status:** ready-for-agent + +- [ ] Delete the separate Enter, Exit, Manage, and Activity selection state authorities, their setters, action-presence phase encoding, and variant lifecycle atoms and guards. +- [ ] Delete duplicate cross-cutting variant switches for Action preview, review pricing, gas warnings, route validity, and Transaction Workflow handoff. +- [ ] Delete hook-owned asynchronous orchestration, React Query or Promise caching introduced by the legacy Classic paths, and React effects that no longer qualify as named external boundaries. +- [ ] Delete shallow tests for removed atoms, setters, object-identity cleanup, and simultaneous Widget Instance isolation; retain or replace their supported behavior at facade, adapter, browser, and embedding seams. +- [ ] Confirm there is one active Classic Transaction Flow authority with no mirrored writes, compatibility adapter, legacy fallback, or externally writable storage. +- [ ] The final implementation keeps application logic React-free, view handlers synchronous, storage private, resources Effect-backed, cleanup scoped, and external boundaries explicit and reviewed. +- [ ] Representative Enter, Exit, Manage, and Activity Resume browser journeys pass in classic and dashboard routing, including deep links, review, Continue, Back, steps, completion, Wallet Scope redirects, tracking, KYC, warnings, and preserved copy. +- [ ] Embedding tests pass for second-mount rejection, first-instance preservation, bundled rerender/unmount, and sequential remount. +- [ ] Published React and bundled interfaces remain compatible apart from the additive bundled `unmount` operation. +- [ ] Focused unit, DOM, and browser suites pass, followed by widget lint/type checking and relevant hygiene checks. +- [ ] The integration branch is green and ready to land as one atomic Classic Flow migration. diff --git a/.scratch/classic-transaction-flow/spec.md b/.scratch/classic-transaction-flow/spec.md new file mode 100644 index 000000000..4358d89c2 --- /dev/null +++ b/.scratch/classic-transaction-flow/spec.md @@ -0,0 +1,162 @@ +# Scope Classic Transaction Flow by Flow Session + +Status: implemented + +## Problem Statement + +The recent Classic Transaction Flow refactor established one deep module and removed the previous Enter, Exit, Manage, and Activity Resume request authorities. It also introduced a global `keepAlive` facade whose active flow, preparation state, navigation outcome, lifecycle cleanup, and Transaction Workflow handoff are coordinated through a branded Classic Transaction Flow Identity. + +That identity now does too many jobs. It names the journey, separates Action-preview resources and execution machines, targets commands and cleanup, suppresses stale asynchronous completions, and validates navigation. The model also stores `Reviewing | Executable` even though the router already owns Review, Steps, and Complete and action presence already determines whether execution is available. Back must replace an otherwise unchanged flow solely to invalidate its action and resource generation. + +The four intake variants remain meaningful because they capture different required facts and action sources. The over-modeling is in their shared lifecycle, not in the tagged intake itself. + +The widget needs a smaller authority shaped around the actual React and Atom lifetime: a runtime-level intake handoff enters one Flow Session tree, the tree owns its Atoms and resources, and leaving the complete journey disposes them. Equal user attempts are separated by a private runtime-local session epoch. That epoch exists only for React remounting, guarded external cleanup, and stale shared-router suppression; it is not a domain identity threaded through state and commands. + +The accepted contract of at most one concurrently mounted Widget Instance per browser document remains unchanged. Concurrency and stale-work protection required inside that Widget Instance also remain required. + +## Solution + +Keep a small Widget application-runtime intake store containing the current immutable Flow Session entry. Every explicit Start captures the tagged intake, advances a monotonic runtime-local epoch, and creates a fresh `{ epoch, intake }` entry, even when its intake is structurally equal to a previous attempt. The epoch is used only by the session root's hidden compare-and-clear finalizer and the React session boundary key so an exiting tree cannot clear or retain a replacement. + +Use the route's keyed `ScopedAtom` provider to create one session root Atom for the mounted Flow Session entry. Its Atoms use the existing application Atom registry, are not `keepAlive`, and are disposed with their mounted tree. A stable Flow Session route spans Review, Steps, and Complete. Session, Review, and Execution each expose one narrow scoped capability instead of one combined nullable context. Descendants dispatch keyless commands against the capability available in their subtree. The root Atom contains the only feature finalizer: epoch-guarded cleanup of the external intake slot. There is no public lifecycle Atom or identity family. + +The intake variants remain `Enter | Exit | Manage | ActivityResume`, but there is no Classic Flow phase state. Review and Execution are fresh subtree modules created with `ScopedAtom` inside the existing application Atom registry. Enter, Exit, and Manage load a fresh Action Preview inside each Review scope. Activity Resume never recycles its selected action into Execution: Enter and Exit history reconstruct fresh preview requests, while Manage history without server passthrough is non-executable. Continue places the reviewed candidate into a private action handoff and publishes a Steps navigation outcome owned by that Review scope. + +The Execution scope reads that action, spans Steps and Complete, and owns its Yield Action and Transaction Workflow. Returning to Review creates a new Review scope, clears the handoff, and permanently discards the previous action and machine. Atom unsubscription interrupts and releases the workflow; no execution key or cleanup Atom is involved. Execution cancellation navigation is owned by the Execution scope, so it cannot replay after unmount. Transaction signing, submission, confirmation, failure, retry, and completion remain Transaction Workflow state. Steps-to-Complete navigation remains a workflow outcome. + +Atom resource interruption, refresh generations, and disposal suppress stale asynchronous publication. The captured Flow Session epoch is compared only when an exiting root clears the runtime intake store or a scoped router output verifies current ownership. Real API, invalid-action, unsupported activity recreation, wallet, and workflow failures remain typed and visible. + +## User Stories + +1. As a widget user, I want Review, Steps, and Complete to belong to one Flow Session so page remounts and animations do not reset my journey. +2. As a widget user, I want every explicit Start to create a fresh session snapshot even when the new intake equals the previous intake. +3. As a widget user, I want my Action Command, selected yield, tokens, validators, and Wallet Scope captured together so later source-page changes cannot alter what I review or execute. +4. As a widget user, I want Review to show action-derived gas and warning information before I Continue. +5. As a widget user, I want Continue to promote only the fresh Action Preview I reviewed into Execution. +6. As a widget user, I want a failed Action Preview to remain retryable without introducing a partially executable flow state. +7. As a widget user, I want returning from Steps to Review to permanently discard the execution action and workflow and require a fresh Action Preview. +8. As a widget user, I want browser Back and host-driven routing into Review to have the same semantics as the widget Back control. +9. As a widget user resuming activity, I want leaving execution to discard its workflow rather than restart or resurrect it through browser history. +10. As a widget user, I want repeated Continue, Back, or Retry input to be harmless rather than produce internal transition errors. +11. As a widget user, I want a late result or navigation from an exiting session unable to affect the current session. +12. As a connected-wallet user, I want disconnect or wallet-owner change to eject and dispose my Flow Session before execution can continue under another owner. +13. As an EVM wallet user, I want address casing differences treated as the same owner. +14. As a wallet user, I want additional-address-only changes to preserve the session and its captured execution inputs. +15. As a widget user, I want normal host-prop updates, live-configuration rerenders, and bundled `rerender` to preserve my Flow Session. +16. As a widget user, I want leaving the whole journey, replacing the application runtime, or unmounting the Widget to release session resources. +17. As a maintainer, I want one tagged immutable intake rather than four mutable request authorities. +18. As a maintainer, I want a scoped facade capability so React descendants cannot target or mutate another session. +19. As a maintainer, I want Action Preview state represented once by its Effect Atom `AsyncResult`, not duplicated by a preparation state machine. +20. As a maintainer, I want Transaction Workflow lifetime owned by a fresh Execution scope rather than session attached-action state or a global flow identity. +21. As a maintainer, I want Review resources and navigation owned by a fresh Review scope while one Execution scope owns Steps and Complete. +22. As a maintainer, I want stale-work protection provided by resource and session lifetimes, with the private session epoch localized to React remounting and shared external boundaries. +23. As a maintainer, I want classic and dashboard routes, deep links, KYC, warnings, tracking, completion behavior, and published copy preserved. +24. As a host developer, I want the existing one-Widget-Instance embedding, bundled unmount, and sequential-remount contracts preserved. + +## Implementation Decisions + +### Intake and Flow Session ownership + +- The runtime intake store is the only state outside the Flow Session tree. Its private state contains the next monotonic epoch and the current immutable `{ epoch, intake }` entry or `null`. +- `start` is synchronous. It snapshots all intake facts, advances the epoch, and atomically replaces the current session. Equal intake still receives a distinct epoch. +- The epoch is local to one Widget application runtime. It uses no timestamp, randomness, object-reference lookup, branded domain key, or UUID. +- The store lives for the Widget application-runtime lifetime so it can bridge the source page and the destination route. The current session is cleared when its matching root exits; the whole store is disposed with runtime replacement or Widget unmount. +- Source UI may synchronously dispatch Start and navigate to its caller-owned Review route. Entry navigation has no asynchronous outcome and is not stored in the session. +- The Flow Session captures a full immutable Wallet Scope and copies mutable collections in the intake snapshot. + +### Scoped facade and React boundary + +- The Flow Session route's `ScopedAtom` creates the private session root Atom directly from the mounted entry. Its Provider is keyed by the entry epoch because Provider input is captured once. It does not use `Atom.family`, whose structural argument equality would conflate structurally equal starts. +- The route tree creates a fresh Review or Execution root Atom with `ScopedAtom.make`. Scoped atoms use the existing application Atom registry; do not create a nested `RegistryProvider` or a second registry. +- Session, Review, and Execution atoms are not `keepAlive` and use immediate idle disposal where necessary. The long-lived intake handoff does not make preview or workflow resources long-lived. +- One stable route boundary spans Review, Steps, and Complete. Navigation between those pages does not end the Flow Session. +- Session, Review, and Execution providers expose three separate narrow scoped capabilities. Do not add a combined context with nullable Review or Execution fields. Descendant hooks are zero-logic adapters, and commands take no session or route-entry key: `continue()`, `back()`, and `retry()`. +- The root finalizer clears the intake store only when the current entry still has its captured epoch. An exiting old root is an idempotent no-op after replacement. +- The facade derives `isCurrentSession` from the runtime intake store. Router outputs and any other shared-world boundary output are suppressed when that value is false, preventing an animated exiting tree from affecting the replacement session. +- Starting a new session or changing routes may temporarily overlap exiting React subtrees. Keyed Session boundaries, scoped child modules, guarded cleanup, and Atom disposal keep those trees isolated; only the newer Flow Session can publish shared navigation. + +### Intake variants and state + +- `Enter | Exit | Manage | ActivityResume` remain tags on the immutable intake because their required facts and action sources differ. +- The tags do not produce four mutable authorities, four lifecycle machines, or four sets of command atoms. Cross-cutting projections branch inside the facade; variant-specific review views remain narrowly typed. +- Remove `ClassicTransactionFlowIdentity`, `ClassicTransactionFlowPhase`, `Reviewing`, and `Executable` from the session model and facade contract. +- The only mutable Session state is the nullable execution-action handoff. Navigation, preview state, and workflow state belong to child scopes, not the Session. +- Execution availability is derived from the session's private action handoff. The router remains authoritative for whether the user is on Review, Steps, or Complete. +- Repeated or out-of-place valid UI intents are idempotent no-ops. Do not replace removed phase and identity variants with a new transition-result protocol. +- Deterministic input copying, projections, validation, and invariant checks remain plain TypeScript. Atom owns reactive state and commands; Effect owns typed asynchronous resources and workflow lifetime. + +### Action Preview and Back + +- Every Review subtree receives a fresh scoped facade. Enter, Exit, and Manage expose one Effect Atom Action Preview resource owned by that scope. The API continues to return the Yield Action candidate needed for gas estimation and warnings. +- Continue is available only after Action Preview success and while KYC permits it. Continue synchronously places that exact candidate into the action handoff and publishes `"Steps"` navigation owned by the Review scope. +- Action Preview loading and failure come directly from its `AsyncResult`. Retry refreshes that resource. There is no separate `Idle | Loading | Failure` preparation union. +- Ordinary preview failure does not place an action in the handoff. Invalid Exit preview content remains a typed, non-retryable failure at the existing validation seam. +- Entering Review from Steps by widget Back, browser history, or host routing creates a new Review scope. Review-scope initialization is the sole operation that synchronously clears the action handoff; the old Execution scope and machine disappear through Atom unsubscription and cannot affect a newer action. +- Back does not create a new Flow Session. The immutable intake snapshot remains unchanged. +- Activity Resume never seeds Execution from its historical action. Enter and Exit history build fresh preview requests. Manage history without the required passthrough is a typed, non-retryable preview failure. +- Review-pricing and gas-warning resources may retain reusable domain cache keys. Review-only subscriptions are released when Review unmounts. +- Execution actions and machines are never resurrected by browser Forward. A later Continue must use a new Action Preview; it does not restart a disposed machine. + +### Navigation and Transaction Workflow + +- Navigation is not session state. Each Review scope owns only `null | "Steps"`; each Execution scope owns only `null | "Review"`. Neither imports or commands React Router. +- Continue publishes Steps from the Review scope. Back publishes Review from the Execution scope. Unmounting the producing scope permanently removes its navigation outcome, so destination-entry consumption is unnecessary. +- The single route adapter renders navigation declaratively and only while the producing scope belongs to the current Flow Session. No session or route-entry identity is carried in the navigation outcome. +- Steps and Complete routes require an action handoff. Review routes are valid from immutable intake and acquire a new Review scope. +- Each Execution scope privately creates its Classic Transaction Workflow from its captured action and mounts the workflow module's dedicated root Atom. That root, rather than the `viewAtom` or a Steps page subscription, retains workflow state and completion subscriptions through both Steps and Complete. It does not pass a session identity through a global workflow-family handoff. +- Unmounting Execution disposes the corresponding machine. A later Continue uses a new previewed action and creates a new machine. +- Yield Action ID remains the workflow diagnostic, API, and history identity. Classic Flow introduces no additional numeric ownership key. +- Signing, submission, confirmation, retry, pending, failure, action-history invalidation, and completion stay inside Transaction Workflow. Steps-to-Complete navigation remains derived from workflow completion. + +### Failure, interruption, and Wallet Scope + +- Atom resource interruption, refresh generation, and node disposal prevent an old preview from publishing into a disposed or refreshed facade. The model does not return `StaleFlow`. +- An external API request may finish remotely after local interruption, but its result cannot attach an action, publish navigation, or mutate the current facade. +- Remove `NotReviewing`, `IdentityNotReplaced`, and other phase/identity coordination results. Preserve typed API, invalid-action, wallet, workflow, and genuine internal invariant failures. +- Wallet Scope owner validity remains network plus primary address, with case-insensitive EVM comparison. Additional-address-only changes neither invalidate the owner nor mutate the captured scope. +- Disconnect or Wallet Scope owner mismatch causes the route boundary to eject and dispose the session. It does not introduce an invalid-wallet facade state. + +### Compatibility and delivery + +- Preserve existing classic and dashboard routes, deep links, Review, Steps, Complete, tracking, KYC, warning behavior, translations, and published copy unless explicitly changed above. +- Preserve the public React and bundled entry APIs, the one-concurrent-Widget-Instance rule, bundled `rerender` and `unmount`, and sequential unmount/remount behavior. +- Perform the scoped-facade conversion atomically. Do not retain the current global facade, phase model, identity service, identity-keyed workflow wrappers, or compatibility mirrors alongside the new authority. +- Keep the Classic Transaction Flow architecture guidance aligned with the implementation: application logic remains React-free, view adapters remain synchronous, and no new React effects own session resources or transitions. + +## Testing Decisions + +- Intake-store tests prove monotonic runtime-local epochs, immutable snapshots, atomic replacement, guarded cleanup, and distinct sessions for structurally equal intake without clocks or randomness. +- Scope and boundary tests prove that equal-intake sessions have isolated session state, every Review and Execution provider gets a fresh scoped facade, and unmount disposes non-`keepAlive` atoms. +- Overlap tests mount an exiting old root and a current replacement together. Old cleanup cannot clear a new store entry, and scoped navigation cannot affect the shared router after its owner is stale. +- Action Preview tests cover eager Review loading, direct `AsyncResult` loading/failure, Retry, promotion of the reviewed candidate, invalid Exit preview, and fresh resources for later Review scopes. +- Back tests cover UI Back, browser history, and host-driven Review entry. Every path acquires a fresh Review scope, discards the execution action and machine, and requires a fresh Action Preview without replacing the intake snapshot. +- Activity Resume tests prove that the historical action is not recycled, reconstructable actions receive fresh previews, and returning to Review disposes the execution machine. +- Command tests prove that repeated Continue, Back, and Retry are idempotent and that facade commands require no key. +- Resource-lifetime tests prove that Review resources release outside Review while one Execution scope and Transaction Workflow remain alive through Steps and Complete. +- Workflow integration tests prove that different Execution scopes cannot share a machine, Activity Resume does not retain a disposed machine, and Yield Action ID remains the diagnostic and history identity. +- Route tests cover missing intake, missing action handoff on Steps, scoped navigation disposal, route-page remounts, animation overlap, and cleanup only after the owning subtree exits. +- Wallet tests cover disconnect, owner change, EVM address casing, additional-address-only changes, and immutable captured execution inputs. +- Runtime tests cover ordinary prop updates, live configuration changes, bundled `rerender`, application-runtime replacement, Widget unmount, and sequential remount. +- Focused behavior and lifecycle tests protect against reintroducing global `keepAlive` flow state, domain flow identity, stored phases, keyed descendant commands, or React-owned asynchronous orchestration. +- Focused DOM route tests cover the production Session, Review, and Execution boundaries, while focused Chromium tests cover real Transaction Workflow execution and interruption. The existing classic and dashboard regression suites continue to protect route, KYC, warning, tracking, completion, and copy behavior outside this lifetime seam. +- Focused unit and DOM suites, representative Chromium suites, widget lint/type checking, and relevant hygiene checks form the validation ladder. + +## Out of Scope + +- Supporting multiple concurrently mounted Widget Instances in one browser document. +- Replacing the existing application Atom registry with nested per-route registries. +- Changing Action-preview API contracts or removing action-derived gas and warning information from Review. +- Validating returned Yield Action address, yield ID, or action type against intake beyond existing focused validation. +- Modeling or compensating for irreversible wallet or submission side effects after Back. +- Moving signing, submission, confirmation, retry, pending, failure, or completion state into Flow Session. +- Redesigning classic or dashboard UI, routes, KYC, warnings, tracking, translations, or copy. +- Changing price or gas-warning domain cache behavior beyond lifecycle wiring. +- Breaking public package entrypoints or the accepted embedding contract. + +## Further Notes + +- This specification uses the repository's Classic Transaction Flow, Flow Session, Action Command, Action Preview, Yield Action, Activity Resume, Wallet Scope, Transaction Workflow, and Widget Instance vocabulary. +- Flow Session epoch is deliberately technical. Equivalent user attempts and overlapping React trees need distinct ownership, but the epoch is not exposed as domain identity or passed through descendant commands. +- The key architectural goal is lifetime alignment: the intake store bridges into the journey, the Flow Session boundary owns only coordination, a fresh Review scope owns review resources, and a fresh Execution scope owns its one-shot action and Transaction Workflow through Steps and Complete. +- Information hiding follows from that ownership. React receives one scoped facade capability and cannot address another session or mutate private storage. +- A synchronization mechanism remains justified when it protects real intra-instance concurrency. Removing Classic Transaction Flow Identity does not remove command serialization, scoped interruption, wallet synchronization, or the private epoch checks at shared external boundaries. diff --git a/.scratch/earn-state-machine/spec.md b/.scratch/earn-state-machine/spec.md new file mode 100644 index 000000000..3a824e678 --- /dev/null +++ b/.scratch/earn-state-machine/spec.md @@ -0,0 +1,317 @@ +# Earn State Machine Contract and Verification + +Status: implemented + +## Problem Statement + +`packages/widget/src/features/earn/state/atoms-state/` is the application-state boundary that combines user intent, widget configuration, Wallet Scope, and several Authoritative Resources into the selection and form consumed by Earn UI. Its behavior is currently covered only in fragments. Important branches are unverified, several loading and failure cases are collapsed into empty data, and some stale or invalid intent can survive behind a fallback projection. + +The module needs an explicit behavioral contract and an exhaustive verification campaign. Tests must prove selection precedence, transition resets, data composition, loading and failure semantics, pagination, retry, refresh, race handling, and capability derivation. When the tests expose behavior that violates this specification, the implementation is corrected in the same effort and the correction is recorded here. + +## Architectural Boundary + +Earn state owns: + +- User intent and deterministic transitions. +- Resolution of Earn Selection from intent and authoritative facts. +- Dependency-aware loading, empty, readiness, and blocking-failure state. +- Reconciliation of invalid selection after authoritative success. +- Stage-targeted first-load retry commands. +- Domain validation represented by `can.*` capabilities. + +Authoritative Resources continue to own canonical remote reads, semantic request identity, caching, pagination, refresh, typed resource failure, semantic invalidation, and stale-result suppression as required by ADR-0008. + +Earn projections consume typed Authoritative Resource results and never inspect HTTP causes directly. Optional preferred-token enrichment treats a failed Legacy token-directory lookup as a missing preference candidate without changing that resource's canonical error contract. Requested initialization lookups remain blocking when their required first acquisition fails. + +The view resolver observes keyed resources lazily in dependency order and normalizes each `AsyncResult` exactly once. Independent initialization and positions reads begin together to avoid a loading waterfall. A usable observation retains its waiting flag so same-key refreshes remain visible without becoming blocking acquisition states. The published view contains normalized token, yield, and positions snapshots; React receives operational atoms only for pagination. Blocking failures carry the already-resolved atom that Retry must refresh. + +`EarnYield` response decoding owns Earn Mechanic Argument projection and validation before authoritative yield facts reach this machine. Every field first decodes through the generated API schema, then the typed API field array resolves to a name-keyed domain record containing only `amount`, `providerId`, `tronResource`, `validatorAddress`, `validatorAddresses`, and `subnetId`. Consumed fields validate the canonical Yield API variants: `amount`, `providerId`, `validatorAddress`, and `validatorAddresses` use type `string`; `tronResource` uses `enum`; and `subnetId` uses `number`. The domain values retain only Widget-consumed required flags, amount bounds, and options rather than wire-only `name`, `type`, or label metadata. Every argument container (`enter`, `exit`, each `manage` action, and `balance`) uses the same projection. The Widget trusts the API contract to provide unique argument names. Valid unrelated fields are projected away; a malformed API field or invalid consumed domain value rejects that yield at the response seam. + +React is a view adapter. It reads the resolved view and dispatches synchronous user commands as required by ADR-0004. It does not reconstruct loading, failure, retry, or domain-validity policy from raw resource flags. + +Both classic and dashboard adapters render blocking `failed` with the shared error treatment and machine Retry command, suppress invalid downstream controls, and disable the CTA. A non-blocking refresh failure retains stale controls and may show a compact warning without replacing the page or activating machine Retry. Successful `no-*` states retain empty treatment and are never rendered as failures. + +## Domain Terms + +The canonical terms are defined in `CONTEXT.md`: + +- **Earn Selection** is the resolved category, token, yield, validators, and form. +- **Earn Initialization** is the one-time seeding of the first selection from init parameters. +- **Earn Readiness** means every fact needed for correct selection and submission eligibility has a usable value. +- **Wallet Scope Owner** is network plus primary address; additional addresses are not owner identity. + +## Machine Invariants + +1. The published selection is valid against the latest successful authoritative facts for the active semantic inputs. +2. A transport or server failure without a usable value is never represented as a successful empty result. +3. Waiting or failed refresh with a prior successful value preserves that value and does not end Earn Readiness. +4. A successful result that removes a selection reconciles and commits a valid fallback; stale hidden intent cannot later snap the selection back. +5. `ready` is published only when every input needed for correct initial selection and submission eligibility has a usable value. +6. A selected yield's consumed mechanic arguments have already decoded into valid domain constraints and options. +7. A yield requiring validator selection has at least one valid selected validator before it is ready. +8. Results from obsolete wallet owners, configuration, categories, tokens, yields, validators, pagination keys, or searches never alter the active view. +9. Earn Initialization runs at most once per Widget Instance. +10. A Wallet Scope Owner change resets intent but does not rerun Earn Initialization. +11. A prior successful value is usable during waiting or refresh failure only when the complete semantic resource key is unchanged. + +## Lifecycle and Reset Rules + +### Wallet resolution + +While wallet resolution is pending, the machine publishes `resolving-wallet`, starts no new owner-scoped loads, disables selection and submission capabilities, and retains the prior view snapshot when one exists. The first pending resolution uses an empty snapshot. + +After resolution settles: + +- The same Wallet Scope Owner continues with existing intent. +- A changed primary address or network resets the complete Earn intent to its default state, then resolves from current authoritative facts without applying Earn Initialization again. +- An additional-address-only change retains intent and refreshes or re-resolves dependent balances and positions. + +### Configuration changes + +- Preference, category-order, and `tokensForEnabledYieldsOnly` changes use new semantic resource keys and reconcile existing intent when possible. +- Validator allow/block/preference configuration changes re-project the canonical validator facts and reconcile any selection that is no longer eligible. +- Switching between classic and category-grouped dashboard modes resets category, token, and every downstream selection and form field. +- Reordering categories does not override an explicit category that remains available. +- Configuration changes never reactivate consumed Earn Initialization. + +### User transitions + +- Re-selecting the same effective category, token, or yield is idempotent. +- Changing category resets token, yield, validators, provider, amount provenance, and Tron resource. +- Changing token resets yield, validators, provider, amount provenance, and Tron resource. +- Changing yield resets validators, provider, amount provenance, and Tron resource. +- Clearing a selection is explicit user intent and cannot reactivate initialization. +- Provider, validator, amount, and Tron commands change only their own field. +- Automatic reconciliation commits through the same reset graph as explicit changes: a reconciled category resets token and downstream state, a reconciled token resets yield and form state, and a reconciled yield resets validators and form state. + +## Dependency and Status Model + +The public status vocabulary is: + +- `resolving-wallet` +- `loading-categories` +- `no-categories` +- `loading-initial-selection` +- `loading-token-options` +- `no-tokens` +- `loading-yields` +- `no-yields` +- `loading-positions` +- `loading-validators` +- `no-validators` +- `ready` +- `failed` + +When several required inputs are waiting, status uses this dependency order: wallet, categories, initialization, token options, yields, positions, validators. Acquisition may run safely in parallel even though the published status has deterministic priority. + +`failed` carries one highest-priority discriminated failure: + +- `ResourceFailure` identifies `categories`, `initial-selection`, `token-options`, `yields`, `positions`, or `validators`, preserves the typed catalog error and raw diagnostic cause, and exposes a Retry command for only the responsible resource. + +The failure is used only when a required first acquisition has no usable value. Retry ignores duplicate commands while its target is waiting. If recovery reveals an independent downstream failure, that failure then becomes active according to dependency priority. A malformed directly requested yield is an ordinary decode-backed resource failure; a tolerant directory omits only the malformed yield and records the decode rejection. + +A later refresh failure retains `ready` and its prior successful value. The responsible resource exposes the non-blocking error; machine-level blocking Retry is not activated. + +Successful empty data uses the matching `no-*` status. It is not a failure and does not offer transport retry. + +## Selection Resolution + +### Category + +- Classic mode always resolves `null`. +- Category-grouped dashboard mode waits for category discovery. +- An explicit or one-time initialized category is accepted only when discovered as available. +- Otherwise, the first discovered category in configured order is selected. +- Successful discovery with no categories resolves `null` and publishes `no-categories`; the machine does not invent a category. +- A category is available when its bounded maximum-size first-page summary defined by ADR-0008 contains at least one enter-enabled yield on a supported network. Earn state does not complete the category catalog to prove existence; if bounded discovery becomes insufficient, the resource contract must move to a dedicated summary endpoint. Reward rate affects ranking, not visibility. + +### Token + +Token precedence is: + +1. Valid explicit user selection. +2. One-time init-yield token. +3. One-time init token, matching exact canonical token key first and then case-insensitive symbol plus network. +4. Preferred token configured for the active network. +5. First merged token option. + +When disconnected, the first configured network may seed a default. When an active wallet network exists without preferences, preferences from another network are not used. + +A configured preferred-token candidate is resolved during initial selection from the complete Legacy token directory response, then intersected with the same API-key and category yield scope before merging. The demand-driven paginated Yield token directory is not eagerly completed to find preferences, and a token discovered only by later browsing does not auto-switch a ready selection. + +Merged token options: + +- Deduplicate by canonical token key. +- Union, deduplicate, and sort available yield IDs. +- Prefer balance values and metadata over init, and init over default. +- Rank positive-balance tokens, init-only tokens, zero-balance tokens, then default-only tokens while preserving source order within each rank. +- Apply category scope to every candidate and remove a token left with no yield IDs. +- When `tokensForEnabledYieldsOnly` is active, intersect yield IDs from every source, including balance and initialization, with the authoritative enabled-yield scope. Initialization cannot bypass API-key product configuration; an excluded init target is consumed as unavailable and falls back. +- On initial connected-wallet acquisition, default discovery, balances, requested init data, and category scope must all settle before the result is authoritative. +- Token provenance is distinct from balance knowledge. Disconnected options have unknown available amount. After a successful connected-wallet scan, a returned amount is authoritative and an otherwise selectable token absent from the scan has known amount `"0"`; waiting or first-load failure leaves the machine unready rather than fabricating zero. + +### Yield + +Yield precedence is: + +1. Valid explicit user selection. +2. One-time valid init yield. +3. Preferred yield for the selected token's network and canonical token key. +4. First yield eligible under balance, positions, and init constraints. +5. First non-zero-reward yield, then first available yield. + +Preferred value `"*"` delegates to eligibility and default ranking. Preferences from another network are not used. + +Yield visibility is determined by API-key-scoped data, enter capability, and supported network. The legacy hard-coded exclusions for Binance BNB and AVAX native staking are removed from both the state resolver and still-live Earn provider/yield helper resources. Zero reward does not hide a yield in state or React selectors; reward affects default ranking only. + +### Validators + +- Authoritative Resources retain raw canonical validators. Earn state applies network or wildcard allow/block/preference configuration and yield-specific eligibility before selection, empty-state, readiness, and capability decisions; React does not filter a second time. +- Preferred-validator acquisition and the first default-validator page both settle before readiness because either can change the correct initial selection. +- Single-select replaces the selection. +- Multi-select toggles membership but cannot remove the final validator. +- Explicit removal is a no-op for the final validator. +- After authoritative refresh, retain the valid selected subset. If none remain, choose a still-valid one-time init validator, then the first preferred validator, then the first active validator. +- Successful required-validator acquisition with no validators publishes `no-validators`. +- Search results have independent keyed pagination, pass through the same eligibility projection, and do not reorder or replace the default loaded list. +- A selected search result may be remembered by the base validator resource. + +### Initialization targets + +- Syntactically invalid init parameters decode to absence before reaching Earn state. +- A well-formed target that resolves as unavailable or disabled is consumed and falls back normally. Transport, server, and decode failures for requested initialization data remain blocking first-load failures. +- A transport or server failure while resolving a requested target is a blocking first-load `initial-selection` failure with retry. +- Account-targeted initialization is not consumed while the wallet owner is absent. Committing a resolved fallback and consuming initialization are separate transitions, so initial wallet connection can reset intent and resolve the same one-time target for its owner without re-arming initialization later. +- Once a yield target is committed, its ID remains a resource seed while selected so transient catalog re-keying cannot replace it with a fallback. +- A consumed, invalidated, or user-overridden target cannot resurrect. +- A successful bounded directory result may explicitly omit requested IDs. Omitted IDs are unavailable selections, not transport failures; explicit intent is reconciled and init intent emits its one diagnostic before fallback. + +## Form Resolution + +### Provider and Tron arguments + +- Explicit values are accepted only if present in the selected yield's advertised options. +- Optional arguments default to `null`. +- Required arguments default to the first advertised option. +- No value such as `ENERGY` is hard-coded outside advertised options. +- `providerId` options decode as `YieldId`; `tronResource` options decode as `TronResource`. +- A required argument without an advertised option, an invalid option, or an unexpected field type rejects the yield during response decoding. +- Refreshed yield metadata reconciles stale option values. + +### Amount + +Amount intent has internal provenance: + +- `untouched` resolves to the yield minimum. +- `manual` preserves the normalized user value, including zero. +- `max` preserves the calculated maximum snapshot for an ordinary yield. +- A force-max yield always derives from the latest available balance and ignores prior manual/max amount. +- Category, token, yield, or Wallet Scope Owner reset returns amount provenance to `untouched`. +- When refreshed yield, position, or balance facts change constraints, `untouched` follows the new minimum, while `manual` and ordinary `max` values remain unchanged and may make `can.submit` false. Enumerated provider and Tron values reconcile to advertised options; user-authored numeric input is not silently rewritten. +- Numeric amount constraints decode as exact finite decimal strings. Missing or null minimum becomes `"0"` and missing maximum becomes `null`; explicit maximum `"0"` retains its unbounded meaning. A maximum of `"-1"` with a non-negative minimum is the Yield API's unbounded-maximum sentinel and normalizes to `null`, while the complete `"-1"`/`"-1"` pair denotes force-max. A negative minimum outside that pair, another negative maximum, or a positive maximum below minimum rejects the yield during response decoding rather than allowing coercion or `NaN`. + +The public form may continue exposing `stakeAmount` and `useMaxAmount`; provenance can remain private. + +## Capabilities and Validation + +Selection capabilities are enabled when their corresponding authoritative options exist and their own dependency is usable. They remain enabled during later pagination or non-blocking refresh. Blocking failures are stage-aware rather than global: category failure removes category-dependent controls; token failure may retain known category choices; yield failure may retain category and token choices; positions or validator failure may retain category, token, and yield choices. Submission and only the controls depending on the failed stage are disabled, so choosing a different upstream option is a valid recovery path alongside Retry. + +`can.submit` requires: + +- `ready` status. +- A connected Wallet Scope Owner. +- Valid token and yield selections. +- Every required validator, provider, and Tron value. +- Non-zero amount satisfying yield minimum and maximum. +- Amount not exceeding known available balance. +- A resolved balance for force-max yields. + +KYC, gas checks, and transaction-execution readiness remain outside this machine. Plain amount and form validation is shared with the UI so the application has one definition. + +## Pagination, Refresh, and Concurrency + +- First-page failure without a usable value is blocking. +- Loading more preserves accumulated items and readiness. +- Later-page failure preserves accumulated items and exposes a non-blocking error. +- Repeating Pull after a later-page failure may retry the same continuation while preserving accumulated items. +- Explicit resource Refresh or Retry restarts the shared Pull from page one, as required by ADR-0008. Machine-level Retry is present only for blocking first-page failure. +- Duplicate items across pages merge by canonical identity. +- Repeated Pull while waiting is ignored. +- Validator search pagination is keyed by normalized search and cannot publish obsolete results into a newer search. +- Explicitly selected search validators may be remembered without changing default-list order. +- Every result is keyed by all semantic inputs. +- Same-key refresh may retain a previous successful value. A changed semantic key clears downstream old-key selection and publishes the new key's loading state; old-key data is never presented as stale data for the new request. +- Late results for obsolete wallet owner, configuration, category, token, yield, validator, or search cannot affect the active view. +- Revisiting a prior semantic key may immediately use its cached successful value under the Authoritative Resource's freshness and invalidation policy. Revisiting alone does not force a fetch. + +## Verification Strategy + +Tests use four seams: + +1. Schema-boundary tests for Earn Mechanic Argument projection, normalization, rejection, tolerant directory omission, and direct-opportunity decode failure. +2. Pure decision tables for reducers, resolvers, validation, precedence, and invariants. +3. Atom-registry integration tests with controllable service Layers and deferred responses for loading, failure, retry, refresh, pagination, reconciliation, and races. +4. A small set of DOM/browser tests proving critical states and commands are consumed correctly by both classic and dashboard UI adapters, including blocking retry recovery and stale-data refresh failure. + +Verification is complete when: + +- Every action and meaningful resolver branch is asserted. +- Every status and capability combination is exercised. +- Initial, waiting, waiting-with-value, success, empty, typed failure, refresh, recovery, pagination, and out-of-order completion are covered. +- Input cross-products are used where inputs genuinely interact. +- Folder-scoped coverage may be inspected to find untested behavior, but no coverage percentage is a completion target or delivery gate. +- Focused unit and DOM tests, affected browser tests, widget lint/type checking, and hygiene checks pass. + +Property-test tooling is not added solely for this effort. Explicit Vitest decision tables and invariants are preferred with the current dependency set. + +## Delivery + +The change lands as one coherent internal cutover. Implementation proceeds test-first in small red/green/refactor steps across pure resolution, catalog projection, machine lifecycle, and UI adapters, but the branch does not retain parallel old/new machine authorities, temporary fallback behavior, or a compatibility facade for behavior this specification replaces. Focused tests run throughout; the completed cutover runs the full widget validation ladder. + +## Expected Behavioral Corrections + +Code inspection identified the following current behaviors that conflict with this specification and must be verified before correction: + +- Token and yield acquisition failures can appear as `no-tokens` or `no-yields`. +- Positions, category, and validator failures can be treated as successful empty facts. +- `ready` can be published before positions or required validators resolve. +- Dashboard token loading can begin from an invented fallback category while discovery is pending. +- The declared `resolving-wallet` state is not reliably published. +- Invalid intent can remain hidden behind a fallback and later snap back. +- Init targets remain permanent fallbacks and can resurrect. +- Wallet Scope Owner changes do not reset complete Earn intent. +- Category changes do not reset token intent. +- Explicit zero amount is indistinguishable from untouched amount. +- Required Tron default is hard-coded instead of derived from advertised options. +- Provider and Tron intent is not reconciled against refreshed options. +- Final-validator removal semantics differ between toggle and explicit remove. +- Three yield IDs are hard-coded out of visibility despite API-key scoping. +- Zero reward can incorrectly remove a dashboard category from availability. +- React's yield selector independently filters zero-reward yields after state resolution. +- Still-live Earn provider/yield helper resources repeat the hard-coded yield exclusions outside `atoms-state`. +- Cross-network preference fallback can influence an active network. +- `can.submit` can be true for disconnected or otherwise invalid form state. + +## Completed Correction Log + +- First-load category, token, yield, position, and validator failures now publish structured blocking failures; refresh failures with a previous value retain readiness. +- Readiness now waits for categories, tokens, yields, positions, and both required-validator sources in dependency order. +- Wallet pending publishes `resolving-wallet` without starting owner-scoped work; address/network owner changes and classic/dashboard mode changes reset intent, while one-time initialization is never re-armed. Account-targeted initialization consumption waits for the initial wallet owner. +- Successful fallback selection is committed to canonical intent, and category changes reset token plus all downstream state. +- Explicit zero amount, connected-wallet balance knowledge, and truthful submission capability are modeled in state. Advertised provider/Tron options and coherent numeric constraints are decoded before state resolution. +- Earn Mechanic Arguments now resolve from API arrays into validated, name-keyed domain records. Resolver-owned `InvalidYieldContract` handling and its form failure stage were removed. +- Preferred token discovery uses the complete Legacy directory and treats optional preference lookup failure as missing; active-network selection never consumes another network's preference. +- Every token source is intersected with category and enabled-yield scope, and stale hard-coded yield exclusions were removed. +- Validator configuration is applied before selection/readiness and React no longer performs a second eligibility filter. +- Zero-reward, enter-enabled yields remain visible in category discovery and selectors. +- Catalog projections forward Refresh to the exact authoritative source, and classic/dashboard failure UI exposes the targeted retry. + +This list becomes a completed correction log as focused tests reproduce each behavior and the implementation is fixed. + +## Out of Scope + +- Replacing or duplicating Authoritative Resource cache and pagination ownership. +- Introducing React Query, hook-owned fetching, Promise caches, or React-owned retries. +- Broad UI redesign, unrelated product copy, KYC policy, gas policy, or transaction execution changes. +- Preserving accidental `atoms-state` behavior when it conflicts with this contract. +- Adding support for multiple concurrently mounted Widget Instances. diff --git a/.scratch/wallet-service-runtime/issues/01-establish-scoped-wallet-runtime-contract.md b/.scratch/wallet-service-runtime/issues/01-establish-scoped-wallet-runtime-contract.md new file mode 100644 index 000000000..4dec1084b --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/01-establish-scoped-wallet-runtime-contract.md @@ -0,0 +1,18 @@ +# 01 — Establish the scoped Wallet Runtime contract + +**What to build:** Expand WalletService so one application-runtime generation captures one Wallet Bootstrap Snapshot, constructs one Wagmi config, and exposes the Wallet Runtime Phase, current snapshot, change stream, and authoritative config without disrupting existing wallet consumers. + +**Blocked by:** None — can start immediately. + +**Status:** ready-for-agent + +- [ ] WalletService captures the normalized bootstrap configuration, enabled networks, decoded initialization parameters, browser environment, and external-provider presence as one immutable Wallet Bootstrap Snapshot. +- [ ] WalletService constructs Wagmi no more than once for its scoped lifetime and retains one service and config identity. +- [ ] The public service contract exposes Bootstrapping, Ready, BootstrapFailed, and InvariantViolated phases together with current and changing Wallet Projections. +- [ ] Core connection and connector watchers are installed before their seed values are read, and Ready is not published until the initial canonical snapshot is available. +- [ ] Reconnect, mobile fallback, and initial chain switching run as scoped background work after core readiness, retaining their existing ordering and recoverable failure policy. +- [ ] Ordinary wallet-related configuration changes after bootstrap do not rebuild Wagmi or change Wallet Topology. +- [ ] Bootstrap construction failures publish a terminal BootstrapFailed phase without exposing a partially ready runtime. +- [ ] Disposing and remounting the application runtime releases the old resources and constructs a fresh Wallet Runtime. +- [ ] The WalletService contract tests exercise observable phases, snapshots, configuration identity, watcher ordering, background initialization, and cleanup without asserting private queue machinery. +- [ ] Existing wallet consumers continue to work while the new contract exists beside the legacy ownership path. diff --git a/.scratch/wallet-service-runtime/issues/02-build-headless-solana-runtime.md b/.scratch/wallet-service-runtime/issues/02-build-headless-solana-runtime.md new file mode 100644 index 000000000..5d5f4a70a --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/02-build-headless-solana-runtime.md @@ -0,0 +1,20 @@ +# 02 — Build the scoped headless Solana runtime + +**What to build:** Provide a scoped, non-React Solana runtime that constructs the RPC connection and maintains the available adapter descriptors with the discovery, readiness, fallback, mobile, and cleanup behavior required by the widget. + +**Blocked by:** None — can start immediately. + +**Status:** ready-for-agent + +- [ ] The runtime constructs one Solana Connection per Wallet Runtime with behavior equivalent to the existing provider configuration. +- [ ] The runtime uses the modern Wallet Standard registry and does not rely on the deprecated `navigator.wallets` registration path. +- [ ] Compatible wallets present at startup and wallets registered later are wrapped and published as adapter descriptors. +- [ ] Registry unregister events remove and dispose the corresponding wrapped adapters according to the Wallet Standard contract. +- [ ] Adapter readiness events update descriptors, and unsupported adapters are excluded. +- [ ] Same-name Standard adapters take precedence over inactive explicit fallbacks without producing duplicate wallet entries. +- [ ] Explicit Phantom, Trust, and WalletConnect fallbacks are instantiated once per scoped runtime. +- [ ] Android Mobile Wallet Adapter inclusion matches the existing environment, installed-wallet, endpoint, and app-identity behavior. +- [ ] All registry listeners, adapter listeners, wrappers, fallbacks, and mobile resources are released with the runtime scope. +- [ ] Production imports of Wallet Standard or adapter packages are declared as direct package dependencies. +- [ ] Deterministic tests cover initial discovery, late registration, unregister disposal, readiness, deduplication, unsupported filtering, Android behavior, and remount cleanup without real wallets or RPC traffic. +- [ ] This expand step does not yet replace the production Wagmi Solana connector path. diff --git a/.scratch/wallet-service-runtime/issues/03-make-service-wagmi-config-authoritative.md b/.scratch/wallet-service-runtime/issues/03-make-service-wagmi-config-authoritative.md new file mode 100644 index 000000000..8376877d3 --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/03-make-service-wagmi-config-authoritative.md @@ -0,0 +1,17 @@ +# 03 — Make the service-owned Wagmi config authoritative + +**What to build:** Route the React Wagmi boundary and the core connection/connector Wallet Projections through WalletService so the mounted widget uses one authoritative config and one source of reactive base Wallet State. + +**Blocked by:** 01 — Establish the scoped Wallet Runtime contract. + +**Status:** ready-for-agent + +- [ ] Wagmi's React context receives an inert fallback only while WalletService is bootstrapping and then receives the service-owned config by reference. +- [ ] No legacy controller or atom path constructs a second production Wagmi config after this migration. +- [ ] Core connection and connector changes are reduced by WalletService and exposed through read-only feature atoms/selectors. +- [ ] Existing feature hooks and UI observe connection, account, chain, and connector changes without writing canonical Wallet State. +- [ ] Equivalent rerenders and ordinary wallet-setting changes keep the same Wallet Runtime and Wagmi config. +- [ ] A new application-runtime generation constructs a fresh Wallet Runtime and config. +- [ ] Manual connect, disconnect, reconnect, and chain-switch actions continue to operate against the authoritative config. +- [ ] The browser contract verifies fallback-to-authoritative handoff, config identity, RainbowKit-facing actions, and read-only projection reactivity. +- [ ] Legacy enrichment and command bridges may remain temporarily only where later tickets still require them, and the test suite remains green. diff --git a/.scratch/wallet-service-runtime/issues/04-own-external-provider-snapshots.md b/.scratch/wallet-service-runtime/issues/04-own-external-provider-snapshots.md new file mode 100644 index 000000000..ac287501e --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/04-own-external-provider-snapshots.md @@ -0,0 +1,19 @@ +# 04 — Own External Provider Snapshot synchronization and invariants + +**What to build:** Make WalletService follow live external-provider values inside the Connector Mode chosen at bootstrap and fail deterministically when host updates would change that mode or produce an impossible connector state. + +**Blocked by:** 03 — Make the service-owned Wagmi config authoritative. + +**Status:** ready-for-agent + +- [ ] External-provider address, chain, supported chains, provider operations, and equivalent live values are read from a service-owned External Provider Snapshot. +- [ ] Live snapshot changes synchronize the external connector and connection without rebuilding Wagmi or rerunning Wallet Bootstrap. +- [ ] An available external address triggers the existing automatic connection behavior without duplicate concurrent attempts. +- [ ] Connector supported-chain, account, and chain notifications are deduplicated and ordered through the WalletService event loop. +- [ ] Connector Mode is fixed from the Wallet Bootstrap Snapshot. +- [ ] External-provider presence changing from absent to present or present to absent enters terminal InvariantViolated. +- [ ] A missing or mismatched external-provider connector in external-provider mode also enters InvariantViolated immediately. +- [ ] Each invariant is logged once, subsequent wallet work fails deterministically, and unrelated application services remain usable. +- [ ] A separate Wallet Runtime generation is unaffected by another generation's invariant violation. +- [ ] Service-level tests cover live value replacement, synchronization, both presence transitions, connector mismatch, log-once behavior, and scoped isolation. +- [ ] The legacy external-provider synchronization owner is no longer mounted after this behavior moves into WalletService. diff --git a/.scratch/wallet-service-runtime/issues/05-publish-complete-wallet-state.md b/.scratch/wallet-service-runtime/issues/05-publish-complete-wallet-state.md new file mode 100644 index 000000000..b7531d9d5 --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/05-publish-complete-wallet-state.md @@ -0,0 +1,17 @@ +# 05 — Publish complete canonical Wallet State + +**What to build:** Extend the service-owned event loop so WalletService publishes complete, atomic Wallet State—including connector chains, Ledger data, Cosmos routing, and additional addresses—while feature atoms become read-only Wallet Projections. + +**Blocked by:** 03 — Make the service-owned Wagmi config authoritative. + +**Status:** ready-for-agent + +- [ ] Canonical Wallet State includes the existing connection status, address, chain, network, connector, connector chains, Ledger account state, placeholder state, and validated additional addresses. +- [ ] Cosmos chain-wallet and other connector-specific command-routing objects remain private service context rather than public Wallet Projection fields. +- [ ] Connection and enrichment events are serialized so consumers observe complete snapshots rather than mixed old/new account, chain, or additional-address data. +- [ ] Recoverable connector-chain, Ledger, Cosmos, or additional-address failures degrade only their relevant slice and do not terminally fail a healthy Wallet Runtime. +- [ ] State changes are deduplicated and available through the service's current snapshot and changes stream. +- [ ] Existing wallet selectors, hooks, workflows, and Wallet Scope derivation retain their observable behavior through read-only adapters. +- [ ] Feature atoms cannot write canonical Wallet State or bind a projected value back into WalletService. +- [ ] Service contract tests drive controlled connection and enrichment events and assert only complete public snapshots plus required private command-routing outcomes. +- [ ] Obsolete state-owner tests are replaced by service and projection contract coverage while low-level protocol driver tests remain intact. diff --git a/.scratch/wallet-service-runtime/issues/06-move-wallet-lifecycle-policies.md b/.scratch/wallet-service-runtime/issues/06-move-wallet-lifecycle-policies.md new file mode 100644 index 000000000..a48497603 --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/06-move-wallet-lifecycle-policies.md @@ -0,0 +1,18 @@ +# 06 — Move wallet lifecycle policies into WalletService + +**What to build:** Run wallet connection tracking and unsupported-chain disconnect behavior as scoped WalletService lifecycle policies driven by canonical Wallet State. + +**Blocked by:** 03 — Make the service-owned Wagmi config authoritative. + +**Status:** ready-for-agent + +- [ ] Each distinct supported wallet connection emits the existing connected-wallet tracking event once. +- [ ] Equivalent Wallet State publications do not duplicate tracking. +- [ ] Leaving and later re-entering a connection resets deduplication so a genuinely new connection is tracked. +- [ ] Each distinct unsupported connection is disconnected once with the active connector. +- [ ] Equivalent unsupported snapshots do not trigger duplicate disconnect attempts. +- [ ] Returning to disconnected or supported state resets unsupported-connection deduplication. +- [ ] Tracking and disconnect failures preserve the current recoverable policy and do not poison the Wallet Runtime. +- [ ] Lifecycle effects consume the serialized service-owned Wallet State rather than a separately mounted state-owning atom. +- [ ] Lifecycle fibers and subscriptions stop when the Wallet Runtime scope is disposed. +- [ ] Service-level tests replace legacy lifecycle-owner tests and assert tracking/disconnect behavior through observable collaborators. diff --git a/.scratch/wallet-service-runtime/issues/07-route-commands-from-wallet-state.md b/.scratch/wallet-service-runtime/issues/07-route-commands-from-wallet-state.md new file mode 100644 index 000000000..6df53e505 --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/07-route-commands-from-wallet-state.md @@ -0,0 +1,18 @@ +# 07 — Route commands from captured Wallet State + +**What to build:** Make every WalletService command route directly from one captured service-owned state/context snapshot, fail immediately when unavailable, and remove the atom-to-service binding lifecycle. + +**Blocked by:** 04 — Own External Provider Snapshot synchronization and invariants; 05 — Publish complete canonical Wallet State; 06 — Move wallet lifecycle policies into WalletService. + +**Status:** ready-for-agent + +- [ ] Transaction signing, message signing, account switching, disconnect, and state reads resolve directly from WalletService-owned state and private routing context. +- [ ] Commands issued during Bootstrapping fail immediately with the appropriate typed failure and never wait on Deferred readiness. +- [ ] Commands issued after BootstrapFailed or InvariantViolated fail immediately and deterministically. +- [ ] An in-flight command continues against the state, controller operations, and connector-specific context captured when it began. +- [ ] A command begun after a Wallet State publication uses the newly published snapshot. +- [ ] EVM, external-provider, Safe, Ledger, Cosmos, Substrate, and miscellaneous routing retain their current success and typed-error behavior. +- [ ] Public command result types continue to distinguish signed payloads from broadcast transaction hashes. +- [ ] WalletBinding, the bind method, Deferred readiness, and the binding atom are removed from production ownership. +- [ ] Command contract tests replace binding-atom tests and cover active-command stability, next-command freshness, phase failures, and representative connector routing. +- [ ] No production command reads feature atom state. diff --git a/.scratch/wallet-service-runtime/issues/08-integrate-dynamic-solana-membership.md b/.scratch/wallet-service-runtime/issues/08-integrate-dynamic-solana-membership.md new file mode 100644 index 000000000..cddfa2aee --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/08-integrate-dynamic-solana-membership.md @@ -0,0 +1,20 @@ +# 08 — Integrate dynamic Solana membership into Wagmi + +**What to build:** Integrate the headless Solana runtime with the fixed Wallet Topology so dynamic Standard discovery and readiness update the existing Wagmi config and RainbowKit-facing hooks without hot-swapping an active adapter. + +**Blocked by:** 02 — Build the scoped headless Solana runtime; 03 — Make the service-owned Wagmi config authoritative; 04 — Own External Provider Snapshot synchronization and invariants. + +**Status:** ready-for-agent + +- [ ] WalletService supplies the headless runtime's Connection and initial Solana adapters when it constructs the one Wagmi config. +- [ ] The React Solana provider, Solana root input, layout-effect bridge, nullable connection race, and unsafe connection cast are removed from the production bootstrap path. +- [ ] A same-name Standard adapter replaces an inactive fallback without rebuilding the Wagmi config. +- [ ] Standard registration while the fallback is active is deferred, and disconnecting that fallback publishes the pending Standard adapter. +- [ ] Unregistering a pending Standard adapter keeps the active fallback; unregistering an inactive visible Standard adapter publishes the fallback. +- [ ] Unregistering an active Standard adapter disconnects and removes it before publishing the fallback. +- [ ] Readiness changes publish a fresh outer connector wrapper while preserving UID, emitter, methods, and underlying adapter identity. +- [ ] Core connector observers and React `useConnectors` consumers both observe membership and readiness changes. +- [ ] Volatile Wagmi connector-store access is encapsulated behind one service-owned membership adapter. +- [ ] External-provider Connector Mode does not accidentally acquire or expose unrelated Solana connectors. +- [ ] Browser characterization covers the prototype transition rules, same-UID readiness refresh, authoritative config identity, and RainbowKit-facing visibility. +- [ ] Runtime disposal removes discovery and readiness listeners without affecting a later remount. diff --git a/.scratch/wallet-service-runtime/issues/09-contract-legacy-wallet-ownership.md b/.scratch/wallet-service-runtime/issues/09-contract-legacy-wallet-ownership.md new file mode 100644 index 000000000..6426b40b9 --- /dev/null +++ b/.scratch/wallet-service-runtime/issues/09-contract-legacy-wallet-ownership.md @@ -0,0 +1,19 @@ +# 09 — Contract legacy wallet ownership and verify compatibility + +**What to build:** Remove the superseded controller, atom ownership, React bridge, and prototype paths after all consumers use WalletService, then verify the complete widget remains publicly and architecturally compatible. + +**Blocked by:** 06 — Move wallet lifecycle policies into WalletService; 07 — Route commands from captured Wallet State; 08 — Integrate dynamic Solana membership into Wagmi. + +**Status:** ready-for-agent + +- [ ] No production path retains initialization-key families, controller-owned Wagmi construction, WalletBinding, binding readiness, external-provider synchronization ownership, lifecycle ownership, or connection/connector/Ledger/Cosmos/additional-address state ownership outside WalletService. +- [ ] Feature atoms that remain are read-only Wallet Projections or selectors with no back-binding to the service. +- [ ] React composition retains only required third-party Wagmi/RainbowKit context boundaries and no longer owns Solana discovery or connection construction. +- [ ] Obsolete modules, exports, tests, dependencies, and imports are removed without deleting unrelated user work. +- [ ] The throwaway prototype command and production-branch prototype files are removed after their validated decisions are represented by tests and the specification. +- [ ] Published React component, bundled renderer, style, and existing wallet-facing API contracts remain compatible. +- [ ] Classic and dashboard wallet workflows continue to connect, disconnect, switch chains/accounts, and sign through the service-owned runtime. +- [ ] Ordinary configuration updates retain one Wallet Runtime, while an application-runtime remount receives clean service state and resources. +- [ ] The focused unit, DOM, and browser wallet suites pass. +- [ ] Package lint/type-check, formatting and hygiene checks, public API contracts, and the appropriate broader test suite pass. +- [ ] The final code follows the accepted WalletService ownership ADR and uses the glossary's Wallet Runtime terminology consistently. diff --git a/.scratch/wallet-service-runtime/spec.md b/.scratch/wallet-service-runtime/spec.md new file mode 100644 index 000000000..0646cdd38 --- /dev/null +++ b/.scratch/wallet-service-runtime/spec.md @@ -0,0 +1,159 @@ +# WalletService-owned Wallet Runtime + +Status: ready-for-agent + +## Problem Statement + +Wallet Bootstrap and Wallet State are currently distributed across keyed atoms, controller resources, React-fed Solana inputs, lifecycle atoms, and a WalletService that must be bound back to atom-owned state before its commands work. This creates multiple authorities for the same wallet, allows configuration changes to reconstruct Wagmi during a mounted widget generation, and makes command routing depend on whichever atom/controller binding is current. + +The widget needs one scoped owner for wallet initialization, state, effects, commands, discovery, and cleanup. A host should receive predictable behavior from the configuration and external provider available when a Wallet Runtime starts, while ordinary later configuration changes must not silently replace the underlying wallet system. The UI and workflows still need reactive wallet data, but they should consume read-only Wallet Projections rather than own or rebind canonical Wallet State. + +Solana is the most visible ownership leak: React providers currently construct the connection and discover wallets, then copy those values into an atom that participates in Wagmi initialization. Removing that bridge requires a headless runtime that preserves supported discovery, fallback, readiness, mobile, and connector behavior without rebuilding the Wagmi config. + +## Solution + +Make WalletService the sole owner of one Wallet Runtime per application-runtime generation. During Wallet Bootstrap it captures one immutable Wallet Bootstrap Snapshot, resolves the enabled networks and initialization inputs, constructs one Wagmi config, installs and seeds its core watchers, and publishes an atomic runtime snapshot. It then processes all wallet, connector, enrichment, external-provider, and discovery events through one serialized event loop. + +WalletService exposes its current runtime snapshot, a stream of changes, the service-owned Wagmi config, and wallet commands. Commands fail immediately until the Wallet Runtime is ready and capture one Wallet State snapshot for the duration of each operation. Feature atoms become read-only adapters and selectors over WalletService. React remains only as the adapter for third-party tree-scoped Wagmi and RainbowKit contracts. + +Wallet Topology remains fixed for the lifetime of the Wallet Runtime. Ordinary wallet-related configuration updates are ignored after bootstrap. External Provider Snapshot values remain live when the runtime started in external-provider Connector Mode, but adding or removing external-provider presence after bootstrap is a Wallet Runtime Invariant violation and terminally poisons only that Wallet Runtime. + +Solana connection construction and wallet discovery move into a scoped headless runtime. It uses the modern Wallet Standard registry, adapter readiness events, explicit fallbacks, Android Mobile Wallet Adapter parity, and dynamic connector membership within the already-created Wagmi config. Same-name Standard adapters take precedence, but an active fallback adapter is never hot-swapped. + +## User Stories + +1. As a widget host, I want wallet infrastructure to initialize once per mounted application generation, so that ordinary configuration rerenders cannot replace an active wallet runtime. +2. As a widget host, I want Wallet Bootstrap to use one atomic snapshot of current inputs, so that the runtime cannot combine values observed at different moments. +3. As a widget host, I want post-bootstrap wallet configuration changes to be ignored, so that the configured Wallet Topology remains stable. +4. As a widget host, I want a remounted application generation to receive a fresh Wallet Runtime, so that old listeners, adapters, connections, and state do not leak into the new mount. +5. As a widget host using an external provider, I want current address, current chain, supported chains, and provider operations to remain live, so that the widget follows the host wallet without rebuilding Wagmi. +6. As a widget host, I want adding or removing external-provider presence after bootstrap to fail visibly as an invariant violation, so that an invalid integration cannot continue in a mixed Connector Mode. +7. As a widget host, I want an external-provider connector mismatch to fail immediately and be logged once, so that impossible connector routing is diagnosed rather than silently ignored. +8. As a widget user, I want wallet connection state to remain reactive, so that UI and workflows reflect connector changes without owning those changes. +9. As a widget user, I want account and chain changes to update atom-backed UI consistently, so that every screen observes the same Wallet State. +10. As a widget user, I want wallet commands to use the state visible when the command begins, so that a concurrent account or connector update cannot reroute an in-flight signing operation. +11. As a widget user, I want commands issued before readiness to fail immediately, so that the UI never waits indefinitely for an internal binding. +12. As a widget user, I want commands issued after a terminal Wallet Runtime failure to return typed failures, so that the widget can surface a deterministic error. +13. As a widget user, I want reconnect behavior to remain unchanged, so that previously authorized wallets still reconnect when possible. +14. As a mobile widget user, I want the existing injected-provider fallback behavior to remain unchanged, so that mobile wallets still connect when reconnect finds no active connection. +15. As a widget user arriving with an initial chain parameter, I want the runtime to attempt the same initial switch, so that entry links retain their current behavior. +16. As a widget user, I want reconnect, mobile fallback, and initial switching failures to remain recoverable, so that manual wallet connection remains available. +17. As a widget user, I want unsupported connected chains to trigger the existing automatic disconnect policy, so that workflows do not operate against unsupported state. +18. As an analytics consumer, I want each supported wallet connection to be tracked once, so that moving lifecycle ownership does not duplicate connection events. +19. As a widget user, I want Ledger account state to remain part of the canonical Wallet State, so that account switching and signing stay coherent. +20. As a widget user, I want Cosmos chain-wallet routing to use the same captured Wallet State as other commands, so that transaction commands do not depend on separately published atoms. +21. As a widget user, I want additional addresses to update as part of an atomic Wallet State publication, so that workflows never combine a new account with stale enrichment. +22. As a widget user, I want a recoverable enrichment failure to degrade only that slice, so that a healthy wallet connection remains usable. +23. As a widget user, I want recoverable connector failures to affect only the relevant connector state, so that unrelated connectors remain available. +24. As a widget user, I want a bootstrap construction failure to produce a terminal BootstrapFailed phase, so that partially initialized wallet resources are never presented as ready. +25. As a widget user, I want Wallet State to become ready only after core Wagmi watchers are installed and seeded, so that the first ready snapshot cannot miss a concurrent event. +26. As a Phantom user, I want a discovered Wallet Standard adapter to take precedence over the explicit Phantom fallback, so that only one Phantom option is displayed. +27. As a Phantom user with an active fallback connection, I want late Standard discovery to be deferred, so that my active adapter is not hot-swapped. +28. As a Phantom user, I want the deferred Standard adapter to appear after I disconnect the fallback, so that the modern adapter becomes available without rebuilding the runtime. +29. As a user connected through a Standard adapter that unregisters, I want the runtime to disconnect it before exposing the fallback, so that connector membership and active state cannot disagree. +30. As a Solana user, I want readiness changes to update wallet availability without changing the underlying adapter, so that transient installation state does not reset my wallet integration. +31. As a Solana user, I want explicit Phantom, Trust, and WalletConnect fallbacks preserved, so that current wallet coverage remains available. +32. As an Android Solana user, I want Mobile Wallet Adapter behavior equivalent to the current React provider, so that removing React ownership does not remove mobile discovery. +33. As a Solana user, I want removed Wallet Standard wallets to be removed and disposed according to the registry contract, so that stale adapters do not remain visible. +34. As a browser-wallet user, I want late EVM provider discovery to update connector membership within the existing config, so that environmental discovery remains reactive. +35. As a RainbowKit consumer, I want dynamic connector membership and readiness to appear through the normal Wagmi hooks, so that the wallet UI needs no private discovery bridge. +36. As a React integrator, I want WagmiConfigProvider to expose the service-owned config after bootstrap, so that Wagmi and WalletService share one authority. +37. As a React integrator, I want an inert fallback config only during bootstrap, so that third-party hooks remain mount-safe before the real config exists. +38. As a feature developer, I want wallet atoms to be read-only Wallet Projections, so that feature code cannot accidentally become a second Wallet State owner. +39. As a feature developer, I want the existing feature-facing wallet selectors and hooks to retain their behavior, so that this ownership refactor does not require UI rewrites. +40. As a package consumer, I want the published React and bundled entry APIs to remain compatible, so that upgrading does not require integration changes. +41. As a maintainer, I want WalletService to expose a small behavioral interface, so that wallet orchestration can be tested without mounting every atom and provider. +42. As a maintainer, I want all listener, fiber, adapter, and registry resources scoped to WalletService, so that disposal is deterministic. +43. As a maintainer, I want volatile Wagmi internal connector-store access encapsulated behind one service-owned seam, so that a dependency upgrade has one repair point. +44. As a maintainer, I want the React Solana providers and root-input bridge removed, so that React is no longer an owner of wallet initialization. +45. As a maintainer, I want the legacy Wallet Standard registration path removed, so that the widget uses the modern registry contract only. +46. As a maintainer, I want one serialized wallet event loop, so that related connection, connector, enrichment, and lifecycle updates publish atomically and in order. +47. As a maintainer, I want private connector-specific routing context to remain outside the public Wallet Projection, so that consumers receive a minimal stable snapshot. +48. As a maintainer, I want one active widget per document to remain the supported lifecycle, so that this refactor does not imply unsupported global isolation guarantees. + +## Implementation Decisions + +- WalletService owns the complete scoped Wallet Runtime: Wallet Bootstrap, the Wagmi config, canonical Wallet State, connector streams, connector-specific routing context, external-provider synchronization, Solana discovery, tracking, unsupported-chain handling, commands, and cleanup. +- Each application-runtime generation constructs one fresh WalletService layer. All watchers, subscriptions, fibers, adapters, Wallet Standard registrations, and other acquired resources are released with that scope. +- WalletService captures one Wallet Bootstrap Snapshot from the current normalized widget configuration, enabled-network result, decoded initialization parameters, browser environment, and external-provider presence. It never reads atom-owned wallet state during or after bootstrap. +- The pure initialization-parameter decoder belongs in the domain layer and may be shared by WalletService and feature projections. Cross-feature initialization atoms may remain for routing and workflow concerns, but they do not own Wallet Bootstrap. +- Wallet Topology is fixed after bootstrap. Later ordinary wallet-related configuration publications do not reconstruct Wagmi, replace connectors selected by configuration, or rerun bootstrap. +- Environment-discovered connector membership may change within the fixed Wallet Topology. This includes browser-injected EVM providers and supported Solana discovery/readiness events. +- Connector Mode is selected once from the Wallet Bootstrap Snapshot. The runtime is either in external-provider mode or another configured connector mode; it cannot transition between modes. +- External Provider Snapshot values remain live through a service-owned current-value abstraction. Changing provider identity, account, chain, supported-chain data, or operations is allowed while external-provider presence remains stable. +- A transition from absent to present external-provider input, present to absent input, or an impossible external-provider connector mismatch is a Wallet Runtime Invariant violation. The service logs it once, enters InvariantViolated, stops accepting wallet work, and poisons only that Wallet Runtime. +- Runtime phases are Bootstrapping, Ready, BootstrapFailed, and InvariantViolated. Bootstrapping may transition to Ready or BootstrapFailed. Ready may transition to InvariantViolated. BootstrapFailed and InvariantViolated are terminal. +- BootstrapFailed is reserved for failures that prevent safe construction of the Wallet Runtime. Recoverable reconnect, initial switching, mobile fallback, connector, and enrichment failures do not produce BootstrapFailed. +- WalletService installs core connection and connector watchers before reading their seed values. Ready is published only after those watchers are active and the first canonical Wallet State has been reduced. +- Reconnect, mobile fallback, and initial requested-chain switching begin after core readiness as scoped background work. They preserve their current ordering and failure-tolerance policy and feed results through the same serialized event loop. +- All wallet-related events enter one serialized service-owned event loop. Each event updates private routing context and publishes one atomic runtime snapshot; consumers never observe partially combined connection, chain, account, Ledger, Cosmos, connector-chain, or additional-address state. +- The public runtime snapshot contains the Wallet Runtime Phase and the minimal Wallet Projection required by consumers. Raw Cosmos chain-wallet objects and other connector-specific command-routing details remain private. +- WalletService exposes the current snapshot and a deduplicated stream of subsequent snapshots. It also exposes the constructed Wagmi config once available and retains one service identity for the runtime generation. +- Commands fail immediately while Bootstrapping and after terminal failure; they never wait on Deferred readiness or an atom binding. Existing operation-specific tagged errors remain the public failure vocabulary, extended only where a distinct runtime-phase failure is necessary. +- Every command captures one canonical state and routing-context snapshot when it begins. An in-flight command completes against that captured snapshot even if later events publish a new Wallet State; the next command uses the new snapshot. +- Transaction signing, message signing, account switching, disconnect, state reads, and connector-specific routing resolve directly from WalletService-owned state. No command reads binding-atom state. +- Tracking a supported connection and disconnecting an unsupported connection move into WalletService lifecycle handling. Both behaviors retain connection-key deduplication and current error-swallowing policy where applicable. +- Atoms become read-only adapters over the WalletService current snapshot and changes stream. They may derive feature-friendly values but cannot write canonical Wallet State or bind it back to the service. +- Initialization-key families, the wallet controller resource, WalletBinding, bind/Deferred readiness, lifecycle ownership atoms, external-provider synchronization atoms, and connection/connector/Ledger/Cosmos/additional-address state ownership atoms are removed or reduced to read-only projections as appropriate. +- WagmiConfigProvider keeps the third-party React context boundary. It provides an inert fallback during Bootstrapping, then provides the WalletService-owned Wagmi config by reference. The service config is not replaced for ordinary configuration changes. +- The Solana runtime constructs the RPC Connection directly and synchronously with behavior equivalent to the current provider. The React Solana provider, Solana root input, layout-effect bridge, nullable connection race, and unsafe connection cast are removed. +- The Solana runtime uses the modern Wallet Standard `getWallets()` registry only. It does not preserve deprecated `navigator.wallets` registration compatibility. +- Wallet Standard and Solana wallet-adapter packages imported by production code become direct package dependencies rather than relying on transitive dependencies. +- The scoped Solana runtime instantiates fallback adapters once per Wallet Runtime, wraps compatible registered Standard wallets, follows registry register/unregister events, subscribes to adapter readiness events, removes and disposes unregistered wrappers, deduplicates by wallet name, filters unsupported adapters, and preserves Android Mobile Wallet Adapter environment behavior. +- Explicit Phantom, Trust, WalletConnect, and Android Mobile Wallet Adapter fallbacks remain. A compatible Standard adapter wins over an inactive same-name fallback. +- The connector-membership transitions validated by the prototype are normative: + + | Event | Active adapter | Required result | + | --- | --- | --- | + | Same-name Standard registers | None or unrelated | Publish Standard instead of fallback | + | Same-name Standard registers | Fallback | Keep fallback active and mark Standard pending | + | Active fallback disconnects | Standard pending | Publish the pending Standard | + | Pending Standard unregisters | Fallback | Discard pending Standard and keep fallback | + | Visible inactive Standard unregisters | None | Publish fallback | + | Active Standard unregisters | Standard | Disconnect Standard, remove it, then publish fallback | + +- An adapter readiness change does not rebuild Wagmi and does not replace the underlying adapter. The prototype established that mutating nested RainbowKit metadata alone does not wake React consumers because Wagmi reuses the previous connector array when all outer connector objects are identical. +- To publish readiness, the service creates a fresh outer connector wrapper with updated metadata while preserving its UID, emitter, methods, and underlying adapter, then republishes connector membership through the existing config. This is wrapper refresh, not an active adapter hot-swap. +- Direct access to Wagmi's internal connector store is encapsulated behind one WalletService-owned connector-membership adapter. No feature atom, React provider, or unrelated connector may depend on that internal API. +- Public package exports and current feature-facing wallet behavior remain compatible. Internal wallet atoms, controllers, bindings, and helpers are not public compatibility constraints and may be removed. +- Production code continues to support one active widget instance per document and clean sequential unmount/remount. Concurrent widget support is not introduced. + +## Testing Decisions + +- The primary test seam is the scoped WalletService public contract. Tests construct the service with controllable bootstrap, environment, Wagmi, registry, tracking, and persistence dependencies; drive events through those dependencies; and assert only runtime phases, current snapshots, change streams, commands, lifecycle effects, invariants, and disposal. +- WalletService tests cover one-time bootstrap, atomic snapshot capture, ignored post-bootstrap configuration, service identity stability, scoped remount isolation, listener cleanup, watcher-before-seed ordering, and terminal bootstrap failure. +- WalletService tests cover serialized state publication across connection, connector, chain, account, Ledger, Cosmos, connector-chain, and additional-address changes. They assert complete observable snapshots rather than private reducer steps. +- Command tests cover immediate pre-ready failure, terminal-phase failure, snapshot capture for in-flight commands, next-command use of newly published state, and coherent routing for signing, disconnect, and account switching. +- External-provider tests cover live address/chain/supported-chain/provider changes, automatic connection synchronization, presence changes in both directions, connector mismatch invariants, log-once behavior, and isolation of the failed Wallet Runtime. +- Lifecycle tests cover connection-event deduplication, unsupported-chain disconnect deduplication, recoverable failures, and reset behavior after returning to a supported/disconnected state. +- Solana headless-runtime tests drive a fake modern Wallet Standard registry and fake adapters. They cover initial discovery, register/unregister, correct unregister disposal, name deduplication, unsupported filtering, readiness events, fallback preservation, Android Mobile Wallet Adapter conditions, and runtime-scope cleanup. +- Connector-membership tests cover every normative transition in the prototype table, including deferred same-name replacement and disconnect-before-fallback ordering. Assertions use visible connector membership, active connection, UID stability, and disposal effects rather than the private state-machine representation. +- One narrow browser contract evolves the existing WagmiConfigProvider/RainbowKit-facing suite. It verifies fallback-only-during-bootstrap behavior, service-owned config identity, no config replacement after ordinary settings changes, connection actions against the authoritative config, dynamic Solana membership, and readiness propagation through `useConnectors` with a preserved UID. +- The same browser contract includes a Wallet Projection probe so that the service snapshot, read-only atom adapter, and Wagmi-facing hooks are observed together at the integration boundary. Separate tests for deleted binding/controller/state-owning atoms are removed. +- Existing low-level driver and connector tests remain for protocol-specific transaction decoding, signing, broadcasting, account switching, and provider error normalization. They should not duplicate Wallet Runtime orchestration cases. +- Existing runtime-generation tests remain prior art for scoped service identity, cleanup, and remount behavior. Existing WalletService contract tests are prior art for typed Effect commands. Existing Wagmi provider browser tests are prior art for authoritative config identity and RainbowKit-facing actions. +- Good tests assert externally observable behavior and lifecycle ordering. They do not assert private event-loop queue types, internal atom families, connector-wrapper construction helpers, or exact module layout. +- Tests use deterministic fake registries, adapters, providers, and operations. They do not require installed browser wallets, real RPC endpoints, network access, or timing-dependent global discovery. +- Public API and hygiene checks must continue to pass. Services remain free of React dependencies and preserve feature-to-service dependency direction as documented design constraints. + +## Out of Scope + +- Migrating from legacy `@solana/web3.js` transaction and Connection types to `@solana/client`, `@solana/web3-compat`, or another Solana SDK. +- Adopting `@solana/connector/headless` or replacing the widget's existing adapter contract with ConnectorKit. +- Preserving the deprecated `navigator.wallets` registration path. +- Reconfiguring Wallet Topology in place after Wallet Bootstrap. +- Supporting a runtime transition into or out of external-provider Connector Mode. +- Hot-swapping an active underlying wallet adapter. +- Supporting multiple concurrent widget instances in one document. +- Redesigning wallet modals, connect buttons, account UI, or user-facing copy. +- Changing published package entrypoints or intentionally breaking public React/bundle integrations. +- Persisting Wallet State beyond the existing persistence contracts. +- Shipping the prototype terminal application as production or test code. + +## Further Notes + +- This specification implements the accepted decision that WalletService owns the Wallet Runtime and follows the repository glossary's Wallet Runtime, Wallet Bootstrap Snapshot, Wallet Topology, Connector Mode, Wallet State, Wallet Projection, External Provider Snapshot, and Wallet Runtime Invariant terminology. +- The Solana research establishes that direct Connection construction, the modern Wallet Standard registry, and adapter readiness events are available without React. It also identifies the installed unregister-handler behavior that must not be copied. +- The connector-membership prototype is a primary-source experiment. It demonstrated real Wagmi core and React observation behavior and discovered the outer-wrapper requirement for readiness updates. Capture it outside the production branch according to the prototype workflow after its decisions are absorbed. +- Access to Wagmi's internal connector store is the main dependency risk. The implementation should characterize that boundary and keep it narrow enough to replace if Wagmi changes its internal API. +- The next planning step is to split this spec into blockers-first tracer-bullet tickets. Each ticket should leave the widget in a working state and should identify which obsolete ownership path it can safely remove. diff --git a/.vscode/settings.json b/.vscode/settings.json index 49e9f9c6e..4e103a2a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,16 @@ { - "typescript.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.autoImportFileExcludePatterns": ["@repos/**"], "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit" }, - - "typescript.preferences.autoImportFileExcludePatterns": ["repos/**"], - "javascript.preferences.autoImportFileExcludePatterns": ["repos/**"], - "files.exclude": { - "repos/**": true + "@repos/**": true }, - "files.watcherExclude": { - "repos/**": true + "@repos/**": true }, - "search.exclude": { - "repos/**": true + "@repos/**": true } } diff --git a/AGENTS.md b/AGENTS.md index 57d739464..92ec010c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,62 +1,144 @@ # StakeKit Widget - Agent Guide -## Project Overview -- Monorepo managed with `pnpm` workspaces + Turborepo. -- Main package is `@stakekit/widget` in `packages/widget` (React + TypeScript + Vite). -- Widget supports two entry modes: - - React component export (`src/index.package.ts`) - - Fully bundled renderer (`src/index.bundle.ts`) -- Runtime branches between classic widget and dashboard variant in `src/App.tsx`. - -## Repo Layout (important paths) -- `packages/widget/src/App.tsx` — root app, router setup, bundle renderer. -- `packages/widget/src/Widget.tsx` — non-dashboard route flow (earn/review/steps/details). -- `packages/widget/src/Dashboard.tsx` + `pages-dashboard/*` — dashboard variant UI. -- `packages/widget/src/providers/*` — global provider composition (API, query, wallet, tracking, theme, stores). -- `packages/widget/src/hooks/*` — feature and API hooks. -- `packages/widget/src/domain/*` — shared domain types/helpers. -- `packages/widget/src/translation/*` — i18n resources (`English`, `French`). -- `packages/widget/tests/*` — Vitest browser tests (MSW-backed). -- `packages/examples/*` — integration examples (`with-vite`, `with-vite-bundled`, `with-nextjs`, `with-cdn-script`). - -## Commands Agents Should Use - -### From repo root (all workspaces via Turbo) -- `pnpm build` — build all packages. -- `pnpm lint` — lint/type-check all packages. -- `pnpm test` — run all workspace tests. -- `pnpm format` — run formatting checks/tasks. -- `pnpm check-hygiene` — check unused deps, unresolved imports, circular deps, etc. - -### Focused widget commands (recommended for most tasks) -- `pnpm --filter @stakekit/widget {command}` - -## Agent Working Guidelines (short) -- Keep public API compatibility in `src/index.package.ts` and `src/index.bundle.ts`. -- React Compiler is enabled. Do not add `useMemo`, `useCallback`, or `React.memo` only for render-performance optimization; prefer plain values/functions. -- When changing user-facing copy, update both: - - `packages/widget/src/translation/English/translations.json` - - `packages/widget/src/translation/French/translations.json` -- After changes, run the lint command to check lint and type errors. - -## Useful Context for Debugging -- API client is configured in `packages/widget/src/providers/api/api-client-provider.tsx`. -- React Query defaults are in `packages/widget/src/providers/query-client/index.tsx`. -- App-level config/env mapping is in `packages/widget/src/config/index.ts`. -- Test bootstrapping + MSW worker setup: - - `packages/widget/tests/utils/setup.ts` - - `packages/widget/tests/mocks/worker.ts` - -## Vendored Repositories - -This project vendors external repositories under `@repos/`. - -- Use vendored repositories as read-only reference material when working with related libraries -- Prefer examples and patterns from the vendored source code over generated guesses or web search results -- Do not edit files under `@repos/` unless explicitly asked -- Do not import from `@repos/` - application code should continue importing from normal package dependencies -- `@repos/effect` is a local-only clone of Effect-TS/effect-smol and may be ignored by Git locally -- When searching `@repos/`, use `rg --no-ignore @repos/` so ignored local reference repositories are included without searching unrelated ignored directories -- Before writing any Effect code, inspect `@repos/effect/LLMS.md` -- Before writing code that interacts with Effect `HttpClient`, inspect `agent-patterns/effect-http-client.md` -- Before writing code that uses Effect `Stream`, inspect `agent-patterns/effect-stream.md` +Monorepo (`pnpm` workspaces + Turborepo). Main package is `@stakekit/widget` in +`packages/widget` (React + TypeScript + Vite), published both as a React +component (`src/index.package.ts`) and as a bundled renderer +(`src/index.bundle.ts`). + +This guide is operational: commands, conventions, and where to read more. It +deliberately does not restate the architecture. + +## Where to look + +| For | Read | +| --- | --- | +| Module ownership, dependency direction, feature entries, Effect services, runtimes, Atom conventions, React Context policy | `packages/widget/ARCHITECTURE.md` | +| Domain vocabulary (use it in code, tests, commits) | `CONTEXT.md` | +| Accepted decisions and their rationale | `docs/adr/` | +| Transaction journeys | `ARCHITECTURE.md` next to `features/{classic-transaction-flow,borrow-transaction-flow,transaction-workflow}` | +| Effect APIs before writing Effect code | `@repos/effect/LLMS.md`, then the smallest relevant `agent-patterns/*.md` (see its README) | +| Issue tracker / triage labels / domain docs | `docs/agents/` | + +`packages/widget/src` is organized by ownership, not by React mechanism: `app/` +composes, `features/` owns feature behavior, `resources/` owns cacheable remote +reads, `services/` owns side effects, `domain/` owns framework-independent rules, +`shared/` owns neutral utilities and the UI kit, `public-api/` owns host-facing +types. + +## Commands + +Run every pnpm command through the version pinned by mise: `mise exec -- pnpm ...`. + +Do not install dependencies unless a manifest or lockfile changed, `node_modules` +is missing or invalid, or the work genuinely requires it. When sandboxed, request +approval to install outside the sandbox so pnpm uses the normal global store — +never fall back to a sandboxed install or a project-local `.pnpm-store`. If +approval is denied, report the blocked install instead of reconfiguring the store. + +Root (all workspaces): + +- `pnpm lint` — Biome + `tsc` per package, plus the root `lint:ast` ast-grep scan. +- `pnpm check-hygiene` — architecture enforcement (see below). **Root only.** +- `pnpm check` — lint + hygiene + test + build. Use before handing off a large change. +- `pnpm build`, `pnpm test`, `pnpm format`. + +Widget only (preferred for most tasks) — `pnpm --filter @stakekit/widget