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 }