Підключення гаманця — це перша взаємодія користувача з вашим застосунком на Solana. Нижче наведено покрокову інструкцію, яка працює в браузері з Devnet-кластером і пояснює, що саме відбувається на кожному етапі.

Як працюють гаманці у Solana

Phantom, Solflare, Backpack — огляд

Гаманець у Solana — це програма, яка зберігає приватний ключ користувача і надає браузерному застосунку обмежений доступ до підписання транзакцій. Приватний ключ ніколи не покидає розширення гаманця.

Найпоширеніші гаманці:

  • Phantom — найпопулярніший гаманець у Solana-екосистемі, браузерне розширення з мобільним додатком.
  • Solflare — багатоланцюговий гаманець із нативною підтримкою Solana, доступний як розширення й окремий додаток.
  • Backpack — гаманець із вбудованою підтримкою xNFT та власним маркетплейсом застосунків.

Усі три гаманці реалізують схожий інтерфейс взаємодії з вебзастосунками, але мають відмінності в деталях API та додаткових можливостях.

Wallet Standard та window.solana

Історично кожен гаманець впроваджував власний спосіб взаємодії. Phantom першим запровадив об'єкт window.solana — глобальну змінну, яку розширення ін'ектує в сторінку. Інші гаманці почали копіювати цей підхід, що призвело до фрагментації.

Wallet Standard — це спільна специфікація, яку розробники гаманців прийняли для уніфікації. Замість окремих глобальних змінних для кожного гаманця, Wallet Standard визначає єдиний реєстр через navigator.wallets або функцію getWallets() із пакета @wallet-standard/base. Застосунок перебирає зареєстровані гаманці та працює з ними через стандартизовану структуру features.

У цій інструкції ми використовуємо window.solana як базовий підхід, оскільки він підтримується всіма згаданими гаманцями й найзручніший для розуміння механіки. Перехід на Wallet Standard — це наступний крок після засвоєння основ.

Виявлення гаманця

Перевірка наявності window.solana

Перед тим як пропонувати користувачу підключитися, застосунок має перевірити, чи встановлено розширення гаманця.

Середовище: браузер (Chrome, Firefox, Edge, Brave)
Кластер: будь-який (Devnet, Mainnet-beta) — виявлення гаманця не залежить від кластера
Передумови: встановлене розширення Phantom, Solflare або Backpack

if (window.solana) {
  console.log('Гаманець знайдено:', window.solana.isPhantom ? 'Phantom' : 'Інший');
} else {
  console.log('Гаманець не виявлено');
}

Об'єкт window.solana містить поле isPhantom (true для Phantom) та поле isSolflare у випадку Solflare. Якщо встановлено кілька гаманців, window.solana зазвичай належить тому, який ін'ектував його останнім.

Обробка випадку без гаманця

Якщо window.solana відсутній, користувачеві потрібно показати зрозуміле повідомлення з інструкцією щодо встановлення гаманця. Не варто виводити технічні деталі — достатньо пояснити, що для роботи потрібен гаманець, і дати кнопку для переходу на сайт обраного гаманця.

if (!window.solana) {
  document.getElementById('wallet-status').textContent =
    'Гаманець не знайдено. Встановіть Phantom або Solflare.';
  return;
}

Очікуваний результат: при відкритті сторінки з встановленим розширенням у консолі з'являється повідомлення про знайдений гаманець. Без розширення — повідомлення для користувача в інтерфейсі.

Підключення та відключення

connect / disconnect — методи

Підключення — це запит до гаманця дозволити застосунку отримувати публічний ключ та підписувати транзакції від імені користувача. Користувач бачить спливаюче вікно розширення з пропозицією підключитися.

async function connectWallet() {
  try {
    const response = await window.solana.connect();
    console.log('Публічний ключ:', response.publicKey.toString());
  } catch (error) {
    console.error('Помилка підключення:', error);
  }
}

Метод connect() повертає об'єкт із полем publicKey — це об'єкт типу PublicKey із пакета @solana/web3.js. Для відображення або порівняння його перетворюють на рядок через toString(), який повертає Base58-закодований адрес.

Відключення розриває зв'язок між застосунком і гаманцем:

async function disconnectWallet() {
  await window.solana.disconnect();
  console.log('Гаманець відключено');
}

Важливо: метод disconnect() не вимагає підтвердження користувача. Застосунок може викликати його самостійно, наприклад, при натисканні кнопки «Вийти».

Отримання publicKey підключеного гаманця

Після підключення публічний ключ доступний через властивість publicKey об'єкта гаманця без повторного виклику connect():

const publicKey = window.solana.publicKey;
if (publicKey) {
  console.log('Адреса:', publicKey.toString());
} else {
  console.log('Гаманець не підключено');
}

Це корисно при перезавантаженні сторінки — якщо гаманець раніше дав дозвіл цьому застосунку, publicKey може бути доступний одразу без додаткового запиту. Проте надійніше перевіряти стан через прослуховування подій.

Для відстеження змін стану підключення використовуйте події:

window.solana.on('connect', (publicKey) => {
  console.log('Підключено:', publicKey.toString());
});

window.solana.on('disconnect', () => {
  console.log('Відключено');
});

window.solana.on('accountChanged', (publicKey) => {
  if (publicKey) {
    console.log('Обліковий запис змінено:', publicKey.toString());
  } else {
    console.log('Обліковий запис відключено в гаманці');
  }
});

Очікуваний результат: після виклику connectWallet() з'являється спливаюче вікно гаманця. Після підтвердження в консолі відображається адреса гаманця. При виклику disconnectWallet() стан оновлюється без спливаючого вікна. Подія accountChanged спрацьовує, коли користувач перемикає обліковий запис усередині гаманця.

Підписання транзакцій

signTransaction — підписання клієнтської транзакції

Підключення гаманця дає можливість підписувати транзакції. Сама відправка транзакції в мережу — це окремий крок, який розглядається в наступному матеріалі. Тут ми зосереджуємося виключно на підписанні.

Середовище: браузер
Пакет: @solana/web3.js версії 1.95.x
Кластер: Devnet
Передумови: підключений гаманець, імпортовані Transaction, PublicKey, SystemProgram та LAMPORTS_PER_SOL із @solana/web3.js

import { Transaction, PublicKey, SystemProgram, LAMPORTS_PER_SOL } from '@solana/web3.js';

async function signExampleTransaction() {
  const transaction = new Transaction();

  transaction.add(
    SystemProgram.transfer({
      fromPubkey: window.solana.publicKey,
      toPubkey: new PublicKey('11111111111111111111111111111111'),
      lamports: LAMPORTS_PER_SOL * 0.001,
    })
  );

  try {
    const signed = await window.solana.signTransaction(transaction);
    console.log('Транзакцію підписано');
    console.log('Кількість підписів:', signed.signatures.length);
  } catch (error) {
    console.error('Підписання скасовано:', error);
  }
}

Метод signTransaction() приймає об'єкт Transaction і повертає той самий об'єкт із доданим підписом у масив signatures. Приватний ключ не передається в застосунок — підпис формується всередині розширення гаманця.

Концептуальний приклад проти production-рішення: наведений вище код створює транзакцію без вказання recentBlockhash та feePayer. Для реального використання ці поля обов'язкові, і їхні значення потрібно отримати через RPC-виклик до кластера. Цей приклад демонструє виключно механіку підписання, а не повний робочий процес відправки.

signMessage — підписання довільного повідомлення

Іноді застосунку не потрібна транзакція, а достатньо підтвердити володіння ключем — наприклад, для авторизації без відправки транзакції в мережу.

async function signExampleMessage() {
  const message = new TextEncoder().encode('Перевірка володіння гаманцем');

  try {
    const { signature } = await window.solana.signMessage(message);
    console.log('Підпис (Base58):', signature);
  } catch (error) {
    console.error('Підписання повідомлення скасовано:', error);
  }
}

Метод signMessage() приймає Uint8Array і повертає об'єкт із полем signature — рядок у кодуванні Base58. Застосунок може перевірити цей підпис за допомогою публічного ключа гаманця та бібліотеки tweetnacl або методів @solana/web3.js, не звертаючись до мережі Solana.

Очікуваний результат: при виклику signExampleMessage() гаманець показує запит на підписання тексту. Після підтвердження в консолі з'являється підпис. Цей підпис можна верифікувати офлайн, порівнявши його з публічним ключем та оригінальним повідомленням.

Типові помилки

Користувач відхилив підключення

Коли користувач натискає «Скасувати» у спливаючому вікні гаманця, метод connect() генерує помилку. Об'єкт помилки зазвичай містить код 4001 і повідомлення User rejected the request.

try {
  await window.solana.connect();
} catch (error) {
  if (error.code === 4001) {
    console.log('Користувач сам відхилив запит — це нормальна ситуація');
  } else {
    console.error('Неочікувана помилка:', error.message);
  }
}

Ця ситуація не є технічною помилкою застосунку. Не варто показувати користувачеві повідомлення про збій — достатньо залишити кнопку підключення активною для повторної спроби.

Гаманець не підтримує потрібний метод

Не всі гаманці реалізують повний набір методів. Наприклад, деякі гаманці можуть не підтримувати signMessage() або signAllTransactions(). Перед викликом методу перевіряйте його наявність:

if (typeof window.solana.signMessage === 'function') {
  const { signature } = await window.solana.signMessage(message);
} else {
  console.log('Цей гаманець не підтримує підписання повідомлень');
}

Також варто враховувати, що деякі старі версії розширень можуть не підтримувати signAllTransactions() — метод для пакетного підписання кількох транзакцій одночасно. Якщо ваш застосунок покладається на цей метод, додайте перевірку та альтернативну гілку з послідовним викликом signTransaction() для кожної транзакції окремо.

Додаткова рекомендація: у production-застосунках замість прямої роботи з window.solana використовуйте бібліотеки-адаптери (зокрема @solana/wallet-adapter-react), які автоматично обробляють ці крайові випадки, керують станом підключення та підтримують Wallet Standard. Детальніше про налаштування адаптерів розглядається в матеріалі «Solana Kit для початківця».

Після підключення гаманця та підписання транзакції наступний логічний крок — відправити підписану транзакцію в мережу. Як це зробити через TypeScript, описано в матеріалі «Як надіслати транзакцію через TypeScript».

Джерела