From 35f3b1c395ff009cf11433a54943e642c2bb47ce Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:58:43 +0530 Subject: [PATCH] feat(tinyplace): resolve identity marketplace seller-side as web-only (#4920) (#5193) --- .../pages/IdentitiesSection.test.tsx | 31 +++ .../agentworld/pages/IdentitiesSection.tsx | 45 ++++ ...24-identity-marketplace-seller-web-only.md | 212 ++++++++++++++++++ ...tity-marketplace-seller-web-only-design.md | 104 +++++++++ src/openhuman/tinyplace/mod.rs | 10 + 5 files changed, 402 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md create mode 100644 docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md diff --git a/app/src/agentworld/pages/IdentitiesSection.test.tsx b/app/src/agentworld/pages/IdentitiesSection.test.tsx index 9f8d2b497..59f50e6d0 100644 --- a/app/src/agentworld/pages/IdentitiesSection.test.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.test.tsx @@ -17,6 +17,7 @@ import { type DirectoryIdentityListingsResponse, PaymentRequiredError, } from '../../lib/agentworld/invokeApiClient'; +import { openUrl } from '../../utils/openUrl'; import { apiClient } from '../AgentWorldShell'; import IdentitiesSection, { IDENTITY_PRICING_TIERS } from './IdentitiesSection'; @@ -39,6 +40,11 @@ vi.mock('../AgentWorldShell', () => ({ }, })); +// External-link opener — assert the seller CTA hands off to the OS browser +// without actually invoking Tauri. +// openUrl returns a Promise (the CTA chains .then/.catch for diagnostics). +vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn(() => Promise.resolve()) })); + // Default happy-path resolutions so async hooks settle without unhandled // rejections. Individual tests override per-case. beforeEach(() => { @@ -1282,3 +1288,28 @@ describe('IDENTITY_PRICING_TIERS', () => { expect(screen.queryByText('$10/yr')).not.toBeInTheDocument(); }); }); + +// Unit coverage only, by design: the affordance is a static info note whose CTA +// hands off to the OS browser via the mocked `openUrl`. There is one real +// cross-process effect (that the OS browser actually opens) but it isn't worth a +// desktop WDIO E2E — these unit tests cover the note + the openUrl call. E2E is +// intentionally skipped for this change (#4920). +describe('Trading tab — seller web-only note', () => { + test('renders the seller pointer note on the Trading tab', async () => { + render(); + await gotoTab('Trading'); + const note = await screen.findByTestId('sell-on-web'); + expect(note).toHaveTextContent(/listing a handle for sale and accepting offers/i); + expect(screen.getByTestId('sell-on-web-cta')).toHaveAttribute( + 'data-analytics-id', + 'identities.sellOnWeb' + ); + }); + + test('CTA opens the tiny.place identities page via openUrl', async () => { + render(); + await gotoTab('Trading'); + await userEvent.click(await screen.findByTestId('sell-on-web-cta')); + expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place/identities'); + }); +}); diff --git a/app/src/agentworld/pages/IdentitiesSection.tsx b/app/src/agentworld/pages/IdentitiesSection.tsx index 47fff1ddb..d173f8a96 100644 --- a/app/src/agentworld/pages/IdentitiesSection.tsx +++ b/app/src/agentworld/pages/IdentitiesSection.tsx @@ -12,7 +12,14 @@ * Offer commitments (signed authorizations — funds move only on acceptance). * Money only moves after the user confirms. The read-only data views (Registry * listing, floor prices, recent sales) are fully functional. + * + * Seller-side *writes* (list a handle for sale, accept / reject an offer/bid) + * are intentionally NOT in-app — they are web-only on tiny.place (the backend + * exposes no seller write routes). Seller-side *reads* do exist (offers on your + * handles, via marketplace_list_offers). The Trading tab links sellers to the + * web app for the write actions it cannot perform (#4920). */ +import debugFactory from 'debug'; import { useCallback, useEffect, useReducer, useRef, useState } from 'react'; import ChipTabs from '../../components/layout/ChipTabs'; @@ -31,12 +38,21 @@ import { type RegistrationChallenge, type RegistryWalletBalance, } from '../../lib/agentworld/invokeApiClient'; +import { openUrl } from '../../utils/openUrl'; import { apiClient } from '../AgentWorldShell'; import { decimalsForAsset, formatAssetAmount } from '../assets'; import CommitFlow from '../components/CommitFlow'; import X402ConfirmDialog from '../components/X402ConfirmDialog'; import { explorerTxUrl as buyExplorerTxUrl, useX402Buy } from '../hooks/useX402Buy'; +const debug = debugFactory('agentworld:identities'); + +// Seller-side identity *writes* (list a handle for sale, accept/reject an offer) +// are web-only — the tiny.place backend exposes no seller write routes (see +// #4920). We point sellers at the web app for those. Hardcoded prod URL, +// matching the `FUND_PAGE_URL` precedent in X402ConfirmDialog.tsx. +const SELL_ON_WEB_URL = 'https://tiny.place/identities'; + // ── Types ───────────────────────────────────────────────────────────────────── type Tab = 'register' | 'registry' | 'trading'; @@ -945,6 +961,35 @@ function TradingTab() { )} + {/* Seller-side *writes* (list-for-sale / accept-reject offer) are web-only + (#4920): the backend exposes no seller write routes. Placed directly + under the listings — where someone who came to sell looks — rather than + below Recent Sales. Viewing offers on your handles is a separate read + that does exist (marketplace_list_offers). */} +
+

+ Listing a handle for sale and accepting offers happens on tiny.place. +

+ +
+ {/* Recent sales */}

diff --git a/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md b/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md new file mode 100644 index 000000000..87ff802cc --- /dev/null +++ b/docs/superpowers/plans/2026-07-24-identity-marketplace-seller-web-only.md @@ -0,0 +1,212 @@ +# Identity Marketplace Seller-Web-Only Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the seller-side dead-end in the desktop Identities → Trading tab with a note pointing users to the web flow, and record that seller-side is web-only by design. + +**Architecture:** Additive UI only. A persistent info note at the bottom of `TradingTab` opens `https://tiny.place/identities` via the existing `openUrl()` helper (mirrors the `X402ConfirmDialog` `/fund` precedent). No SDK, core, or `invokeApiClient` changes — the tiny.place backend has no seller routes. Scoping recorded in two doc comments. + +**Tech Stack:** React 19 + TypeScript, Vitest + Testing Library, `@tauri-apps/plugin-opener` (via `app/src/utils/openUrl.ts`), Rust doc comment. + +## Global Constraints + +- **No i18n:** `IdentitiesSection.tsx` is entirely hardcoded English (e.g. `"Commitment submitted."` line 854, `"Purchased {name}"` line 887). New strings are hardcoded English to match. **No locale-file edits** — do NOT add `useT()` here. +- **Web URL is a hardcoded prod constant:** `https://tiny.place/identities`. Mirrors `FUND_PAGE_URL = 'https://tiny.place/fund'` in `X402ConfirmDialog.tsx`. The tiny.place *web* frontend has no per-env base in `config.ts`. +- **External links go through `openUrl`** from `app/src/utils/openUrl.ts` — never a raw `window.open` or `` for the CTA (the `` explorer links elsewhere in the file are read-only tx links, a different case). +- **Out of scope (do not touch):** `vendor/tinyplace/sdk/**`, `src/openhuman/tinyplace/manifest.rs`, `app/src/lib/agentworld/invokeApiClient.ts`, buy/bid/offer flows. +- **Run commands from repo root** unless noted. The worktree root is `.claude/worktrees/fix-4920`. + +--- + +### Task 1: Seller "sell on tiny.place" note in the Trading tab + +**Files:** +- Modify: `app/src/agentworld/pages/IdentitiesSection.tsx` (imports ~16-37; header docstring ~1-16; `TradingTab` return, insert before line 996 `

` that closes the outer `space-y-4`) +- Test: `app/src/agentworld/pages/IdentitiesSection.test.tsx` (add a `describe` block; add an `openUrl` mock near the top mocks) + +**Interfaces:** +- Consumes: `openUrl(url: string): Promise` from `../../utils/openUrl`; existing `Button` component from `../../components/ui/Button`. +- Produces: a rendered note with testid `sell-on-web` containing a CTA button testid `sell-on-web-cta` that calls `openUrl('https://tiny.place/identities')`. + +- [ ] **Step 1: Add the `openUrl` mock to the existing test file** + +In `app/src/agentworld/pages/IdentitiesSection.test.tsx`, directly after the existing `vi.mock('../AgentWorldShell', …)` block (ends ~line 41), add: + +```ts +// External-link opener — assert the seller CTA hands off to the OS browser +// without actually invoking Tauri. +vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn() })); +``` + +And add its import next to the other imports at the top of the file (after the `IdentitiesSection` import line): + +```ts +import { openUrl } from '../../utils/openUrl'; +``` + +- [ ] **Step 2: Write the failing test** + +Append this `describe` block to `app/src/agentworld/pages/IdentitiesSection.test.tsx` (after the last Trading-tab describe block; reuses the file's existing `gotoTab` helper, `render`, `screen`, `userEvent`): + +```ts +describe('Trading tab — seller web-only note', () => { + test('renders the seller pointer note on the Trading tab', async () => { + render(); + await gotoTab('Trading'); + const note = await screen.findByTestId('sell-on-web'); + expect(note).toHaveTextContent(/selling a handle or responding to offers/i); + }); + + test('CTA opens the tiny.place identities page via openUrl', async () => { + render(); + await gotoTab('Trading'); + await userEvent.click(await screen.findByTestId('sell-on-web-cta')); + expect(vi.mocked(openUrl)).toHaveBeenCalledWith('https://tiny.place/identities'); + }); +}); +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: `pnpm --filter openhuman-app test -- IdentitiesSection --run -t "seller web-only"` +Expected: FAIL — `Unable to find an element by: [data-testid="sell-on-web"]`. + +- [ ] **Step 4: Add the `openUrl` import + web-URL constant to the component** + +In `app/src/agentworld/pages/IdentitiesSection.tsx`, add the import. Import order groups deeper relative paths first, so place it directly **above** `import { apiClient } from '../AgentWorldShell';` (line 33): + +```ts +import { openUrl } from '../../utils/openUrl'; +``` + +Then, immediately after the import block (before the `// ── Types ──` comment at line 39), add the constant: + +```ts +// Seller-side identity actions (list a handle for sale, accept/reject an offer) +// are web-only — the tiny.place backend exposes no seller routes and the +// vendored SDK is a buyer-side compatibility wrapper (see #4920). We point +// sellers at the web app instead. Hardcoded prod URL, matching the +// `FUND_PAGE_URL` precedent in X402ConfirmDialog.tsx. +const SELL_ON_WEB_URL = 'https://tiny.place/identities'; +``` + +- [ ] **Step 5: Insert the note JSX in `TradingTab`** + +In the same file, insert the following block immediately **before** line 996 (the `` that closes the outer `
`), i.e. after the Recent Sales `
` at line 995: + +```tsx + {/* Seller-side is web-only (#4920): no in-app list-for-sale / accept-offer + path exists, so point sellers at the tiny.place web app. */} +
+

+ Selling a handle or responding to offers happens on tiny.place. +

+ +
+``` + +- [ ] **Step 6: Update the header docstring to record the web-only scope** + +In the header docstring, extend the `Write flows are live x402:` list (ends with the "Money only moves…" sentence, ~line 12-14). Add a paragraph immediately before the closing `*/` (line 15): + +```text + * + * Seller-side actions (list a handle for sale, accept / reject an offer/bid) + * are intentionally NOT in-app — they are web-only on tiny.place. The backend + * exposes no seller routes and the vendored SDK is a buyer-side compatibility + * wrapper, so the Trading tab links sellers to the web app instead (#4920). +``` + +- [ ] **Step 7: Run the tests to verify they pass** + +Run: `pnpm --filter openhuman-app test -- IdentitiesSection --run` +Expected: PASS — the new `seller web-only` tests pass and the pre-existing IdentitiesSection tests still pass. + +**E2E scope (approved exception):** no desktop WDIO E2E is added for this change. The affordance is a static info note whose CTA hands off to the OS browser via `openUrl` — there is no cross-process behavior an E2E could assert beyond the unit tests above. This is a documented exception to the standard unit-and-E2E expectation; the exception is also recorded in the design spec's Testing section and in a comment on the test's `describe` block. + +- [ ] **Step 8: Typecheck, lint, and format** + +Run: `pnpm typecheck && pnpm --filter openhuman-app lint -- --fix app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx && pnpm --filter openhuman-app exec prettier --write app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx` +Expected: no type errors, no lint errors, files formatted (import order may be auto-fixed). + +- [ ] **Step 9: Commit** + +```bash +git add app/src/agentworld/pages/IdentitiesSection.tsx app/src/agentworld/pages/IdentitiesSection.test.tsx +git commit -m "feat(tinyplace): point sellers to tiny.place web from Trading tab (#4920)" +``` + +--- + +### Task 2: Record web-only scope in the tinyplace core domain doc + +**Files:** +- Modify: `src/openhuman/tinyplace/mod.rs` (module doc comment, after the `## Seed derivation` block ending line 26) + +Docs-only; no test (Rust doc comment). Folded into a single commit. + +- [ ] **Step 1: Add a scoping note to the module doc** + +In `src/openhuman/tinyplace/mod.rs`, insert this doc-comment section immediately after line 26 (`//! The seed is never logged…`) and before the blank line preceding `pub(crate) mod agent;`: + +```rust +//! +//! ## Marketplace scope (buyer-side only) +//! +//! The identity marketplace handlers here are **buyer-side only**: buy a +//! listing, bid, and make an offer. Seller-side actions — listing a handle for +//! sale and accepting / rejecting offers — are **web-only on tiny.place** (the +//! backend exposes no seller routes). The desktop Trading tab links sellers to +//! the web app instead. See #4920. +``` + +- [ ] **Step 2: Verify it compiles** + +Run: `GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml` +Expected: compiles (doc comment only — no code change). Warnings unrelated to this change are fine. + +- [ ] **Step 3: Format** + +Run: `cargo fmt --manifest-path Cargo.toml` +Expected: no diff on the doc-comment lines (doc comments are not reflowed). + +- [ ] **Step 4: Commit** + +```bash +git add src/openhuman/tinyplace/mod.rs +git commit -m "docs(tinyplace): note identity marketplace is buyer-side only, selling is web-only (#4920)" +``` + +--- + +### Task 3: GitHub bookkeeping (outward-facing — CONFIRM before running) + +**Not code.** Do NOT run these until the user explicitly confirms editing the epic and commenting on the issue (both are outward-facing). Until then, capture them in the PR body. + +- [ ] **Step 1: Comment the resolution on #4920** + +```bash +gh issue comment 4920 --repo tinyhumansai/openhuman --body "Resolved as **web-only by design**: the tiny.place backend exposes no seller routes and the vendored SDK is a buyer-side compatibility wrapper, so listing a handle / accepting-rejecting offers stays on the tiny.place web app. The desktop Trading tab now points sellers there instead of dead-ending. Scope recorded in \`IdentitiesSection.tsx\` and \`src/openhuman/tinyplace/mod.rs\`. See PR ." +``` + +- [ ] **Step 2: Mark #4776 §9 seller items N/A** + +Edit the #4776 body: change the "Sell / list a handle for sale" and "Accept / reject offer" table rows' Status to `N/A (web-only — #4920)`, and change `- [ ] Sell / list flow works` to `- [x] Sell / list flow works — N/A, web-only (#4920)`. (Manual body edit via `gh issue edit 4776 --repo tinyhumansai/openhuman --body-file -` after fetching and patching the current body, to avoid clobbering other sections.) + +--- + +## Self-Review + +- **Spec coverage:** UI affordance → Task 1 (steps 4-5). Hardcoded-English constraint → Global Constraints + Task 1 (no `useT`). Docstring scoping → Task 1 step 6 + Task 2. Test → Task 1 steps 1-3, 7. GitHub bookkeeping → Task 3. Out-of-scope items → Global Constraints. All spec sections covered. +- **Placeholder scan:** `` / `` in Task 3 are intentional fill-at-runtime values for outward-facing text, not code placeholders. No code step contains TBD/TODO. +- **Type consistency:** testids `sell-on-web` / `sell-on-web-cta` and constant `SELL_ON_WEB_URL` used identically in test (Task 1 steps 2) and component (Task 1 steps 4-5). `openUrl` signature matches `app/src/utils/openUrl.ts`. diff --git a/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md b/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md new file mode 100644 index 000000000..ad566ab32 --- /dev/null +++ b/docs/superpowers/specs/2026-07-24-identity-marketplace-seller-web-only-design.md @@ -0,0 +1,104 @@ +# Design: Identity marketplace seller-side is web-only (#4920) + +**Date:** 2026-07-24 +**Issue:** [#4920](https://github.com/tinyhumansai/openhuman/issues/4920) — *[Tiny Place] Identity marketplace is buyer-only: no list-for-sale or accept/reject-offer path* +**Epic:** #4776 §9 (Trading / Identities marketplace) + +## Problem + +The desktop app's identity marketplace (Agent World → Identities → Trading tab) supports +only **buyer-side** actions: browse listings, buy, bid, and make an offer. There is no path +to **sell** (list one's own `@handle`) and no path to **accept / reject** an incoming offer +or bid. A user who wants to sell opens the Trading tab and finds a dead end. + +## Decision + +**Seller-side actions are web-only by design.** The tiny.place backend does not expose +seller routes (create-listing, accept/reject-offer), and the vendored Rust SDK +(`vendor/tinyplace/sdk/rust/src/api/marketplace.rs`) is explicitly a *"compatibility wrapper +for marketplace endpoints used by OpenHuman"* — buyer-side only. Listing a handle and +responding to offers happens on the tiny.place web app. + +This resolves #4920 not by building the full seller stack (SDK → core handler → +`invokeApiClient` → UI), but by: + +1. Removing the in-app dead-end with a clear pointer to the web flow. +2. Recording the web-only scoping decision in-repo. +3. Marking the corresponding #4776 §9 items N/A. + +## Scope + +### In scope + +1. **UI affordance** — `app/src/agentworld/pages/IdentitiesSection.tsx`, Trading tab. + Add an informational note beneath the listings / recent-sales area: + + > ℹ️ Selling a handle or responding to offers happens on tiny.place. **[Open tiny.place →]** + + - The CTA opens `https://tiny.place/identities` via `openUrl()` + (`app/src/utils/openUrl.ts` → `tauri-plugin-opener`), mirroring the existing + `https://tiny.place/fund` precedent in `X402ConfirmDialog.tsx`. + - The web URL is a hardcoded production constant, matching the `/fund` precedent + (the tiny.place *web* frontend has no per-env base in `config.ts`; only the API + host is env-derived). + - Strings are **hardcoded English**, matching this file's existing convention. Unlike + sibling agentworld pages, `IdentitiesSection.tsx` is entirely un-i18n'd (its buy/commit + banners use literal strings, e.g. `"Commitment submitted."`, `"Purchased {name}"`), and + this passes CI (`i18n:check` / `i18n:english:check` validate locale-file parity, not + hardcoded JSX). Introducing a single `useT()` string here would be inconsistent; a + full-file i18n conversion is out of scope. No locale-file changes. + +2. **Scoping documentation** + - Extend the `IdentitiesSection.tsx` header docstring to state that seller-side + (list-for-sale, accept/reject offer) is **web-only by design**, with the reason. + - Add a one-line pointer in the tinyplace domain module doc + (`src/openhuman/tinyplace/mod.rs`). + +3. **Test** — co-located Vitest test asserting the Trading tab renders the seller note and + the CTA invokes `openUrl` with `https://tiny.place/identities` (mock `openUrl`). Covers + the new diff lines for the ≥80% changed-line coverage gate. + +4. **GitHub bookkeeping** (not code; done via `gh`, noted in the PR body) + - #4776 §9: mark "Sell / list a handle for sale", "Accept / reject offer", and the + "Sell / list flow works" checkbox as N/A, referencing #4920's resolution. + - Comment the web-only resolution on #4920. + +### Out of scope (YAGNI) + +- Vendored SDK seller methods (`create_listing` / `accept_offer` / `reject_offer`). +- Core `handle_tinyplace_marketplace_*` seller handlers. +- `invokeApiClient.ts` seller methods. +- Full i18n conversion of `IdentitiesSection.tsx` (the new strings are hardcoded English, + matching the file). + +## Components & data flow + +```text +Trading tab (IdentitiesSection.tsx) + └─ SellOnWebNote ──click──▶ openUrl('https://tiny.place/identities') + └─ tauri-plugin-opener → OS default browser +``` + +No core RPC, no network from the app itself — the CTA hands off to the OS browser. + +## Error handling + +`openUrl()` already handles the CEF IPC-bridge gap by falling back to `window.open` for +http(s) URLs and logs a low-PII telemetry breadcrumb. No new error surface is introduced. + +## Testing + +- **Unit (Vitest):** render `TradingTab`, assert the seller note text is present, that the + CTA carries the `identities.sellOnWeb` analytics id, and that clicking it calls the mocked + `openUrl` with the expected URL. +- No new Rust behavior → no Rust test changes. +- **E2E: intentionally skipped.** The affordance is a static info note whose CTA hands off to + the OS browser via `openUrl` — there is no cross-process behavior a desktop WDIO E2E could + assert beyond the unit tests. Documented as an approved exception to the standard + unit-and-E2E expectation for this change. + +## Rollback / risk + +Low risk: additive UI note + docstring. No behavior change to existing buy/bid/offer flows, +and no i18n/locale files touched (strings are hardcoded English, matching the file). Revert +is a clean removal of the note block plus its docstring note and the two unit tests. diff --git a/src/openhuman/tinyplace/mod.rs b/src/openhuman/tinyplace/mod.rs index c99f90f04..f42c1b3b6 100644 --- a/src/openhuman/tinyplace/mod.rs +++ b/src/openhuman/tinyplace/mod.rs @@ -24,6 +24,16 @@ //! access. The seed is derived via the same SLIP-0010 path used for all Solana //! signing (`m/44'/501'/0'/0'`); the wallet key becomes the tiny.place identity. //! The seed is never logged, persisted, or returned across any IPC boundary. +//! +//! ## Marketplace scope (seller-side writes are web-only) +//! +//! The identity marketplace **write** handlers here are buyer-side only: buy a +//! listing, bid, and make an offer. Seller-side *writes* — listing a handle for +//! sale and accepting / rejecting offers — are **web-only on tiny.place** (the +//! backend exposes no seller write routes). Seller-side *reads* do exist: +//! `marketplace_list_offers` takes a `name` filter for offers targeting a handle +//! you own. The desktop Trading tab links sellers to the web app for the actions +//! it cannot perform. See #4920. pub(crate) mod agent; mod agent_tools;