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 }