refactor(mobile-app): 重构账号创建流程,分离钱包获取逻辑
API 变更:
- POST /user/auto-create 快速返回 userSerialNum, username, avatarSvg
- GET /user/{userSerialNum}/wallet 异步获取钱包和助记词
AccountService:
- 移除 MPC share 本地加密逻辑
- 新增 getWalletInfo() 方法支持轮询获取钱包状态
- CreateAccountResponse 移除 userId,使用 userSerialNum
页面修改:
- onboarding_page: 适配新的创建账号响应
- backup_mnemonic_page: 进入时调用 API 获取钱包,支持 loading/error 状态
- verify_mnemonic_page: 字段从 serialNumber 改为 userSerialNum
- wallet_created_page: 同上
清理:
- 删除 mpc_share_service.dart
- 删除相关测试文件
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
289691dc3c
commit
20d82906f6
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
@ -6,7 +5,6 @@ import '../network/api_client.dart';
|
||||||
import '../storage/secure_storage.dart';
|
import '../storage/secure_storage.dart';
|
||||||
import '../storage/storage_keys.dart';
|
import '../storage/storage_keys.dart';
|
||||||
import '../errors/exceptions.dart';
|
import '../errors/exceptions.dart';
|
||||||
import 'mpc_share_service.dart';
|
|
||||||
|
|
||||||
/// 设备硬件信息 (存储在 deviceName 字段中)
|
/// 设备硬件信息 (存储在 deviceName 字段中)
|
||||||
class DeviceHardwareInfo {
|
class DeviceHardwareInfo {
|
||||||
|
|
@ -17,6 +15,7 @@ class DeviceHardwareInfo {
|
||||||
final String? product; // 产品名 (如 "venus_eea")
|
final String? product; // 产品名 (如 "venus_eea")
|
||||||
final String? hardware; // 硬件名 (如 "qcom")
|
final String? hardware; // 硬件名 (如 "qcom")
|
||||||
final String? osVersion; // 系统版本 (如 "13")
|
final String? osVersion; // 系统版本 (如 "13")
|
||||||
|
final String? platform; // 平台 (如 "android", "ios")
|
||||||
final int? sdkInt; // SDK 版本 (如 33)
|
final int? sdkInt; // SDK 版本 (如 33)
|
||||||
final bool? isPhysicalDevice; // 是否真机
|
final bool? isPhysicalDevice; // 是否真机
|
||||||
|
|
||||||
|
|
@ -28,6 +27,7 @@ class DeviceHardwareInfo {
|
||||||
this.product,
|
this.product,
|
||||||
this.hardware,
|
this.hardware,
|
||||||
this.osVersion,
|
this.osVersion,
|
||||||
|
this.platform,
|
||||||
this.sdkInt,
|
this.sdkInt,
|
||||||
this.isPhysicalDevice,
|
this.isPhysicalDevice,
|
||||||
});
|
});
|
||||||
|
|
@ -40,6 +40,7 @@ class DeviceHardwareInfo {
|
||||||
if (product != null) 'product': product,
|
if (product != null) 'product': product,
|
||||||
if (hardware != null) 'hardware': hardware,
|
if (hardware != null) 'hardware': hardware,
|
||||||
if (osVersion != null) 'osVersion': osVersion,
|
if (osVersion != null) 'osVersion': osVersion,
|
||||||
|
if (platform != null) 'platform': platform,
|
||||||
if (sdkInt != null) 'sdkInt': sdkInt,
|
if (sdkInt != null) 'sdkInt': sdkInt,
|
||||||
if (isPhysicalDevice != null) 'isPhysicalDevice': isPhysicalDevice,
|
if (isPhysicalDevice != null) 'isPhysicalDevice': isPhysicalDevice,
|
||||||
};
|
};
|
||||||
|
|
@ -65,46 +66,64 @@ class CreateAccountRequest {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建账号响应
|
/// 创建账号响应 (新版 - 不含钱包信息)
|
||||||
class CreateAccountResponse {
|
class CreateAccountResponse {
|
||||||
final String userId;
|
final int userSerialNum; // 用户序列号
|
||||||
final int accountSequence;
|
final String referralCode; // 推荐码
|
||||||
final String referralCode;
|
final String username; // 随机用户名
|
||||||
final String? mnemonic;
|
final String avatarSvg; // 随机 SVG 头像
|
||||||
final String? clientShareData; // MPC 客户端分片数据
|
|
||||||
final String? publicKey; // MPC 公钥
|
|
||||||
final WalletAddresses walletAddresses;
|
|
||||||
final String accessToken;
|
final String accessToken;
|
||||||
final String refreshToken;
|
final String refreshToken;
|
||||||
|
|
||||||
CreateAccountResponse({
|
CreateAccountResponse({
|
||||||
required this.userId,
|
required this.userSerialNum,
|
||||||
required this.accountSequence,
|
|
||||||
required this.referralCode,
|
required this.referralCode,
|
||||||
this.mnemonic,
|
required this.username,
|
||||||
this.clientShareData,
|
required this.avatarSvg,
|
||||||
this.publicKey,
|
|
||||||
required this.walletAddresses,
|
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.refreshToken,
|
required this.refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory CreateAccountResponse.fromJson(Map<String, dynamic> json) {
|
factory CreateAccountResponse.fromJson(Map<String, dynamic> json) {
|
||||||
return CreateAccountResponse(
|
return CreateAccountResponse(
|
||||||
userId: json['userId'] as String,
|
userSerialNum: json['userSerialNum'] as int,
|
||||||
accountSequence: json['accountSequence'] as int,
|
|
||||||
referralCode: json['referralCode'] as String,
|
referralCode: json['referralCode'] as String,
|
||||||
mnemonic: json['mnemonic'] as String?,
|
username: json['username'] as String,
|
||||||
clientShareData: json['clientShareData'] as String?,
|
avatarSvg: json['avatarSvg'] as String,
|
||||||
publicKey: json['publicKey'] as String?,
|
|
||||||
walletAddresses:
|
|
||||||
WalletAddresses.fromJson(json['walletAddresses'] as Map<String, dynamic>),
|
|
||||||
accessToken: json['accessToken'] as String,
|
accessToken: json['accessToken'] as String,
|
||||||
refreshToken: json['refreshToken'] as String,
|
refreshToken: json['refreshToken'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 钱包信息响应
|
||||||
|
class WalletInfoResponse {
|
||||||
|
final String status; // "generating" | "ready" | "failed"
|
||||||
|
final WalletAddresses? walletAddresses;
|
||||||
|
final String? mnemonic; // 12 词助记词
|
||||||
|
|
||||||
|
WalletInfoResponse({
|
||||||
|
required this.status,
|
||||||
|
this.walletAddresses,
|
||||||
|
this.mnemonic,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory WalletInfoResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return WalletInfoResponse(
|
||||||
|
status: json['status'] as String,
|
||||||
|
walletAddresses: json['walletAddresses'] != null
|
||||||
|
? WalletAddresses.fromJson(
|
||||||
|
json['walletAddresses'] as Map<String, dynamic>)
|
||||||
|
: null,
|
||||||
|
mnemonic: json['mnemonic'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isReady => status == 'ready';
|
||||||
|
bool get isGenerating => status == 'generating';
|
||||||
|
bool get isFailed => status == 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
/// 钱包地址
|
/// 钱包地址
|
||||||
class WalletAddresses {
|
class WalletAddresses {
|
||||||
final String kava;
|
final String kava;
|
||||||
|
|
@ -128,7 +147,7 @@ class WalletAddresses {
|
||||||
|
|
||||||
/// 账号服务
|
/// 账号服务
|
||||||
///
|
///
|
||||||
/// 处理账号创建、恢复等功能
|
/// 处理账号创建、钱包获取等功能
|
||||||
class AccountService {
|
class AccountService {
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
final SecureStorage _secureStorage;
|
final SecureStorage _secureStorage;
|
||||||
|
|
@ -175,6 +194,7 @@ class AccountService {
|
||||||
product: info.product,
|
product: info.product,
|
||||||
hardware: info.hardware,
|
hardware: info.hardware,
|
||||||
osVersion: info.version.release,
|
osVersion: info.version.release,
|
||||||
|
platform: 'android',
|
||||||
sdkInt: info.version.sdkInt,
|
sdkInt: info.version.sdkInt,
|
||||||
isPhysicalDevice: info.isPhysicalDevice,
|
isPhysicalDevice: info.isPhysicalDevice,
|
||||||
);
|
);
|
||||||
|
|
@ -186,6 +206,7 @@ class AccountService {
|
||||||
model: info.model,
|
model: info.model,
|
||||||
device: info.name,
|
device: info.name,
|
||||||
osVersion: info.systemVersion,
|
osVersion: info.systemVersion,
|
||||||
|
platform: 'ios',
|
||||||
isPhysicalDevice: info.isPhysicalDevice,
|
isPhysicalDevice: info.isPhysicalDevice,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -194,9 +215,9 @@ class AccountService {
|
||||||
return DeviceHardwareInfo();
|
return DeviceHardwareInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 自动创建账号 (首次打开APP)
|
/// 创建账号 (快速返回,不含钱包信息)
|
||||||
///
|
///
|
||||||
/// 使用 MPC 2-of-3 协议生成钱包地址
|
/// 钱包在后台异步生成,需要单独调用 getWalletInfo 获取
|
||||||
Future<CreateAccountResponse> createAccount({
|
Future<CreateAccountResponse> createAccount({
|
||||||
String? inviterReferralCode,
|
String? inviterReferralCode,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
@ -235,22 +256,59 @@ class AccountService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取钱包信息 (包含助记词)
|
||||||
|
///
|
||||||
|
/// 用 userSerialNum 查询钱包生成状态和助记词
|
||||||
|
Future<WalletInfoResponse> getWalletInfo(int userSerialNum) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get('/user/$userSerialNum/wallet');
|
||||||
|
|
||||||
|
if (response.data == null) {
|
||||||
|
throw const ApiException('获取钱包信息失败: 空响应');
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = WalletInfoResponse.fromJson(
|
||||||
|
response.data as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果钱包已就绪,保存钱包地址和助记词
|
||||||
|
if (result.isReady) {
|
||||||
|
await _saveWalletData(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} on ApiException {
|
||||||
|
rethrow;
|
||||||
|
} catch (e) {
|
||||||
|
throw ApiException('获取钱包信息失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 保存账号数据
|
/// 保存账号数据
|
||||||
Future<void> _saveAccountData(
|
Future<void> _saveAccountData(
|
||||||
CreateAccountResponse response,
|
CreateAccountResponse response,
|
||||||
String deviceId,
|
String deviceId,
|
||||||
) async {
|
) async {
|
||||||
// 保存基本信息
|
// 保存用户序列号
|
||||||
await _secureStorage.write(key: StorageKeys.userId, value: response.userId);
|
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.accountSequence,
|
key: StorageKeys.userSerialNum,
|
||||||
value: response.accountSequence.toString(),
|
value: response.userSerialNum.toString(),
|
||||||
);
|
);
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.referralCode,
|
key: StorageKeys.referralCode,
|
||||||
value: response.referralCode,
|
value: response.referralCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
await _secureStorage.write(
|
||||||
|
key: StorageKeys.username,
|
||||||
|
value: response.username,
|
||||||
|
);
|
||||||
|
await _secureStorage.write(
|
||||||
|
key: StorageKeys.avatarSvg,
|
||||||
|
value: response.avatarSvg,
|
||||||
|
);
|
||||||
|
|
||||||
// 保存 Token
|
// 保存 Token
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.accessToken,
|
key: StorageKeys.accessToken,
|
||||||
|
|
@ -261,89 +319,78 @@ class AccountService {
|
||||||
value: response.refreshToken,
|
value: response.refreshToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 保存钱包地址
|
// 保存设备 ID
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.walletAddressBsc,
|
key: StorageKeys.deviceId,
|
||||||
value: response.walletAddresses.bsc,
|
value: deviceId,
|
||||||
);
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.walletAddressKava,
|
|
||||||
value: response.walletAddresses.kava,
|
|
||||||
);
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.walletAddressDst,
|
|
||||||
value: response.walletAddresses.dst,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 保存 MPC 数据 (如果有)
|
// 标记账号已创建
|
||||||
if (response.clientShareData != null &&
|
await _secureStorage.write(
|
||||||
response.clientShareData!.isNotEmpty) {
|
key: StorageKeys.isAccountCreated,
|
||||||
|
value: 'true',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保存钱包数据
|
||||||
|
Future<void> _saveWalletData(WalletInfoResponse response) async {
|
||||||
|
if (response.walletAddresses != null) {
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.mpcClientShareData,
|
key: StorageKeys.walletAddressBsc,
|
||||||
value: response.clientShareData!,
|
value: response.walletAddresses!.bsc,
|
||||||
);
|
);
|
||||||
|
await _secureStorage.write(
|
||||||
|
key: StorageKeys.walletAddressKava,
|
||||||
|
value: response.walletAddresses!.kava,
|
||||||
|
);
|
||||||
|
await _secureStorage.write(
|
||||||
|
key: StorageKeys.walletAddressDst,
|
||||||
|
value: response.walletAddresses!.dst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 生成随机 12 词助记词并加密 share
|
if (response.mnemonic != null && response.mnemonic!.isNotEmpty) {
|
||||||
// 流程: 生成随机助记词 → PBKDF2 派生密钥 → AES 加密 share
|
|
||||||
final shareBackup = MpcShareService.createShareBackup(response.clientShareData!);
|
|
||||||
|
|
||||||
// 保存生成的助记词(用户需要备份的 12 词)
|
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.mnemonic,
|
key: StorageKeys.mnemonic,
|
||||||
value: shareBackup.mnemonic,
|
value: response.mnemonic!,
|
||||||
);
|
|
||||||
|
|
||||||
// 保存加密的 share 数据(密文 + IV + 认证标签)
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.mpcEncryptedShare,
|
|
||||||
value: shareBackup.encryptedShare,
|
|
||||||
);
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.mpcShareIv,
|
|
||||||
value: shareBackup.iv,
|
|
||||||
);
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.mpcShareAuthTag,
|
|
||||||
value: shareBackup.authTag,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.publicKey != null && response.publicKey!.isNotEmpty) {
|
// 标记钱包已就绪
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.mpcPublicKey,
|
|
||||||
value: response.publicKey!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果后端返回了传统助记词(非 MPC 模式),也保存
|
|
||||||
if (response.mnemonic != null && response.mnemonic!.isNotEmpty) {
|
|
||||||
// 检查是否已经有助记词(MPC 模式已生成)
|
|
||||||
final existingMnemonic = await _secureStorage.read(key: StorageKeys.mnemonic);
|
|
||||||
if (existingMnemonic == null || existingMnemonic.isEmpty) {
|
|
||||||
await _secureStorage.write(
|
|
||||||
key: StorageKeys.mnemonic,
|
|
||||||
value: response.mnemonic!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记钱包已创建
|
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
key: StorageKeys.isWalletCreated,
|
key: StorageKeys.isWalletReady,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查是否已创建账号
|
/// 检查是否已创建账号
|
||||||
Future<bool> hasAccount() async {
|
Future<bool> hasAccount() async {
|
||||||
final isCreated = await _secureStorage.read(key: StorageKeys.isWalletCreated);
|
final isCreated =
|
||||||
|
await _secureStorage.read(key: StorageKeys.isAccountCreated);
|
||||||
return isCreated == 'true';
|
return isCreated == 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取账号序列号
|
/// 检查钱包是否已就绪
|
||||||
Future<int?> getAccountSequence() async {
|
Future<bool> isWalletReady() async {
|
||||||
final sequence = await _secureStorage.read(key: StorageKeys.accountSequence);
|
final isReady = await _secureStorage.read(key: StorageKeys.isWalletReady);
|
||||||
return sequence != null ? int.tryParse(sequence) : null;
|
return isReady == 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户序列号
|
||||||
|
Future<int?> getUserSerialNum() async {
|
||||||
|
final serialNum =
|
||||||
|
await _secureStorage.read(key: StorageKeys.userSerialNum);
|
||||||
|
return serialNum != null ? int.tryParse(serialNum) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户名
|
||||||
|
Future<String?> getUsername() async {
|
||||||
|
return _secureStorage.read(key: StorageKeys.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取头像 SVG
|
||||||
|
Future<String?> getAvatarSvg() async {
|
||||||
|
return _secureStorage.read(key: StorageKeys.avatarSvg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取推荐码
|
/// 获取推荐码
|
||||||
|
|
@ -351,7 +398,7 @@ class AccountService {
|
||||||
return _secureStorage.read(key: StorageKeys.referralCode);
|
return _secureStorage.read(key: StorageKeys.referralCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取钱包地址
|
/// 获取钱包地址 (从本地存储)
|
||||||
Future<WalletAddresses?> getWalletAddresses() async {
|
Future<WalletAddresses?> getWalletAddresses() async {
|
||||||
final bsc = await _secureStorage.read(key: StorageKeys.walletAddressBsc);
|
final bsc = await _secureStorage.read(key: StorageKeys.walletAddressBsc);
|
||||||
final kava = await _secureStorage.read(key: StorageKeys.walletAddressKava);
|
final kava = await _secureStorage.read(key: StorageKeys.walletAddressKava);
|
||||||
|
|
@ -364,65 +411,11 @@ class AccountService {
|
||||||
return WalletAddresses(kava: kava, dst: dst, bsc: bsc);
|
return WalletAddresses(kava: kava, dst: dst, bsc: bsc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取备份助记词(12词)
|
/// 获取助记词 (从本地存储)
|
||||||
///
|
|
||||||
/// 返回从 MPC 客户端分片生成的助记词
|
|
||||||
Future<String?> getMnemonic() async {
|
Future<String?> getMnemonic() async {
|
||||||
return _secureStorage.read(key: StorageKeys.mnemonic);
|
return _secureStorage.read(key: StorageKeys.mnemonic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取 MPC 客户端分片数据
|
|
||||||
Future<String?> getMpcClientShareData() async {
|
|
||||||
return _secureStorage.read(key: StorageKeys.mpcClientShareData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证助记词是否能解密存储的 share
|
|
||||||
///
|
|
||||||
/// 尝试用输入的助记词解密,如果成功且与原始 share 匹配则返回 true
|
|
||||||
Future<bool> verifyMnemonic(String mnemonic) async {
|
|
||||||
try {
|
|
||||||
final recovered = await recoverShareFromMnemonic(mnemonic);
|
|
||||||
final original = await _secureStorage.read(key: StorageKeys.mpcClientShareData);
|
|
||||||
return recovered != null && recovered == original;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从助记词恢复 MPC 分片数据
|
|
||||||
///
|
|
||||||
/// 流程: 输入助记词 → PBKDF2 派生密钥 → AES 解密 → 获得原始 share
|
|
||||||
Future<String?> recoverShareFromMnemonic(String mnemonic) async {
|
|
||||||
// 验证助记词格式
|
|
||||||
if (!MpcShareService.validateMnemonic(mnemonic)) {
|
|
||||||
throw const ValidationException('助记词格式无效');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取加密数据
|
|
||||||
final encryptedShare = await _secureStorage.read(key: StorageKeys.mpcEncryptedShare);
|
|
||||||
final iv = await _secureStorage.read(key: StorageKeys.mpcShareIv);
|
|
||||||
final authTag = await _secureStorage.read(key: StorageKeys.mpcShareAuthTag);
|
|
||||||
|
|
||||||
if (encryptedShare == null || iv == null || authTag == null) {
|
|
||||||
// 本地没有加密 share 数据
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用助记词解密
|
|
||||||
final recoveredShare = MpcShareService.recoverShare(
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
encryptedShare: encryptedShare,
|
|
||||||
iv: iv,
|
|
||||||
authTag: authTag,
|
|
||||||
);
|
|
||||||
return recoveredShare;
|
|
||||||
} catch (e) {
|
|
||||||
// 解密失败(助记词错误或数据损坏)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 标记助记词已备份
|
/// 标记助记词已备份
|
||||||
Future<void> markMnemonicBackedUp() async {
|
Future<void> markMnemonicBackedUp() async {
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
|
|
@ -433,7 +426,8 @@ class AccountService {
|
||||||
|
|
||||||
/// 检查助记词是否已备份
|
/// 检查助记词是否已备份
|
||||||
Future<bool> isMnemonicBackedUp() async {
|
Future<bool> isMnemonicBackedUp() async {
|
||||||
final isBackedUp = await _secureStorage.read(key: StorageKeys.isMnemonicBackedUp);
|
final isBackedUp =
|
||||||
|
await _secureStorage.read(key: StorageKeys.isMnemonicBackedUp);
|
||||||
return isBackedUp == 'true';
|
return isBackedUp == 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,323 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
import 'package:bip39/bip39.dart' as bip39;
|
|
||||||
|
|
||||||
/// MPC Share 服务
|
|
||||||
///
|
|
||||||
/// 处理 MPC 客户端分片的加密备份与恢复
|
|
||||||
///
|
|
||||||
/// 设计原理 (基于 Binance tss-lib):
|
|
||||||
/// - MPC share 是 256 bit 的秘密标量 (secp256k1 曲线)
|
|
||||||
/// - 需要用户能够安全备份并完全恢复原始 share
|
|
||||||
///
|
|
||||||
/// 加密方案:
|
|
||||||
/// 1. 生成随机 128 bit entropy → 12 词 BIP39 助记词
|
|
||||||
/// 2. 使用 PBKDF2(mnemonic, salt, iterations=100000) 派生 256 bit 密钥
|
|
||||||
/// 3. 使用 AES-256-GCM 加密 share (带认证标签)
|
|
||||||
/// 4. 用户只需备份 12 词助记词
|
|
||||||
///
|
|
||||||
/// 恢复流程:
|
|
||||||
/// 1. 用户输入 12 词助记词
|
|
||||||
/// 2. PBKDF2 派生密钥
|
|
||||||
/// 3. AES-256-GCM 解密
|
|
||||||
/// 4. 获得原始 share
|
|
||||||
///
|
|
||||||
/// 参考:
|
|
||||||
/// - BIP39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
|
||||||
/// - tss-lib: https://github.com/bnb-chain/tss-lib
|
|
||||||
class MpcShareService {
|
|
||||||
/// PBKDF2 迭代次数 (安全性与性能平衡)
|
|
||||||
static const int _pbkdf2Iterations = 100000;
|
|
||||||
|
|
||||||
/// 盐值 (固定盐,也可以考虑使用随机盐并存储)
|
|
||||||
static const String _salt = 'rwa-durian-mpc-share-v1';
|
|
||||||
|
|
||||||
/// 生成 12 词 BIP39 助记词 (128 bit entropy)
|
|
||||||
///
|
|
||||||
/// 这个助记词用于加密 MPC share,用户需要安全备份
|
|
||||||
static String generateMnemonic() {
|
|
||||||
return bip39.generateMnemonic(strength: 128); // 128 bit → 12 words
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 创建 MPC Share 备份
|
|
||||||
///
|
|
||||||
/// 步骤:
|
|
||||||
/// 1. 生成随机 12 词助记词
|
|
||||||
/// 2. 从助记词派生加密密钥 (PBKDF2)
|
|
||||||
/// 3. 使用 AES-256 加密 share (这里用 XOR + HMAC 模拟,实际应用需用 pointycastle)
|
|
||||||
/// 4. 返回助记词和加密数据
|
|
||||||
///
|
|
||||||
/// [clientShareData] - MPC 客户端分片数据 (base64 编码的 256 bit 数据)
|
|
||||||
static MpcShareBackup createShareBackup(String clientShareData) {
|
|
||||||
if (clientShareData.isEmpty) {
|
|
||||||
throw ArgumentError('clientShareData cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: 生成随机助记词
|
|
||||||
final mnemonic = generateMnemonic();
|
|
||||||
|
|
||||||
// Step 2: 使用助记词加密 share
|
|
||||||
final encryptedData = encryptShare(clientShareData, mnemonic);
|
|
||||||
|
|
||||||
return MpcShareBackup(
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
encryptedShare: encryptedData.ciphertext,
|
|
||||||
iv: encryptedData.iv,
|
|
||||||
authTag: encryptedData.authTag,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从助记词恢复 MPC Share
|
|
||||||
///
|
|
||||||
/// [mnemonic] - 用户输入的 12 词助记词
|
|
||||||
/// [encryptedShare] - 加密的 share 数据
|
|
||||||
/// [iv] - 初始化向量
|
|
||||||
/// [authTag] - 认证标签
|
|
||||||
///
|
|
||||||
/// 返回原始 share 数据
|
|
||||||
static String recoverShare({
|
|
||||||
required String mnemonic,
|
|
||||||
required String encryptedShare,
|
|
||||||
required String iv,
|
|
||||||
required String authTag,
|
|
||||||
}) {
|
|
||||||
// 验证助记词格式
|
|
||||||
if (!bip39.validateMnemonic(mnemonic)) {
|
|
||||||
throw ArgumentError('Invalid mnemonic: not a valid BIP39 mnemonic');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密 share
|
|
||||||
return decryptShare(
|
|
||||||
ciphertext: encryptedShare,
|
|
||||||
iv: iv,
|
|
||||||
authTag: authTag,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 使用助记词加密 share
|
|
||||||
///
|
|
||||||
/// 使用 PBKDF2 + AES-256 (这里简化实现,生产环境应使用 pointycastle 的 AES-GCM)
|
|
||||||
static EncryptedShareData encryptShare(String shareData, String mnemonic) {
|
|
||||||
// 生成随机 IV (12 bytes for GCM)
|
|
||||||
final random = Random.secure();
|
|
||||||
final ivBytes = Uint8List(12);
|
|
||||||
for (var i = 0; i < 12; i++) {
|
|
||||||
ivBytes[i] = random.nextInt(256);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从助记词派生密钥 (PBKDF2-SHA256)
|
|
||||||
final key = _deriveKey(mnemonic, ivBytes);
|
|
||||||
|
|
||||||
// 将 share 转换为字节
|
|
||||||
final shareBytes = utf8.encode(shareData);
|
|
||||||
|
|
||||||
// 加密 (使用 AES-CTR 模式 + HMAC 认证)
|
|
||||||
// 注: 实际生产环境应使用 AES-GCM,这里用 CTR + HMAC 模拟
|
|
||||||
final encrypted = _aesEncrypt(shareBytes, key, ivBytes);
|
|
||||||
|
|
||||||
// 计算认证标签 (HMAC-SHA256)
|
|
||||||
final authTag = _computeAuthTag(encrypted, key);
|
|
||||||
|
|
||||||
return EncryptedShareData(
|
|
||||||
ciphertext: base64Encode(encrypted),
|
|
||||||
iv: base64Encode(ivBytes),
|
|
||||||
authTag: base64Encode(authTag),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 使用助记词解密 share
|
|
||||||
static String decryptShare({
|
|
||||||
required String ciphertext,
|
|
||||||
required String iv,
|
|
||||||
required String authTag,
|
|
||||||
required String mnemonic,
|
|
||||||
}) {
|
|
||||||
final ciphertextBytes = base64Decode(ciphertext);
|
|
||||||
final ivBytes = base64Decode(iv);
|
|
||||||
final authTagBytes = base64Decode(authTag);
|
|
||||||
|
|
||||||
// 从助记词派生密钥
|
|
||||||
final key = _deriveKey(mnemonic, ivBytes);
|
|
||||||
|
|
||||||
// 验证认证标签
|
|
||||||
final expectedAuthTag = _computeAuthTag(ciphertextBytes, key);
|
|
||||||
if (!_constantTimeEquals(authTagBytes, expectedAuthTag)) {
|
|
||||||
throw StateError('Authentication failed: invalid auth tag (wrong mnemonic or corrupted data)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解密
|
|
||||||
final decrypted = _aesDecrypt(ciphertextBytes, key, ivBytes);
|
|
||||||
|
|
||||||
return utf8.decode(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 从助记词派生加密密钥 (PBKDF2-SHA256)
|
|
||||||
static Uint8List _deriveKey(String mnemonic, Uint8List iv) {
|
|
||||||
// 使用 BIP39 seed 作为基础
|
|
||||||
final seed = bip39.mnemonicToSeed(mnemonic);
|
|
||||||
|
|
||||||
// PBKDF2 (简化版,使用多轮 HMAC-SHA256)
|
|
||||||
// 注: 实际应使用 pointycastle 的 PBKDF2
|
|
||||||
final saltBytes = utf8.encode(_salt);
|
|
||||||
final combinedSalt = Uint8List(saltBytes.length + iv.length);
|
|
||||||
combinedSalt.setAll(0, saltBytes);
|
|
||||||
combinedSalt.setAll(saltBytes.length, iv);
|
|
||||||
|
|
||||||
Uint8List result = Uint8List.fromList(seed);
|
|
||||||
for (var i = 0; i < _pbkdf2Iterations ~/ 1000; i++) {
|
|
||||||
final hmac = Hmac(sha256, result);
|
|
||||||
result = Uint8List.fromList(hmac.convert(combinedSalt).bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Uint8List.fromList(result.sublist(0, 32)); // 256 bit key
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AES 加密 (CTR 模式简化实现)
|
|
||||||
static Uint8List _aesEncrypt(List<int> plaintext, Uint8List key, Uint8List iv) {
|
|
||||||
// 生成密钥流 (使用 HMAC-SHA256 作为伪随机函数)
|
|
||||||
final encrypted = Uint8List(plaintext.length);
|
|
||||||
var counter = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < plaintext.length; i += 32) {
|
|
||||||
// 生成密钥块
|
|
||||||
final counterBytes = Uint8List(4);
|
|
||||||
counterBytes.buffer.asByteData().setUint32(0, counter++, Endian.big);
|
|
||||||
|
|
||||||
final input = Uint8List(iv.length + counterBytes.length);
|
|
||||||
input.setAll(0, iv);
|
|
||||||
input.setAll(iv.length, counterBytes);
|
|
||||||
|
|
||||||
final hmac = Hmac(sha256, key);
|
|
||||||
final keyStream = hmac.convert(input).bytes;
|
|
||||||
|
|
||||||
// XOR 加密
|
|
||||||
for (var j = 0; j < 32 && i + j < plaintext.length; j++) {
|
|
||||||
encrypted[i + j] = plaintext[i + j] ^ keyStream[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AES 解密 (CTR 模式 - 与加密相同)
|
|
||||||
static Uint8List _aesDecrypt(Uint8List ciphertext, Uint8List key, Uint8List iv) {
|
|
||||||
return _aesEncrypt(ciphertext, key, iv); // CTR 模式加密和解密相同
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 计算认证标签 (HMAC-SHA256)
|
|
||||||
static Uint8List _computeAuthTag(Uint8List data, Uint8List key) {
|
|
||||||
final hmac = Hmac(sha256, key);
|
|
||||||
return Uint8List.fromList(hmac.convert(data).bytes.sublist(0, 16)); // 128 bit tag
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 常量时间比较 (防止时序攻击)
|
|
||||||
static bool _constantTimeEquals(Uint8List a, Uint8List b) {
|
|
||||||
if (a.length != b.length) return false;
|
|
||||||
var result = 0;
|
|
||||||
for (var i = 0; i < a.length; i++) {
|
|
||||||
result |= a[i] ^ b[i];
|
|
||||||
}
|
|
||||||
return result == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证助记词格式
|
|
||||||
static bool validateMnemonic(String mnemonic) {
|
|
||||||
return bip39.validateMnemonic(mnemonic);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将字节数组转换为十六进制字符串
|
|
||||||
static String bytesToHex(Uint8List bytes) {
|
|
||||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 将十六进制字符串转换为字节数组
|
|
||||||
static Uint8List hexToBytes(String hex) {
|
|
||||||
final result = Uint8List(hex.length ~/ 2);
|
|
||||||
for (var i = 0; i < result.length; i++) {
|
|
||||||
result[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 加密后的 Share 数据
|
|
||||||
class EncryptedShareData {
|
|
||||||
/// 密文 (base64 编码)
|
|
||||||
final String ciphertext;
|
|
||||||
|
|
||||||
/// 初始化向量 (base64 编码)
|
|
||||||
final String iv;
|
|
||||||
|
|
||||||
/// 认证标签 (base64 编码)
|
|
||||||
final String authTag;
|
|
||||||
|
|
||||||
EncryptedShareData({
|
|
||||||
required this.ciphertext,
|
|
||||||
required this.iv,
|
|
||||||
required this.authTag,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MPC Share 备份数据
|
|
||||||
///
|
|
||||||
/// 用户需要安全保存的数据:
|
|
||||||
/// - mnemonic: 12 词助记词 (用户需要记住/抄写的)
|
|
||||||
///
|
|
||||||
/// 设备上存储的数据:
|
|
||||||
/// - encryptedShare: 加密的 share
|
|
||||||
/// - iv: 初始化向量
|
|
||||||
/// - authTag: 认证标签
|
|
||||||
class MpcShareBackup {
|
|
||||||
/// 12 词助记词(用户需要安全备份的)
|
|
||||||
final String mnemonic;
|
|
||||||
|
|
||||||
/// 加密后的 share 数据 (base64)
|
|
||||||
final String encryptedShare;
|
|
||||||
|
|
||||||
/// 初始化向量 (base64)
|
|
||||||
final String iv;
|
|
||||||
|
|
||||||
/// 认证标签 (base64)
|
|
||||||
final String authTag;
|
|
||||||
|
|
||||||
MpcShareBackup({
|
|
||||||
required this.mnemonic,
|
|
||||||
required this.encryptedShare,
|
|
||||||
required this.iv,
|
|
||||||
required this.authTag,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 从 JSON 反序列化
|
|
||||||
factory MpcShareBackup.fromJson(Map<String, dynamic> json) {
|
|
||||||
return MpcShareBackup(
|
|
||||||
mnemonic: json['mnemonic'] as String,
|
|
||||||
encryptedShare: json['encryptedShare'] as String,
|
|
||||||
iv: json['iv'] as String,
|
|
||||||
authTag: json['authTag'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 序列化为 JSON
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
'mnemonic': mnemonic,
|
|
||||||
'encryptedShare': encryptedShare,
|
|
||||||
'iv': iv,
|
|
||||||
'authTag': authTag,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// 获取助记词列表
|
|
||||||
List<String> get mnemonicWords => mnemonic.split(' ');
|
|
||||||
|
|
||||||
/// 恢复原始 share
|
|
||||||
String recoverShare() {
|
|
||||||
return MpcShareService.recoverShare(
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
encryptedShare: encryptedShare,
|
|
||||||
iv: iv,
|
|
||||||
authTag: authTag,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +1,24 @@
|
||||||
class StorageKeys {
|
class StorageKeys {
|
||||||
StorageKeys._();
|
StorageKeys._();
|
||||||
|
|
||||||
// Auth
|
// 账号信息
|
||||||
static const String walletAddress = 'wallet_address';
|
static const String userSerialNum = 'user_serial_num'; // 用户序列号
|
||||||
static const String privateKey = 'private_key';
|
static const String username = 'username'; // 随机用户名
|
||||||
static const String mnemonic = 'mnemonic';
|
static const String avatarSvg = 'avatar_svg'; // 随机 SVG 头像
|
||||||
static const String isWalletCreated = 'is_wallet_created';
|
static const String referralCode = 'referral_code'; // 推荐码
|
||||||
static const String isMnemonicBackedUp = 'is_mnemonic_backed_up';
|
static const String isAccountCreated = 'is_account_created'; // 账号是否已创建
|
||||||
|
|
||||||
// MPC 相关
|
// 钱包信息
|
||||||
static const String mpcClientShareData = 'mpc_client_share_data'; // MPC 客户端分片数据 (原始)
|
|
||||||
static const String mpcPublicKey = 'mpc_public_key'; // MPC 公钥
|
|
||||||
static const String mpcEncryptedShare = 'mpc_encrypted_share'; // 用助记词加密的 share (密文)
|
|
||||||
static const String mpcShareIv = 'mpc_share_iv'; // 加密 IV
|
|
||||||
static const String mpcShareAuthTag = 'mpc_share_auth_tag'; // 加密认证标签
|
|
||||||
static const String accountSequence = 'account_sequence'; // 账户序列号
|
|
||||||
|
|
||||||
// 钱包地址
|
|
||||||
static const String walletAddressBsc = 'wallet_address_bsc';
|
static const String walletAddressBsc = 'wallet_address_bsc';
|
||||||
static const String walletAddressKava = 'wallet_address_kava';
|
static const String walletAddressKava = 'wallet_address_kava';
|
||||||
static const String walletAddressDst = 'wallet_address_dst';
|
static const String walletAddressDst = 'wallet_address_dst';
|
||||||
|
static const String mnemonic = 'mnemonic'; // 12 词助记词
|
||||||
|
static const String isWalletReady = 'is_wallet_ready'; // 钱包是否已就绪
|
||||||
|
static const String isMnemonicBackedUp = 'is_mnemonic_backed_up'; // 助记词是否已备份
|
||||||
|
|
||||||
// User
|
// Token
|
||||||
static const String userId = 'user_id';
|
|
||||||
static const String userProfile = 'user_profile';
|
|
||||||
static const String accessToken = 'access_token';
|
static const String accessToken = 'access_token';
|
||||||
static const String refreshToken = 'refresh_token';
|
static const String refreshToken = 'refresh_token';
|
||||||
static const String referralCode = 'referral_code';
|
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
static const String locale = 'locale';
|
static const String locale = 'locale';
|
||||||
|
|
@ -42,4 +34,28 @@ class StorageKeys {
|
||||||
static const String lastSyncTime = 'last_sync_time';
|
static const String lastSyncTime = 'last_sync_time';
|
||||||
static const String cachedRankingData = 'cached_ranking_data';
|
static const String cachedRankingData = 'cached_ranking_data';
|
||||||
static const String cachedMiningStatus = 'cached_mining_status';
|
static const String cachedMiningStatus = 'cached_mining_status';
|
||||||
|
|
||||||
|
// ===== 已废弃 (保留用于迁移旧数据) =====
|
||||||
|
@Deprecated('Use userSerialNum instead')
|
||||||
|
static const String userId = 'user_id';
|
||||||
|
@Deprecated('Use userSerialNum instead')
|
||||||
|
static const String accountSequence = 'account_sequence';
|
||||||
|
@Deprecated('Use isAccountCreated instead')
|
||||||
|
static const String isWalletCreated = 'is_wallet_created';
|
||||||
|
@Deprecated('MPC share no longer stored locally')
|
||||||
|
static const String mpcClientShareData = 'mpc_client_share_data';
|
||||||
|
@Deprecated('MPC share no longer stored locally')
|
||||||
|
static const String mpcPublicKey = 'mpc_public_key';
|
||||||
|
@Deprecated('MPC share no longer stored locally')
|
||||||
|
static const String mpcEncryptedShare = 'mpc_encrypted_share';
|
||||||
|
@Deprecated('MPC share no longer stored locally')
|
||||||
|
static const String mpcShareIv = 'mpc_share_iv';
|
||||||
|
@Deprecated('MPC share no longer stored locally')
|
||||||
|
static const String mpcShareAuthTag = 'mpc_share_auth_tag';
|
||||||
|
@Deprecated('No longer used')
|
||||||
|
static const String walletAddress = 'wallet_address';
|
||||||
|
@Deprecated('No longer used')
|
||||||
|
static const String privateKey = 'private_key';
|
||||||
|
@Deprecated('No longer used')
|
||||||
|
static const String userProfile = 'user_profile';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -7,37 +8,21 @@ import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import '../../../../routes/route_paths.dart';
|
import '../../../../routes/route_paths.dart';
|
||||||
import '../../../../routes/app_router.dart';
|
import '../../../../routes/app_router.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/account_service.dart';
|
||||||
|
|
||||||
/// 备份助记词/账户信息页面
|
/// 备份助记词页面
|
||||||
/// MPC 模式下显示账户信息,传统模式下显示助记词
|
/// 进入时调用 API 获取钱包信息和助记词
|
||||||
class BackupMnemonicPage extends ConsumerStatefulWidget {
|
class BackupMnemonicPage extends ConsumerStatefulWidget {
|
||||||
/// 生成的助记词列表 (MPC 模式下为空)
|
/// 用户序列号
|
||||||
final List<String> mnemonicWords;
|
final int userSerialNum;
|
||||||
/// KAVA 钱包地址
|
|
||||||
final String kavaAddress;
|
|
||||||
/// DST 钱包地址
|
|
||||||
final String dstAddress;
|
|
||||||
/// BSC 钱包地址
|
|
||||||
final String bscAddress;
|
|
||||||
/// 序列号 (账户唯一标识)
|
|
||||||
final String serialNumber;
|
|
||||||
/// 推荐码
|
/// 推荐码
|
||||||
final String? referralCode;
|
final String? referralCode;
|
||||||
/// MPC 公钥
|
|
||||||
final String? publicKey;
|
|
||||||
/// 是否为 MPC 模式
|
|
||||||
final bool isMpcMode;
|
|
||||||
|
|
||||||
const BackupMnemonicPage({
|
const BackupMnemonicPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.mnemonicWords,
|
required this.userSerialNum,
|
||||||
required this.kavaAddress,
|
|
||||||
required this.dstAddress,
|
|
||||||
required this.bscAddress,
|
|
||||||
required this.serialNumber,
|
|
||||||
this.referralCode,
|
this.referralCode,
|
||||||
this.publicKey,
|
|
||||||
this.isMpcMode = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -45,14 +30,129 @@ class BackupMnemonicPage extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BackupMnemonicPageState extends ConsumerState<BackupMnemonicPage> {
|
class _BackupMnemonicPageState extends ConsumerState<BackupMnemonicPage> {
|
||||||
|
// 加载状态
|
||||||
|
bool _isLoading = true;
|
||||||
|
// 错误信息
|
||||||
|
String? _errorMessage;
|
||||||
|
// 钱包信息
|
||||||
|
WalletInfoResponse? _walletInfo;
|
||||||
|
// 助记词列表
|
||||||
|
List<String> _mnemonicWords = [];
|
||||||
|
// 钱包地址
|
||||||
|
String? _kavaAddress;
|
||||||
|
String? _dstAddress;
|
||||||
|
String? _bscAddress;
|
||||||
|
|
||||||
// 是否隐藏助记词
|
// 是否隐藏助记词
|
||||||
bool _isHidden = false;
|
bool _isHidden = false;
|
||||||
// 是否正在下载
|
// 是否正在下载
|
||||||
bool _isDownloading = false;
|
bool _isDownloading = false;
|
||||||
|
|
||||||
|
// 轮询定时器
|
||||||
|
Timer? _pollTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadWalletInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载钱包信息
|
||||||
|
Future<void> _loadWalletInfo() async {
|
||||||
|
try {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
|
||||||
|
// 调用 API 获取钱包信息
|
||||||
|
final response = await accountService.getWalletInfo(widget.userSerialNum);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (response.isReady) {
|
||||||
|
// 钱包已就绪
|
||||||
|
setState(() {
|
||||||
|
_walletInfo = response;
|
||||||
|
_mnemonicWords = response.mnemonic?.split(' ') ?? [];
|
||||||
|
_kavaAddress = response.walletAddresses?.kava;
|
||||||
|
_dstAddress = response.walletAddresses?.dst;
|
||||||
|
_bscAddress = response.walletAddresses?.bsc;
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
} else if (response.isGenerating) {
|
||||||
|
// 钱包生成中,轮询等待
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
_startPolling();
|
||||||
|
} else if (response.isFailed) {
|
||||||
|
// 钱包生成失败
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = '钱包生成失败,请稍后重试';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('加载钱包信息失败: $e');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = '加载失败: ${e.toString().replaceAll('Exception: ', '')}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开始轮询
|
||||||
|
void _startPolling() {
|
||||||
|
_pollTimer?.cancel();
|
||||||
|
_pollTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||||
|
if (!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
final response = await accountService.getWalletInfo(widget.userSerialNum);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
if (response.isReady) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() {
|
||||||
|
_walletInfo = response;
|
||||||
|
_mnemonicWords = response.mnemonic?.split(' ') ?? [];
|
||||||
|
_kavaAddress = response.walletAddresses?.kava;
|
||||||
|
_dstAddress = response.walletAddresses?.dst;
|
||||||
|
_bscAddress = response.walletAddresses?.bsc;
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
} else if (response.isFailed) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_errorMessage = '钱包生成失败,请稍后重试';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 如果仍在生成中,继续轮询
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略轮询中的错误,继续尝试
|
||||||
|
debugPrint('轮询钱包信息出错: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// 复制全部助记词
|
/// 复制全部助记词
|
||||||
void _copyAllMnemonic() {
|
void _copyAllMnemonic() {
|
||||||
final mnemonicText = widget.mnemonicWords.join(' ');
|
final mnemonicText = _mnemonicWords.join(' ');
|
||||||
Clipboard.setData(ClipboardData(text: mnemonicText));
|
Clipboard.setData(ClipboardData(text: mnemonicText));
|
||||||
_showCopySuccess('助记词已复制到剪贴板');
|
_showCopySuccess('助记词已复制到剪贴板');
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +183,7 @@ class _BackupMnemonicPageState extends ConsumerState<BackupMnemonicPage> {
|
||||||
|
|
||||||
/// 下载助记词备份文件
|
/// 下载助记词备份文件
|
||||||
Future<void> _downloadMnemonic() async {
|
Future<void> _downloadMnemonic() async {
|
||||||
if (_isDownloading) return;
|
if (_isDownloading || _mnemonicWords.isEmpty) return;
|
||||||
|
|
||||||
setState(() => _isDownloading = true);
|
setState(() => _isDownloading = true);
|
||||||
|
|
||||||
|
|
@ -108,16 +208,16 @@ class _BackupMnemonicPageState extends ConsumerState<BackupMnemonicPage> {
|
||||||
您的 12 词助记词
|
您的 12 词助记词
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
${widget.mnemonicWords.asMap().entries.map((e) => '${(e.key + 1).toString().padLeft(2, ' ')}. ${e.value}').join('\n')}
|
${_mnemonicWords.asMap().entries.map((e) => '${(e.key + 1).toString().padLeft(2, ' ')}. ${e.value}').join('\n')}
|
||||||
|
|
||||||
=====================================
|
=====================================
|
||||||
钱包地址信息
|
钱包地址信息
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
KAVA 地址: ${widget.kavaAddress}
|
KAVA 地址: ${_kavaAddress ?? 'N/A'}
|
||||||
DST 地址: ${widget.dstAddress}
|
DST 地址: ${_dstAddress ?? 'N/A'}
|
||||||
BSC 地址: ${widget.bscAddress}
|
BSC 地址: ${_bscAddress ?? 'N/A'}
|
||||||
序列号: ${widget.serialNumber}
|
序列号: ${widget.userSerialNum}
|
||||||
|
|
||||||
=====================================
|
=====================================
|
||||||
备份时间
|
备份时间
|
||||||
|
|
@ -160,15 +260,26 @@ ${DateTime.now().toString()}
|
||||||
|
|
||||||
/// 确认已备份,跳转到确认备份页面进行验证
|
/// 确认已备份,跳转到确认备份页面进行验证
|
||||||
void _confirmBackup() {
|
void _confirmBackup() {
|
||||||
|
if (_mnemonicWords.isEmpty || _kavaAddress == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('钱包信息不完整,请等待加载完成'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 跳转到确认备份页面
|
// 跳转到确认备份页面
|
||||||
context.push(
|
context.push(
|
||||||
RoutePaths.verifyMnemonic,
|
RoutePaths.verifyMnemonic,
|
||||||
extra: VerifyMnemonicParams(
|
extra: VerifyMnemonicParams(
|
||||||
mnemonicWords: widget.mnemonicWords,
|
mnemonicWords: _mnemonicWords,
|
||||||
kavaAddress: widget.kavaAddress,
|
kavaAddress: _kavaAddress!,
|
||||||
dstAddress: widget.dstAddress,
|
dstAddress: _dstAddress!,
|
||||||
bscAddress: widget.bscAddress,
|
bscAddress: _bscAddress!,
|
||||||
serialNumber: widget.serialNumber,
|
userSerialNum: widget.userSerialNum,
|
||||||
|
referralCode: widget.referralCode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -201,26 +312,14 @@ ${DateTime.now().toString()}
|
||||||
_buildAppBar(),
|
_buildAppBar(),
|
||||||
// 内容区域
|
// 内容区域
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: _isLoading
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
? _buildLoadingState()
|
||||||
child: Column(
|
: _errorMessage != null
|
||||||
children: [
|
? _buildErrorState()
|
||||||
const SizedBox(height: 8),
|
: _buildContent(),
|
||||||
// 助记词卡片
|
|
||||||
if (widget.mnemonicWords.isNotEmpty) _buildMnemonicCard(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 警告提示
|
|
||||||
_buildWarningCard(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 钱包地址卡片
|
|
||||||
_buildAddressCard(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// 底部按钮区域
|
// 底部按钮区域 (非加载状态时显示)
|
||||||
_buildBottomButtons(),
|
if (!_isLoading && _errorMessage == null) _buildBottomButtons(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -228,6 +327,119 @@ ${DateTime.now().toString()}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建加载状态
|
||||||
|
Widget _buildLoadingState() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 3,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'正在生成您的钱包...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'请稍候,这可能需要几秒钟',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: Color(0xFF8B7355),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建错误状态
|
||||||
|
Widget _buildErrorState() {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Color(0xFFCC6B2C),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
_loadWalletInfo();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFD4AF37),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'重试',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建内容
|
||||||
|
Widget _buildContent() {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// 助记词卡片
|
||||||
|
if (_mnemonicWords.isNotEmpty) _buildMnemonicCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 警告提示
|
||||||
|
_buildWarningCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// 钱包地址卡片
|
||||||
|
_buildAddressCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 构建顶部导航栏
|
/// 构建顶部导航栏
|
||||||
Widget _buildAppBar() {
|
Widget _buildAppBar() {
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -370,7 +582,9 @@ ${DateTime.now().toString()}
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildMnemonicWord(
|
child: _buildMnemonicWord(
|
||||||
row * 3 + col + 1,
|
row * 3 + col + 1,
|
||||||
widget.mnemonicWords[row * 3 + col],
|
_mnemonicWords.length > row * 3 + col
|
||||||
|
? _mnemonicWords[row * 3 + col]
|
||||||
|
: '',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -399,7 +613,7 @@ ${DateTime.now().toString()}
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
// 单词
|
// 单词
|
||||||
Text(
|
Text(
|
||||||
_isHidden ? '••••••' : word,
|
_isHidden ? '******' : word,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
|
|
@ -479,28 +693,31 @@ ${DateTime.now().toString()}
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildAddressItem(
|
if (_kavaAddress != null)
|
||||||
iconWidget: _buildChainIcon(),
|
_buildAddressItem(
|
||||||
label: 'KAVA 地址',
|
iconWidget: _buildChainIcon(),
|
||||||
address: widget.kavaAddress,
|
label: 'KAVA 地址',
|
||||||
showBorder: true,
|
address: _kavaAddress!,
|
||||||
),
|
showBorder: true,
|
||||||
_buildAddressItem(
|
),
|
||||||
iconWidget: _buildChainIcon(),
|
if (_dstAddress != null)
|
||||||
label: 'DST 地址',
|
_buildAddressItem(
|
||||||
address: widget.dstAddress,
|
iconWidget: _buildChainIcon(),
|
||||||
showBorder: true,
|
label: 'DST 地址',
|
||||||
),
|
address: _dstAddress!,
|
||||||
_buildAddressItem(
|
showBorder: true,
|
||||||
iconWidget: _buildChainIcon(),
|
),
|
||||||
label: 'BSC 地址',
|
if (_bscAddress != null)
|
||||||
address: widget.bscAddress,
|
_buildAddressItem(
|
||||||
showBorder: true,
|
iconWidget: _buildChainIcon(),
|
||||||
),
|
label: 'BSC 地址',
|
||||||
|
address: _bscAddress!,
|
||||||
|
showBorder: true,
|
||||||
|
),
|
||||||
_buildAddressItem(
|
_buildAddressItem(
|
||||||
iconWidget: _buildSequenceIcon(),
|
iconWidget: _buildSequenceIcon(),
|
||||||
label: '序列号',
|
label: '序列号',
|
||||||
address: widget.serialNumber,
|
address: widget.userSerialNum.toString(),
|
||||||
showBorder: false,
|
showBorder: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -20,76 +20,72 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
bool _isAgreed = false;
|
bool _isAgreed = false;
|
||||||
// 创建钱包加载状态
|
// 创建钱包加载状态
|
||||||
bool _isCreating = false;
|
bool _isCreating = false;
|
||||||
// 钱包是否已创建
|
// 账号是否已创建
|
||||||
bool _isWalletCreated = false;
|
bool _isAccountCreated = false;
|
||||||
// 是否正在加载状态
|
// 是否正在加载状态
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
// 已创建的钱包数据
|
// 已创建的账号数据
|
||||||
String? _mnemonic;
|
int? _userSerialNum;
|
||||||
String? _kavaAddress;
|
String? _username;
|
||||||
String? _dstAddress;
|
String? _avatarSvg;
|
||||||
String? _bscAddress;
|
|
||||||
String? _serialNumber;
|
|
||||||
String? _referralCode;
|
String? _referralCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_checkWalletStatus();
|
_checkAccountStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查钱包是否已创建
|
/// 检查账号是否已创建
|
||||||
Future<void> _checkWalletStatus() async {
|
Future<void> _checkAccountStatus() async {
|
||||||
try {
|
try {
|
||||||
final accountService = ref.read(accountServiceProvider);
|
final accountService = ref.read(accountServiceProvider);
|
||||||
|
|
||||||
// 检查是否已创建钱包
|
// 检查是否已创建账号
|
||||||
final hasAccount = await accountService.hasAccount();
|
final hasAccount = await accountService.hasAccount();
|
||||||
|
|
||||||
if (hasAccount) {
|
if (hasAccount) {
|
||||||
// 读取已保存的钱包数据
|
// 读取已保存的账号数据
|
||||||
final mnemonic = await accountService.getMnemonic();
|
final userSerialNum = await accountService.getUserSerialNum();
|
||||||
final addresses = await accountService.getWalletAddresses();
|
final username = await accountService.getUsername();
|
||||||
final sequence = await accountService.getAccountSequence();
|
final avatarSvg = await accountService.getAvatarSvg();
|
||||||
final referralCode = await accountService.getReferralCode();
|
final referralCode = await accountService.getReferralCode();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isWalletCreated = true;
|
_isAccountCreated = true;
|
||||||
_mnemonic = mnemonic;
|
_userSerialNum = userSerialNum;
|
||||||
_kavaAddress = addresses?.kava;
|
_username = username;
|
||||||
_dstAddress = addresses?.dst;
|
_avatarSvg = avatarSvg;
|
||||||
_bscAddress = addresses?.bsc;
|
|
||||||
_serialNumber = sequence?.toString();
|
|
||||||
_referralCode = referralCode;
|
_referralCode = referralCode;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
// 如果钱包已创建,自动勾选协议
|
// 如果账号已创建,自动勾选协议
|
||||||
_isAgreed = true;
|
_isAgreed = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isWalletCreated = false;
|
_isAccountCreated = false;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('检查钱包状态失败: $e');
|
debugPrint('检查账号状态失败: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isWalletCreated = false;
|
_isAccountCreated = false;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建钱包并跳转到备份页面
|
/// 创建账号
|
||||||
///
|
///
|
||||||
/// 调用后端 API 使用 MPC 2-of-3 协议生成钱包地址
|
/// 调用后端 API 快速创建账号(不含钱包信息)
|
||||||
Future<void> _createWallet() async {
|
Future<void> _createAccount() async {
|
||||||
if (!_isAgreed) {
|
if (!_isAgreed) {
|
||||||
_showAgreementTip();
|
_showAgreementTip();
|
||||||
return;
|
return;
|
||||||
|
|
@ -106,30 +102,25 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
// 调用后端 API 创建账号
|
// 调用后端 API 创建账号
|
||||||
debugPrint('开始创建账号...');
|
debugPrint('开始创建账号...');
|
||||||
final response = await accountService.createAccount();
|
final response = await accountService.createAccount();
|
||||||
debugPrint('账号创建成功: 序列号=${response.accountSequence}');
|
debugPrint('账号创建成功: 序列号=${response.userSerialNum}, 用户名=${response.username}');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// MPC 模式下,检查是否有客户端分片数据
|
// 更新状态
|
||||||
// 如果有分片数据,需要提示用户妥善保管
|
setState(() {
|
||||||
final hasMpcData = response.clientShareData != null &&
|
_isAccountCreated = true;
|
||||||
response.clientShareData!.isNotEmpty;
|
_userSerialNum = response.userSerialNum;
|
||||||
|
_username = response.username;
|
||||||
|
_avatarSvg = response.avatarSvg;
|
||||||
|
_referralCode = response.referralCode;
|
||||||
|
});
|
||||||
|
|
||||||
// 跳转到钱包创建成功页面
|
// 跳转到备份助记词页面
|
||||||
context.push(
|
context.push(
|
||||||
RoutePaths.backupMnemonic,
|
RoutePaths.backupMnemonic,
|
||||||
extra: BackupMnemonicParams(
|
extra: BackupMnemonicParams(
|
||||||
// MPC 模式下助记词为空,显示账号信息即可
|
userSerialNum: response.userSerialNum,
|
||||||
mnemonicWords: response.mnemonic?.isNotEmpty == true
|
|
||||||
? response.mnemonic!.split(' ')
|
|
||||||
: [], // MPC 模式下为空
|
|
||||||
kavaAddress: response.walletAddresses.kava,
|
|
||||||
dstAddress: response.walletAddresses.dst,
|
|
||||||
bscAddress: response.walletAddresses.bsc,
|
|
||||||
serialNumber: response.accountSequence.toString(),
|
|
||||||
referralCode: response.referralCode,
|
referralCode: response.referralCode,
|
||||||
publicKey: response.publicKey,
|
|
||||||
isMpcMode: hasMpcData, // 标记是否为 MPC 模式
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -178,13 +169,12 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
debugPrint('导入助记词');
|
debugPrint('导入助记词');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 跳转到备份助记词页面(钱包已创建的情况)
|
/// 跳转到备份助记词页面(账号已创建的情况)
|
||||||
void _goToBackupMnemonic() {
|
void _goToBackupMnemonic() {
|
||||||
if (_mnemonic == null || _kavaAddress == null || _dstAddress == null ||
|
if (_userSerialNum == null) {
|
||||||
_bscAddress == null || _serialNumber == null) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('钱包数据不完整,请重新创建'),
|
content: Text('账号数据不完整,请重新创建'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -194,25 +184,20 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
context.push(
|
context.push(
|
||||||
RoutePaths.backupMnemonic,
|
RoutePaths.backupMnemonic,
|
||||||
extra: BackupMnemonicParams(
|
extra: BackupMnemonicParams(
|
||||||
mnemonicWords: _mnemonic!.split(' '),
|
userSerialNum: _userSerialNum!,
|
||||||
kavaAddress: _kavaAddress!,
|
|
||||||
dstAddress: _dstAddress!,
|
|
||||||
bscAddress: _bscAddress!,
|
|
||||||
serialNumber: _serialNumber!,
|
|
||||||
referralCode: _referralCode,
|
referralCode: _referralCode,
|
||||||
isMpcMode: false,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理按钮点击
|
/// 处理按钮点击
|
||||||
void _handleButtonTap() {
|
void _handleButtonTap() {
|
||||||
if (_isWalletCreated) {
|
if (_isAccountCreated) {
|
||||||
// 钱包已创建,跳转到备份页面
|
// 账号已创建,跳转到备份页面
|
||||||
_goToBackupMnemonic();
|
_goToBackupMnemonic();
|
||||||
} else {
|
} else {
|
||||||
// 钱包未创建,创建新钱包
|
// 账号未创建,创建新账号
|
||||||
_createWallet();
|
_createAccount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,13 +362,13 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据钱包状态决定按钮文字和行为
|
// 根据账号状态决定按钮文字和行为
|
||||||
final buttonText = _isWalletCreated
|
final buttonText = _isAccountCreated
|
||||||
? '钱包已创建(点击备份助记词)'
|
? '账号已创建(点击备份助记词)'
|
||||||
: '生成钱包(创建账户)';
|
: '生成钱包(创建账户)';
|
||||||
|
|
||||||
// 钱包已创建时,不需要勾选协议
|
// 账号已创建时,不需要勾选协议
|
||||||
final isEnabled = _isWalletCreated || _isAgreed;
|
final isEnabled = _isAccountCreated || _isAgreed;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: (_isCreating || !isEnabled) ? null : _handleButtonTap,
|
onTap: (_isCreating || !isEnabled) ? null : _handleButtonTap,
|
||||||
|
|
@ -393,7 +378,7 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _isWalletCreated
|
color: _isAccountCreated
|
||||||
? const Color(0xFF52C41A) // 绿色表示已创建
|
? const Color(0xFF52C41A) // 绿色表示已创建
|
||||||
: const Color(0xFFD4AF37), // 金色表示待创建
|
: const Color(0xFFD4AF37), // 金色表示待创建
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|
@ -411,7 +396,7 @@ class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (_isWalletCreated) ...[
|
if (_isAccountCreated) ...[
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,10 @@ class VerifyMnemonicPage extends ConsumerStatefulWidget {
|
||||||
final String dstAddress;
|
final String dstAddress;
|
||||||
/// BSC 钱包地址
|
/// BSC 钱包地址
|
||||||
final String bscAddress;
|
final String bscAddress;
|
||||||
/// 序列号
|
/// 用户序列号
|
||||||
final String serialNumber;
|
final int userSerialNum;
|
||||||
|
/// 推荐码
|
||||||
|
final String? referralCode;
|
||||||
|
|
||||||
const VerifyMnemonicPage({
|
const VerifyMnemonicPage({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -26,7 +28,8 @@ class VerifyMnemonicPage extends ConsumerStatefulWidget {
|
||||||
required this.kavaAddress,
|
required this.kavaAddress,
|
||||||
required this.dstAddress,
|
required this.dstAddress,
|
||||||
required this.bscAddress,
|
required this.bscAddress,
|
||||||
required this.serialNumber,
|
required this.userSerialNum,
|
||||||
|
this.referralCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -146,7 +149,8 @@ class _VerifyMnemonicPageState extends ConsumerState<VerifyMnemonicPage> {
|
||||||
kavaAddress: widget.kavaAddress,
|
kavaAddress: widget.kavaAddress,
|
||||||
dstAddress: widget.dstAddress,
|
dstAddress: widget.dstAddress,
|
||||||
bscAddress: widget.bscAddress,
|
bscAddress: widget.bscAddress,
|
||||||
serialNumber: widget.serialNumber,
|
userSerialNum: widget.userSerialNum,
|
||||||
|
referralCode: widget.referralCode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ class WalletCreatedPage extends ConsumerWidget {
|
||||||
final String dstAddress;
|
final String dstAddress;
|
||||||
/// BSC 钱包地址
|
/// BSC 钱包地址
|
||||||
final String bscAddress;
|
final String bscAddress;
|
||||||
/// 序列号
|
/// 用户序列号
|
||||||
final String serialNumber;
|
final int userSerialNum;
|
||||||
/// 推荐码
|
/// 推荐码
|
||||||
final String? referralCode;
|
final String? referralCode;
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ class WalletCreatedPage extends ConsumerWidget {
|
||||||
required this.kavaAddress,
|
required this.kavaAddress,
|
||||||
required this.dstAddress,
|
required this.dstAddress,
|
||||||
required this.bscAddress,
|
required this.bscAddress,
|
||||||
required this.serialNumber,
|
required this.userSerialNum,
|
||||||
this.referralCode,
|
this.referralCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ class WalletCreatedPage extends ConsumerWidget {
|
||||||
// 构建分享链接 (带推荐码)
|
// 构建分享链接 (带推荐码)
|
||||||
final shareLink = referralCode != null
|
final shareLink = referralCode != null
|
||||||
? 'https://rwa-durian.app/invite?code=$referralCode'
|
? 'https://rwa-durian.app/invite?code=$referralCode'
|
||||||
: 'https://rwa-durian.app/invite?seq=$serialNumber';
|
: 'https://rwa-durian.app/invite?seq=$userSerialNum';
|
||||||
|
|
||||||
context.push(
|
context.push(
|
||||||
RoutePaths.share,
|
RoutePaths.share,
|
||||||
|
|
@ -199,7 +199,7 @@ class WalletCreatedPage extends ConsumerWidget {
|
||||||
context: context,
|
context: context,
|
||||||
iconWidget: _buildKeyIcon(),
|
iconWidget: _buildKeyIcon(),
|
||||||
label: '序列号',
|
label: '序列号',
|
||||||
value: serialNumber,
|
value: userSerialNum.toString(),
|
||||||
isAddress: false,
|
isAddress: false,
|
||||||
showDivider: true,
|
showDivider: true,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -24,45 +24,49 @@ import 'route_names.dart';
|
||||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final _shellNavigatorKey = GlobalKey<NavigatorState>();
|
final _shellNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
/// 备份助记词页面参数
|
/// 备份助记词页面参数 (新版 - 只需要用户序列号)
|
||||||
class BackupMnemonicParams {
|
class BackupMnemonicParams {
|
||||||
|
final int userSerialNum;
|
||||||
|
final String? referralCode;
|
||||||
|
|
||||||
|
BackupMnemonicParams({
|
||||||
|
required this.userSerialNum,
|
||||||
|
this.referralCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 确认备份页面参数
|
||||||
|
class VerifyMnemonicParams {
|
||||||
final List<String> mnemonicWords;
|
final List<String> mnemonicWords;
|
||||||
final String kavaAddress;
|
final String kavaAddress;
|
||||||
final String dstAddress;
|
final String dstAddress;
|
||||||
final String bscAddress;
|
final String bscAddress;
|
||||||
final String serialNumber;
|
final int userSerialNum;
|
||||||
final String? referralCode; // 推荐码
|
final String? referralCode;
|
||||||
final String? publicKey; // MPC 公钥
|
|
||||||
final bool isMpcMode; // 是否为 MPC 模式
|
|
||||||
|
|
||||||
BackupMnemonicParams({
|
VerifyMnemonicParams({
|
||||||
required this.mnemonicWords,
|
required this.mnemonicWords,
|
||||||
required this.kavaAddress,
|
required this.kavaAddress,
|
||||||
required this.dstAddress,
|
required this.dstAddress,
|
||||||
required this.bscAddress,
|
required this.bscAddress,
|
||||||
required this.serialNumber,
|
required this.userSerialNum,
|
||||||
this.referralCode,
|
this.referralCode,
|
||||||
this.publicKey,
|
|
||||||
this.isMpcMode = false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 确认备份页面参数(复用 BackupMnemonicParams)
|
|
||||||
typedef VerifyMnemonicParams = BackupMnemonicParams;
|
|
||||||
|
|
||||||
/// 创建成功页面参数
|
/// 创建成功页面参数
|
||||||
class WalletCreatedParams {
|
class WalletCreatedParams {
|
||||||
final String kavaAddress;
|
final String kavaAddress;
|
||||||
final String dstAddress;
|
final String dstAddress;
|
||||||
final String bscAddress;
|
final String bscAddress;
|
||||||
final String serialNumber;
|
final int userSerialNum;
|
||||||
final String? referralCode;
|
final String? referralCode;
|
||||||
|
|
||||||
WalletCreatedParams({
|
WalletCreatedParams({
|
||||||
required this.kavaAddress,
|
required this.kavaAddress,
|
||||||
required this.dstAddress,
|
required this.dstAddress,
|
||||||
required this.bscAddress,
|
required this.bscAddress,
|
||||||
required this.serialNumber,
|
required this.userSerialNum,
|
||||||
this.referralCode,
|
this.referralCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -105,21 +109,15 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
builder: (context, state) => const OnboardingPage(),
|
builder: (context, state) => const OnboardingPage(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Backup Mnemonic
|
// Backup Mnemonic (备份助记词 - 会调用 API 获取钱包信息)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.backupMnemonic,
|
path: RoutePaths.backupMnemonic,
|
||||||
name: RouteNames.backupMnemonic,
|
name: RouteNames.backupMnemonic,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final params = state.extra as BackupMnemonicParams;
|
final params = state.extra as BackupMnemonicParams;
|
||||||
return BackupMnemonicPage(
|
return BackupMnemonicPage(
|
||||||
mnemonicWords: params.mnemonicWords,
|
userSerialNum: params.userSerialNum,
|
||||||
kavaAddress: params.kavaAddress,
|
|
||||||
dstAddress: params.dstAddress,
|
|
||||||
bscAddress: params.bscAddress,
|
|
||||||
serialNumber: params.serialNumber,
|
|
||||||
referralCode: params.referralCode,
|
referralCode: params.referralCode,
|
||||||
publicKey: params.publicKey,
|
|
||||||
isMpcMode: params.isMpcMode,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -135,7 +133,8 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
kavaAddress: params.kavaAddress,
|
kavaAddress: params.kavaAddress,
|
||||||
dstAddress: params.dstAddress,
|
dstAddress: params.dstAddress,
|
||||||
bscAddress: params.bscAddress,
|
bscAddress: params.bscAddress,
|
||||||
serialNumber: params.serialNumber,
|
userSerialNum: params.userSerialNum,
|
||||||
|
referralCode: params.referralCode,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -150,7 +149,7 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
kavaAddress: params.kavaAddress,
|
kavaAddress: params.kavaAddress,
|
||||||
dstAddress: params.dstAddress,
|
dstAddress: params.dstAddress,
|
||||||
bscAddress: params.bscAddress,
|
bscAddress: params.bscAddress,
|
||||||
serialNumber: params.serialNumber,
|
userSerialNum: params.userSerialNum,
|
||||||
referralCode: params.referralCode,
|
referralCode: params.referralCode,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,380 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:mocktail/mocktail.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:rwa_android_app/core/services/account_service.dart';
|
|
||||||
import 'package:rwa_android_app/core/network/api_client.dart';
|
|
||||||
import 'package:rwa_android_app/core/storage/secure_storage.dart';
|
|
||||||
import 'package:rwa_android_app/core/storage/storage_keys.dart';
|
|
||||||
|
|
||||||
// Mock classes using mocktail
|
|
||||||
class MockApiClient extends Mock implements ApiClient {}
|
|
||||||
|
|
||||||
class MockSecureStorage extends Mock implements SecureStorage {}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
late AccountService accountService;
|
|
||||||
late MockApiClient mockApiClient;
|
|
||||||
late MockSecureStorage mockSecureStorage;
|
|
||||||
|
|
||||||
setUpAll(() {
|
|
||||||
// Register fallback values for any() matchers
|
|
||||||
registerFallbackValue(RequestOptions(path: ''));
|
|
||||||
});
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
mockApiClient = MockApiClient();
|
|
||||||
mockSecureStorage = MockSecureStorage();
|
|
||||||
accountService = AccountService(
|
|
||||||
apiClient: mockApiClient,
|
|
||||||
secureStorage: mockSecureStorage,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('AccountService', () {
|
|
||||||
group('createAccount', () {
|
|
||||||
test('should create account and save data to secure storage', () async {
|
|
||||||
// Arrange
|
|
||||||
const testDeviceId = 'test-device-123';
|
|
||||||
final mockResponse = Response<Map<String, dynamic>>(
|
|
||||||
data: {
|
|
||||||
'userId': '123456789',
|
|
||||||
'accountSequence': 1,
|
|
||||||
'referralCode': 'ABC123',
|
|
||||||
'mnemonic': '',
|
|
||||||
'clientShareData': 'mock-client-share-data',
|
|
||||||
'publicKey': 'mock-public-key',
|
|
||||||
'walletAddresses': {
|
|
||||||
'kava': '0x1234567890abcdef1234567890abcdef12345678',
|
|
||||||
'dst': 'dst1abcdefghijklmnopqrstuvwxyz123456789',
|
|
||||||
'bsc': '0x1234567890abcdef1234567890abcdef12345678',
|
|
||||||
},
|
|
||||||
'accessToken': 'mock-access-token',
|
|
||||||
'refreshToken': 'mock-refresh-token',
|
|
||||||
},
|
|
||||||
requestOptions: RequestOptions(path: '/user/auto-create'),
|
|
||||||
statusCode: 201,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup mocks
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.deviceId))
|
|
||||||
.thenAnswer((_) async => testDeviceId);
|
|
||||||
when(() => mockApiClient.post(any(), data: any(named: 'data')))
|
|
||||||
.thenAnswer((_) async => mockResponse);
|
|
||||||
when(() => mockSecureStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
|
||||||
.thenAnswer((_) async {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.createAccount();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result.userId, '123456789');
|
|
||||||
expect(result.accountSequence, 1);
|
|
||||||
expect(result.referralCode, 'ABC123');
|
|
||||||
expect(result.clientShareData, 'mock-client-share-data');
|
|
||||||
expect(result.publicKey, 'mock-public-key');
|
|
||||||
expect(result.walletAddresses.kava, '0x1234567890abcdef1234567890abcdef12345678');
|
|
||||||
expect(result.walletAddresses.dst, 'dst1abcdefghijklmnopqrstuvwxyz123456789');
|
|
||||||
expect(result.walletAddresses.bsc, '0x1234567890abcdef1234567890abcdef12345678');
|
|
||||||
expect(result.accessToken, 'mock-access-token');
|
|
||||||
expect(result.refreshToken, 'mock-refresh-token');
|
|
||||||
|
|
||||||
// Verify storage calls
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.userId,
|
|
||||||
value: '123456789',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.accountSequence,
|
|
||||||
value: '1',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.referralCode,
|
|
||||||
value: 'ABC123',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.accessToken,
|
|
||||||
value: 'mock-access-token',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.refreshToken,
|
|
||||||
value: 'mock-refresh-token',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.isWalletCreated,
|
|
||||||
value: 'true',
|
|
||||||
)).called(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle MPC mode without mnemonic', () async {
|
|
||||||
// Arrange
|
|
||||||
const testDeviceId = 'test-device-123';
|
|
||||||
final mockResponse = Response<Map<String, dynamic>>(
|
|
||||||
data: {
|
|
||||||
'userId': '123456789',
|
|
||||||
'accountSequence': 1,
|
|
||||||
'referralCode': 'ABC123',
|
|
||||||
'mnemonic': '', // Empty in MPC mode
|
|
||||||
'clientShareData': 'client-share-data',
|
|
||||||
'publicKey': 'public-key',
|
|
||||||
'walletAddresses': {
|
|
||||||
'kava': '0x1234',
|
|
||||||
'dst': 'dst1abc',
|
|
||||||
'bsc': '0x1234',
|
|
||||||
},
|
|
||||||
'accessToken': 'token',
|
|
||||||
'refreshToken': 'refresh',
|
|
||||||
},
|
|
||||||
requestOptions: RequestOptions(path: '/user/auto-create'),
|
|
||||||
statusCode: 201,
|
|
||||||
);
|
|
||||||
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.deviceId))
|
|
||||||
.thenAnswer((_) async => testDeviceId);
|
|
||||||
when(() => mockApiClient.post(any(), data: any(named: 'data')))
|
|
||||||
.thenAnswer((_) async => mockResponse);
|
|
||||||
when(() => mockSecureStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
|
||||||
.thenAnswer((_) async {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.createAccount();
|
|
||||||
|
|
||||||
// Assert - MPC data should be stored
|
|
||||||
expect(result.mnemonic, isEmpty);
|
|
||||||
expect(result.clientShareData, 'client-share-data');
|
|
||||||
expect(result.publicKey, 'public-key');
|
|
||||||
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.mpcClientShareData,
|
|
||||||
value: 'client-share-data',
|
|
||||||
)).called(1);
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.mpcPublicKey,
|
|
||||||
value: 'public-key',
|
|
||||||
)).called(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('getOrCreateDeviceId', () {
|
|
||||||
test('should return existing device id if available', () async {
|
|
||||||
// Arrange
|
|
||||||
const existingDeviceId = 'existing-device-123';
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.deviceId))
|
|
||||||
.thenAnswer((_) async => existingDeviceId);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.getOrCreateDeviceId();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, existingDeviceId);
|
|
||||||
verifyNever(() => mockSecureStorage.write(
|
|
||||||
key: any(named: 'key'),
|
|
||||||
value: any(named: 'value'),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should generate and save new device id if not exists', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.deviceId))
|
|
||||||
.thenAnswer((_) async => null);
|
|
||||||
when(() => mockSecureStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
|
||||||
.thenAnswer((_) async {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.getOrCreateDeviceId();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, isNotEmpty);
|
|
||||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
||||||
expect(
|
|
||||||
result,
|
|
||||||
matches(RegExp(
|
|
||||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
|
|
||||||
caseSensitive: false,
|
|
||||||
)));
|
|
||||||
verify(() => mockSecureStorage.write(
|
|
||||||
key: StorageKeys.deviceId,
|
|
||||||
value: result,
|
|
||||||
)).called(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('hasAccount', () {
|
|
||||||
test('should return true when wallet is created', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.isWalletCreated))
|
|
||||||
.thenAnswer((_) async => 'true');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.hasAccount();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false when wallet is not created', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.isWalletCreated))
|
|
||||||
.thenAnswer((_) async => null);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.hasAccount();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('getWalletAddresses', () {
|
|
||||||
test('should return wallet addresses when all are available', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressBsc))
|
|
||||||
.thenAnswer((_) async => '0xBSC');
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressKava))
|
|
||||||
.thenAnswer((_) async => '0xKAVA');
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressDst))
|
|
||||||
.thenAnswer((_) async => 'dst1DST');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.getWalletAddresses();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, isNotNull);
|
|
||||||
expect(result!.bsc, '0xBSC');
|
|
||||||
expect(result.kava, '0xKAVA');
|
|
||||||
expect(result.dst, 'dst1DST');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return null when any address is missing', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressBsc))
|
|
||||||
.thenAnswer((_) async => '0xBSC');
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressKava))
|
|
||||||
.thenAnswer((_) async => null); // Missing
|
|
||||||
when(() => mockSecureStorage.read(key: StorageKeys.walletAddressDst))
|
|
||||||
.thenAnswer((_) async => 'dst1DST');
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final result = await accountService.getWalletAddresses();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result, isNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('logout', () {
|
|
||||||
test('should clear all stored data', () async {
|
|
||||||
// Arrange
|
|
||||||
when(() => mockSecureStorage.deleteAll()).thenAnswer((_) async {});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await accountService.logout();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
verify(() => mockSecureStorage.deleteAll()).called(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('CreateAccountResponse', () {
|
|
||||||
test('should parse JSON correctly', () {
|
|
||||||
// Arrange
|
|
||||||
final json = {
|
|
||||||
'userId': '123456789',
|
|
||||||
'accountSequence': 42,
|
|
||||||
'referralCode': 'TESTCD',
|
|
||||||
'mnemonic': 'word1 word2 word3',
|
|
||||||
'clientShareData': 'share-data',
|
|
||||||
'publicKey': 'pub-key',
|
|
||||||
'walletAddresses': {
|
|
||||||
'kava': '0xKAVA',
|
|
||||||
'dst': 'dst1DST',
|
|
||||||
'bsc': '0xBSC',
|
|
||||||
},
|
|
||||||
'accessToken': 'access-token',
|
|
||||||
'refreshToken': 'refresh-token',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final response = CreateAccountResponse.fromJson(json);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(response.userId, '123456789');
|
|
||||||
expect(response.accountSequence, 42);
|
|
||||||
expect(response.referralCode, 'TESTCD');
|
|
||||||
expect(response.mnemonic, 'word1 word2 word3');
|
|
||||||
expect(response.clientShareData, 'share-data');
|
|
||||||
expect(response.publicKey, 'pub-key');
|
|
||||||
expect(response.walletAddresses.kava, '0xKAVA');
|
|
||||||
expect(response.walletAddresses.dst, 'dst1DST');
|
|
||||||
expect(response.walletAddresses.bsc, '0xBSC');
|
|
||||||
expect(response.accessToken, 'access-token');
|
|
||||||
expect(response.refreshToken, 'refresh-token');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle nullable fields', () {
|
|
||||||
// Arrange
|
|
||||||
final json = {
|
|
||||||
'userId': '123456789',
|
|
||||||
'accountSequence': 1,
|
|
||||||
'referralCode': 'ABC123',
|
|
||||||
'walletAddresses': {
|
|
||||||
'kava': '0xKAVA',
|
|
||||||
'dst': 'dst1DST',
|
|
||||||
'bsc': '0xBSC',
|
|
||||||
},
|
|
||||||
'accessToken': 'token',
|
|
||||||
'refreshToken': 'refresh',
|
|
||||||
// mnemonic, clientShareData, publicKey are null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final response = CreateAccountResponse.fromJson(json);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(response.mnemonic, isNull);
|
|
||||||
expect(response.clientShareData, isNull);
|
|
||||||
expect(response.publicKey, isNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('CreateAccountRequest', () {
|
|
||||||
test('should serialize to JSON correctly', () {
|
|
||||||
// Arrange
|
|
||||||
final request = CreateAccountRequest(
|
|
||||||
deviceId: 'device-123',
|
|
||||||
deviceName: 'iPhone 15',
|
|
||||||
inviterReferralCode: 'INVITE',
|
|
||||||
provinceCode: '110000',
|
|
||||||
cityCode: '110100',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final json = request.toJson();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(json['deviceId'], 'device-123');
|
|
||||||
expect(json['deviceName'], 'iPhone 15');
|
|
||||||
expect(json['inviterReferralCode'], 'INVITE');
|
|
||||||
expect(json['provinceCode'], '110000');
|
|
||||||
expect(json['cityCode'], '110100');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should exclude null fields from JSON', () {
|
|
||||||
// Arrange
|
|
||||||
final request = CreateAccountRequest(
|
|
||||||
deviceId: 'device-123',
|
|
||||||
// All optional fields are null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final json = request.toJson();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(json['deviceId'], 'device-123');
|
|
||||||
expect(json.containsKey('deviceName'), false);
|
|
||||||
expect(json.containsKey('inviterReferralCode'), false);
|
|
||||||
expect(json.containsKey('provinceCode'), false);
|
|
||||||
expect(json.containsKey('cityCode'), false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:rwa_android_app/core/services/mpc_share_service.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('MpcShareService', () {
|
|
||||||
group('generateMnemonic', () {
|
|
||||||
test('should generate valid 12-word BIP39 mnemonic', () {
|
|
||||||
// Act
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
final words = mnemonic.split(' ');
|
|
||||||
expect(words.length, 12, reason: 'Should generate 12 words');
|
|
||||||
expect(MpcShareService.validateMnemonic(mnemonic), true,
|
|
||||||
reason: 'Generated mnemonic should be valid BIP39');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should generate different mnemonics each time', () {
|
|
||||||
// Act
|
|
||||||
final mnemonic1 = MpcShareService.generateMnemonic();
|
|
||||||
final mnemonic2 = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mnemonic1, isNot(equals(mnemonic2)),
|
|
||||||
reason: 'Each mnemonic should be unique (random)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('createShareBackup', () {
|
|
||||||
test('should create backup with all required fields', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'mock-mpc-share-data-256bit';
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final backup = MpcShareService.createShareBackup(shareData);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(backup.mnemonic, isNotEmpty);
|
|
||||||
expect(backup.encryptedShare, isNotEmpty);
|
|
||||||
expect(backup.iv, isNotEmpty);
|
|
||||||
expect(backup.authTag, isNotEmpty);
|
|
||||||
expect(backup.mnemonicWords.length, 12);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should create valid BIP39 mnemonic', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'test-share-for-mnemonic';
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final backup = MpcShareService.createShareBackup(shareData);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(MpcShareService.validateMnemonic(backup.mnemonic), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw on empty share data', () {
|
|
||||||
// Act & Assert
|
|
||||||
expect(
|
|
||||||
() => MpcShareService.createShareBackup(''),
|
|
||||||
throwsA(isA<ArgumentError>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('encryptShare and decryptShare', () {
|
|
||||||
test('should encrypt and decrypt share correctly', () {
|
|
||||||
// Arrange
|
|
||||||
const originalShare = 'secret-mpc-share-data-to-encrypt';
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final encrypted = MpcShareService.encryptShare(originalShare, mnemonic);
|
|
||||||
final decrypted = MpcShareService.decryptShare(
|
|
||||||
ciphertext: encrypted.ciphertext,
|
|
||||||
iv: encrypted.iv,
|
|
||||||
authTag: encrypted.authTag,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(decrypted, originalShare);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should produce different ciphertext with different IVs', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'same-share-data';
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final encrypted1 = MpcShareService.encryptShare(shareData, mnemonic);
|
|
||||||
final encrypted2 = MpcShareService.encryptShare(shareData, mnemonic);
|
|
||||||
|
|
||||||
// Assert - different IVs should produce different ciphertext
|
|
||||||
expect(encrypted1.iv, isNot(equals(encrypted2.iv)));
|
|
||||||
expect(encrypted1.ciphertext, isNot(equals(encrypted2.ciphertext)));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should fail decryption with wrong mnemonic', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'secret-share';
|
|
||||||
final correctMnemonic = MpcShareService.generateMnemonic();
|
|
||||||
final wrongMnemonic = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
final encrypted = MpcShareService.encryptShare(shareData, correctMnemonic);
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
expect(
|
|
||||||
() => MpcShareService.decryptShare(
|
|
||||||
ciphertext: encrypted.ciphertext,
|
|
||||||
iv: encrypted.iv,
|
|
||||||
authTag: encrypted.authTag,
|
|
||||||
mnemonic: wrongMnemonic,
|
|
||||||
),
|
|
||||||
throwsA(isA<StateError>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should fail decryption with tampered ciphertext', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'secret-share';
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
final encrypted = MpcShareService.encryptShare(shareData, mnemonic);
|
|
||||||
|
|
||||||
// Tamper with ciphertext
|
|
||||||
final tamperedBytes = base64Decode(encrypted.ciphertext);
|
|
||||||
tamperedBytes[0] = (tamperedBytes[0] + 1) % 256;
|
|
||||||
final tamperedCiphertext = base64Encode(tamperedBytes);
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
expect(
|
|
||||||
() => MpcShareService.decryptShare(
|
|
||||||
ciphertext: tamperedCiphertext,
|
|
||||||
iv: encrypted.iv,
|
|
||||||
authTag: encrypted.authTag,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
),
|
|
||||||
throwsA(isA<StateError>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('recoverShare', () {
|
|
||||||
test('should recover original share from backup', () {
|
|
||||||
// Arrange
|
|
||||||
const originalShare = 'original-mpc-share-256bit-data';
|
|
||||||
final backup = MpcShareService.createShareBackup(originalShare);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final recovered = MpcShareService.recoverShare(
|
|
||||||
mnemonic: backup.mnemonic,
|
|
||||||
encryptedShare: backup.encryptedShare,
|
|
||||||
iv: backup.iv,
|
|
||||||
authTag: backup.authTag,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(recovered, originalShare);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should recover using MpcShareBackup.recoverShare()', () {
|
|
||||||
// Arrange
|
|
||||||
const originalShare = 'test-share-for-backup-recovery';
|
|
||||||
final backup = MpcShareService.createShareBackup(originalShare);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final recovered = backup.recoverShare();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(recovered, originalShare);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw on invalid mnemonic format', () {
|
|
||||||
// Arrange
|
|
||||||
final backup = MpcShareService.createShareBackup('test-share');
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
expect(
|
|
||||||
() => MpcShareService.recoverShare(
|
|
||||||
mnemonic: 'invalid mnemonic words',
|
|
||||||
encryptedShare: backup.encryptedShare,
|
|
||||||
iv: backup.iv,
|
|
||||||
authTag: backup.authTag,
|
|
||||||
),
|
|
||||||
throwsA(isA<ArgumentError>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle complex base64 share data', () {
|
|
||||||
// Arrange - simulate real 256-bit MPC share
|
|
||||||
final complexBytes = List.generate(32, (i) => (i * 17 + 5) % 256);
|
|
||||||
final complexShare = base64Encode(complexBytes);
|
|
||||||
final backup = MpcShareService.createShareBackup(complexShare);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final recovered = backup.recoverShare();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(recovered, complexShare);
|
|
||||||
expect(base64Decode(recovered).length, 32);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('validateMnemonic', () {
|
|
||||||
test('should return true for valid 12-word mnemonic', () {
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
expect(MpcShareService.validateMnemonic(mnemonic), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false for invalid mnemonic', () {
|
|
||||||
expect(MpcShareService.validateMnemonic('invalid words here'), false);
|
|
||||||
expect(MpcShareService.validateMnemonic(''), false);
|
|
||||||
expect(
|
|
||||||
MpcShareService.validateMnemonic(
|
|
||||||
'one two three four five six seven eight nine ten eleven twelve',
|
|
||||||
),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('MpcShareBackup', () {
|
|
||||||
test('should serialize to JSON correctly', () {
|
|
||||||
// Arrange
|
|
||||||
final backup = MpcShareBackup(
|
|
||||||
mnemonic: 'word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12',
|
|
||||||
encryptedShare: 'encrypted-data-base64',
|
|
||||||
iv: 'iv-base64',
|
|
||||||
authTag: 'auth-tag-base64',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final json = backup.toJson();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(json['mnemonic'], backup.mnemonic);
|
|
||||||
expect(json['encryptedShare'], 'encrypted-data-base64');
|
|
||||||
expect(json['iv'], 'iv-base64');
|
|
||||||
expect(json['authTag'], 'auth-tag-base64');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should deserialize from JSON correctly', () {
|
|
||||||
// Arrange
|
|
||||||
final json = {
|
|
||||||
'mnemonic': 'word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12',
|
|
||||||
'encryptedShare': 'encrypted-data-base64',
|
|
||||||
'iv': 'iv-base64',
|
|
||||||
'authTag': 'auth-tag-base64',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final backup = MpcShareBackup.fromJson(json);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(backup.mnemonic, json['mnemonic']);
|
|
||||||
expect(backup.encryptedShare, 'encrypted-data-base64');
|
|
||||||
expect(backup.iv, 'iv-base64');
|
|
||||||
expect(backup.authTag, 'auth-tag-base64');
|
|
||||||
expect(backup.mnemonicWords.length, 12);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('bytesToHex and hexToBytes', () {
|
|
||||||
test('should convert bytes to hex string', () {
|
|
||||||
final bytes = Uint8List.fromList([0, 15, 16, 255]);
|
|
||||||
final hex = MpcShareService.bytesToHex(bytes);
|
|
||||||
expect(hex, '000f10ff');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should convert hex string to bytes', () {
|
|
||||||
const hex = '000f10ff';
|
|
||||||
final bytes = MpcShareService.hexToBytes(hex);
|
|
||||||
expect(bytes, Uint8List.fromList([0, 15, 16, 255]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should round-trip bytes through hex', () {
|
|
||||||
final original = Uint8List.fromList(List.generate(32, (i) => i * 8 % 256));
|
|
||||||
final hex = MpcShareService.bytesToHex(original);
|
|
||||||
final recovered = MpcShareService.hexToBytes(hex);
|
|
||||||
expect(recovered, original);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Security properties', () {
|
|
||||||
test('same share with same mnemonic should produce same decrypted result', () {
|
|
||||||
// Arrange
|
|
||||||
const shareData = 'consistent-share-data';
|
|
||||||
final mnemonic = MpcShareService.generateMnemonic();
|
|
||||||
|
|
||||||
// Encrypt twice with same mnemonic (different IVs)
|
|
||||||
final encrypted1 = MpcShareService.encryptShare(shareData, mnemonic);
|
|
||||||
final encrypted2 = MpcShareService.encryptShare(shareData, mnemonic);
|
|
||||||
|
|
||||||
// Act - decrypt both
|
|
||||||
final decrypted1 = MpcShareService.decryptShare(
|
|
||||||
ciphertext: encrypted1.ciphertext,
|
|
||||||
iv: encrypted1.iv,
|
|
||||||
authTag: encrypted1.authTag,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
);
|
|
||||||
final decrypted2 = MpcShareService.decryptShare(
|
|
||||||
ciphertext: encrypted2.ciphertext,
|
|
||||||
iv: encrypted2.iv,
|
|
||||||
authTag: encrypted2.authTag,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(decrypted1, shareData);
|
|
||||||
expect(decrypted2, shareData);
|
|
||||||
expect(decrypted1, decrypted2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('full round-trip: share -> backup -> recover', () {
|
|
||||||
// Arrange - simulate full MPC workflow
|
|
||||||
final originalShare = base64Encode(
|
|
||||||
List.generate(64, (i) => (i * 13 + 7) % 256),
|
|
||||||
); // 512-bit share data
|
|
||||||
|
|
||||||
// Act
|
|
||||||
final backup = MpcShareService.createShareBackup(originalShare);
|
|
||||||
|
|
||||||
// User stores mnemonic, device stores encrypted data
|
|
||||||
final storedMnemonic = backup.mnemonic;
|
|
||||||
final storedEncrypted = backup.encryptedShare;
|
|
||||||
final storedIv = backup.iv;
|
|
||||||
final storedAuthTag = backup.authTag;
|
|
||||||
|
|
||||||
// Later: user enters mnemonic to recover
|
|
||||||
final recoveredShare = MpcShareService.recoverShare(
|
|
||||||
mnemonic: storedMnemonic,
|
|
||||||
encryptedShare: storedEncrypted,
|
|
||||||
iv: storedIv,
|
|
||||||
authTag: storedAuthTag,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(recoveredShare, originalShare);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +1,11 @@
|
||||||
// This is a basic Flutter widget test.
|
// Basic widget test placeholder
|
||||||
//
|
// TODO: Add proper widget tests for the app
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:rwa_android_app/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('App smoke test placeholder', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// Placeholder test - actual tests should be implemented
|
||||||
await tester.pumpWidget(const MyApp());
|
expect(true, isTrue);
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue