rwadurian/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts

475 lines
16 KiB
TypeScript

import Decimal from 'decimal.js';
import {
UserId, WalletId, AssetType, WalletStatus, Money, Balance, Hashpower,
} from '@/domain/value-objects';
import {
DomainEvent, DepositCompletedEvent, BalanceDeductedEvent,
RewardAddedEvent, RewardMovedToSettleableEvent, RewardExpiredEvent,
SettlementCompletedEvent,
} from '@/domain/events';
import {
DomainError, InsufficientBalanceError, WalletFrozenError,
} from '@/shared/exceptions/domain.exception';
export interface WalletBalances {
usdt: Balance;
dst: Balance;
bnb: Balance;
og: Balance;
rwad: Balance;
}
export interface WalletRewards {
pendingUsdt: Money;
pendingHashpower: Hashpower;
pendingExpireAt: Date | null;
settleableUsdt: Money;
settleableHashpower: Hashpower;
settledTotalUsdt: Money;
settledTotalHashpower: Hashpower;
expiredTotalUsdt: Money;
expiredTotalHashpower: Hashpower;
}
export class WalletAccount {
private readonly _walletId: WalletId;
private readonly _accountSequence: string; // 跨服务关联标识 (全局唯一业务ID)
private readonly _userId: UserId; // 保留兼容
private _balances: WalletBalances;
private _hashpower: Hashpower;
private _rewards: WalletRewards;
private _status: WalletStatus;
private _hasPlanted: boolean; // 是否已认种过
private readonly _createdAt: Date;
private _updatedAt: Date;
private _domainEvents: DomainEvent[] = [];
private constructor(
walletId: WalletId,
accountSequence: string,
userId: UserId,
balances: WalletBalances,
hashpower: Hashpower,
rewards: WalletRewards,
status: WalletStatus,
hasPlanted: boolean,
createdAt: Date,
updatedAt: Date,
) {
this._walletId = walletId;
this._accountSequence = accountSequence;
this._userId = userId;
this._balances = balances;
this._hashpower = hashpower;
this._rewards = rewards;
this._status = status;
this._hasPlanted = hasPlanted;
this._createdAt = createdAt;
this._updatedAt = updatedAt;
}
// Getters
get walletId(): WalletId { return this._walletId; }
get accountSequence(): string { return this._accountSequence; }
get userId(): UserId { return this._userId; }
get balances(): WalletBalances { return this._balances; }
get hashpower(): Hashpower { return this._hashpower; }
get rewards(): WalletRewards { return this._rewards; }
get status(): WalletStatus { return this._status; }
get createdAt(): Date { return this._createdAt; }
get updatedAt(): Date { return this._updatedAt; }
get hasPlanted(): boolean { return this._hasPlanted; }
get isActive(): boolean { return this._status === WalletStatus.ACTIVE; }
get domainEvents(): DomainEvent[] { return [...this._domainEvents]; }
static createNew(accountSequence: string, userId: UserId): WalletAccount {
const now = new Date();
return new WalletAccount(
WalletId.create(0), // Will be set by database
accountSequence,
userId,
{
usdt: Balance.zero('USDT'),
dst: Balance.zero('DST'),
bnb: Balance.zero('BNB'),
og: Balance.zero('OG'),
rwad: Balance.zero('RWAD'),
},
Hashpower.zero(),
{
pendingUsdt: Money.zero('USDT'),
pendingHashpower: Hashpower.zero(),
pendingExpireAt: null,
settleableUsdt: Money.zero('USDT'),
settleableHashpower: Hashpower.zero(),
settledTotalUsdt: Money.zero('USDT'),
settledTotalHashpower: Hashpower.zero(),
expiredTotalUsdt: Money.zero('USDT'),
expiredTotalHashpower: Hashpower.zero(),
},
WalletStatus.ACTIVE,
false, // hasPlanted
now,
now,
);
}
static reconstruct(params: {
walletId: bigint;
accountSequence: string;
userId: bigint;
usdtAvailable: Decimal;
usdtFrozen: Decimal;
dstAvailable: Decimal;
dstFrozen: Decimal;
bnbAvailable: Decimal;
bnbFrozen: Decimal;
ogAvailable: Decimal;
ogFrozen: Decimal;
rwadAvailable: Decimal;
rwadFrozen: Decimal;
hashpower: Decimal;
pendingUsdt: Decimal;
pendingHashpower: Decimal;
pendingExpireAt: Date | null;
settleableUsdt: Decimal;
settleableHashpower: Decimal;
settledTotalUsdt: Decimal;
settledTotalHashpower: Decimal;
expiredTotalUsdt: Decimal;
expiredTotalHashpower: Decimal;
status: string;
hasPlanted: boolean;
createdAt: Date;
updatedAt: Date;
}): WalletAccount {
return new WalletAccount(
WalletId.create(params.walletId),
params.accountSequence,
UserId.create(params.userId),
{
usdt: Balance.create(Money.USDT(params.usdtAvailable), Money.USDT(params.usdtFrozen)),
dst: Balance.create(Money.DST(params.dstAvailable), Money.DST(params.dstFrozen)),
bnb: Balance.create(Money.BNB(params.bnbAvailable), Money.BNB(params.bnbFrozen)),
og: Balance.create(Money.OG(params.ogAvailable), Money.OG(params.ogFrozen)),
rwad: Balance.create(Money.RWAD(params.rwadAvailable), Money.RWAD(params.rwadFrozen)),
},
Hashpower.create(params.hashpower),
{
pendingUsdt: Money.USDT(params.pendingUsdt),
pendingHashpower: Hashpower.create(params.pendingHashpower),
pendingExpireAt: params.pendingExpireAt,
settleableUsdt: Money.USDT(params.settleableUsdt),
settleableHashpower: Hashpower.create(params.settleableHashpower),
settledTotalUsdt: Money.USDT(params.settledTotalUsdt),
settledTotalHashpower: Hashpower.create(params.settledTotalHashpower),
expiredTotalUsdt: Money.USDT(params.expiredTotalUsdt),
expiredTotalHashpower: Hashpower.create(params.expiredTotalHashpower),
},
params.status as WalletStatus,
params.hasPlanted,
params.createdAt,
params.updatedAt,
);
}
// 标记为已认种
markAsPlanted(): void {
this._hasPlanted = true;
this._updatedAt = new Date();
}
// 充值入账
deposit(amount: Money, chainType: string, txHash: string): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.add(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
this.addDomainEvent(new DepositCompletedEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
amount: amount.value.toString(),
assetType: amount.currency,
chainType,
txHash,
balanceAfter: newBalance.available.value.toString(),
}));
}
// 直接增加可用余额(用于系统账户分配)
addAvailableBalance(amount: Money): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.add(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
}
// 扣款 (如认种支付)
deduct(amount: Money, reason: string, refOrderId?: string): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
if (balance.available.lessThan(amount)) {
throw new InsufficientBalanceError(
amount.currency,
amount.value.toString(),
balance.available.value.toString(),
);
}
const newBalance = balance.deduct(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
this.addDomainEvent(new BalanceDeductedEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
amount: amount.value.toString(),
assetType: amount.currency,
reason,
refOrderId,
balanceAfter: newBalance.available.value.toString(),
}));
}
// 冻结资金
freeze(amount: Money): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.freeze(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
}
// 解冻资金
unfreeze(amount: Money): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.unfreeze(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
}
// 从冻结余额扣款
deductFrozen(amount: Money, reason: string, refOrderId?: string): void {
this.ensureActive();
const balance = this.getBalance(amount.currency as AssetType);
const newBalance = balance.deductFrozen(amount);
this.setBalance(amount.currency as AssetType, newBalance);
this._updatedAt = new Date();
this.addDomainEvent(new BalanceDeductedEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
amount: amount.value.toString(),
assetType: amount.currency,
reason,
refOrderId,
balanceAfter: newBalance.available.value.toString(),
}));
}
// 添加待领取奖励
addPendingReward(usdtAmount: Money, hashpowerAmount: Hashpower, expireAt: Date, refOrderId?: string): void {
this.ensureActive();
this._rewards = {
...this._rewards,
pendingUsdt: this._rewards.pendingUsdt.add(usdtAmount),
pendingHashpower: this._rewards.pendingHashpower.add(hashpowerAmount),
pendingExpireAt: expireAt,
};
this._updatedAt = new Date();
this.addDomainEvent(new RewardAddedEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
usdtAmount: usdtAmount.value.toString(),
hashpowerAmount: hashpowerAmount.value.toString(),
expireAt: expireAt.toISOString(),
refOrderId,
}));
}
// 待领取 -> 可结算
movePendingToSettleable(): void {
this.ensureActive();
if (this._rewards.pendingUsdt.isZero() && this._rewards.pendingHashpower.isZero()) {
throw new DomainError('No pending rewards to move');
}
const movedUsdt = this._rewards.pendingUsdt;
const movedHashpower = this._rewards.pendingHashpower;
this._rewards = {
...this._rewards,
pendingUsdt: Money.zero('USDT'),
pendingHashpower: Hashpower.zero(),
pendingExpireAt: null,
settleableUsdt: this._rewards.settleableUsdt.add(movedUsdt),
settleableHashpower: this._rewards.settleableHashpower.add(movedHashpower),
};
// 增加算力
this._hashpower = this._hashpower.add(movedHashpower);
this._updatedAt = new Date();
this.addDomainEvent(new RewardMovedToSettleableEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
usdtAmount: movedUsdt.value.toString(),
hashpowerAmount: movedHashpower.value.toString(),
}));
}
/**
* 直接增加可结算余额(用于 pending_rewards 表方案)
* 当用户认种后,从 pending_rewards 表中结算的金额直接加到可结算余额
*/
addSettleableReward(usdtAmount: Money, hashpowerAmount: Hashpower): void {
this.ensureActive();
this._rewards = {
...this._rewards,
settleableUsdt: this._rewards.settleableUsdt.add(usdtAmount),
settleableHashpower: this._rewards.settleableHashpower.add(hashpowerAmount),
};
// 增加算力
this._hashpower = this._hashpower.add(hashpowerAmount);
this._updatedAt = new Date();
this.addDomainEvent(new RewardMovedToSettleableEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
usdtAmount: usdtAmount.value.toString(),
hashpowerAmount: hashpowerAmount.value.toString(),
}));
}
// 奖励过期
expirePendingRewards(): void {
if (this._rewards.pendingUsdt.isZero() && this._rewards.pendingHashpower.isZero()) {
return;
}
const expiredUsdt = this._rewards.pendingUsdt;
const expiredHashpower = this._rewards.pendingHashpower;
this._rewards = {
...this._rewards,
pendingUsdt: Money.zero('USDT'),
pendingHashpower: Hashpower.zero(),
pendingExpireAt: null,
expiredTotalUsdt: this._rewards.expiredTotalUsdt.add(expiredUsdt),
expiredTotalHashpower: this._rewards.expiredTotalHashpower.add(expiredHashpower),
};
this._updatedAt = new Date();
this.addDomainEvent(new RewardExpiredEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
usdtAmount: expiredUsdt.value.toString(),
hashpowerAmount: expiredHashpower.value.toString(),
}));
}
// 结算奖励
settleRewards(usdtAmount: Money, settleCurrency: string, receivedAmount: Money, settlementOrderId: string, swapTxHash?: string): void {
this.ensureActive();
if (this._rewards.settleableUsdt.lessThan(usdtAmount)) {
throw new InsufficientBalanceError(
'USDT (settleable)',
usdtAmount.value.toString(),
this._rewards.settleableUsdt.value.toString(),
);
}
// 扣减可结算USDT
this._rewards = {
...this._rewards,
settleableUsdt: this._rewards.settleableUsdt.subtract(usdtAmount),
settledTotalUsdt: this._rewards.settledTotalUsdt.add(usdtAmount),
};
// 增加目标币种余额
const targetBalance = this.getBalance(settleCurrency as AssetType);
this.setBalance(settleCurrency as AssetType, targetBalance.add(receivedAmount));
this._updatedAt = new Date();
this.addDomainEvent(new SettlementCompletedEvent({
userId: this._userId.toString(),
walletId: this._walletId.toString(),
settlementOrderId,
usdtAmount: usdtAmount.value.toString(),
settleCurrency,
receivedAmount: receivedAmount.value.toString(),
swapTxHash,
}));
}
// 冻结钱包
freezeWallet(): void {
if (this._status === WalletStatus.FROZEN) {
throw new DomainError('Wallet is already frozen');
}
this._status = WalletStatus.FROZEN;
this._updatedAt = new Date();
}
// 解冻钱包
unfreezeWallet(): void {
if (this._status !== WalletStatus.FROZEN) {
throw new DomainError('Wallet is not frozen');
}
this._status = WalletStatus.ACTIVE;
this._updatedAt = new Date();
}
private getBalance(assetType: AssetType): Balance {
switch (assetType) {
case AssetType.USDT: return this._balances.usdt;
case AssetType.DST: return this._balances.dst;
case AssetType.BNB: return this._balances.bnb;
case AssetType.OG: return this._balances.og;
case AssetType.RWAD: return this._balances.rwad;
default: throw new DomainError(`Unknown asset type: ${assetType}`);
}
}
private setBalance(assetType: AssetType, balance: Balance): void {
switch (assetType) {
case AssetType.USDT: this._balances.usdt = balance; break;
case AssetType.DST: this._balances.dst = balance; break;
case AssetType.BNB: this._balances.bnb = balance; break;
case AssetType.OG: this._balances.og = balance; break;
case AssetType.RWAD: this._balances.rwad = balance; break;
default: throw new DomainError(`Unknown asset type: ${assetType}`);
}
}
private ensureActive(): void {
if (this._status !== WalletStatus.ACTIVE) {
throw new WalletFrozenError(this._walletId.toString());
}
}
private addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents = [];
}
}