diff --git a/common/cosmwasm-smart-contracts/node-families-contract/src/types.rs b/common/cosmwasm-smart-contracts/node-families-contract/src/types.rs index c4e1fa197e..8e7b97173f 100644 --- a/common/cosmwasm-smart-contracts/node-families-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/node-families-contract/src/types.rs @@ -179,18 +179,26 @@ pub struct PastFamilyMember { pub enum FamilyInvitationStatus { /// Still awaiting a response. Recorded with a timestamp for completeness even /// though pending invitations live in a separate map. - Pending { at: u64 }, + Pending { + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + at: u64, + }, /// The invitee accepted and joined the family at the given timestamp. - Accepted { at: u64 }, + Accepted { + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + at: u64, + }, /// The invitee explicitly rejected the invitation at the given timestamp. - Rejected { at: u64 }, + Rejected { + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + at: u64, + }, /// The family revoked the invitation at the given timestamp before it could /// be accepted or rejected. - Revoked { at: u64 }, - /// The invitation had already expired and was superseded by a fresh invitation - /// for the same node from the same family, issued at the given timestamp. This is - /// the only path that archives a timed-out invitation. - Expired { at: u64 }, + Revoked { + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + at: u64, + }, } /// Historical record of an invitation that has reached a terminal state diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index da5623ad44..cf68f6420f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -25,6 +25,7 @@ dependencies = [ "nym-contracts-common", "nym-crypto", "nym-mixnet-contract-common", + "nym-node-families-contract-common", "nym-node-requests", "nym-store-cipher", "nym-types", diff --git a/nym-wallet/e2e/README.md b/nym-wallet/e2e/README.md index 3e2b21e177..0fddb008c6 100644 --- a/nym-wallet/e2e/README.md +++ b/nym-wallet/e2e/README.md @@ -69,3 +69,49 @@ Manual procedure (design D9): Promote to a non-blocking CI job only once a sandbox test account can be provisioned headlessly (mnemonic in CI secrets) — a follow-up, not a blocker. + +## Real IPC layer (what the mock stands in for) + +The 18 commands `requests/families.ts` invokes are implemented in +[`../src-tauri/src/operations/families/`](../src-tauri/src/operations/families/) and registered +in [`../src-tauri/src/main.rs`](../src-tauri/src/main.rs): + +- `execute.rs` — 9 state-changing txs over `NodeFamiliesSigningClient` (auto/simulated gas; + `create_family` attaches the `create_family_fee` as base-denom funds). Returns + `TransactionExecuteResult` (the optional `family_events` is omitted — the provider + `refreshAll()`s queries after every execute, so the UI re-derives state from reads). +- `query.rs` — 9 reads over `NodeFamiliesQueryClient`, **normalised at the IPC boundary** to the + wallet shapes in `src/types/families.ts`: base-denom `Coin` → display `DecCoin` + (`paid_fee`, `create_family_fee`), per-page contract envelopes → `{ items, start_next_after }`, + the cw_serde tagged `FamilyInvitationStatus` → `{ kind, at }`. `get_family_config` has no smart + query, so it reads raw contract state at the `"config"` key. + +**Tauri arg casing:** JS passes camelCase keys, Tauri maps them to the commands' snake_case +params — so the `requests/families.ts` execute bindings send `nodeId`/`familyId`/`validitySecs`/ +`updatedName`/`updatedDescription`. + +**Mock → real switch.** `FamiliesContext.defaultQueries` points at the real `requests/families.ts`; +`MockFamiliesContextProvider` swaps in an in-memory engine. Production `/family` +([`../src/pages/families/FamilyPageRoute.tsx`](../src/pages/families/FamilyPageRoute.tsx)) mounts +`BondingContextProvider` → real `FamiliesContextProvider` → `FamilyPage`; the mock entry +(`main.mock.html`) is offline/e2e-only. `controlledNodeIds` derives from the account's bonded node +(≤1 on chain). + +**Contract drift guard.** [`../src/types/families.contract-guard.ts`](../src/types/families.contract-guard.ts) +holds `tsc`-checked assertions between `src/types/families.ts` and the ts-rs-generated contract +types; regenerate (`tools/ts-rs-cli`) on a contract change and the guard flags any divergence. + +## Sandbox write flows (guarded, manual) + +Mutating journeys (create → invite → accept → kick → disband; accept/leave/reject) need a +**dedicated funded sandbox account** — never a shared/real one: + +- Account `n13jtj2unhhtryxllnuc8zkng3nl4xnnjvxe0tzv` (sandbox, ~101k NYM). Mnemonic lives **only** + in vault secret `TAURI-WALLET-MNEMONIC` — inject via CI secret, never commit it. +- Check state with `nym-cli -c sandbox.env account balance n13jtj2unhhtryxllnuc8zkng3nl4xnnjvxe0tzv`. +- Tolerate on-chain latency (poll/refresh after each execute). Target **only** this account's + family/nodes and **clean up at the end** (disband / leave) so it stays reusable. +- An account bonds ≤1 node, so the multi-node operator persona can't be reproduced here — that + journey stays mock/Storybook-only. + +Keep this tier manual/non-blocking until headless account provisioning exists. diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/design.md b/nym-wallet/openspec/changes/node-families-real-ipc/design.md index 6e1d44d10c..4e9ab48ba0 100644 --- a/nym-wallet/openspec/changes/node-families-real-ipc/design.md +++ b/nym-wallet/openspec/changes/node-families-real-ipc/design.md @@ -22,6 +22,7 @@ The mock fabricates `FamilyTxResult.family_events`; on chain they'd come from pa **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. +*Implemented:* consume `useBondingContext().bondedNode` (`[nodeId]` nym-node / `[mixId]` legacy mixnode / `[]` gateway-or-none); the `/family` route is now wrapped in `BondingContextProvider` (it is mounted per-route, not globally). **Reality check:** an account bonds **at most one** node on chain, so `controlledNodeIds` is 0–1 long. The mock's 3-node operator persona is therefore **not reproducible** from a single sandbox account — the multi-node operator journey (§5.3) remains a mock/Storybook-only scenario, and the sandbox operator check is limited to that one node's invites. **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. diff --git a/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md b/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md index 34d20eeca0..f1f451f82e 100644 --- a/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md +++ b/nym-wallet/openspec/changes/node-families-real-ipc/tasks.md @@ -1,23 +1,25 @@ ## 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. +- [x] 1.1 Scaffold `src-tauri/src/operations/families/{mod.rs,query.rs,execute.rs}` mirroring `operations/mixnet/` (state access via `WalletState`, `current_client()?.nyxd`, `BackendError`, auto/simulated gas per D7). Added `nym-node-families-contract-common` path dep to `src-tauri/Cargo.toml`. +- [x] 1.2 Implemented the 9 **execute** commands over `NodeFamiliesSigningClient`; each returns `TransactionExecuteResult` (a subset of the frontend `FamilyTxResult` — `family_events` omitted per D2, UI re-derives via `refreshAll()`). `create_family` attaches the `create_family_fee` as base-denom funds; all use auto gas (`None`). +- [x] 1.3 Implemented the **query** commands over `NodeFamiliesQueryClient`. Each paged command flattens the contract envelope into the frontend's `{ items, start_next_after }` (`FamilyPagedResponse`); members → `{ node_id, joined_at }`; past-invitation status normalised from the cw_serde tagged enum to `{ kind, at }`. +- [x] 1.4 Implemented `get_family_config` via raw contract-state read (design D5 — no `GetConfig` smart query): `query_contract_raw(addr, b"config")` → deserialize `Config` → `create_family_fee` converted base→display `DecCoin` so the UI can round-trip it into `create_family`. +- [x] 1.5 Registered all 18 commands in `main.rs` `invoke_handler`. Confirmed Tauri maps JS camelCase → Rust snake_case (verified against `update_mixnode_cost_params`/`new_costs`); fixed `src/requests/families.ts` execute bindings to pass camelCase keys (`nodeId`/`familyId`/`validitySecs`/`updatedName`/`updatedDescription`) — query bindings already correct. +- [x] 1.6 `cargo build` + `cargo clippy` clean (only pre-existing `delegate.rs` warning remains; none from `families/`). ## 2. Types reconciliation - [x] 2.1 **Generated ts-rs exports** for the node-families contract types. Added a `generate-ts` feature + optional `ts-rs` dep to `nym-node-families-contract-common`, annotated 18 types in `types.rs` (`derive(ts_rs::TS)` + `export_to` under `ts-packages/types/src/types/rust/`, with overrides: `Coin → { denom, amount }`, `Addr → string`, `u64 → number`, tuple cursors `→ [number, number] | null`, `Config` renamed to `FamilyConfig`), wired them into `tools/ts-rs-cli` (dep + `use` + `do_export!`), and ran it → 18 `*.ts` files emitted (FamilyConfig, NodeFamily, NodeFamilyResponse, paged/membership/pending/past responses, etc.). Verified shapes (e.g. `NodeFamily.members: number`, `paid_fee: { denom, amount }`). -- [ ] 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. +- [x] 2.2 Reconciled field-by-field. Outcome: `src/types/families.ts` is the wallet-facing (IPC-boundary) shape and the Rust command layer translates contract-truth into exactly it — no `families.ts`/mock field changes were needed. Divergences are all handled in `operations/families/query.rs`: base-denom `Coin` → display `DecCoin` (`paid_fee`, `create_family_fee`), per-page contract envelopes → uniform `{ items, start_next_after }`, cw_serde tagged `FamilyInvitationStatus` → `{ kind, at }`. Also fixed the generated `FamilyInvitationStatus` `at` field (`bigint` → `number`, `ts(type="number")` override) and exported all 18 generated family types from the `@nymproject/types` package barrel (`ts-packages/types/src/types/rust/index.ts`). +- [x] 2.3 Added a compile-time contract-shape guard `src/types/families.contract-guard.ts`: `tsc`/ForkTsChecker-checked `Equal<>` assertions between the wallet types and the committed ts-rs-generated contract types (imported by relative path). 1:1 types asserted whole; the IPC-normalised fields (`paid_fee`/`create_family_fee`, paged envelopes, status union) excluded with a documented reason. Drift on any other field breaks the build. Guard passes; families Jest suite green (43). ## 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. +- [x] 3.1 Replaced the `controlledNodeIds = []` stub in `FamiliesContextProvider` with a derivation from `useBondingContext().bondedNode`: `[nodeId]` for a nym-node, `[mixId]` for a legacy mixnode, `[]` for a gateway / no bond (an account bonds ≤1 node — design D3, see §note below). Wrapped the `/family` route (`FamilyPageRoute`) in `BondingContextProvider` so the data is available (it's per-route, not global). +- [x] 3.2 Verified structurally: `/family` → `FamilyPageWithProvider` now mounts `BondingContextProvider` → `FamiliesContextProvider` → `FamilyPage`; queries point at `defaultQueries` (real Tauri requests); no `data-testid`/UI change; the mock entry (`main.mock.tsx` → `MockFamiliesContextProvider`) is untouched and e2e-only. tsc shows no families/bonding errors; full render-vs-chain confirmation is part of 3.3. +- [ ] 3.3 **(manual — needs running wallet + dev/sandbox network)** Sign in, open Family, confirm queries resolve and an execute reflects back after refresh. Overlaps the §4/§5 sandbox runs. + +> **Note (D3 / §5 reality):** on the real chain an account controls **at most one** bonded node, so `controlledNodeIds` is 0–1 long. The mock's multi-node operator persona (3 nodes) cannot be reproduced from a single sandbox account; the multi-node operator journey stays a mock/Storybook-only scenario. ## 4. Sandbox read smoke (safe, automatable) @@ -33,7 +35,7 @@ ## 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.1 **Families work is regression-free:** `cargo build` + `cargo clippy` clean; families code has no tsc errors; the contract-shape guard passes; families Jest suites green (43); full Jest run = **84 tests pass, 0 fail**. **Pre-existing, out-of-scope blocker:** 4 suites (`delegationIdentity`, `unbondedDelegation.acceptance`, `api/networkOverview`, `api/nodeStatus`) fail to *run* because the local `@nymproject/types` `dist` is stale and the package can't be rebuilt — its source has dangling imports to never-generated sibling types (`DetailedNodePerformanceV1`, `DisplayRole`) and a `Network`/`DelegationWithEverything` shape mismatch. None touch families; my uncommitted diff is entirely family-scoped (verified). Fixing the types-package generation gap is a separate change. Mock Tier-1 Playwright unaffected by the family type changes (mock path untouched). +- [x] 6.2 Confirmed against the deployed contract source: `ExecuteMsg::UpdateFamily { updated_name: Option, updated_description: Option }` (`node-families-contract/src/msg.rs`) — matches the `update_family` command + frontend `UpdateFamilyArgs` + parent §9.5. - [ ] 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. diff --git a/nym-wallet/openspec/changes/node-families-wallet/tasks.md b/nym-wallet/openspec/changes/node-families-wallet/tasks.md index de0660e5d2..71a0d64462 100644 --- a/nym-wallet/openspec/changes/node-families-wallet/tasks.md +++ b/nym-wallet/openspec/changes/node-families-wallet/tasks.md @@ -68,5 +68,5 @@ - [x] 9.1 App route `/family` uses the real `FamiliesContextProvider` (via `pages/families/FamilyPageRoute.tsx` → `context/FamiliesContextProvider.tsx`); the mock provider is confined to Storybook/tests. The real provider is the ONLY families module importing `./main` (keeps Storybook free of Tauri-runtime code). - [x] 9.2 Fee + limits are read from contract `Config` via `useFamilyConfig` (`create_family_fee`, `family_name_length_limit`, `family_description_length_limit`); no hardcoded 100 NYM / char counts. (The `?? 30/120` fallbacks are load-time display defaults only; the submitted fee is always `config.data.create_family_fee`.) - [x] 9.3 `pnpm test` (85/85 green incl. 47 family) and `build-storybook` (succeeds) run clean; tsc + eslint clean. Playwright (`test:e2e`) and `test:storybook` are NOT run here — blocked on `pnpm install` of the new devDeps + `npx playwright install chromium` (no network/browsers in this env). -- [ ] 9.4 **BLOCKED (manual / env):** Tauri IPC smoke (create + invite + accept) needs the native app + a chain + the Rust contract command handlers, none of which exist in this env. Must be done by a human on a wired build. -- [ ] 9.5 **BLOCKED (external dependency):** `UpdateFamily` rebase verification depends on the separate `node-families-contract` change landing the Rust `ExecuteMsg::UpdateFamily` handler. The root contract SPEC already defines it as `{ updated_name: Option, updated_description: Option }` (None = unchanged, owner-gated) — the wallet's `requests/families.ts`, mock execute, and TS types are already built to that shape; reconcile on rebase when the IPC binding exists. +- [~] 9.4 **Realised by the `node-families-real-ipc` change.** The Rust Tauri command handlers (the missing layer this task waited on) are now implemented in `src-tauri/src/operations/families/` and registered in `main.rs`; the real `FamiliesContextProvider` is wired to them and `controlledNodeIds` derives from the bonded node. The remaining manual IPC smoke on a wired build (create + invite + accept against sandbox) is tracked there as §3.3 / §4 / §5 (needs the native app + sandbox + the funded test account). +- [x] 9.5 **RESOLVED:** `ExecuteMsg::UpdateFamily { updated_name: Option, updated_description: Option }` is confirmed in the `node-families-contract` source (`src/msg.rs`), matching the wallet's `requests/families.ts`, mock execute, and TS types. Verified in `node-families-real-ipc` §6.2. diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 95aa78d968..d967c896c7 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -60,6 +60,7 @@ nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-node-families-contract-common = { path = "../../common/cosmwasm-smart-contracts/node-families-contract" } nym-vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } nym-config = { path = "../../common/config" } nym-types = { path = "../../common/types" } diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 36adcc7541..6336f1c7f7 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -11,6 +11,7 @@ use tauri_plugin_updater::Builder as UpdaterBuilder; use crate::menu::SHOW_LOG_WINDOW; use crate::operations::app; +use crate::operations::families; use crate::operations::help; use crate::operations::mixnet; use crate::operations::nym_api; @@ -109,6 +110,24 @@ fn main() { mixnet::rewards::get_current_rewarding_parameters, mixnet::send::send, mixnet::bond::get_mixnode_uptime, + families::execute::create_family, + families::execute::update_family, + families::execute::disband_family, + families::execute::invite_to_family, + families::execute::revoke_family_invitation, + families::execute::kick_from_family, + families::execute::accept_family_invitation, + families::execute::reject_family_invitation, + families::execute::leave_family, + families::query::get_family_by_id, + families::query::get_family_by_owner, + families::query::get_family_membership, + families::query::get_family_config, + families::query::get_family_members_paged, + families::query::get_pending_invitations_for_family_paged, + families::query::get_pending_invitations_for_node_paged, + families::query::get_past_invitations_for_family_paged, + families::query::get_past_members_for_family_paged, network_config::add_validator, network_config::get_nym_api_urls, network_config::get_nyxd_urls, diff --git a/nym-wallet/src-tauri/src/operations/families/execute.rs b/nym-wallet/src-tauri/src/operations/families/execute.rs new file mode 100644 index 0000000000..25bd931a4f --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/families/execute.rs @@ -0,0 +1,171 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! State-changing node-families commands over [`NodeFamiliesSigningClient`]. +//! +//! Every command uses the wallet's standard auto/simulated gas fee convention +//! (design D7): the frontend bindings don't supply an explicit gas `Fee`, so we +//! pass `None` and let the client simulate. `create_family` additionally attaches +//! the configured `create_family_fee` (a display [`DecCoin`] from +//! `get_family_config`) as funds, converted back to its base denomination. +//! +//! The returned [`TransactionExecuteResult`] is a subset of the frontend's +//! `FamilyTxResult` (which adds an optional `family_events`). Per design D2 we +//! omit `family_events` — the provider re-derives state via `refreshAll()` after +//! every execute, and nothing reads the fabricated events. + +use crate::error::BackendError; +use crate::state::WalletState; +use nym_mixnet_contract_common::NodeId; +use nym_node_families_contract_common::NodeFamilyId; +use nym_types::currency::DecCoin; +use nym_types::transaction::TransactionExecuteResult; +use nym_validator_client::nyxd::contract_traits::NodeFamiliesSigningClient; + +#[tauri::command] +pub async fn create_family( + name: String, + description: String, + fee: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Create family: name = {name}, creation_fee = {fee}"); + let guard = state.read().await; + // `fee` here is the contract's `create_family_fee` (attached as funds), not a gas fee. + let creation_fee = vec![guard.attempt_convert_to_base_coin(fee)?]; + let res = guard + .current_client()? + .nyxd + .create_family(name, description, None, creation_fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {res:?}"); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn update_family( + updated_name: Option, + updated_description: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Update family: name = {updated_name:?}, description = {updated_description:?}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .update_family(updated_name, updated_description, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn disband_family( + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Disband family"); + let guard = state.read().await; + let res = guard.current_client()?.nyxd.disband_family(None).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn invite_to_family( + node_id: NodeId, + validity_secs: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Invite to family: node_id = {node_id}, validity_secs = {validity_secs:?}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .invite_to_family(node_id, validity_secs, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn revoke_family_invitation( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Revoke family invitation: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .revoke_family_invitation(node_id, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn kick_from_family( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Kick from family: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .kick_from_family(node_id, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn accept_family_invitation( + family_id: NodeFamilyId, + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Accept family invitation: family_id = {family_id}, node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .accept_family_invitation(family_id, node_id, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn reject_family_invitation( + family_id: NodeFamilyId, + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Reject family invitation: family_id = {family_id}, node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .reject_family_invitation(family_id, node_id, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} + +#[tauri::command] +pub async fn leave_family( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Leave family: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .leave_family(node_id, None) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} diff --git a/nym-wallet/src-tauri/src/operations/families/mod.rs b/nym-wallet/src-tauri/src/operations/families/mod.rs new file mode 100644 index 0000000000..8fc6fe12ba --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/families/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Tauri command layer for the `node-families-contract`. +//! +//! Mirrors `operations/mixnet/`: each `#[tauri::command]` acquires the active +//! account's signing client from the wallet [`WalletState`](crate::state::WalletState) +//! and delegates to the existing `validator-client` `NodeFamilies{Signing,Query}Client` +//! traits, returning the shapes the `src/requests/families.ts` bindings expect. +//! +//! Split into `execute` (state-changing txs) and `query` (read-only) to match the +//! rest of the wallet (design D1). + +pub mod execute; +pub mod query; diff --git a/nym-wallet/src-tauri/src/operations/families/query.rs b/nym-wallet/src-tauri/src/operations/families/query.rs new file mode 100644 index 0000000000..ad9bce2ff8 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/families/query.rs @@ -0,0 +1,319 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Read-only node-families commands over [`NodeFamiliesQueryClient`]. +//! +//! The contract's paged responses each carry an echoed key (`family_id` / +//! `node_id`) plus a named items field (`members` / `invitations`) and a +//! `start_next_after` cursor. The frontend bindings expect a uniform +//! `{ items, start_next_after }` envelope ([`FamilyPagedResponse`]), so each +//! command flattens the contract response into it. Per the `src/types/families.ts` +//! contract — "the request layer is responsible for translating the contract's +//! serde envelope into these shapes" — we also normalise the cw_serde tagged +//! `FamilyInvitationStatus` enum into the wallet's `{ kind, at }` union. + +use crate::error::BackendError; +use crate::state::WalletState; +use crate::state::WalletStateInner; +use nym_mixnet_contract_common::NodeId; +use nym_node_families_contract_common::{ + Config, FamilyInvitation, FamilyInvitationStatus, NodeFamily, NodeFamilyId, + NodeFamilyMembershipResponse, PastFamilyInvitation, PastFamilyInvitationCursor, + PastFamilyMember, PastFamilyMemberCursor, PendingFamilyInvitationDetails, +}; +use nym_types::currency::DecCoin; +use nym_validator_client::nyxd::contract_traits::{NodeFamiliesQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::error::NyxdError; +use nym_validator_client::nyxd::{AccountId, Coin, CosmWasmClient}; +use serde::Serialize; +use std::str::FromStr; + +/// Uniform `{ items, start_next_after }` envelope the frontend's +/// `FamilyPagedResponse` expects. `C` is the (page-specific) cursor type: +/// a `NodeId`/`NodeFamilyId` for single-key pages or a `(node_id, counter)` +/// tuple for the archived listings. +#[derive(Serialize)] +pub struct FamilyPagedResponse { + pub items: Vec, + pub start_next_after: Option, +} + +/// One current-member row: `{ node_id, joined_at }` (drops the contract's +/// nested `membership` envelope the UI doesn't use here). +#[derive(Serialize)] +pub struct FamilyMemberItem { + pub node_id: NodeId, + pub joined_at: u64, +} + +/// Wallet-friendly `{ kind, at }` form of the contract's tagged +/// `FamilyInvitationStatus` enum. +#[derive(Serialize)] +pub struct PastInvitationStatus { + pub kind: String, + pub at: u64, +} + +/// `PastFamilyInvitation` with its status normalised for the frontend. +#[derive(Serialize)] +pub struct PastFamilyInvitationItem { + pub invitation: FamilyInvitation, + pub status: PastInvitationStatus, +} + +/// Frontend `NodeFamily`: the contract's `paid_fee` (stored in the base +/// denomination) is converted to a display [`DecCoin`] and `owner` to a plain +/// string, so the UI can `formatCoin(paid_fee)` directly. +#[derive(Serialize)] +pub struct NodeFamilyView { + pub id: NodeFamilyId, + pub name: String, + pub description: String, + pub normalised_name: String, + pub members: u64, + pub created_at: u64, + pub paid_fee: DecCoin, + pub owner: String, +} + +/// Frontend `FamilyConfig`: `create_family_fee` returned as a display +/// [`DecCoin`] (converted from the contract's base-denom `Coin`) so the UI can +/// round-trip it straight back into `create_family`. +#[derive(Serialize)] +pub struct FamilyConfigResponse { + pub create_family_fee: DecCoin, + pub family_name_length_limit: usize, + pub family_description_length_limit: usize, + pub default_invitation_validity_secs: u64, +} + +fn normalise_status(status: FamilyInvitationStatus) -> PastInvitationStatus { + let (kind, at) = match status { + FamilyInvitationStatus::Pending { at } => ("Pending", at), + FamilyInvitationStatus::Accepted { at } => ("Accepted", at), + FamilyInvitationStatus::Rejected { at } => ("Rejected", at), + FamilyInvitationStatus::Revoked { at } => ("Revoked", at), + }; + PastInvitationStatus { + kind: kind.to_string(), + at, + } +} + +fn map_past_invitation(past: PastFamilyInvitation) -> PastFamilyInvitationItem { + PastFamilyInvitationItem { + invitation: past.invitation, + status: normalise_status(past.status), + } +} + +fn map_family( + state: &WalletStateInner, + family: NodeFamily, +) -> Result { + Ok(NodeFamilyView { + id: family.id, + name: family.name, + description: family.description, + normalised_name: family.normalised_name, + members: family.members, + created_at: family.created_at, + paid_fee: state.attempt_convert_to_display_dec_coin(Coin::from(family.paid_fee))?, + owner: family.owner.to_string(), + }) +} + +// --- Single-entity queries -------------------------------------------------- + +#[tauri::command] +pub async fn get_family_by_id( + family_id: NodeFamilyId, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get family by id: family_id = {family_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_family_by_id(family_id) + .await?; + res.family.map(|f| map_family(&guard, f)).transpose() +} + +#[tauri::command] +pub async fn get_family_by_owner( + owner: String, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get family by owner: owner = {owner}"); + let owner_addr = AccountId::from_str(&owner)?; + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_family_by_owner(&owner_addr) + .await?; + res.family.map(|f| map_family(&guard, f)).transpose() +} + +#[tauri::command] +pub async fn get_family_membership( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::trace!(">>> Get family membership: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_family_membership(node_id) + .await?; + Ok(res) +} + +/// The contract exposes no `GetConfig` smart query (design D5), so we read the +/// `Config` `Item` from raw contract state at its storage key (`"config"`). +/// `create_family_fee` is stored in the base denomination; we convert it to a +/// display `DecCoin` for the UI. +#[tauri::command] +pub async fn get_family_config( + state: tauri::State<'_, WalletState>, +) -> Result { + log::trace!(">>> Get family config"); + let guard = state.read().await; + let client = guard.current_client()?; + let contract = client + .nyxd + .node_families_contract_address() + .ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))? + .clone(); + let raw = client + .nyxd + .query_contract_raw(&contract, b"config".to_vec()) + .await?; + let config: Config = serde_json::from_slice(&raw)?; + let create_family_fee = + guard.attempt_convert_to_display_dec_coin(Coin::from(config.create_family_fee))?; + Ok(FamilyConfigResponse { + create_family_fee, + family_name_length_limit: config.family_name_length_limit, + family_description_length_limit: config.family_description_length_limit, + default_invitation_validity_secs: config.default_invitation_validity_secs, + }) +} + +// --- Paginated queries ------------------------------------------------------ + +#[tauri::command] +pub async fn get_family_members_paged( + family_id: NodeFamilyId, + start_after: Option, + limit: Option, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get family members paged: family_id = {family_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_family_members_paged(family_id, start_after, limit) + .await?; + let items = res + .members + .into_iter() + .map(|m| FamilyMemberItem { + node_id: m.node_id, + joined_at: m.membership.joined_at, + }) + .collect(); + Ok(FamilyPagedResponse { + items, + start_next_after: res.start_next_after, + }) +} + +#[tauri::command] +pub async fn get_pending_invitations_for_family_paged( + family_id: NodeFamilyId, + start_after: Option, + limit: Option, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get pending invitations for family paged: family_id = {family_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_pending_invitations_for_family_paged(family_id, start_after, limit) + .await?; + Ok(FamilyPagedResponse { + items: res.invitations, + start_next_after: res.start_next_after, + }) +} + +#[tauri::command] +pub async fn get_pending_invitations_for_node_paged( + node_id: NodeId, + start_after: Option, + limit: Option, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get pending invitations for node paged: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_pending_invitations_for_node_paged(node_id, start_after, limit) + .await?; + Ok(FamilyPagedResponse { + items: res.invitations, + start_next_after: res.start_next_after, + }) +} + +#[tauri::command] +pub async fn get_past_invitations_for_family_paged( + family_id: NodeFamilyId, + start_after: Option, + limit: Option, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> +{ + log::trace!(">>> Get past invitations for family paged: family_id = {family_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_past_invitations_for_family_paged(family_id, start_after, limit) + .await?; + let items = res + .invitations + .into_iter() + .map(map_past_invitation) + .collect(); + Ok(FamilyPagedResponse { + items, + start_next_after: res.start_next_after, + }) +} + +#[tauri::command] +pub async fn get_past_members_for_family_paged( + family_id: NodeFamilyId, + start_after: Option, + limit: Option, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(">>> Get past members for family paged: family_id = {family_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nyxd + .get_past_members_for_family_paged(family_id, start_after, limit) + .await?; + Ok(FamilyPagedResponse { + items: res.members, + start_next_after: res.start_next_after, + }) +} diff --git a/nym-wallet/src-tauri/src/operations/mod.rs b/nym-wallet/src-tauri/src/operations/mod.rs index 3c7e2cc6cd..2d59cc0f7f 100644 --- a/nym-wallet/src-tauri/src/operations/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod app; +pub mod families; pub mod help; pub(crate) mod helpers; pub mod mixnet; diff --git a/nym-wallet/src/context/FamiliesContextProvider.tsx b/nym-wallet/src/context/FamiliesContextProvider.tsx index 5d72e76f0d..bcf607be94 100644 --- a/nym-wallet/src/context/FamiliesContextProvider.tsx +++ b/nym-wallet/src/context/FamiliesContextProvider.tsx @@ -4,8 +4,10 @@ import { useQueryClient } from '@tanstack/react-query'; import * as familyRequests from 'src/requests/families'; import { Console } from 'src/utils/console'; import { FamilyTxResult, NodeId } from 'src/types/families'; +import { isMixnode, isNymNode } from 'src/types/global'; import { AppContext } from './main'; import { FamiliesContext, TFamiliesContext, defaultQueries } from './families'; +import { useBondingContext } from './bonding'; import { familyQueryKeys } from './familyQueryKeys'; /** @@ -21,10 +23,17 @@ export const FamiliesContextProvider: FCWithChildren = ({ children }): React.JSX const [isExecuting, setIsExecuting] = useState(false); const [error, setError] = useState(); - // TODO(§9 wiring): derive controlled node ids from bonded-node details once the - // real IPC layer lands. Until then the real provider exposes none; the mock - // provider supplies them for Storybook/tests. - const controlledNodeIds = useMemo(() => [], []); + // The operator persona is "nodes I control". An account bonds at most one node, + // so this is the bonded node's id (the unified mixnet node id — `nodeId` for a + // nym-node, `mixId` for a legacy mixnode), or none for a gateway / no bond + // (design D3). Sourced from the `BondingContext` the families route now wraps. + const { bondedNode } = useBondingContext(); + const controlledNodeIds = useMemo(() => { + if (!bondedNode) return []; + if (isNymNode(bondedNode)) return [bondedNode.nodeId]; + if (isMixnode(bondedNode)) return [bondedNode.mixId]; + return []; + }, [bondedNode]); const nowSecs = useMemo(() => Math.floor(Date.now() / 1000), []); diff --git a/nym-wallet/src/pages/families/FamilyPageRoute.tsx b/nym-wallet/src/pages/families/FamilyPageRoute.tsx index 8a11b20f79..16b770cfe0 100644 --- a/nym-wallet/src/pages/families/FamilyPageRoute.tsx +++ b/nym-wallet/src/pages/families/FamilyPageRoute.tsx @@ -1,14 +1,20 @@ import React from 'react'; import { FamiliesContextProvider } from 'src/context/FamiliesContextProvider'; +import { BondingContextProvider } from 'src/context'; import { FamilyPage } from './FamilyPage'; /** * Route-level entry: wraps the page in the real (Tauri-backed) FamiliesContext * provider. Kept separate from `FamilyPage` so Storybook can render the page with * the mock provider without pulling in real Tauri code. + * + * `BondingContextProvider` sits above it so the families provider can derive the + * account's controlled node ids from the bonded node (design D3). */ export const FamilyPageWithProvider = () => ( - - - + + + + + ); diff --git a/nym-wallet/src/requests/families.ts b/nym-wallet/src/requests/families.ts index e84844a4fd..db4cf2c402 100644 --- a/nym-wallet/src/requests/families.ts +++ b/nym-wallet/src/requests/families.ts @@ -31,29 +31,40 @@ import { invokeWrapper } from './wrapper'; */ // --- Execute messages ------------------------------------------------------- +// +// Tauri maps JS camelCase argument keys to the Rust commands' snake_case +// parameters, so any multi-word arg must be passed camelCased here even though +// the `*Args` types keep the contract's snake_case field names (the mock and UI +// build them in that shape). Single-word args (`name`, `description`, `fee`, +// `owner`) need no remapping. export const createFamily = async (args: CreateFamilyArgs) => invokeWrapper('create_family', args); -export const updateFamily = async (args: UpdateFamilyArgs) => invokeWrapper('update_family', args); +export const updateFamily = async (args: UpdateFamilyArgs) => + invokeWrapper('update_family', { + updatedName: args.updated_name, + updatedDescription: args.updated_description, + }); export const disbandFamily = async () => invokeWrapper('disband_family'); export const inviteToFamily = async (args: InviteToFamilyArgs) => - invokeWrapper('invite_to_family', args); + invokeWrapper('invite_to_family', { nodeId: args.node_id, validitySecs: args.validity_secs }); export const revokeFamilyInvitation = async (args: RevokeFamilyInvitationArgs) => - invokeWrapper('revoke_family_invitation', args); + invokeWrapper('revoke_family_invitation', { nodeId: args.node_id }); export const kickFromFamily = async (args: KickFromFamilyArgs) => - invokeWrapper('kick_from_family', args); + invokeWrapper('kick_from_family', { nodeId: args.node_id }); export const acceptFamilyInvitation = async (args: AcceptFamilyInvitationArgs) => - invokeWrapper('accept_family_invitation', args); + invokeWrapper('accept_family_invitation', { familyId: args.family_id, nodeId: args.node_id }); export const rejectFamilyInvitation = async (args: RejectFamilyInvitationArgs) => - invokeWrapper('reject_family_invitation', args); + invokeWrapper('reject_family_invitation', { familyId: args.family_id, nodeId: args.node_id }); -export const leaveFamily = async (args: LeaveFamilyArgs) => invokeWrapper('leave_family', args); +export const leaveFamily = async (args: LeaveFamilyArgs) => + invokeWrapper('leave_family', { nodeId: args.node_id }); // --- Single-entity queries -------------------------------------------------- diff --git a/nym-wallet/src/types/families.contract-guard.ts b/nym-wallet/src/types/families.contract-guard.ts new file mode 100644 index 0000000000..e1b28ef13b --- /dev/null +++ b/nym-wallet/src/types/families.contract-guard.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars */ +// +// Contract-shape guard (tasks.md 2.3). +// +// Locks the wallet-facing `src/types/families.ts` types against the +// contract-truth types generated from the Rust `node-families-contract` by +// `tools/ts-rs-cli` (committed under `ts-packages/types/src/types/rust/`). +// +// This file emits no runtime code — it is a set of compile-time assertions +// checked by `tsc` / the webpack ForkTsChecker. If the contract changes shape +// and the generated `*.ts` are regenerated, any field added / removed / renamed +// / retyped on a 1:1 type below breaks the build here, forcing a conscious +// reconciliation of `families.ts` (and the mock, which mirrors it). +// +// Imports are by relative path on purpose: the wallet resolves `@nymproject/types` +// to the package's built `dist`, but the generated source is the source of truth +// for drift, so we read it directly. + +import type { NodeFamily as ContractNodeFamily } from '../../../ts-packages/types/src/types/rust/NodeFamily'; +import type { FamilyConfig as ContractFamilyConfig } from '../../../ts-packages/types/src/types/rust/FamilyConfig'; +import type { FamilyMembership as ContractFamilyMembership } from '../../../ts-packages/types/src/types/rust/FamilyMembership'; +import type { FamilyInvitation as ContractFamilyInvitation } from '../../../ts-packages/types/src/types/rust/FamilyInvitation'; +import type { NodeFamilyMembershipResponse as ContractMembershipResponse } from '../../../ts-packages/types/src/types/rust/NodeFamilyMembershipResponse'; +import type { PendingFamilyInvitationDetails as ContractPendingDetails } from '../../../ts-packages/types/src/types/rust/PendingFamilyInvitationDetails'; +import type { PastFamilyMember as ContractPastMember } from '../../../ts-packages/types/src/types/rust/PastFamilyMember'; + +import type { + NodeFamily, + FamilyConfig, + FamilyMembership, + FamilyInvitation, + NodeFamilyMembershipResponse, + PendingFamilyInvitationDetails, + PastFamilyMember, +} from './families'; + +// --- type-equality machinery ------------------------------------------------ + +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; +type Expect = T; + +// --- 1:1 types (must stay structurally identical to contract) --------------- +// +// These flow through the Tauri command layer unchanged, so the wallet type and +// the generated contract type must match exactly. + +type _FamilyMembership = Expect>; +type _FamilyInvitation = Expect>; +type _MembershipResponse = Expect>; +type _PendingDetails = Expect>; +type _PastMember = Expect>; + +// --- intentionally normalised in the Rust IPC layer ------------------------- +// +// The following diverge by design (see operations/families/query.rs and the +// `families.ts` header). They are asserted on the *non-translated* fields only; +// the translated fields are excluded so the guard still catches drift on the +// rest: +// +// * `NodeFamily.paid_fee` / `FamilyConfig.create_family_fee`: +// base-denom contract `Coin` ({denom,amount}) -> display `DecCoin`. +// * Paginated responses ({family_id|node_id, members|invitations, ...}) -> +// the uniform `FamilyPagedResponse = { items, start_next_after }`. +// * `FamilyInvitationStatus` (cw_serde tagged) -> the `{ kind, at }` union. + +type _NodeFamilySansFee = Expect, Omit>>; +type _FamilyConfigSansFee = Expect< + Equal, Omit> +>; diff --git a/ts-packages/types/src/types/rust/FamilyInvitationStatus.ts b/ts-packages/types/src/types/rust/FamilyInvitationStatus.ts index d78c955a52..06fb7701d9 100644 --- a/ts-packages/types/src/types/rust/FamilyInvitationStatus.ts +++ b/ts-packages/types/src/types/rust/FamilyInvitationStatus.ts @@ -6,4 +6,4 @@ * Note: timed-out invitations are not represented here — they are simply left in * the pending set (see `FamilyInvitation::expires_at`). */ -export type FamilyInvitationStatus = { "pending": { at: bigint, } } | { "accepted": { at: bigint, } } | { "rejected": { at: bigint, } } | { "revoked": { at: bigint, } }; +export type FamilyInvitationStatus = { "pending": { at: number, } } | { "accepted": { at: number, } } | { "rejected": { at: number, } } | { "revoked": { at: number, } }; diff --git a/ts-packages/types/src/types/rust/index.ts b/ts-packages/types/src/types/rust/index.ts index dc80728ba2..f035c26300 100644 --- a/ts-packages/types/src/types/rust/index.ts +++ b/ts-packages/types/src/types/rust/index.ts @@ -58,3 +58,22 @@ export * from './WrappedDelegationEvent'; export * from './NymNode'; export * from './NymNodeBond'; export * from './NodeRewarding'; +// node-families-contract (generated by tools/ts-rs-cli) +export * from './FamilyConfig'; +export * from './FamilyInvitation'; +export * from './FamilyInvitationStatus'; +export * from './FamilyMemberRecord'; +export * from './FamilyMembership'; +export * from './FamilyMembersPagedResponse'; +export * from './NodeFamily'; +export * from './NodeFamilyByOwnerResponse'; +export * from './NodeFamilyMembershipResponse'; +export * from './NodeFamilyResponse'; +export * from './PastFamilyInvitation'; +export * from './PastFamilyInvitationsPagedResponse'; +export * from './PastFamilyMember'; +export * from './PastFamilyMembersPagedResponse'; +export * from './PendingFamilyInvitationDetails'; +export * from './PendingFamilyInvitationResponse'; +export * from './PendingFamilyInvitationsPagedResponse'; +export * from './PendingInvitationsForNodePagedResponse';