rwadurian/backend/mpc-system/services/account/adapters/input/http/account_handler.go

860 lines
28 KiB
Go

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),
})
}