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:
NAC Admin 2026-02-26 02:54:20 +08:00
parent bf60deb5d2
commit 1545d54159
22 changed files with 5414 additions and 0 deletions

View File

@ -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禁止unsafePHP使用PDO预处理所有密钥通过.env注入
---
## 六、未来扩展
本架构已为以下功能预留接口:
- 交易所模块XIC/USDT、XIC/USDC交易对
- 中间链和跨链桥
- 移动端App安卓/苹果)
- PC端域名化wallet.newassetchain.io
- 多语言支持i18n
---
## 七、关联工单
- 工单 #33:钱包后台管理系统
- 工单 #34VISION智能钱包
- 工单 #35:多链支持
- 工单 #37:一键上链关联

3282
nac_wallet_service/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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))
}

View File

@ -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(),
})
}
}
}
}

View File

@ -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"
})))
}

View File

@ -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 })))
}

View File

@ -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" }
}))
}

View File

@ -0,0 +1,5 @@
pub mod wallet;
pub mod transaction;
pub mod fee;
pub mod admin;
pub mod health;

View File

@ -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 })))
}

View File

@ -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 })))
}

View File

@ -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
}

View File

@ -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 })
}
}

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
pub mod wallet;
pub mod fee;
pub mod transaction;

View File

@ -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>,
}

View File

@ -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>,
}

View File

@ -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())
}

View File

@ -0,0 +1,2 @@
pub mod wallet_service;
pub mod fee_service;

View File

@ -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"),
}),
}
}

View File

@ -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 })))
}

View File

@ -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"),
}),
}
}