9abda8d01e
P5 (final execution phase) of v0.7 delivers REQ-056, REQ-059, REQ-064, REQ-066: x/cover — Bill of Rights ceremony (REQ-056) + Pier Selection (REQ-066): MsgCounselReviewBillOfRights (bonded Counsel, Staked=true gate); MsgSelectPier (Guild Council + GuildKeeper shim + PierSelectionIndex); MsgRevokePierSelection (supermajority + Counsel witness); PierSelectionIndex + PierSelectionRecord structs; bill_review/pier_selection/pier_index stores. x/guild — Secession cooling (REQ-064) + Stand->Pier (REQ-059, D-074): MsgInitiateSecession (SecessionStartedAt + lien audit); MsgCompleteSecession (21d Cover-active / 14d non-Cover cooling + lien audit + covenant clearance + pro-rata settlement event); MsgEscalateStandToPier (10M Grain-cents D-074 threshold, soft upgrade); MsgAcceptPierInvitation; Guild.SecessionStartedAt + SecededAt + Lien.Cleared additive; LOCAL StandPierEscalationAnnualPass VolumeCents const (G-003 local-const mirror of x/stand canonical). x/stand — StandPierEscalationAnnualPassVolumeCents=10000000 canonical const. Coverage: cover/keeper 94.6%, guild/keeper 94.1%, stand/types 100%. G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean. Lexicon green. ---ci--- project: oy phase: 5 milestone: v0.7 status: execute ---/ci---
387 lines
14 KiB
Go
387 lines
14 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
|
|
}
|
|
|
|
// --- P5: Stand→Pier eligibility + acceptance stores (REQ-059, D-074) -----------
|
|
//
|
|
// (REQ-059, D-074). Two new stores. The pier_eligible/ store is keyed by
|
|
// StandID -> bool (the MsgEscalateStandToPier handler sets true when the
|
|
// Stand's annual Pass volume exceeds StandPierEscalationAnnualPassVolumeCents).
|
|
// The pier_accepted/ store is keyed by StandID -> bool (the
|
|
// MsgAcceptPierInvitation handler sets true when the Stand accepts the Pier
|
|
// invitation; the handler REJECTS if the Stand is not Pier-eligible). Both
|
|
// stores hold a single byte (0x00 = false, 0x01 = true) — no JSON marshal
|
|
// needed for a single bool.
|
|
|
|
var pierEligibleKeyPrefix = []byte("pier_eligible/")
|
|
|
|
func pierEligibleKey(standID string) []byte {
|
|
return append(pierEligibleKeyPrefix, []byte(standID)...)
|
|
}
|
|
|
|
// GetStandPierEligible loads a Stand's Pier-eligibility flag (REQ-059,
|
|
// D-074). Returns true if the Stand was marked Pier-eligible by the
|
|
// MsgEscalateStandToPier handler, false otherwise.
|
|
func (k Keeper) GetStandPierEligible(ctx sdk.Context, standID string) bool {
|
|
store := ctx.KVStore(k.storeKey)
|
|
bz := store.Get(pierEligibleKey(standID))
|
|
return len(bz) == 1 && bz[0] == 0x01
|
|
}
|
|
|
|
// SetStandPierEligible persists a Stand's Pier-eligibility flag.
|
|
func (k Keeper) SetStandPierEligible(ctx sdk.Context, standID string, eligible bool) {
|
|
store := ctx.KVStore(k.storeKey)
|
|
v := []byte{0x00}
|
|
if eligible {
|
|
v = []byte{0x01}
|
|
}
|
|
store.Set(pierEligibleKey(standID), v)
|
|
}
|
|
|
|
var pierAcceptedKeyPrefix = []byte("pier_accepted/")
|
|
|
|
func pierAcceptedKey(standID string) []byte {
|
|
return append(pierAcceptedKeyPrefix, []byte(standID)...)
|
|
}
|
|
|
|
// GetStandPierAccepted loads a Stand's Pier-acceptance flag (REQ-059,
|
|
// D-074). Returns true if the Stand accepted the Pier invitation via the
|
|
// MsgAcceptPierInvitation handler, false otherwise.
|
|
func (k Keeper) GetStandPierAccepted(ctx sdk.Context, standID string) bool {
|
|
store := ctx.KVStore(k.storeKey)
|
|
bz := store.Get(pierAcceptedKey(standID))
|
|
return len(bz) == 1 && bz[0] == 0x01
|
|
}
|
|
|
|
// SetStandPierAccepted persists a Stand's Pier-acceptance flag.
|
|
func (k Keeper) SetStandPierAccepted(ctx sdk.Context, standID string, accepted bool) {
|
|
store := ctx.KVStore(k.storeKey)
|
|
v := []byte{0x00}
|
|
if accepted {
|
|
v = []byte{0x01}
|
|
}
|
|
store.Set(pierAcceptedKey(standID), v)
|
|
}
|
|
|
|
// --- P5: Secession lien-audit helper (REQ-064) --------------------------------
|
|
//
|
|
// CheckLiensCleared returns true if every lien on the named Guild (both the
|
|
// founding-locked liens on the Guild's GoodStandingLiens slice + the post-
|
|
// founding liens in the lien/ store) has Cleared=true OR Amount=0 (a
|
|
// cleared-or-zero lien passes the audit). The MsgInitiateSecession +
|
|
// MsgCompleteSecession handlers consult this. A Guild with no liens returns
|
|
// true (the audit passes vacuously).
|
|
func (k Keeper) CheckLiensCleared(ctx sdk.Context, guildID string) bool {
|
|
g, ok := k.GetGuild(ctx, guildID)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, l := range g.GoodStandingLiens {
|
|
if l.Amount != 0 && !l.Cleared {
|
|
return false
|
|
}
|
|
}
|
|
for _, l := range k.AllLiens(ctx, guildID) {
|
|
if l.Amount != 0 && !l.Cleared {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ChapterIsCoverActive returns true if the named Chapter has any lien
|
|
// (founding-locked or post-founding) that references a Cover Pool covenant
|
|
// (CoverPoolCovenantRef non-empty). The MsgCompleteSecession handler
|
|
// consults this to choose the cooling period: Cover-active = 21d, non-Cover
|
|
// = 14d (REQ-064). A Guild that is not found or is not a Chapter returns
|
|
// false (the handler rejects non-Chapters upstream).
|
|
func (k Keeper) ChapterIsCoverActive(ctx sdk.Context, guildID string) bool {
|
|
g, ok := k.GetGuild(ctx, guildID)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for _, l := range g.GoodStandingLiens {
|
|
if l.CoverPoolCovenantRef != "" {
|
|
return true
|
|
}
|
|
}
|
|
for _, l := range k.AllLiens(ctx, guildID) {
|
|
if l.CoverPoolCovenantRef != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|