feat(co-managed-wallet): 添加签名会话API和Service-Party-App HTTP客户端
## Account Service 新增 API - GET /api/v1/co-managed/sessions/by-invite-code/:inviteCode - 通过邀请码查询keygen会话 - POST /api/v1/co-managed/sign - 创建签名会话 - GET /api/v1/co-managed/sign/by-invite-code/:inviteCode - 通过邀请码查询签名会话 ## Service-Party-App 变更 - 新增 account-client.ts HTTP客户端模块 - 集成Account服务API到Electron主进程 - 添加account相关IPC处理器 - 更新preload.ts暴露account API到渲染进程 - Settings页面添加Account服务URL配置 ## 文档更新 - 更新 docs/service-party-app.md 反映实际实现 - 添加Account Service HTTP API说明 - 添加签名流程文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
81c8db9d50
commit
de29fa4800
|
|
@ -3,6 +3,7 @@ package http
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -19,6 +20,7 @@ import (
|
||||||
// This is a completely independent handler that does not affect existing functionality
|
// This is a completely independent handler that does not affect existing functionality
|
||||||
type CoManagedHTTPHandler struct {
|
type CoManagedHTTPHandler struct {
|
||||||
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
||||||
|
db *sql.DB // Database connection for invite_code lookups
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler
|
// NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler
|
||||||
|
|
@ -27,6 +29,18 @@ func NewCoManagedHTTPHandler(
|
||||||
) *CoManagedHTTPHandler {
|
) *CoManagedHTTPHandler {
|
||||||
return &CoManagedHTTPHandler{
|
return &CoManagedHTTPHandler{
|
||||||
sessionCoordinatorClient: sessionCoordinatorClient,
|
sessionCoordinatorClient: sessionCoordinatorClient,
|
||||||
|
db: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCoManagedHTTPHandlerWithDB creates a new CoManagedHTTPHandler with database support
|
||||||
|
func NewCoManagedHTTPHandlerWithDB(
|
||||||
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
|
||||||
|
db *sql.DB,
|
||||||
|
) *CoManagedHTTPHandler {
|
||||||
|
return &CoManagedHTTPHandler{
|
||||||
|
sessionCoordinatorClient: sessionCoordinatorClient,
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,9 +48,15 @@ func NewCoManagedHTTPHandler(
|
||||||
func (h *CoManagedHTTPHandler) RegisterRoutes(router *gin.RouterGroup) {
|
func (h *CoManagedHTTPHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||||
coManaged := router.Group("/co-managed")
|
coManaged := router.Group("/co-managed")
|
||||||
{
|
{
|
||||||
|
// Keygen session routes
|
||||||
coManaged.POST("/sessions", h.CreateSession)
|
coManaged.POST("/sessions", h.CreateSession)
|
||||||
coManaged.POST("/sessions/:sessionId/join", h.JoinSession)
|
coManaged.POST("/sessions/:sessionId/join", h.JoinSession)
|
||||||
coManaged.GET("/sessions/:sessionId", h.GetSessionStatus)
|
coManaged.GET("/sessions/:sessionId", h.GetSessionStatus)
|
||||||
|
coManaged.GET("/sessions/by-invite-code/:inviteCode", h.GetSessionByInviteCode)
|
||||||
|
|
||||||
|
// Sign session routes (new - does not affect existing functionality)
|
||||||
|
coManaged.POST("/sign", h.CreateSignSession)
|
||||||
|
coManaged.GET("/sign/by-invite-code/:inviteCode", h.GetSignSessionByInviteCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,3 +320,323 @@ func (h *CoManagedHTTPHandler) GetSessionStatus(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Get Session By Invite Code
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// GetSessionByInviteCode handles looking up a session by its invite code
|
||||||
|
// This allows Service-Party-App to find the session_id from an invite_code
|
||||||
|
func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
|
||||||
|
inviteCode := c.Param("inviteCode")
|
||||||
|
if inviteCode == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invite_code is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if database connection is available
|
||||||
|
if h.db == nil {
|
||||||
|
logger.Error("Database connection not available for invite_code lookup")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "service configuration error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query database for session by invite_code
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var sessionID string
|
||||||
|
var walletName string
|
||||||
|
var thresholdN, thresholdT int
|
||||||
|
var status string
|
||||||
|
var expiresAt time.Time
|
||||||
|
|
||||||
|
err := h.db.QueryRowContext(ctx, `
|
||||||
|
SELECT id, COALESCE(wallet_name, ''), threshold_n, threshold_t, status, expires_at
|
||||||
|
FROM mpc_sessions
|
||||||
|
WHERE invite_code = $1 AND session_type = 'co_managed_keygen'
|
||||||
|
`, inviteCode).Scan(&sessionID, &walletName, &thresholdN, &thresholdT, &status, &expiresAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
logger.Info("Session not found for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode))
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "session not found or expired"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("Failed to query session by invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup session"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session is expired
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
logger.Info("Session expired for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.String("session_id", sessionID))
|
||||||
|
c.JSON(http.StatusGone, gin.H{"error": "session has expired"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wildcard join token from session coordinator
|
||||||
|
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to get session status from coordinator",
|
||||||
|
zap.String("session_id", sessionID),
|
||||||
|
zap.Error(err))
|
||||||
|
// Return basic info without join token
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"session_id": sessionID,
|
||||||
|
"wallet_name": walletName,
|
||||||
|
"threshold_n": thresholdN,
|
||||||
|
"threshold_t": thresholdT,
|
||||||
|
"status": status,
|
||||||
|
"expires_at": expiresAt.UnixMilli(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Found session for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.String("session_id", sessionID),
|
||||||
|
zap.String("wallet_name", walletName))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"session_id": sessionID,
|
||||||
|
"wallet_name": walletName,
|
||||||
|
"threshold_n": thresholdN,
|
||||||
|
"threshold_t": thresholdT,
|
||||||
|
"status": statusResp.Status,
|
||||||
|
"completed_parties": statusResp.CompletedParties,
|
||||||
|
"total_parties": statusResp.TotalParties,
|
||||||
|
"expires_at": expiresAt.UnixMilli(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Co-Managed Sign Session (NEW - Independent)
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// SignPartyInfo contains party information for signing
|
||||||
|
type SignPartyInfo struct {
|
||||||
|
PartyID string `json:"party_id" binding:"required"`
|
||||||
|
PartyIndex int32 `json:"party_index" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSignSessionRequest represents the request for creating a co-managed sign session
|
||||||
|
type CreateSignSessionRequest struct {
|
||||||
|
KeygenSessionID string `json:"keygen_session_id" binding:"required"` // The keygen session that created the wallet
|
||||||
|
WalletName string `json:"wallet_name"` // Wallet name (for display)
|
||||||
|
MessageHash string `json:"message_hash" binding:"required"` // Hex-encoded message hash to sign
|
||||||
|
Parties []SignPartyInfo `json:"parties" binding:"required,min=1"` // Parties to participate in signing (t+1)
|
||||||
|
ThresholdT int `json:"threshold_t" binding:"required,min=1"` // Signing threshold
|
||||||
|
InitiatorName string `json:"initiator_name"` // Initiator's display name
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSignSession handles creating a new co-managed sign session
|
||||||
|
// This is a completely new endpoint that does not affect existing sign functionality
|
||||||
|
func (h *CoManagedHTTPHandler) CreateSignSession(c *gin.Context) {
|
||||||
|
var req CreateSignSessionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate keygen_session_id format
|
||||||
|
if _, err := uuid.Parse(req.KeygenSessionID); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen_session_id format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate message_hash (should be hex encoded)
|
||||||
|
messageHash, err := hex.DecodeString(req.MessageHash)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "message_hash must be hex encoded"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate party count >= threshold + 1
|
||||||
|
if len(req.Parties) < req.ThresholdT+1 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": fmt.Sprintf("need at least %d parties for threshold %d", req.ThresholdT+1, req.ThresholdT),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate invite code for sign session
|
||||||
|
inviteCode := generateInviteCode()
|
||||||
|
|
||||||
|
// Convert parties to gRPC format
|
||||||
|
parties := make([]grpcclient.SigningPartyInfo, len(req.Parties))
|
||||||
|
for i, p := range req.Parties {
|
||||||
|
parties[i] = grpcclient.SigningPartyInfo{
|
||||||
|
PartyID: p.PartyID,
|
||||||
|
PartyIndex: p.PartyIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call session coordinator via gRPC
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logger.Info("Creating co-managed sign session",
|
||||||
|
zap.String("keygen_session_id", req.KeygenSessionID),
|
||||||
|
zap.String("wallet_name", req.WalletName),
|
||||||
|
zap.Int("threshold_t", req.ThresholdT),
|
||||||
|
zap.Int("num_parties", len(req.Parties)),
|
||||||
|
zap.String("invite_code", inviteCode))
|
||||||
|
|
||||||
|
// Create signing session
|
||||||
|
// Note: delegateUserShare is nil for co-managed wallets (no delegate party)
|
||||||
|
resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto(
|
||||||
|
ctx,
|
||||||
|
int32(req.ThresholdT),
|
||||||
|
parties,
|
||||||
|
messageHash,
|
||||||
|
86400, // 24 hour expiry
|
||||||
|
nil, // No delegate share for co-managed wallets
|
||||||
|
req.KeygenSessionID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to create co-managed sign session", zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store invite_code mapping in database (for lookup)
|
||||||
|
if h.db != nil {
|
||||||
|
_, dbErr := h.db.ExecContext(ctx, `
|
||||||
|
UPDATE mpc_sessions
|
||||||
|
SET invite_code = $1, wallet_name = $2
|
||||||
|
WHERE id = $3
|
||||||
|
`, inviteCode, req.WalletName, resp.SessionID)
|
||||||
|
if dbErr != nil {
|
||||||
|
logger.Warn("Failed to store invite_code for sign session",
|
||||||
|
zap.String("session_id", resp.SessionID),
|
||||||
|
zap.Error(dbErr))
|
||||||
|
// Don't fail the request, just log the warning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wildcard join token for participants
|
||||||
|
wildcardToken := ""
|
||||||
|
if token, ok := resp.JoinTokens["*"]; ok {
|
||||||
|
wildcardToken = token
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Co-managed sign session created successfully",
|
||||||
|
zap.String("session_id", resp.SessionID),
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.Int("num_parties", len(resp.SelectedParties)))
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"session_id": resp.SessionID,
|
||||||
|
"keygen_session_id": req.KeygenSessionID,
|
||||||
|
"wallet_name": req.WalletName,
|
||||||
|
"invite_code": inviteCode,
|
||||||
|
"join_token": wildcardToken,
|
||||||
|
"threshold_t": req.ThresholdT,
|
||||||
|
"selected_parties": resp.SelectedParties,
|
||||||
|
"status": "waiting_for_participants",
|
||||||
|
"current_participants": 0,
|
||||||
|
"required_participants": len(req.Parties),
|
||||||
|
"expires_at": resp.ExpiresAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignSessionByInviteCode handles looking up a sign session by its invite code
|
||||||
|
// This is a completely new endpoint that does not affect existing functionality
|
||||||
|
func (h *CoManagedHTTPHandler) GetSignSessionByInviteCode(c *gin.Context) {
|
||||||
|
inviteCode := c.Param("inviteCode")
|
||||||
|
if inviteCode == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invite_code is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if database connection is available
|
||||||
|
if h.db == nil {
|
||||||
|
logger.Error("Database connection not available for sign invite_code lookup")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "service configuration error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query database for sign session by invite_code
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var sessionID string
|
||||||
|
var walletName string
|
||||||
|
var keygenSessionID string
|
||||||
|
var thresholdN, thresholdT int
|
||||||
|
var status string
|
||||||
|
var expiresAt time.Time
|
||||||
|
|
||||||
|
err := h.db.QueryRowContext(ctx, `
|
||||||
|
SELECT id, COALESCE(wallet_name, ''), COALESCE(keygen_session_id::text, ''),
|
||||||
|
threshold_n, threshold_t, status, expires_at
|
||||||
|
FROM mpc_sessions
|
||||||
|
WHERE invite_code = $1 AND session_type = 'sign'
|
||||||
|
`, inviteCode).Scan(&sessionID, &walletName, &keygenSessionID, &thresholdN, &thresholdT, &status, &expiresAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
logger.Info("Sign session not found for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode))
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "sign session not found or expired"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Error("Failed to query sign session by invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.Error(err))
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to lookup sign session"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session is expired
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
logger.Info("Sign session expired for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.String("session_id", sessionID))
|
||||||
|
c.JSON(http.StatusGone, gin.H{"error": "sign session has expired"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session status from coordinator
|
||||||
|
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to get sign session status from coordinator",
|
||||||
|
zap.String("session_id", sessionID),
|
||||||
|
zap.Error(err))
|
||||||
|
// Return basic info without detailed status
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"session_id": sessionID,
|
||||||
|
"keygen_session_id": keygenSessionID,
|
||||||
|
"wallet_name": walletName,
|
||||||
|
"threshold_n": thresholdN,
|
||||||
|
"threshold_t": thresholdT,
|
||||||
|
"status": status,
|
||||||
|
"expires_at": expiresAt.UnixMilli(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Found sign session for invite_code",
|
||||||
|
zap.String("invite_code", inviteCode),
|
||||||
|
zap.String("session_id", sessionID),
|
||||||
|
zap.String("wallet_name", walletName))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"session_id": sessionID,
|
||||||
|
"keygen_session_id": keygenSessionID,
|
||||||
|
"wallet_name": walletName,
|
||||||
|
"threshold_n": thresholdN,
|
||||||
|
"threshold_t": thresholdT,
|
||||||
|
"status": statusResp.Status,
|
||||||
|
"completed_parties": statusResp.CompletedParties,
|
||||||
|
"total_parties": statusResp.TotalParties,
|
||||||
|
"expires_at": expiresAt.UnixMilli(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ func main() {
|
||||||
getRecoveryStatusUC,
|
getRecoveryStatusUC,
|
||||||
cancelRecoveryUC,
|
cancelRecoveryUC,
|
||||||
sessionCoordinatorClient,
|
sessionCoordinatorClient,
|
||||||
|
db,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
errChan <- fmt.Errorf("HTTP server error: %w", err)
|
errChan <- fmt.Errorf("HTTP server error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -239,6 +240,7 @@ func startHTTPServer(
|
||||||
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase,
|
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase,
|
||||||
cancelRecoveryUC *use_cases.CancelRecoveryUseCase,
|
cancelRecoveryUC *use_cases.CancelRecoveryUseCase,
|
||||||
sessionCoordinatorClient *grpcadapter.SessionCoordinatorClient,
|
sessionCoordinatorClient *grpcadapter.SessionCoordinatorClient,
|
||||||
|
db *sql.DB,
|
||||||
) error {
|
) error {
|
||||||
// Set Gin mode
|
// Set Gin mode
|
||||||
if cfg.Server.Environment == "production" {
|
if cfg.Server.Environment == "production" {
|
||||||
|
|
@ -301,7 +303,8 @@ func startHTTPServer(
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create co-managed wallet handler (independent from existing functionality)
|
// Create co-managed wallet handler (independent from existing functionality)
|
||||||
coManagedHandler := httphandler.NewCoManagedHTTPHandler(sessionCoordinatorClient)
|
// Uses database connection for invite_code lookups
|
||||||
|
coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db)
|
||||||
|
|
||||||
// Configure authentication middleware
|
// Configure authentication middleware
|
||||||
// Skip paths that don't require authentication
|
// Skip paths that don't require authentication
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { GrpcClient } from './modules/grpc-client';
|
||||||
import { DatabaseManager } from './modules/database';
|
import { DatabaseManager } from './modules/database';
|
||||||
import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation';
|
import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation';
|
||||||
import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service';
|
import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service';
|
||||||
|
import { AccountClient } from './modules/account-client';
|
||||||
|
|
||||||
// 内置 HTTP 服务器端口
|
// 内置 HTTP 服务器端口
|
||||||
const HTTP_PORT = 3456;
|
const HTTP_PORT = 3456;
|
||||||
|
|
@ -15,6 +16,7 @@ let grpcClient: GrpcClient | null = null;
|
||||||
let database: DatabaseManager | null = null;
|
let database: DatabaseManager | null = null;
|
||||||
let httpServer: ReturnType<typeof express.application.listen> | null = null;
|
let httpServer: ReturnType<typeof express.application.listen> | null = null;
|
||||||
let kavaTxService: KavaTxService | null = null;
|
let kavaTxService: KavaTxService | null = null;
|
||||||
|
let accountClient: AccountClient | null = null;
|
||||||
|
|
||||||
// 创建主窗口
|
// 创建主窗口
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
|
@ -92,6 +94,12 @@ async function initServices() {
|
||||||
// 初始化 Kava 交易服务
|
// 初始化 Kava 交易服务
|
||||||
kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG);
|
kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG);
|
||||||
|
|
||||||
|
// 初始化 Account 服务 HTTP 客户端
|
||||||
|
// 从数据库读取 Account 服务 URL,默认使用生产环境地址
|
||||||
|
const settings = database.getAllSettings();
|
||||||
|
const accountServiceUrl = settings['account_service_url'] || 'https://api.szaiai.com';
|
||||||
|
accountClient = new AccountClient(accountServiceUrl);
|
||||||
|
|
||||||
// 设置 IPC 处理器
|
// 设置 IPC 处理器
|
||||||
setupIpcHandlers();
|
setupIpcHandlers();
|
||||||
}
|
}
|
||||||
|
|
@ -132,22 +140,70 @@ function setupIpcHandlers() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 创建会话
|
// gRPC - 创建会话 (通过 Account 服务 HTTP API)
|
||||||
ipcMain.handle('grpc:createSession', async (_event, _params) => {
|
ipcMain.handle('grpc:createSession', async (_event, params) => {
|
||||||
// TODO: 实现创建会话逻辑
|
try {
|
||||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
const result = await accountClient?.createKeygenSession({
|
||||||
|
wallet_name: params.walletName,
|
||||||
|
threshold_t: params.thresholdT,
|
||||||
|
threshold_n: params.thresholdN,
|
||||||
|
persistent_count: 0, // 服务端 party 数量,共管钱包模式下为 0
|
||||||
|
external_count: params.thresholdN, // 所有参与方都是外部 party
|
||||||
|
expires_in_seconds: 86400, // 24 小时
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
inviteCode: result?.invite_code,
|
||||||
|
walletName: result?.wallet_name,
|
||||||
|
expiresAt: result?.expires_at,
|
||||||
|
joinTokens: result?.join_tokens,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 验证邀请码
|
// gRPC - 验证邀请码 (通过 Account 服务 HTTP API)
|
||||||
ipcMain.handle('grpc:validateInviteCode', async (_event, { code: _code }) => {
|
ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => {
|
||||||
// TODO: 实现验证邀请码逻辑
|
try {
|
||||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
const result = await accountClient?.getSessionByInviteCode(code);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
sessionInfo: {
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
walletName: result?.wallet_name,
|
||||||
|
threshold: {
|
||||||
|
t: result?.threshold_t,
|
||||||
|
n: result?.threshold_n,
|
||||||
|
},
|
||||||
|
status: result?.status,
|
||||||
|
currentParticipants: result?.joined_parties || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 获取会话状态
|
// gRPC - 获取会话状态 (通过 Account 服务 HTTP API)
|
||||||
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId: _sessionId }) => {
|
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => {
|
||||||
// TODO: 实现获取会话状态逻辑
|
try {
|
||||||
return { success: false, error: '功能尚未实现' };
|
const result = await accountClient?.getSessionStatus(sessionId);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
session: {
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
status: result?.status,
|
||||||
|
completedParties: result?.completed_parties,
|
||||||
|
totalParties: result?.total_parties,
|
||||||
|
sessionType: result?.session_type,
|
||||||
|
publicKey: result?.public_key,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 测试连接
|
// gRPC - 测试连接
|
||||||
|
|
@ -161,16 +217,152 @@ function setupIpcHandlers() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 验证签名会话
|
// gRPC - 验证签名会话 (通过 Account 服务 HTTP API)
|
||||||
ipcMain.handle('grpc:validateSigningSession', async (_event, { code: _code }) => {
|
ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => {
|
||||||
// TODO: 实现验证签名会话逻辑
|
try {
|
||||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
const result = await accountClient?.getSignSessionByInviteCode(code);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
session: {
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
keygenSessionId: result?.keygen_session_id,
|
||||||
|
walletName: result?.wallet_name,
|
||||||
|
messageHash: result?.message_hash,
|
||||||
|
threshold: {
|
||||||
|
t: result?.threshold_t,
|
||||||
|
n: result?.parties?.length || 0,
|
||||||
|
},
|
||||||
|
currentParticipants: result?.joined_count || 0,
|
||||||
|
status: result?.status,
|
||||||
|
parties: result?.parties,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 加入签名会话
|
// gRPC - 加入签名会话
|
||||||
ipcMain.handle('grpc:joinSigningSession', async (_event, _params) => {
|
ipcMain.handle('grpc:joinSigningSession', async (_event, params) => {
|
||||||
// TODO: 实现加入签名会话逻辑
|
try {
|
||||||
return { success: false, error: '功能尚未实现' };
|
// 从本地 SQLite 获取 share 数据
|
||||||
|
const share = database?.getShare(params.shareId, params.password);
|
||||||
|
if (!share) {
|
||||||
|
return { success: false, error: 'Share not found or incorrect password' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加入签名会话 (通过 gRPC)
|
||||||
|
// TODO: 实际加入会话逻辑需要使用 gRPC client
|
||||||
|
// 这里先返回成功,表示验证通过
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
partyId: share.party_id,
|
||||||
|
partyIndex: share.party_index,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Account 服务相关 (HTTP API)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
// 创建签名会话
|
||||||
|
ipcMain.handle('account:createSignSession', async (_event, params) => {
|
||||||
|
try {
|
||||||
|
// 从本地 SQLite 获取 share 数据
|
||||||
|
const share = database?.getShare(params.shareId, params.password);
|
||||||
|
if (!share) {
|
||||||
|
return { success: false, error: 'Share not found or incorrect password' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 participants_json 获取参与方列表
|
||||||
|
const participants = JSON.parse(share.participants_json || '[]');
|
||||||
|
const parties = participants.map((p: { partyId: string }, index: number) => ({
|
||||||
|
party_id: p.partyId,
|
||||||
|
party_index: index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await accountClient?.createSignSession({
|
||||||
|
keygen_session_id: share.session_id,
|
||||||
|
wallet_name: share.wallet_name,
|
||||||
|
message_hash: params.messageHash,
|
||||||
|
parties: parties,
|
||||||
|
threshold_t: share.threshold_t,
|
||||||
|
initiator_name: params.initiatorName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
inviteCode: result?.invite_code,
|
||||||
|
expiresAt: result?.expires_at,
|
||||||
|
joinToken: result?.join_token,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取签名会话状态 (通过邀请码)
|
||||||
|
ipcMain.handle('account:getSignSessionByInviteCode', async (_event, { inviteCode }) => {
|
||||||
|
try {
|
||||||
|
const result = await accountClient?.getSignSessionByInviteCode(inviteCode);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
session: {
|
||||||
|
sessionId: result?.session_id,
|
||||||
|
keygenSessionId: result?.keygen_session_id,
|
||||||
|
walletName: result?.wallet_name,
|
||||||
|
messageHash: result?.message_hash,
|
||||||
|
thresholdT: result?.threshold_t,
|
||||||
|
status: result?.status,
|
||||||
|
inviteCode: result?.invite_code,
|
||||||
|
expiresAt: result?.expires_at,
|
||||||
|
parties: result?.parties,
|
||||||
|
joinedCount: result?.joined_count,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Account 服务健康检查
|
||||||
|
ipcMain.handle('account:healthCheck', async () => {
|
||||||
|
try {
|
||||||
|
const result = await accountClient?.healthCheck();
|
||||||
|
return { success: true, data: result };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试 Account 服务连接
|
||||||
|
ipcMain.handle('account:testConnection', async () => {
|
||||||
|
try {
|
||||||
|
const connected = await accountClient?.testConnection();
|
||||||
|
return { success: connected };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 Account 服务 URL
|
||||||
|
ipcMain.handle('account:updateUrl', async (_event, { url }) => {
|
||||||
|
try {
|
||||||
|
accountClient?.setBaseUrl(url);
|
||||||
|
database?.setSetting('account_service_url', url);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 Account 服务 URL
|
||||||
|
ipcMain.handle('account:getUrl', async () => {
|
||||||
|
return accountClient?.getBaseUrl() || 'https://api.szaiai.com';
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
/**
|
||||||
|
* Account Service HTTP Client
|
||||||
|
*
|
||||||
|
* 用于 Service-Party-App 调用 Account 服务的 HTTP API
|
||||||
|
* 主要用于创建/查询 keygen 和 sign 会话
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 类型定义
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Keygen 会话相关
|
||||||
|
export interface CreateKeygenSessionRequest {
|
||||||
|
wallet_name: string;
|
||||||
|
threshold_t: number;
|
||||||
|
threshold_n: number;
|
||||||
|
persistent_count: number;
|
||||||
|
external_count: number;
|
||||||
|
expires_in_seconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateKeygenSessionResponse {
|
||||||
|
session_id: string;
|
||||||
|
invite_code: string;
|
||||||
|
wallet_name: string;
|
||||||
|
threshold_n: number;
|
||||||
|
threshold_t: number;
|
||||||
|
selected_server_parties: string[];
|
||||||
|
join_tokens: Record<string, string>;
|
||||||
|
expires_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinSessionRequest {
|
||||||
|
party_id: string;
|
||||||
|
join_token: string;
|
||||||
|
device_type?: string;
|
||||||
|
device_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartyInfo {
|
||||||
|
party_id: string;
|
||||||
|
party_index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionInfo {
|
||||||
|
session_id: string;
|
||||||
|
session_type: string;
|
||||||
|
threshold_n: number;
|
||||||
|
threshold_t: number;
|
||||||
|
status: string;
|
||||||
|
wallet_name: string;
|
||||||
|
invite_code: string;
|
||||||
|
keygen_session_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JoinSessionResponse {
|
||||||
|
success: boolean;
|
||||||
|
party_index: number;
|
||||||
|
session_info: SessionInfo;
|
||||||
|
other_parties: PartyInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSessionStatusResponse {
|
||||||
|
session_id: string;
|
||||||
|
status: string;
|
||||||
|
completed_parties: number;
|
||||||
|
total_parties: number;
|
||||||
|
session_type: string;
|
||||||
|
public_key?: string;
|
||||||
|
signature?: string;
|
||||||
|
has_delegate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSessionByInviteCodeResponse {
|
||||||
|
session_id: string;
|
||||||
|
wallet_name: string;
|
||||||
|
threshold_n: number;
|
||||||
|
threshold_t: number;
|
||||||
|
status: string;
|
||||||
|
invite_code: string;
|
||||||
|
expires_at: number;
|
||||||
|
joined_parties: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign 会话相关
|
||||||
|
export interface SignPartyInfo {
|
||||||
|
party_id: string;
|
||||||
|
party_index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSignSessionRequest {
|
||||||
|
keygen_session_id: string;
|
||||||
|
wallet_name: string;
|
||||||
|
message_hash: string;
|
||||||
|
parties: SignPartyInfo[];
|
||||||
|
threshold_t: number;
|
||||||
|
initiator_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSignSessionResponse {
|
||||||
|
session_id: string;
|
||||||
|
invite_code: string;
|
||||||
|
keygen_session_id: string;
|
||||||
|
message_hash: string;
|
||||||
|
threshold_t: number;
|
||||||
|
parties: SignPartyInfo[];
|
||||||
|
expires_at: number;
|
||||||
|
join_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSignSessionByInviteCodeResponse {
|
||||||
|
session_id: string;
|
||||||
|
keygen_session_id: string;
|
||||||
|
wallet_name: string;
|
||||||
|
message_hash: string;
|
||||||
|
threshold_t: number;
|
||||||
|
status: string;
|
||||||
|
invite_code: string;
|
||||||
|
expires_at: number;
|
||||||
|
parties: SignPartyInfo[];
|
||||||
|
joined_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HTTP 客户端类
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export class AccountClient {
|
||||||
|
private baseUrl: string;
|
||||||
|
private timeout: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
* @param baseUrl Account 服务的基础 URL (例如: https://api.szaiai.com 或 http://localhost:8080)
|
||||||
|
* @param timeout 请求超时时间 (毫秒)
|
||||||
|
*/
|
||||||
|
constructor(baseUrl: string, timeout: number = 30000) {
|
||||||
|
// 移除末尾的斜杠
|
||||||
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新基础 URL
|
||||||
|
*/
|
||||||
|
setBaseUrl(baseUrl: string): void {
|
||||||
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前基础 URL
|
||||||
|
*/
|
||||||
|
getBaseUrl(): string {
|
||||||
|
return this.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送 HTTP 请求
|
||||||
|
*/
|
||||||
|
private async request<T>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
|
path: string,
|
||||||
|
body?: unknown
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${path}`;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[AccountClient] ${method} ${url}`, body ? JSON.stringify(body) : '');
|
||||||
|
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
let data: T | ErrorResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid JSON response: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = data as ErrorResponse;
|
||||||
|
throw new Error(errorData.message || errorData.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[AccountClient] Response:`, data);
|
||||||
|
return data as T;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
throw new Error(`Request timeout after ${this.timeout}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Keygen 会话 API
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Keygen 会话
|
||||||
|
*/
|
||||||
|
async createKeygenSession(
|
||||||
|
params: CreateKeygenSessionRequest
|
||||||
|
): Promise<CreateKeygenSessionResponse> {
|
||||||
|
return this.request<CreateKeygenSessionResponse>(
|
||||||
|
'POST',
|
||||||
|
'/api/v1/co-managed/sessions',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加入会话
|
||||||
|
*/
|
||||||
|
async joinSession(
|
||||||
|
sessionId: string,
|
||||||
|
params: JoinSessionRequest
|
||||||
|
): Promise<JoinSessionResponse> {
|
||||||
|
return this.request<JoinSessionResponse>(
|
||||||
|
'POST',
|
||||||
|
`/api/v1/co-managed/sessions/${sessionId}/join`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话状态
|
||||||
|
*/
|
||||||
|
async getSessionStatus(sessionId: string): Promise<GetSessionStatusResponse> {
|
||||||
|
return this.request<GetSessionStatusResponse>(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/co-managed/sessions/${sessionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邀请码查询 Keygen 会话
|
||||||
|
*/
|
||||||
|
async getSessionByInviteCode(inviteCode: string): Promise<GetSessionByInviteCodeResponse> {
|
||||||
|
return this.request<GetSessionByInviteCodeResponse>(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/co-managed/sessions/by-invite-code/${inviteCode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Sign 会话 API
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Sign 会话
|
||||||
|
*/
|
||||||
|
async createSignSession(
|
||||||
|
params: CreateSignSessionRequest
|
||||||
|
): Promise<CreateSignSessionResponse> {
|
||||||
|
return this.request<CreateSignSessionResponse>(
|
||||||
|
'POST',
|
||||||
|
'/api/v1/co-managed/sign',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过邀请码查询 Sign 会话
|
||||||
|
*/
|
||||||
|
async getSignSessionByInviteCode(inviteCode: string): Promise<GetSignSessionByInviteCodeResponse> {
|
||||||
|
return this.request<GetSignSessionByInviteCodeResponse>(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/co-managed/sign/by-invite-code/${inviteCode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 健康检查
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 健康检查
|
||||||
|
*/
|
||||||
|
async healthCheck(): Promise<{ status: string; service: string }> {
|
||||||
|
return this.request<{ status: string; service: string }>(
|
||||||
|
'GET',
|
||||||
|
'/health'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
*/
|
||||||
|
async testConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.healthCheck();
|
||||||
|
return result.status === 'healthy';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 默认实例
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// 默认使用生产环境地址
|
||||||
|
const DEFAULT_ACCOUNT_SERVICE_URL = 'https://api.szaiai.com';
|
||||||
|
|
||||||
|
// 创建默认客户端实例
|
||||||
|
export const accountClient = new AccountClient(DEFAULT_ACCOUNT_SERVICE_URL);
|
||||||
|
|
@ -67,6 +67,36 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Account 服务相关 (HTTP API)
|
||||||
|
// ===========================================================================
|
||||||
|
account: {
|
||||||
|
// 创建签名会话
|
||||||
|
createSignSession: (params: {
|
||||||
|
shareId: string;
|
||||||
|
password: string;
|
||||||
|
messageHash: string;
|
||||||
|
initiatorName?: string;
|
||||||
|
}) => ipcRenderer.invoke('account:createSignSession', params),
|
||||||
|
|
||||||
|
// 获取签名会话状态 (通过邀请码)
|
||||||
|
getSignSessionByInviteCode: (inviteCode: string) =>
|
||||||
|
ipcRenderer.invoke('account:getSignSessionByInviteCode', { inviteCode }),
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
healthCheck: () => ipcRenderer.invoke('account:healthCheck'),
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
testConnection: () => ipcRenderer.invoke('account:testConnection'),
|
||||||
|
|
||||||
|
// 更新服务 URL
|
||||||
|
updateUrl: (url: string) =>
|
||||||
|
ipcRenderer.invoke('account:updateUrl', { url }),
|
||||||
|
|
||||||
|
// 获取当前服务 URL
|
||||||
|
getUrl: () => ipcRenderer.invoke('account:getUrl'),
|
||||||
|
},
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// 存储相关 (SQLite)
|
// 存储相关 (SQLite)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import styles from './Settings.module.css';
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
messageRouterUrl: string;
|
messageRouterUrl: string;
|
||||||
|
accountServiceUrl: string;
|
||||||
autoBackup: boolean;
|
autoBackup: boolean;
|
||||||
backupPath: string;
|
backupPath: string;
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +14,7 @@ export default function Settings() {
|
||||||
|
|
||||||
const [settings, setSettings] = useState<Settings>({
|
const [settings, setSettings] = useState<Settings>({
|
||||||
messageRouterUrl: 'mpc-grpc.szaiai.com:443', // 生产环境默认地址
|
messageRouterUrl: 'mpc-grpc.szaiai.com:443', // 生产环境默认地址
|
||||||
|
accountServiceUrl: 'https://api.szaiai.com', // Account 服务默认地址
|
||||||
autoBackup: false,
|
autoBackup: false,
|
||||||
backupPath: '',
|
backupPath: '',
|
||||||
});
|
});
|
||||||
|
|
@ -27,8 +29,12 @@ export default function Settings() {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.storage.getSettings();
|
const result = await window.electronAPI.storage.getSettings();
|
||||||
|
const accountUrl = await window.electronAPI.account.getUrl();
|
||||||
if (result) {
|
if (result) {
|
||||||
setSettings(result);
|
setSettings({
|
||||||
|
...result,
|
||||||
|
accountServiceUrl: accountUrl || 'https://api.szaiai.com',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load settings:', err);
|
console.error('Failed to load settings:', err);
|
||||||
|
|
@ -43,6 +49,7 @@ export default function Settings() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.storage.saveSettings(settings);
|
await window.electronAPI.storage.saveSettings(settings);
|
||||||
|
await window.electronAPI.account.updateUrl(settings.accountServiceUrl);
|
||||||
setMessage({ type: 'success', text: '设置已保存' });
|
setMessage({ type: 'success', text: '设置已保存' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage({ type: 'error', text: '保存设置失败' });
|
setMessage({ type: 'error', text: '保存设置失败' });
|
||||||
|
|
@ -57,12 +64,29 @@ export default function Settings() {
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.grpc.testConnection(settings.messageRouterUrl);
|
const result = await window.electronAPI.grpc.testConnection(settings.messageRouterUrl);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setMessage({ type: 'success', text: '连接成功' });
|
setMessage({ type: 'success', text: 'Message Router 连接成功' });
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: result.error || '连接失败' });
|
setMessage({ type: 'error', text: result.error || 'Message Router 连接失败' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage({ type: 'error', text: '连接测试失败' });
|
setMessage({ type: 'error', text: 'Message Router 连接测试失败' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestAccountConnection = async () => {
|
||||||
|
setMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先更新 URL
|
||||||
|
await window.electronAPI.account.updateUrl(settings.accountServiceUrl);
|
||||||
|
const result = await window.electronAPI.account.testConnection();
|
||||||
|
if (result.success) {
|
||||||
|
setMessage({ type: 'success', text: 'Account 服务连接成功' });
|
||||||
|
} else {
|
||||||
|
setMessage({ type: 'error', text: result.error || 'Account 服务连接失败' });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setMessage({ type: 'error', text: 'Account 服务连接测试失败' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -120,6 +144,28 @@ export default function Settings() {
|
||||||
输入 Message Router 服务的 gRPC 地址 (生产环境: mpc-grpc.szaiai.com:443)
|
输入 Message Router 服务的 gRPC 地址 (生产环境: mpc-grpc.szaiai.com:443)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Account 服务地址</label>
|
||||||
|
<div className={styles.inputWithButton}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={settings.accountServiceUrl}
|
||||||
|
onChange={(e) => setSettings(prev => ({ ...prev, accountServiceUrl: e.target.value }))}
|
||||||
|
placeholder="https://api.szaiai.com"
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={styles.testButton}
|
||||||
|
onClick={handleTestAccountConnection}
|
||||||
|
>
|
||||||
|
测试连接
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className={styles.hint}>
|
||||||
|
输入 Account 服务的 HTTP 地址 (生产环境: https://api.szaiai.com)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,55 @@ interface JoinSigningSessionParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JoinSigningSessionResult {
|
interface JoinSigningSessionResult {
|
||||||
|
success: boolean;
|
||||||
|
partyId?: string;
|
||||||
|
partyIndex?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account 服务相关类型
|
||||||
|
interface CreateSignSessionParams {
|
||||||
|
shareId: string;
|
||||||
|
password: string;
|
||||||
|
messageHash: string;
|
||||||
|
initiatorName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSignSessionResult {
|
||||||
|
success: boolean;
|
||||||
|
sessionId?: string;
|
||||||
|
inviteCode?: string;
|
||||||
|
expiresAt?: number;
|
||||||
|
joinToken?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignSessionInfo {
|
||||||
|
sessionId: string;
|
||||||
|
keygenSessionId: string;
|
||||||
|
walletName: string;
|
||||||
|
messageHash: string;
|
||||||
|
thresholdT: number;
|
||||||
|
status: string;
|
||||||
|
inviteCode: string;
|
||||||
|
expiresAt: number;
|
||||||
|
parties: Array<{ party_id: string; party_index: number }>;
|
||||||
|
joinedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSignSessionByInviteCodeResult {
|
||||||
|
success: boolean;
|
||||||
|
session?: SignSessionInfo;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountHealthCheckResult {
|
||||||
|
success: boolean;
|
||||||
|
data?: { status: string; service: string };
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountTestConnectionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +395,16 @@ interface KavaHealthCheckResult {
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
interface ElectronAPI {
|
interface ElectronAPI {
|
||||||
|
// Account 服务相关 (HTTP API)
|
||||||
|
account: {
|
||||||
|
createSignSession: (params: CreateSignSessionParams) => Promise<CreateSignSessionResult>;
|
||||||
|
getSignSessionByInviteCode: (inviteCode: string) => Promise<GetSignSessionByInviteCodeResult>;
|
||||||
|
healthCheck: () => Promise<AccountHealthCheckResult>;
|
||||||
|
testConnection: () => Promise<AccountTestConnectionResult>;
|
||||||
|
updateUrl: (url: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
getUrl: () => Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
// gRPC 相关
|
// gRPC 相关
|
||||||
grpc: {
|
grpc: {
|
||||||
createSession: (params: CreateSessionParams) => Promise<CreateSessionResult>;
|
createSession: (params: CreateSessionParams) => Promise<CreateSessionResult>;
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,56 @@
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
Service Party App 是一个**跨平台 Electron 桌面应用**,用于参与分布式多方共管钱包的创建和签名过程。用户可以在各自的电脑上运行此应用,通过扫描邀请二维码加入共管钱包创建会话,参与 TSS (Threshold Signature Scheme) 密钥生成协议。
|
Service Party App 是一个**跨平台 Electron 桌面应用**,用于参与分布式多方共管钱包的创建和签名过程。用户可以在各自的电脑上运行此应用,通过扫描邀请二维码或输入邀请码加入共管钱包创建会话,参与 TSS (Threshold Signature Scheme) 密钥生成协议。
|
||||||
|
|
||||||
## 应用场景
|
## 应用场景
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
│ Admin Web │ │ Service Party │ │ Service Party │
|
│ Service Party │ │ Service Party │ │ Service Party │
|
||||||
│ (管理员电脑) │ │ App (用户A电脑) │ │ App (用户B电脑) │
|
│ App (用户A) │ │ App (用户B) │ │ App (用户C) │
|
||||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||||
│ │ │
|
│ │ │
|
||||||
│ 创建会话/生成邀请码 │ 扫码加入 │ 扫码加入
|
│ 创建会话/生成邀请码 │ 输入邀请码加入 │ 输入邀请码加入
|
||||||
│ │ │
|
│ │ │
|
||||||
└───────────────────────┼───────────────────────┘
|
└───────────────────────┼───────────────────────┘
|
||||||
│
|
│
|
||||||
┌────────────┴────────────┐
|
┌────────────┴────────────┐
|
||||||
|
│ Account Service │
|
||||||
|
│ (HTTP API - 会话管理) │
|
||||||
|
└────────────┬────────────┘
|
||||||
|
│
|
||||||
|
┌────────────┴────────────┐
|
||||||
│ Message Router │
|
│ Message Router │
|
||||||
│ (gRPC 消息路由) │
|
│ (gRPC 消息路由) │
|
||||||
└────────────┬────────────┘
|
└────────────┬────────────┘
|
||||||
│
|
│
|
||||||
┌────────────┴────────────┐
|
┌────────────┴────────────┐
|
||||||
│ TSS Keygen Protocol │
|
│ TSS Keygen/Sign │
|
||||||
│ (分布式密钥生成) │
|
│ (分布式密钥生成/签名) │
|
||||||
└─────────────────────────┘
|
└─────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 典型使用流程
|
### 典型使用流程
|
||||||
|
|
||||||
1. **管理员** 在 Admin Web 创建共管钱包会话,配置阈值 (如 2-of-3)
|
#### Keygen 流程 (密钥生成)
|
||||||
2. **管理员** 生成邀请二维码,分享给参与方
|
|
||||||
3. **参与方** 在各自电脑上打开 Service Party App,扫描二维码加入
|
1. **发起方** 在 Service Party App 输入钱包名称和用户名称,创建共管钱包会话
|
||||||
4. **所有参与方就绪后**,自动开始 TSS 密钥生成协议
|
2. **发起方** 获得邀请码 (格式: XXXX-XXXX-XXXX),分享给其他参与方
|
||||||
5. **完成后**,每个参与方本地保存自己的加密 share,管理员获得钱包公钥
|
3. **参与方** 在各自电脑上打开 Service Party App,输入邀请码加入
|
||||||
|
4. **等待所有参与方就绪** (24小时超时)
|
||||||
|
5. **所有参与方到齐后**,自动开始 TSS 密钥生成协议
|
||||||
|
6. **完成后**,每个参与方在本地 SQLite 保存自己的加密 share
|
||||||
|
|
||||||
|
#### Sign 流程 (签名)
|
||||||
|
|
||||||
|
1. **发起方** 从本地 SQLite 选择要签名的钱包
|
||||||
|
2. **发起方** 输入待签名的消息哈希,创建签名会话
|
||||||
|
3. **发起方** 获得签名邀请码,分享给其他参与方 (必须是 keygen 时的参与方)
|
||||||
|
4. **参与方** 输入邀请码加入签名会话
|
||||||
|
5. **等待足够数量的参与方** (threshold_t + 1 人,24小时超时)
|
||||||
|
6. **参与方到齐后**,自动开始 TSS 签名协议
|
||||||
|
7. **完成后**,返回签名结果,可用于广播交易
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
|
|
@ -44,45 +62,44 @@ backend/mpc-system/services/service-party-app/
|
||||||
│ ├── preload.ts # 预加载脚本 (安全 IPC)
|
│ ├── preload.ts # 预加载脚本 (安全 IPC)
|
||||||
│ └── modules/
|
│ └── modules/
|
||||||
│ ├── grpc-client.ts # gRPC 客户端 (连接 Message Router)
|
│ ├── grpc-client.ts # gRPC 客户端 (连接 Message Router)
|
||||||
|
│ ├── account-client.ts # HTTP 客户端 (连接 Account Service)
|
||||||
│ ├── tss-handler.ts # TSS 协议处理器
|
│ ├── tss-handler.ts # TSS 协议处理器
|
||||||
│ └── storage.ts # 本地加密存储 (AES-256-GCM)
|
│ ├── database.ts # 本地 SQLite 存储 (AES-256-GCM 加密)
|
||||||
|
│ ├── address-derivation.ts # 地址派生 (多链支持)
|
||||||
|
│ ├── kava-client.ts # Kava 区块链客户端
|
||||||
|
│ └── kava-tx-service.ts # Kava 交易构建服务
|
||||||
│
|
│
|
||||||
├── src/ # React 前端 (渲染进程)
|
├── src/ # React 前端 (渲染进程)
|
||||||
│ ├── App.tsx # 应用入口
|
│ ├── App.tsx # 应用入口
|
||||||
│ ├── pages/
|
│ ├── pages/
|
||||||
│ │ ├── Home.tsx # 主页 - Share 列表
|
│ │ ├── Home.tsx # 主页 - Share 列表
|
||||||
│ │ ├── Join.tsx # 加入会话页面
|
│ │ ├── Join.tsx # 加入会话页面
|
||||||
│ │ ├── Create.tsx # 创建会话页面 (可选)
|
│ │ ├── Create.tsx # 创建会话页面
|
||||||
│ │ ├── Session.tsx # 会话进度页面
|
│ │ ├── Session.tsx # 会话进度页面
|
||||||
|
│ │ ├── Sign.tsx # 签名页面
|
||||||
│ │ └── Settings.tsx # 设置页面
|
│ │ └── Settings.tsx # 设置页面
|
||||||
│ ├── components/
|
│ ├── components/ # UI 组件
|
||||||
│ │ ├── ShareCard.tsx # Share 卡片组件
|
│ ├── stores/
|
||||||
│ │ ├── JoinForm.tsx # 加入表单
|
│ │ └── appStore.ts # Zustand 状态管理
|
||||||
│ │ ├── ProgressSteps.tsx # 进度步骤
|
│ ├── types/
|
||||||
│ │ ├── ExportDialog.tsx # 导出对话框
|
│ │ └── electron.d.ts # TypeScript 类型定义
|
||||||
│ │ └── ImportDialog.tsx # 导入对话框
|
│ └── utils/
|
||||||
│ └── hooks/
|
│ └── address.ts # 地址工具函数
|
||||||
│ ├── useGrpc.ts # gRPC 连接 Hook
|
|
||||||
│ ├── useSession.ts # 会话状态 Hook
|
|
||||||
│ └── useStorage.ts # 本地存储 Hook
|
|
||||||
│
|
│
|
||||||
├── tss-party/ # Go TSS 子进程
|
├── tss-party/ # Go TSS 子进程
|
||||||
│ ├── main.go # TSS 协议执行程序
|
│ ├── main.go # TSS 协议执行程序
|
||||||
│ ├── go.mod # Go 模块定义
|
│ ├── go.mod # Go 模块定义
|
||||||
│ └── go.sum # 依赖锁定
|
│ └── go.sum # 依赖锁定
|
||||||
│
|
│
|
||||||
├── bin/ # 编译后的二进制文件
|
├── proto/ # gRPC Proto 文件
|
||||||
│ ├── win32-x64/
|
│ └── message_router.proto # Message Router 接口定义
|
||||||
│ │ └── tss-party.exe
|
|
||||||
│ ├── darwin-x64/
|
|
||||||
│ │ └── tss-party
|
|
||||||
│ └── linux-x64/
|
|
||||||
│ └── tss-party
|
|
||||||
│
|
│
|
||||||
├── package.json # Node.js 依赖
|
├── package.json # Node.js 依赖
|
||||||
├── electron-builder.json # Electron 打包配置
|
├── electron-builder.json # Electron 打包配置
|
||||||
├── tsconfig.json # TypeScript 配置
|
├── tsconfig.json # TypeScript 配置
|
||||||
└── vite.config.ts # Vite 构建配置
|
├── tsconfig.electron.json # Electron TypeScript 配置
|
||||||
|
├── vite.config.ts # Vite 构建配置
|
||||||
|
└── build-windows.bat # Windows 一键编译脚本
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术架构
|
## 技术架构
|
||||||
|
|
@ -95,12 +112,50 @@ backend/mpc-system/services/service-party-app/
|
||||||
| 前端框架 | React 18 + TypeScript | 与 admin-web 保持一致 |
|
| 前端框架 | React 18 + TypeScript | 与 admin-web 保持一致 |
|
||||||
| 构建工具 | Vite | 快速开发和构建 |
|
| 构建工具 | Vite | 快速开发和构建 |
|
||||||
| 状态管理 | Zustand | 轻量级状态管理 |
|
| 状态管理 | Zustand | 轻量级状态管理 |
|
||||||
| gRPC 客户端 | @grpc/grpc-js | Node.js gRPC 实现 |
|
| gRPC 客户端 | @grpc/grpc-js | Node.js gRPC 实现 (连接 Message Router) |
|
||||||
|
| HTTP 客户端 | Fetch API | 连接 Account Service |
|
||||||
| TSS 协议 | Go 子进程 | 使用 bnb-chain/tss-lib |
|
| TSS 协议 | Go 子进程 | 使用 bnb-chain/tss-lib |
|
||||||
| 本地存储 | electron-store | 加密本地存储 |
|
| 本地存储 | sql.js (SQLite) | 纯 JavaScript SQLite 实现 |
|
||||||
| 加密算法 | AES-256-GCM | Share 加密存储 |
|
| 加密算法 | AES-256-GCM | Share 加密存储,PBKDF2 密钥派生 |
|
||||||
| 打包工具 | electron-builder | 多平台打包 |
|
| 打包工具 | electron-builder | 多平台打包 |
|
||||||
|
|
||||||
|
### 服务连接
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Service Party App │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐│
|
||||||
|
│ │ main.ts ││
|
||||||
|
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
|
||||||
|
│ │ │ grpcClient │ │accountClient │ │ database │ ││
|
||||||
|
│ │ │ (gRPC) │ │ (HTTP) │ │ (SQLite) │ ││
|
||||||
|
│ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ ││
|
||||||
|
│ │ │ │ ││
|
||||||
|
│ └─────────┼─────────────────┼──────────────────────────────┘│
|
||||||
|
└────────────┼─────────────────┼───────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ gRPC (TLS) │ HTTPS
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Message Router │ │ Account Service │
|
||||||
|
│ (消息路由) │ │ (会话管理) │
|
||||||
|
└─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Account Service HTTP API
|
||||||
|
|
||||||
|
Service Party App 通过 HTTP 调用 Account Service 管理会话:
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/v1/co-managed/sessions` | POST | 创建 Keygen 会话 |
|
||||||
|
| `/api/v1/co-managed/sessions/:sessionId/join` | POST | 加入会话 |
|
||||||
|
| `/api/v1/co-managed/sessions/:sessionId` | GET | 获取会话状态 |
|
||||||
|
| `/api/v1/co-managed/sessions/by-invite-code/:code` | GET | 通过邀请码查询会话 |
|
||||||
|
| `/api/v1/co-managed/sign` | POST | 创建 Sign 会话 |
|
||||||
|
| `/api/v1/co-managed/sign/by-invite-code/:code` | GET | 通过邀请码查询签名会话 |
|
||||||
|
| `/health` | GET | 健康检查 |
|
||||||
|
|
||||||
### TSS 子进程架构
|
### TSS 子进程架构
|
||||||
|
|
||||||
为什么使用 Go 子进程而不是 WASM?
|
为什么使用 Go 子进程而不是 WASM?
|
||||||
|
|
@ -128,9 +183,10 @@ backend/mpc-system/services/service-party-app/
|
||||||
│ ┌────────────────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
│ │ - 使用 bnb-chain/tss-lib v2 │ │
|
│ │ - 使用 bnb-chain/tss-lib v2 │ │
|
||||||
│ │ - 执行 GG20 Keygen 协议 (4 轮) │ │
|
│ │ - 执行 GG20 Keygen 协议 (4 轮) │ │
|
||||||
|
│ │ - 执行 GG20 Signing 协议 │ │
|
||||||
│ │ - 通过 stdin 接收 MPC 消息 │ │
|
│ │ - 通过 stdin 接收 MPC 消息 │ │
|
||||||
│ │ - 通过 stdout 发送 MPC 消息 │ │
|
│ │ - 通过 stdout 发送 MPC 消息 │ │
|
||||||
│ │ - 完成后返回公钥 + 加密 share │ │
|
│ │ - 完成后返回公钥 + 加密 share / 签名结果 │ │
|
||||||
│ └────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
@ -154,8 +210,9 @@ interface OutgoingMessage {
|
||||||
isBroadcast?: boolean;
|
isBroadcast?: boolean;
|
||||||
toParties?: string[];
|
toParties?: string[];
|
||||||
payload?: string; // base64 encoded
|
payload?: string; // base64 encoded
|
||||||
publicKey?: string; // base64 encoded (result)
|
publicKey?: string; // base64 encoded (keygen result)
|
||||||
encryptedShare?: string; // base64 encoded (result)
|
encryptedShare?: string; // base64 encoded (keygen result)
|
||||||
|
signature?: string; // base64 encoded (sign result)
|
||||||
partyIndex?: number;
|
partyIndex?: number;
|
||||||
round?: number;
|
round?: number;
|
||||||
totalRounds?: number;
|
totalRounds?: number;
|
||||||
|
|
@ -167,13 +224,13 @@ interface OutgoingMessage {
|
||||||
|
|
||||||
### 1. 加入会话
|
### 1. 加入会话
|
||||||
|
|
||||||
用户通过以下方式加入共管钱包创建会话:
|
用户通过以下方式加入共管钱包创建/签名会话:
|
||||||
|
|
||||||
- **扫描二维码**: 使用摄像头扫描 Admin Web 生成的邀请二维码
|
- **输入邀请码**: 手动输入 12 位邀请码 (格式: XXXX-XXXX-XXXX)
|
||||||
- **粘贴邀请链接**: 手动粘贴邀请链接
|
- **扫描二维码**: 使用摄像头扫描邀请二维码 (可选)
|
||||||
- **输入邀请码**: 手动输入 6 位邀请码
|
- **粘贴邀请链接**: 手动粘贴邀请链接 (可选)
|
||||||
|
|
||||||
### 2. TSS 密钥生成
|
### 2. TSS 密钥生成 (Keygen)
|
||||||
|
|
||||||
参与 GG20 (Gennaro-Goldfeder 2020) 门限签名密钥生成协议:
|
参与 GG20 (Gennaro-Goldfeder 2020) 门限签名密钥生成协议:
|
||||||
|
|
||||||
|
|
@ -181,35 +238,58 @@ interface OutgoingMessage {
|
||||||
- 4 轮消息交换
|
- 4 轮消息交换
|
||||||
- 零知识证明保证安全性
|
- 零知识证明保证安全性
|
||||||
- 每个参与方获得自己的 share,无需信任其他方
|
- 每个参与方获得自己的 share,无需信任其他方
|
||||||
|
- 24 小时超时等待所有参与方
|
||||||
|
|
||||||
### 3. 本地加密存储
|
### 3. TSS 签名 (Sign)
|
||||||
|
|
||||||
Share 使用 AES-256-GCM 加密后存储在本地:
|
参与 GG20 门限签名协议:
|
||||||
|
|
||||||
|
- 使用 Keygen 时保存的 share
|
||||||
|
- 仅需 threshold_t + 1 个参与方即可签名
|
||||||
|
- 24 小时超时等待足够参与方
|
||||||
|
- 返回可用于广播的签名结果
|
||||||
|
|
||||||
|
### 4. 本地 SQLite 存储
|
||||||
|
|
||||||
|
Share 使用 AES-256-GCM 加密后存储在本地 SQLite 数据库:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ShareEntry {
|
interface ShareRecord {
|
||||||
id: string; // Share 唯一标识
|
id: string; // Share 唯一标识 (UUID)
|
||||||
sessionId: string; // 会话 ID
|
session_id: string; // Keygen 会话 ID
|
||||||
walletName: string; // 钱包名称
|
wallet_name: string; // 钱包名称
|
||||||
partyId: string; // 参与方 ID
|
party_id: string; // 参与方 ID
|
||||||
partyIndex: number; // 参与方索引
|
party_index: number; // 参与方索引
|
||||||
threshold: {
|
threshold_t: number; // 签名阈值
|
||||||
t: number; // 签名阈值
|
threshold_n: number; // 总参与方数
|
||||||
n: number; // 总参与方数
|
public_key_hex: string; // 钱包公钥 (hex)
|
||||||
};
|
encrypted_share: string; // AES-256-GCM 加密的 share
|
||||||
publicKey: string; // 钱包公钥 (hex)
|
created_at: string; // 创建时间
|
||||||
encryptedShare: string; // AES-256-GCM 加密的 share
|
last_used_at: string | null; // 最后使用时间
|
||||||
createdAt: string; // 创建时间
|
participants_json: string; // JSON: 参与方列表 [{partyId, name}]
|
||||||
metadata: {
|
|
||||||
participants: Array<{
|
|
||||||
partyId: string;
|
|
||||||
name: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 备份与恢复
|
**重要**: `participants_json` 字段存储了 Keygen 时所有参与方的信息,Sign 时必须从这里选择参与方。
|
||||||
|
|
||||||
|
### 5. 地址派生
|
||||||
|
|
||||||
|
支持从公钥派生多链地址:
|
||||||
|
|
||||||
|
- **Kava** (主链)
|
||||||
|
- **Cosmos** 系列链
|
||||||
|
- **Ethereum** 兼容链
|
||||||
|
|
||||||
|
### 6. Kava 交易
|
||||||
|
|
||||||
|
支持构建和广播 Kava 交易:
|
||||||
|
|
||||||
|
- 查询余额
|
||||||
|
- 构建转账交易 (待 TSS 签名)
|
||||||
|
- 广播已签名交易
|
||||||
|
- 查询交易状态
|
||||||
|
|
||||||
|
### 7. 备份与恢复
|
||||||
|
|
||||||
- **导出备份**: 将加密的 share 导出为文件
|
- **导出备份**: 将加密的 share 导出为文件
|
||||||
- **导入恢复**: 从备份文件恢复 share
|
- **导入恢复**: 从备份文件恢复 share
|
||||||
|
|
@ -221,7 +301,14 @@ interface ShareEntry {
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- Go 1.21+
|
- Go 1.21+
|
||||||
- pnpm 或 npm
|
- npm 或 pnpm
|
||||||
|
|
||||||
|
### Windows 一键编译
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend/mpc-system/services/service-party-app
|
||||||
|
build-windows.bat
|
||||||
|
```
|
||||||
|
|
||||||
### 编译 TSS 子进程
|
### 编译 TSS 子进程
|
||||||
|
|
||||||
|
|
@ -230,9 +317,12 @@ interface ShareEntry {
|
||||||
cd backend/mpc-system/services/service-party-app/tss-party
|
cd backend/mpc-system/services/service-party-app/tss-party
|
||||||
go build -o ../bin/win32-x64/tss-party.exe .
|
go build -o ../bin/win32-x64/tss-party.exe .
|
||||||
|
|
||||||
# macOS
|
# macOS (Intel)
|
||||||
GOOS=darwin GOARCH=amd64 go build -o ../bin/darwin-x64/tss-party .
|
GOOS=darwin GOARCH=amd64 go build -o ../bin/darwin-x64/tss-party .
|
||||||
|
|
||||||
|
# macOS (Apple Silicon)
|
||||||
|
GOOS=darwin GOARCH=arm64 go build -o ../bin/darwin-arm64/tss-party .
|
||||||
|
|
||||||
# Linux
|
# Linux
|
||||||
GOOS=linux GOARCH=amd64 go build -o ../bin/linux-x64/tss-party .
|
GOOS=linux GOARCH=amd64 go build -o ../bin/linux-x64/tss-party .
|
||||||
```
|
```
|
||||||
|
|
@ -252,39 +342,51 @@ npm run dev
|
||||||
### 生产构建
|
### 生产构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建前端
|
# 构建前端 + Electron
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# 打包 Electron 应用
|
# 打包 Windows 安装包
|
||||||
npm run package
|
npm run build:win
|
||||||
|
|
||||||
# 输出目录: dist/
|
# 输出目录: release/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### 设置页面配置项
|
||||||
|
|
||||||
|
| 配置项 | 说明 | 默认值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| Message Router 地址 | gRPC 服务地址 | mpc-grpc.szaiai.com:443 |
|
||||||
|
| Account 服务地址 | HTTP API 地址 | https://api.szaiai.com |
|
||||||
|
| 自动备份 | 创建 share 后自动备份 | false |
|
||||||
|
| 备份目录 | 自动备份文件保存位置 | (空) |
|
||||||
|
|
||||||
## 安全考虑
|
## 安全考虑
|
||||||
|
|
||||||
### Share 安全
|
### Share 安全
|
||||||
|
|
||||||
1. **本地加密**: Share 使用 AES-256-GCM 加密存储
|
1. **本地加密**: Share 使用 AES-256-GCM 加密存储
|
||||||
2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生
|
2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生 (100,000 次迭代)
|
||||||
3. **内存保护**: Share 在内存中的时间尽量短
|
3. **内存保护**: Share 在内存中的时间尽量短
|
||||||
4. **安全删除**: 删除 share 时安全擦除
|
4. **安全删除**: 删除 share 时同时删除派生地址和签名历史
|
||||||
|
|
||||||
### 网络安全
|
### 网络安全
|
||||||
|
|
||||||
1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS
|
1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS (端口 443)
|
||||||
2. **消息签名**: MPC 消息包含签名验证
|
2. **HTTPS**: 与 Account Service 的 HTTP 连接使用 HTTPS
|
||||||
3. **会话隔离**: 每个会话使用独立的密钥对
|
3. **消息签名**: MPC 消息包含签名验证
|
||||||
|
4. **会话隔离**: 每个会话使用独立的密钥对
|
||||||
|
|
||||||
### 应用安全
|
### 应用安全
|
||||||
|
|
||||||
1. **代码签名**: 发布的应用经过代码签名
|
1. **代码签名**: 发布的应用经过代码签名
|
||||||
2. **自动更新**: 支持安全的自动更新机制
|
2. **沙箱隔离**: Electron 渲染进程在沙箱中运行
|
||||||
3. **沙箱隔离**: Electron 渲染进程在沙箱中运行
|
3. **Context Isolation**: preload 脚本使用 contextBridge 安全暴露 API
|
||||||
|
|
||||||
## 与现有系统的集成
|
## 与现有系统的集成
|
||||||
|
|
||||||
### 与 Message Router 的通信
|
### 与 Message Router 的通信 (gRPC)
|
||||||
|
|
||||||
```
|
```
|
||||||
Service Party App
|
Service Party App
|
||||||
|
|
@ -295,7 +397,7 @@ Service Party App
|
||||||
│ Message Router │
|
│ Message Router │
|
||||||
│ ┌───────────────┐ │
|
│ ┌───────────────┐ │
|
||||||
│ │ RegisterParty │ │ ← 注册为参与方
|
│ │ RegisterParty │ │ ← 注册为参与方
|
||||||
│ │ Heartbeat │ │ ← 心跳保活
|
│ │ Heartbeat │ │ ← 心跳保活 (30秒)
|
||||||
│ │ JoinSession │ │ ← 加入会话
|
│ │ JoinSession │ │ ← 加入会话
|
||||||
│ │ Subscribe │ │ ← 订阅消息
|
│ │ Subscribe │ │ ← 订阅消息
|
||||||
│ │ RouteMessage │ │ ← 发送消息
|
│ │ RouteMessage │ │ ← 发送消息
|
||||||
|
|
@ -304,36 +406,56 @@ Service Party App
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 与 Admin Web 的协作
|
### 与 Account Service 的通信 (HTTP)
|
||||||
|
|
||||||
1. Admin Web 创建 `co_managed_keygen` 类型的会话
|
```
|
||||||
2. Session Coordinator 生成邀请码
|
Service Party App
|
||||||
3. Admin Web 显示邀请二维码
|
│
|
||||||
4. Service Party App 扫码获取会话信息
|
│ HTTPS
|
||||||
5. 双方通过 Message Router 交换 MPC 消息
|
▼
|
||||||
6. 完成后各自获得结果
|
┌─────────────────────┐
|
||||||
|
│ Account Service │
|
||||||
|
│ ┌───────────────┐ │
|
||||||
|
│ │ CreateSession │ │ ← 创建 Keygen/Sign 会话
|
||||||
|
│ │ JoinSession │ │ ← 加入会话
|
||||||
|
│ │ GetSession │ │ ← 查询会话状态
|
||||||
|
│ │ ByInviteCode │ │ ← 通过邀请码查询
|
||||||
|
│ └───────────────┘ │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
## 未来扩展
|
### 数据流
|
||||||
|
|
||||||
### 签名功能
|
1. **Keygen 创建**:
|
||||||
|
- App → Account Service: 创建会话,获取 session_id + invite_code
|
||||||
|
- App → Message Router: RegisterParty
|
||||||
|
- App → Message Router: SubscribeSessionEvents
|
||||||
|
- 等待其他参与方加入...
|
||||||
|
|
||||||
当前版本仅支持密钥生成,未来将支持:
|
2. **Keygen 加入**:
|
||||||
|
- App → Account Service: 通过 invite_code 查询 session_id
|
||||||
|
- App → Message Router: RegisterParty
|
||||||
|
- App → Account Service: JoinSession
|
||||||
|
- 等待所有参与方就绪...
|
||||||
|
|
||||||
- 参与门限签名
|
3. **Keygen 执行**:
|
||||||
- 支持多种签名算法 (ECDSA, EdDSA)
|
- Message Router 通知所有参与方开始
|
||||||
- 批量签名
|
- 各 App 启动 TSS 子进程
|
||||||
|
- 通过 Message Router 交换 MPC 消息
|
||||||
|
- 完成后各自保存 share 到本地 SQLite
|
||||||
|
|
||||||
### 密钥刷新
|
4. **Sign 创建**:
|
||||||
|
- App → SQLite: 读取 share 和 participants_json
|
||||||
|
- App → Account Service: 创建签名会话,指定参与方列表
|
||||||
|
- 分享 invite_code 给其他参与方
|
||||||
|
|
||||||
- 支持 share 刷新而不改变公钥
|
5. **Sign 执行**:
|
||||||
- 支持增加/减少参与方
|
- 足够参与方加入后自动开始
|
||||||
|
- 通过 Message Router 交换 MPC 消息
|
||||||
### 硬件钱包集成
|
- 完成后返回签名结果
|
||||||
|
|
||||||
- 支持将 share 存储在硬件安全模块
|
|
||||||
- 支持 Ledger/Trezor 等硬件钱包
|
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|
||||||
- [共管钱包实现计划](./co-managed-wallet-implementation-plan.md)
|
- [共管钱包实现计划](./co-managed-wallet-implementation-plan.md)
|
||||||
- [MPC 系统架构](./architecture/)
|
- [MPC 系统架构](../backend/mpc-system/docs/01-architecture.md)
|
||||||
|
- [API 参考](../backend/mpc-system/docs/02-api-reference.md)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue