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:
Apple
2025-11-15 08:56:14 -08:00
parent c552199eed
commit 582ab75b03
13 changed files with 994 additions and 25 deletions

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

View File

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

View File

@@ -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

View File

@@ -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>;

View File

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

View File

@@ -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 {