diff --git a/backend/scripts/init-hot-wallet.sh b/backend/scripts/init-hot-wallet.sh index e90c23df..4bdee317 100644 --- a/backend/scripts/init-hot-wallet.sh +++ b/backend/scripts/init-hot-wallet.sh @@ -6,8 +6,10 @@ # 用途: 创建系统热钱包的 MPC 密钥,用于提现转账 # # 前提条件: -# 1. mpc-service 正在运行 +# 1. mpc-service 正在运行 (默认端口 3013) # 2. mpc-system 正在运行且所有 party 已启动 +# 3. jq 已安装 (用于解析 JSON) +# 4. curl 已安装 # # 使用方法: # ./init-hot-wallet.sh [options] @@ -17,171 +19,435 @@ # -n, --threshold-n 总 party 数量 (默认: 3) # -t, --threshold-t 签名门限值 (默认: 2) # -h, --host mpc-service 地址 (默认: http://localhost:3013) +# -v, --verbose 显示详细调试信息 # --help 显示帮助 # +# 示例: +# ./init-hot-wallet.sh +# ./init-hot-wallet.sh -u my-hot-wallet -h http://192.168.1.111:3013 +# ./init-hot-wallet.sh --username prod-hot-wallet --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="system-hot-wallet" THRESHOLD_N=3 THRESHOLD_T=2 MPC_HOST="http://localhost:3013" +VERBOSE=false + +# 日志函数 +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 +} + +# 显示帮助 +show_help() { + head -35 "$0" | tail -30 + exit 0 +} + +# 检查依赖 +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 已安装" + + log_success "依赖检查通过" +} + +# 检查服务连通性 +check_service() { + log_info "检查 MPC 服务连通性..." + + local health_url="$MPC_HOST/api/v1/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 服务: $MPC_HOST" + echo "" + echo "请检查:" + echo " 1. mpc-service 是否正在运行" + 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 服务响应异常 (HTTP $http_code)" + exit 1 + fi + + log_success "MPC 服务连接正常" +} # 解析参数 -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 - ;; - -h|--host) - MPC_HOST="$2" - shift 2 - ;; - --help) - head -30 "$0" | tail -25 - exit 0 - ;; - *) - echo "未知参数: $1" - exit 1 - ;; - esac -done +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 + ;; + -h|--host) + MPC_HOST="$2" + shift 2 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + --help) + show_help + ;; + *) + log_error "未知参数: $1" + echo "使用 --help 查看帮助" + exit 1 + ;; + esac + done +} -echo "==============================================" -echo "热钱包 MPC 初始化" -echo "==============================================" -echo "用户名: $USERNAME" -echo "门限: $THRESHOLD_T-of-$THRESHOLD_N" -echo "MPC 服务: $MPC_HOST" -echo "==============================================" -echo "" +# 验证参数 +validate_params() { + if [ -z "$USERNAME" ]; then + log_error "用户名不能为空" + exit 1 + fi -# Step 1: 创建 Keygen 会话 -echo "[1/4] 创建 Keygen 会话..." -KEYGEN_RESPONSE=$(curl -s -X POST "$MPC_HOST/mpc/keygen" \ - -H "Content-Type: application/json" \ - -d "{ + 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 会话 +create_keygen_session() { + log_info "创建 Keygen 会话..." + + local url="$MPC_HOST/api/v1/mpc/keygen" + local payload="{ \"username\": \"$USERNAME\", \"thresholdN\": $THRESHOLD_N, \"thresholdT\": $THRESHOLD_T, \"requireDelegate\": true - }") + }" -SESSION_ID=$(echo "$KEYGEN_RESPONSE" | jq -r '.sessionId') -STATUS=$(echo "$KEYGEN_RESPONSE" | jq -r '.status') + log_debug "请求: POST $url" + log_debug "负载: $payload" -if [ "$SESSION_ID" == "null" ] || [ -z "$SESSION_ID" ]; then - echo "错误: 创建 Keygen 会话失败" - echo "响应: $KEYGEN_RESPONSE" - exit 1 -fi + KEYGEN_RESPONSE=$(curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + --connect-timeout 30 \ + --max-time 60) -echo "会话 ID: $SESSION_ID" -echo "状态: $STATUS" -echo "" + log_debug "响应: $KEYGEN_RESPONSE" -# Step 2: 轮询等待 Keygen 完成 -echo "[2/4] 等待 Keygen 完成..." -MAX_ATTEMPTS=180 # 最多等待 6 分钟 -ATTEMPT=0 -PUBLIC_KEY="" - -while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - STATUS_RESPONSE=$(curl -s -X GET "$MPC_HOST/mpc/keygen/$SESSION_ID/status") - STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status') - PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey // empty') - - echo " 轮询 #$((ATTEMPT + 1)): 状态=$STATUS" - - if [ "$STATUS" == "completed" ]; then - echo "" - echo "Keygen 完成!" - break - fi - - if [ "$STATUS" == "failed" ] || [ "$STATUS" == "expired" ]; then - echo "" - echo "错误: Keygen 失败,状态=$STATUS" + # 检查是否为有效 JSON + if ! echo "$KEYGEN_RESPONSE" | jq . &>/dev/null; then + log_error "API 返回无效 JSON" + echo "响应内容: $KEYGEN_RESPONSE" exit 1 fi - ATTEMPT=$((ATTEMPT + 1)) - sleep 2 -done + # 检查是否有错误 + local error_msg=$(echo "$KEYGEN_RESPONSE" | jq -r '.message // .error // empty') + if [ -n "$error_msg" ] && [ "$error_msg" != "null" ]; then + local status_code=$(echo "$KEYGEN_RESPONSE" | jq -r '.statusCode // empty') + if [ -n "$status_code" ] && [ "$status_code" != "200" ] && [ "$status_code" != "201" ]; then + log_error "API 错误: $error_msg" + exit 1 + fi + fi -if [ "$STATUS" != "completed" ]; then - echo "错误: Keygen 超时" + SESSION_ID=$(echo "$KEYGEN_RESPONSE" | jq -r '.sessionId') + local status=$(echo "$KEYGEN_RESPONSE" | jq -r '.status') + + 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" +} + +# 等待 Keygen 完成 +wait_for_keygen() { + log_info "等待 Keygen 完成 (最多 6 分钟)..." + echo "" + + local max_attempts=180 # 6 分钟 (每 2 秒轮询一次) + local attempt=0 + local last_status="" + local spinner=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local spinner_idx=0 + + while [ $attempt -lt $max_attempts ]; do + local url="$MPC_HOST/api/v1/mpc/keygen/$SESSION_ID/status" + STATUS_RESPONSE=$(curl -s -X GET "$url" --connect-timeout 10) + + log_debug "轮询响应: $STATUS_RESPONSE" + + STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status') + PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey // 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 -fi +} -# Step 3: 显示公钥 -echo "" -echo "[3/4] 获取公钥..." -if [ -z "$PUBLIC_KEY" ]; then - # 再次获取状态以确保拿到公钥 - STATUS_RESPONSE=$(curl -s -X GET "$MPC_HOST/mpc/keygen/$SESSION_ID/status") - PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey') -fi +# 获取公钥 +get_public_key() { + log_info "获取公钥..." -if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then - echo "错误: 无法获取公钥" - exit 1 -fi + if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then + # 再次获取状态以确保拿到公钥 + local url="$MPC_HOST/api/v1/mpc/keygen/$SESSION_ID/status" + STATUS_RESPONSE=$(curl -s -X GET "$url") + PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey') + fi -echo "公钥: $PUBLIC_KEY" + if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then + log_error "无法获取公钥" + exit 1 + fi -# Step 4: 从公钥派生 EVM 地址 -echo "" -echo "[4/4] 派生 EVM 地址..." + log_success "公钥获取成功" + echo " 公钥: ${PUBLIC_KEY:0:16}...${PUBLIC_KEY: -16}" +} -# 使用 node 计算地址(如果安装了 ethers) -if command -v node &> /dev/null; then - ADDRESS=$(node -e " - const { computeAddress } = require('ethers'); - try { - const address = computeAddress('0x$PUBLIC_KEY'); - console.log(address); - } catch (e) { - console.error('Error:', e.message); - process.exit(1); - } - " 2>/dev/null || echo "") +# 派生 EVM 地址 +derive_address() { + log_info "派生 EVM 地址..." - if [ -n "$ADDRESS" ] && [ "$ADDRESS" != "Error:"* ]; then - echo "EVM 地址: $ADDRESS" + ADDRESS="" + + # 方法 1: 使用 node + ethers + if command -v node &> /dev/null; then + log_debug "尝试使用 Node.js + ethers 计算地址" + + ADDRESS=$(node -e " + try { + const { computeAddress } = require('ethers'); + const address = computeAddress('0x$PUBLIC_KEY'); + console.log(address); + } catch (e) { + // ethers 可能未安装 + process.exit(1); + } + " 2>/dev/null) || ADDRESS="" + fi + + # 方法 2: 使用 Python + web3 (备选) + if [ -z "$ADDRESS" ] && command -v python3 &> /dev/null; then + log_debug "尝试使用 Python + eth_keys 计算地址" + + ADDRESS=$(python3 -c " +try: + from eth_keys import keys + from eth_utils import to_checksum_address + public_key_bytes = bytes.fromhex('$PUBLIC_KEY') + if len(public_key_bytes) == 65 and public_key_bytes[0] == 4: + public_key_bytes = public_key_bytes[1:] + pk = keys.PublicKey(public_key_bytes) + print(pk.to_checksum_address()) +except Exception as e: + pass +" 2>/dev/null) || ADDRESS="" + fi + + # 方法 3: 使用 openssl + keccak256 (更复杂,暂不实现) + + if [ -n "$ADDRESS" ]; then + log_success "EVM 地址派生成功" + echo " 地址: $ADDRESS" else - echo "提示: 无法自动计算地址,请手动计算" + log_warn "无法自动计算地址" + echo "" + echo "请手动从公钥计算 EVM 地址,或安装以下工具之一:" + echo " - Node.js + ethers: npm install -g ethers" + echo " - Python + eth_keys: pip install eth-keys eth-utils" + echo "" echo "公钥 (hex): $PUBLIC_KEY" fi -else - echo "提示: 未安装 Node.js,请手动从公钥计算 EVM 地址" - echo "公钥 (hex): $PUBLIC_KEY" -fi +} -echo "" -echo "==============================================" -echo "初始化完成!" -echo "==============================================" -echo "" -echo "请将以下配置添加到 blockchain-service 的环境变量:" -echo "" -echo " HOT_WALLET_USERNAME=$USERNAME" -if [ -n "$ADDRESS" ] && [ "$ADDRESS" != "Error:"* ]; then - echo " HOT_WALLET_ADDRESS=$ADDRESS" -else - echo " HOT_WALLET_ADDRESS=<从公钥计算的地址>" -fi -echo "" -echo "==============================================" +# 显示结果 +show_result() { + echo "" + echo -e "${GREEN}==============================================" + echo "热钱包初始化完成!" + echo "==============================================${NC}" + echo "" + echo "配置信息:" + echo " 用户名: $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=$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=$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 " 详细模式: $VERBOSE" + echo "" + + check_dependencies + check_service + + echo "" + echo "开始初始化流程..." + echo "" + + create_keygen_session + wait_for_keygen + get_public_key + derive_address + + show_result +} + +# 执行主函数 +main "$@"