5a12ab0e76
web/store instantiates real x/identity/types.Reach + x/stash/types.Stash (app-layer consumption per D-070, NOT a cross-x/ import). CreateReach atomically creates Reach (IsNomad=true) + Stash (D-071). G-027 validates HolderID/PublicKey (non-empty, <=128, no path separators, no template syntax). import_test.go enforces G-025: web/ imports only x/*/types, never x/*/keeper or x/<module> (module.go). Coverage 100%. ---ci--- project: oy phase: 1 milestone: v0.6 status: execute ---/ci---
147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
// 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
|
|
}
|