rwadurian/backend/services/identity-service/src/domain/value-objects/index.ts

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