311 lines
8.3 KiB
TypeScript
311 lines
8.3 KiB
TypeScript
import { DomainError } from '@/shared/exceptions/domain.exception';
|
|
import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
import * as bip39 from '@scure/bip39';
|
|
import { wordlist } from '@scure/bip39/wordlists/english';
|
|
|
|
// ============ UserId ============
|
|
export class UserId {
|
|
constructor(public readonly value: bigint) {
|
|
// 允许 0 作为临时值(表示未持久化的新账户)
|
|
if (value === null || value === undefined) {
|
|
throw new DomainError('UserId不能为空');
|
|
}
|
|
}
|
|
|
|
static create(value: bigint | string | number): UserId {
|
|
if (typeof value === 'string') {
|
|
return new UserId(BigInt(value));
|
|
}
|
|
if (typeof value === 'number') {
|
|
return new UserId(BigInt(value));
|
|
}
|
|
return new UserId(value);
|
|
}
|
|
|
|
equals(other: UserId): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
|
|
toString(): string {
|
|
return this.value.toString();
|
|
}
|
|
}
|
|
|
|
// ============ AccountSequence ============
|
|
export class AccountSequence {
|
|
constructor(public readonly value: number) {
|
|
if (value <= 0) throw new DomainError('账户序列号必须大于0');
|
|
}
|
|
|
|
static create(value: number): AccountSequence {
|
|
return new AccountSequence(value);
|
|
}
|
|
|
|
static next(current: AccountSequence): AccountSequence {
|
|
return new AccountSequence(current.value + 1);
|
|
}
|
|
|
|
equals(other: AccountSequence): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
}
|
|
|
|
// ============ PhoneNumber ============
|
|
export class PhoneNumber {
|
|
constructor(public readonly value: string) {
|
|
if (!/^1[3-9]\d{9}$/.test(value)) {
|
|
throw new DomainError('手机号格式错误');
|
|
}
|
|
}
|
|
|
|
static create(value: string): PhoneNumber {
|
|
return new PhoneNumber(value);
|
|
}
|
|
|
|
equals(other: PhoneNumber): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
|
|
masked(): string {
|
|
return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
}
|
|
}
|
|
|
|
// ============ ReferralCode ============
|
|
export class ReferralCode {
|
|
constructor(public readonly value: string) {
|
|
if (!/^[A-Z0-9]{6}$/.test(value)) {
|
|
throw new DomainError('推荐码格式错误');
|
|
}
|
|
}
|
|
|
|
static generate(): ReferralCode {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
let code = '';
|
|
for (let i = 0; i < 6; i++) {
|
|
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return new ReferralCode(code);
|
|
}
|
|
|
|
static create(value: string): ReferralCode {
|
|
return new ReferralCode(value.toUpperCase());
|
|
}
|
|
|
|
equals(other: ReferralCode): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
}
|
|
|
|
// ============ ProvinceCode & CityCode ============
|
|
export class ProvinceCode {
|
|
constructor(public readonly value: string) {}
|
|
|
|
static create(value: string): ProvinceCode {
|
|
return new ProvinceCode(value || 'DEFAULT');
|
|
}
|
|
}
|
|
|
|
export class CityCode {
|
|
constructor(public readonly value: string) {}
|
|
|
|
static create(value: string): CityCode {
|
|
return new CityCode(value || 'DEFAULT');
|
|
}
|
|
}
|
|
|
|
// ============ Mnemonic ============
|
|
export class Mnemonic {
|
|
constructor(public readonly value: string) {
|
|
if (!bip39.validateMnemonic(value, wordlist)) {
|
|
throw new DomainError('助记词格式错误');
|
|
}
|
|
}
|
|
|
|
static generate(): Mnemonic {
|
|
const mnemonic = bip39.generateMnemonic(wordlist, 128);
|
|
return new Mnemonic(mnemonic);
|
|
}
|
|
|
|
static create(value: string): Mnemonic {
|
|
return new Mnemonic(value);
|
|
}
|
|
|
|
toSeed(): Uint8Array {
|
|
return bip39.mnemonicToSeedSync(this.value);
|
|
}
|
|
|
|
getWords(): string[] {
|
|
return this.value.split(' ');
|
|
}
|
|
|
|
equals(other: Mnemonic): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
}
|
|
|
|
// ============ HardwareInfo ============
|
|
export interface HardwareInfo {
|
|
platform?: string; // ios, android, web
|
|
deviceModel?: string; // iPhone 15 Pro, Pixel 8
|
|
osVersion?: string; // iOS 17.2, Android 14
|
|
appVersion?: string; // 1.0.0
|
|
screenWidth?: number;
|
|
screenHeight?: number;
|
|
locale?: string; // zh-CN, en-US
|
|
timezone?: string; // Asia/Shanghai
|
|
}
|
|
|
|
// ============ DeviceInfo ============
|
|
export class DeviceInfo {
|
|
private _lastActiveAt: Date;
|
|
private _hardwareInfo: HardwareInfo;
|
|
|
|
constructor(
|
|
public readonly deviceId: string,
|
|
public readonly deviceName: string,
|
|
public readonly addedAt: Date,
|
|
lastActiveAt: Date,
|
|
hardwareInfo?: HardwareInfo,
|
|
) {
|
|
this._lastActiveAt = lastActiveAt;
|
|
this._hardwareInfo = hardwareInfo || {};
|
|
}
|
|
|
|
get lastActiveAt(): Date {
|
|
return this._lastActiveAt;
|
|
}
|
|
|
|
get hardwareInfo(): HardwareInfo {
|
|
return this._hardwareInfo;
|
|
}
|
|
|
|
get platform(): string | undefined {
|
|
return this._hardwareInfo.platform;
|
|
}
|
|
|
|
get deviceModel(): string | undefined {
|
|
return this._hardwareInfo.deviceModel;
|
|
}
|
|
|
|
get osVersion(): string | undefined {
|
|
return this._hardwareInfo.osVersion;
|
|
}
|
|
|
|
get appVersion(): string | undefined {
|
|
return this._hardwareInfo.appVersion;
|
|
}
|
|
|
|
updateActivity(): void {
|
|
this._lastActiveAt = new Date();
|
|
}
|
|
|
|
updateHardwareInfo(info: HardwareInfo): void {
|
|
this._hardwareInfo = { ...this._hardwareInfo, ...info };
|
|
}
|
|
}
|
|
|
|
// ============ ChainType ============
|
|
export enum ChainType {
|
|
KAVA = 'KAVA',
|
|
DST = 'DST',
|
|
BSC = 'BSC',
|
|
}
|
|
|
|
export const CHAIN_CONFIG = {
|
|
[ChainType.KAVA]: { prefix: 'kava', derivationPath: "m/44'/459'/0'/0/0" },
|
|
[ChainType.DST]: { prefix: 'dst', derivationPath: "m/44'/118'/0'/0/0" },
|
|
[ChainType.BSC]: { prefix: '0x', derivationPath: "m/44'/60'/0'/0/0" },
|
|
};
|
|
|
|
// ============ KYCInfo ============
|
|
export class KYCInfo {
|
|
constructor(
|
|
public readonly realName: string,
|
|
public readonly idCardNumber: string,
|
|
public readonly idCardFrontUrl: string,
|
|
public readonly idCardBackUrl: string,
|
|
) {
|
|
if (!realName || realName.length < 2) {
|
|
throw new DomainError('真实姓名不合法');
|
|
}
|
|
if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/.test(idCardNumber)) {
|
|
throw new DomainError('身份证号格式错误');
|
|
}
|
|
}
|
|
|
|
static create(params: { realName: string; idCardNumber: string; idCardFrontUrl: string; idCardBackUrl: string }): KYCInfo {
|
|
return new KYCInfo(params.realName, params.idCardNumber, params.idCardFrontUrl, params.idCardBackUrl);
|
|
}
|
|
|
|
maskedIdCardNumber(): string {
|
|
return this.idCardNumber.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2');
|
|
}
|
|
}
|
|
|
|
// ============ Enums ============
|
|
export enum KYCStatus {
|
|
NOT_VERIFIED = 'NOT_VERIFIED',
|
|
PENDING = 'PENDING',
|
|
VERIFIED = 'VERIFIED',
|
|
REJECTED = 'REJECTED',
|
|
}
|
|
|
|
export enum AccountStatus {
|
|
ACTIVE = 'ACTIVE',
|
|
FROZEN = 'FROZEN',
|
|
DEACTIVATED = 'DEACTIVATED',
|
|
}
|
|
|
|
export enum AddressStatus {
|
|
ACTIVE = 'ACTIVE',
|
|
DISABLED = 'DISABLED',
|
|
}
|
|
|
|
// ============ AddressId ============
|
|
export class AddressId {
|
|
constructor(public readonly value: string) {}
|
|
|
|
static generate(): AddressId {
|
|
return new AddressId(crypto.randomUUID());
|
|
}
|
|
|
|
static create(value: string): AddressId {
|
|
return new AddressId(value);
|
|
}
|
|
}
|
|
|
|
// ============ MnemonicEncryption ============
|
|
export class MnemonicEncryption {
|
|
static encrypt(mnemonic: string, key: string): string {
|
|
const derivedKey = this.deriveKey(key);
|
|
const iv = randomBytes(16);
|
|
const cipher = createCipheriv('aes-256-gcm', derivedKey, iv);
|
|
|
|
let encrypted = cipher.update(mnemonic, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
return JSON.stringify({
|
|
encrypted,
|
|
authTag: authTag.toString('hex'),
|
|
iv: iv.toString('hex'),
|
|
});
|
|
}
|
|
|
|
static decrypt(encryptedData: string, key: string): string {
|
|
const { encrypted, authTag, iv } = JSON.parse(encryptedData);
|
|
const derivedKey = this.deriveKey(key);
|
|
const decipher = createDecipheriv('aes-256-gcm', derivedKey, Buffer.from(iv, 'hex'));
|
|
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
|
|
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
return decrypted;
|
|
}
|
|
|
|
private static deriveKey(password: string): Buffer {
|
|
return scryptSync(password, 'rwa-wallet-salt', 32);
|
|
}
|
|
}
|