504 lines
14 KiB
Bash
504 lines
14 KiB
Bash
#!/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 "$@"
|