a70d6faa59
---ci--- project: oy phase: 5 milestone: v0.5 status: complete requirements: covered: [REQ-037] partial: [] ---/ci---
513 lines
20 KiB
Go
513 lines
20 KiB
Go
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
|
|
}
|