feat: 钱包系统三方关联集成 - 注册/上链/钱包微服务
- 新增: nac_wallet_service/ Rust钱包微服务(Actix-Web + PostgreSQL) - 新增: onboarding/backend/nac_wallet_client.py Python钱包客户端 - 新增: id.newassetchain.io NacWalletService.php PHP钱包客户端 - 修改: AuthController.php 注册时自动创建NAC原生钱包 - 修改: success.blade.php 展示钱包地址和助记词(仅一次) - 修改: onboarding/routers/users.py 注册时自动创建NAC钱包 - 修改: onboarding/routers/onboarding.py chain-confirm集成XIC手续费+钱包签名 - 新增: ISSUE_WALLET_INTEGRATION_DELIVERY.md 交付报告 双币模式: XIC(治理币,默认手续费) + XTZH(稳定币,铸造) 安全: 助记词AES-256-GCM加密,仅一次返回,内网通信 关联工单: #33 #34 #35 #37
This commit is contained in:
parent
bf60deb5d2
commit
1545d54159
|
|
@ -0,0 +1,104 @@
|
||||||
|
# NAC钱包系统关联集成交付报告
|
||||||
|
|
||||||
|
**日期**: 2026-02-26
|
||||||
|
**工单**: 注册系统 + 钱包模块 + 一键上链系统关联集成
|
||||||
|
**状态**: 100% 完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、本次工作概述
|
||||||
|
|
||||||
|
本次工作实现了NAC公链三大核心系统的完整关联:
|
||||||
|
|
||||||
|
- 注册系统 (id.newassetchain.io) -- 注册时自动创建NAC原生钱包
|
||||||
|
- NAC钱包微服务 (nac-wallet-service, 端口8701) -- 上链时调用钱包签名 + XIC手续费计算
|
||||||
|
- 一键上链系统 (onboarding.newassetchain.io)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、技术架构
|
||||||
|
|
||||||
|
### 双币模式
|
||||||
|
- **XIC(资产治理币)**:可从交易所购买,用于支付所有平台手续费(默认)
|
||||||
|
- **XTZH(资产稳定币)**:需要铸造,用于资产计价和结算
|
||||||
|
|
||||||
|
### 手续费机制
|
||||||
|
- 所有手续费默认以 **XIC** 支付
|
||||||
|
- 持有XIC数量越多,享受更高折扣(VIP等级制度)
|
||||||
|
- 管理员可通过后台动态调整费率(前期推广可设为0)
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
- **PostgreSQL 14**(钱包专用数据库):nac_wallet
|
||||||
|
- **MySQL**(PHP注册系统):nac_auth
|
||||||
|
- **MongoDB**(一键上链系统)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、新增/修改文件清单
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
|
||||||
|
| 文件路径 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| /opt/nac/nac_wallet_service/ | Rust钱包微服务(Actix-Web + PostgreSQL) |
|
||||||
|
| /opt/nac/onboarding/backend/nac_wallet_client.py | Python钱包客户端(供onboarding调用) |
|
||||||
|
| /var/www/id.newassetchain.io/app/Services/NacWalletService.php | PHP钱包客户端(供Laravel调用) |
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
| 文件路径 | 修改内容 |
|
||||||
|
|---------|---------|
|
||||||
|
| /var/www/id.newassetchain.io/app/Http/Controllers/AuthController.php | 注册时自动调用钱包微服务创建NAC钱包 |
|
||||||
|
| /var/www/id.newassetchain.io/resources/views/auth/success.blade.php | 注册成功页面展示钱包地址和助记词 |
|
||||||
|
| /opt/nac/onboarding/backend/routers/users.py | 注册时自动创建NAC钱包 |
|
||||||
|
| /opt/nac/onboarding/backend/routers/onboarding.py | chain-confirm步骤集成钱包签名和XIC手续费 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、测试结果
|
||||||
|
|
||||||
|
### 4.1 PHP注册系统(id.newassetchain.io)
|
||||||
|
- [PASS] 注册API:返回钱包地址 + 双币余额 + 12个助记词
|
||||||
|
- [PASS] 登录API:正常返回JWT token
|
||||||
|
- [PASS] 钱包查询API:返回XIC和XTZH余额
|
||||||
|
- [PASS] 前端UI:注册成功页面正确展示助记词(仅一次)
|
||||||
|
|
||||||
|
### 4.2 一键上链系统(onboarding.newassetchain.io)
|
||||||
|
- [PASS] 注册API:返回钱包地址 + 双币余额 + 12个助记词
|
||||||
|
- [PASS] chain-confirm步骤:集成XIC手续费估算 + 钱包签名(降级安全处理)
|
||||||
|
|
||||||
|
### 4.3 钱包微服务(内网8701端口)
|
||||||
|
- [PASS] 健康检查:status=healthy, database=connected
|
||||||
|
- [PASS] 创建钱包:生成NAC原生32字节地址 + 加密助记词
|
||||||
|
- [PASS] 手续费配置:支持管理员动态调整
|
||||||
|
- [PASS] 安全认证:内部API密钥验证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、安全措施
|
||||||
|
|
||||||
|
1. **私钥安全**:助记词使用AES-256-GCM加密存储,服务器上永无明文
|
||||||
|
2. **助记词一次性**:仅注册时返回一次,系统不再存储明文
|
||||||
|
3. **内网通信**:钱包微服务仅监听127.0.0.1:8701,不对外网暴露
|
||||||
|
4. **API密钥认证**:服务间调用使用内部密钥验证
|
||||||
|
5. **代码安全**:Rust禁止unsafe,PHP使用PDO预处理,所有密钥通过.env注入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、未来扩展
|
||||||
|
|
||||||
|
本架构已为以下功能预留接口:
|
||||||
|
- 交易所模块(XIC/USDT、XIC/USDC交易对)
|
||||||
|
- 中间链和跨链桥
|
||||||
|
- 移动端App(安卓/苹果)
|
||||||
|
- PC端域名化(wallet.newassetchain.io)
|
||||||
|
- 多语言支持(i18n)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、关联工单
|
||||||
|
|
||||||
|
- 工单 #33:钱包后台管理系统
|
||||||
|
- 工单 #34:VISION智能钱包
|
||||||
|
- 工单 #35:多链支持
|
||||||
|
- 工单 #37:一键上链关联
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,57 @@
|
||||||
|
[package]
|
||||||
|
name = "nac-wallet-service"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["NAC Wallet Working Group"]
|
||||||
|
description = "NAC原生钱包微服务 - 基于Actix-Web,封装nac-wallet-core"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Web框架
|
||||||
|
actix-web = "4"
|
||||||
|
actix-rt = "2"
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-chrono-0_4"] }
|
||||||
|
deadpool-postgres = "0.12"
|
||||||
|
|
||||||
|
# 序列化
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# 异步运行时
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
futures-util = "0.3"
|
||||||
|
|
||||||
|
# 密码学
|
||||||
|
sha3 = "0.10"
|
||||||
|
rand = "0.8"
|
||||||
|
hex = "0.4"
|
||||||
|
ed25519-dalek = { version = "2.0", features = ["rand_core"] }
|
||||||
|
bip39 = "2.0"
|
||||||
|
sha2 = "0.10"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
pbkdf2 = { version = "0.12", features = ["simple"] }
|
||||||
|
hmac = "0.12"
|
||||||
|
zeroize = { version = "1.6", features = ["derive"] }
|
||||||
|
base64 = "0.21"
|
||||||
|
|
||||||
|
# 认证
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
# 工具
|
||||||
|
thiserror = "1"
|
||||||
|
anyhow = "1"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
dotenvy = "0.15"
|
||||||
|
validator = { version = "0.18", features = ["derive"] }
|
||||||
|
rust_decimal = { version = "1.33", features = ["tokio-pg"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
use deadpool_postgres::{Config as PgConfig, Pool, Runtime};
|
||||||
|
use tokio_postgres::NoTls;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
/// 应用配置
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub jwt_secret: String,
|
||||||
|
pub admin_jwt_secret: String,
|
||||||
|
pub db_host: String,
|
||||||
|
pub db_port: u16,
|
||||||
|
pub db_name: String,
|
||||||
|
pub db_user: String,
|
||||||
|
pub db_password: String,
|
||||||
|
pub db_pool_size: usize,
|
||||||
|
/// 内部服务调用密钥(PHP注册服务、上链服务使用)
|
||||||
|
pub internal_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
pub fn from_env() -> Result<Self, String> {
|
||||||
|
Ok(AppConfig {
|
||||||
|
host: env::var("WALLET_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
|
||||||
|
port: env::var("WALLET_PORT")
|
||||||
|
.unwrap_or_else(|_| "8701".to_string())
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "WALLET_PORT必须是有效端口号")?,
|
||||||
|
jwt_secret: env::var("JWT_SECRET")
|
||||||
|
.map_err(|_| "JWT_SECRET环境变量未设置")?,
|
||||||
|
admin_jwt_secret: env::var("ADMIN_JWT_SECRET")
|
||||||
|
.map_err(|_| "ADMIN_JWT_SECRET环境变量未设置")?,
|
||||||
|
db_host: env::var("PG_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
|
||||||
|
db_port: env::var("PG_PORT")
|
||||||
|
.unwrap_or_else(|_| "5432".to_string())
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| "PG_PORT必须是有效端口号")?,
|
||||||
|
db_name: env::var("PG_DBNAME").unwrap_or_else(|_| "nac_wallet".to_string()),
|
||||||
|
db_user: env::var("PG_USER").unwrap_or_else(|_| "nac_wallet_user".to_string()),
|
||||||
|
db_password: env::var("PG_PASSWORD")
|
||||||
|
.map_err(|_| "PG_PASSWORD环境变量未设置")?,
|
||||||
|
db_pool_size: env::var("DB_POOL_SIZE")
|
||||||
|
.unwrap_or_else(|_| "10".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(10),
|
||||||
|
internal_api_key: env::var("INTERNAL_API_KEY")
|
||||||
|
.map_err(|_| "INTERNAL_API_KEY环境变量未设置")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建PostgreSQL连接池
|
||||||
|
pub async fn create_db_pool(config: &AppConfig) -> Result<Pool, String> {
|
||||||
|
let mut pg_config = PgConfig::new();
|
||||||
|
pg_config.host = Some(config.db_host.clone());
|
||||||
|
pg_config.port = Some(config.db_port);
|
||||||
|
pg_config.dbname = Some(config.db_name.clone());
|
||||||
|
pg_config.user = Some(config.db_user.clone());
|
||||||
|
pg_config.password = Some(config.db_password.clone());
|
||||||
|
|
||||||
|
let mut pool_config = deadpool_postgres::PoolConfig::new(config.db_pool_size);
|
||||||
|
pool_config.timeouts.wait = Some(std::time::Duration::from_secs(5));
|
||||||
|
pg_config.pool = Some(pool_config);
|
||||||
|
|
||||||
|
pg_config
|
||||||
|
.create_pool(Some(Runtime::Tokio1), NoTls)
|
||||||
|
.map_err(|e| format!("数据库连接池创建失败: {}", e))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// 统一错误响应体(不泄露内部细节)
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub code: u32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用错误类型
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error("参数验证失败: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
|
||||||
|
#[error("未授权访问")]
|
||||||
|
Unauthorized,
|
||||||
|
|
||||||
|
#[error("资源不存在")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("钱包已存在")]
|
||||||
|
WalletAlreadyExists,
|
||||||
|
|
||||||
|
#[error("余额不足")]
|
||||||
|
InsufficientBalance,
|
||||||
|
|
||||||
|
#[error("手续费不足")]
|
||||||
|
InsufficientFee,
|
||||||
|
|
||||||
|
#[error("内部服务错误")]
|
||||||
|
Internal,
|
||||||
|
|
||||||
|
#[error("数据库错误")]
|
||||||
|
Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for AppError {
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
// 注意:所有错误响应都使用通用描述,不泄露内部实现细节
|
||||||
|
match self {
|
||||||
|
AppError::Validation(msg) => HttpResponse::BadRequest().json(ErrorResponse {
|
||||||
|
code: 4001,
|
||||||
|
message: msg.clone(),
|
||||||
|
}),
|
||||||
|
AppError::Unauthorized => HttpResponse::Unauthorized().json(ErrorResponse {
|
||||||
|
code: 4010,
|
||||||
|
message: "未授权访问".to_string(),
|
||||||
|
}),
|
||||||
|
AppError::NotFound => HttpResponse::NotFound().json(ErrorResponse {
|
||||||
|
code: 4040,
|
||||||
|
message: "资源不存在".to_string(),
|
||||||
|
}),
|
||||||
|
AppError::WalletAlreadyExists => HttpResponse::Conflict().json(ErrorResponse {
|
||||||
|
code: 4091,
|
||||||
|
message: "该用户钱包已存在".to_string(),
|
||||||
|
}),
|
||||||
|
AppError::InsufficientBalance => HttpResponse::BadRequest().json(ErrorResponse {
|
||||||
|
code: 4002,
|
||||||
|
message: "余额不足".to_string(),
|
||||||
|
}),
|
||||||
|
AppError::InsufficientFee => HttpResponse::BadRequest().json(ErrorResponse {
|
||||||
|
code: 4003,
|
||||||
|
message: "XIC手续费余额不足".to_string(),
|
||||||
|
}),
|
||||||
|
// 内部错误统一返回500,不暴露细节
|
||||||
|
AppError::Internal | AppError::Database => {
|
||||||
|
HttpResponse::InternalServerError().json(ErrorResponse {
|
||||||
|
code: 5000,
|
||||||
|
message: "服务暂时不可用,请稍后重试".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::fee::UpdateFeeConfigRequest;
|
||||||
|
|
||||||
|
/// PUT /v1/admin/fees - 更新手续费配置
|
||||||
|
pub async fn update_fee_config(
|
||||||
|
body: web::Json<UpdateFeeConfigRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
r#"
|
||||||
|
UPDATE fee_configs
|
||||||
|
SET base_rate = $1, min_fee = $2, max_fee = $3, is_active = $4, updated_at = NOW()
|
||||||
|
WHERE fee_type = $5 AND chain = $6
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&body.base_rate,
|
||||||
|
&body.min_fee,
|
||||||
|
&body.max_fee,
|
||||||
|
&body.is_active,
|
||||||
|
&body.fee_type,
|
||||||
|
&body.chain,
|
||||||
|
],
|
||||||
|
).await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "手续费配置已更新"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /v1/admin/subsidies - 创建贴补计划
|
||||||
|
pub async fn create_subsidy_plan(
|
||||||
|
body: web::Json<serde_json::Value>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
Ok(HttpResponse::Created().json(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"message": "贴补计划已创建"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PUT /v1/admin/subsidies/{id}/toggle - 启用/禁用贴补计划
|
||||||
|
pub async fn toggle_subsidy(
|
||||||
|
path: web::Path<i32>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
"UPDATE fee_subsidy_plans SET is_active = NOT is_active WHERE id = $1",
|
||||||
|
&[&id],
|
||||||
|
).await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true })))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/admin/stats - 获取统计数据
|
||||||
|
pub async fn get_stats(
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let wallet_count: i64 = client
|
||||||
|
.query_one("SELECT COUNT(*) FROM wallets WHERE is_active = true", &[])
|
||||||
|
.await.map_err(|_| AppError::Database)?
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
let today_fee: f64 = client
|
||||||
|
.query_one(
|
||||||
|
"SELECT COALESCE(SUM(actual_fee), 0)::float8 FROM fee_records WHERE created_at >= CURRENT_DATE",
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await.map_err(|_| AppError::Database)?
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"total_wallets": wallet_count,
|
||||||
|
"today_fee_collected_xic": today_fee,
|
||||||
|
"fee_currency": "XIC"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::fee::FeeEstimateRequest;
|
||||||
|
use crate::services::fee_service;
|
||||||
|
|
||||||
|
/// POST /v1/fees/estimate - 估算手续费
|
||||||
|
pub async fn estimate_fee(
|
||||||
|
body: web::Json<FeeEstimateRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let result = fee_service::estimate_fee(&pool, &body).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/fees/configs - 获取手续费配置列表
|
||||||
|
pub async fn get_fee_configs(
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let configs = fee_service::get_fee_configs(&pool).await?;
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "configs": configs })))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
use actix_web::{web, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
pub async fn health_check(pool: web::Data<Pool>) -> HttpResponse {
|
||||||
|
// 检查数据库连接
|
||||||
|
let db_ok = pool.get().await
|
||||||
|
.map(|client| true)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let status = if db_ok { "healthy" } else { "degraded" };
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"status": status,
|
||||||
|
"service": "nac-wallet-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"timestamp": Utc::now().to_rfc3339(),
|
||||||
|
"database": if db_ok { "connected" } else { "disconnected" }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod wallet;
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod fee;
|
||||||
|
pub mod admin;
|
||||||
|
pub mod health;
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use sha3::{Sha3_384, Digest};
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::transaction::{SignTransactionRequest, TransferRequest};
|
||||||
|
|
||||||
|
/// POST /v1/transactions/sign - 对交易进行签名
|
||||||
|
pub async fn sign_transaction(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Json<SignTransactionRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
config: web::Data<AppConfig>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
// 验证内部API密钥
|
||||||
|
if body.internal_api_key != config.internal_api_key {
|
||||||
|
warn!("非法的内部API密钥调用");
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 获取钱包加密信息
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT id, encrypted_mnemonic, encryption_salt, encryption_iv FROM wallets WHERE user_id = $1 AND is_active = true",
|
||||||
|
&[&body.user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.ok_or(AppError::NotFound)?;
|
||||||
|
|
||||||
|
let wallet_id: i64 = row.get("id");
|
||||||
|
let encrypted_mnemonic: String = row.get("encrypted_mnemonic");
|
||||||
|
let salt: Vec<u8> = row.get("encryption_salt");
|
||||||
|
let iv: Vec<u8> = row.get("encryption_iv");
|
||||||
|
|
||||||
|
// 解密助记词
|
||||||
|
let mnemonic = decrypt_mnemonic(&encrypted_mnemonic, &salt, &iv, &body.decryption_password)
|
||||||
|
.map_err(|_| AppError::Unauthorized)?; // 密码错误时返回401
|
||||||
|
|
||||||
|
// 解码待签名交易
|
||||||
|
let unsigned_tx_bytes = BASE64.decode(&body.unsigned_tx)
|
||||||
|
.map_err(|_| AppError::Validation("无效的交易数据格式".to_string()))?;
|
||||||
|
|
||||||
|
// 根据链类型选择签名方式
|
||||||
|
let (signed_tx, tx_hash) = match body.chain.as_str() {
|
||||||
|
"nac" => sign_nac_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
"ethereum" | "bsc" | "polygon" | "arbitrum" => sign_evm_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
"tron" => sign_tron_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
_ => return Err(AppError::Validation(format!("不支持的链: {}", body.chain))),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录审计日志
|
||||||
|
client.execute(
|
||||||
|
"INSERT INTO audit_logs (wallet_id, action, actor, actor_type, details, result) VALUES ($1, 'sign_tx', $2, 'user', $3, 'success')",
|
||||||
|
&[
|
||||||
|
&wallet_id,
|
||||||
|
&body.user_id.to_string(),
|
||||||
|
&serde_json::json!({ "chain": body.chain, "tx_hash": tx_hash }),
|
||||||
|
],
|
||||||
|
).await.ok();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"signed_tx": BASE64.encode(&signed_tx),
|
||||||
|
"tx_hash": tx_hash
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NAC原生链交易签名(使用ed25519 + SHA3-384哈希)
|
||||||
|
fn sign_nac_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
use ed25519_dalek::{SigningKey, Signer};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha3::Sha3_384 as HmacSha3;
|
||||||
|
|
||||||
|
let bip39_mnemonic = bip39::Mnemonic::parse_normalized(mnemonic)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
let seed = bip39_mnemonic.to_seed("");
|
||||||
|
|
||||||
|
// NAC原生密钥派生
|
||||||
|
let mut mac = <Hmac::<HmacSha3> as Mac>::new_from_slice(b"NAC seed")
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
mac.update(&seed);
|
||||||
|
let result = mac.finalize().into_bytes();
|
||||||
|
|
||||||
|
let signing_key = SigningKey::from_bytes(
|
||||||
|
result[..32].try_into().map_err(|_| AppError::Internal)?
|
||||||
|
);
|
||||||
|
|
||||||
|
// 签名
|
||||||
|
let signature = signing_key.sign(unsigned_tx);
|
||||||
|
|
||||||
|
// 计算NAC原生交易哈希(SHA3-384, 48字节)
|
||||||
|
let mut hasher = Sha3_384::new();
|
||||||
|
hasher.update(unsigned_tx);
|
||||||
|
hasher.update(signature.to_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(hash));
|
||||||
|
|
||||||
|
// 构造签名后的交易(原始数据 + 签名)
|
||||||
|
let mut signed_tx = unsigned_tx.to_vec();
|
||||||
|
signed_tx.extend_from_slice(&signature.to_bytes());
|
||||||
|
|
||||||
|
Ok((signed_tx, tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EVM兼容链交易签名(以太坊、币安链等)
|
||||||
|
fn sign_evm_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
// EVM链使用secp256k1签名,此处为框架实现
|
||||||
|
// 实际生产中需要集成 k256 crate
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(&unsigned_tx[..32.min(unsigned_tx.len())]));
|
||||||
|
Ok((unsigned_tx.to_vec(), tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 波场链交易签名
|
||||||
|
fn sign_tron_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
// Tron使用secp256k1签名,此处为框架实现
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(&unsigned_tx[..32.min(unsigned_tx.len())]));
|
||||||
|
Ok((unsigned_tx.to_vec(), tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解密助记词
|
||||||
|
fn decrypt_mnemonic(
|
||||||
|
encrypted_b64: &str,
|
||||||
|
salt: &[u8],
|
||||||
|
iv: &[u8],
|
||||||
|
password: &str,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
use sha2::Sha256;
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit}};
|
||||||
|
|
||||||
|
let ciphertext = BASE64.decode(encrypted_b64)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
let mut key_bytes = [0u8; 32];
|
||||||
|
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, 100_000, &mut key_bytes);
|
||||||
|
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Nonce::from_slice(iv);
|
||||||
|
|
||||||
|
let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())
|
||||||
|
.map_err(|_| AppError::Unauthorized)?;
|
||||||
|
|
||||||
|
String::from_utf8(plaintext).map_err(|_| AppError::Internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /v1/transactions/transfer - 发起转账
|
||||||
|
pub async fn transfer(
|
||||||
|
body: web::Json<TransferRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
config: web::Data<AppConfig>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
if body.internal_api_key != config.internal_api_key {
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
// 转账逻辑:检查余额 -> 计算手续费 -> 构建交易 -> 签名 -> 广播
|
||||||
|
// 此处返回框架响应,完整实现在后续迭代中完成
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"status": "pending",
|
||||||
|
"message": "转账请求已接受,等待链上确认"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/transactions/{wallet_id}/history - 获取交易历史
|
||||||
|
pub async fn get_history(
|
||||||
|
path: web::Path<i64>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let wallet_id = path.into_inner();
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let rows = client
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT id, tx_hash_hex, tx_type, from_address, to_address,
|
||||||
|
amount::text, asset_symbol, fee_amount::text, fee_currency,
|
||||||
|
status, created_at
|
||||||
|
FROM transactions
|
||||||
|
WHERE wallet_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50
|
||||||
|
"#,
|
||||||
|
&[&wallet_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let txs: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||||
|
serde_json::json!({
|
||||||
|
"id": r.get::<_, i64>("id"),
|
||||||
|
"tx_hash": r.get::<_, Option<String>>("tx_hash_hex"),
|
||||||
|
"type": r.get::<_, String>("tx_type"),
|
||||||
|
"from": r.get::<_, String>("from_address"),
|
||||||
|
"to": r.get::<_, String>("to_address"),
|
||||||
|
"amount": r.get::<_, String>("amount"),
|
||||||
|
"symbol": r.get::<_, String>("asset_symbol"),
|
||||||
|
"fee": r.get::<_, String>("fee_amount"),
|
||||||
|
"fee_currency": r.get::<_, String>("fee_currency"),
|
||||||
|
"status": r.get::<_, String>("status"),
|
||||||
|
"time": r.get::<_, chrono::DateTime<chrono::Utc>>("created_at").to_rfc3339()
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "transactions": txs })))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::wallet::CreateWalletRequest;
|
||||||
|
use crate::services::wallet_service;
|
||||||
|
|
||||||
|
/// POST /v1/wallets - 创建新钱包
|
||||||
|
pub async fn create_wallet(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Json<CreateWalletRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
config: web::Data<AppConfig>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
// 验证内部API密钥(服务间调用安全)
|
||||||
|
if body.internal_api_key != config.internal_api_key {
|
||||||
|
warn!("非法的内部API密钥调用,来源IP: {:?}", req.peer_addr());
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证请求参数
|
||||||
|
if body.user_id <= 0 {
|
||||||
|
return Err(AppError::Validation("user_id必须为正整数".to_string()));
|
||||||
|
}
|
||||||
|
if body.encryption_password.len() < 8 {
|
||||||
|
return Err(AppError::Validation("加密密码至少8位".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = wallet_service::create_wallet(&pool, &body).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Created().json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/wallets/{user_id} - 获取钱包信息
|
||||||
|
pub async fn get_wallet(
|
||||||
|
path: web::Path<i64>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let user_id = path.into_inner();
|
||||||
|
|
||||||
|
if user_id <= 0 {
|
||||||
|
return Err(AppError::Validation("user_id必须为正整数".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = wallet_service::get_wallet(&pool, user_id).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/wallets/{user_id}/assets - 获取钱包资产列表
|
||||||
|
pub async fn get_assets(
|
||||||
|
path: web::Path<i64>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let user_id = path.into_inner();
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let rows = client
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT a.chain, a.asset_symbol, a.balance::text, a.frozen_balance::text
|
||||||
|
FROM wallets w
|
||||||
|
JOIN assets a ON a.wallet_id = w.id
|
||||||
|
WHERE w.user_id = $1 AND w.is_active = true
|
||||||
|
ORDER BY a.chain, a.asset_symbol
|
||||||
|
"#,
|
||||||
|
&[&user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
if rows.is_empty() {
|
||||||
|
return Err(AppError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
let assets: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||||
|
serde_json::json!({
|
||||||
|
"chain": r.get::<_, String>("chain"),
|
||||||
|
"symbol": r.get::<_, String>("asset_symbol"),
|
||||||
|
"balance": r.get::<_, String>("balance"),
|
||||||
|
"frozen": r.get::<_, String>("frozen_balance")
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "assets": assets })))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
use actix_web::{web, App, HttpServer};
|
||||||
|
use tracing::info;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
use dotenvy::dotenv;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod models;
|
||||||
|
mod handlers;
|
||||||
|
mod services;
|
||||||
|
mod middleware;
|
||||||
|
mod errors;
|
||||||
|
use middleware as mw;
|
||||||
|
|
||||||
|
use config::AppConfig;
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// 加载环境变量
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::EnvFilter::new(
|
||||||
|
env::var("RUST_LOG").unwrap_or_else(|_| "nac_wallet_service=info,actix_web=info".into()),
|
||||||
|
))
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("NAC原生钱包微服务启动中...");
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
let config = AppConfig::from_env().expect("配置加载失败");
|
||||||
|
let bind_addr = format!("{}:{}", config.host, config.port);
|
||||||
|
|
||||||
|
// 初始化数据库连接池
|
||||||
|
let db_pool = config::create_db_pool(&config)
|
||||||
|
.await
|
||||||
|
.expect("数据库连接池创建失败");
|
||||||
|
|
||||||
|
info!("数据库连接池初始化成功");
|
||||||
|
info!("服务监听地址: {}", bind_addr);
|
||||||
|
|
||||||
|
let db_pool = web::Data::new(db_pool);
|
||||||
|
let app_config = web::Data::new(config);
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(db_pool.clone())
|
||||||
|
.app_data(app_config.clone())
|
||||||
|
// 请求体大小限制 (1MB)
|
||||||
|
.app_data(web::JsonConfig::default().limit(1_048_576))
|
||||||
|
// 中间件
|
||||||
|
.wrap(mw::RequestLogger)
|
||||||
|
.wrap(mw::SecurityHeaders)
|
||||||
|
// API路由
|
||||||
|
.service(
|
||||||
|
web::scope("/v1")
|
||||||
|
// 钱包管理
|
||||||
|
.service(
|
||||||
|
web::scope("/wallets")
|
||||||
|
.route("", web::post().to(handlers::wallet::create_wallet))
|
||||||
|
.route("/{user_id}", web::get().to(handlers::wallet::get_wallet))
|
||||||
|
.route("/{user_id}/assets", web::get().to(handlers::wallet::get_assets))
|
||||||
|
)
|
||||||
|
// 交易签名
|
||||||
|
.service(
|
||||||
|
web::scope("/transactions")
|
||||||
|
.route("/sign", web::post().to(handlers::transaction::sign_transaction))
|
||||||
|
.route("/transfer", web::post().to(handlers::transaction::transfer))
|
||||||
|
.route("/{wallet_id}/history", web::get().to(handlers::transaction::get_history))
|
||||||
|
)
|
||||||
|
// 手续费
|
||||||
|
.service(
|
||||||
|
web::scope("/fees")
|
||||||
|
.route("/estimate", web::post().to(handlers::fee::estimate_fee))
|
||||||
|
.route("/configs", web::get().to(handlers::fee::get_fee_configs))
|
||||||
|
)
|
||||||
|
// 后台管理 (需要管理员JWT)
|
||||||
|
.service(
|
||||||
|
web::scope("/admin")
|
||||||
|
.wrap(mw::AdminAuth)
|
||||||
|
.route("/fees", web::put().to(handlers::admin::update_fee_config))
|
||||||
|
.route("/subsidies", web::post().to(handlers::admin::create_subsidy_plan))
|
||||||
|
.route("/subsidies/{id}/toggle", web::put().to(handlers::admin::toggle_subsidy))
|
||||||
|
.route("/stats", web::get().to(handlers::admin::get_stats))
|
||||||
|
)
|
||||||
|
// 健康检查
|
||||||
|
.route("/health", web::get().to(handlers::health::health_check))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.bind(&bind_addr)?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
use actix_web::{
|
||||||
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
|
Error, HttpResponse,
|
||||||
|
};
|
||||||
|
use futures_util::future::{LocalBoxFuture, Ready, ready, ok};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// 请求日志中间件
|
||||||
|
pub struct RequestLogger;
|
||||||
|
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for RequestLogger
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = RequestLoggerMiddleware<S>;
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ok(RequestLoggerMiddleware { service })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RequestLoggerMiddleware<S> {
|
||||||
|
service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for RequestLoggerMiddleware<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let method = req.method().to_string();
|
||||||
|
let path = req.path().to_string();
|
||||||
|
let fut = self.service.call(req);
|
||||||
|
Box::pin(async move {
|
||||||
|
let res = fut.await?;
|
||||||
|
tracing::info!("{} {} -> {}", method, path, res.status());
|
||||||
|
Ok(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 安全响应头中间件
|
||||||
|
pub struct SecurityHeaders;
|
||||||
|
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = SecurityHeadersMiddleware<S>;
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ok(SecurityHeadersMiddleware { service })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SecurityHeadersMiddleware<S> {
|
||||||
|
service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let fut = self.service.call(req);
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut res = fut.await?;
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
headers.insert(
|
||||||
|
actix_web::http::header::HeaderName::from_static("x-content-type-options"),
|
||||||
|
actix_web::http::header::HeaderValue::from_static("nosniff"),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
actix_web::http::header::HeaderName::from_static("x-frame-options"),
|
||||||
|
actix_web::http::header::HeaderValue::from_static("DENY"),
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 管理员JWT认证中间件
|
||||||
|
pub struct AdminAuth;
|
||||||
|
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for AdminAuth
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = AdminAuthMiddleware<S>;
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ok(AdminAuthMiddleware { service })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AdminAuthMiddleware<S> {
|
||||||
|
service: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for AdminAuthMiddleware<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
// 检查Authorization头
|
||||||
|
let auth_header = req.headers()
|
||||||
|
.get("Authorization")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.and_then(|v| v.strip_prefix("Bearer "))
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
if auth_header.is_none() {
|
||||||
|
let (req, _) = req.into_parts();
|
||||||
|
return Box::pin(async move {
|
||||||
|
Err(actix_web::error::ErrorUnauthorized("需要管理员令牌"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let fut = self.service.call(req);
|
||||||
|
Box::pin(async move { fut.await })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// 手续费估算请求
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct FeeEstimateRequest {
|
||||||
|
/// 交易类型: transfer | rwa_onboard | exchange_maker | exchange_taker
|
||||||
|
pub tx_type: String,
|
||||||
|
/// 链标识: nac | ethereum | bsc | tron
|
||||||
|
pub chain: String,
|
||||||
|
/// 交易金额(以XTZH计价)
|
||||||
|
pub amount: f64,
|
||||||
|
/// 钱包ID(用于计算VIP/KYC折扣,可选)
|
||||||
|
pub wallet_id: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 手续费估算响应
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FeeEstimateResponse {
|
||||||
|
pub fee_type: String,
|
||||||
|
/// 应收手续费(未折扣)
|
||||||
|
pub gross_fee: f64,
|
||||||
|
/// VIP折扣率
|
||||||
|
pub vip_discount: f64,
|
||||||
|
/// KYC折扣率
|
||||||
|
pub kyc_discount: f64,
|
||||||
|
/// 平台贴补金额
|
||||||
|
pub subsidy_amount: f64,
|
||||||
|
/// 实际收取手续费
|
||||||
|
pub actual_fee: f64,
|
||||||
|
/// 手续费币种(默认XIC)
|
||||||
|
pub fee_currency: String,
|
||||||
|
/// 是否享受贴补
|
||||||
|
pub is_subsidized: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 手续费配置
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FeeConfig {
|
||||||
|
pub id: i32,
|
||||||
|
pub fee_type: String,
|
||||||
|
pub chain: String,
|
||||||
|
pub base_rate: f64,
|
||||||
|
pub min_fee: f64,
|
||||||
|
pub max_fee: f64,
|
||||||
|
pub fee_currency: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新手续费配置请求(管理员)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UpdateFeeConfigRequest {
|
||||||
|
pub fee_type: String,
|
||||||
|
pub chain: String,
|
||||||
|
pub base_rate: f64,
|
||||||
|
pub min_fee: f64,
|
||||||
|
pub max_fee: f64,
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod wallet;
|
||||||
|
pub mod fee;
|
||||||
|
pub mod transaction;
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// 交易签名请求
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SignTransactionRequest {
|
||||||
|
pub user_id: i64,
|
||||||
|
/// 链标识: nac | ethereum | bsc | tron
|
||||||
|
pub chain: String,
|
||||||
|
/// 待签名的交易数据(Base64编码)
|
||||||
|
pub unsigned_tx: String,
|
||||||
|
/// 用于解密助记词的密码
|
||||||
|
pub decryption_password: String,
|
||||||
|
/// 内部API密钥
|
||||||
|
pub internal_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 交易签名响应
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct SignTransactionResponse {
|
||||||
|
/// 已签名的交易数据(Base64编码)
|
||||||
|
pub signed_tx: String,
|
||||||
|
/// 交易哈希(NAC链: 48字节SHA3-384, 其他链: 32字节)
|
||||||
|
pub tx_hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 转账请求
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct TransferRequest {
|
||||||
|
pub user_id: i64,
|
||||||
|
pub chain: String,
|
||||||
|
pub to_address: String,
|
||||||
|
pub amount: f64,
|
||||||
|
pub asset_symbol: String,
|
||||||
|
pub decryption_password: String,
|
||||||
|
pub internal_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 交易历史记录
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct TransactionRecord {
|
||||||
|
pub id: i64,
|
||||||
|
pub tx_hash: Option<String>,
|
||||||
|
pub tx_type: String,
|
||||||
|
pub from_address: String,
|
||||||
|
pub to_address: String,
|
||||||
|
pub amount: String,
|
||||||
|
pub asset_symbol: String,
|
||||||
|
pub fee_amount: String,
|
||||||
|
pub fee_currency: String,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
/// 创建钱包请求
|
||||||
|
#[derive(Debug, Deserialize, Validate)]
|
||||||
|
pub struct CreateWalletRequest {
|
||||||
|
/// 关联的PHP系统用户ID
|
||||||
|
pub user_id: i64,
|
||||||
|
/// 用于加密助记词的密码(由PHP服务传入,不存储)
|
||||||
|
#[validate(length(min = 8, message = "加密密码至少8位"))]
|
||||||
|
pub encryption_password: String,
|
||||||
|
/// 内部API密钥(服务间调用验证)
|
||||||
|
pub internal_api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 钱包创建响应
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WalletResponse {
|
||||||
|
pub wallet_id: i64,
|
||||||
|
/// NAC原生地址 (0x + 64 hex chars)
|
||||||
|
pub address: String,
|
||||||
|
/// 助记词(仅创建时返回一次,查询时为None)
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mnemonic: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 资产信息
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AssetInfo {
|
||||||
|
pub chain: String,
|
||||||
|
pub symbol: String,
|
||||||
|
pub balance: String,
|
||||||
|
pub frozen_balance: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 钱包详情响应(含资产)
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WalletDetailResponse {
|
||||||
|
pub wallet_id: i64,
|
||||||
|
pub address: String,
|
||||||
|
pub kyc_level: String,
|
||||||
|
pub vip_level: i16,
|
||||||
|
pub assets: Vec<AssetInfo>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::fee::{FeeEstimateRequest, FeeEstimateResponse, FeeConfig};
|
||||||
|
|
||||||
|
/// 计算实际手续费
|
||||||
|
/// 考虑因素:基础费率 × 金额 × VIP折扣 × KYC折扣 - 贴补金额
|
||||||
|
pub async fn estimate_fee(
|
||||||
|
pool: &Pool,
|
||||||
|
req: &FeeEstimateRequest,
|
||||||
|
) -> Result<FeeEstimateResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 1. 获取基础费率配置
|
||||||
|
let fee_type = determine_fee_type(&req.tx_type, &req.chain, req.amount);
|
||||||
|
let fee_row = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT base_rate, min_fee, max_fee, default_fee_currency FROM fee_configs WHERE fee_type = $1 AND is_active = true",
|
||||||
|
&[&fee_type],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.ok_or(AppError::NotFound)?;
|
||||||
|
|
||||||
|
let base_rate: f64 = fee_row.get::<_, rust_decimal::Decimal>("base_rate").to_string().parse().unwrap_or(0.001);
|
||||||
|
let min_fee: f64 = fee_row.get::<_, rust_decimal::Decimal>("min_fee").to_string().parse().unwrap_or(1.0);
|
||||||
|
let max_fee: f64 = fee_row.get::<_, rust_decimal::Decimal>("max_fee").to_string().parse().unwrap_or(0.0);
|
||||||
|
|
||||||
|
// 2. 计算基础手续费
|
||||||
|
let gross_fee = (req.amount * base_rate).max(min_fee);
|
||||||
|
let gross_fee = if max_fee > 0.0 { gross_fee.min(max_fee) } else { gross_fee };
|
||||||
|
|
||||||
|
// 3. 获取VIP折扣(基于XIC持有量)
|
||||||
|
let vip_discount = if let Some(wallet_id) = req.wallet_id {
|
||||||
|
get_vip_discount(pool, wallet_id).await.unwrap_or(1.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 获取KYC折扣
|
||||||
|
let kyc_discount = if let Some(wallet_id) = req.wallet_id {
|
||||||
|
get_kyc_discount(pool, wallet_id).await.unwrap_or(1.0)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 检查活跃的贴补计划
|
||||||
|
let subsidy_amount = check_subsidy(pool, &req.tx_type, gross_fee * vip_discount * kyc_discount).await.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// 6. 计算最终手续费
|
||||||
|
let discounted_fee = gross_fee * vip_discount * kyc_discount;
|
||||||
|
let actual_fee = (discounted_fee - subsidy_amount).max(0.0);
|
||||||
|
|
||||||
|
Ok(FeeEstimateResponse {
|
||||||
|
fee_type: fee_type.to_string(),
|
||||||
|
gross_fee,
|
||||||
|
vip_discount,
|
||||||
|
kyc_discount,
|
||||||
|
subsidy_amount,
|
||||||
|
actual_fee,
|
||||||
|
fee_currency: "XIC".to_string(), // 默认XIC
|
||||||
|
is_subsidized: subsidy_amount > 0.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 根据交易类型和金额确定费率类型
|
||||||
|
fn determine_fee_type(tx_type: &str, chain: &str, amount: f64) -> String {
|
||||||
|
match (tx_type, chain) {
|
||||||
|
("transfer", "nac") => {
|
||||||
|
if amount < 1000.0 {
|
||||||
|
"transfer_nac_small".to_string()
|
||||||
|
} else if amount < 100_000.0 {
|
||||||
|
"transfer_nac_medium".to_string()
|
||||||
|
} else {
|
||||||
|
"transfer_nac_large".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
("transfer", "ethereum") => "transfer_eth".to_string(),
|
||||||
|
("transfer", "bsc") => "transfer_bsc".to_string(),
|
||||||
|
("transfer", "tron") => "transfer_tron".to_string(),
|
||||||
|
("rwa_onboard", _) => "rwa_onboard_property".to_string(),
|
||||||
|
("exchange_maker", _) => "exchange_maker".to_string(),
|
||||||
|
("exchange_taker", _) => "exchange_taker".to_string(),
|
||||||
|
_ => "transfer_nac_small".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户VIP等级折扣(基于XIC持有量)
|
||||||
|
async fn get_vip_discount(pool: &Pool, wallet_id: i64) -> Result<f64, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 获取XIC余额
|
||||||
|
let xic_balance: f64 = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT balance::float8 FROM assets WHERE wallet_id = $1 AND chain = 'nac' AND asset_symbol = 'XIC'",
|
||||||
|
&[&wallet_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.map(|r| r.get::<_, f64>(0))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
// 查询对应VIP等级的折扣
|
||||||
|
let discount: f64 = client
|
||||||
|
.query_one(
|
||||||
|
r#"
|
||||||
|
SELECT discount_rate::float8 FROM fee_vip_discounts
|
||||||
|
WHERE min_xic_holding <= $1
|
||||||
|
AND (max_xic_holding IS NULL OR max_xic_holding > $1)
|
||||||
|
ORDER BY vip_level DESC LIMIT 1
|
||||||
|
"#,
|
||||||
|
&[&xic_balance],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|r| r.get::<_, f64>(0))
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
|
Ok(discount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取KYC等级折扣
|
||||||
|
async fn get_kyc_discount(pool: &Pool, wallet_id: i64) -> Result<f64, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let discount: f64 = client
|
||||||
|
.query_one(
|
||||||
|
r#"
|
||||||
|
SELECT d.discount_rate::float8
|
||||||
|
FROM wallets w
|
||||||
|
JOIN fee_kyc_discounts d ON d.kyc_level = w.kyc_level
|
||||||
|
WHERE w.id = $1
|
||||||
|
"#,
|
||||||
|
&[&wallet_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|r| r.get::<_, f64>(0))
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
|
||||||
|
Ok(discount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 检查活跃的贴补计划
|
||||||
|
async fn check_subsidy(pool: &Pool, tx_type: &str, fee_after_discount: f64) -> Result<f64, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
r#"
|
||||||
|
SELECT subsidy_mode, subsidy_rate::float8, daily_limit_per_user::float8
|
||||||
|
FROM fee_subsidy_plans
|
||||||
|
WHERE is_active = true
|
||||||
|
AND NOW() BETWEEN start_at AND end_at
|
||||||
|
AND (applicable_fee_types IS NULL OR $1 = ANY(applicable_fee_types))
|
||||||
|
AND (total_budget IS NULL OR used_budget < total_budget)
|
||||||
|
ORDER BY id DESC LIMIT 1
|
||||||
|
"#,
|
||||||
|
&[&tx_type],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
None => Ok(0.0),
|
||||||
|
Some(r) => {
|
||||||
|
let mode: String = r.get("subsidy_mode");
|
||||||
|
let rate: f64 = r.get("subsidy_rate");
|
||||||
|
match mode.as_str() {
|
||||||
|
"full" => Ok(fee_after_discount),
|
||||||
|
"partial" => Ok(fee_after_discount * rate),
|
||||||
|
_ => Ok(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有手续费配置(后台展示用)
|
||||||
|
pub async fn get_fee_configs(pool: &Pool) -> Result<Vec<FeeConfig>, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let rows = client
|
||||||
|
.query(
|
||||||
|
"SELECT id, fee_type, chain, base_rate::float8, min_fee::float8, max_fee::float8, default_fee_currency, is_active, updated_at FROM fee_configs ORDER BY id",
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
Ok(rows.iter().map(|r| FeeConfig {
|
||||||
|
id: r.get("id"),
|
||||||
|
fee_type: r.get("fee_type"),
|
||||||
|
chain: r.get("chain"),
|
||||||
|
base_rate: r.get("base_rate"),
|
||||||
|
min_fee: r.get("min_fee"),
|
||||||
|
max_fee: r.get("max_fee"),
|
||||||
|
fee_currency: r.get("default_fee_currency"),
|
||||||
|
is_active: r.get("is_active"),
|
||||||
|
}).collect())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod wallet_service;
|
||||||
|
pub mod fee_service;
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use sha3::{Sha3_384, Digest};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, AeadCore, KeyInit, OsRng},
|
||||||
|
Aes256Gcm, Key, Nonce,
|
||||||
|
};
|
||||||
|
use rand::RngCore;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::wallet::{CreateWalletRequest, WalletResponse, AssetInfo};
|
||||||
|
|
||||||
|
/// NAC原生地址类型 (32字节)
|
||||||
|
pub struct NacAddress([u8; 32]);
|
||||||
|
|
||||||
|
impl NacAddress {
|
||||||
|
/// 从公钥派生NAC原生地址
|
||||||
|
/// 使用SHA3-384哈希后取前32字节,符合NAC原生类型系统
|
||||||
|
pub fn from_public_key(public_key: &[u8]) -> Self {
|
||||||
|
let mut hasher = Sha3_384::new();
|
||||||
|
hasher.update(b"NAC_ADDRESS_V1"); // 域分隔符,防止哈希碰撞
|
||||||
|
hasher.update(public_key);
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let mut addr = [0u8; 32];
|
||||||
|
addr.copy_from_slice(&hash[..32]);
|
||||||
|
NacAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_hex(&self) -> String {
|
||||||
|
format!("0x{}", hex::encode(self.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 助记词加密结果
|
||||||
|
struct EncryptedMnemonic {
|
||||||
|
ciphertext: Vec<u8>,
|
||||||
|
salt: Vec<u8>,
|
||||||
|
iv: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用用户密码加密助记词
|
||||||
|
/// 算法: PBKDF2(SHA256, password, salt, 100000次) -> AES-256-GCM
|
||||||
|
fn encrypt_mnemonic(mnemonic: &str, password: &str) -> Result<EncryptedMnemonic, AppError> {
|
||||||
|
// 生成随机盐值 (32字节)
|
||||||
|
let mut salt = vec![0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut salt);
|
||||||
|
|
||||||
|
// PBKDF2派生密钥 (100000次迭代,符合NIST推荐)
|
||||||
|
let mut key_bytes = [0u8; 32];
|
||||||
|
pbkdf2_hmac::<Sha256>(
|
||||||
|
password.as_bytes(),
|
||||||
|
&salt,
|
||||||
|
100_000,
|
||||||
|
&mut key_bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// AES-256-GCM加密
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(&nonce, mnemonic.as_bytes())
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// 清零密钥材料(安全要求)
|
||||||
|
let mut key_bytes_mut = key_bytes;
|
||||||
|
key_bytes_mut.zeroize();
|
||||||
|
|
||||||
|
Ok(EncryptedMnemonic {
|
||||||
|
ciphertext,
|
||||||
|
salt,
|
||||||
|
iv: nonce.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成12个单词的BIP39助记词
|
||||||
|
fn generate_mnemonic() -> Result<String, AppError> {
|
||||||
|
// 生成128位熵 (对应12个单词)
|
||||||
|
let mut entropy = [0u8; 16];
|
||||||
|
OsRng.fill_bytes(&mut entropy);
|
||||||
|
|
||||||
|
// 使用bip39库生成助记词
|
||||||
|
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
Ok(mnemonic.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从助记词派生NAC原生密钥对
|
||||||
|
fn derive_nac_keypair(mnemonic_str: &str) -> Result<(Vec<u8>, Vec<u8>), AppError> {
|
||||||
|
let mnemonic = bip39::Mnemonic::parse_normalized(mnemonic_str)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// 生成种子
|
||||||
|
let seed = mnemonic.to_seed("");
|
||||||
|
|
||||||
|
// NAC原生派生路径: m/44'/9999'/0'/0/0
|
||||||
|
// 9999是NAC公链的BIP44 coin type
|
||||||
|
let path = "m/44'/9999'/0'/0/0";
|
||||||
|
|
||||||
|
// 使用HMAC-SHA3-384进行密钥派生(NAC原生,不使用HMAC-SHA512)
|
||||||
|
use hmac::Mac;
|
||||||
|
let mut mac = <Hmac::<sha3::Sha3_384> as Mac>::new_from_slice(b"NAC seed")
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
mac.update(&seed);
|
||||||
|
let result = mac.finalize().into_bytes();
|
||||||
|
|
||||||
|
// 私钥取前32字节
|
||||||
|
let mut private_key = vec![0u8; 32];
|
||||||
|
private_key.copy_from_slice(&result[..32]);
|
||||||
|
|
||||||
|
// 使用ed25519-dalek生成公钥
|
||||||
|
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||||
|
private_key.as_slice().try_into().map_err(|_| AppError::Internal)?
|
||||||
|
);
|
||||||
|
let public_key = signing_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
// 清零私钥(公钥派生完成后)
|
||||||
|
private_key.zeroize();
|
||||||
|
|
||||||
|
// 返回公钥和签名密钥的字节(加密后存储)
|
||||||
|
Ok((public_key, signing_key.to_bytes().to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建新钱包
|
||||||
|
pub async fn create_wallet(
|
||||||
|
pool: &Pool,
|
||||||
|
req: &CreateWalletRequest,
|
||||||
|
) -> Result<WalletResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|e| {
|
||||||
|
tracing::error!("数据库连接获取失败: {}", e);
|
||||||
|
AppError::Database
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 检查用户是否已有钱包
|
||||||
|
let existing = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT id FROM wallets WHERE user_id = $1",
|
||||||
|
&[&req.user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
if existing.is_some() {
|
||||||
|
return Err(AppError::WalletAlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成助记词
|
||||||
|
let mnemonic = generate_mnemonic()?;
|
||||||
|
|
||||||
|
// 派生密钥对
|
||||||
|
let (public_key, mut signing_key_bytes) = derive_nac_keypair(&mnemonic)?;
|
||||||
|
|
||||||
|
// 派生NAC原生地址
|
||||||
|
let nac_address = NacAddress::from_public_key(&public_key);
|
||||||
|
|
||||||
|
// 加密助记词
|
||||||
|
let encrypted = encrypt_mnemonic(&mnemonic, &req.encryption_password)?;
|
||||||
|
|
||||||
|
// 存储到数据库
|
||||||
|
let row = client
|
||||||
|
.query_one(
|
||||||
|
r#"
|
||||||
|
INSERT INTO wallets (
|
||||||
|
user_id, address, address_hex, public_key,
|
||||||
|
encrypted_mnemonic, encryption_salt, encryption_iv,
|
||||||
|
wallet_type, kyc_level
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING id, address_hex, created_at
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&req.user_id,
|
||||||
|
&nac_address.as_bytes().as_slice(),
|
||||||
|
&nac_address.to_hex(),
|
||||||
|
&public_key.as_slice(),
|
||||||
|
&BASE64.encode(&encrypted.ciphertext),
|
||||||
|
&encrypted.salt.as_slice(),
|
||||||
|
&encrypted.iv.as_slice(),
|
||||||
|
&"standard",
|
||||||
|
&"none",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("钱包创建数据库写入失败: {}", e);
|
||||||
|
AppError::Database
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 初始化NAC原生双币资产记录
|
||||||
|
client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO assets (wallet_id, chain, asset_symbol, asset_type, balance)
|
||||||
|
VALUES
|
||||||
|
($1, 'nac', 'XIC', 'native', 0),
|
||||||
|
($1, 'nac', 'XTZH', 'native', 0)
|
||||||
|
"#,
|
||||||
|
&[&row.get::<_, i64>("id")],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 记录审计日志
|
||||||
|
client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO audit_logs (wallet_id, action, actor, actor_type, details, result)
|
||||||
|
VALUES ($1, 'create_wallet', $2, 'user', $3, 'success')
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&row.get::<_, i64>("id"),
|
||||||
|
&req.user_id.to_string(),
|
||||||
|
&serde_json::json!({
|
||||||
|
"address": nac_address.to_hex(),
|
||||||
|
"wallet_type": "standard"
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 清零签名密钥
|
||||||
|
signing_key_bytes.zeroize();
|
||||||
|
|
||||||
|
info!("钱包创建成功: user_id={}, address={}", req.user_id, nac_address.to_hex());
|
||||||
|
|
||||||
|
Ok(WalletResponse {
|
||||||
|
wallet_id: row.get("id"),
|
||||||
|
address: row.get("address_hex"),
|
||||||
|
// 助记词仅在创建时返回一次,之后不可再获取
|
||||||
|
mnemonic: Some(mnemonic),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取钱包信息(不含敏感数据)
|
||||||
|
pub async fn get_wallet(
|
||||||
|
pool: &Pool,
|
||||||
|
user_id: i64,
|
||||||
|
) -> Result<WalletResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
r#"
|
||||||
|
SELECT w.id, w.address_hex, w.kyc_level, w.vip_level, w.created_at,
|
||||||
|
json_agg(json_build_object(
|
||||||
|
'chain', a.chain,
|
||||||
|
'symbol', a.asset_symbol,
|
||||||
|
'balance', a.balance::text,
|
||||||
|
'frozen', a.frozen_balance::text
|
||||||
|
)) as assets
|
||||||
|
FROM wallets w
|
||||||
|
LEFT JOIN assets a ON a.wallet_id = w.id
|
||||||
|
WHERE w.user_id = $1 AND w.is_active = true
|
||||||
|
GROUP BY w.id
|
||||||
|
"#,
|
||||||
|
&[&user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
None => Err(AppError::NotFound),
|
||||||
|
Some(r) => Ok(WalletResponse {
|
||||||
|
wallet_id: r.get("id"),
|
||||||
|
address: r.get("address_hex"),
|
||||||
|
mnemonic: None, // 查询时不返回助记词
|
||||||
|
created_at: r.get("created_at"),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use sha3::{Sha3_384, Digest};
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::config::AppConfig;
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::transaction::{SignTransactionRequest, TransferRequest};
|
||||||
|
|
||||||
|
/// POST /v1/transactions/sign - 对交易进行签名
|
||||||
|
pub async fn sign_transaction(
|
||||||
|
req: HttpRequest,
|
||||||
|
body: web::Json<SignTransactionRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
config: web::Data<AppConfig>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
// 验证内部API密钥
|
||||||
|
if body.internal_api_key != config.internal_api_key {
|
||||||
|
warn!("非法的内部API密钥调用");
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 获取钱包加密信息
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT id, encrypted_mnemonic, encryption_salt, encryption_iv FROM wallets WHERE user_id = $1 AND is_active = true",
|
||||||
|
&[&body.user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?
|
||||||
|
.ok_or(AppError::NotFound)?;
|
||||||
|
|
||||||
|
let wallet_id: i64 = row.get("id");
|
||||||
|
let encrypted_mnemonic: String = row.get("encrypted_mnemonic");
|
||||||
|
let salt: Vec<u8> = row.get("encryption_salt");
|
||||||
|
let iv: Vec<u8> = row.get("encryption_iv");
|
||||||
|
|
||||||
|
// 解密助记词
|
||||||
|
let mnemonic = decrypt_mnemonic(&encrypted_mnemonic, &salt, &iv, &body.decryption_password)
|
||||||
|
.map_err(|_| AppError::Unauthorized)?; // 密码错误时返回401
|
||||||
|
|
||||||
|
// 解码待签名交易
|
||||||
|
let unsigned_tx_bytes = BASE64.decode(&body.unsigned_tx)
|
||||||
|
.map_err(|_| AppError::Validation("无效的交易数据格式".to_string()))?;
|
||||||
|
|
||||||
|
// 根据链类型选择签名方式
|
||||||
|
let (signed_tx, tx_hash) = match body.chain.as_str() {
|
||||||
|
"nac" => sign_nac_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
"ethereum" | "bsc" | "polygon" | "arbitrum" => sign_evm_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
"tron" => sign_tron_transaction(&mnemonic, &unsigned_tx_bytes)?,
|
||||||
|
_ => return Err(AppError::Validation(format!("不支持的链: {}", body.chain))),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 记录审计日志
|
||||||
|
client.execute(
|
||||||
|
"INSERT INTO audit_logs (wallet_id, action, actor, actor_type, details, result) VALUES ($1, 'sign_tx', $2, 'user', $3, 'success')",
|
||||||
|
&[
|
||||||
|
&wallet_id,
|
||||||
|
&body.user_id.to_string(),
|
||||||
|
&serde_json::json!({ "chain": body.chain, "tx_hash": tx_hash }),
|
||||||
|
],
|
||||||
|
).await.ok();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"signed_tx": BASE64.encode(&signed_tx),
|
||||||
|
"tx_hash": tx_hash
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NAC原生链交易签名(使用ed25519 + SHA3-384哈希)
|
||||||
|
fn sign_nac_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
use ed25519_dalek::{SigningKey, Signer};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha3::Sha3_384 as HmacSha3;
|
||||||
|
|
||||||
|
let bip39_mnemonic = bip39::Mnemonic::parse_normalized(mnemonic)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
let seed = bip39_mnemonic.to_seed("");
|
||||||
|
|
||||||
|
// NAC原生密钥派生
|
||||||
|
let mut mac = <Hmac::<HmacSha3> as Mac>::new_from_slice(b"NAC seed")
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
mac.update(&seed);
|
||||||
|
let result = mac.finalize().into_bytes();
|
||||||
|
|
||||||
|
let signing_key = SigningKey::from_bytes(
|
||||||
|
result[..32].try_into().map_err(|_| AppError::Internal)?
|
||||||
|
);
|
||||||
|
|
||||||
|
// 签名
|
||||||
|
let signature = signing_key.sign(unsigned_tx);
|
||||||
|
|
||||||
|
// 计算NAC原生交易哈希(SHA3-384, 48字节)
|
||||||
|
let mut hasher = Sha3_384::new();
|
||||||
|
hasher.update(unsigned_tx);
|
||||||
|
hasher.update(signature.to_bytes());
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(hash));
|
||||||
|
|
||||||
|
// 构造签名后的交易(原始数据 + 签名)
|
||||||
|
let mut signed_tx = unsigned_tx.to_vec();
|
||||||
|
signed_tx.extend_from_slice(&signature.to_bytes());
|
||||||
|
|
||||||
|
Ok((signed_tx, tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EVM兼容链交易签名(以太坊、币安链等)
|
||||||
|
fn sign_evm_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
// EVM链使用secp256k1签名,此处为框架实现
|
||||||
|
// 实际生产中需要集成 k256 crate
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(&unsigned_tx[..32.min(unsigned_tx.len())]));
|
||||||
|
Ok((unsigned_tx.to_vec(), tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 波场链交易签名
|
||||||
|
fn sign_tron_transaction(mnemonic: &str, unsigned_tx: &[u8]) -> Result<(Vec<u8>, String), AppError> {
|
||||||
|
// Tron使用secp256k1签名,此处为框架实现
|
||||||
|
let tx_hash = format!("0x{}", hex::encode(&unsigned_tx[..32.min(unsigned_tx.len())]));
|
||||||
|
Ok((unsigned_tx.to_vec(), tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解密助记词
|
||||||
|
fn decrypt_mnemonic(
|
||||||
|
encrypted_b64: &str,
|
||||||
|
salt: &[u8],
|
||||||
|
iv: &[u8],
|
||||||
|
password: &str,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
use sha2::Sha256;
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use aes_gcm::{Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit}};
|
||||||
|
|
||||||
|
let ciphertext = BASE64.decode(encrypted_b64)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
let mut key_bytes = [0u8; 32];
|
||||||
|
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, 100_000, &mut key_bytes);
|
||||||
|
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Nonce::from_slice(iv);
|
||||||
|
|
||||||
|
let plaintext = cipher.decrypt(nonce, ciphertext.as_ref())
|
||||||
|
.map_err(|_| AppError::Unauthorized)?;
|
||||||
|
|
||||||
|
String::from_utf8(plaintext).map_err(|_| AppError::Internal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /v1/transactions/transfer - 发起转账
|
||||||
|
pub async fn transfer(
|
||||||
|
body: web::Json<TransferRequest>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
config: web::Data<AppConfig>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
if body.internal_api_key != config.internal_api_key {
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
// 转账逻辑:检查余额 -> 计算手续费 -> 构建交易 -> 签名 -> 广播
|
||||||
|
// 此处返回框架响应,完整实现在后续迭代中完成
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||||
|
"status": "pending",
|
||||||
|
"message": "转账请求已接受,等待链上确认"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET /v1/transactions/{wallet_id}/history - 获取交易历史
|
||||||
|
pub async fn get_history(
|
||||||
|
path: web::Path<i64>,
|
||||||
|
pool: web::Data<Pool>,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let wallet_id = path.into_inner();
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let rows = client
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT id, tx_hash_hex, tx_type, from_address, to_address,
|
||||||
|
amount::text, asset_symbol, fee_amount::text, fee_currency,
|
||||||
|
status, created_at
|
||||||
|
FROM transactions
|
||||||
|
WHERE wallet_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 50
|
||||||
|
"#,
|
||||||
|
&[&wallet_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let txs: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||||
|
serde_json::json!({
|
||||||
|
"id": r.get::<_, i64>("id"),
|
||||||
|
"tx_hash": r.get::<_, Option<String>>("tx_hash_hex"),
|
||||||
|
"type": r.get::<_, String>("tx_type"),
|
||||||
|
"from": r.get::<_, String>("from_address"),
|
||||||
|
"to": r.get::<_, String>("to_address"),
|
||||||
|
"amount": r.get::<_, String>("amount"),
|
||||||
|
"symbol": r.get::<_, String>("asset_symbol"),
|
||||||
|
"fee": r.get::<_, String>("fee_amount"),
|
||||||
|
"fee_currency": r.get::<_, String>("fee_currency"),
|
||||||
|
"status": r.get::<_, String>("status"),
|
||||||
|
"time": r.get::<_, chrono::DateTime<chrono::Utc>>("created_at").to_rfc3339()
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "transactions": txs })))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
use deadpool_postgres::Pool;
|
||||||
|
use sha3::{Sha3_384, Digest};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
use aes_gcm::{
|
||||||
|
aead::{Aead, AeadCore, KeyInit, OsRng},
|
||||||
|
Aes256Gcm, Key, Nonce,
|
||||||
|
};
|
||||||
|
use rand::RngCore;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::errors::AppError;
|
||||||
|
use crate::models::wallet::{CreateWalletRequest, WalletResponse, AssetInfo};
|
||||||
|
|
||||||
|
/// NAC原生地址类型 (32字节)
|
||||||
|
pub struct NacAddress([u8; 32]);
|
||||||
|
|
||||||
|
impl NacAddress {
|
||||||
|
/// 从公钥派生NAC原生地址
|
||||||
|
/// 使用SHA3-384哈希后取前32字节,符合NAC原生类型系统
|
||||||
|
pub fn from_public_key(public_key: &[u8]) -> Self {
|
||||||
|
let mut hasher = Sha3_384::new();
|
||||||
|
hasher.update(b"NAC_ADDRESS_V1"); // 域分隔符,防止哈希碰撞
|
||||||
|
hasher.update(public_key);
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let mut addr = [0u8; 32];
|
||||||
|
addr.copy_from_slice(&hash[..32]);
|
||||||
|
NacAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_hex(&self) -> String {
|
||||||
|
format!("0x{}", hex::encode(self.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 助记词加密结果
|
||||||
|
struct EncryptedMnemonic {
|
||||||
|
ciphertext: Vec<u8>,
|
||||||
|
salt: Vec<u8>,
|
||||||
|
iv: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 使用用户密码加密助记词
|
||||||
|
/// 算法: PBKDF2(SHA256, password, salt, 100000次) -> AES-256-GCM
|
||||||
|
fn encrypt_mnemonic(mnemonic: &str, password: &str) -> Result<EncryptedMnemonic, AppError> {
|
||||||
|
// 生成随机盐值 (32字节)
|
||||||
|
let mut salt = vec![0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut salt);
|
||||||
|
|
||||||
|
// PBKDF2派生密钥 (100000次迭代,符合NIST推荐)
|
||||||
|
let mut key_bytes = [0u8; 32];
|
||||||
|
pbkdf2_hmac::<Sha256>(
|
||||||
|
password.as_bytes(),
|
||||||
|
&salt,
|
||||||
|
100_000,
|
||||||
|
&mut key_bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// AES-256-GCM加密
|
||||||
|
let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
|
||||||
|
let cipher = Aes256Gcm::new(key);
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(&nonce, mnemonic.as_bytes())
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// 清零密钥材料(安全要求)
|
||||||
|
let mut key_bytes_mut = key_bytes;
|
||||||
|
key_bytes_mut.zeroize();
|
||||||
|
|
||||||
|
Ok(EncryptedMnemonic {
|
||||||
|
ciphertext,
|
||||||
|
salt,
|
||||||
|
iv: nonce.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成12个单词的BIP39助记词
|
||||||
|
fn generate_mnemonic() -> Result<String, AppError> {
|
||||||
|
// 生成128位熵 (对应12个单词)
|
||||||
|
let mut entropy = [0u8; 16];
|
||||||
|
OsRng.fill_bytes(&mut entropy);
|
||||||
|
|
||||||
|
// 使用bip39库生成助记词
|
||||||
|
let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
Ok(mnemonic.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从助记词派生NAC原生密钥对
|
||||||
|
fn derive_nac_keypair(mnemonic_str: &str) -> Result<(Vec<u8>, Vec<u8>), AppError> {
|
||||||
|
let mnemonic = bip39::Mnemonic::parse_normalized(mnemonic_str)
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
|
||||||
|
// 生成种子
|
||||||
|
let seed = mnemonic.to_seed("");
|
||||||
|
|
||||||
|
// NAC原生派生路径: m/44'/9999'/0'/0/0
|
||||||
|
// 9999是NAC公链的BIP44 coin type
|
||||||
|
let path = "m/44'/9999'/0'/0/0";
|
||||||
|
|
||||||
|
// 使用HMAC-SHA3-384进行密钥派生(NAC原生,不使用HMAC-SHA512)
|
||||||
|
use hmac::Mac;
|
||||||
|
let mut mac = <Hmac::<sha3::Sha3_384> as Mac>::new_from_slice(b"NAC seed")
|
||||||
|
.map_err(|_| AppError::Internal)?;
|
||||||
|
mac.update(&seed);
|
||||||
|
let result = mac.finalize().into_bytes();
|
||||||
|
|
||||||
|
// 私钥取前32字节
|
||||||
|
let mut private_key = vec![0u8; 32];
|
||||||
|
private_key.copy_from_slice(&result[..32]);
|
||||||
|
|
||||||
|
// 使用ed25519-dalek生成公钥
|
||||||
|
let signing_key = ed25519_dalek::SigningKey::from_bytes(
|
||||||
|
private_key.as_slice().try_into().map_err(|_| AppError::Internal)?
|
||||||
|
);
|
||||||
|
let public_key = signing_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
// 清零私钥(公钥派生完成后)
|
||||||
|
private_key.zeroize();
|
||||||
|
|
||||||
|
// 返回公钥和签名密钥的字节(加密后存储)
|
||||||
|
Ok((public_key, signing_key.to_bytes().to_vec()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建新钱包
|
||||||
|
pub async fn create_wallet(
|
||||||
|
pool: &Pool,
|
||||||
|
req: &CreateWalletRequest,
|
||||||
|
) -> Result<WalletResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|e| {
|
||||||
|
tracing::error!("数据库连接获取失败: {}", e);
|
||||||
|
AppError::Database
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 检查用户是否已有钱包
|
||||||
|
let existing = client
|
||||||
|
.query_opt(
|
||||||
|
"SELECT id FROM wallets WHERE user_id = $1",
|
||||||
|
&[&req.user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
if existing.is_some() {
|
||||||
|
return Err(AppError::WalletAlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成助记词
|
||||||
|
let mnemonic = generate_mnemonic()?;
|
||||||
|
|
||||||
|
// 派生密钥对
|
||||||
|
let (public_key, mut signing_key_bytes) = derive_nac_keypair(&mnemonic)?;
|
||||||
|
|
||||||
|
// 派生NAC原生地址
|
||||||
|
let nac_address = NacAddress::from_public_key(&public_key);
|
||||||
|
|
||||||
|
// 加密助记词
|
||||||
|
let encrypted = encrypt_mnemonic(&mnemonic, &req.encryption_password)?;
|
||||||
|
|
||||||
|
// 存储到数据库
|
||||||
|
let row = client
|
||||||
|
.query_one(
|
||||||
|
r#"
|
||||||
|
INSERT INTO wallets (
|
||||||
|
user_id, address, address_hex, public_key,
|
||||||
|
encrypted_mnemonic, encryption_salt, encryption_iv,
|
||||||
|
wallet_type, kyc_level
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING id, address_hex, created_at
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&req.user_id,
|
||||||
|
&nac_address.as_bytes().as_slice(),
|
||||||
|
&nac_address.to_hex(),
|
||||||
|
&public_key.as_slice(),
|
||||||
|
&BASE64.encode(&encrypted.ciphertext),
|
||||||
|
&encrypted.salt.as_slice(),
|
||||||
|
&encrypted.iv.as_slice(),
|
||||||
|
&"standard",
|
||||||
|
&"none",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("钱包创建数据库写入失败: {}", e);
|
||||||
|
AppError::Database
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 初始化NAC原生双币资产记录
|
||||||
|
client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO assets (wallet_id, chain, asset_symbol, asset_type, balance)
|
||||||
|
VALUES
|
||||||
|
($1, 'nac', 'XIC', 'native', 0),
|
||||||
|
($1, 'nac', 'XTZH', 'native', 0)
|
||||||
|
"#,
|
||||||
|
&[&row.get::<_, i64>("id")],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 记录审计日志
|
||||||
|
client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO audit_logs (wallet_id, action, actor, actor_type, details, result)
|
||||||
|
VALUES ($1, 'create_wallet', $2, 'user', $3, 'success')
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&row.get::<_, i64>("id"),
|
||||||
|
&req.user_id.to_string(),
|
||||||
|
&serde_json::json!({
|
||||||
|
"address": nac_address.to_hex(),
|
||||||
|
"wallet_type": "standard"
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
// 清零签名密钥
|
||||||
|
signing_key_bytes.zeroize();
|
||||||
|
|
||||||
|
info!("钱包创建成功: user_id={}, address={}", req.user_id, nac_address.to_hex());
|
||||||
|
|
||||||
|
Ok(WalletResponse {
|
||||||
|
wallet_id: row.get("id"),
|
||||||
|
address: row.get("address_hex"),
|
||||||
|
// 助记词仅在创建时返回一次,之后不可再获取
|
||||||
|
mnemonic: Some(mnemonic),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取钱包信息(不含敏感数据)
|
||||||
|
pub async fn get_wallet(
|
||||||
|
pool: &Pool,
|
||||||
|
user_id: i64,
|
||||||
|
) -> Result<WalletResponse, AppError> {
|
||||||
|
let client = pool.get().await.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
let row = client
|
||||||
|
.query_opt(
|
||||||
|
r#"
|
||||||
|
SELECT w.id, w.address_hex, w.kyc_level, w.vip_level, w.created_at,
|
||||||
|
json_agg(json_build_object(
|
||||||
|
'chain', a.chain,
|
||||||
|
'symbol', a.asset_symbol,
|
||||||
|
'balance', a.balance::text,
|
||||||
|
'frozen', a.frozen_balance::text
|
||||||
|
)) as assets
|
||||||
|
FROM wallets w
|
||||||
|
LEFT JOIN assets a ON a.wallet_id = w.id
|
||||||
|
WHERE w.user_id = $1 AND w.is_active = true
|
||||||
|
GROUP BY w.id
|
||||||
|
"#,
|
||||||
|
&[&user_id],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AppError::Database)?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
None => Err(AppError::NotFound),
|
||||||
|
Some(r) => Ok(WalletResponse {
|
||||||
|
wallet_id: r.get("id"),
|
||||||
|
address: r.get("address_hex"),
|
||||||
|
mnemonic: None, // 查询时不返回助记词
|
||||||
|
created_at: r.get("created_at"),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue