#!/bin/bash # ============================================================================= # 热钱包 MPC 初始化脚本 # ============================================================================= # # 用途: 创建系统热钱包的 MPC 密钥,用于提现转账 # # 前提条件: # 1. mpc-system account-service 正在运行 (端口 4000) # 2. mpc-system session-coordinator 和 server-party 已启动 # 3. jq 已安装 (用于解析 JSON) # 4. curl 已安装 # 5. 环境变量 MPC_JWT_SECRET 已设置(或通过 --jwt-secret 参数传入) # # 使用方法: # ./init-hot-wallet.sh --username --threshold-n --threshold-t [--jwt-secret ] # # 示例: # export MPC_JWT_SECRET="your_jwt_secret_key" # ./init-hot-wallet.sh --username rwadurian-system-hot-wallet-01 --threshold-n 3 --threshold-t 2 # # ============================================================================= set -e # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color # 配置 - 通过参数传入 USERNAME="" THRESHOLD_N=3 THRESHOLD_T=2 MPC_HOST="http://localhost:4000" JWT_SECRET="${MPC_JWT_SECRET:-}" VERBOSE=true # 日志函数 log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_debug() { if [ "$VERBOSE" = true ]; then echo -e "${CYAN}[DEBUG]${NC} $1" fi } # 生成 JWT Token (简化版,用于 MPC 系统认证) # 使用 openssl + base64 生成 HS256 JWT generate_jwt_token() { local secret="$1" local now=$(date +%s) local exp=$((now + 86400)) # 24小时后过期 local jti=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen 2>/dev/null || echo "$(date +%s)-$$") # JWT Header local header='{"alg":"HS256","typ":"JWT"}' local header_b64=$(echo -n "$header" | base64 -w0 | tr '+/' '-_' | tr -d '=') # JWT Payload local payload="{\"jti\":\"$jti\",\"iss\":\"init-script\",\"sub\":\"system\",\"party_id\":\"init-script\",\"token_type\":\"access\",\"iat\":$now,\"nbf\":$now,\"exp\":$exp}" local payload_b64=$(echo -n "$payload" | base64 -w0 | tr '+/' '-_' | tr -d '=') # JWT Signature (HMAC-SHA256) local signature=$(echo -n "${header_b64}.${payload_b64}" | openssl dgst -sha256 -hmac "$secret" -binary | base64 -w0 | tr '+/' '-_' | tr -d '=') echo "${header_b64}.${payload_b64}.${signature}" } # 检查依赖 check_dependencies() { log_info "检查依赖..." if ! command -v curl &> /dev/null; then log_error "curl 未安装,请先安装 curl" exit 1 fi log_debug "✓ curl 已安装" if ! command -v jq &> /dev/null; then log_error "jq 未安装,请先安装 jq" echo " Ubuntu/Debian: sudo apt-get install jq" echo " CentOS/RHEL: sudo yum install jq" echo " macOS: brew install jq" exit 1 fi log_debug "✓ jq 已安装" if ! command -v openssl &> /dev/null; then log_error "openssl 未安装,请先安装 openssl" exit 1 fi log_debug "✓ openssl 已安装" log_success "依赖检查通过" } # 检查服务连通性 check_service() { log_info "检查 MPC account-service 连通性..." local health_url="$MPC_HOST/health" log_debug "请求: GET $health_url" local response response=$(curl -s -w "\n%{http_code}" --connect-timeout 5 "$health_url" 2>/dev/null) || { log_error "无法连接到 MPC account-service: $MPC_HOST" echo "" echo "请检查:" echo " 1. mpc-system account-service 是否正在运行 (端口 4000)" echo " 2. 地址和端口是否正确" echo " 3. 网络是否可达" exit 1 } local http_code=$(echo "$response" | tail -1) local body=$(echo "$response" | sed '$d') log_debug "HTTP 状态码: $http_code" log_debug "响应内容: $body" if [ "$http_code" != "200" ]; then log_error "MPC account-service 响应异常 (HTTP $http_code)" exit 1 fi log_success "MPC account-service 连接正常" } # 解析参数 parse_args() { while [[ $# -gt 0 ]]; do case $1 in -u|--username) USERNAME="$2" shift 2 ;; -n|--threshold-n) THRESHOLD_N="$2" shift 2 ;; -t|--threshold-t) THRESHOLD_T="$2" shift 2 ;; --jwt-secret) JWT_SECRET="$2" shift 2 ;; *) log_error "未知参数: $1" echo "用法: ./init-hot-wallet.sh --username --threshold-n --threshold-t [--jwt-secret ]" exit 1 ;; esac done } # 验证参数 validate_params() { if [ -z "$USERNAME" ]; then log_error "用户名不能为空" echo "" echo "用法: ./init-hot-wallet.sh --username --threshold-n --threshold-t [--jwt-secret ]" echo "示例: ./init-hot-wallet.sh --username rwadurian-system-hot-wallet-01 --threshold-n 3 --threshold-t 2" exit 1 fi if [ -z "$JWT_SECRET" ]; then log_error "JWT_SECRET 不能为空" echo "" echo "请通过以下方式之一提供 JWT_SECRET:" echo " 1. 设置环境变量: export MPC_JWT_SECRET='your_secret'" echo " 2. 命令行参数: --jwt-secret 'your_secret'" exit 1 fi if [ "$THRESHOLD_N" -lt 2 ]; then log_error "总 party 数量 (threshold-n) 必须 >= 2" exit 1 fi if [ "$THRESHOLD_T" -lt 1 ] || [ "$THRESHOLD_T" -gt "$THRESHOLD_N" ]; then log_error "签名门限 (threshold-t) 必须在 1 到 $THRESHOLD_N 之间" exit 1 fi } # 创建 Keygen 会话 # 直接调用 mpc-system account-service API (使用 snake_case) create_keygen_session() { log_info "创建 Keygen 会话..." # 生成 JWT Token local jwt_token=$(generate_jwt_token "$JWT_SECRET") log_debug "JWT Token: ${jwt_token:0:50}..." local url="$MPC_HOST/api/v1/mpc/keygen" # mpc-system 使用 snake_case: threshold_n, threshold_t, require_delegate local payload="{ \"username\": \"$USERNAME\", \"threshold_n\": $THRESHOLD_N, \"threshold_t\": $THRESHOLD_T, \"require_delegate\": false }" log_debug "请求: POST $url" log_debug "负载: $payload" KEYGEN_RESPONSE=$(curl -s -X POST "$url" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $jwt_token" \ -d "$payload" \ --connect-timeout 30 \ --max-time 60) log_debug "响应: $KEYGEN_RESPONSE" # 检查是否为有效 JSON if ! echo "$KEYGEN_RESPONSE" | jq . &>/dev/null; then log_error "API 返回无效 JSON" echo "响应内容: $KEYGEN_RESPONSE" exit 1 fi # 检查是否有错误 local error_msg=$(echo "$KEYGEN_RESPONSE" | jq -r '.error // empty') if [ -n "$error_msg" ] && [ "$error_msg" != "null" ]; then log_error "API 错误: $error_msg" exit 1 fi # mpc-system 返回 snake_case: session_id SESSION_ID=$(echo "$KEYGEN_RESPONSE" | jq -r '.session_id') local status=$(echo "$KEYGEN_RESPONSE" | jq -r '.status') local selected_parties=$(echo "$KEYGEN_RESPONSE" | jq -r '.selected_parties | join(", ")') if [ "$SESSION_ID" == "null" ] || [ -z "$SESSION_ID" ]; then log_error "创建 Keygen 会话失败" echo "响应: $KEYGEN_RESPONSE" exit 1 fi log_success "会话已创建" echo " 会话 ID: $SESSION_ID" echo " 状态: $status" echo " 选中 Party: $selected_parties" } # 等待 Keygen 完成 wait_for_keygen() { log_info "等待 Keygen 完成 (最多 6 分钟)..." echo "" # 生成 JWT Token local jwt_token=$(generate_jwt_token "$JWT_SECRET") local max_attempts=180 # 6 分钟 (每 2 秒轮询一次) local attempt=0 local last_status="" local spinner=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local spinner_idx=0 while [ $attempt -lt $max_attempts ]; do # mpc-system 状态查询 API: /api/v1/mpc/sessions/{sessionId} local url="$MPC_HOST/api/v1/mpc/sessions/$SESSION_ID" STATUS_RESPONSE=$(curl -s -X GET "$url" -H "Authorization: Bearer $jwt_token" --connect-timeout 10) log_debug "轮询响应: $STATUS_RESPONSE" STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status') # mpc-system 返回 snake_case: public_key PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.public_key // empty') # 只在状态变化时输出 if [ "$STATUS" != "$last_status" ]; then echo "" echo " 状态变化: $last_status -> $STATUS" last_status="$STATUS" fi # 显示进度 local elapsed=$((attempt * 2)) local spin_char="${spinner[$spinner_idx]}" spinner_idx=$(( (spinner_idx + 1) % ${#spinner[@]} )) printf "\r ${spin_char} 等待中... (%d 秒)" "$elapsed" if [ "$STATUS" == "completed" ]; then echo "" log_success "Keygen 完成!" return 0 fi if [ "$STATUS" == "failed" ]; then echo "" local error=$(echo "$STATUS_RESPONSE" | jq -r '.error // "未知错误"') log_error "Keygen 失败: $error" exit 1 fi if [ "$STATUS" == "expired" ]; then echo "" log_error "Keygen 会话已过期" exit 1 fi attempt=$((attempt + 1)) sleep 2 done echo "" log_error "Keygen 超时 (6 分钟)" exit 1 } # 获取公钥 get_public_key() { log_info "获取公钥..." if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then # 再次获取状态以确保拿到公钥 local jwt_token=$(generate_jwt_token "$JWT_SECRET") local url="$MPC_HOST/api/v1/mpc/sessions/$SESSION_ID" STATUS_RESPONSE=$(curl -s -X GET "$url" -H "Authorization: Bearer $jwt_token") PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.public_key') fi if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then log_error "无法获取公钥" exit 1 fi log_success "公钥获取成功" echo " 公钥: ${PUBLIC_KEY:0:16}...${PUBLIC_KEY: -16}" } # 获取 mpc-system 实际创建的 username # mpc-system 会自动生成 wallet-{session_id前8位} 格式的 username get_actual_username() { log_info "获取 mpc-system 创建的实际 username..." # 等待账户创建完成 (session-coordinator 异步创建) sleep 3 # 通过数据库查询实际的 username local db_query_result=$(docker exec mpc-postgres psql -U postgres -d mpc_system -t -A -c \ "SELECT username FROM accounts WHERE keygen_session_id = '$SESSION_ID' LIMIT 1;" 2>/dev/null) if [ -z "$db_query_result" ]; then log_warn "未找到账户,等待 5 秒后重试..." sleep 5 db_query_result=$(docker exec mpc-postgres psql -U postgres -d mpc_system -t -A -c \ "SELECT username FROM accounts WHERE keygen_session_id = '$SESSION_ID' LIMIT 1;" 2>/dev/null) fi if [ -n "$db_query_result" ]; then ACTUAL_USERNAME="$db_query_result" log_success "获取到实际 username: $ACTUAL_USERNAME" else # 如果无法查询数据库,使用预期的格式 ACTUAL_USERNAME="wallet-${SESSION_ID:0:8}" log_warn "无法查询数据库,使用预期格式: $ACTUAL_USERNAME" fi } # 派生 EVM 地址 derive_address() { log_info "派生 EVM 地址..." ADDRESS="" # 方法 1: 使用 Docker 容器里的 node (rwa-blockchain-service 容器有 ethers) log_debug "尝试使用 Docker 容器计算地址" ADDRESS=$(docker exec rwa-blockchain-service node -e " const { computeAddress } = require('ethers'); console.log(computeAddress('0x$PUBLIC_KEY')); " 2>/dev/null) # 方法 2: 使用本地 node + blockchain-service 目录的 ethers if [ -z "$ADDRESS" ] && command -v node &> /dev/null; then local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local service_dir="$script_dir/../services/blockchain-service" if [ -d "$service_dir/node_modules/ethers" ]; then log_debug "使用本地 blockchain-service 目录的 ethers" ADDRESS=$(cd "$service_dir" && node -e " const { computeAddress } = require('ethers'); console.log(computeAddress('0x$PUBLIC_KEY')); " 2>/dev/null) fi fi if [ -n "$ADDRESS" ]; then log_success "EVM 地址派生成功" echo " 地址: $ADDRESS" else log_error "计算地址失败" echo "公钥 (hex): $PUBLIC_KEY" fi } # 显示结果 show_result() { # 使用 mpc-system 实际创建的 username local final_username="${ACTUAL_USERNAME:-$USERNAME}" echo "" echo -e "${GREEN}==============================================" echo "热钱包初始化完成!" echo "==============================================${NC}" echo "" echo "配置信息:" echo " 用户名: $final_username" echo " 门限: $THRESHOLD_T-of-$THRESHOLD_N" echo " 会话ID: $SESSION_ID" echo " 公钥: ${PUBLIC_KEY:0:32}..." if [ -n "$ADDRESS" ]; then echo " 地址: $ADDRESS" fi echo "" echo -e "${YELLOW}请将以下配置添加到环境变量:${NC}" echo "" echo " # 在 backend/services/.env 中添加:" echo " HOT_WALLET_USERNAME=$final_username" if [ -n "$ADDRESS" ]; then echo " HOT_WALLET_ADDRESS=$ADDRESS" else echo " HOT_WALLET_ADDRESS=<请手动从公钥计算>" fi echo "" # 如果有地址,提供一键复制的格式 if [ -n "$ADDRESS" ]; then echo -e "${CYAN}一键复制 (追加到 .env):${NC}" echo "" echo "cat >> backend/services/.env << 'EOF'" echo "# Hot Wallet Configuration (initialized $(date +%Y-%m-%d))" echo "HOT_WALLET_USERNAME=$final_username" echo "HOT_WALLET_ADDRESS=$ADDRESS" echo "EOF" echo "" fi echo "==============================================" } # 主函数 main() { echo "" echo -e "${GREEN}==============================================" echo " 热钱包 MPC 初始化脚本" echo "==============================================${NC}" echo "" parse_args "$@" validate_params echo "配置:" echo " 用户名: $USERNAME" echo " 门限: $THRESHOLD_T-of-$THRESHOLD_N" echo " MPC 服务: $MPC_HOST" echo "" check_dependencies check_service echo "" echo "开始初始化流程..." echo "" create_keygen_session wait_for_keygen get_public_key get_actual_username derive_address show_result } # 执行主函数 main "$@"