// Package store is the in-memory mock data layer for the OpenYield web UI. // // It instantiates the real x/*/types structs (Reach, Stash, StashActivity) // from in-memory fixtures and provides create/get/list methods. This is the // app-layer consumption of protocol types (D-070), NOT a cross-x/ production // import — web/ is NOT an x/ module. No keeper, no Cosmos runtime, no app.go // (G-003 boundary enforced by import_test.go / G-025). package store import ( "fmt" "strings" "sync" "time" identitytypes "github.com/oy/openyield/x/identity/types" stashtypes "github.com/oy/openyield/x/stash/types" ) // seedBalanceGrain is the test balance seeded to a new Stash at signup (D-071 // example: 500000 Grain = 50 Bread per GrainsPerBread=10000). const seedBalanceGrain int64 = 500000 // Store is the in-memory mock store. All methods are goroutine-safe (mu). type Store struct { mu sync.Mutex reaches map[string]identitytypes.Reach stashes map[string]stashtypes.Stash stashActivities map[string]stashtypes.StashActivity } // NewStore constructs a Store seeded from fixtures (fixtures.go). func NewStore() *Store { s := &Store{ reaches: map[string]identitytypes.Reach{}, stashes: map[string]stashtypes.Stash{}, stashActivities: map[string]stashtypes.StashActivity{}, } s.seed() return s } // CreateReach atomically creates a Reach (IsNomad=true) + a Stash (D-071). // G-027: HolderID and PublicKey are validated (non-empty, <=128 bytes, no // path separators, no template syntax) before any map write. Returns the // created Reach + Stash. func (s *Store) CreateReach(holderID, publicKey string) (identitytypes.Reach, stashtypes.Stash, error) { if err := validateReachInput(holderID, publicKey); err != nil { return identitytypes.Reach{}, stashtypes.Stash{}, err } s.mu.Lock() defer s.mu.Unlock() if _, dup := s.reaches[holderID]; dup { return identitytypes.Reach{}, stashtypes.Stash{}, fmt.Errorf("holder %q already has a Reach", holderID) } now := time.Now().Unix() reachID := "reach-" + holderID stashID := "stash-" + holderID reach := identitytypes.Reach{ ReachID: reachID, HolderID: holderID, CreatedAt: now, PublicKey: publicKey, IsNomad: true, } stash := stashtypes.Stash{ HolderID: holderID, StashID: stashID, CreatedAt: now, LastActive: now, BalanceGrain: seedBalanceGrain, } activity := stashtypes.StashActivity{ StashID: stashID, ActiveDays: 1, MaxGapDays: 1, LastActivityDay: now, } s.reaches[holderID] = reach s.stashes[holderID] = stash s.stashActivities[stashID] = activity return reach, stash, nil } // ListReaches returns all seeded + created Reaches. func (s *Store) ListReaches() []identitytypes.Reach { s.mu.Lock() defer s.mu.Unlock() out := make([]identitytypes.Reach, 0, len(s.reaches)) for _, r := range s.reaches { out = append(out, r) } return out } // GetReach returns the Reach for a holderID (by HolderID, the stable key). func (s *Store) GetReach(holderID string) (identitytypes.Reach, bool) { s.mu.Lock() defer s.mu.Unlock() r, ok := s.reaches[holderID] return r, ok } // GetStash returns the Stash for a holderID. func (s *Store) GetStash(holderID string) (stashtypes.Stash, bool) { s.mu.Lock() defer s.mu.Unlock() st, ok := s.stashes[holderID] return st, ok } // GetStashActivity returns the StashActivity for a stashID. func (s *Store) GetStashActivity(stashID string) (stashtypes.StashActivity, bool) { s.mu.Lock() defer s.mu.Unlock() a, ok := s.stashActivities[stashID] return a, ok } // validateReachInput enforces G-027: HolderID and PublicKey must be non-empty, // <=128 bytes, and contain no path separators or template syntax. This is a // prototype-robustness gate (the mock store uses holderID as a map key). func validateReachInput(holderID, publicKey string) error { if holderID == "" { return fmt.Errorf("holder id is required") } if len(holderID) > 128 { return fmt.Errorf("holder id too long (max 128)") } if strings.ContainsAny(holderID, "/\\") { return fmt.Errorf("holder id must not contain path separators") } if strings.Contains(holderID, "{{") { return fmt.Errorf("holder id must not contain template syntax") } if publicKey == "" { return fmt.Errorf("public key is required") } if len(publicKey) > 128 { return fmt.Errorf("public key too long (max 128)") } if strings.ContainsAny(publicKey, "/\\") { return fmt.Errorf("public key must not contain path separators") } return nil }