rwadurian/backend/scripts/init-hot-wallet.sh

504 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <wallet-name> --threshold-n <n> --threshold-t <t> [--jwt-secret <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 <name> --threshold-n <n> --threshold-t <t> [--jwt-secret <secret>]"
exit 1
;;
esac
done
}
# 验证参数
validate_params() {
if [ -z "$USERNAME" ]; then
log_error "用户名不能为空"
echo ""
echo "用法: ./init-hot-wallet.sh --username <name> --threshold-n <n> --threshold-t <t> [--jwt-secret <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=""
# 检查 node 是否可用
if ! command -v node &> /dev/null; then
log_error "Node.js 未安装,无法计算地址"
echo "公钥 (hex): $PUBLIC_KEY"
return
fi
# 获取脚本所在目录,进入 blockchain-service 目录执行(那里有 ethers
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)
else
# 尝试直接执行
log_debug "尝试直接使用 node + ethers"
ADDRESS=$(node -e "
const { computeAddress } = require('ethers');
console.log(computeAddress('0x$PUBLIC_KEY'));
" 2>/dev/null)
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 "$@"