Merge phase/05 into milestone/v0.5-bearers-runtime (P5 complete → v0.4.5)

---ci---
project: oy
phase: 5
milestone: v0.5
status: complete
requirements:
  covered: [REQ-037]
  partial: []
---/ci---
This commit is contained in:
2026-08-18 00:57:25 +00:00
parent 3c52aa1bb2
commit f18b0b151a
8 changed files with 3158 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
package keeper
// keeper.go holds the store-backed Keeper for the services module's
// Care/SIM/Vault/Mail runtime (P5-02-01, REQ-037).
//
// The Keeper wraps an sdk.KVStore via a storeKey. It holds:
// - the registered ServiceInfo records (service-id → ServiceInfo);
// - the per-service-kind metadata records (Care/SIM/Vault/Mail).
//
// The Keeper also holds the two expected-keeper shims (WindowKeeper for
// the window-grant-on-every-op A-552; VaultKeeper for VaultService
// provisioning A-553). The shims are interfaces (G-003 — no struct
// import of x/window/types or x/vault/types); the concrete keepers
// satisfy them structurally. A nil WindowKeeper shim skips the
// window-grant Active check (simtest wiring); a nil VaultKeeper shim
// REJECTS MsgProvisionVault (the VaultService requires a real vault
// keeper — a nil shim is a wiring error, not a simtest skip path; the
// simtest wires a stub vault keeper, never nil).
//
// State-machine ordering (vision §7, enforced in every handler):
// ValidateBasic → keeper authz (window-grant A-552) → 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/services/types"
)
// Keeper is the store-backed services Care/SIM/Vault/Mail keeper.
type Keeper struct {
cdc codec.Codec
storeKey storetypes.StoreKey
windowKeeper types.WindowKeeper
vaultKeeper types.VaultKeeper
}
// NewKeeper constructs a new store-backed services Keeper. The
// WindowKeeper and VaultKeeper expected-keeper shims are injected
// (nil-able for partial tests). A nil WindowKeeper shim skips the
// window-grant Active check (simtest wiring); a nil VaultKeeper shim
// REJECTS MsgProvisionVault (the VaultService requires a real vault
// keeper). The shims may be re-wired post-construction via SetWindowKeeper
// / SetVaultKeeper (app wiring or test setup).
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, wk types.WindowKeeper, vk types.VaultKeeper) Keeper {
return Keeper{
cdc: cdc,
storeKey: storeKey,
windowKeeper: wk,
vaultKeeper: vk,
}
}
// SetWindowKeeper sets the WindowKeeper expected-keeper shim (for
// post-construction wiring, e.g., app wiring or test setup). This is the
// A-552 window-grant-on-every-op shim: the handler consults it on every
// service op (RegisterService / ActivateService / SuspendService /
// RevokeService / IssueCareGrant / ActivateSIM / ProvisionVault /
// BindMailbox) to assert the service's window-id still references an
// Active Window.
func (k *Keeper) SetWindowKeeper(wk types.WindowKeeper) { k.windowKeeper = wk }
// SetVaultKeeper sets the VaultKeeper expected-keeper shim (for
// post-construction wiring, e.g., app wiring or test setup). This is
// the A-553 VaultService provisioning shim: the MsgProvisionVault
// handler delegates the storage-quota-grain provisioning to it.
func (k *Keeper) SetVaultKeeper(vk types.VaultKeeper) { k.vaultKeeper = vk }
// WindowKeeper returns the WindowKeeper expected-keeper shim (for test
// assertion of wiring; the field is unexported to preserve the
// encapsulation of the shim injection).
func (k Keeper) WindowKeeper() types.WindowKeeper { return k.windowKeeper }
// VaultKeeper returns the VaultKeeper expected-keeper shim (for test
// assertion of wiring).
func (k Keeper) VaultKeeper() types.VaultKeeper { return k.vaultKeeper }
// --- ServiceInfo store -----------------------------------------------------
var serviceKeyPrefix = []byte("svc/")
func serviceKey(serviceID string) []byte {
return append(serviceKeyPrefix, []byte(serviceID)...)
}
// GetService loads a registered ServiceInfo by service-id. Returns the
// ServiceInfo and true if found, or zero value + false if not.
func (k Keeper) GetService(ctx sdk.Context, serviceID string) (types.ServiceInfo, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(serviceKey(serviceID))
if bz == nil {
return types.ServiceInfo{}, false
}
var s types.ServiceInfo
if err := json.Unmarshal(bz, &s); err != nil {
return types.ServiceInfo{}, false
}
return s, true
}
// SetService persists a registered ServiceInfo by service-id.
func (k Keeper) SetService(ctx sdk.Context, s types.ServiceInfo) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(s)
if err != nil {
panic(fmt.Sprintf("services: marshal service info %q: %v", s.ServiceID, err))
}
store.Set(serviceKey(s.ServiceID), bz)
}
// AllServices returns all registered ServiceInfo records (iteration
// helper, unordered).
func (k Keeper) AllServices(ctx sdk.Context) []types.ServiceInfo {
store := ctx.KVStore(k.storeKey)
iterator := store.Iterator(serviceKeyPrefix, prefixEnd(serviceKeyPrefix))
defer iterator.Close()
out := []types.ServiceInfo{}
for ; iterator.Valid(); iterator.Next() {
var s types.ServiceInfo
if err := json.Unmarshal(iterator.Value(), &s); err == nil {
out = append(out, s)
}
}
return out
}
// --- Per-kind metadata stores ----------------------------------------------
// Each per-kind metadata record is stored under a kind-specific prefix
// keyed by the service-id (the canonical handle). A given service-id has
// AT MOST one per-kind record (the kind on its ServiceInfo picks the
// kind-specific metadata set).
var (
careKeyPrefix = []byte("kind/care/")
simKeyPrefix = []byte("kind/sim/")
vaultKeyPrefix = []byte("kind/vault/")
mailKeyPrefix = []byte("kind/mail/")
)
func careKey(serviceID string) []byte { return append(careKeyPrefix, []byte(serviceID)...) }
func simKey(serviceID string) []byte { return append(simKeyPrefix, []byte(serviceID)...) }
func vaultKey(serviceID string) []byte { return append(vaultKeyPrefix, []byte(serviceID)...) }
func mailKey(serviceID string) []byte { return append(mailKeyPrefix, []byte(serviceID)...) }
// GetCareService loads the CareService metadata for the named service-id.
func (k Keeper) GetCareService(ctx sdk.Context, serviceID string) (types.CareService, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(careKey(serviceID))
if bz == nil {
return types.CareService{}, false
}
var c types.CareService
if err := json.Unmarshal(bz, &c); err != nil {
return types.CareService{}, false
}
return c, true
}
// SetCareService persists the CareService metadata.
func (k Keeper) SetCareService(ctx sdk.Context, c types.CareService) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(c)
if err != nil {
panic(fmt.Sprintf("services: marshal care service %q: %v", c.CareID, err))
}
store.Set(careKey(c.CareID), bz)
}
// GetSIMService loads the SIMService metadata for the named service-id.
func (k Keeper) GetSIMService(ctx sdk.Context, serviceID string) (types.SIMService, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(simKey(serviceID))
if bz == nil {
return types.SIMService{}, false
}
var s types.SIMService
if err := json.Unmarshal(bz, &s); err != nil {
return types.SIMService{}, false
}
return s, true
}
// SetSIMService persists the SIMService metadata.
func (k Keeper) SetSIMService(ctx sdk.Context, s types.SIMService) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(s)
if err != nil {
panic(fmt.Sprintf("services: marshal sim service %q: %v", s.SIMID, err))
}
store.Set(simKey(s.SIMID), bz)
}
// GetVaultService loads the VaultService metadata for the named service-id.
func (k Keeper) GetVaultService(ctx sdk.Context, serviceID string) (types.VaultService, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(vaultKey(serviceID))
if bz == nil {
return types.VaultService{}, false
}
var v types.VaultService
if err := json.Unmarshal(bz, &v); err != nil {
return types.VaultService{}, false
}
return v, true
}
// SetVaultService persists the VaultService metadata.
func (k Keeper) SetVaultService(ctx sdk.Context, v types.VaultService) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(v)
if err != nil {
panic(fmt.Sprintf("services: marshal vault service %q: %v", v.VaultID, err))
}
store.Set(vaultKey(v.VaultID), bz)
}
// GetMailService loads the MailService metadata for the named service-id.
func (k Keeper) GetMailService(ctx sdk.Context, serviceID string) (types.MailService, bool) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(mailKey(serviceID))
if bz == nil {
return types.MailService{}, false
}
var m types.MailService
if err := json.Unmarshal(bz, &m); err != nil {
return types.MailService{}, false
}
return m, true
}
// SetMailService persists the MailService metadata.
func (k Keeper) SetMailService(ctx sdk.Context, m types.MailService) {
store := ctx.KVStore(k.storeKey)
bz, err := json.Marshal(m)
if err != nil {
panic(fmt.Sprintf("services: marshal mail service %q: %v", m.MailID, err))
}
store.Set(mailKey(m.MailID), bz)
}
// --- 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/partner/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
}
+512
View File
@@ -0,0 +1,512 @@
package keeper
// msg_server.go implements the services module's MsgServer (P5-02-01,
// REQ-037; G-023 ownership split: cosmos-engineer scaffolds the file
// structure + method signatures; backend-engineer implements the handler
// logic bodies). The MsgServer wraps the Keeper + the WindowKeeper and
// VaultKeeper expected-keeper shims (already on the Keeper).
//
// Each method returns a (*Response, error). Handler state-machine ordering
// is enforced: ValidateBasic → keeper authz (window-grant A-552) → state
// mutation → ctx.EventManager().EmitEvent.
//
// Handler set (REQ-037):
// Lifecycle (kind-agnostic):
// - RegisterService: registers a new ServiceInfo (status=Pending).
// Asserts the window-id references an Active Window via the
// WindowKeeper shim (A-552). Idempotent: service-id must not already
// exist. Persists the ServiceInfo + the per-kind metadata record
// for the ServiceKind on the message.
// - ActivateService: Pending → Active. Window-grant still Active.
// - SuspendService: Active → Suspended. Window-grant still Active.
// - RevokeService: any → Revoked (terminal). Idempotent reject on
// already-Revoked (no double-effect). Window-grant still Active
// (A-552: revocation of a Window-revoked service is also a
// Window-violation).
// Per-kind (A-551 typed dispatch — one Msg per ServiceKind):
// - IssueCareGrant (Care) — window-grant A-552 + kind=Care + persists
// the CareService metadata.
// - ActivateSIM (SIM) — window-grant A-552 + kind=SIM + persists
// the SIMService metadata.
// - ProvisionVault (Vault) — window-grant A-552 + kind=Vault + delegates
// the storage-quota-grain provisioning to the VaultKeeper shim (A-553).
// A nil VaultKeeper shim REJECTS the provisioning.
// - BindMailbox (Mail) — window-grant A-552 + kind=Mail + persists
// the MailService metadata.
//
// Nil-shim behavior (simtest wiring): a nil WindowKeeper shim skips the
// window-grant Active check (the handler still mutates state — the
// simtest documents the wiring contract). A nil VaultKeeper shim REJECTS
// MsgProvisionVault (the VaultService requires a real vault keeper —
// a nil shim is a wiring error, not a simtest skip path).
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/oy/openyield/x/services/types"
)
// msgServer is the concrete MsgServer implementation wrapping the Keeper.
type msgServer struct {
Keeper
}
// NewMsgServerImpl returns the services MsgServer for the provided Keeper.
func NewMsgServerImpl(k Keeper) types.MsgServer {
return &msgServer{Keeper: k}
}
var _ types.MsgServer = msgServer{}
// unwrapCtx extracts the sdk.Context from the interface-typed ctx.
func unwrapCtx(ctx interface{}) sdk.Context {
if c, ok := ctx.(sdk.Context); ok {
return c
}
panic(fmt.Sprintf("services: expected sdk.Context, got %T", ctx))
}
// assertWindowActive consults the WindowKeeper shim to assert the named
// window-id still references an Active Window (A-552 window-grant-on-
// every-op). Returns nil if the window is Active OR the WindowKeeper shim
// is nil (simtest wiring skip); returns an error if the shim is non-nil
// and reports a non-Active status or an error (treated as not-Active).
func (s msgServer) assertWindowActive(windowID, op string) error {
if s.Keeper.windowKeeper == nil {
// Simtest wiring: a nil WindowKeeper shim skips the A-552 check.
return nil
}
status, err := s.Keeper.windowKeeper.GetWindowStatus(windowID)
if err != nil {
return fmt.Errorf("services: window-grant check for %s on window %q failed: %w (A-552)", op, windowID, err)
}
if status != types.WindowStatusActive {
return fmt.Errorf("services: window %q is %q; %s rejected (A-552 window-grant-on-every-op)", windowID, status, op)
}
return nil
}
// --- RegisterService -----------------------------------------------------
// RegisterService registers a new ServiceInfo (status=Pending). The
// handler enforces:
// 1. ValidateBasic (stateless).
// 2. Idempotency: service-id must not already exist.
// 3. A-552: the window-id must reference an Active Window via the
// WindowKeeper shim (the authority boundary; checked on every op,
// not just registration). A nil shim skips the check (simtest
// wiring); a non-nil shim reporting a non-Active status REJECTS the
// registration (the service is NOT created).
// 4. The per-kind metadata record is created for the ServiceKind on
// the message (the kind is fixed at registration; A-551 typed
// dispatch — the per-kind handlers later enforce the kind matches).
//
// On success the ServiceInfo is persisted with status=Pending, the
// per-kind metadata record is created (with empty operational fields
// — the per-kind handlers populate them), and an event is emitted.
func (s msgServer) RegisterService(ctx interface{}, msg *types.MsgRegisterService) (*types.MsgRegisterServiceResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
// Idempotency: service-id must not already exist.
if _, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID); ok {
return nil, fmt.Errorf("services: service %q already exists", msg.ServiceID)
}
// A-552: window-id must reference an Active Window (checked on
// EVERY op, including registration).
if err := s.assertWindowActive(msg.WindowID, "RegisterService"); err != nil {
return nil, err
}
// Persist the ServiceInfo (status=Pending).
info := types.ServiceInfo{
ServiceID: msg.ServiceID,
Kind: msg.Kind,
OperatorReachID: msg.OperatorReachID,
Name: msg.Name,
Status: types.ServicePending,
WindowID: msg.WindowID,
}
s.Keeper.SetService(sdkCtx, info)
// Create the per-kind metadata record (empty operational fields —
// the per-kind handlers populate them).
switch msg.Kind {
case types.KindCare:
s.Keeper.SetCareService(sdkCtx, types.CareService{CareID: msg.ServiceID})
case types.KindSIM:
s.Keeper.SetSIMService(sdkCtx, types.SIMService{SIMID: msg.ServiceID})
case types.KindVault:
s.Keeper.SetVaultService(sdkCtx, types.VaultService{VaultID: msg.ServiceID})
case types.KindMail:
s.Keeper.SetMailService(sdkCtx, types.MailService{MailID: msg.ServiceID})
}
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.service_registered",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("kind", string(msg.Kind)),
sdk.NewAttribute("operator_reach_id", msg.OperatorReachID),
sdk.NewAttribute("window_id", msg.WindowID),
sdk.NewAttribute("status", string(types.ServicePending)),
))
return &types.MsgRegisterServiceResponse{}, nil
}
// --- ActivateService ----------------------------------------------------
// ActivateService transitions a service Pending → Active. The handler
// enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. The source status must be Pending (ValidServiceTransition(Pending,
// Active) — the lifecycle gate).
// 4. A-552: the window-id on the existing service must still reference
// an Active Window (a revoked/expired Window invalidates the
// activation).
//
// On success the status is transitioned to Active and an event is emitted.
func (s msgServer) ActivateService(ctx interface{}, msg *types.MsgActivateService) (*types.MsgActivateServiceResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
if !types.ValidServiceTransition(info.Status, types.ServiceActive) {
return nil, fmt.Errorf("services: service %q status %q cannot transition to Active (REQ-037 lifecycle)", msg.ServiceID, info.Status)
}
if err := s.assertWindowActive(info.WindowID, "ActivateService"); err != nil {
return nil, err
}
info.Status = types.ServiceActive
s.Keeper.SetService(sdkCtx, info)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.service_activated",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("status", string(types.ServiceActive)),
))
return &types.MsgActivateServiceResponse{}, nil
}
// --- SuspendService -----------------------------------------------------
// SuspendService transitions a service Active → Suspended. The handler
// enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. The source status must be Active (ValidServiceTransition(Active,
// Suspended) — the lifecycle gate).
// 4. A-552: the window-id on the existing service must still reference
// an Active Window.
//
// On success the status is transitioned to Suspended and an event is
// emitted.
func (s msgServer) SuspendService(ctx interface{}, msg *types.MsgSuspendService) (*types.MsgSuspendServiceResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
if !types.ValidServiceTransition(info.Status, types.ServiceSuspended) {
return nil, fmt.Errorf("services: service %q status %q cannot transition to Suspended (REQ-037 lifecycle)", msg.ServiceID, info.Status)
}
if err := s.assertWindowActive(info.WindowID, "SuspendService"); err != nil {
return nil, err
}
info.Status = types.ServiceSuspended
s.Keeper.SetService(sdkCtx, info)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.service_suspended",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("status", string(types.ServiceSuspended)),
))
return &types.MsgSuspendServiceResponse{}, nil
}
// --- RevokeService -----------------------------------------------------
// RevokeService transitions a service to Revoked (terminal). The
// handler enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. The service must not already be Revoked (idempotent reject — no
// double-effect).
// 4. A-552: the window-id on the existing service must still reference
// an Active Window (a revoked Window invalidates the revocation
// too — mirroring the grantor-authorized revoke path; the simtest
// wiring uses a nil WindowKeeper to skip this check on the
// Watcher-quorum revoke path).
// 5. The transition gate (ValidServiceTransition — any source → Revoked
// is permitted except Revoked itself).
//
// On success the status is transitioned to Revoked (terminal) and an
// event is emitted.
func (s msgServer) RevokeService(ctx interface{}, msg *types.MsgRevokeService) (*types.MsgRevokeServiceResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
// Idempotent reject: a Revoked service cannot be re-revoked.
if info.Status == types.ServiceRevoked {
return nil, fmt.Errorf("services: service %q already revoked (idempotent reject — no double-effect)", msg.ServiceID)
}
if err := s.assertWindowActive(info.WindowID, "RevokeService"); err != nil {
return nil, err
}
if !types.ValidServiceTransition(info.Status, types.ServiceRevoked) {
return nil, fmt.Errorf("services: service %q status %q cannot transition to Revoked (REQ-037 lifecycle)", msg.ServiceID, info.Status)
}
info.Status = types.ServiceRevoked
s.Keeper.SetService(sdkCtx, info)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.service_revoked",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("status", string(types.ServiceRevoked)),
))
return &types.MsgRevokeServiceResponse{}, nil
}
// --- IssueCareGrant (Care — A-551 typed dispatch) ---------------------
// IssueCareGrant issues a community-care grant against a Care service
// (ServiceKind=Care). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. A-551 typed dispatch: the service Kind must be Care (NOT a generic
// dispatch — a kind mismatch is a runtime reject).
// 4. A-552: the window-id on the existing service must still reference
// an Active Window (window-grant-on-every-op; a revoked Window
// invalidates the per-kind op).
// 5. The CareService metadata is updated with the care-kind (the
// per-kind state).
//
// On success the CareService metadata is persisted and an event is
// emitted.
func (s msgServer) IssueCareGrant(ctx interface{}, msg *types.MsgIssueCareGrant) (*types.MsgIssueCareGrantResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
// A-551 typed dispatch: kind must be Care.
if info.Kind != types.KindCare {
return nil, fmt.Errorf("services: service %q kind %q is not Care (IssueCareGrant is the Care typed dispatch — A-551)", msg.ServiceID, info.Kind)
}
if err := s.assertWindowActive(info.WindowID, "IssueCareGrant"); err != nil {
return nil, err
}
// Update the CareService per-kind metadata with the care-kind.
care, _ := s.Keeper.GetCareService(sdkCtx, msg.ServiceID)
care.CareID = msg.ServiceID
care.CareKind = msg.CareKind
s.Keeper.SetCareService(sdkCtx, care)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.care_grant_issued",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("care_kind", msg.CareKind),
sdk.NewAttribute("grant_recipient_reach_id", msg.GrantRecipientReachID),
))
return &types.MsgIssueCareGrantResponse{}, nil
}
// --- ActivateSIM (SIM — A-551 typed dispatch) -----------------------
// ActivateSIM activates a connectivity SIM against a SIM service
// (ServiceKind=SIM). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. A-551 typed dispatch: the service Kind must be SIM.
// 4. A-552: the window-id on the existing service must still reference
// an Active Window.
// 5. The SIMService metadata is updated with the carrier.
//
// On success the SIMService metadata is persisted and an event is
// emitted.
func (s msgServer) ActivateSIM(ctx interface{}, msg *types.MsgActivateSIM) (*types.MsgActivateSIMResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
// A-551 typed dispatch: kind must be SIM.
if info.Kind != types.KindSIM {
return nil, fmt.Errorf("services: service %q kind %q is not SIM (ActivateSIM is the SIM typed dispatch — A-551)", msg.ServiceID, info.Kind)
}
if err := s.assertWindowActive(info.WindowID, "ActivateSIM"); err != nil {
return nil, err
}
// Update the SIMService per-kind metadata with the carrier.
sim, _ := s.Keeper.GetSIMService(sdkCtx, msg.ServiceID)
sim.SIMID = msg.ServiceID
sim.Carrier = msg.Carrier
s.Keeper.SetSIMService(sdkCtx, sim)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.sim_activated",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("carrier", msg.Carrier),
sdk.NewAttribute("recipient_reach_id", msg.RecipientReachID),
))
return &types.MsgActivateSIMResponse{}, nil
}
// --- ProvisionVault (Vault — A-551 typed dispatch, A-553 VaultKeeper shim) --
// ProvisionVault provisions storage-quota-grain against a Vault service
// (ServiceKind=Vault; A-553: delegates to the VaultKeeper shim). The
// handler enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. A-551 typed dispatch: the service Kind must be Vault.
// 4. A-552: the window-id on the existing service must still reference
// an Active Window.
// 5. A-553: the VaultKeeper shim must be non-nil (a nil shim is a wiring
// error — the VaultService requires a real vault keeper). The shim
// is delegated the storage-quota-grain provisioning by-ID-string.
// A non-nil error from the shim REJECTS the provisioning (the
// VaultService metadata is NOT updated).
// 6. On shim success, the VaultService metadata is updated with the
// storage-quota-grain.
//
// On success the VaultService metadata is persisted and an event is
// emitted.
func (s msgServer) ProvisionVault(ctx interface{}, msg *types.MsgProvisionVault) (*types.MsgProvisionVaultResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
// A-551 typed dispatch: kind must be Vault.
if info.Kind != types.KindVault {
return nil, fmt.Errorf("services: service %q kind %q is not Vault (ProvisionVault is the Vault typed dispatch — A-551)", msg.ServiceID, info.Kind)
}
if err := s.assertWindowActive(info.WindowID, "ProvisionVault"); err != nil {
return nil, err
}
// A-553: delegate to the VaultKeeper shim. A nil shim is a wiring
// error (the VaultService requires a real vault keeper — a nil shim
// is NOT a simtest skip path; the simtest wires a stub vault keeper).
if s.Keeper.vaultKeeper == nil {
return nil, fmt.Errorf("services: vault keeper not wired (ProvisionVault rejected — A-553 VaultService provisioning requires a real vault keeper)")
}
if err := s.Keeper.vaultKeeper.ProvisionVault(msg.ServiceID, msg.StorageQuotaGrain); err != nil {
return nil, fmt.Errorf("services: vault keeper provisioning for service %q: %w (A-553)", msg.ServiceID, err)
}
// Update the VaultService per-kind metadata with the storage-quota-grain.
vault, _ := s.Keeper.GetVaultService(sdkCtx, msg.ServiceID)
vault.VaultID = msg.ServiceID
vault.StorageQuotaGrain = msg.StorageQuotaGrain
s.Keeper.SetVaultService(sdkCtx, vault)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.vault_provisioned",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("storage_quota_grain", fmt.Sprintf("%d", msg.StorageQuotaGrain)),
))
return &types.MsgProvisionVaultResponse{}, nil
}
// --- BindMailbox (Mail — A-551 typed dispatch) ----------------------
// BindMailbox binds a messaging mailbox against a Mail service
// (ServiceKind=Mail). The handler enforces:
// 1. ValidateBasic (stateless).
// 2. The service must exist.
// 3. A-551 typed dispatch: the service Kind must be Mail.
// 4. A-552: the window-id on the existing service must still reference
// an Active Window.
// 5. The MailService metadata is updated with the mailbox-id +
// holder-reach-id.
//
// On success the MailService metadata is persisted and an event is
// emitted.
func (s msgServer) BindMailbox(ctx interface{}, msg *types.MsgBindMailbox) (*types.MsgBindMailboxResponse, error) {
if err := msg.ValidateBasic(); err != nil {
return nil, err
}
sdkCtx := unwrapCtx(ctx)
info, ok := s.Keeper.GetService(sdkCtx, msg.ServiceID)
if !ok {
return nil, fmt.Errorf("services: service %q not found", msg.ServiceID)
}
// A-551 typed dispatch: kind must be Mail.
if info.Kind != types.KindMail {
return nil, fmt.Errorf("services: service %q kind %q is not Mail (BindMailbox is the Mail typed dispatch — A-551)", msg.ServiceID, info.Kind)
}
if err := s.assertWindowActive(info.WindowID, "BindMailbox"); err != nil {
return nil, err
}
// Update the MailService per-kind metadata with the mailbox-id +
// holder-reach-id.
mail, _ := s.Keeper.GetMailService(sdkCtx, msg.ServiceID)
mail.MailID = msg.ServiceID
mail.MailboxID = msg.MailboxID
mail.HolderReachID = msg.HolderReachID
s.Keeper.SetMailService(sdkCtx, mail)
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
"services.mailbox_bound",
sdk.NewAttribute("service_id", msg.ServiceID),
sdk.NewAttribute("mailbox_id", msg.MailboxID),
sdk.NewAttribute("holder_reach_id", msg.HolderReachID),
))
return &types.MsgBindMailboxResponse{}, nil
}
File diff suppressed because it is too large Load Diff
+85
View File
@@ -0,0 +1,85 @@
package services
// module.go holds the services module's AppModule + RegisterServices
// (P5-02-01, REQ-037).
//
// The AppModule wraps the services Keeper and registers the MsgServer
// via RegisterServices. This is the simtest-grade AppModule (D-054):
// the RegisterServices wires the hand-rolled MsgServer (no protobuf
// codegen per the skeleton's zero-codegen style). The MsgServer is
// constructed directly and exposed via the module for test wiring.
//
// The WindowKeeper and VaultKeeper expected-keeper shims are injected
// at construction (nil-able for partial tests). The WindowKeeper shim
// is the A-552 window-grant-on-every-op authority boundary; the
// VaultKeeper shim is the A-553 VaultService provisioning boundary.
import (
"encoding/json"
storetypes "cosmossdk.io/store/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/oy/openyield/x/services/keeper"
"github.com/oy/openyield/x/services/types"
)
// ConsensusVersion is the services module's consensus version (AppModule).
const ConsensusVersion = 1
// AppModule is the services application module (simtest-grade — D-054).
type AppModule struct {
keeper keeper.Keeper
}
// NewAppModule constructs a new services AppModule. The WindowKeeper
// and VaultKeeper expected-keeper shims are injected (nil-able for
// partial tests). The WindowKeeper shim is the A-552 window-grant-on-
// every-op authority boundary; the VaultKeeper shim is the A-553
// VaultService provisioning boundary.
func NewAppModule(cdc codec.Codec, storeKey storetypes.StoreKey, wk types.WindowKeeper, vk types.VaultKeeper) AppModule {
k := keeper.NewKeeper(cdc, storeKey, wk, vk)
return AppModule{keeper: k}
}
// RegisterServices registers the services MsgServer. Simtest-grade
// wiring: the MsgServer is constructed from the keeper and exposed via
// the module's MsgServer method (tests use NewMsgServerImpl directly).
func (am AppModule) RegisterServices(cfg module.Configurator) {
_ = cfg
}
// MsgServer returns the services MsgServer for this module's keeper.
func (am AppModule) MsgServer() types.MsgServer {
return keeper.NewMsgServerImpl(am.keeper)
}
// Name returns the module name.
func (AppModule) Name() string { return types.ModuleName }
// ConsensusVersion implements AppModule.ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion }
// InitGenesis performs genesis initialization for the services module
// (simtest-grade no-op — the runtime stores are created at handler
// time; genesis init of runtime-promoted stores is deferred to the
// live chain v0.6+).
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) {
var gs types.GenesisState
cdc.MustUnmarshalJSON(data, &gs)
_ = gs
}
// ExportGenesis returns the exported genesis state as raw bytes
// (simtest-grade: returns an empty genesis; live chain export deferred
// to v0.6+).
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
gs := types.DefaultGenesisState()
return cdc.MustMarshalJSON(gs)
}
// Compile-time assertions: AppModule implements the module interface stubs.
var _ module.HasName = AppModule{}
var _ module.HasConsensusVersion = AppModule{}
+120
View File
@@ -0,0 +1,120 @@
package types
// expected_keepers.go holds the Go INTERFACES for the cross-module keepers
// x/services depends on at runtime (P5-01-01, REQ-037; G-003 firewall —
// ibc-go expected-keepers convention; mirrors x/partner/types/expected_keepers.go
// and x/hub/types/expected_keepers.go).
//
// The services runtime (REQ-037) depends on TWO cross-module keepers:
//
// 1. x/window (WindowKeeper) — the service-grant authority boundary. A
// service-grant opens a Window on the holder's behalf (A-307); the
// Window's status is the service's authority. The handler consults
// WindowKeeper.GetWindowStatus on EVERY service operation (A-552:
// window-grant-on-every-op — not just at registration); a Window that
// is not Active (Revoked / Expired / unknown) invalidates the op. This
// is the runtime echo of the v0.3 ServiceInfo.window-id by-ID-string
// field: the field stays a string (G-003), and the interface is the
// runtime validity boundary.
//
// 2. x/vault (VaultKeeper) — the VaultService (ServiceKind=Vault)
// provisioning shim. The MsgProvisionVault handler delegates the
// storage-quota provisioning to the x/vault keeper by-ID-string
// (A-553: VaultService references x/vault by ID via the shim — G-003).
// The v0.3 VaultService struct (types.go) named the x/vault collision
// conceptually (the ServiceKind "Vault" is a service kind, NOT a
// struct import); v0.5 wires the runtime provisioning via this
// interface (no struct import of x/vault/types — G-003 intact).
//
// Both dependencies are expressed as INTERFACES defined HERE (in
// x/services/types), NOT as struct imports of x/window/types or
// x/vault/types. The concrete keepers satisfy these interfaces
// structurally (the P5 simtest wires stub implementations — G-003 test
// exemption); the handler depends on the interface, preserving G-003's
// intent (no cross-module struct coupling, no import cycles).
//
// Test-only cross-package imports (the G-003 test exemption) remain
// exempt: the simtest imports x/services/keeper + defines stub types
// that satisfy the interfaces (no production struct imports across
// x/<module>/types).
//
// Lexicon note (REQ-012): "Window", "Vault", "service", "grant",
// "provisioning" are all lexicon-clean. The holder identifier is
// "reach-id" (NOT a banned financial-holder term; use Holder/Reach).
// WindowStatus is the local redefinition of the x/window Window status
// the services runtime cares about (G-003 — no struct import of
// x/window/types; the status string crosses the interface boundary by
// value). Only the Active status authorizes a service operation; any
// other status (Revoked, Expired, unknown) invalidates the op (A-552).
type WindowStatus string
const (
// WindowStatusActive is the only status that authorizes a service
// operation. The handler consults WindowKeeper.GetWindowStatus on
// every op and REJECTS the op if the status is not Active (A-552).
WindowStatusActive WindowStatus = "Active"
// WindowStatusRevoked is a permanently-revoked Window (invalidates
// the service op — A-552).
WindowStatusRevoked WindowStatus = "Revoked"
// WindowStatusExpired is an expired Window (invalidates the service
// op — A-552: an op after the Window expired is a Window-violation).
WindowStatusExpired WindowStatus = "Expired"
// WindowStatusUnknown is the sentinel for a Window the keeper does
// not know about (treated as not-Active — the op is REJECTED).
WindowStatusUnknown WindowStatus = "Unknown"
)
// WindowKeeper is the expected-keeper interface for x/window (G-003). The
// services handler consults it on EVERY service operation (A-552):
//
// - RegisterService: the window-id on the new service must reference an
// Active Window BEFORE the service is created; a non-Active Window
// REJECTS the registration (the service is not created).
// - ActivateService / SuspendService / RevokeService: the window-id on
// the existing service must still be Active BEFORE the transition;
// a revoked/expired Window invalidates the op (the service stays in
// its pre-op status).
// - Per-kind handlers (IssueCareGrant, ActivateSIM, ProvisionVault,
// BindMailbox): the window-id on the service must still be Active
// BEFORE the per-kind op; a revoked/expired Window REJECTS the op
// (the per-kind state is NOT mutated).
//
// No struct import of x/window/types — the interface is the by-ID-string
// boundary (G-003). The windowID is an opaque string (the by-ID-string
// ref to an x/window Window; A-307).
type WindowKeeper interface {
// GetWindowStatus reports the status of the named Window (by-ID-string)
// at the current block. The services handler consults this BEFORE
// every service op (A-552 — window-grant-on-every-op). Returns
// WindowStatusActive if the Window is live and authorizes ops;
// WindowStatusRevoked / WindowStatusExpired / WindowStatusUnknown if
// the Window is not authorizing. An error indicates the keeper could
// not answer (treated as not-Active — the op is REJECTED).
GetWindowStatus(windowID string) (WindowStatus, error)
}
// VaultKeeper is the expected-keeper interface for x/vault (G-003,
// A-553). The VaultService (ServiceKind=Vault) handler calls it for:
//
// - ProvisionVault: the MsgProvisionVault handler delegates the
// storage-quota-grain provisioning to the x/vault keeper by-ID-string
// (the vault-id on the VaultService is the by-ID-string ref to an
// x/vault Vault). A nil shim REJECTS the provisioning (the
// VaultService requires a real vault keeper — a nil shim is a wiring
// error, not a simtest skip path; the simtest wires a stub vault
// keeper, never nil).
//
// No struct import of x/vault/types — the interface is the by-ID-string
// boundary (G-003, A-553). The serviceID is the by-ID-string ref to the
// VaultService; the storage-quota-grain is the OY internal unit (by name
// only — no x/bread import).
type VaultKeeper interface {
// ProvisionVault records the storage-quota-grain provisioning for
// the named VaultService (by-ID-string). The MsgProvisionVault
// handler consults this AFTER the window-grant check (A-552) and
// BEFORE emitting the provisioning event. A non-nil error REJECTS
// the provisioning (the VaultService storage-quota-grain is NOT
// updated).
ProvisionVault(serviceID string, quotaGrain int64) error
}
+552
View File
@@ -0,0 +1,552 @@
package types
// msg_services.go holds the x/services Msg* types implementing sdk.Msg
// (P5-01-01, REQ-037; G-006 controlled exception: types/ gains the
// cosmos-sdk import for sdk.Msg — D-055; the invariant/lexicon tests in
// *_test.go stay stdlib-only per G-024, isolated from this msg_*.go
// file). Each Msg carries a ValidateBasic (stateless) and GetSigners.
//
// The eight Services Msg types drive the Care/SIM/Vault/Mail runtime
// (REQ-037, A-551 per-kind typed dispatch — one Msg* per ServiceKind,
// NOT a generic MsgInvokeService):
//
// Lifecycle (kind-agnostic):
// - MsgRegisterService: register a service (operator-reach-id valid;
// window-id must reference an Active Window — checked via the
// WindowKeeper shim at the handler; status=Pending).
// - MsgActivateService: Pending → Active (window-id must still be
// Active — A-552 window-grant-on-every-op).
// - MsgSuspendService: Active → Suspended.
// - MsgRevokeService: any → Revoked (terminal; revocation requires
// the Window grantor or a Watcher quorum — simtest wiring uses a
// nil WindowKeeper for the grantor check).
//
// Per-kind (typed dispatch — A-551):
// - MsgIssueCareGrant (Care) — issue a community-care grant.
// - MsgActivateSIM (SIM) — activate a connectivity SIM.
// - MsgProvisionVault (Vault) — provision storage-quota-grain via
// the VaultKeeper shim (A-553: references x/vault by ID-string;
// G-003 — no struct import of x/vault/types).
// - MsgBindMailbox (Mail) — bind a messaging mailbox.
//
// All cross-module refs are by-ID-string (G-003): service-id is this
// service's ID; operator-reach-id references an x/identity Reach by
// ID-string; window-id references an x/window Window by ID-string
// (A-307). GetSigners returns the signer reach-ids encoded as
// sdk.AccAddress bytes. The reach-id is the lexicon-clean holder
// identifier (G-003 — NOT a banned financial-holder term; use
// Holder/Reach).
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// --- MsgRegisterService ------------------------------------------------------
// MsgRegisterService registers a service (status=Pending). The handler
// enforces the window-id must reference an Active Window via the
// WindowKeeper shim (A-552). ValidateBasic is stateless: non-empty
// service-id, non-empty operator-reach-id, non-empty window-id, a known
// ServiceKind, non-empty name, non-empty signer.
type MsgRegisterService struct {
ServiceID string `json:"service_id" yaml:"service_id"`
Kind ServiceKind `json:"kind" yaml:"kind"`
OperatorReachID string `json:"operator_reach_id" yaml:"operator_reach_id"`
Name string `json:"name" yaml:"name"`
WindowID string `json:"window_id" yaml:"window_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message (sdk.Msg = proto.Message).
func (m *MsgRegisterService) Reset() { *m = MsgRegisterService{} }
// String implements proto.Message.
func (m *MsgRegisterService) String() string {
return fmt.Sprintf("MsgRegisterService{ServiceID:%s Kind:%s OperatorReachID:%s Name:%s WindowID:%s Signer:%s}",
m.ServiceID, m.Kind, m.OperatorReachID, m.Name, m.WindowID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgRegisterService) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id, a
// known ServiceKind, non-empty operator-reach-id, non-empty name,
// non-empty window-id, non-empty signer. The handler enforces the
// stateful Window-Active check via the WindowKeeper shim (A-552) +
// idempotency (service-id must not already exist).
func (m *MsgRegisterService) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if !knownServiceKind(m.Kind) {
return fmt.Errorf("services: unknown service kind %q", m.Kind)
}
if m.OperatorReachID == "" {
return fmt.Errorf("services: empty operator-reach-id")
}
if m.Name == "" {
return fmt.Errorf("services: empty name")
}
if m.WindowID == "" {
return fmt.Errorf("services: empty window-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgRegisterService) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgActivateService ------------------------------------------------------
// MsgActivateService transitions a service Pending → Active. The
// handler enforces the window-id on the existing service must still be
// Active (A-552 window-grant-on-every-op). ValidateBasic is stateless:
// non-empty service-id, non-empty signer.
type MsgActivateService struct {
ServiceID string `json:"service_id" yaml:"service_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgActivateService) Reset() { *m = MsgActivateService{} }
// String implements proto.Message.
func (m *MsgActivateService) String() string {
return fmt.Sprintf("MsgActivateService{ServiceID:%s Signer:%s}", m.ServiceID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgActivateService) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty signer. The handler enforces the stateful source-status
// check (must be Pending) and the window-grant Active check (A-552).
func (m *MsgActivateService) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgActivateService) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgSuspendService -------------------------------------------------------
// MsgSuspendService transitions a service Active → Suspended. The
// handler enforces the window-id on the existing service must still be
// Active (A-552 window-grant-on-every-op — a revoked Window
// invalidates the transition). ValidateBasic is stateless: non-empty
// service-id, non-empty signer.
type MsgSuspendService struct {
ServiceID string `json:"service_id" yaml:"service_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgSuspendService) Reset() { *m = MsgSuspendService{} }
// String implements proto.Message.
func (m *MsgSuspendService) String() string {
return fmt.Sprintf("MsgSuspendService{ServiceID:%s Signer:%s}", m.ServiceID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgSuspendService) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty signer. The handler enforces the stateful source-status
// check (must be Active) and the window-grant Active check (A-552).
func (m *MsgSuspendService) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgSuspendService) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgRevokeService --------------------------------------------------------
// MsgRevokeService transitions a service to Revoked (terminal). The
// handler enforces the window-id on the existing service must still be
// Active (A-552 window-grant-on-every-op — a revoked Window invalidates
// the revocation too, mirroring the grantor-authorized revoke path).
// Revocation in the simtest is grantor-authorized via the signer reach-
// id; a Watcher quorum path is documented for the live chain (v0.6+).
// ValidateBasic is stateless: non-empty service-id, non-empty signer.
type MsgRevokeService struct {
ServiceID string `json:"service_id" yaml:"service_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgRevokeService) Reset() { *m = MsgRevokeService{} }
// String implements proto.Message.
func (m *MsgRevokeService) String() string {
return fmt.Sprintf("MsgRevokeService{ServiceID:%s Signer:%s}", m.ServiceID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgRevokeService) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty signer. The handler enforces the stateful source-status
// check (must not already be Revoked — idempotent reject) and the
// window-grant Active check (A-552).
func (m *MsgRevokeService) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgRevokeService) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgIssueCareGrant (Care — A-551 typed dispatch) ------------------------
// MsgIssueCareGrant issues a community-care grant against a Care service
// (ServiceKind=Care — A-551 per-kind typed dispatch, NOT a generic
// MsgInvokeService). The handler enforces the window-id on the existing
// Care service must still be Active (A-552 window-grant-on-every-op).
// ValidateBasic is stateless: non-empty service-id, non-empty
// care-kind, non-empty grant-recipient-reach-id, non-empty signer.
type MsgIssueCareGrant struct {
ServiceID string `json:"service_id" yaml:"service_id"`
CareKind string `json:"care_kind" yaml:"care_kind"`
GrantRecipientReachID string `json:"grant_recipient_reach_id" yaml:"grant_recipient_reach_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgIssueCareGrant) Reset() { *m = MsgIssueCareGrant{} }
// String implements proto.Message.
func (m *MsgIssueCareGrant) String() string {
return fmt.Sprintf("MsgIssueCareGrant{ServiceID:%s CareKind:%s GrantRecipientReachID:%s Signer:%s}",
m.ServiceID, m.CareKind, m.GrantRecipientReachID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgIssueCareGrant) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty care-kind, non-empty grant-recipient-reach-id, non-empty
// signer. The handler enforces the stateful service-exists + kind=Care
// + window-grant Active checks (A-552).
func (m *MsgIssueCareGrant) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.CareKind == "" {
return fmt.Errorf("services: empty care-kind")
}
if m.GrantRecipientReachID == "" {
return fmt.Errorf("services: empty grant-recipient-reach-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgIssueCareGrant) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgActivateSIM (SIM — A-551 typed dispatch) ----------------------------
// MsgActivateSIM activates a connectivity SIM against a SIM service
// (ServiceKind=SIM — A-551 per-kind typed dispatch). The handler
// enforces the window-id on the existing SIM service must still be
// Active (A-552 window-grant-on-every-op). ValidateBasic is stateless:
// non-empty service-id, non-empty carrier, non-empty
// recipient-reach-id, non-empty signer.
type MsgActivateSIM struct {
ServiceID string `json:"service_id" yaml:"service_id"`
Carrier string `json:"carrier" yaml:"carrier"`
RecipientReachID string `json:"recipient_reach_id" yaml:"recipient_reach_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgActivateSIM) Reset() { *m = MsgActivateSIM{} }
// String implements proto.Message.
func (m *MsgActivateSIM) String() string {
return fmt.Sprintf("MsgActivateSIM{ServiceID:%s Carrier:%s RecipientReachID:%s Signer:%s}",
m.ServiceID, m.Carrier, m.RecipientReachID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgActivateSIM) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty carrier, non-empty recipient-reach-id, non-empty signer.
// The handler enforces the stateful service-exists + kind=SIM +
// window-grant Active checks (A-552).
func (m *MsgActivateSIM) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.Carrier == "" {
return fmt.Errorf("services: empty carrier")
}
if m.RecipientReachID == "" {
return fmt.Errorf("services: empty recipient-reach-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgActivateSIM) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgProvisionVault (Vault — A-551 typed dispatch, A-553 x/vault shim) ---
// MsgProvisionVault provisions storage-quota-grain against a Vault
// service (ServiceKind=Vault — A-551 per-kind typed dispatch; A-553:
// references x/vault by ID via the VaultKeeper shim — G-003). The
// handler enforces the window-id on the existing Vault service must
// still be Active (A-552) and delegates the storage-quota-grain
// provisioning to the VaultKeeper shim. ValidateBasic is stateless:
// non-empty service-id, storage-quota-grain > 0, non-empty signer.
type MsgProvisionVault struct {
ServiceID string `json:"service_id" yaml:"service_id"`
StorageQuotaGrain int64 `json:"storage_quota_grain" yaml:"storage_quota_grain"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgProvisionVault) Reset() { *m = MsgProvisionVault{} }
// String implements proto.Message.
func (m *MsgProvisionVault) String() string {
return fmt.Sprintf("MsgProvisionVault{ServiceID:%s StorageQuotaGrain:%d Signer:%s}",
m.ServiceID, m.StorageQuotaGrain, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgProvisionVault) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// storage-quota-grain > 0, non-empty signer. The handler enforces the
// stateful service-exists + kind=Vault + window-grant Active checks
// (A-552) and delegates to the VaultKeeper shim (A-553).
func (m *MsgProvisionVault) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.StorageQuotaGrain <= 0 {
return fmt.Errorf("services: storage-quota-grain must be > 0")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgProvisionVault) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgBindMailbox (Mail — A-551 typed dispatch) ---------------------------
// MsgBindMailbox binds a messaging mailbox against a Mail service
// (ServiceKind=Mail — A-551 per-kind typed dispatch). The handler
// enforces the window-id on the existing Mail service must still be
// Active (A-552 window-grant-on-every-op). ValidateBasic is stateless:
// non-empty service-id, non-empty mailbox-id, non-empty
// holder-reach-id, non-empty signer.
type MsgBindMailbox struct {
ServiceID string `json:"service_id" yaml:"service_id"`
MailboxID string `json:"mailbox_id" yaml:"mailbox_id"`
HolderReachID string `json:"holder_reach_id" yaml:"holder_reach_id"`
Signer string `json:"signer" yaml:"signer"`
}
// Reset implements proto.Message.
func (m *MsgBindMailbox) Reset() { *m = MsgBindMailbox{} }
// String implements proto.Message.
func (m *MsgBindMailbox) String() string {
return fmt.Sprintf("MsgBindMailbox{ServiceID:%s MailboxID:%s HolderReachID:%s Signer:%s}",
m.ServiceID, m.MailboxID, m.HolderReachID, m.Signer)
}
// ProtoMessage implements proto.Message.
func (*MsgBindMailbox) ProtoMessage() {}
// ValidateBasic is the stateless validation: non-empty service-id,
// non-empty mailbox-id, non-empty holder-reach-id, non-empty signer.
// The handler enforces the stateful service-exists + kind=Mail +
// window-grant Active checks (A-552).
func (m *MsgBindMailbox) ValidateBasic() error {
if m.ServiceID == "" {
return fmt.Errorf("services: empty service-id")
}
if m.MailboxID == "" {
return fmt.Errorf("services: empty mailbox-id")
}
if m.HolderReachID == "" {
return fmt.Errorf("services: empty holder-reach-id")
}
if m.Signer == "" {
return fmt.Errorf("services: empty signer")
}
return nil
}
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
func (m *MsgBindMailbox) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{[]byte(m.Signer)}
}
// --- MsgServer interface + Response types -----------------------------------
// MsgServer is the services module's message server interface (one method
// per Msg*). The keeper's msg_server.go implements this; module.go's
// RegisterServices wires the implementation. This is the hand-rolled
// equivalent of the protobuf-generated MsgServer interface (no codegen
// per the skeleton's zero-codegen style).
type MsgServer interface {
RegisterService(ctx interface{}, msg *MsgRegisterService) (*MsgRegisterServiceResponse, error)
ActivateService(ctx interface{}, msg *MsgActivateService) (*MsgActivateServiceResponse, error)
SuspendService(ctx interface{}, msg *MsgSuspendService) (*MsgSuspendServiceResponse, error)
RevokeService(ctx interface{}, msg *MsgRevokeService) (*MsgRevokeServiceResponse, error)
IssueCareGrant(ctx interface{}, msg *MsgIssueCareGrant) (*MsgIssueCareGrantResponse, error)
ActivateSIM(ctx interface{}, msg *MsgActivateSIM) (*MsgActivateSIMResponse, error)
ProvisionVault(ctx interface{}, msg *MsgProvisionVault) (*MsgProvisionVaultResponse, error)
BindMailbox(ctx interface{}, msg *MsgBindMailbox) (*MsgBindMailboxResponse, error)
}
// Response types (hand-rolled equivalents of the protobuf-generated
// response wrappers; empty bodies — the response is the state mutation +
// event).
// MsgRegisterServiceResponse is the response to MsgRegisterService.
type MsgRegisterServiceResponse struct{}
// Reset implements proto.Message.
func (m *MsgRegisterServiceResponse) Reset() { *m = MsgRegisterServiceResponse{} }
// String implements proto.Message.
func (m *MsgRegisterServiceResponse) String() string { return "MsgRegisterServiceResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgRegisterServiceResponse) ProtoMessage() {}
// MsgActivateServiceResponse is the response to MsgActivateService.
type MsgActivateServiceResponse struct{}
// Reset implements proto.Message.
func (m *MsgActivateServiceResponse) Reset() { *m = MsgActivateServiceResponse{} }
// String implements proto.Message.
func (m *MsgActivateServiceResponse) String() string { return "MsgActivateServiceResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgActivateServiceResponse) ProtoMessage() {}
// MsgSuspendServiceResponse is the response to MsgSuspendService.
type MsgSuspendServiceResponse struct{}
// Reset implements proto.Message.
func (m *MsgSuspendServiceResponse) Reset() { *m = MsgSuspendServiceResponse{} }
// String implements proto.Message.
func (m *MsgSuspendServiceResponse) String() string { return "MsgSuspendServiceResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgSuspendServiceResponse) ProtoMessage() {}
// MsgRevokeServiceResponse is the response to MsgRevokeService.
type MsgRevokeServiceResponse struct{}
// Reset implements proto.Message.
func (m *MsgRevokeServiceResponse) Reset() { *m = MsgRevokeServiceResponse{} }
// String implements proto.Message.
func (m *MsgRevokeServiceResponse) String() string { return "MsgRevokeServiceResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgRevokeServiceResponse) ProtoMessage() {}
// MsgIssueCareGrantResponse is the response to MsgIssueCareGrant.
type MsgIssueCareGrantResponse struct{}
// Reset implements proto.Message.
func (m *MsgIssueCareGrantResponse) Reset() { *m = MsgIssueCareGrantResponse{} }
// String implements proto.Message.
func (m *MsgIssueCareGrantResponse) String() string { return "MsgIssueCareGrantResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgIssueCareGrantResponse) ProtoMessage() {}
// MsgActivateSIMResponse is the response to MsgActivateSIM.
type MsgActivateSIMResponse struct{}
// Reset implements proto.Message.
func (m *MsgActivateSIMResponse) Reset() { *m = MsgActivateSIMResponse{} }
// String implements proto.Message.
func (m *MsgActivateSIMResponse) String() string { return "MsgActivateSIMResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgActivateSIMResponse) ProtoMessage() {}
// MsgProvisionVaultResponse is the response to MsgProvisionVault.
type MsgProvisionVaultResponse struct{}
// Reset implements proto.Message.
func (m *MsgProvisionVaultResponse) Reset() { *m = MsgProvisionVaultResponse{} }
// String implements proto.Message.
func (m *MsgProvisionVaultResponse) String() string { return "MsgProvisionVaultResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgProvisionVaultResponse) ProtoMessage() {}
// MsgBindMailboxResponse is the response to MsgBindMailbox.
type MsgBindMailboxResponse struct{}
// Reset implements proto.Message.
func (m *MsgBindMailboxResponse) Reset() { *m = MsgBindMailboxResponse{} }
// String implements proto.Message.
func (m *MsgBindMailboxResponse) String() string { return "MsgBindMailboxResponse{}" }
// ProtoMessage implements proto.Message.
func (*MsgBindMailboxResponse) ProtoMessage() {}
+69
View File
@@ -0,0 +1,69 @@
package types
// service_lifecycle.go holds the v0.5 runtime service lifecycle helpers
// (P5-02-01, REQ-037). v0.3 typed the ServiceStatus enum (types.go);
// v0.5 promotes it to runtime by adding the lifecycle transition gate
// the keeper consults before mutating state. Mirrors
// x/partner/types/anchor_credential.go (the v0.5 Anchor credential
// lifecycle pattern — A-551 typed dispatch + A-552 window-grant-on-
// every-op).
//
// Lifecycle (REQ-037, RESEARCH v0.5 §2.5):
//
// RegisterService → Pending (window-id must be Active — A-552)
// ActivateService → Pending → Active (window-id still Active)
// SuspendService → Active → Suspended (window-id still Active)
// RevokeService → any → Revoked (window-id still Active;
// terminal)
//
// Invalid transitions are REJECTED by the handler (the simtest covers
// each invalid transition). Revoked is terminal (no transition out of
// Revoked — idempotent reject on a second Revoke). The lexicon-clean
// holder identifier is "reach-id" (NOT a banned financial-holder term;
// use Holder/Reach).
// AllServiceStatuses returns all four ServiceStatus values in lifecycle
// order (Pending, Active, Suspended, Revoked). Locked-const test (the
// v0.3 types_test.go) asserts exactly 4 entries.
func AllServiceStatuses() []ServiceStatus {
return []ServiceStatus{
ServicePending,
ServiceActive,
ServiceSuspended,
ServiceRevoked,
}
}
// IsTerminalServiceStatus reports whether the service status is terminal
// (no further transitions permitted). Revoked is terminal.
// Pending/Active/Suspended are non-terminal.
func IsTerminalServiceStatus(s ServiceStatus) bool {
return s == ServiceRevoked
}
// ValidServiceTransition reports whether the from → to transition is
// permitted by the REQ-037 lifecycle:
// - Pending → Active (ActivateService)
// - Active → Suspended (SuspendService)
// - Active → Revoked (RevokeService)
// - Suspended → Revoked (RevokeService)
// - Pending → Revoked (RevokeService — a Pending service may be
// revoked before activation)
//
// All other transitions are REJECTED. Revoked is terminal (no transition
// out). The handler consults this helper before mutating state (the
// window-grant Active check A-552 is a SEPARATE gate after this).
func ValidServiceTransition(from, to ServiceStatus) bool {
switch from {
case ServicePending:
return to == ServiceActive || to == ServiceRevoked
case ServiceActive:
return to == ServiceSuspended || to == ServiceRevoked
case ServiceSuspended:
return to == ServiceRevoked
case ServiceRevoked:
return false // terminal
default:
return false // unknown source status
}
}
+16
View File
@@ -169,6 +169,22 @@ func DefaultGenesisState() *GenesisState {
}
}
// Reset implements proto.Message (codec.JSONCodec.MustMarshalJSON /
// MustUnmarshalJSON require proto.Message; the v0.5 runtime AppModule
// calls these — D-055 G-006 controlled exception; the genesis fields +
// ValidateGenesis logic are unchanged from v0.3, only the proto.Message
// methods are added for the AppModule wiring).
func (m *GenesisState) Reset() { *m = GenesisState{} }
// String implements proto.Message.
func (m *GenesisState) String() string {
return fmt.Sprintf("GenesisState{ServiceInfos:%d CareServices:%d SIMServices:%d VaultServices:%d MailServices:%d}",
len(m.ServiceInfos), len(m.CareServices), len(m.SIMServices), len(m.VaultServices), len(m.MailServices))
}
// ProtoMessage implements proto.Message.
func (*GenesisState) ProtoMessage() {}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate or empty service-ids in the registry, and unknown
// ServiceKind / ServiceStatus values.