Commit graph

8 commits

Author SHA1 Message Date
2e25e34d11 feat(plantare): PlantareService — orchestrate the signed handshake
The app-layer driver that turns local intent into a signed proposal,
counter-signs or declines incoming ones, and reconciles every move into
the local ledger. Cryptography and wire format stay in commons_core;
this owns persistence and orchestration.

- propose(): builds + self-signs a PlantarePledge, records the local row
  (remoteState=proposed, my stub) and sends it. Direction maps to
  debtor/creditor: iReturn = I received and owe, owedToMe = I gave.
- accept(): counter-signs the exact in-hand proposal (verified) and sends
  the doubly-signed copy back, closing the row.
- decline(): notifies the proposer and marks the row declined.
- ingest(): verifies stubs before storing — a bad-signature proposal or a
  half-signed "accept" is dropped; proposals not addressed to me are
  ignored. Pending proposals held in memory keyed by pledge id (relay
  redelivers on reconnect), so accept signs the verified payload.

Wired into DI + Bootstrap alongside the inbox/sync listeners and torn
down/rebuilt on identity switch. SocialSession now exposes the transport.

Tests: full propose→accept close, decline, and the three drop paths with
a records-only transport + real repos. services/data green.
2026-07-14 11:21:46 +02:00
7869a7163b feat(social): standard NIP-56 reporting — ReportTransport in commons_core, report sheet on offers and chats, local hide + optional block 2026-07-13 08:26:38 +02:00
7e7340d643 feat(ratings): Wallapop-style ratings in chat and offer detail
- SocialSession grows a ratings transport (fifth interface on the one
  shared channel).
- PeerRatingCubit: count, locale-aware average, and circleCount — how
  many ratings come from your own circle (reuses the ego-centric trust
  rule), the signal that makes strangers' review-stuffing irrelevant.
- Chat: slim rating strip under the trust banner; you can rate only
  someone you've talked with (soft anchor until the signed bilateral
  exchange form lands). Sheet with 5 stars + optional comment,
  pre-filled for editing, with a take-back action.
- Offer detail: author's stars under their name, with 'N from people
  you know' when your circle rated them; nothing shown when unrated.
- i18n ratings.* (en/es/pt/ast); tests for cubit and sheet.
2026-07-11 13:13:43 +02:00
d0dbed9bc2 feat(sync): device-to-device inventory sync, end to end
Wires the encrypted sync transport into a working replication between one
identity's own devices, reusing the CRDT-ready model (HLC + tombstones +
LWW) the data model was built for.

- SocialSession gains a 5th interface, sync, over the shared connection,
  built with a per-install deviceId + the app's inventory namespace.
- DeviceIdStore: a stable random per-install id in the keystore — distinct
  from the seed-derived CRDT node id, which is IDENTICAL across a user's
  devices (so it couldn't tell them apart / they'd clobber each other's
  replaceable record).
- InventorySnapshotIO (implemented by ExportImportService): build/apply the
  inventory as the SAME interchange JSON as backups but UNSEALED — the sync
  transport does the encryption, so it must not double-seal. Apply = the
  existing idempotent LWW importInventory.
- SyncService: on (re)connect publishes this device's snapshot + subscribes
  to the others'; a local edit (debounced, off tableUpdates) republishes;
  incoming snapshots merge idempotently; skips its own echo; a byte check
  skips re-publishing an unchanged snapshot so the ping-pong converges.
  Foreground only, like the inbox.
- DI/Bootstrap/switchSocialAccount wire and lifecycle it per identity.

Tests: DeviceIdStore, ExportImportService snapshot round-trip (unsealed +
idempotent), SyncService.handleRemote (applies others', skips own).

Follow-ups: deltas vs full-snapshot pushes; background sync; conflict UI.
2026-07-10 23:45:40 +02:00
a109a84714 feat(identity): switch social identity, all from the one backup
Adds pseudonymous, switchable social identities derived from the SAME root
seed via an account index (NostrKeyDerivation.deriveFromSeed(seed, account)).
HKDF is one-way so accounts are unlinkable to the Ğ1 key; account 0 is the
original identity, byte-for-byte unchanged (no rotation for current users),
and every account regenerates from the single seed — so switching adds
nothing to back up.

- SocialAccountStore: keystore-backed active account + max created.
- Per-identity stores (chats, profile, name cache) namespaced by account
  scope (account 0 = legacy keys, no migration) so identities never mix.
- switchSocialAccount() re-derives the identity, re-scopes the stores and
  restarts the inbox listener; RestartWidget rebuilds the tree to pick up
  the new social singletons. DB/inventory untouched.
- Profile 'Your identities' switcher: list, create, switch (with a note
  that messages/contacts are kept separate per identity). i18n en/es/pt/ast.

Tests: account-indexed derivation (legacy 0 unchanged, accounts distinct
yet deterministic, negatives rejected); SocialAccountStore; per-identity
store scope isolation. Resolves the flagged 'change identity' decision
(open-decisions §B).

Known: switching resets navigation to home (full tree rebuild).
2026-07-10 20:22:24 +02:00
30246f4292 feat(block2): profiles — your identity card + name/about (NIP-01 kind:0)
Drawer 'Profile' is now live.
- commons_core: ProfileTransport + UserProfile + NostrProfileTransport (kind:0
  publish/fetch), exposed as SocialSession.profile. Round-trip tested against the
  in-process relay (fast dart test).
- app: ProfileStore (local name/about, keystore) + ProfileScreen — shows your
  shareable npub with copy, edits display name + 'about', saves locally and (when
  online) publishes kind:0 so peers can recognise you instead of a raw key.
- Drawer 'Profile' -> /profile (gated like Market/Chat).
- i18n en/es/pt. ProfileStore + profile transport covered by plain/dart tests.

Analyzer clean (both packages).
2026-07-10 12:37:07 +02:00
ee78b0ed62 feat(block2): relay pool — publish to many relays, discover deduped, tolerate failures
Relay-strategy hardening (network-trust §3): don't hang on one relay.
- NostrChannel interface: what the transports need (sign, publish, subscribe,
  reqOnce). Implemented by NostrConnection (single relay) and the new RelayPool.
- RelayPool: connects to all configured relays (skips unreachable ones, throws
  only when none are reachable), publishes to every live relay (accepted if any
  accepts), merges discovery deduped by event id, and survives a relay dropping.
- Transports now depend on NostrChannel, not a concrete connection.
- SocialService.openSession takes ALL configured relays (was relays.first);
  createOffersCubit passes the whole list.

commons_core 63 tests green (3 new pool tests over two in-process relays);
app_seeds social/market 18 green.
2026-07-10 10:35:39 +02:00
51f9338433 feat(block2): wire the social identity into app_seeds (DI, no UI)
Third slice: bridge Block 1's root seed to the Block 2 transport foundation.

- SocialService: derives the secp256k1 Nostr identity from the SAME root seed
  the recovery QR backs up (no extra backup), exposes npub/pubkey, and opens a
  SocialSession = one shared connection carrying all three transports.
- injector.dart: registers SocialService in get_it. Local-first — derivation is
  cheap + offline; no relay is contacted at startup. Replaces the old
  "nodeId slice of the seed is the social pubkey" placeholder; keeps the CRDT
  author id distinct (unifying it would rewrite authorship — a later decision).
- Test: deterministic identity from the seed hex, matches commons_core, rejects
  malformed hex. 5 tests green; app_seeds analyzes clean (0 errors).
2026-07-10 02:44:37 +02:00