NYM-1199: Propose implementation of real Node Families IPC in wallet
This change introduces the design, requirements, and tasks for connecting the Node Families UI to the on-chain contract via the wallet's Tauri IPC layer. The frontend currently calls 18 commands that are not implemented on the Rust side, leaving a gap between the mock UI and the deployed contract. This proposal details how to bridge this gap, replace the mock provider with real data, and verify journeys against the sandbox environment.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-09
|
||||
@@ -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<Fee>` (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?
|
||||
@@ -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
|
||||
<!-- None: families-app-mock-build / families-app-e2e (from node-families-tauri-webdriver-e2e) are reused, not changed in requirement. -->
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
@@ -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<String>`).
|
||||
- [ ] 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.
|
||||
Reference in New Issue
Block a user