475 lines
16 KiB
TypeScript
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 = [];
|
|
}
|
|
}
|