package http import ( "context" "encoding/hex" "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/rwadurian/mpc-system/pkg/logger" "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc" "github.com/rwadurian/mpc-system/services/account/application/ports" "github.com/rwadurian/mpc-system/services/account/application/use_cases" "github.com/rwadurian/mpc-system/services/account/domain/value_objects" "go.uber.org/zap" ) // AccountHTTPHandler handles HTTP requests for accounts type AccountHTTPHandler struct { createAccountUC *use_cases.CreateAccountUseCase getAccountUC *use_cases.GetAccountUseCase updateAccountUC *use_cases.UpdateAccountUseCase listAccountsUC *use_cases.ListAccountsUseCase getAccountSharesUC *use_cases.GetAccountSharesUseCase deactivateShareUC *use_cases.DeactivateShareUseCase loginUC *use_cases.LoginUseCase refreshTokenUC *use_cases.RefreshTokenUseCase generateChallengeUC *use_cases.GenerateChallengeUseCase initiateRecoveryUC *use_cases.InitiateRecoveryUseCase completeRecoveryUC *use_cases.CompleteRecoveryUseCase getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase cancelRecoveryUC *use_cases.CancelRecoveryUseCase sessionCoordinatorClient *grpc.SessionCoordinatorClient } // NewAccountHTTPHandler creates a new AccountHTTPHandler func NewAccountHTTPHandler( createAccountUC *use_cases.CreateAccountUseCase, getAccountUC *use_cases.GetAccountUseCase, updateAccountUC *use_cases.UpdateAccountUseCase, listAccountsUC *use_cases.ListAccountsUseCase, getAccountSharesUC *use_cases.GetAccountSharesUseCase, deactivateShareUC *use_cases.DeactivateShareUseCase, loginUC *use_cases.LoginUseCase, refreshTokenUC *use_cases.RefreshTokenUseCase, generateChallengeUC *use_cases.GenerateChallengeUseCase, initiateRecoveryUC *use_cases.InitiateRecoveryUseCase, completeRecoveryUC *use_cases.CompleteRecoveryUseCase, getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase, cancelRecoveryUC *use_cases.CancelRecoveryUseCase, sessionCoordinatorClient *grpc.SessionCoordinatorClient, ) *AccountHTTPHandler { return &AccountHTTPHandler{ createAccountUC: createAccountUC, getAccountUC: getAccountUC, updateAccountUC: updateAccountUC, listAccountsUC: listAccountsUC, getAccountSharesUC: getAccountSharesUC, deactivateShareUC: deactivateShareUC, loginUC: loginUC, refreshTokenUC: refreshTokenUC, generateChallengeUC: generateChallengeUC, initiateRecoveryUC: initiateRecoveryUC, completeRecoveryUC: completeRecoveryUC, getRecoveryStatusUC: getRecoveryStatusUC, cancelRecoveryUC: cancelRecoveryUC, sessionCoordinatorClient: sessionCoordinatorClient, } } // RegisterRoutes registers HTTP routes func (h *AccountHTTPHandler) RegisterRoutes(router *gin.RouterGroup) { accounts := router.Group("/accounts") { accounts.POST("", h.CreateAccount) accounts.POST("/from-keygen", h.CreateAccountFromKeygen) accounts.GET("", h.ListAccounts) accounts.GET("/:id", h.GetAccount) accounts.PUT("/:id", h.UpdateAccount) accounts.GET("/:id/shares", h.GetAccountShares) accounts.DELETE("/:id/shares/:shareId", h.DeactivateShare) } auth := router.Group("/auth") { auth.POST("/challenge", h.GenerateChallenge) auth.POST("/login", h.Login) auth.POST("/refresh", h.RefreshToken) } recovery := router.Group("/recovery") { recovery.POST("", h.InitiateRecovery) recovery.GET("/:id", h.GetRecoveryStatus) recovery.POST("/:id/complete", h.CompleteRecovery) recovery.POST("/:id/cancel", h.CancelRecovery) } // MPC session management mpc := router.Group("/mpc") { mpc.POST("/keygen", h.CreateKeygenSession) mpc.POST("/sign", h.CreateSigningSession) mpc.GET("/sessions/:id", h.GetSessionStatus) } } // CreateAccountRequest represents the request for creating an account type CreateAccountRequest struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"omitempty,email"` Phone *string `json:"phone"` PublicKey string `json:"publicKey" binding:"required"` KeygenSessionID string `json:"keygenSessionId" binding:"required"` ThresholdN int `json:"thresholdN" binding:"required,min=1"` ThresholdT int `json:"thresholdT" binding:"required,min=1"` Shares []ShareInput `json:"shares" binding:"required,min=1"` } // ShareInput represents a share in the request type ShareInput struct { ShareType string `json:"shareType" binding:"required"` PartyID string `json:"partyId" binding:"required"` PartyIndex int `json:"partyIndex" binding:"required,min=0"` DeviceType *string `json:"deviceType"` DeviceID *string `json:"deviceId"` } // CreateAccount handles account creation func (h *AccountHTTPHandler) CreateAccount(c *gin.Context) { var req CreateAccountRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } keygenSessionID, err := uuid.Parse(req.KeygenSessionID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen session ID"}) return } shares := make([]ports.ShareInput, len(req.Shares)) for i, s := range req.Shares { shares[i] = ports.ShareInput{ ShareType: value_objects.ShareType(s.ShareType), PartyID: s.PartyID, PartyIndex: s.PartyIndex, DeviceType: s.DeviceType, DeviceID: s.DeviceID, } } // Decode hex-encoded public key publicKeyBytes, err := hex.DecodeString(req.PublicKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"}) return } output, err := h.createAccountUC.Execute(c.Request.Context(), ports.CreateAccountInput{ Username: req.Username, Email: req.Email, Phone: req.Phone, PublicKey: publicKeyBytes, KeygenSessionID: keygenSessionID, ThresholdN: req.ThresholdN, ThresholdT: req.ThresholdT, Shares: shares, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "account": output.Account, "shares": output.Shares, }) } // GetAccount handles getting account by ID func (h *AccountHTTPHandler) GetAccount(c *gin.Context) { idStr := c.Param("id") accountID, err := value_objects.AccountIDFromString(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } output, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{ AccountID: &accountID, }) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "account": output.Account, "shares": output.Shares, }) } // UpdateAccountRequest represents the request for updating an account type UpdateAccountRequest struct { Phone *string `json:"phone"` } // UpdateAccount handles account updates func (h *AccountHTTPHandler) UpdateAccount(c *gin.Context) { idStr := c.Param("id") accountID, err := value_objects.AccountIDFromString(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } var req UpdateAccountRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } output, err := h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{ AccountID: accountID, Phone: req.Phone, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, output.Account) } // ListAccounts handles listing accounts func (h *AccountHTTPHandler) ListAccounts(c *gin.Context) { var offset, limit int if o := c.Query("offset"); o != "" { // Parse offset } if l := c.Query("limit"); l != "" { // Parse limit } output, err := h.listAccountsUC.Execute(c.Request.Context(), use_cases.ListAccountsInput{ Offset: offset, Limit: limit, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "accounts": output.Accounts, "total": output.Total, }) } // GetAccountShares handles getting account shares func (h *AccountHTTPHandler) GetAccountShares(c *gin.Context) { idStr := c.Param("id") accountID, err := value_objects.AccountIDFromString(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } output, err := h.getAccountSharesUC.Execute(c.Request.Context(), accountID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "shares": output.Shares, }) } // DeactivateShare handles share deactivation func (h *AccountHTTPHandler) DeactivateShare(c *gin.Context) { idStr := c.Param("id") accountID, err := value_objects.AccountIDFromString(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } shareID := c.Param("shareId") err = h.deactivateShareUC.Execute(c.Request.Context(), ports.DeactivateShareInput{ AccountID: accountID, ShareID: shareID, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "share deactivated"}) } // GenerateChallengeRequest represents the request for generating a challenge type GenerateChallengeRequest struct { Username string `json:"username" binding:"required"` } // GenerateChallenge handles challenge generation func (h *AccountHTTPHandler) GenerateChallenge(c *gin.Context) { var req GenerateChallengeRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } output, err := h.generateChallengeUC.Execute(c.Request.Context(), use_cases.GenerateChallengeInput{ Username: req.Username, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "challengeId": output.ChallengeID, "challenge": hex.EncodeToString(output.Challenge), "expiresAt": output.ExpiresAt, }) } // LoginRequest represents the request for login type LoginRequest struct { Username string `json:"username" binding:"required"` Challenge string `json:"challenge" binding:"required"` Signature string `json:"signature" binding:"required"` } // Login handles user login func (h *AccountHTTPHandler) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Decode hex-encoded challenge and signature // Return 401 Unauthorized for invalid formats (treated as invalid credentials) challengeBytes, err := hex.DecodeString(req.Challenge) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } signatureBytes, err := hex.DecodeString(req.Signature) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } output, err := h.loginUC.Execute(c.Request.Context(), ports.LoginInput{ Username: req.Username, Challenge: challengeBytes, Signature: signatureBytes, }) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "account": output.Account, "accessToken": output.AccessToken, "refreshToken": output.RefreshToken, }) } // RefreshTokenRequest represents the request for refreshing tokens type RefreshTokenRequest struct { RefreshToken string `json:"refreshToken" binding:"required"` } // RefreshToken handles token refresh func (h *AccountHTTPHandler) RefreshToken(c *gin.Context) { var req RefreshTokenRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } output, err := h.refreshTokenUC.Execute(c.Request.Context(), use_cases.RefreshTokenInput{ RefreshToken: req.RefreshToken, }) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "accessToken": output.AccessToken, "refreshToken": output.RefreshToken, }) } // InitiateRecoveryRequest represents the request for initiating recovery type InitiateRecoveryRequest struct { AccountID string `json:"accountId" binding:"required"` RecoveryType string `json:"recoveryType" binding:"required"` OldShareType *string `json:"oldShareType"` } // InitiateRecovery handles recovery initiation func (h *AccountHTTPHandler) InitiateRecovery(c *gin.Context) { var req InitiateRecoveryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } accountID, err := value_objects.AccountIDFromString(req.AccountID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } input := ports.InitiateRecoveryInput{ AccountID: accountID, RecoveryType: value_objects.RecoveryType(req.RecoveryType), } if req.OldShareType != nil { st := value_objects.ShareType(*req.OldShareType) input.OldShareType = &st } output, err := h.initiateRecoveryUC.Execute(c.Request.Context(), input) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "recoverySession": output.RecoverySession, }) } // GetRecoveryStatus handles getting recovery status func (h *AccountHTTPHandler) GetRecoveryStatus(c *gin.Context) { id := c.Param("id") output, err := h.getRecoveryStatusUC.Execute(c.Request.Context(), use_cases.GetRecoveryStatusInput{ RecoverySessionID: id, }) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, output.RecoverySession) } // CompleteRecoveryRequest represents the request for completing recovery type CompleteRecoveryRequest struct { NewPublicKey string `json:"newPublicKey" binding:"required"` NewKeygenSessionID string `json:"newKeygenSessionId" binding:"required"` NewShares []ShareInput `json:"newShares" binding:"required,min=1"` } // CompleteRecovery handles recovery completion func (h *AccountHTTPHandler) CompleteRecovery(c *gin.Context) { id := c.Param("id") var req CompleteRecoveryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } newKeygenSessionID, err := uuid.Parse(req.NewKeygenSessionID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen session ID"}) return } newShares := make([]ports.ShareInput, len(req.NewShares)) for i, s := range req.NewShares { newShares[i] = ports.ShareInput{ ShareType: value_objects.ShareType(s.ShareType), PartyID: s.PartyID, PartyIndex: s.PartyIndex, DeviceType: s.DeviceType, DeviceID: s.DeviceID, } } // Decode hex-encoded public key newPublicKeyBytes, err := hex.DecodeString(req.NewPublicKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"}) return } output, err := h.completeRecoveryUC.Execute(c.Request.Context(), ports.CompleteRecoveryInput{ RecoverySessionID: id, NewPublicKey: newPublicKeyBytes, NewKeygenSessionID: newKeygenSessionID, NewShares: newShares, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, output.Account) } // CancelRecovery handles recovery cancellation func (h *AccountHTTPHandler) CancelRecovery(c *gin.Context) { id := c.Param("id") err := h.cancelRecoveryUC.Execute(c.Request.Context(), use_cases.CancelRecoveryInput{ RecoverySessionID: id, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "recovery cancelled"}) } // ============================================ // MPC Session Management Endpoints // ============================================ // CreateKeygenSessionRequest represents the request for creating a keygen session // Coordinator will automatically select parties from registered pool type CreateKeygenSessionRequest struct { ThresholdN int `json:"threshold_n" binding:"required,min=2"` // Total number of parties (e.g., 3) ThresholdT int `json:"threshold_t" binding:"required,min=1"` // Threshold for signing (e.g., 2) RequireDelegate bool `json:"require_delegate"` // If true, one party will be delegate (returns share to user) } // CreateKeygenSession handles creating a new keygen session // Parties are automatically selected by Coordinator from registered pool func (h *AccountHTTPHandler) CreateKeygenSession(c *gin.Context) { var req CreateKeygenSessionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate threshold if req.ThresholdT > req.ThresholdN { c.JSON(http.StatusBadRequest, gin.H{"error": "threshold_t cannot be greater than threshold_n"}) return } // Call session coordinator via gRPC (no participants - coordinator selects automatically) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() logger.Info("Calling CreateKeygenSession via gRPC (auto party selection)", zap.Int("threshold_n", req.ThresholdN), zap.Int("threshold_t", req.ThresholdT), zap.Bool("require_delegate", req.RequireDelegate)) // Calculate party composition based on require_delegate var persistentCount, delegateCount int if req.RequireDelegate { // One delegate party, rest are persistent delegateCount = 1 persistentCount = req.ThresholdN - 1 } else { // All persistent parties persistentCount = req.ThresholdN delegateCount = 0 } resp, err := h.sessionCoordinatorClient.CreateKeygenSessionAuto( ctx, int32(req.ThresholdN), int32(req.ThresholdT), int32(persistentCount), int32(delegateCount), 600, // 10 minutes expiry ) if err != nil { logger.Error("gRPC CreateKeygenSession failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } logger.Info("gRPC CreateKeygenSession succeeded", zap.String("session_id", resp.SessionID), zap.Int("num_parties", len(resp.SelectedParties))) // Return response with selected parties info c.JSON(http.StatusCreated, gin.H{ "session_id": resp.SessionID, "session_type": "keygen", "threshold_n": req.ThresholdN, "threshold_t": req.ThresholdT, "selected_parties": resp.SelectedParties, "delegate_party": resp.DelegateParty, // The party that will return share to user "status": "created", }) } // CreateSigningSessionRequest represents the request for creating a signing session // Coordinator will automatically select parties based on account's registered shares type CreateSigningSessionRequest struct { AccountID string `json:"account_id" binding:"required"` // Account to sign for MessageHash string `json:"message_hash" binding:"required"` // SHA-256 hash to sign (hex encoded) UserShare string `json:"user_share"` // Optional: user's encrypted share (hex) if delegate party is used } // CreateSigningSession handles creating a new signing session // Parties are automatically selected based on the account's registered shares func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) { var req CreateSigningSessionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate account ID accountID, err := value_objects.AccountIDFromString(req.AccountID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"}) return } // Decode message hash messageHash, err := hex.DecodeString(req.MessageHash) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message_hash format (expected hex)"}) return } if len(messageHash) != 32 { c.JSON(http.StatusBadRequest, gin.H{"error": "message_hash must be 32 bytes (SHA-256)"}) return } // Get account to verify it exists and get share info accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{ AccountID: &accountID, }) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "account not found"}) return } // Get the party IDs from account shares var partyIDs []string for _, share := range accountOutput.Shares { if share.IsActive { partyIDs = append(partyIDs, share.PartyID) } } // Validate we have enough active shares if len(partyIDs) < accountOutput.Account.ThresholdT { c.JSON(http.StatusBadRequest, gin.H{ "error": "insufficient active shares for signing", "required": accountOutput.Account.ThresholdT, "active": len(partyIDs), }) return } // Call session coordinator via gRPC ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() logger.Info("Calling CreateSigningSession via gRPC (auto party selection)", zap.String("account_id", req.AccountID), zap.Int("threshold_t", accountOutput.Account.ThresholdT), zap.Int("available_parties", len(partyIDs))) resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto( ctx, int32(accountOutput.Account.ThresholdT), partyIDs, messageHash, 600, // 10 minutes expiry ) if err != nil { logger.Error("gRPC CreateSigningSession failed", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } logger.Info("gRPC CreateSigningSession succeeded", zap.String("session_id", resp.SessionID), zap.Int("num_parties", len(resp.SelectedParties))) c.JSON(http.StatusCreated, gin.H{ "session_id": resp.SessionID, "session_type": "sign", "account_id": req.AccountID, "message_hash": req.MessageHash, "threshold_t": accountOutput.Account.ThresholdT, "selected_parties": resp.SelectedParties, "status": "created", }) } // GetSessionStatus handles getting session status func (h *AccountHTTPHandler) GetSessionStatus(c *gin.Context) { sessionID := c.Param("id") // Validate session ID format if _, err := uuid.Parse(sessionID); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session ID format"}) return } // Call session coordinator via gRPC ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } response := gin.H{ "session_id": sessionID, "status": resp.Status, "completed_parties": resp.CompletedParties, "total_parties": resp.TotalParties, } if len(resp.PublicKey) > 0 { response["public_key"] = hex.EncodeToString(resp.PublicKey) } if len(resp.Signature) > 0 { response["signature"] = hex.EncodeToString(resp.Signature) } c.JSON(http.StatusOK, response) } // ============================================ // Account Creation from Keygen (Internal API) // ============================================ // CreateAccountFromKeygenRequest represents the request from Session Coordinator // after keygen completion type CreateAccountFromKeygenRequest struct { PublicKey string `json:"public_key" binding:"required"` KeygenSessionID string `json:"keygen_session_id" binding:"required"` ThresholdN int `json:"threshold_n" binding:"required,min=2"` ThresholdT int `json:"threshold_t" binding:"required,min=1"` Shares []ShareInfoFromKeygenInput `json:"shares" binding:"required,min=1"` } // ShareInfoFromKeygenInput represents share info from keygen type ShareInfoFromKeygenInput struct { PartyID string `json:"party_id" binding:"required"` PartyIndex int `json:"party_index"` ShareType string `json:"share_type" binding:"required"` // "persistent" or "delegate" } // CreateAccountFromKeygen handles account creation after keygen completion // This is called by Session Coordinator when all parties complete keygen func (h *AccountHTTPHandler) CreateAccountFromKeygen(c *gin.Context) { var req CreateAccountFromKeygenRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate threshold if req.ThresholdT > req.ThresholdN { c.JSON(http.StatusBadRequest, gin.H{"error": "threshold_t cannot be greater than threshold_n"}) return } // Decode public key publicKey, err := hex.DecodeString(req.PublicKey) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"}) return } // Parse keygen session ID keygenSessionID, err := uuid.Parse(req.KeygenSessionID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen_session_id format"}) return } // Generate a unique username based on keygen session ID // In production, you might want a different naming scheme username := "wallet-" + req.KeygenSessionID[:8] // Convert shares - map share type string to value_objects.ShareType shares := make([]ports.ShareInput, len(req.Shares)) for i, s := range req.Shares { var shareType value_objects.ShareType switch s.ShareType { case "persistent", "server": shareType = value_objects.ShareTypeServer case "delegate", "user_device": shareType = value_objects.ShareTypeUserDevice default: shareType = value_objects.ShareTypeServer } shares[i] = ports.ShareInput{ ShareType: shareType, PartyID: s.PartyID, PartyIndex: s.PartyIndex, } } logger.Info("Creating account from keygen", zap.String("keygen_session_id", req.KeygenSessionID), zap.String("username", username), zap.Int("threshold_n", req.ThresholdN), zap.Int("threshold_t", req.ThresholdT), zap.Int("num_shares", len(shares))) // Create account output, err := h.createAccountUC.Execute(c.Request.Context(), ports.CreateAccountInput{ Username: username, PublicKey: publicKey, KeygenSessionID: keygenSessionID, ThresholdN: req.ThresholdN, ThresholdT: req.ThresholdT, Shares: shares, }) if err != nil { logger.Error("Failed to create account from keygen", zap.String("keygen_session_id", req.KeygenSessionID), zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } logger.Info("Account created from keygen successfully", zap.String("account_id", output.Account.ID.String()), zap.String("username", output.Account.Username), zap.String("keygen_session_id", req.KeygenSessionID)) c.JSON(http.StatusCreated, gin.H{ "account_id": output.Account.ID.String(), "username": output.Account.Username, "public_key": hex.EncodeToString(output.Account.PublicKey), }) }