Перейти к содержанию

Операции с инвестициями (Investment Operations Endpoints - Integration-Only)

Аудитория: разработчики, frontend инженеры Последнее обновление: 2025-11-17 Архитектура: Integration-Only + Single Strategy MVP Краткое содержание: Упрощенная документация Investment endpoints для Single Strategy MVP (10% APY) — автоматическое создание инвестиций, просмотр производительности, operator-управляемые операции. Fordefi + Crypto2B интеграция для asset management.


Обзор endpoints (Single Strategy MVP)

User Investment Endpoints (3) - Simplified MVP

Метод Endpoint Описание Auth Required MVP Status
POST /api/user/investments Автосоздание инвестиции (single strategy 10% APY) ✅ JWT ✅ Core
GET /api/user/investments Портфель пользователя (single strategy) ✅ JWT ✅ Core
GET /api/user/investments/performance Производительность портфеля ✅ JWT ✅ Core

Operator Investment Endpoints (4) - Simplified MVP

Метод Endpoint Описание Auth Required MVP Status
GET /api/admin/investments Список всех инвестиций (operator view) ✅ Admin JWT ✅ Core
GET /api/admin/investments/:id Детали инвестиции ✅ Admin JWT ✅ Core
POST /api/admin/investments/:id/close Частичное закрытие инвестиции ✅ Admin JWT ✅ Core
POST /api/webhooks/fordefi/yield Webhook обновления доходности ❌ No ✅ Core

🚫 Убрано из MVP: сложные операции (DELETE, bulk-approve, reject, cancel) упрощены до single strategy автоматического управления.


👤 USER INVESTMENT ENDPOINTS

💼 POST /api/user/investments (Single Strategy MVP)

Автоматическое создание инвестиции в единственной стратегии MVP (10% APY фиксированная доходность).

Запрос

POST /api/user/investments
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "strategyId": "balanced-10",
  "amount": "1000.00",
  "currency": "USDC",
  "idempotencyKey": "uuid-123-abc-456"
}

Заголовки:

Заголовок Тип Обязательно Описание
Authorization Bearer <token> ✅ Да User JWT токен
Content-Type application/json ✅ Да Формат тела запроса

Схема тела запроса:

Поле Тип Обязательно Описание
strategyId string ✅ Да ID стратегии (conservative-5, balanced-10, aggressive-20)
amount string ✅ Да Сумма инвестиции (SafeDecimal, минимум: 10 USDC)
currency string ❌ Нет Валюта (по умолчанию: USDC)
idempotencyKey string ⚠️ Рекомендуется UUID v4 для предотвращения дублей

Правила валидации:

  • Amount ≥ 10 USDC (минимальная инвестиция)
  • Amount ≤ доступный баланс пользователя
  • Стратегия должна быть активна
  • Баланс проверяется перед обработкой

Ответ

Успех (201 Created):

{
  "success": true,
  "data": {
    "id": "inv_abc123",
    "userId": "user_123",
    "strategyId": "balanced-10",
    "strategyName": "Balanced 10% APY Strategy",
    "amount": "1000.00",
    "currency": "USDC",
    "apy": 10.0,
    "status": "active",
    "allocation": {
      "pendle": "300.00",
      "curve": "400.00",
      "convex": "300.00"
    },
    "performance": {
      "currentValue": "1000.00",
      "totalEarnings": "0.00",
      "roi": 0.0,
      "dailyYield": "0.00"
    },
    "createdAt": "2025-10-06T12:34:56Z",
    "lastUpdated": "2025-10-06T12:34:56Z",
    "nextYieldDate": "2025-10-07T00:00:00Z"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Ошибки:

Код Код ошибки Описание
400 INVALID_AMOUNT Amount < 10 USDC
422 INSUFFICIENT_BALANCE Баланс < сумма
422 STRATEGY_NOT_AVAILABLE Стратегия недоступна
409 DUPLICATE_INVESTMENT Idempotency key использован

Пример cURL

TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X POST https://app.saga.surf/api/user/investments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "strategyId": "balanced-10",
    "amount": "1000.00",
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000"
  }'

Пример TypeScript

import { v4 as uuidv4 } from 'uuid';

const createInvestment = async (strategyId: string, amount: string) => {
  const response = await fetch('/api/user/investments', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      strategyId,
      amount,
      idempotencyKey: uuidv4()
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error.message);
  }

  const { data: investment } = await response.json();
  console.log('Инвестиция создана:', investment.id);
  return investment;
};

await createInvestment('balanced-10', '1000.00');

GET /api/user/investments

Получить список всех инвестиций пользователя с фильтрацией и pagination.

Запрос

GET /api/user/investments?status=active&strategyId=balanced-10&page=1&limit=20
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Query параметры:

Параметр Тип Описание Default
page number Номер страницы 1
limit number Элементов на странице (макс: 100) 20
status string Фильтр: active, paused, closed Все
strategyId string Фильтр по стратегии Все
sortBy string Поле: createdAt, amount, currentValue createdAt
orderBy string Порядок: asc, desc desc

Ответ

Успех (200 OK):

{
  "success": true,
  "data": [
    {
      "id": "inv_abc123",
      "strategyId": "balanced-10",
      "strategyName": "Balanced 10% APY Strategy",
      "amount": "1000.00",
      "currentValue": "1050.25",
      "totalEarnings": "50.25",
      "apy": 10.0,
      "roi": 5.03,
      "status": "active",
      "createdAt": "2025-10-01T10:00:00Z",
      "nextYieldDate": "2025-10-07T00:00:00Z"
    }
  ],
  "summary": {
    "totalInvested": "1000.00",
    "totalCurrentValue": "1050.25",
    "totalEarnings": "50.25",
    "averageROI": 5.03,
    "activeInvestments": 1
  },
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1,
    "hasNext": false,
    "hasPrevious": false
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Пример TypeScript

const fetchInvestments = async (filters = {}) => {
  const params = new URLSearchParams({
    page: '1',
    limit: '20',
    ...filters
  });

  const response = await fetch(`/api/user/investments?${params}`, {
    headers: { 'Authorization': `Bearer ${token}` }
  });

  const { data: investments, summary } = await response.json();

  console.log('Портфель:', summary.totalCurrentValue, 'USDC');
  console.log('Доходность:', summary.totalEarnings, 'USDC');

  return { investments, summary };
};

await fetchInvestments({ status: 'active' });

GET /api/user/investments/:id

Получить детальную информацию о конкретной инвестиции.

Запрос

GET /api/user/investments/inv_abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Path параметры:

Параметр Тип Описание
id string ID инвестиции

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "inv_abc123",
    "userId": "user_123",
    "strategyId": "balanced-10",
    "strategyName": "Balanced 10% APY Strategy",
    "amount": "1000.00",
    "currency": "USDC",
    "apy": 10.0,
    "status": "active",
    "allocation": {
      "pendle": {
        "amount": "300.00",
        "percentage": 30.0,
        "protocol": "Pendle Finance",
        "currentValue": "307.50"
      },
      "curve": {
        "amount": "400.00",
        "percentage": 40.0,
        "protocol": "Curve 3pool",
        "currentValue": "410.00"
      },
      "convex": {
        "amount": "300.00",
        "percentage": 30.0,
        "protocol": "Convex Boost",
        "currentValue": "332.75"
      }
    },
    "performance": {
      "currentValue": "1050.25",
      "totalEarnings": "50.25",
      "roi": 5.03,
      "dailyYield": "2.74",
      "projectedAnnualReturn": "100.00"
    },
    "createdAt": "2025-10-01T10:00:00Z",
    "nextYieldDate": "2025-10-07T00:00:00Z"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Ошибки:

Код Код ошибки Описание
403 PERMISSION_DENIED Чужая инвестиция
404 INVESTMENT_NOT_FOUND ID не существует

DELETE /api/user/investments/:id

Удалить инвестицию (вывод всех средств на баланс).

Запрос

DELETE /api/user/investments/inv_abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "investmentId": "inv_abc123",
    "status": "closed",
    "finalValue": "1050.25",
    "initialInvestment": "1000.00",
    "totalEarnings": "50.25",
    "roi": 5.03,
    "withdrawnAmount": "1050.25",
    "closedAt": "2025-10-06T12:34:56Z",
    "fees": {
      "managementFee": "2.51",
      "gasFee": "0.005 ETH"
    }
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

🚫 POST /api/user/investments/:id/cancel

Отменить инвестицию (альтернативный метод к DELETE).

Запрос

POST /api/user/investments/inv_abc123/cancel
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Path параметры:

Параметр Тип Описание
id string ID инвестиции для отмены

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "inv_abc123",
    "status": "cancelled",
    "message": "Investment cancelled successfully"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Ошибки:

Код Код ошибки Описание
403 PERMISSION_DENIED Чужая инвестиция
404 INVESTMENT_NOT_FOUND ID не существует
422 INVESTMENT_ALREADY_CLOSED Уже закрыта

Пример TypeScript

const cancelInvestment = async (investmentId: string) => {
  const response = await fetch(`/api/user/investments/${investmentId}/cancel`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  const { data } = await response.json();

  console.log('Инвестиция отменена:', data.id);
  console.log('Статус:', data.status);

  return data;
};

await cancelInvestment('inv_abc123');

GET /api/user/investments/performance

Получить производительность всего инвестиционного портфеля.

Запрос

GET /api/user/investments/performance
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Заголовки:

Заголовок Значение Обязательно
Authorization Bearer <token> ✅ Да

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "portfolio": {
      "totalInvested": "5000.00",
      "totalCurrentValue": "5425.80",
      "totalEarnings": "425.80",
      "totalReturn": "425.80",
      "averageROI": 8.52,
      "weightedAPY": 12.5
    },
    "investments": [
      {
        "strategyId": "aggressive-20",
        "strategyName": "Aggressive 20% APY",
        "totalInvested": "2000.00",
        "currentValue": "2200.00",
        "earnings": "200.00",
        "roi": 10.0,
        "count": 1
      },
      {
        "strategyId": "balanced-10",
        "strategyName": "Balanced 10% APY",
        "totalInvested": "2000.00",
        "currentValue": "2150.00",
        "earnings": "150.00",
        "roi": 7.5,
        "count": 2
      },
      {
        "strategyId": "conservative-5",
        "strategyName": "Conservative 5% APY",
        "totalInvested": "1000.00",
        "currentValue": "1075.80",
        "earnings": "75.80",
        "roi": 7.58,
        "count": 1
      }
    ],
    "breakdown": {
      "byStatus": {
        "active": 4,
        "paused": 0,
        "closed": 1
      },
      "byStrategy": {
        "aggressive-20": 1,
        "balanced-10": 2,
        "conservative-5": 1
      }
    }
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Поля ответа:

Поле Тип Описание
portfolio.totalInvested string Общая сумма всех инвестиций
portfolio.totalCurrentValue string Текущая стоимость портфеля
portfolio.totalEarnings string Общая доходность
portfolio.totalReturn string Общий доход (earnings)
portfolio.averageROI number Средневзвешенный ROI
portfolio.weightedAPY number Взвешенный APY портфеля
investments array Breakdown по стратегиям
breakdown object Статистика по статусам и стратегиям

Ошибки:

Код Код ошибки Описание
401 UNAUTHORIZED Токен невалиден
500 INTERNAL_ERROR Ошибка получения данных

Пример cURL

TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://app.saga.surf/api/user/investments/performance \
  -H "Authorization: Bearer $TOKEN"

Пример TypeScript

const getPortfolioPerformance = async () => {
  const response = await fetch('/api/user/investments/performance', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  const { data } = await response.json();

  console.log('=== PORTFOLIO PERFORMANCE ===');
  console.log('Total Invested:', data.portfolio.totalInvested, 'USDC');
  console.log('Current Value:', data.portfolio.totalCurrentValue, 'USDC');
  console.log('Total Earnings:', data.portfolio.totalEarnings, 'USDC');
  console.log('Average ROI:', data.portfolio.averageROI, '%');
  console.log('Weighted APY:', data.portfolio.weightedAPY, '%');

  console.log('\n=== BREAKDOWN BY STRATEGY ===');
  data.investments.forEach(strategy => {
    console.log(`${strategy.strategyName}:`);
    console.log(`  Invested: ${strategy.totalInvested} USDC`);
    console.log(`  Current: ${strategy.currentValue} USDC`);
    console.log(`  Earnings: ${strategy.earnings} USDC (${strategy.roi}%)`);
    console.log(`  Count: ${strategy.count} investments`);
  });

  return data;
};

await getPortfolioPerformance();

React Hook для Portfolio Performance

import { useState, useEffect } from 'react';

interface PortfolioPerformance {
  portfolio: {
    totalInvested: string;
    totalCurrentValue: string;
    totalEarnings: string;
    averageROI: number;
    weightedAPY: number;
  };
  investments: Array<{
    strategyId: string;
    strategyName: string;
    totalInvested: string;
    currentValue: string;
    earnings: string;
    roi: number;
    count: number;
  }>;
  breakdown: {
    byStatus: Record<string, number>;
    byStrategy: Record<string, number>;
  };
}

export const usePortfolioPerformance = () => {
  const [performance, setPerformance] = useState<PortfolioPerformance | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchPerformance = async () => {
      try {
        const token = localStorage.getItem('saga_access_token');
        const response = await fetch('/api/user/investments/performance', {
          headers: {
            'Authorization': `Bearer ${token}`
          }
        });

        if (!response.ok) {
          throw new Error('Failed to fetch portfolio performance');
        }

        const { data } = await response.json();
        setPerformance(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };

    fetchPerformance();
  }, []);

  return { performance, loading, error };
};

// Usage in component:
// const { performance, loading, error } = usePortfolioPerformance();

ADMIN INVESTMENT ENDPOINTS

GET /api/admin/investments/queue

Получить очередь pending инвестиций для одобрения администратором.

Запрос

GET /api/admin/investments/queue
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Заголовки:

Заголовок Значение Обязательно
Authorization Bearer <admin_token> ✅ Да

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "pendingInvestments": [
      {
        "investmentId": "inv_pending123",
        "userId": "user_456",
        "userEmail": "investor@example.com",
        "strategy": "Balanced 10% APY Strategy",
        "amount": "5000.00",
        "currency": "USDC",
        "createdAt": "2025-10-06T10:30:00Z",
        "priority": "50.00"
      },
      {
        "investmentId": "inv_pending124",
        "userId": "user_789",
        "userEmail": "trader@example.com",
        "strategy": "Aggressive 20% APY Strategy",
        "amount": "10000.00",
        "currency": "USDC",
        "createdAt": "2025-10-06T09:15:00Z",
        "priority": "100.00"
      }
    ],
    "total": 2,
    "queueSize": 2
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Поля ответа:

Поле Тип Описание
pendingInvestments array Список pending инвестиций
pendingInvestments[].priority string Приоритет (рассчитывается по сумме)
total number Всего pending инвестиций
queueSize number Размер очереди

Ошибки:

Код Код ошибки Описание
401 UNAUTHORIZED Не admin токен
403 ADMIN_REQUIRED Требуется admin роль

Пример cURL

ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIs..."
curl -X GET https://admin.saga.surf/api/admin/investments/queue \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Пример TypeScript

const getInvestmentQueue = async () => {
  const adminToken = localStorage.getItem('saga_admin_token');

  const response = await fetch('/api/admin/investments/queue', {
    headers: {
      'Authorization': `Bearer ${adminToken}`
    }
  });

  const { data } = await response.json();

  console.log(`${data.queueSize} инвестиций в очереди:`);
  data.pendingInvestments.forEach(inv => {
    console.log(`- ${inv.userEmail}: ${inv.amount} USDC (${inv.strategy})`);
    console.log(`  Приоритет: ${inv.priority}, Создано: ${inv.createdAt}`);
  });

  return data;
};

await getInvestmentQueue();

POST /api/admin/investments/bulk-approve

Массовое одобрение нескольких инвестиций одновременно.

Запрос

POST /api/admin/investments/bulk-approve
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "investmentIds": [
    "inv_pending123",
    "inv_pending124",
    "inv_pending125"
  ]
}

Схема тела запроса:

Поле Тип Обязательно Описание
investmentIds array ✅ Да Массив ID инвестиций для одобрения

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "approved": [
      {
        "investmentId": "inv_pending123",
        "status": "approved",
        "approvedAt": "2025-10-06T12:34:56Z"
      },
      {
        "investmentId": "inv_pending124",
        "status": "approved",
        "approvedAt": "2025-10-06T12:34:56Z"
      }
    ],
    "failed": [
      {
        "investmentId": "inv_pending125",
        "error": "Investment already approved",
        "code": "ALREADY_APPROVED"
      }
    ],
    "summary": {
      "total": 3,
      "approved": 2,
      "failed": 1
    }
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Ошибки:

Код Код ошибки Описание
400 INVALID_REQUEST Пустой массив investmentIds
401 UNAUTHORIZED Не admin токен

Пример TypeScript

const bulkApproveInvestments = async (investmentIds: string[]) => {
  const adminToken = localStorage.getItem('saga_admin_token');

  const response = await fetch('/api/admin/investments/bulk-approve', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${adminToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ investmentIds })
  });

  const { data } = await response.json();

  console.log(`Одобрено: ${data.summary.approved}/${data.summary.total}`);

  if (data.failed.length > 0) {
    console.log('Ошибки:');
    data.failed.forEach(f => {
      console.log(`- ${f.investmentId}: ${f.error}`);
    });
  }

  return data;
};

await bulkApproveInvestments(['inv_pending123', 'inv_pending124']);

POST /api/admin/investments/:id/approve

Одобрить конкретную инвестицию.

Запрос

POST /api/admin/investments/inv_pending123/approve
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Path параметры:

Параметр Тип Описание
id string ID инвестиции для одобрения

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "investmentId": "inv_pending123",
    "status": "approved",
    "approvedAt": "2025-10-06T12:34:56Z",
    "approvedBy": "admin_789",
    "previousStatus": "pending"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Ошибки:

Код Код ошибки Описание
404 INVESTMENT_NOT_FOUND ID не существует
422 ALREADY_APPROVED Уже одобрена

POST /api/admin/investments/:id/reject

Отклонить инвестицию.

Запрос

POST /api/admin/investments/inv_pending123/reject
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "reason": "Недостаточная документация KYC"
}

Схема тела запроса:

Поле Тип Обязательно Описание
reason string ⚠️ Рекомендуется Причина отклонения

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "investmentId": "inv_pending123",
    "status": "rejected",
    "rejectedAt": "2025-10-06T12:34:56Z",
    "rejectedBy": "admin_789",
    "reason": "Недостаточная документация KYC"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

POST /api/admin/investments/:id/close

Закрыть инвестицию (admin принудительное закрытие).

Запрос

POST /api/admin/investments/inv_abc123/close
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "reason": "Стратегия больше не поддерживается"
}

Схема тела запроса:

Поле Тип Обязательно Описание
reason string ✅ Да Причина закрытия

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "investmentId": "inv_abc123",
    "status": "closed",
    "closedAt": "2025-10-06T12:34:56Z",
    "closedBy": "admin_789",
    "finalValue": "1050.25",
    "reason": "Стратегия больше не поддерживается",
    "fundsReturned": true
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

PUT /api/admin/investments/:id

Обновить параметры инвестиции.

Запрос

PUT /api/admin/investments/inv_abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "strategyId": "aggressive-20",
  "amount": "1200.00"
}

Схема тела запроса:

Поле Тип Обязательно Описание
strategyId string ❌ Нет Новая стратегия
amount string ❌ Нет Новая сумма
status string ❌ Нет Новый статус

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "inv_abc123",
    "strategyId": "aggressive-20",
    "amount": "1200.00",
    "status": "active",
    "updatedAt": "2025-10-06T12:34:56Z",
    "updatedBy": "admin_789"
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

GET /api/admin/investments/:id

Получить детали инвестиции (admin view с расширенной информацией).

Запрос

GET /api/admin/investments/inv_abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "id": "inv_abc123",
    "userId": "user_456",
    "userEmail": "investor@example.com",
    "strategyId": "balanced-10",
    "strategyName": "Balanced 10% APY Strategy",
    "amount": "1000.00",
    "status": "active",
    "createdAt": "2025-10-01T10:00:00Z",
    "approvedAt": "2025-10-01T10:30:00Z",
    "approvedBy": "admin_123",
    "performance": {
      "currentValue": "1050.25",
      "totalEarnings": "50.25",
      "roi": 5.03
    },
    "adminMetadata": {
      "riskScore": 3.5,
      "compliance": "verified",
      "lastAudit": "2025-10-05T14:00:00Z"
    }
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

GET /api/admin/investments

Список всех инвестиций с расширенными admin фильтрами.

Запрос

GET /api/admin/investments?status=pending&userId=user_456&page=1&limit=50
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Query параметры:

Параметр Тип Описание Default
page number Номер страницы 1
limit number Элементов на странице 50
status string Фильтр по статусу Все
userId string Фильтр по пользователю Все
strategyId string Фильтр по стратегии Все
sortBy string Поле сортировки createdAt
sortDir string Направление: asc, desc desc

Ответ

Успех (200 OK):

{
  "success": true,
  "data": {
    "investments": [
      {
        "id": "inv_abc123",
        "userId": "user_456",
        "userEmail": "investor@example.com",
        "strategyId": "balanced-10",
        "strategyName": "Balanced 10% APY Strategy",
        "amount": "1000.00",
        "status": "active",
        "createdAt": "2025-10-01T10:00:00Z"
      }
    ],
    "total": 1,
    "pagination": {
      "page": 1,
      "limit": 50,
      "totalPages": 1
    }
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

💼 POST /api/admin/investments

Создать инвестицию от имени пользователя (admin operation).

Запрос

POST /api/admin/investments
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "userId": "user_456",
  "strategyId": "balanced-10",
  "amount": "1000.00",
  "autoApprove": true
}

Схема тела запроса:

Поле Тип Обязательно Описание
userId string ✅ Да ID пользователя
strategyId string ✅ Да ID стратегии
amount string ✅ Да Сумма инвестиции
autoApprove boolean ❌ Нет Автоматическое одобрение (default: false)

Ответ

Успех (201 Created):

{
  "success": true,
  "data": {
    "id": "inv_admin_created_123",
    "userId": "user_456",
    "strategyId": "balanced-10",
    "amount": "1000.00",
    "status": "approved",
    "createdAt": "2025-10-06T12:34:56Z",
    "createdBy": "admin_789",
    "autoApproved": true
  },
  "timestamp": "2025-10-06T12:34:56Z"
}

Use Cases

Use Case 1: Complete Investment Flow (User)

import { v4 as uuidv4 } from 'uuid';

async function completeInvestmentFlow() {
  // 1. Проверить портфель
  const { performance } = await fetch('/api/user/investments/performance', {
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  console.log('Текущий портфель:', performance.portfolio.totalCurrentValue);

  // 2. Создать новую инвестицию
  const newInvestment = await fetch('/api/user/investments', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      strategyId: 'balanced-10',
      amount: '1000.00',
      idempotencyKey: uuidv4()
    })
  }).then(r => r.json());

  console.log('Инвестиция создана:', newInvestment.data.id);

  // 3. Получить детали
  const details = await fetch(`/api/user/investments/${newInvestment.data.id}`, {
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  console.log('Allocation:', details.data.allocation);

  // 4. При необходимости отменить
  if (needCancel) {
    await fetch(`/api/user/investments/${newInvestment.data.id}/cancel`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${token}` }
    });
  }
}

Use Case 2: Admin Investment Queue Management

async function manageInvestmentQueue() {
  const adminToken = localStorage.getItem('saga_admin_token');

  // 1. Получить очередь
  const queue = await fetch('/api/admin/investments/queue', {
    headers: { 'Authorization': `Bearer ${adminToken}` }
  }).then(r => r.json());

  console.log(`${queue.data.queueSize} pending инвестиций`);

  // 2. Bulk approve высокоприоритетных
  const highPriority = queue.data.pendingInvestments
    .filter(inv => parseFloat(inv.priority) >= 75)
    .map(inv => inv.investmentId);

  if (highPriority.length > 0) {
    const result = await fetch('/api/admin/investments/bulk-approve', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${adminToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ investmentIds: highPriority })
    }).then(r => r.json());

    console.log(`Approved: ${result.data.summary.approved}`);
  }

  // 3. Reject низкоприоритетных
  const lowPriority = queue.data.pendingInvestments
    .filter(inv => parseFloat(inv.priority) < 25);

  for (const inv of lowPriority) {
    await fetch(`/api/admin/investments/${inv.investmentId}/reject`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${adminToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        reason: 'Низкий приоритет - требуется дополнительная проверка'
      })
    });
  }
}

Use Case 3: Portfolio Dashboard Component

import { usePortfolioPerformance } from './hooks/usePortfolioPerformance';
import { Loader, AlertCircle } from 'lucide-react';

export const PortfolioDashboard = () => {
  const { performance, loading, error } = usePortfolioPerformance();

  if (loading) return <Loader className="animate-spin" />;
  if (error) return <AlertCircle className="text-red-500" />;
  if (!performance) return null;

  const { portfolio, investments } = performance;

  return (
    <div className="space-y-6">
      <div className="grid grid-cols-3 gap-4">
        <Card>
          <h3>Total Invested</h3>
          <p className="text-2xl font-bold">{portfolio.totalInvested} USDC</p>
        </Card>

        <Card>
          <h3>Current Value</h3>
          <p className="text-2xl font-bold">{portfolio.totalCurrentValue} USDC</p>
        </Card>

        <Card>
          <h3>Total Earnings</h3>
          <p className="text-2xl font-bold text-green-600">
            +{portfolio.totalEarnings} USDC ({portfolio.averageROI}%)
          </p>
        </Card>
      </div>

      <div>
        <h3 className="text-lg font-semibold mb-4">Breakdown by Strategy</h3>
        {investments.map(strategy => (
          <div key={strategy.strategyId} className="border-b py-3">
            <div className="flex justify-between items-center">
              <div>
                <p className="font-medium">{strategy.strategyName}</p>
                <p className="text-sm text-gray-500">
                  {strategy.count} {strategy.count === 1 ? 'investment' : 'investments'}
                </p>
              </div>
              <div className="text-right">
                <p className="font-semibold">{strategy.currentValue} USDC</p>
                <p className="text-sm text-green-600">
                  +{strategy.earnings} USDC ({strategy.roi}%)
                </p>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

Use Case 4: Admin Investment Management Panel

import { useState, useEffect } from 'react';

export const AdminInvestmentPanel = () => {
  const [queue, setQueue] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchQueue();
  }, []);

  const fetchQueue = async () => {
    const adminToken = localStorage.getItem('saga_admin_token');
    const response = await fetch('/api/admin/investments/queue', {
      headers: { 'Authorization': `Bearer ${adminToken}` }
    });
    const { data } = await response.json();
    setQueue(data.pendingInvestments);
    setLoading(false);
  };

  const handleApprove = async (investmentId: string) => {
    const adminToken = localStorage.getItem('saga_admin_token');
    await fetch(`/api/admin/investments/${investmentId}/approve`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${adminToken}` }
    });
    fetchQueue(); // Refresh
  };

  const handleReject = async (investmentId: string, reason: string) => {
    const adminToken = localStorage.getItem('saga_admin_token');
    await fetch(`/api/admin/investments/${investmentId}/reject`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${adminToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ reason })
    });
    fetchQueue(); // Refresh
  };

  if (loading) return <Loader />;

  return (
    <div>
      <h2 className="text-xl font-bold mb-4">
        Investment Queue ({queue.length} pending)
      </h2>

      <div className="space-y-3">
        {queue.map(inv => (
          <div key={inv.investmentId} className="border p-4 rounded">
            <div className="flex justify-between items-start mb-2">
              <div>
                <p className="font-medium">{inv.userEmail}</p>
                <p className="text-sm text-gray-600">{inv.strategy}</p>
              </div>
              <div className="text-right">
                <p className="font-bold">{inv.amount} USDC</p>
                <p className="text-xs text-gray-500">
                  Priority: {inv.priority}
                </p>
              </div>
            </div>

            <div className="flex gap-2 mt-3">
              <button
                onClick={() => handleApprove(inv.investmentId)}
                className="px-4 py-2 bg-green-600 text-white rounded"
              >
                Approve
              </button>
              <button
                onClick={() => {
                  const reason = prompt('Причина отклонения:');
                  if (reason) handleReject(inv.investmentId, reason);
                }}
                className="px-4 py-2 bg-red-600 text-white rounded"
              >
                Reject
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

Use Case 5: Investment Cancellation with Confirmation

async function cancelInvestmentWithConfirmation(investmentId: string) {
  // 1. Получить детали для подтверждения
  const details = await fetch(`/api/user/investments/${investmentId}`, {
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  const { data: investment } = details;

  // 2. Показать confirmation dialog
  const confirmed = confirm(`
    Отменить инвестицию?

    Стратегия: ${investment.strategyName}
    Начальная сумма: ${investment.amount} USDC
    Текущая стоимость: ${investment.performance.currentValue} USDC
    Доходность: ${investment.performance.totalEarnings} USDC

    Средства будут возвращены на ваш баланс.
  `);

  if (!confirmed) {
    console.log('Отмена отменена пользователем');
    return;
  }

  // 3. Выполнить отмену
  const result = await fetch(`/api/user/investments/${investmentId}/cancel`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  console.log('Инвестиция отменена:', result.data);

  // 4. Обновить UI - показать новый баланс
  const balance = await fetch('/api/user/balance', {
    headers: { 'Authorization': `Bearer ${token}` }
  }).then(r => r.json());

  console.log('Новый баланс:', balance.data.availableBalance);

  return result.data;
}

Связанная документация

Другие User Endpoints:

Admin Documentation:

Руководства:

Архитектура:


Technical Details

Investment Status State Machine

pending → approved → active → closed
         rejected

Статусы:

  • pending - Ожидает одобрения админа
  • approved - Одобрена админом
  • active - Активна и зарабатывает доходность
  • paused - Временно приостановлена
  • closed - Закрыта (средства выведены)
  • rejected - Отклонена админом

Priority Calculation

Приоритет инвестиций в admin queue рассчитывается по формуле:

priority = Math.min(100, (amount / 100) * 1.5)

Диапазоны:

  • 0-25: Низкий приоритет
  • 26-50: Средний приоритет
  • 51-75: Высокий приоритет
  • 76-100: Критический приоритет

Performance Metrics

ROI (Return on Investment):

ROI = ((currentValue - initialInvestment) / initialInvestment) * 100

Weighted APY:

weightedAPY = Σ(investment.amount * investment.apy) / totalInvested

Daily Yield (projected):

dailyYield = (amount * apy) / 365

Security Best Practices

Frontend:

  • Всегда валидировать суммы перед отправкой
  • Использовать idempotency keys для создания инвестиций
  • Показывать confirmation dialogs для критичных операций (cancel, delete)
  • Хранить admin токены отдельно от user токенов
  • Implement automatic token refresh для бесшовного UX

Backend:

  • Investment amounts валидируются через SafeDecimal
  • Admin операции требуют дополнительной авторизации
  • Bulk operations имеют rate limiting
  • Все investment state changes логируются в audit trail
  • Защита от race conditions через database transactions



📋 Метаданные

Версия: 2.6.268

Обновлено: 2025-11-14

Статус: Ready for Implementation

Архитектура: Single Strategy (10% APY), Fordefi + Crypto2B Asset Management


"Single Strategy MVP: единственная инвестиционная опция, автоматическое создание, external asset management"

— Saga Development Team (Phase 2 API Updates)