Files
openyield/x/guild/keeper/keeper.go
T
cloudinit-bot b6d7b1a9ec feat(guild): P3 Guild Charter + Chapter Federation + Household/Confederation
Extends x/guild with P3 (v0.7) Guild Charter + Chapter Federation runtime
+ Household one-tap exit + Confederation Voice delegation + D-087
PierCarriesVoice locked const + REQ-064 cooling consts.

- x/guild/types: extend Guild (CommonBondHash, PublicProfile,
  ParentGuildID, IsChapter, SecessionTermsHash, GoodStandingLiens); add
  GuildPublicProfile, Lien, SecessionTerms, ConfederationVoice structs;
  add PierCarriesVoice=false (D-087), CoolingSecessionCoverActiveDays=21,
  CoolingSecessionNonCoverDays=14 consts; extend Params + GenesisState
  (Chapters slice + Chapter->ParentGuildID ref check).
- x/guild/types/msg_guild.go: 5 Msg* (CreateGuild, CreateChapter,
  OneTapExitStand, DelegateConfederationVoice, AddLien) + MsgServer
  interface + Response types (Disclaimer surfaced per REQ-061).
- x/guild/types/expected_keepers.go: StandKeeper + StashKeeper G-003 shims.
- x/guild/keeper: NEW store-backed Keeper (guild/lien/delegation stores)
  + MsgServer handlers + simtest (8 cases).
- x/guild/module.go: AppModule (D-054 simtest-grade).
- x/stand/types: IsHousehold + IsConfederation helpers + ConfederationVoice
  type-level scaffold (REQ-057/REQ-058).

Coverage: x/guild 85.7%, keeper 94.3%, types 97.1%.
G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean.

REQs: REQ-051, REQ-053, REQ-057, REQ-058, REQ-061

---ci---
project: oy
phase: 3
milestone: v0.7
status: execute
---/ci---
2026-08-19 02:16:42 +00:00

276 lines
10 KiB
Go

package keeper
// keeper.go holds the store-backed Keeper for the guild module's Guild
// Charter + Chapter Federation + Household + Confederation runtime (P3,
// REQ-051, REQ-053, REQ-057, REQ-058).
//
// The Keeper wraps an sdk.KVStore via a storeKey. It holds:
// - the Guild records (guild-id -> Guild; both Parent Guilds and Chapters
// are stored here — a Chapter is a Guild with IsChapter=true);
// - the Lien records (guild-id + lien-idx -> Lien; the AddLien handler
// appends here with SecuredAtFounding=false; founding-locked liens
// (SecuredAtFounding=true) are stored on the Guild itself at creation);
// - the Confederation Voice delegation records
// (confederation-stand-id + member-stand-id -> ConfederationVoice).
//
// The Keeper also holds the two expected-keeper shims (StandKeeper for the
// Household/Confederation type check; StashKeeper for the asset return on
// Household one-tap exit). The shims are interfaces (G-003 — no struct
// import of x/stand/types or x/stash/types); the concrete keepers (or
// simtest stubs) satisfy them structurally.
//
// State-machine ordering (vision §7, enforced in every handler):
// ValidateBasic -> handler authz/gate -> state mutation -> ctx.EventManager().EmitEvent
import (
"encoding/json"
"fmt"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/oy/openyield/x/guild/types"
)
// Keeper is the store-backed guild Keeper.
type Keeper struct {
cdc codec.Codec
storeKey storetypes.StoreKey
standKeeper types.StandKeeper
stashKeeper types.StashKeeper
paramsHolder types.Params
}
// NewKeeper constructs a new store-backed guild Keeper. The StandKeeper +
// StashKeeper expected-keeper shims are injected (StandKeeper is nil-able
// for partial wiring — the OneTapExitStand + DelegateConfederationVoice
// handlers REJECT on a nil StandKeeper (the type check is load-bearing);
// StashKeeper is nil-able — a nil StashKeeper skips the asset return on
// one-tap exit (simtest wiring)).
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandKeeper, stashK types.StashKeeper) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
standKeeper: sk,
stashKeeper: stashK,
paramsHolder: types.DefaultParams(),
}
}
// SetStandKeeper sets the StandKeeper expected-keeper shim (for
// post-construction wiring, e.g., app wiring or test setup).
func (k *Keeper) SetStandKeeper(sk types.StandKeeper) { k.standKeeper = sk }
// SetStashKeeper sets the StashKeeper expected-keeper shim.
func (k *Keeper) SetStashKeeper(stashK types.StashKeeper) { k.stashKeeper = stashK }
// SetParams sets the Params (simtest-grade override; a future version will
// load from the params store).
func (k *Keeper) SetParams(p types.Params) { k.paramsHolder = p }
// Params returns the effective Params.
func (k Keeper) Params() types.Params { return k.paramsHolder }
// StoreKey returns the keeper's store key (exported for simtest access to
// the underlying KVStore, e.g., to inject corrupt bytes for marshal-error
// coverage). Mirrors the x/cover simtest pattern.
func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey }
// --- Guild store --------------------------------------------------------------
var guildKeyPrefix = []byte("guild/")
func guildKey(guildID string) []byte {
return append(guildKeyPrefix, []byte(guildID)...)
}
// GetGuild loads a Guild by guild-id. Returns the Guild and true if found,
// or zero value + false if not. Both Parent Guilds and Chapters are stored
// here (a Chapter is a Guild with IsChapter=true).
func (k Keeper) GetGuild(ctx sdk.Context, guildID string) (types.Guild, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(guildKey(guildID))
if bz == nil {
return types.Guild{}, false
}
var g types.Guild
if err := json.Unmarshal(bz, &g); err != nil {
return types.Guild{}, false
}
return g, true
}
// SetGuild persists a Guild by guild-id.
func (k Keeper) SetGuild(ctx sdk.Context, g types.Guild) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(g)
if err != nil {
panic(fmt.Sprintf("guild: marshal guild %q: %v", g.GuildID, err))
}
store.Set(guildKey(g.GuildID), bz)
}
// AllGuilds returns all persisted Guild records (iteration helper,
// unordered). Both Parent Guilds and Chapters are returned.
func (k Keeper) AllGuilds(ctx sdk.Context) []types.Guild {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(guildKeyPrefix, prefixEnd(guildKeyPrefix))
defer iterator.Close()
out := []types.Guild{}
for ; iterator.Valid(); iterator.Next() {
var g types.Guild
if err := json.Unmarshal(iterator.Value(), &g); err == nil {
out = append(out, g)
}
}
return out
}
// --- Lien store ---------------------------------------------------------------
//
// The Lien store is keyed by guild-id + lien-idx. The AddLien handler
// appends here with SecuredAtFounding=false. Founding-locked liens
// (SecuredAtFounding=true) are stored on the Guild itself at creation
// (GoodStandingLiens slice); the AddLien handler rejects any new
// SecuredAtFounding=true lien (founding is a one-time event — REQ-053).
var lienKeyPrefix = []byte("lien/")
func lienKey(guildID string, idx uint32) []byte {
return append(lienKeyPrefix, []byte(fmt.Sprintf("%s/%d", guildID, idx))...)
}
// GetLien loads a Lien by guild-id + lien-idx. Returns the Lien and true if
// found, or zero value + false if not.
func (k Keeper) GetLien(ctx sdk.Context, guildID string, idx uint32) (types.Lien, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(lienKey(guildID, idx))
if bz == nil {
return types.Lien{}, false
}
var l types.Lien
if err := json.Unmarshal(bz, &l); err != nil {
return types.Lien{}, false
}
return l, true
}
// SetLien persists a Lien by guild-id + lien-idx.
func (k Keeper) SetLien(ctx sdk.Context, guildID string, idx uint32, l types.Lien) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(l)
if err != nil {
panic(fmt.Sprintf("guild: marshal lien %s/%d: %v", guildID, idx, err))
}
store.Set(lienKey(guildID, idx), bz)
}
// AllLiens returns all persisted Lien records for a guild (iteration helper,
// unordered — the idx ordering is NOT preserved across iterations; the
// simtest asserts count + content, not order).
func (k Keeper) AllLiens(ctx sdk.Context, guildID string) []types.Lien {
prefix := append(lienKeyPrefix, []byte(guildID+"/")...)
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(prefix, prefixEnd(prefix))
defer iterator.Close()
out := []types.Lien{}
for ; iterator.Valid(); iterator.Next() {
var l types.Lien
if err := json.Unmarshal(iterator.Value(), &l); err == nil {
out = append(out, l)
}
}
return out
}
// NextLienIdx returns the next lien-idx for a guild (the count of existing
// liens — the AddLien handler uses this to assign the new lien's idx). The
// founding-locked liens on the Guild's GoodStandingLiens slice do NOT
// consume an idx in this store (they are stored on the Guild itself); only
// post-founding liens (SecuredAtFounding=false) added via AddLien consume an
// idx here.
func (k Keeper) NextLienIdx(ctx sdk.Context, guildID string) uint32 {
return uint32(len(k.AllLiens(ctx, guildID)))
}
// --- Confederation Voice delegation store --------------------------------------
//
// The delegation store is keyed by confederation-stand-id + member-stand-id.
// The DelegateConfederationVoice handler records one delegation per member
// Stand (a duplicate delegation from the same MemberStandID is REJECTED).
// One-Stand-one-Vote: each member Stand gets exactly 1 Voice in the
// Confederation's aggregate, regardless of size.
var delegationKeyPrefix = []byte("delegation/")
func delegationKey(confederationStandID, memberStandID string) []byte {
return append(delegationKeyPrefix, []byte(fmt.Sprintf("%s/%s", confederationStandID, memberStandID))...)
}
// GetDelegation loads a ConfederationVoice delegation by confederation-stand-id
// + member-stand-id. Returns the ConfederationVoice (from x/guild/types) and
// true if found, or zero value + false if not.
func (k Keeper) GetDelegation(ctx sdk.Context, confederationStandID, memberStandID string) (types.ConfederationVoice, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(delegationKey(confederationStandID, memberStandID))
if bz == nil {
return types.ConfederationVoice{}, false
}
var v types.ConfederationVoice
if err := json.Unmarshal(bz, &v); err != nil {
return types.ConfederationVoice{}, false
}
return v, true
}
// SetDelegation persists a ConfederationVoice delegation by confederation-
// stand-id + member-stand-id.
func (k Keeper) SetDelegation(ctx sdk.Context, v types.ConfederationVoice) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("guild: marshal delegation %s/%s: %v", v.ConfederationStandID, v.MemberStandID, err))
}
store.Set(delegationKey(v.ConfederationStandID, v.MemberStandID), bz)
}
// AllDelegations returns all persisted ConfederationVoice delegations for a
// Confederation Stand (iteration helper, unordered).
func (k Keeper) AllDelegations(ctx sdk.Context, confederationStandID string) []types.ConfederationVoice {
prefix := append(delegationKeyPrefix, []byte(confederationStandID+"/")...)
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(prefix, prefixEnd(prefix))
defer iterator.Close()
out := []types.ConfederationVoice{}
for ; iterator.Valid(); iterator.Next() {
var v types.ConfederationVoice
if err := json.Unmarshal(iterator.Value(), &v); err == nil {
out = append(out, v)
}
}
return out
}
// --- prefixEnd helper ---------------------------------------------------------
// prefixEnd returns the key that sorts immediately after all keys sharing
// the given prefix (the standard prefix-iteration end key: increment the
// last byte, drop overflow). Used for store.Iterator(start, prefixEnd(start))
// prefix scans. Mirrors x/hub/keeper/keeper.go + x/cover/keeper/keeper.go.
func prefixEnd(prefix []byte) []byte {
if len(prefix) == 0 {
return nil
}
end := make([]byte, len(prefix))
copy(end, prefix)
for i := len(end) - 1; i >= 0; i-- {
end[i]++
if end[i] != 0 {
return end
}
}
// All bytes were 0xFF; return nil (iterate to end of store).
return nil
}