b6d7b1a9ec
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---
276 lines
10 KiB
Go
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
|
|
}
|