diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/.openspec.yaml b/nym-wallet/openspec/changes/node-families-real-ipc/.openspec.yaml new file mode 100644 index 0000000000..573544603b --- /dev/null +++ b/nym-wallet/openspec/changes/node-families-real-ipc/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-09 diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/design.md b/nym-wallet/openspec/changes/node-families-real-ipc/design.md new file mode 100644 index 0000000000..95c4db3580 --- /dev/null +++ b/nym-wallet/openspec/changes/node-families-real-ipc/design.md @@ -0,0 +1,55 @@ +## Context + +Three layers already exist; only the middle one is missing: + +- **Frontend** — `src/requests/families.ts` defines all 18 IPC bindings (9 execute + 9 query) with command names + arg shapes; the real `FamiliesContextProvider` already calls them (and `refreshAll()`s queries after each execute). The only stub is `controlledNodeIds = []`. +- **On-chain client** — `validator-client` exposes `NodeFamiliesSigningClient` (create/update/disband/invite/revoke/accept/reject/leave/kick) and `NodeFamiliesQueryClient` (by-id/by-owner/membership/members-paged/pending/past/…). The `node-families-contract` is **deployed to sandbox** (one family, one member). +- **Wallet Tauri layer (MISSING)** — `src-tauri/src/operations/` has `mixnet/`, `vesting/`, etc. but **no `families/`**, so the 18 invoked commands have no handler. This is the entire gap. + +## Goals / Non-Goals + +**Goals:** implement the 18 Tauri commands over the existing validator-client traits; return frontend-typed results; switch the running app to real data (drop the `controlledNodeIds` stub); verify journeys against sandbox (read smoke + guarded writes). + +**Non-Goals:** contract changes; mainnet; UI/`data-testid` changes; replacing the mock entry (it stays for offline e2e). + +## Decisions + +**D1 — Mirror `operations/mixnet/` for the families command module.** +Add `src-tauri/src/operations/families/{mod,query,execute}.rs`. Each `#[tauri::command]` acquires the account's client from the wallet `State` (as mixnet ops do), calls the corresponding `NodeFamilies{Signing,Query}Client` method, and returns the mapped type. Register all 18 in `main.rs` `invoke_handler`. *Alternative:* one mega-file — rejected for clarity; split execute vs query. + +**D2 — `family_events`: rely on post-execute query refresh, parse events best-effort.** +The mock fabricates `FamilyTxResult.family_events`; on chain they'd come from parsing the tx's wasm events. The provider already `refreshAll()`s all family queries after every execute, so the UI re-derives state from queries, not from `family_events`. Decision: return the real `TransactionExecuteResult` fields and populate `family_events` best-effort (parse wasm events if cheap; otherwise empty) — the UI does not depend on it for correctness. Revisit if a view reads `family_events` directly. *Alternative:* full event parsing up front — deferred as unnecessary for the journeys. + +**D3 — Derive `controlledNodeIds` from the account's bonded nodes.** +Reuse the existing bonding/account node info the wallet already fetches (the operator persona needs "nodes I control"). Replace the `useMemo(() => [], [])` stub in `FamiliesContextProvider` with that derivation. Keep it resilient when the account controls no nodes. + +**D4 — Generate contract types via ts-rs (`nym-wallet-types`) and reconcile with `src/types/families.ts`.** +`src/types/families.ts` was hand-written for the mock. Prefer generating the canonical shapes from the contract Rust types (ts-rs, as other wallet types are) and reconciling field-by-field (cursors, paged envelope, membership, `FamilyTxResult`). Where the hand-written type and generated type diverge, the generated (contract-truth) shape wins; update the mock/types accordingly so mock and real stay parity. + +**D5 — `get_family_config` maps to the contract config/state query.** +The signing trait has `update_node_families_config`; the read side is a contract config/state query (via the generic `query_node_families_contract` if no dedicated method). Wire `get_family_config` to it and shape it to the frontend `FamilyConfig` (e.g. `create_family_fee`). + +**D6 — Sandbox e2e in two stages; writes are guarded and may stay partly manual.** +(1) **Read smoke** (non-mutating): point a build at the real provider + sandbox, assert the Family page renders the known family/member — automatable and safe. (2) **Write flows**: require a *dedicated funded sandbox test account* (mnemonic via CI secret) and tolerate on-chain latency/fees; they mutate real state, so they must target only that account's family/nodes and clean up (disband/leave) at the end. Iterate until the owner/operator journeys pass. If headless account provisioning isn't available, the write tier stays a documented manual run while the read smoke gates CI. *This is the riskiest part and the one most likely to need iteration.* + +**D7 — Fees: follow the wallet's existing execute convention.** +Reuse however `operations/mixnet/` supplies `Option` (auto/simulated default) so the families execute commands behave consistently with the rest of the wallet. + +## Risks / Trade-offs + +- **Type drift mock↔chain** → D4 reconciliation against ts-rs-generated types; a contract-shape test guards it. +- **`family_events` shape mismatch** → D2 leans on query refresh; UI doesn't depend on the fabricated events. +- **Shared sandbox mutation / flakiness / fees** → D6 dedicated funded test account, target-only-self, cleanup; read smoke gates CI, writes non-blocking/manual until provisioning exists. +- **`UpdateFamily` contract shape** (parent §9.5) → verify `update_family` args (`updated_name`/`updated_description: Option`) against the deployed contract on rebase. +- **Sandbox availability/endpoint** → the read smoke must fail soft (skip/non-blocking) if sandbox is down, to avoid false CI reds. + +## Migration Plan + +Additive: the commands don't exist today, so adding them only enables the already-present provider. Roll out: (1) Rust commands + registration (compiles, app boots), (2) type reconciliation, (3) `controlledNodeIds` derivation, (4) read smoke vs sandbox, (5) guarded write flows + iterate. Rollback = the mock entry still runs the UI offline regardless. + +## Open Questions + +- Is there a dedicated funded **sandbox test account** we can use headlessly (mnemonic in CI secrets), or does the write tier stay manual initially? +- Does any current view actually read `FamilyTxResult.family_events`, or is query-refresh sufficient (confirms D2)? +- `get_family_config` — dedicated contract query vs generic state read (confirm during impl). +- Should `src/types/families.ts` be replaced wholesale by ts-rs output, or kept and reconciled selectively? diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/proposal.md b/nym-wallet/openspec/changes/node-families-real-ipc/proposal.md new file mode 100644 index 0000000000..8b353aacdf --- /dev/null +++ b/nym-wallet/openspec/changes/node-families-real-ipc/proposal.md @@ -0,0 +1,28 @@ +## Why + +The Node Families UI is complete and proven against the in-memory mock, but it has never run on real chain data: the frontend invokes 18 Tauri commands (`src/requests/families.ts`) that **don't exist on the Rust side yet**, so the real `FamiliesContextProvider` can't resolve anything. The on-chain pieces are already in place — the `node-families-contract` is deployed to **sandbox**, and `validator-client` already exposes full `NodeFamiliesSigningClient` + `NodeFamiliesQueryClient` traits. The only missing link is the wallet's Tauri command layer that bridges the two. This change adds it and switches the running app from mock to real IPC. + +## What Changes + +- Add a wallet Tauri command module (`src-tauri/src/operations/families/`) implementing the **18 commands** the frontend already calls (9 execute + 9 query), each delegating to the existing `validator-client` node-families traits via the wallet's signing/query client (mirroring `operations/mixnet/`). Register them in the `invoke_handler` (`main.rs`). +- Map chain results to the frontend's TS contract: execute commands return `FamilyTxResult` (incl. `family_events` parsed from the tx, which the mock currently fabricates); query commands return the existing `NodeFamily` / paged / membership shapes. Confirm `nym-wallet-types` (ts-rs) generation matches `src/types/families.ts`, or reconcile. +- Finish the **real provider**: `FamiliesContextProvider` already wires the 18 requests; remove its `controlledNodeIds` stub (currently `[]`) by deriving controlled node ids from the connected account's bonded nodes, so owner/operator personas work on real data. No change to `FamilyPage`. +- The real provider is **already** the default `/family` route; the mock entry (`main.mock.tsx`) stays for the offline Tier-1 suite. This change makes the *production* app show live family data. +- **Iterate to green against sandbox**: a real-IPC tier — first a read-only smoke (queries render the sandbox family/member), then guarded execute flows against a funded sandbox test account — run until the journeys pass. Replaces the parent change's manual task 9.4 with an automated path where feasible. + +## Capabilities + +### New Capabilities +- `families-real-ipc`: The wallet's Tauri command layer for the node-families contract (queries + execute via the nyxd/validator client) and the real-data provider wiring that replaces the mock in the running app. + +### Modified Capabilities + + +## Impact + +- **Rust (`nym-wallet/src-tauri`)**: new `operations/families/{mod.rs,queries.rs,execute.rs}` (or similar); `main.rs` `invoke_handler` additions; uses `nym_validator_client` node-families traits + wallet `State`/signing client; tx-event parsing for `family_events`. Possibly new error variants. +- **Types**: `nym-wallet-types` ts-rs exports for the contract types; reconcile with `src/types/families.ts` (esp. `FamilyTxResult.family_events`, cursors, paged shapes). +- **Frontend (`src`)**: `FamiliesContextProvider` `controlledNodeIds` derivation (drop the `[]` stub); no UI/`data-testid` changes. +- **e2e**: a real-IPC tier (sandbox) — read smoke + guarded write flows; needs a sandbox test account (mnemonic via secrets) and a network/account that tolerates lifecycle mutations. +- **Dependency / sequencing**: builds on `node-families-tauri-webdriver-e2e` (mock app, Tier-1/2 harness) and parent `node-families-wallet` (§9.4/9.5 — real IPC + `UpdateFamily` contract shape). The sandbox contract being live satisfies the external prerequisite. +- **Out of scope**: contract changes (the contract is deployed as-is); mainnet wiring; UI redesign. diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/specs/families-real-ipc/spec.md b/nym-wallet/openspec/changes/node-families-real-ipc/specs/families-real-ipc/spec.md new file mode 100644 index 0000000000..3a493aa502 --- /dev/null +++ b/nym-wallet/openspec/changes/node-families-real-ipc/specs/families-real-ipc/spec.md @@ -0,0 +1,51 @@ +## ADDED Requirements + +### Requirement: Tauri command layer for the node-families contract + +The wallet SHALL implement the Tauri commands the frontend invokes for node families — the 9 execute commands (`create_family`, `update_family`, `disband_family`, `invite_to_family`, `revoke_family_invitation`, `kick_from_family`, `accept_family_invitation`, `reject_family_invitation`, `leave_family`) and the query commands (`get_family_by_id`, `get_family_by_owner`, `get_family_membership`, `get_family_config`, `get_family_members_paged`, `get_pending_invitations_for_family_paged`, `get_pending_invitations_for_node_paged`, `get_past_invitations_for_family_paged`, `get_past_members_for_family_paged`). Each command MUST delegate to the existing `validator-client` node-families traits using the connected account's client, and MUST be registered in the Tauri `invoke_handler`. Command names and argument shapes MUST match `src/requests/families.ts`. + +#### Scenario: Every frontend command resolves on the Rust side + +- **WHEN** the frontend invokes any of the 18 node-families commands +- **THEN** a registered Rust handler executes it against the node-families contract via the validator client +- **AND** no command falls through to "command not found" + +#### Scenario: Execute returns a parsed family transaction result + +- **WHEN** an execute command (e.g. `create_family`) succeeds on chain +- **THEN** the command returns a `FamilyTxResult` whose shape matches the frontend type, including `family_events` derived from the transaction (not fabricated) + +#### Scenario: Queries return the frontend-typed shapes + +- **WHEN** a query command runs against the contract +- **THEN** it returns data matching the corresponding `src/types/families.ts` shape (`NodeFamily`, membership, paged response with cursor) + +### Requirement: Real provider replaces the mock in the running app + +The production `/family` route SHALL render `FamilyPage` backed by the real `FamiliesContextProvider` (Tauri IPC), not the mock. The provider SHALL derive `controlledNodeIds` from the connected account's bonded nodes (removing the current empty-stub), so the operator view reflects nodes the account actually controls. The mock entry SHALL remain available for the offline e2e suite but MUST NOT back the production route. + +#### Scenario: Production app shows live family data + +- **WHEN** a signed-in account opens the Family page in the production app connected to a network with the contract +- **THEN** the page renders that account's family / invites from on-chain queries via the real provider +- **AND** the operator tab lists the account's controlled bonded nodes + +#### Scenario: Mock stays isolated to e2e + +- **WHEN** the production build is produced +- **THEN** the `/family` route uses `FamiliesContextProvider` and the mock provider/entry is excluded (per the mock-build gate) + +### Requirement: Journeys pass against the sandbox contract + +The owner and operator journeys SHALL be verifiable against the node-families contract on **sandbox** through the real provider. A read-only smoke MUST confirm queries render the known sandbox family/member via real IPC. Execute flows, when exercised, MUST run against a dedicated funded sandbox test account and MUST NOT depend on or corrupt unrelated shared state; the suite SHALL be iterated until the targeted journeys pass. + +#### Scenario: Sandbox read smoke + +- **WHEN** the app is connected to sandbox and the Family page loads +- **THEN** the real queries return and render the sandbox family/member without any state-changing transaction + +#### Scenario: Sandbox execute flow (guarded) + +- **WHEN** an execute journey runs against a funded sandbox test account +- **THEN** the on-chain state change is reflected back through the real queries in the UI +- **AND** the flow targets only that test account's family/nodes diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md b/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md new file mode 100644 index 0000000000..2d1e4bb7b4 --- /dev/null +++ b/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md @@ -0,0 +1,39 @@ +## 1. Rust Tauri command layer (operations/families) + +- [ ] 1.1 Scaffold `src-tauri/src/operations/families/{mod.rs,query.rs,execute.rs}` mirroring `operations/mixnet/` (state access, client acquisition, error handling, fee convention per design D7). +- [ ] 1.2 Implement the 9 **execute** commands (`create_family`, `update_family`, `disband_family`, `invite_to_family`, `revoke_family_invitation`, `kick_from_family`, `accept_family_invitation`, `reject_family_invitation`, `leave_family`) over `NodeFamiliesSigningClient`; return `TransactionExecuteResult`-based `FamilyTxResult` (family_events best-effort per D2). +- [ ] 1.3 Implement the **query** commands (`get_family_by_id`, `get_family_by_owner`, `get_family_membership`, `get_family_members_paged`, `get_pending_invitations_for_family_paged`, `get_pending_invitations_for_node_paged`, `get_past_invitations_for_family_paged`, `get_past_members_for_family_paged`) over `NodeFamiliesQueryClient`. +- [ ] 1.4 Implement `get_family_config` against the contract config/state query (design D5), shaped to the frontend `FamilyConfig`. +- [ ] 1.5 Register all 18 commands in the `main.rs` `invoke_handler`; confirm names/arg casing match `src/requests/families.ts`. +- [ ] 1.6 `cargo build` / `cargo clippy` clean; app boots with the commands registered. + +## 2. Types reconciliation + +- [ ] 2.1 Generate ts-rs exports for the node-families contract types into `nym-wallet-types` (as other wallet types) and diff against `src/types/families.ts` (cursors, paged envelope, membership, `FamilyTxResult`). +- [ ] 2.2 Reconcile divergences toward the contract-truth shape; update `src/types/families.ts` (and the mock fixtures/engine if a field changes) so mock and real stay at parity. +- [ ] 2.3 Add/confirm a contract-shape guard (test or ts-rs check) so future contract drift is caught. + +## 3. Real provider wiring (replace mock in the running app) + +- [ ] 3.1 In `FamiliesContextProvider`, replace the `controlledNodeIds = []` stub with a derivation from the connected account's bonded nodes (design D3); resilient when none. +- [ ] 3.2 Verify the production `/family` route renders live data via the real provider (no `data-testid`/UI change); mock entry remains e2e-only. +- [ ] 3.3 Manual smoke on a dev network: sign in, open Family, confirm queries resolve and an execute (e.g. create on a throwaway account) reflects back after refresh. + +## 4. Sandbox read smoke (safe, automatable) + +- [ ] 4.1 Add a read-only smoke that connects the real provider to **sandbox** and asserts the Family page renders the known family/member via real IPC (no state change). Pin the known family id; fail-soft if sandbox is unreachable (design D6). +- [ ] 4.2 Wire it as a non-blocking CI step (or documented run) separate from the mock suites. + +## 5. Sandbox execute flows (guarded) + iterate to green + +- [ ] 5.1 Provision/identify a **dedicated funded sandbox test account** (mnemonic via CI secret); confirm it can pay fees and is safe to mutate. +- [ ] 5.2 Run the owner journey (create → invite → accept → kick → disband) against that account on sandbox through the real provider; handle on-chain latency (poll/refresh) and clean up state at the end. +- [ ] 5.3 Run the operator journey (accept → leave, reject) against seeded invites for that account's nodes. +- [ ] 5.4 Iterate (fix command/type/timing issues) until both journeys pass against sandbox; keep the write tier non-blocking/manual until headless provisioning is reliable. + +## 6. Verify & docs + +- [ ] 6.1 `cargo build` + `tsc` + eslint clean; `pnpm test` (Jest) green; mock Tier-1 Playwright still green (no regression from type changes). +- [ ] 6.2 Confirm `update_family` arg shape matches the deployed contract (parent §9.5: `updated_name`/`updated_description: Option`). +- [ ] 6.3 Document the real-IPC path (commands, the mock→real switch, sandbox smoke + how to run write flows with a test account) in `e2e/README.md` / wallet README. +- [ ] 6.4 Update parent change `node-families-wallet` §9.4 to point at this change as the realization of real IPC.