feat: add MicroDAO balance checks and DAARION.city integration
- Update Wallet Service: balance checks (1 DAARION for create, 0.01 for usage) - Update DAOFactory Service: use new balance checks - Add DB migration: teams type field and city_links table - Add DAARION.city seed data - Create teams API routes with balance validation - Add DAARION.city remote repository - Add sync scripts and documentation
This commit is contained in:
156
src/api/http/teams.routes.ts
Normal file
156
src/api/http/teams.routes.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Teams Routes (MicroDAO)
|
||||
* Based on: api-mvp.md, updated for MicroDAO creation with balance checks
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/teams - Create MicroDAO (requires 1 DAARION on balance)
|
||||
* - GET /api/v1/teams - List teams/MicroDAO
|
||||
* - GET /api/v1/teams/:teamId - Get team/MicroDAO by ID
|
||||
* - POST /api/v1/teams/:teamId/members - Invite member (requires balance check)
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { walletService } from '../../services/wallet/wallet.service';
|
||||
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
|
||||
import type { CreateTeamRequest } from '../../../types/api';
|
||||
|
||||
export const teamsRoutes = Router();
|
||||
|
||||
// POST /api/v1/teams - Create MicroDAO
|
||||
teamsRoutes.post('/', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const input: CreateTeamRequest = req.body;
|
||||
|
||||
// 1. Check wallet balance - need 1 DAARION on balance
|
||||
const hasEnough = await walletService.hasEnoughForMicroDaoCreate(userId);
|
||||
if (!hasEnough) {
|
||||
res.status(403).json({
|
||||
error: 'INSUFFICIENT_BALANCE',
|
||||
message: 'Need 1 DAARION on balance to create MicroDAO',
|
||||
required: { daarion: 1.0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Create MicroDAO through DAOFactory
|
||||
const daoResult = await daoFactoryService.createDao(userId, {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
type: input.mode === 'confidential' ? 'private' : 'public',
|
||||
level: 'A4', // User-created MicroDAO are A4 level
|
||||
});
|
||||
|
||||
// 3. TODO: Create team record in database
|
||||
// For now, return DAO result
|
||||
res.status(201).json({
|
||||
id: daoResult.daoId,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
mode: input.mode || 'public',
|
||||
type: input.type || 'community',
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
error: error.message?.includes('INSUFFICIENT_BALANCE') ? 'INSUFFICIENT_BALANCE' : 'BAD_REQUEST',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/teams - List teams/MicroDAO
|
||||
teamsRoutes.get('/', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
|
||||
// TODO: Get teams from database
|
||||
// For now, return empty list
|
||||
res.json({ teams: [] });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/teams/:teamId - Get team/MicroDAO by ID
|
||||
teamsRoutes.get('/:teamId', async (req, res) => {
|
||||
try {
|
||||
const { teamId } = req.params;
|
||||
|
||||
// TODO: Get team from database
|
||||
res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: `Team ${teamId} not found`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/teams/:teamId/members - Invite member
|
||||
teamsRoutes.post('/:teamId/members', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId; // Current user (admin)
|
||||
const { teamId } = req.params;
|
||||
const { email, role = 'member' } = req.body;
|
||||
|
||||
// 1. Check if current user is admin (has 1 DAARION)
|
||||
const isAdmin = await walletService.hasEnoughForAdminRole(userId);
|
||||
if (!isAdmin) {
|
||||
res.status(403).json({
|
||||
error: 'ACCESS_DENIED',
|
||||
message: 'Need 1 DAARION on balance to invite members',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check invited user balance based on role
|
||||
// TODO: Get invited user ID from email
|
||||
const invitedUserId = `user_${email}`; // Placeholder
|
||||
|
||||
if (role === 'admin') {
|
||||
// Admin role requires 1 DAARION
|
||||
const hasEnough = await walletService.hasEnoughForAdminRole(invitedUserId);
|
||||
if (!hasEnough) {
|
||||
res.status(403).json({
|
||||
error: 'INSUFFICIENT_BALANCE',
|
||||
message: 'Invited user needs 1 DAARION on balance to be Admin',
|
||||
required: { daarion: 1.0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Member role requires 0.01 DAARION
|
||||
const hasEnough = await walletService.hasEnoughForMicroDaoUsage(invitedUserId);
|
||||
if (!hasEnough) {
|
||||
res.status(403).json({
|
||||
error: 'INSUFFICIENT_BALANCE',
|
||||
message: 'Invited user needs 0.01 DAARION on balance to use MicroDAO',
|
||||
required: { daarion: 0.01 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. TODO: Create team member record in database
|
||||
res.status(201).json({
|
||||
team_id: teamId,
|
||||
user_id: invitedUserId,
|
||||
email,
|
||||
role,
|
||||
status: 'invited',
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
error: 'BAD_REQUEST',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { pdpRoutes } from './api/http/pdp.routes';
|
||||
import { vendorRoutes } from './api/http/vendor.routes';
|
||||
import { platformsRoutes } from './api/http/platforms.routes';
|
||||
import { agentsRoutes } from './api/http/agents.routes';
|
||||
import { teamsRoutes } from './api/http/teams.routes';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -24,6 +25,7 @@ app.use(contextMiddleware);
|
||||
|
||||
// Routes
|
||||
app.use('/api/v1/dao', daoRoutes);
|
||||
app.use('/api/v1/teams', teamsRoutes);
|
||||
app.use('/api/v1/wallet', walletRoutes);
|
||||
app.use('/api/v1/pdp', pdpRoutes);
|
||||
app.use('/api/v1/platforms', platformsRoutes);
|
||||
|
||||
@@ -18,12 +18,13 @@ import { registryService } from '../registry/registry.service';
|
||||
export class DaoFactoryService {
|
||||
/**
|
||||
* Create a new MicroDAO (A3 or A4)
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async createDao(userId: string, input: CreateDaoInput): Promise<{ daoId: string }> {
|
||||
// 1. Check wallet balance
|
||||
const hasEnough = await walletService.hasEnoughForDaoCreate(userId);
|
||||
// 1. Check wallet balance - need 1 DAARION on balance
|
||||
const hasEnough = await walletService.hasEnoughForMicroDaoCreate(userId);
|
||||
if (!hasEnough) {
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAAR or 0.01 DAARION');
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAARION on balance to create MicroDAO');
|
||||
}
|
||||
|
||||
// 2. Check PDP policy
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
/**
|
||||
* Wallet Service Interface
|
||||
* Based on: core-services-mvp.md
|
||||
* Based on: core-services-mvp.md, updated for MicroDAO requirements
|
||||
*/
|
||||
|
||||
import type { Balance } from '../../domain/wallet/types';
|
||||
|
||||
export interface WalletService {
|
||||
getBalances(userId: string): Promise<Balance[]>;
|
||||
getDaarionBalance(userId: string): Promise<number>;
|
||||
|
||||
// MicroDAO access checks (balance-based, no staking)
|
||||
hasEnoughForMicroDaoCreate(userId: string): Promise<boolean>; // 1 DAARION
|
||||
hasEnoughForAdminRole(userId: string): Promise<boolean>; // 1 DAARION
|
||||
hasEnoughForMicroDaoUsage(userId: string): Promise<boolean>; // 0.01 DAARION
|
||||
|
||||
// Legacy methods (deprecated)
|
||||
hasEnoughForDaoCreate(userId: string): Promise<boolean>;
|
||||
hasEnoughForVendorRegister(userId: string): Promise<boolean>;
|
||||
hasEnoughForPlatformCreate(userId: string): Promise<boolean>;
|
||||
|
||||
@@ -20,38 +20,62 @@ export class WalletService implements IWalletService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough tokens to create a DAO
|
||||
* Requires: 1 DAAR OR 0.01 DAARION
|
||||
* Check if user has enough DAARION to create a MicroDAO
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForMicroDaoCreate(userId: string): Promise<boolean> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) >= 1.0 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough DAARION to be Admin
|
||||
* Requires: 1 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForAdminRole(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough DAARION to use MicroDAO service
|
||||
* Requires: 0.01 DAARION on balance (not staked)
|
||||
*/
|
||||
async hasEnoughForMicroDaoUsage(userId: string): Promise<boolean> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) >= 0.01 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAARION balance for user
|
||||
*/
|
||||
async getDaarionBalance(userId: string): Promise<number> {
|
||||
const balances = await this.getBalances(userId);
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
return daarion ? parseFloat(daarion.amount) : 0;
|
||||
}
|
||||
|
||||
// Legacy methods (deprecated, kept for backward compatibility)
|
||||
/**
|
||||
* @deprecated Use hasEnoughForMicroDaoCreate instead
|
||||
*/
|
||||
async hasEnoughForDaoCreate(userId: string): Promise<boolean> {
|
||||
const balances = await this.getBalances(userId);
|
||||
|
||||
const daar = balances.find(b => b.symbol === 'DAAR');
|
||||
const daarion = balances.find(b => b.symbol === 'DAARION');
|
||||
|
||||
// Check: 1 DAAR OR 0.01 DAARION
|
||||
const hasEnoughDaar = daar && parseFloat(daar.amount) >= 1.0;
|
||||
const hasEnoughDaarion = daarion && parseFloat(daarion.amount) >= 0.01;
|
||||
|
||||
return hasEnoughDaar || hasEnoughDaarion;
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough staked DAARION for vendor registration
|
||||
* Requires: 0.01 DAARION staked
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForVendorRegister(userId: string): Promise<boolean> {
|
||||
const staked = await walletAdapter.getStakedDaarion(userId);
|
||||
return staked >= 0.01;
|
||||
return this.hasEnoughForMicroDaoUsage(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enough staked DAARION for platform creation
|
||||
* Requires: 1 DAARION staked
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForPlatformCreate(userId: string): Promise<boolean> {
|
||||
const staked = await walletAdapter.getStakedDaarion(userId);
|
||||
return staked >= 1.0;
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,15 +12,21 @@ export interface User {
|
||||
export interface Team {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
mode: 'public' | 'confidential';
|
||||
type?: 'city' | 'platform' | 'community' | 'guild' | 'lab' | 'personal';
|
||||
parent_team_id?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateTeamRequest {
|
||||
name: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
type?: 'community' | 'guild' | 'lab' | 'personal';
|
||||
mode?: 'public' | 'confidential';
|
||||
}
|
||||
|
||||
export interface UpdateTeamRequest {
|
||||
|
||||
Reference in New Issue
Block a user