tane/spike/block2_spike/lib/src/offer.dart
vjrj 49b872b405 spike(block2): de-risk social layer — key derivation, OfferTransport, NIP-99 round-trip
Throwaway research spike (spike/ branch, outside pub workspace, no prod deps
touched). Validates the open Block 2 decisions before committing a funded round:

1. Deterministic one-way secp256k1 (Nostr) key derived from the Ğ1 root seed
   via HKDF — user still backs up ONE thing. Reproducible + signs (BIP340).
2. OfferTransport abstraction over Nostr NIP-99 (kind 30402); Offer stays
   inventory/location-agnostic, geohash coarsened on the wire (tested).
3. publish->discover-by-geohash proven end-to-end against an in-process
   hermetic mini-relay (~34ms round-trip).

Findings + risks + recommendation in docs/design/spike-block2-findings.md.
Block 1 suite untouched. 14 tests green, analyzer clean.
2026-07-10 01:57:25 +02:00

90 lines
2.8 KiB
Dart

/// The four (well, five) reciprocity modes from sharing-model.md §3, plus the
/// generic `lend` the core reserves for the future "library of things".
enum OfferType { gift, exchange, sale, wanted, lend }
enum OfferStatus { active, reserved, closed }
/// A publishable offer — the "shop window", deliberately DECOUPLED from Lot /
/// inventory (sharing-model.md §2, core-domain-boundary.md §4.3).
///
/// It carries only a chosen, denormalised *summary* of what's offered — never a
/// foreign key into the seed tables, never the full inventory, never an exact
/// address. This is the privacy seam: the core can publish and sync offers
/// WITHOUT understanding seeds. Everything here is what the person elected to
/// reveal.
class Offer {
const Offer({
required this.id,
required this.authorPubkeyHex,
required this.summary,
required this.type,
required this.approxGeohash,
this.status = OfferStatus.active,
this.category,
this.priceAmount,
this.priceCurrency,
this.exchangeTerms,
this.radiusKm,
this.imageUrl,
this.expiresAt,
});
/// Stable per-offer identifier (NIP-99 `d` tag → addressable event).
final String id;
/// The publishing identity (BIP340 x-only pubkey hex). Derived, per Q1.
final String authorPubkeyHex;
/// Human summary the author chose to publish (e.g. "Tomate rosa de Barbastro").
final String summary;
final OfferType type;
final OfferStatus status;
/// Free-text category, prefilled in-app from Species.family but opaque here.
final String? category;
/// LOW-PRECISION geohash only (sharing-model.md §2: "10 km near you", never
/// exact coordinates). The spike enforces a max length in the transport.
final String approxGeohash;
final int? radiusKm;
final num? priceAmount; // only for sale
final String? priceCurrency; // ISO 4217 or a community currency (e.g. "G1")
final String? exchangeTerms; // only for exchange
final String? imageUrl;
final DateTime? expiresAt;
}
/// Result of publishing to a transport.
class PublishResult {
const PublishResult({
required this.accepted,
required this.transportRef,
this.message = '',
});
/// Whether the relay/instance accepted the event.
final bool accepted;
/// The transport-side id (Nostr event id / ActivityPub object id).
final String transportRef;
final String message;
}
/// A discovery query: coarse location + optional facets. Note there is NO way to
/// ask for "everything from person X's inventory" — you can only browse the
/// public shop window by area and kind.
class DiscoveryQuery {
const DiscoveryQuery({
required this.geohashPrefix,
this.types = const {},
this.limit = 100,
});
/// Coarse geohash prefix to search near (e.g. "u09" ≈ tens of km).
final String geohashPrefix;
final Set<OfferType> types;
final int limit;
}