feat: split share pool into A (100亿) and B (200万) accounts

Backend changes:
- mining-wallet-service: Split SHARE_POOL into SHARE_POOL_A (100亿, for burning)
  and SHARE_POOL_B (200万, for mining distribution)
- Add /pool-accounts/share-pool-balance API endpoint to get total balance
- Update pool initialization logic and seed data
- Fix Kong routing for mining-wallet-service (strip_path: true)
- Fix Kong routing for trading-service (strip_path: true)

Constant updates (100.02亿 = 10,002,000,000):
- mining-service: TOTAL_SHARES
- trading-service: TOTAL_SHARES, trading config defaults
- trading-service seed: initial green points = 5760

Frontend changes:
- Add sharePoolBalanceProvider to fetch pool balance from mining-wallet-service
- Update contribution page to display real-time share pool balance (A + B)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-15 05:55:52 -08:00
parent a1508b208e
commit 40869ef00f
15 changed files with 170 additions and 56 deletions

View File

@ -357,18 +357,19 @@ services:
# ---------------------------------------------------------------------------
# Mining Wallet Service 2.0 - 挖矿钱包服务
# 前端路径: /api/v2/mining-wallet/... -> 后端路径: /api/v2/...
# ---------------------------------------------------------------------------
- name: mining-wallet-service
url: http://192.168.1.111:3025
url: http://192.168.1.111:3025/api/v2
routes:
- name: mining-wallet-api
paths:
- /api/v2/mining-wallet
strip_path: false
strip_path: true
- name: mining-wallet-health
paths:
- /api/v2/mining-wallet/health
strip_path: false
strip_path: true
# =============================================================================
# Plugins - 全局插件配置

View File

@ -12,7 +12,7 @@ datasource db {
// 挖矿全局配置
model MiningConfig {
id String @id @default(uuid())
totalShares Decimal @db.Decimal(30, 8) // 总积分股数量 (100.02B)
totalShares Decimal @db.Decimal(30, 8) // 总积分股数量 (100.02亿)
distributionPool Decimal @db.Decimal(30, 8) // 分配池 (200M)
remainingDistribution Decimal @db.Decimal(30, 8) // 剩余可分配
halvingPeriodYears Int @default(2) // 减半周期(年)

View File

@ -19,7 +19,7 @@ async function main() {
const now = new Date();
// 常量
const TOTAL_SHARES = new Decimal('100020000000'); // 100.02B
const TOTAL_SHARES = new Decimal('10002000000'); // 100.02亿
const DISTRIBUTION_POOL = new Decimal('2000000'); // 200万
const ERA1_DISTRIBUTION = new Decimal('1000000'); // 100万第一个两年
const BURN_TARGET = new Decimal('10000000000'); // 100亿

View File

@ -6,8 +6,8 @@ import { Price } from '../value-objects/price.vo';
*
*/
export class MiningCalculatorService {
// 总积分股数量: 100.02B
static readonly TOTAL_SHARES = new ShareAmount('100020000000');
// 总积分股数量: 100.02亿 (SHARE_POOL_A: 100亿 + SHARE_POOL_B: 200万)
static readonly TOTAL_SHARES = new ShareAmount('10002000000');
// 初始分配池: 200M
static readonly INITIAL_DISTRIBUTION_POOL = new ShareAmount('200000000');

View File

@ -27,7 +27,8 @@ enum SystemAccountType {
// 池账户类型
enum PoolAccountType {
SHARE_POOL // 积分股池 - 总股池
SHARE_POOL_A // 积分股池A - 初始100亿
SHARE_POOL_B // 积分股池B - 初始200万
BLACK_HOLE_POOL // 黑洞积分股池 - 销毁池
CIRCULATION_POOL // 流通积分股池 - 交易流通池
}

View File

@ -62,19 +62,28 @@ async function main() {
}
}
// 2. 初始化池账户(积分股池、黑洞池、流通池)
// 2. 初始化池账户积分股池A/B、黑洞池、流通池
// 积分股池A: 100亿 (10,000,000,000) - 用于销毁
// 积分股池B: 200万 (2,000,000) - 用于挖矿分配
// 总计: 100.02亿 (10,002,000,000)
const poolAccounts = [
{
poolType: 'SHARE_POOL',
name: '积分股池',
balance: new Decimal('100000000'), // 1亿初始发行量
description: '挖矿奖励的来源池,总发行量',
poolType: 'SHARE_POOL_A',
name: '积分股池A',
balance: new Decimal('10000000000'), // 100亿初始发行量
description: '销毁池来源初始100亿',
},
{
poolType: 'SHARE_POOL_B',
name: '积分股池B',
balance: new Decimal('2000000'), // 200万初始发行量
description: '挖矿分配池初始200万',
},
{
poolType: 'BLACK_HOLE_POOL',
name: '黑洞池',
balance: new Decimal('0'),
targetBurn: new Decimal('50000000'), // 目标销毁5000万
targetBurn: new Decimal('10000000000'), // 目标销毁100亿
description: '销毁的积分股,用于减少流通量',
},
{

View File

@ -6,7 +6,8 @@ import { PoolAccountType, TransactionType } from '@prisma/client';
import Decimal from 'decimal.js';
class InitializePoolsDto {
sharePool: { name: string; initialBalance?: string };
sharePoolA: { name: string; initialBalance?: string };
sharePoolB: { name: string; initialBalance?: string };
blackHolePool: { name: string; targetBurn: string };
circulationPool: { name: string; initialBalance?: string };
}
@ -78,10 +79,16 @@ export class PoolAccountController {
@ApiResponse({ status: 201, description: '池账户初始化成功' })
async initialize(@Body() dto: InitializePoolsDto) {
return this.poolAccountService.initializePools({
sharePool: {
name: dto.sharePool.name,
initialBalance: dto.sharePool.initialBalance
? new Decimal(dto.sharePool.initialBalance)
sharePoolA: {
name: dto.sharePoolA.name,
initialBalance: dto.sharePoolA.initialBalance
? new Decimal(dto.sharePoolA.initialBalance)
: undefined,
},
sharePoolB: {
name: dto.sharePoolB.name,
initialBalance: dto.sharePoolB.initialBalance
? new Decimal(dto.sharePoolB.initialBalance)
: undefined,
},
blackHolePool: {
@ -97,6 +104,14 @@ export class PoolAccountController {
});
}
@Get('share-pool-balance')
@Public()
@ApiOperation({ summary: '获取积分股池总余量' })
@ApiResponse({ status: 200, description: '积分股池A+B的总余量' })
async getSharePoolTotalBalance() {
return this.poolAccountService.getSharePoolTotalBalance();
}
@Post('burn')
@AdminOnly()
@ApiOperation({ summary: '销毁到黑洞池' })

View File

@ -6,7 +6,11 @@ import Decimal from 'decimal.js';
import { DomainException } from '../../shared/filters/domain-exception.filter';
export interface InitializePoolsInput {
sharePool: {
sharePoolA: {
name: string;
initialBalance?: Decimal;
};
sharePoolB: {
name: string;
initialBalance?: Decimal;
};
@ -30,27 +34,45 @@ export class PoolAccountService {
) {}
/**
*
*
*/
async initializePools(input: InitializePoolsInput): Promise<PoolAccount[]> {
const pools: PoolAccount[] = [];
// 积分股池
const sharePool = await this.poolAccountRepo.create({
poolType: 'SHARE_POOL',
name: input.sharePool.name,
// 积分股池A - 初始100亿
const sharePoolA = await this.poolAccountRepo.create({
poolType: 'SHARE_POOL_A',
name: input.sharePoolA.name,
});
pools.push(sharePool);
pools.push(sharePoolA);
// 如果有初始余额,注入资金
if (input.sharePool.initialBalance?.greaterThan(0)) {
if (input.sharePoolA.initialBalance?.greaterThan(0)) {
await this.poolAccountRepo.updateBalanceWithTransaction(
'SHARE_POOL',
input.sharePool.initialBalance,
'SHARE_POOL_A',
input.sharePoolA.initialBalance,
{
transactionType: 'INITIAL_INJECT',
counterpartyType: 'EXTERNAL',
memo: `初始注入, 数量${input.sharePool.initialBalance.toFixed(8)}`,
memo: `初始注入积分股池A, 数量${input.sharePoolA.initialBalance.toFixed(8)}`,
},
);
}
// 积分股池B - 初始200万
const sharePoolB = await this.poolAccountRepo.create({
poolType: 'SHARE_POOL_B',
name: input.sharePoolB.name,
});
pools.push(sharePoolB);
if (input.sharePoolB.initialBalance?.greaterThan(0)) {
await this.poolAccountRepo.updateBalanceWithTransaction(
'SHARE_POOL_B',
input.sharePoolB.initialBalance,
{
transactionType: 'INITIAL_INJECT',
counterpartyType: 'EXTERNAL',
memo: `初始注入积分股池B, 数量${input.sharePoolB.initialBalance.toFixed(8)}`,
},
);
}
@ -93,7 +115,8 @@ export class PoolAccountService {
type: p.poolType,
name: p.name,
})),
sharePoolInitialBalance: input.sharePool.initialBalance?.toString() || '0',
sharePoolAInitialBalance: input.sharePoolA.initialBalance?.toString() || '0',
sharePoolBInitialBalance: input.sharePoolB.initialBalance?.toString() || '0',
blackHoleTargetBurn: input.blackHolePool.targetBurn.toString(),
circulationPoolInitialBalance: input.circulationPool.initialBalance?.toString() || '0',
initializedAt: new Date().toISOString(),
@ -106,6 +129,7 @@ export class PoolAccountService {
/**
*
* SHARE_POOL_B (200)
*/
async distributeMiningReward(
toAccountSeq: string,
@ -117,10 +141,12 @@ export class PoolAccountService {
referenceId?: string;
},
): Promise<void> {
const sourcePool: PoolAccountType = 'SHARE_POOL_B';
const memo = `挖矿分配给用户[${toAccountSeq}], 算力占比${miningInfo.contributionRatio.mul(100).toFixed(4)}%, 分钟${miningInfo.miningMinute.toISOString()}`;
await this.poolAccountRepo.updateBalanceWithTransaction(
'SHARE_POOL',
sourcePool,
amount.negated(),
{
transactionType: 'MINING_DISTRIBUTE',
@ -139,7 +165,7 @@ export class PoolAccountService {
await this.outboxRepo.create({
aggregateType: 'PoolAccount',
aggregateId: 'SHARE_POOL',
aggregateId: sourcePool,
eventType: 'MINING_DISTRIBUTED',
payload: {
toAccountSeq,
@ -151,6 +177,30 @@ export class PoolAccountService {
});
}
/**
* (SHARE_POOL_A + SHARE_POOL_B)
*/
async getSharePoolTotalBalance(): Promise<{
poolA: string;
poolB: string;
total: string;
}> {
const [poolA, poolB] = await Promise.all([
this.poolAccountRepo.findByType('SHARE_POOL_A'),
this.poolAccountRepo.findByType('SHARE_POOL_B'),
]);
const balanceA = poolA ? new Decimal(poolA.balance.toString()) : new Decimal(0);
const balanceB = poolB ? new Decimal(poolB.balance.toString()) : new Decimal(0);
const total = balanceA.plus(balanceB);
return {
poolA: balanceA.toFixed(8),
poolB: balanceB.toFixed(8),
total: total.toFixed(8),
};
}
/**
*
*/

View File

@ -12,8 +12,8 @@ datasource db {
// 交易全局配置
model TradingConfig {
id String @id @default(uuid())
// 总积分股数量: 100.02B
totalShares Decimal @default(100020000000) @map("total_shares") @db.Decimal(30, 8)
// 总积分股数量: 100.02亿
totalShares Decimal @default(10002000000) @map("total_shares") @db.Decimal(30, 8)
// 目标销毁量: 100亿 (4年销毁完)
burnTarget Decimal @default(10000000000) @map("burn_target") @db.Decimal(30, 8)
// 销毁周期: 4年 (分钟数) 365*4*1440 = 2102400

View File

@ -66,7 +66,7 @@ async function main() {
if (!existingConfig) {
await prisma.tradingConfig.create({
data: {
totalShares: new Decimal('100020000000'), // 100.02B 总积分股
totalShares: new Decimal('10002000000'), // 100.02亿 总积分股
burnTarget: new Decimal('10000000000'), // 100亿目标销毁量
burnPeriodMinutes: 2102400, // 4年 = 365*4*1440 分钟
minuteBurnRate: new Decimal('4756.468797564687'), // 每分钟销毁率
@ -93,17 +93,17 @@ async function main() {
console.log('Black hole account already exists');
}
// 4. 初始化积分股池
// 4. 初始化积分股池(绿积分池)
const existingSharePool = await prisma.sharePool.findFirst();
if (!existingSharePool) {
await prisma.sharePool.create({
data: {
greenPoints: new Decimal(0), // 初始绿积分为 0
totalInflow: new Decimal(0),
greenPoints: new Decimal(5760), // 初始绿积分为 5760
totalInflow: new Decimal(5760),
totalOutflow: new Decimal(0),
},
});
console.log('Created share pool');
console.log('Created share pool with initial green points: 5760');
} else {
console.log('Share pool already exists');
}

View File

@ -14,8 +14,8 @@ import { Money } from '../value-objects/money.vo';
* 7. = ÷ 24 ÷ 60 ÷ 60
*/
export class TradingCalculatorService {
// 总积分股数量: 100.02B
static readonly TOTAL_SHARES = new Decimal('100020000000');
// 总积分股数量: 100.02亿 (SHARE_POOL_A: 100亿 + SHARE_POOL_B: 200万)
static readonly TOTAL_SHARES = new Decimal('10002000000');
// 目标销毁量: 100亿 (4年销毁完)
static readonly BURN_TARGET = new Decimal('10000000000');

View File

@ -35,7 +35,7 @@ export class TradingConfigRepository {
const record = await this.prisma.tradingConfig.create({
data: {
totalShares: new Decimal('100020000000'),
totalShares: new Decimal('10002000000'), // 100.02亿
burnTarget: new Decimal('10000000000'),
burnPeriodMinutes: 2102400, // 365 * 4 * 1440
minuteBurnRate: new Decimal('4756.468797564687'),

View File

@ -59,4 +59,7 @@ class ApiEndpoints {
// Planting Ledger (Kong路由: /api/v2/contribution)
static String plantingLedger(String accountSequence) =>
'/api/v2/contribution/accounts/$accountSequence/planting-ledger';
// Mining Wallet Service 2.0 (Kong路由: /api/v2/mining-wallet)
static const String sharePoolBalance = '/api/v2/mining-wallet/pool-accounts/share-pool-balance';
}

View File

@ -5,10 +5,8 @@ import '../../../core/constants/app_colors.dart';
import '../../../core/router/routes.dart';
import '../../../core/utils/format_utils.dart';
import '../../../domain/entities/contribution.dart';
import '../../../domain/entities/market_overview.dart';
import '../../providers/user_providers.dart';
import '../../providers/contribution_providers.dart';
import '../../providers/trading_providers.dart';
import '../../widgets/shimmer_loading.dart';
class ContributionPage extends ConsumerWidget {
@ -30,8 +28,8 @@ class ContributionPage extends ConsumerWidget {
//
final estimatedEarnings = ref.watch(estimatedEarningsProvider(accountSequence));
final statsAsync = ref.watch(contributionStatsProvider);
//
final marketAsync = ref.watch(marketOverviewProvider);
//
final sharePoolAsync = ref.watch(sharePoolBalanceProvider);
// Extract loading state and data from AsyncValue
final isLoading = contributionAsync.isLoading;
@ -39,8 +37,8 @@ class ContributionPage extends ConsumerWidget {
final hasError = contributionAsync.hasError;
final error = contributionAsync.error;
final isStatsLoading = statsAsync.isLoading;
final isMarketLoading = marketAsync.isLoading;
final market = marketAsync.valueOrNull;
final isSharePoolLoading = sharePoolAsync.isLoading;
final sharePoolBalance = sharePoolAsync.valueOrNull;
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
@ -50,7 +48,7 @@ class ContributionPage extends ConsumerWidget {
onRefresh: () async {
ref.invalidate(contributionProvider(accountSequence));
ref.invalidate(contributionStatsProvider);
ref.invalidate(marketOverviewProvider);
ref.invalidate(sharePoolBalanceProvider);
},
child: hasError && contribution == null
? Center(
@ -78,7 +76,7 @@ class ContributionPage extends ConsumerWidget {
sliver: SliverList(
delegate: SliverChildListDelegate([
//
_buildTotalContributionCard(ref, contribution, isLoading, market, isMarketLoading),
_buildTotalContributionCard(ref, contribution, isLoading, sharePoolBalance, isSharePoolLoading),
const SizedBox(height: 16),
//
_buildThreeColumnStats(ref, contribution, isLoading),
@ -167,8 +165,8 @@ class ContributionPage extends ConsumerWidget {
WidgetRef ref,
Contribution? contribution,
bool isLoading,
MarketOverview? market,
bool isMarketLoading,
SharePoolBalance? sharePoolBalance,
bool isSharePoolLoading,
) {
final total = contribution?.totalContribution ?? '0';
final hideAmounts = ref.watch(hideAmountsProvider);
@ -220,13 +218,13 @@ class ContributionPage extends ConsumerWidget {
'积分股池实时余量: ',
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.9)),
),
isMarketLoading
isSharePoolLoading
? const ShimmerText(
placeholder: '----',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _orange),
)
: Text(
hideAmounts ? '******' : formatAmount(market?.greenPoints ?? '0'),
hideAmounts ? '******' : formatAmount(sharePoolBalance?.total ?? '0'),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: _orange),
),
],

View File

@ -6,6 +6,8 @@ import '../../domain/entities/contribution_stats.dart';
import '../../domain/usecases/contribution/get_user_contribution.dart';
import '../../domain/repositories/contribution_repository.dart';
import '../../core/di/injection.dart';
import '../../core/network/api_client.dart';
import '../../core/network/api_endpoints.dart';
final getUserContributionUseCaseProvider = Provider<GetUserContribution>((ref) {
return getIt<GetUserContribution>();
@ -176,3 +178,38 @@ final estimatedEarningsProvider = Provider.family<EstimatedEarnings, String>((re
///
final hideAmountsProvider = StateProvider<bool>((ref) => false);
///
class SharePoolBalance {
final String poolA;
final String poolB;
final String total;
const SharePoolBalance({
required this.poolA,
required this.poolB,
required this.total,
});
factory SharePoolBalance.fromJson(Map<String, dynamic> json) {
return SharePoolBalance(
poolA: json['poolA'] ?? '0',
poolB: json['poolB'] ?? '0',
total: json['total'] ?? '0',
);
}
static const zero = SharePoolBalance(poolA: '0', poolB: '0', total: '0');
}
/// Provider
final sharePoolBalanceProvider = FutureProvider<SharePoolBalance>((ref) async {
final client = getIt<ApiClient>();
try {
final response = await client.get(ApiEndpoints.sharePoolBalance);
return SharePoolBalance.fromJson(response.data as Map<String, dynamic>);
} catch (e) {
return SharePoolBalance.zero;
}
});