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 (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -19,6 +20,7 @@ import (
|
|||
// This is a completely independent handler that does not affect existing functionality
|
||||
type CoManagedHTTPHandler struct {
|
||||
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
||||
db *sql.DB // Database connection for invite_code lookups
|
||||
}
|
||||
|
||||
// NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler
|
||||
|
|
@ -27,6 +29,18 @@ func NewCoManagedHTTPHandler(
|
|||
) *CoManagedHTTPHandler {
|
||||
return &CoManagedHTTPHandler{
|
||||
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) {
|
||||
coManaged := router.Group("/co-managed")
|
||||
{
|
||||
// Keygen session routes
|
||||
coManaged.POST("/sessions", h.CreateSession)
|
||||
coManaged.POST("/sessions/:sessionId/join", h.JoinSession)
|
||||
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)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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,
|
||||
cancelRecoveryUC,
|
||||
sessionCoordinatorClient,
|
||||
db,
|
||||
); err != nil {
|
||||
errChan <- fmt.Errorf("HTTP server error: %w", err)
|
||||
}
|
||||
|
|
@ -239,6 +240,7 @@ func startHTTPServer(
|
|||
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase,
|
||||
cancelRecoveryUC *use_cases.CancelRecoveryUseCase,
|
||||
sessionCoordinatorClient *grpcadapter.SessionCoordinatorClient,
|
||||
db *sql.DB,
|
||||
) error {
|
||||
// Set Gin mode
|
||||
if cfg.Server.Environment == "production" {
|
||||
|
|
@ -301,7 +303,8 @@ func startHTTPServer(
|
|||
})
|
||||
|
||||
// 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
|
||||
// Skip paths that don't require authentication
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { GrpcClient } from './modules/grpc-client';
|
|||
import { DatabaseManager } from './modules/database';
|
||||
import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation';
|
||||
import { KavaTxService, KAVA_MAINNET_TX_CONFIG } from './modules/kava-tx-service';
|
||||
import { AccountClient } from './modules/account-client';
|
||||
|
||||
// 内置 HTTP 服务器端口
|
||||
const HTTP_PORT = 3456;
|
||||
|
|
@ -15,6 +16,7 @@ let grpcClient: GrpcClient | null = null;
|
|||
let database: DatabaseManager | null = null;
|
||||
let httpServer: ReturnType<typeof express.application.listen> | null = null;
|
||||
let kavaTxService: KavaTxService | null = null;
|
||||
let accountClient: AccountClient | null = null;
|
||||
|
||||
// 创建主窗口
|
||||
function createWindow() {
|
||||
|
|
@ -92,6 +94,12 @@ async function initServices() {
|
|||
// 初始化 Kava 交易服务
|
||||
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 处理器
|
||||
setupIpcHandlers();
|
||||
}
|
||||
|
|
@ -132,22 +140,70 @@ function setupIpcHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
// gRPC - 创建会话
|
||||
ipcMain.handle('grpc:createSession', async (_event, _params) => {
|
||||
// TODO: 实现创建会话逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
// gRPC - 创建会话 (通过 Account 服务 HTTP API)
|
||||
ipcMain.handle('grpc:createSession', async (_event, params) => {
|
||||
try {
|
||||
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 - 验证邀请码
|
||||
ipcMain.handle('grpc:validateInviteCode', async (_event, { code: _code }) => {
|
||||
// TODO: 实现验证邀请码逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
// gRPC - 验证邀请码 (通过 Account 服务 HTTP API)
|
||||
ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => {
|
||||
try {
|
||||
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 - 获取会话状态
|
||||
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId: _sessionId }) => {
|
||||
// TODO: 实现获取会话状态逻辑
|
||||
return { success: false, error: '功能尚未实现' };
|
||||
// gRPC - 获取会话状态 (通过 Account 服务 HTTP API)
|
||||
ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => {
|
||||
try {
|
||||
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 - 测试连接
|
||||
|
|
@ -161,16 +217,152 @@ function setupIpcHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
// gRPC - 验证签名会话
|
||||
ipcMain.handle('grpc:validateSigningSession', async (_event, { code: _code }) => {
|
||||
// TODO: 实现验证签名会话逻辑
|
||||
return { success: false, error: '功能尚未实现 - 需要连接到 Session Coordinator' };
|
||||
// gRPC - 验证签名会话 (通过 Account 服务 HTTP API)
|
||||
ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => {
|
||||
try {
|
||||
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 - 加入签名会话
|
||||
ipcMain.handle('grpc:joinSigningSession', async (_event, _params) => {
|
||||
// TODO: 实现加入签名会话逻辑
|
||||
return { success: false, error: '功能尚未实现' };
|
||||
ipcMain.handle('grpc:joinSigningSession', 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' };
|
||||
}
|
||||
|
||||
// 加入签名会话 (通过 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)
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import styles from './Settings.module.css';
|
|||
|
||||
interface Settings {
|
||||
messageRouterUrl: string;
|
||||
accountServiceUrl: string;
|
||||
autoBackup: boolean;
|
||||
backupPath: string;
|
||||
}
|
||||
|
|
@ -13,6 +14,7 @@ export default function Settings() {
|
|||
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
messageRouterUrl: 'mpc-grpc.szaiai.com:443', // 生产环境默认地址
|
||||
accountServiceUrl: 'https://api.szaiai.com', // Account 服务默认地址
|
||||
autoBackup: false,
|
||||
backupPath: '',
|
||||
});
|
||||
|
|
@ -27,8 +29,12 @@ export default function Settings() {
|
|||
const loadSettings = async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.storage.getSettings();
|
||||
const accountUrl = await window.electronAPI.account.getUrl();
|
||||
if (result) {
|
||||
setSettings(result);
|
||||
setSettings({
|
||||
...result,
|
||||
accountServiceUrl: accountUrl || 'https://api.szaiai.com',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings:', err);
|
||||
|
|
@ -43,6 +49,7 @@ export default function Settings() {
|
|||
|
||||
try {
|
||||
await window.electronAPI.storage.saveSettings(settings);
|
||||
await window.electronAPI.account.updateUrl(settings.accountServiceUrl);
|
||||
setMessage({ type: 'success', text: '设置已保存' });
|
||||
} catch (err) {
|
||||
setMessage({ type: 'error', text: '保存设置失败' });
|
||||
|
|
@ -57,12 +64,29 @@ export default function Settings() {
|
|||
try {
|
||||
const result = await window.electronAPI.grpc.testConnection(settings.messageRouterUrl);
|
||||
if (result.success) {
|
||||
setMessage({ type: 'success', text: '连接成功' });
|
||||
setMessage({ type: 'success', text: 'Message Router 连接成功' });
|
||||
} else {
|
||||
setMessage({ type: 'error', text: result.error || '连接失败' });
|
||||
setMessage({ type: 'error', text: result.error || 'Message Router 连接失败' });
|
||||
}
|
||||
} 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)
|
||||
</p>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,55 @@ interface JoinSigningSessionParams {
|
|||
}
|
||||
|
||||
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;
|
||||
error?: string;
|
||||
}
|
||||
|
|
@ -346,6 +395,16 @@ interface KavaHealthCheckResult {
|
|||
// ===========================================================================
|
||||
|
||||
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: {
|
||||
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 │
|
||||
│ (管理员电脑) │ │ App (用户A电脑) │ │ App (用户B电脑) │
|
||||
│ Service Party │ │ Service Party │ │ Service Party │
|
||||
│ App (用户A) │ │ App (用户B) │ │ App (用户C) │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
│ 创建会话/生成邀请码 │ 扫码加入 │ 扫码加入
|
||||
│ 创建会话/生成邀请码 │ 输入邀请码加入 │ 输入邀请码加入
|
||||
│ │ │
|
||||
└───────────────────────┼───────────────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ Account Service │
|
||||
│ (HTTP API - 会话管理) │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ Message Router │
|
||||
│ (gRPC 消息路由) │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────┴────────────┐
|
||||
│ TSS Keygen Protocol │
|
||||
│ (分布式密钥生成) │
|
||||
│ TSS Keygen/Sign │
|
||||
│ (分布式密钥生成/签名) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### 典型使用流程
|
||||
|
||||
1. **管理员** 在 Admin Web 创建共管钱包会话,配置阈值 (如 2-of-3)
|
||||
2. **管理员** 生成邀请二维码,分享给参与方
|
||||
3. **参与方** 在各自电脑上打开 Service Party App,扫描二维码加入
|
||||
4. **所有参与方就绪后**,自动开始 TSS 密钥生成协议
|
||||
5. **完成后**,每个参与方本地保存自己的加密 share,管理员获得钱包公钥
|
||||
#### Keygen 流程 (密钥生成)
|
||||
|
||||
1. **发起方** 在 Service Party App 输入钱包名称和用户名称,创建共管钱包会话
|
||||
2. **发起方** 获得邀请码 (格式: XXXX-XXXX-XXXX),分享给其他参与方
|
||||
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)
|
||||
│ └── modules/
|
||||
│ ├── grpc-client.ts # gRPC 客户端 (连接 Message Router)
|
||||
│ ├── account-client.ts # HTTP 客户端 (连接 Account Service)
|
||||
│ ├── 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 前端 (渲染进程)
|
||||
│ ├── App.tsx # 应用入口
|
||||
│ ├── pages/
|
||||
│ │ ├── Home.tsx # 主页 - Share 列表
|
||||
│ │ ├── Join.tsx # 加入会话页面
|
||||
│ │ ├── Create.tsx # 创建会话页面 (可选)
|
||||
│ │ ├── Create.tsx # 创建会话页面
|
||||
│ │ ├── Session.tsx # 会话进度页面
|
||||
│ │ ├── Sign.tsx # 签名页面
|
||||
│ │ └── Settings.tsx # 设置页面
|
||||
│ ├── components/
|
||||
│ │ ├── ShareCard.tsx # Share 卡片组件
|
||||
│ │ ├── JoinForm.tsx # 加入表单
|
||||
│ │ ├── ProgressSteps.tsx # 进度步骤
|
||||
│ │ ├── ExportDialog.tsx # 导出对话框
|
||||
│ │ └── ImportDialog.tsx # 导入对话框
|
||||
│ └── hooks/
|
||||
│ ├── useGrpc.ts # gRPC 连接 Hook
|
||||
│ ├── useSession.ts # 会话状态 Hook
|
||||
│ └── useStorage.ts # 本地存储 Hook
|
||||
│ ├── components/ # UI 组件
|
||||
│ ├── stores/
|
||||
│ │ └── appStore.ts # Zustand 状态管理
|
||||
│ ├── types/
|
||||
│ │ └── electron.d.ts # TypeScript 类型定义
|
||||
│ └── utils/
|
||||
│ └── address.ts # 地址工具函数
|
||||
│
|
||||
├── tss-party/ # Go TSS 子进程
|
||||
│ ├── main.go # TSS 协议执行程序
|
||||
│ ├── go.mod # Go 模块定义
|
||||
│ └── go.sum # 依赖锁定
|
||||
│
|
||||
├── bin/ # 编译后的二进制文件
|
||||
│ ├── win32-x64/
|
||||
│ │ └── tss-party.exe
|
||||
│ ├── darwin-x64/
|
||||
│ │ └── tss-party
|
||||
│ └── linux-x64/
|
||||
│ └── tss-party
|
||||
├── proto/ # gRPC Proto 文件
|
||||
│ └── message_router.proto # Message Router 接口定义
|
||||
│
|
||||
├── package.json # Node.js 依赖
|
||||
├── electron-builder.json # Electron 打包配置
|
||||
├── 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 保持一致 |
|
||||
| 构建工具 | Vite | 快速开发和构建 |
|
||||
| 状态管理 | 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 |
|
||||
| 本地存储 | electron-store | 加密本地存储 |
|
||||
| 加密算法 | AES-256-GCM | Share 加密存储 |
|
||||
| 本地存储 | sql.js (SQLite) | 纯 JavaScript SQLite 实现 |
|
||||
| 加密算法 | AES-256-GCM | Share 加密存储,PBKDF2 密钥派生 |
|
||||
| 打包工具 | 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 子进程架构
|
||||
|
||||
为什么使用 Go 子进程而不是 WASM?
|
||||
|
|
@ -128,9 +183,10 @@ backend/mpc-system/services/service-party-app/
|
|||
│ ┌────────────────────────────────────────────────────────┐ │
|
||||
│ │ - 使用 bnb-chain/tss-lib v2 │ │
|
||||
│ │ - 执行 GG20 Keygen 协议 (4 轮) │ │
|
||||
│ │ - 执行 GG20 Signing 协议 │ │
|
||||
│ │ - 通过 stdin 接收 MPC 消息 │ │
|
||||
│ │ - 通过 stdout 发送 MPC 消息 │ │
|
||||
│ │ - 完成后返回公钥 + 加密 share │ │
|
||||
│ │ - 完成后返回公钥 + 加密 share / 签名结果 │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
|
@ -154,8 +210,9 @@ interface OutgoingMessage {
|
|||
isBroadcast?: boolean;
|
||||
toParties?: string[];
|
||||
payload?: string; // base64 encoded
|
||||
publicKey?: string; // base64 encoded (result)
|
||||
encryptedShare?: string; // base64 encoded (result)
|
||||
publicKey?: string; // base64 encoded (keygen result)
|
||||
encryptedShare?: string; // base64 encoded (keygen result)
|
||||
signature?: string; // base64 encoded (sign result)
|
||||
partyIndex?: number;
|
||||
round?: number;
|
||||
totalRounds?: number;
|
||||
|
|
@ -167,13 +224,13 @@ interface OutgoingMessage {
|
|||
|
||||
### 1. 加入会话
|
||||
|
||||
用户通过以下方式加入共管钱包创建会话:
|
||||
用户通过以下方式加入共管钱包创建/签名会话:
|
||||
|
||||
- **扫描二维码**: 使用摄像头扫描 Admin Web 生成的邀请二维码
|
||||
- **粘贴邀请链接**: 手动粘贴邀请链接
|
||||
- **输入邀请码**: 手动输入 6 位邀请码
|
||||
- **输入邀请码**: 手动输入 12 位邀请码 (格式: XXXX-XXXX-XXXX)
|
||||
- **扫描二维码**: 使用摄像头扫描邀请二维码 (可选)
|
||||
- **粘贴邀请链接**: 手动粘贴邀请链接 (可选)
|
||||
|
||||
### 2. TSS 密钥生成
|
||||
### 2. TSS 密钥生成 (Keygen)
|
||||
|
||||
参与 GG20 (Gennaro-Goldfeder 2020) 门限签名密钥生成协议:
|
||||
|
||||
|
|
@ -181,35 +238,58 @@ interface OutgoingMessage {
|
|||
- 4 轮消息交换
|
||||
- 零知识证明保证安全性
|
||||
- 每个参与方获得自己的 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
|
||||
interface ShareEntry {
|
||||
id: string; // Share 唯一标识
|
||||
sessionId: string; // 会话 ID
|
||||
walletName: string; // 钱包名称
|
||||
partyId: string; // 参与方 ID
|
||||
partyIndex: number; // 参与方索引
|
||||
threshold: {
|
||||
t: number; // 签名阈值
|
||||
n: number; // 总参与方数
|
||||
};
|
||||
publicKey: string; // 钱包公钥 (hex)
|
||||
encryptedShare: string; // AES-256-GCM 加密的 share
|
||||
createdAt: string; // 创建时间
|
||||
metadata: {
|
||||
participants: Array<{
|
||||
partyId: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
interface ShareRecord {
|
||||
id: string; // Share 唯一标识 (UUID)
|
||||
session_id: string; // Keygen 会话 ID
|
||||
wallet_name: string; // 钱包名称
|
||||
party_id: string; // 参与方 ID
|
||||
party_index: number; // 参与方索引
|
||||
threshold_t: number; // 签名阈值
|
||||
threshold_n: number; // 总参与方数
|
||||
public_key_hex: string; // 钱包公钥 (hex)
|
||||
encrypted_share: string; // AES-256-GCM 加密的 share
|
||||
created_at: string; // 创建时间
|
||||
last_used_at: string | null; // 最后使用时间
|
||||
participants_json: string; // JSON: 参与方列表 [{partyId, name}]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 备份与恢复
|
||||
**重要**: `participants_json` 字段存储了 Keygen 时所有参与方的信息,Sign 时必须从这里选择参与方。
|
||||
|
||||
### 5. 地址派生
|
||||
|
||||
支持从公钥派生多链地址:
|
||||
|
||||
- **Kava** (主链)
|
||||
- **Cosmos** 系列链
|
||||
- **Ethereum** 兼容链
|
||||
|
||||
### 6. Kava 交易
|
||||
|
||||
支持构建和广播 Kava 交易:
|
||||
|
||||
- 查询余额
|
||||
- 构建转账交易 (待 TSS 签名)
|
||||
- 广播已签名交易
|
||||
- 查询交易状态
|
||||
|
||||
### 7. 备份与恢复
|
||||
|
||||
- **导出备份**: 将加密的 share 导出为文件
|
||||
- **导入恢复**: 从备份文件恢复 share
|
||||
|
|
@ -221,7 +301,14 @@ interface ShareEntry {
|
|||
|
||||
- Node.js 18+
|
||||
- Go 1.21+
|
||||
- pnpm 或 npm
|
||||
- npm 或 pnpm
|
||||
|
||||
### Windows 一键编译
|
||||
|
||||
```bash
|
||||
cd backend/mpc-system/services/service-party-app
|
||||
build-windows.bat
|
||||
```
|
||||
|
||||
### 编译 TSS 子进程
|
||||
|
||||
|
|
@ -230,9 +317,12 @@ interface ShareEntry {
|
|||
cd backend/mpc-system/services/service-party-app/tss-party
|
||||
go build -o ../bin/win32-x64/tss-party.exe .
|
||||
|
||||
# macOS
|
||||
# macOS (Intel)
|
||||
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
|
||||
GOOS=linux GOARCH=amd64 go build -o ../bin/linux-x64/tss-party .
|
||||
```
|
||||
|
|
@ -252,39 +342,51 @@ npm run dev
|
|||
### 生产构建
|
||||
|
||||
```bash
|
||||
# 构建前端
|
||||
# 构建前端 + Electron
|
||||
npm run build
|
||||
|
||||
# 打包 Electron 应用
|
||||
npm run package
|
||||
# 打包 Windows 安装包
|
||||
npm run build:win
|
||||
|
||||
# 输出目录: dist/
|
||||
# 输出目录: release/
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
### 设置页面配置项
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| Message Router 地址 | gRPC 服务地址 | mpc-grpc.szaiai.com:443 |
|
||||
| Account 服务地址 | HTTP API 地址 | https://api.szaiai.com |
|
||||
| 自动备份 | 创建 share 后自动备份 | false |
|
||||
| 备份目录 | 自动备份文件保存位置 | (空) |
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### Share 安全
|
||||
|
||||
1. **本地加密**: Share 使用 AES-256-GCM 加密存储
|
||||
2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生
|
||||
2. **密钥派生**: 加密密钥由用户密码通过 PBKDF2 派生 (100,000 次迭代)
|
||||
3. **内存保护**: Share 在内存中的时间尽量短
|
||||
4. **安全删除**: 删除 share 时安全擦除
|
||||
4. **安全删除**: 删除 share 时同时删除派生地址和签名历史
|
||||
|
||||
### 网络安全
|
||||
|
||||
1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS
|
||||
2. **消息签名**: MPC 消息包含签名验证
|
||||
3. **会话隔离**: 每个会话使用独立的密钥对
|
||||
1. **TLS 加密**: 与 Message Router 的 gRPC 连接使用 TLS (端口 443)
|
||||
2. **HTTPS**: 与 Account Service 的 HTTP 连接使用 HTTPS
|
||||
3. **消息签名**: MPC 消息包含签名验证
|
||||
4. **会话隔离**: 每个会话使用独立的密钥对
|
||||
|
||||
### 应用安全
|
||||
|
||||
1. **代码签名**: 发布的应用经过代码签名
|
||||
2. **自动更新**: 支持安全的自动更新机制
|
||||
3. **沙箱隔离**: Electron 渲染进程在沙箱中运行
|
||||
2. **沙箱隔离**: Electron 渲染进程在沙箱中运行
|
||||
3. **Context Isolation**: preload 脚本使用 contextBridge 安全暴露 API
|
||||
|
||||
## 与现有系统的集成
|
||||
|
||||
### 与 Message Router 的通信
|
||||
### 与 Message Router 的通信 (gRPC)
|
||||
|
||||
```
|
||||
Service Party App
|
||||
|
|
@ -295,7 +397,7 @@ Service Party App
|
|||
│ Message Router │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ RegisterParty │ │ ← 注册为参与方
|
||||
│ │ Heartbeat │ │ ← 心跳保活
|
||||
│ │ Heartbeat │ │ ← 心跳保活 (30秒)
|
||||
│ │ JoinSession │ │ ← 加入会话
|
||||
│ │ Subscribe │ │ ← 订阅消息
|
||||
│ │ RouteMessage │ │ ← 发送消息
|
||||
|
|
@ -304,36 +406,56 @@ Service Party App
|
|||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 与 Admin Web 的协作
|
||||
### 与 Account Service 的通信 (HTTP)
|
||||
|
||||
1. Admin Web 创建 `co_managed_keygen` 类型的会话
|
||||
2. Session Coordinator 生成邀请码
|
||||
3. Admin Web 显示邀请二维码
|
||||
4. Service Party App 扫码获取会话信息
|
||||
5. 双方通过 Message Router 交换 MPC 消息
|
||||
6. 完成后各自获得结果
|
||||
```
|
||||
Service Party App
|
||||
│
|
||||
│ HTTPS
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 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
|
||||
- 等待所有参与方就绪...
|
||||
|
||||
- 参与门限签名
|
||||
- 支持多种签名算法 (ECDSA, EdDSA)
|
||||
- 批量签名
|
||||
3. **Keygen 执行**:
|
||||
- Message Router 通知所有参与方开始
|
||||
- 各 App 启动 TSS 子进程
|
||||
- 通过 Message Router 交换 MPC 消息
|
||||
- 完成后各自保存 share 到本地 SQLite
|
||||
|
||||
### 密钥刷新
|
||||
4. **Sign 创建**:
|
||||
- App → SQLite: 读取 share 和 participants_json
|
||||
- App → Account Service: 创建签名会话,指定参与方列表
|
||||
- 分享 invite_code 给其他参与方
|
||||
|
||||
- 支持 share 刷新而不改变公钥
|
||||
- 支持增加/减少参与方
|
||||
|
||||
### 硬件钱包集成
|
||||
|
||||
- 支持将 share 存储在硬件安全模块
|
||||
- 支持 Ledger/Trezor 等硬件钱包
|
||||
5. **Sign 执行**:
|
||||
- 足够参与方加入后自动开始
|
||||
- 通过 Message Router 交换 MPC 消息
|
||||
- 完成后返回签名结果
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [共管钱包实现计划](./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