feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
This commit is contained in:
47
backend/app.ts
Normal file
47
backend/app.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Application Entry Point
|
||||
* Sets up HTTP server and registers routes
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { config } from './infra/config/env';
|
||||
import { logger } from './infra/logger/logger';
|
||||
import { authMiddleware } from './api/middleware/auth.middleware';
|
||||
import { contextMiddleware } from './api/middleware/context.middleware';
|
||||
import { daoRoutes } from './api/http/dao.routes';
|
||||
import { walletRoutes } from './api/http/wallet.routes';
|
||||
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();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(authMiddleware);
|
||||
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);
|
||||
app.use('/api/v1/platforms', vendorRoutes); // Vendor routes under platforms
|
||||
app.use('/api/v1', agentsRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
const port = config.port;
|
||||
app.listen(port, () => {
|
||||
logger.info(`Server started on port ${port}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
30
backend/domain/dao/dao.logic.ts
Normal file
30
backend/domain/dao/dao.logic.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Pure domain logic for DAO operations
|
||||
* No I/O, no side effects
|
||||
*/
|
||||
|
||||
import type { DaoRecord, DaoLevel, FederationMode } from './types';
|
||||
|
||||
/**
|
||||
* Check if DAO can become a SuperDAO
|
||||
*/
|
||||
export function canBecomeSuperDao(dao: DaoRecord, childCount: number): boolean {
|
||||
return childCount >= 1 && dao.federationMode === 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DAO can join a federation
|
||||
*/
|
||||
export function canJoinFederation(dao: DaoRecord, targetLevel: DaoLevel): boolean {
|
||||
// A3/A4 can join, exceptions for A2 handled by PDP
|
||||
return (dao.level === 'A3' || dao.level === 'A4') && dao.federationMode === 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DAO can leave federation
|
||||
*/
|
||||
export function canLeaveFederation(dao: DaoRecord): boolean {
|
||||
return dao.federationMode === 'member' && dao.parentDaoId !== null;
|
||||
}
|
||||
|
||||
|
||||
37
backend/domain/dao/types.ts
Normal file
37
backend/domain/dao/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Domain types for DAO entities
|
||||
* Based on: microdao-architecture.md, superdao-federation.md
|
||||
*/
|
||||
|
||||
export type DaoLevel = 'A1' | 'A2' | 'A3' | 'A4';
|
||||
export type DaoType = 'platform' | 'public' | 'private';
|
||||
export type FederationMode = 'none' | 'member' | 'superdao';
|
||||
|
||||
export interface DaoRecord {
|
||||
daoId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
level: DaoLevel;
|
||||
type: DaoType;
|
||||
parentDaoId?: string | null;
|
||||
federationMode: FederationMode;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateDaoInput {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: 'public' | 'private';
|
||||
level: 'A3' | 'A4';
|
||||
settings?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreatePlatformInput {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
domain?: string; // 'energy' | 'food' | 'water' | ...
|
||||
}
|
||||
|
||||
|
||||
38
backend/domain/pdp/policy.model.ts
Normal file
38
backend/domain/pdp/policy.model.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* PDP Policy Model
|
||||
* Based on: pdp_access.md, core-services-mvp.md
|
||||
*/
|
||||
|
||||
export type Decision = 'allow' | 'deny' | 'require-elevation';
|
||||
|
||||
export type PolicyId =
|
||||
| 'policy.dao.create'
|
||||
| 'policy.vendor.register'
|
||||
| 'policy.platform.create'
|
||||
| 'policy.federation.join'
|
||||
| 'policy.federation.leave'
|
||||
| 'policy.federation.create-superdao'
|
||||
| 'policy.federation.dissolve'
|
||||
| 'policy.agent.run';
|
||||
|
||||
export interface PdpContext {
|
||||
userId?: string;
|
||||
daoId?: string;
|
||||
daoLevel?: 'A1' | 'A2' | 'A3' | 'A4';
|
||||
// Additional context: roles, balances, staking, etc.
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PdpRequest {
|
||||
policyId: PolicyId;
|
||||
resource: Record<string, unknown>;
|
||||
context: PdpContext;
|
||||
}
|
||||
|
||||
export interface PdpResponse {
|
||||
decision: Decision;
|
||||
reason?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
14
backend/domain/user/types.ts
Normal file
14
backend/domain/user/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Domain types for User
|
||||
*/
|
||||
|
||||
export type UserRole = 'owner' | 'admin' | 'member' | 'guest' | 'agent';
|
||||
|
||||
export interface User {
|
||||
userId: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles?: UserRole[];
|
||||
}
|
||||
|
||||
|
||||
27
backend/domain/wallet/types.ts
Normal file
27
backend/domain/wallet/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Domain types for Wallet
|
||||
* Based on: core-services-mvp.md, tokenomics/city-tokenomics.md
|
||||
*/
|
||||
|
||||
export type TokenSymbol = 'DAAR' | 'DAARION';
|
||||
|
||||
export interface Balance {
|
||||
symbol: TokenSymbol;
|
||||
amount: string; // Decimal as string to avoid precision issues
|
||||
}
|
||||
|
||||
export interface WalletBalances {
|
||||
userId: string;
|
||||
balances: Balance[];
|
||||
}
|
||||
|
||||
export interface AccessCheck {
|
||||
check: 'dao.create' | 'vendor.register' | 'platform.create';
|
||||
}
|
||||
|
||||
export interface AccessCheckResult {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
|
||||
53
backend/http/agents.routes.ts
Normal file
53
backend/http/agents.routes.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Agents Routes (MVP Stub)
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/dao/{dao_id}/agents/{agent_id}/invoke - Invoke agent
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { pdpService } from '../../services/pdp/pdp.service';
|
||||
|
||||
export const agentsRoutes = Router();
|
||||
|
||||
// POST /api/v1/dao/{dao_id}/agents/{agent_id}/invoke - Invoke agent
|
||||
agentsRoutes.post('/:daoId/agents/:agentId/invoke', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const { daoId, agentId } = req.params;
|
||||
const { input, metadata } = req.body;
|
||||
|
||||
// Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.agent.run',
|
||||
{ agentId },
|
||||
{ userId, daoId }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
res.status(403).json({
|
||||
error: 'ACCESS_DENIED',
|
||||
message: pdpResult.reason || 'PDP denied',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// MVP: Return stub response
|
||||
// TODO: Implement actual agent invocation
|
||||
const runId = `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
res.json({
|
||||
run_id: runId,
|
||||
status: 'queued',
|
||||
output: null,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
78
backend/http/dao.routes.ts
Normal file
78
backend/http/dao.routes.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* DAO Routes
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/dao - Create DAO
|
||||
* - GET /api/v1/dao/{dao_id} - Get DAO by ID
|
||||
* - GET /api/v1/dao - List DAOs
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
|
||||
import { registryService } from '../../services/registry/registry.service';
|
||||
import type { CreateDaoInput } from '../../domain/dao/types';
|
||||
|
||||
export const daoRoutes = Router();
|
||||
|
||||
// POST /api/v1/dao - Create DAO
|
||||
daoRoutes.post('/', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const input: CreateDaoInput = req.body;
|
||||
|
||||
const result = await daoFactoryService.createDao(userId, input);
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
error: error.message || 'BAD_REQUEST',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/dao/{dao_id} - Get DAO by ID
|
||||
daoRoutes.get('/:daoId', async (req, res) => {
|
||||
try {
|
||||
const { daoId } = req.params;
|
||||
const dao = await registryService.getDaoById(daoId);
|
||||
|
||||
if (!dao) {
|
||||
res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: `DAO ${daoId} not found`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(dao);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/dao - List DAOs
|
||||
daoRoutes.get('/', async (req, res) => {
|
||||
try {
|
||||
const { level, type } = req.query;
|
||||
const filter = {
|
||||
level: level as string | undefined,
|
||||
type: type as string | undefined,
|
||||
};
|
||||
|
||||
const daos = await registryService.listDaos(filter);
|
||||
|
||||
res.json({ items: daos });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
38
backend/http/pdp.routes.ts
Normal file
38
backend/http/pdp.routes.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* PDP Routes
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/pdp/check - Check policy
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { pdpService } from '../../services/pdp/pdp.service';
|
||||
import type { PdpRequest } from '../../domain/pdp/policy.model';
|
||||
|
||||
export const pdpRoutes = Router();
|
||||
|
||||
// POST /api/v1/pdp/check - Check policy
|
||||
pdpRoutes.post('/check', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const { policy, resource, context }: PdpRequest = req.body;
|
||||
|
||||
const result = await pdpService.check(policy, resource, {
|
||||
...context,
|
||||
userId: context.userId || userId,
|
||||
});
|
||||
|
||||
res.json({
|
||||
decision: result.decision,
|
||||
reason: result.reason || null,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
48
backend/http/platforms.routes.ts
Normal file
48
backend/http/platforms.routes.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Platforms Routes
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/platforms - Create platform
|
||||
* - GET /api/v1/platforms - List platforms
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { daoFactoryService } from '../../services/dao-factory/dao-factory.service';
|
||||
import { registryService } from '../../services/registry/registry.service';
|
||||
import type { CreatePlatformInput } from '../../domain/dao/types';
|
||||
|
||||
export const platformsRoutes = Router();
|
||||
|
||||
// POST /api/v1/platforms - Create platform
|
||||
platformsRoutes.post('/', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const input: CreatePlatformInput = req.body;
|
||||
|
||||
const result = await daoFactoryService.createPlatform(userId, input);
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (error: any) {
|
||||
res.status(400).json({
|
||||
error: error.message || 'BAD_REQUEST',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/platforms - List platforms
|
||||
platformsRoutes.get('/', async (req, res) => {
|
||||
try {
|
||||
const platforms = await registryService.listPlatforms();
|
||||
|
||||
res.json({ items: platforms });
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
156
backend/http/teams.routes.ts
Normal file
156
backend/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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
52
backend/http/vendor.routes.ts
Normal file
52
backend/http/vendor.routes.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Vendor Routes
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/platforms/{platform_id}/vendors - Register vendor
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { pdpService } from '../../services/pdp/pdp.service';
|
||||
|
||||
export const vendorRoutes = Router();
|
||||
|
||||
// POST /api/v1/platforms/{platform_id}/vendors - Register vendor
|
||||
vendorRoutes.post('/:platformId/vendors', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const { platformId } = req.params;
|
||||
const { display_name, contact } = req.body;
|
||||
|
||||
// Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.vendor.register',
|
||||
{ platformId },
|
||||
{ userId, daoId: platformId, daoLevel: 'A2' }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
res.status(403).json({
|
||||
error: 'ACCESS_DENIED',
|
||||
message: pdpResult.reason || 'PDP denied',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Save vendor to database
|
||||
const vendorId = `vendor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
res.status(201).json({
|
||||
vendor_id: vendorId,
|
||||
platform_id: platformId,
|
||||
status: 'approved',
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
72
backend/http/wallet.routes.ts
Normal file
72
backend/http/wallet.routes.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Wallet Routes
|
||||
* Based on: api-mvp.md
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/v1/wallet/me - Get user balances
|
||||
* - POST /api/v1/wallet/check-access - Check access for action
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { walletService } from '../../services/wallet/wallet.service';
|
||||
import type { AccessCheck } from '../../domain/wallet/types';
|
||||
|
||||
export const walletRoutes = Router();
|
||||
|
||||
// GET /api/v1/wallet/me - Get user balances
|
||||
walletRoutes.get('/me', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const balances = await walletService.getBalances(userId);
|
||||
|
||||
res.json({
|
||||
user_id: userId,
|
||||
balances,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/v1/wallet/check-access - Check access
|
||||
walletRoutes.post('/check-access', async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).userId;
|
||||
const { check }: AccessCheck = req.body;
|
||||
|
||||
let allowed = false;
|
||||
let reason: string | undefined;
|
||||
|
||||
switch (check) {
|
||||
case 'dao.create':
|
||||
allowed = await walletService.hasEnoughForDaoCreate(userId);
|
||||
if (!allowed) reason = 'INSUFFICIENT_BALANCE';
|
||||
break;
|
||||
case 'vendor.register':
|
||||
allowed = await walletService.hasEnoughForVendorRegister(userId);
|
||||
if (!allowed) reason = 'INSUFFICIENT_STAKED_DAARION';
|
||||
break;
|
||||
case 'platform.create':
|
||||
allowed = await walletService.hasEnoughForPlatformCreate(userId);
|
||||
if (!allowed) reason = 'INSUFFICIENT_STAKED_DAARION';
|
||||
break;
|
||||
default:
|
||||
reason = 'UNKNOWN_CHECK';
|
||||
}
|
||||
|
||||
res.json({
|
||||
allowed,
|
||||
reason: allowed ? null : reason,
|
||||
});
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
21
backend/infra/config/env.ts
Normal file
21
backend/infra/config/env.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Environment Configuration
|
||||
* Load and validate environment variables
|
||||
*/
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:3000',
|
||||
|
||||
// Database
|
||||
dbUrl: process.env.DATABASE_URL || 'postgresql://localhost:5432/microdao',
|
||||
|
||||
// Auth
|
||||
jwtSecret: process.env.JWT_SECRET || 'change-me-in-production',
|
||||
|
||||
// Wallet/Chain (future)
|
||||
chainRpcUrl: process.env.CHAIN_RPC_URL || '',
|
||||
};
|
||||
|
||||
|
||||
20
backend/infra/db/client.ts
Normal file
20
backend/infra/db/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Database Client
|
||||
* MVP: Placeholder for future DB connection
|
||||
* TODO: Replace with actual DB client (PostgreSQL, etc.)
|
||||
*/
|
||||
|
||||
// MVP: No-op
|
||||
export const dbClient = {
|
||||
connect: async () => {
|
||||
// TODO: Implement actual DB connection
|
||||
console.log('[DB] Connected (stub)');
|
||||
},
|
||||
|
||||
disconnect: async () => {
|
||||
// TODO: Implement actual DB disconnection
|
||||
console.log('[DB] Disconnected (stub)');
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
44
backend/infra/db/dao.repository.ts
Normal file
44
backend/infra/db/dao.repository.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* DAO Repository
|
||||
* Database access layer for DAO records
|
||||
* MVP: In-memory storage, replace with actual DB later
|
||||
*/
|
||||
|
||||
import type { DaoRecord } from '../../domain/dao/types';
|
||||
|
||||
// MVP: In-memory storage
|
||||
const daoStore: Map<string, DaoRecord> = new Map();
|
||||
|
||||
export const daoRepository = {
|
||||
async save(record: DaoRecord): Promise<void> {
|
||||
daoStore.set(record.daoId, record);
|
||||
},
|
||||
|
||||
async findById(daoId: string): Promise<DaoRecord | null> {
|
||||
return daoStore.get(daoId) || null;
|
||||
},
|
||||
|
||||
async findAll(filter?: { level?: string; type?: string }): Promise<DaoRecord[]> {
|
||||
const all = Array.from(daoStore.values());
|
||||
|
||||
if (!filter) {
|
||||
return all;
|
||||
}
|
||||
|
||||
return all.filter(dao => {
|
||||
if (filter.level && dao.level !== filter.level) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type && dao.type !== filter.type) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
async delete(daoId: string): Promise<void> {
|
||||
daoStore.delete(daoId);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
27
backend/infra/logger/logger.ts
Normal file
27
backend/infra/logger/logger.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Logger
|
||||
* MVP: Simple console logger
|
||||
* Future: Replace with proper logging library (Winston, Pino, etc.)
|
||||
*/
|
||||
|
||||
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||||
|
||||
export const logger = {
|
||||
info: (message: string, ...args: unknown[]) => {
|
||||
console.log(`[INFO] ${message}`, ...args);
|
||||
},
|
||||
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
console.warn(`[WARN] ${message}`, ...args);
|
||||
},
|
||||
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
console.error(`[ERROR] ${message}`, ...args);
|
||||
},
|
||||
|
||||
debug: (message: string, ...args: unknown[]) => {
|
||||
console.debug(`[DEBUG] ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
39
backend/middleware/auth.middleware.ts
Normal file
39
backend/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Auth Middleware
|
||||
* Validates Bearer token
|
||||
* MVP: Simple validation, replace with proper JWT validation later
|
||||
*/
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({
|
||||
error: 'UNAUTHORIZED',
|
||||
message: 'Missing or invalid Authorization header',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// MVP: Simple validation (just check token exists)
|
||||
// TODO: Validate JWT token properly
|
||||
if (!token) {
|
||||
res.status(401).json({
|
||||
error: 'UNAUTHORIZED',
|
||||
message: 'Invalid token',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach user ID to request (MVP: extract from token)
|
||||
// TODO: Decode JWT and extract userId
|
||||
(req as any).userId = 'user_stub'; // Replace with actual user ID from token
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
18
backend/middleware/context.middleware.ts
Normal file
18
backend/middleware/context.middleware.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Context Middleware
|
||||
* Extracts X-DAO-ID header and attaches to request context
|
||||
*/
|
||||
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function contextMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const daoId = req.headers['x-dao-id'] as string | undefined;
|
||||
|
||||
if (daoId) {
|
||||
(req as any).daoId = daoId;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
110
backend/services/dao-factory/dao-factory.service.ts
Normal file
110
backend/services/dao-factory/dao-factory.service.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* DAOFactory Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Create new DAO (A3/A4)
|
||||
* - Create platforms (A2)
|
||||
* - Validate input
|
||||
* - Call PDP for access checks
|
||||
* - Write DAO to Registry
|
||||
*/
|
||||
|
||||
import type { CreateDaoInput, CreatePlatformInput } from '../../domain/dao/types';
|
||||
import { pdpService } from '../pdp/pdp.service';
|
||||
import { walletService } from '../wallet/wallet.service';
|
||||
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 - need 1 DAARION on balance
|
||||
const hasEnough = await walletService.hasEnoughForMicroDaoCreate(userId);
|
||||
if (!hasEnough) {
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAARION on balance to create MicroDAO');
|
||||
}
|
||||
|
||||
// 2. Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.dao.create',
|
||||
{ type: 'dao' },
|
||||
{ userId, daoLevel: input.level }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
throw new Error(`ACCESS_DENIED: ${pdpResult.reason || 'PDP denied'}`);
|
||||
}
|
||||
|
||||
// 3. Create DAO record
|
||||
const daoId = this.generateDaoId();
|
||||
const daoRecord = {
|
||||
daoId,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
level: input.level,
|
||||
type: input.type,
|
||||
parentDaoId: null,
|
||||
federationMode: 'none' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 4. Save to Registry
|
||||
await registryService.saveDao(daoRecord);
|
||||
|
||||
return { daoId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new platform (A2)
|
||||
*/
|
||||
async createPlatform(userId: string, input: CreatePlatformInput): Promise<{ daoId: string }> {
|
||||
// 1. Check wallet balance
|
||||
const hasEnough = await walletService.hasEnoughForPlatformCreate(userId);
|
||||
if (!hasEnough) {
|
||||
throw new Error('INSUFFICIENT_BALANCE: Need 1 DAARION staked');
|
||||
}
|
||||
|
||||
// 2. Check PDP policy
|
||||
const pdpResult = await pdpService.check(
|
||||
'policy.platform.create',
|
||||
{ type: 'platform' },
|
||||
{ userId, daoLevel: 'A2' }
|
||||
);
|
||||
|
||||
if (pdpResult.decision !== 'allow') {
|
||||
throw new Error(`ACCESS_DENIED: ${pdpResult.reason || 'PDP denied'}`);
|
||||
}
|
||||
|
||||
// 3. Create platform record
|
||||
const daoId = this.generateDaoId();
|
||||
const daoRecord = {
|
||||
daoId,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
level: 'A2' as const,
|
||||
type: 'platform' as const,
|
||||
parentDaoId: null,
|
||||
federationMode: 'none' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 4. Save to Registry
|
||||
await registryService.saveDao(daoRecord);
|
||||
|
||||
return { daoId };
|
||||
}
|
||||
|
||||
private generateDaoId(): string {
|
||||
// MVP: Simple UUID-like generation
|
||||
// TODO: Use proper UUID library
|
||||
return `dao_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const daoFactoryService = new DaoFactoryService();
|
||||
|
||||
|
||||
100
backend/services/pdp/pdp.service.ts
Normal file
100
backend/services/pdp/pdp.service.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* PDP Service (MVP)
|
||||
* Based on: pdp_access.md, core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Centralized access decision making
|
||||
* - Interpret policies from pdp_access.md
|
||||
* - Provide simple API for other services
|
||||
*/
|
||||
|
||||
import type { PdpRequest, PdpResponse, PolicyId, PdpContext } from '../../domain/pdp/policy.model';
|
||||
import { policiesConfig } from './policies.config';
|
||||
import { walletService } from '../wallet/wallet.service';
|
||||
|
||||
export class PdpService {
|
||||
/**
|
||||
* Check policy and return decision
|
||||
*/
|
||||
async check(
|
||||
policyId: PolicyId,
|
||||
resource: Record<string, unknown>,
|
||||
context: PdpContext
|
||||
): Promise<PdpResponse> {
|
||||
const policy = policiesConfig[policyId];
|
||||
|
||||
if (!policy) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: `Policy ${policyId} not found`,
|
||||
};
|
||||
}
|
||||
|
||||
// Evaluate policy conditions
|
||||
const result = await this.evaluatePolicy(policy, resource, context);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async evaluatePolicy(
|
||||
policy: any,
|
||||
resource: Record<string, unknown>,
|
||||
context: PdpContext
|
||||
): Promise<PdpResponse> {
|
||||
// MVP: Simple evaluation
|
||||
// Future: More complex condition evaluation
|
||||
|
||||
// Example: policy.dao.create
|
||||
if (policy.id === 'policy.dao.create') {
|
||||
const hasEnough = await walletService.hasEnoughForDaoCreate(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_BALANCE',
|
||||
details: {
|
||||
required: { DAAR: 1.0, DAARION: 0.01 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Example: policy.platform.create
|
||||
if (policy.id === 'policy.platform.create') {
|
||||
const hasEnough = await walletService.hasEnoughForPlatformCreate(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_BALANCE',
|
||||
details: {
|
||||
required: { DAARION: 1.0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Example: policy.vendor.register
|
||||
if (policy.id === 'policy.vendor.register') {
|
||||
const hasEnough = await walletService.hasEnoughForVendorRegister(context.userId || '');
|
||||
if (!hasEnough) {
|
||||
return {
|
||||
decision: 'deny',
|
||||
reason: 'INSUFFICIENT_STAKED_DAARION',
|
||||
details: {
|
||||
required: { DAARION: 0.01 },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
|
||||
// Default: allow (for MVP, can be more restrictive later)
|
||||
return { decision: 'allow' };
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const pdpService = new PdpService();
|
||||
|
||||
|
||||
76
backend/services/pdp/policies.config.ts
Normal file
76
backend/services/pdp/policies.config.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Policies Configuration
|
||||
* Based on: pdp_access.md
|
||||
*
|
||||
* Initial set of policies for MVP
|
||||
*/
|
||||
|
||||
export const policiesConfig = {
|
||||
'policy.dao.create': {
|
||||
id: 'policy.dao.create',
|
||||
description: 'Створення нового MicroDAO',
|
||||
conditions: [
|
||||
{
|
||||
type: 'or',
|
||||
rules: [
|
||||
{ type: 'balance', token: 'DAAR', gte: 1 },
|
||||
{ type: 'balance', token: 'DAARION', gte: 0.01 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'policy.vendor.register': {
|
||||
id: 'policy.vendor.register',
|
||||
description: 'Реєстрація вендора на платформі',
|
||||
conditions: [
|
||||
{ type: 'staked', token: 'DAARION', gte: 0.01 },
|
||||
],
|
||||
},
|
||||
'policy.platform.create': {
|
||||
id: 'policy.platform.create',
|
||||
description: 'Створення платформи',
|
||||
conditions: [
|
||||
{ type: 'staked', token: 'DAARION', gte: 1 },
|
||||
],
|
||||
},
|
||||
'policy.federation.join': {
|
||||
id: 'policy.federation.join',
|
||||
description: 'Вступ DAO до SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'target', property: 'federation_mode', value: 'superdao' },
|
||||
],
|
||||
},
|
||||
'policy.federation.leave': {
|
||||
id: 'policy.federation.leave',
|
||||
description: 'Вихід DAO з SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
],
|
||||
},
|
||||
'policy.federation.create-superdao': {
|
||||
id: 'policy.federation.create-superdao',
|
||||
description: 'Створення SuperDAO',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'dao', property: 'child_count', gte: 1 },
|
||||
],
|
||||
},
|
||||
'policy.federation.dissolve': {
|
||||
id: 'policy.federation.dissolve',
|
||||
description: 'Розформування федерації',
|
||||
conditions: [
|
||||
{ type: 'role', value: 'owner' },
|
||||
{ type: 'dao', property: 'level', ne: 'A1' },
|
||||
],
|
||||
},
|
||||
'policy.agent.run': {
|
||||
id: 'policy.agent.run',
|
||||
description: 'Запуск агента',
|
||||
conditions: [
|
||||
{ type: 'agent', property: 'registered', value: true },
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
47
backend/services/registry/registry.service.ts
Normal file
47
backend/services/registry/registry.service.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Registry Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Store all DAO information
|
||||
* - Mark DAO as platform (A2) or MicroDAO (A3/A4)
|
||||
* - Provide public catalog of DAO/platforms
|
||||
*/
|
||||
|
||||
import type { DaoRecord } from '../../domain/dao/types';
|
||||
import { daoRepository } from '../../infra/db/dao.repository';
|
||||
|
||||
export class RegistryService {
|
||||
/**
|
||||
* Save DAO record to registry
|
||||
*/
|
||||
async saveDao(record: DaoRecord): Promise<void> {
|
||||
await daoRepository.save(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DAO by ID
|
||||
*/
|
||||
async getDaoById(daoId: string): Promise<DaoRecord | null> {
|
||||
return daoRepository.findById(daoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* List DAOs with optional filters
|
||||
*/
|
||||
async listDaos(filter?: { level?: string; type?: string }): Promise<DaoRecord[]> {
|
||||
return daoRepository.findAll(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all platforms (A2, type=platform)
|
||||
*/
|
||||
async listPlatforms(): Promise<DaoRecord[]> {
|
||||
return daoRepository.findAll({ level: 'A2', type: 'platform' });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const registryService = new RegistryService();
|
||||
|
||||
|
||||
36
backend/services/wallet/wallet.adapter.ts
Normal file
36
backend/services/wallet/wallet.adapter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Wallet Adapter (MVP Stub)
|
||||
*
|
||||
* On MVP: returns mock data or reads from DB/stub
|
||||
* Future: integrate with on-chain data
|
||||
*/
|
||||
|
||||
import type { Balance } from '../../domain/wallet/types';
|
||||
|
||||
/**
|
||||
* Get balances from external source (on-chain / DB / stub)
|
||||
*/
|
||||
export async function getBalances(userId: string): Promise<Balance[]> {
|
||||
// MVP: Return mock data
|
||||
// TODO: Replace with actual DB/on-chain integration
|
||||
return [
|
||||
{ symbol: 'DAAR', amount: '0.0' },
|
||||
{ symbol: 'DAARION', amount: '0.0' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get staked DAARION amount
|
||||
*/
|
||||
export async function getStakedDaarion(userId: string): Promise<number> {
|
||||
// MVP: Return mock data
|
||||
// TODO: Replace with actual DB/on-chain integration
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
export const walletAdapter = {
|
||||
getBalances,
|
||||
getStakedDaarion,
|
||||
};
|
||||
|
||||
|
||||
23
backend/services/wallet/wallet.interface.ts
Normal file
23
backend/services/wallet/wallet.interface.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Wallet Service Interface
|
||||
* 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>;
|
||||
}
|
||||
|
||||
|
||||
85
backend/services/wallet/wallet.service.ts
Normal file
85
backend/services/wallet/wallet.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Wallet Service (MVP)
|
||||
* Based on: core-services-mvp.md
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Read DAAR / DAARION balances
|
||||
* - Provide helper functions for access checks
|
||||
*/
|
||||
|
||||
import type { WalletService as IWalletService } from './wallet.interface';
|
||||
import { walletAdapter } from './wallet.adapter';
|
||||
import type { Balance, TokenSymbol } from '../../domain/wallet/types';
|
||||
|
||||
export class WalletService implements IWalletService {
|
||||
/**
|
||||
* Get user balances for DAAR and DAARION
|
||||
*/
|
||||
async getBalances(userId: string): Promise<Balance[]> {
|
||||
return walletAdapter.getBalances(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForVendorRegister(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoUsage(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used in current implementation
|
||||
*/
|
||||
async hasEnoughForPlatformCreate(userId: string): Promise<boolean> {
|
||||
return this.hasEnoughForMicroDaoCreate(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const walletService = new WalletService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user