Перейти к содержимому
Последнее обновление

Приём депозитов для обменников

Пошаговое руководство по настройке приёма USDT от пользователей вашей платформы.

Сценарий

Вы — обменник или биржа. Ваши пользователи хотят пополнить баланс в USDT:

  1. Пользователь запрашивает адрес для пополнения
  2. Ваша система создаёт уникальный кошелёк
  3. Пользователь отправляет USDT
  4. Lumo проводит AML-проверку и пересылает средства на ваш адрес
  5. Вы получаете webhook и зачисляете сумму пользователю

Преимущества

  • Без блокчейн-кода — не нужно работать с TRON напрямую
  • AML из коробки — автоматическая проверка входящих транзакций
  • Автопересылка — средства собираются на вашем адресе depositForwardAddress
  • Webhooks — real-time уведомления

Деньги пользователей не зачисляются на баланс компании в Lumo: они пересылаются в сети на ваш адрес. С баланса в Lumo списывается только комиссия за обработку каждого депозита — держите там запас.

Шаг 1: Настройка webhook и адреса пересылки

Сначала настройте endpoint для получения уведомлений. Подпись проверяйте по сырому телу запроса:

const crypto = require('crypto');
const express = require('express');

const app = express();

app.post(
  '/webhooks/lumo',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.get('x-lumo-signature') || '';
    const expected = crypto
      .createHmac('sha256', process.env.LUMO_WEBHOOK_SECRET)
      .update(req.body) // сырое тело, Buffer
      .digest('hex');

    if (
      signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    ) {
      return res.status(401).send('Invalid signature');
    }

    res.status(200).send('OK');
    await handleWebhook(JSON.parse(req.body.toString('utf8')));
  },
);

async function handleWebhook(payload) {
  // У депозитов на кошельки пользователей всегда есть externalId
  if (!('externalId' in payload)) return;

  switch (payload.event) {
    case 'deposit.completed':
      await creditUserBalance(payload);
      break;
    case 'deposit.aml_failed':
      await handleAmlFailed(payload);
      break;
    case 'deposit.failed':
      await notifySupport(payload);
      break;
  }
}

Сгенерируйте секрет подписи и обновите профиль компании — адрес для вебхуков и TRON-адрес, куда будут пересылаться депозиты:

curl -X POST 'https://b2b.lumowallet.io/auth/company/webhook-secret/rotate' \
  -H 'X-API-Key: YOUR_API_KEY'

curl -X PATCH 'https://b2b.lumowallet.io/auth/company/profile' \
  -H 'X-API-Key: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "webhookUrl": "https://your-server.com/webhooks/lumo",
    "depositForwardAddress": "TQ2x4bW8ZGq3NkL2bUQvX8sJ7d1cQh1xYz"
  }'

Задайте depositForwardAddress до создания первого кошелька: пока он не задан, депозиты пересылаются на казначейский адрес Lumo, а не вам.

Шаг 2: Создание кошелька для пользователя

Когда пользователь запрашивает адрес для пополнения:

async function getDepositAddress(userId) {
  const response = await fetch('https://b2b.lumowallet.io/user-wallets', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.LUMO_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      externalId: userId  // Ваш внутренний ID пользователя
    })
  });
  
  const wallet = await response.json();
  return wallet.address;
}

Ответ:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "companyId": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
  "externalId": "user-12345",
  "address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
  "isBlocked": false,
  "blockReason": null,
  "lastUsdtBalance": 0,
  "lastTrxBalance": 0,
  "createdAt": "2026-03-10T12:00:00.000Z"
}

💡 Идемпотентность: Повторный запрос с тем же externalId вернёт существующий незаблокированный кошелёк. Создание кошелька требует право balance.withdraw.

Шаг 3: Показ адреса пользователю

function DepositPage({ userId }) {
  const [address, setAddress] = useState(null);
  
  useEffect(() => {
    getDepositAddress(userId).then(setAddress);
  }, [userId]);
  
  return (
    <div>
      <h2>Пополнение баланса</h2>
      <p>Отправьте USDT (TRC-20) на адрес:</p>
      <AddressBox>{address}</AddressBox>
      <QRCode value={address} />
      <Warning>
        ⚠️ Отправляйте только USDT в сети TRON (TRC-20)
      </Warning>
    </div>
  );
}

Шаг 4: Обработка депозита

Когда пользователь отправляет USDT, Lumo проверяет перевод и пересылает его на ваш depositForwardAddress. По кошелькам пользователей приходят три события: deposit.completed, deposit.aml_failed и deposit.failed. Промежуточных событий (deposit.created, deposit.aml_pending) нет — если хотите показывать пользователю ожидание, отслеживайте входящий перевод на адрес сами или опрашивайте GET /user-wallets/deposits.

Депозит завершён

{
  "event": "deposit.completed",
  "depositId": "770e8400-e29b-41d4-a716-446655440002",
  "externalId": "user-12345",
  "txHash": "c4d2e8...19fa",
  "walletAddress": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
  "amount": 100,
  "status": "completed",
  "amlCheckId": null,
  "amlResult": null,
  "sweepTxHash": "d9a0b1...77e3",
  "error": null,
  "blockTimestamp": null,
  "createdAt": "2026-03-10T12:05:00.000Z",
  "updatedAt": "2026-03-10T12:05:00.000Z"
}

Пересылка на ваш адрес отправлена в сеть (sweepTxHash). Зачислите сумму пользователю — ровно один раз на depositId:

async function creditUserBalance({ depositId, externalId, amount }) {
  await db.transaction(async (tx) => {
    // уникальный индекс по depositId не даст зачислить дважды
    const inserted = await tx.deposits.insertIfAbsent({ depositId, externalId, amount });
    if (!inserted) return;
    await tx.users.increment(externalId, 'balance', amount);
  });

  await notifyUser(externalId, { type: 'deposit_success', amount });
}

Пересылка не удалась

deposit.failed — переслать депозит не удалось, причина в error. Например, на балансе компании в Lumo не хватило средств на комиссию за обработку (Insufficient company balance for processing fee): пересылка повторяется через 5 минут, 1 час и 4 часа, и только потом приходит deposit.failed. Средства при этом остаются на адресе пользователя — напишите в поддержку с depositId.

deposit.failed не всегда окончательный: после некоторых ошибок пересылка повторяется, и по тому же depositId может прийти deposit.completed. Поэтому не помечайте депозит навсегда проваленным и зачисляйте по deposit.completed.

Шаг 5: Обработка AML-блокировок

При deposit.aml_failed кошелёк пользователя блокируется, средства остаются на его адресе, в amlResult — показатель риска:

async function handleAmlFailed(payload) {
  const { externalId, amount, depositId, amlResult } = payload;

  // Уведомить пользователя
  await notifyUser(externalId, {
    type: 'deposit_blocked',
    message: 'Депозит заблокирован для проверки'
  });

  // Создать тикет для поддержки
  await createSupportTicket({
    type: 'aml_review',
    userId: externalId,
    depositId,
    amount,
    risk: amlResult,
  });
}

Новые депозиты на заблокированный адрес не обрабатываются. Чтобы пользователь мог пополнять снова, выдайте ему новый адрес — повторный POST /user-wallets с тем же externalId создаст новый кошелёк. Средства с заблокированного кошелька можно вывести на указанный адрес через POST /user-wallets/{walletId}/withdraw, см. User Wallets.

Шаг 6: Просмотр истории депозитов

async function getUserDeposits(userId) {
  const response = await fetch(
    `https://b2b.lumowallet.io/user-wallets/deposits?externalId=${userId}`,
    { headers: { 'X-API-Key': process.env.LUMO_API_KEY } }
  );
  
  return response.json();
}

Блокировка кошелька

При подозрительной активности заблокируйте кошелёк:

async function blockUserWallet(walletId, reason) {
  await fetch(`https://b2b.lumowallet.io/user-wallets/${walletId}/block`, {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.LUMO_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ reason })
  });
}

Замена кошелька

Если пользователь просит новый адрес:

async function replaceUserWallet(walletId) {
  const response = await fetch(
    `https://b2b.lumowallet.io/user-wallets/${walletId}/replace`,
    {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.LUMO_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ reason: 'User requested new address' })
    }
  );
  
  const newWallet = await response.json();
  return newWallet.address;  // Новый адрес
}

Полный пример

class DepositService {
  constructor(apiKey, webhookSecret) {
    this.apiKey = apiKey;
    this.webhookSecret = webhookSecret;
    this.baseUrl = 'https://b2b.lumowallet.io';
  }

  async getOrCreateWallet(userId) {
    const response = await fetch(`${this.baseUrl}/user-wallets`, {
      method: 'POST',
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ externalId: userId })
    });
    
    return response.json();
  }

  // rawBody — Buffer с телом запроса в том виде, в каком оно пришло
  verifyWebhook(rawBody, signature) {
    const expected = crypto
      .createHmac('sha256', this.webhookSecret)
      .update(rawBody)
      .digest('hex');
    return (
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
  }

  async handleWebhook(payload) {
    switch (payload.event) {
      case 'deposit.completed':
        await this.onDepositCompleted(payload);
        break;
      case 'deposit.aml_failed':
        await this.onAmlFailed(payload);
        break;
      case 'deposit.failed':
        await this.onDepositFailed(payload);
        break;
    }
  }

  async onDepositCompleted({ depositId, externalId, amount }) {
    // Зачисляем баланс ровно один раз на depositId
    const isNew = await db.processedDeposits.insertIfAbsent(depositId);
    if (!isNew) return;
    await db.users.increment(externalId, 'balance', amount);
    
    // Уведомляем пользователя
    await notifications.send(externalId, {
      title: 'Депозит зачислен',
      body: `+${amount} USDT`
    });
  }

  async onAmlFailed({ externalId, depositId }) {
    await notifications.send(externalId, {
      title: 'Депозит на проверке',
      body: 'Свяжитесь с поддержкой'
    });
  }

  async onDepositFailed(payload) {
    await support.notify('Депозит не переслан', payload);
  }
}

Чек-лист интеграции

  • Настроен webhook endpoint
  • Webhook URL и depositForwardAddress заданы в профиле
  • Сгенерирован секрет подписи, подпись проверяется по сырому телу
  • На балансе компании в Lumo есть запас на комиссии за обработку
  • Создание кошельков для пользователей
  • UI для показа адреса + QR-код
  • Обработка deposit.completed
  • Обработка deposit.aml_failed и deposit.failed
  • Зачисление баланса пользователю ровно один раз на depositId
  • Уведомления пользователю
  • Логирование всех событий