Web3 для JavaScript-разработчиков на примере Solana

в 9:15, , рубрики: Solana, web3
Web3 для JavaScript-разработчиков на примере Solana - 1

Рассказать надёжный способ заработать на криптовалютах? Устраиваетесь в блокчейн-проект разработчиком, пишете код, получаете зарплату. Готово, вы стали богаче, пока другие прогорали на бирже!

Шутки шутками, но мир web3 разросся в индустрию со своими вакансиями. И в ней востребован JavaScript, причём не только на фронтенде. Так что у JS-разработчиков появился смысл обращать внимание на этот сегмент, а на нашей конференции HolyJS появились доклады об этом.

И для Хабра мы решили перевести в текстовый вид выступление Александра Казакова @AlexandrKazakov помогающее влиться в тему. В этом докладе рассматривается блокчейн Solana, но после него проще будет разбираться и с другими вроде Ethereum или TON. Если вам удобнее видео, прилагаем ссылки: YouTube, VK Видео. Далее — текст от лица спикера.

Содержание

  • Что такое Solana

  • Настройка. Подключение и настройка библиотеки Web3.js.

  • Криптокошельки. Как создавать, из чего состоят, как использовать.

  • Базовая информация о транзакциях.

  • Транзакции Solana (SOL): как создаются, как отправлять.

  • SPL-токены, или мемкоины. Как создать свой SPL-токен, транзакции SPL.

  • NFT-токены: как создать NFT-токен, транзакции NFT.

  • Примеры использования. Как написать криптокошелек, Web3-бот в телеграме, мобильное Web3-приложение, Web3-игру.

  • Куда двигаться дальше: авторизация в веб-приложении по криптокошелькам, криптоботы для DEX, смарт-контракты.

Что такое Solana

Solana — блокчейн с быстрой обработкой транзакций и низкими комиссиями. Поддерживает смарт-контракты и децентрализованные приложения (dApps).

Я ввел запрос «крупнейшиие блокчейны», и на скриншоте видно, что  по метрике TVL Solana оказывается на втором месте в списке.

Web3 для JavaScript-разработчиков на примере Solana - 2

Предлагаю скачать один из этих Solana-кошельков, если вы никогда не использовали Solana:

  • Trust Wallet — поддерживает несколько сетей, довольно популярен. 

  • Phantom — разрабатывался именно под Solana. 

В тексте будут примеры из Phantom, но вы можете использовать любой кошелек. Есть как мобильные приложения, так и расширения для Google Chrome. 

Настройка

Для начала установите официальную библиотеку от Solana для работы в Web3:

npm install @solana/web3.js

Подключение к сети Solana

Создаем объект connection:

import { Connection } from '@solana/web3.js'const connection = new Connection(solanaRpcUrl, {
commitment: 'confirmed', // уровень подтверждения транзакций
});

const connection — это объект, который содержит методы и свойства для взаимодействия с Solana RPC-сервером. Он позволяет выполнять запросы, получать данные о блоках, транзакциях, аккаунтах и многом другом.

В три строки подключаемся к сети, а точнее, это что-то вроде конфига, через который мы потом будем вызывать те или иные методы и взаимодействовать с сетью.

Тут нас интересует следующее:

  •  commitment: ‘confirmed’. У Solana усть три уровня подтверждения транзакции: «Обработана», «Подтверждена» и «Завершена». У нас уровень «Подтверждена confirmed)»..

  • Переменная solanaRpcUrl. При создании объекта connection нужно обязательно передавать URL на RPC-узел Solana: без этого ничего не сработает. 

Solana RPC URL

const solanaRpcUrl = https://api.mainnet.solana.com     

        https://mainnet.helius-rpc.com/?api-key=*

У меня есть два примера URL на такие узлы. Первый — https://api.mainnet.solana.com. Это URL официального Mainnet RPC-узла Solana. Как и во многих блокчейнах, у Solana есть три окружения: Devnet, Testnet, Mainnet. Mainnet — рабочая версия, и наша задача — подключиться к ней. Официальный Mainnet RPC-узел Solana зачастую не работоспособен из-за большой нагрузки на сеть, он почти никогда не используется, даже для небольшого тестирования.

Следующий вариант URL — Helius RPC. Это платный сервис, который по подписке предоставляет доступ к своим RPC-узлам Solana. Можно выбирать различные тарифные планы в зависимости от потребностей и нагрузки, которую собираемся давать. RPC-узел, к которому будем подключаться, — это интерфейс для взаимодействия с блокчейном. 

RPC-узлы

Расскажу еще немного об RPC-узлах.

  • Можем создать свой RPC-узел. 

  • Можем подключаться к готовому платному RPC-узлу с помесячной оплатой, например, QuickNode или Helius.

  • Есть бесплатные общедоступные RPC, но они часто работают нестабильно и не подходят даже для тестирования. Не рекомендую: вы установите, потратите кучу времени, а окажется, что все дело в бесплатном RPC-узле. 

  • Есть три основные сети (окружения): Mainnet Beta, Devnet, Testnet. Сейчас мы разбираемся с Mainnet: как работать в проде.

Примеры операций через RPC-узлы

Виды операций, которые мы можем выполнять, используя RPC-узел:

  • Получение информации о блоках, транзакциях, балансах.

  • Отправка новых транзакций.

  • Взаимодействие с программами в блокчейне.

  • Получение данных аккаунтов (кошельков).

Криптокошельки

Подробно разберем, что это и как с ними работать, на примере Phantom. У Phantom есть мобильное приложение и плагин для Google Chrome. Попробуем создать новый аккаунт при помощи плагина Google Chrome:

Web3 для JavaScript-разработчиков на примере Solana - 3

Что происходит в этот момент? В коде это будет выглядеть так:

import { Keypair } from '@solana/web3.js';

const newWallet = Keypair.generate();

Мы вызываем у класса Keypair, который импортируем из библиотеки Web3, метод generate(). Мы создали криптокошелек в Solana, и в newWallet записалась пара ключей: приватный и публичный. Сейчас посмотрим подробнее, для чего работает каждый ключ.

Давайте выведем в консоль и посмотрим, что же там находится.

console.log('Созданный кошелек:', newWallet);

Видим следующее:

Созданный кошелек: Keypair {
  _keypair: {
    publicKey: Uint8Array(32) [
      2, 87, 19, 63, 250, 248, 72 ... 8, 189, 48, 78, 150, 70
    ],

    secretKey: Uint8Array(64) [
      215, 145, 82, 72, 92, 122, 3 ... 8, 189, 48, 78, 150, 70
    ]
  }
}

// ключи в виде массива байтов

Там находится объект Keypair, и есть два ключа: публичный и секретный. Второй я привык называть приватным.

Подробно разберем каждый из ключей. Скажу только, что публичный ключ и есть адрес вашего Solana-кошелька. Ключи в виде массива байтов.

Возможно, вы скажете: «Подожди, ранее я уже создавал криптокошельки, и они выглядели по-другому. Обычно мой публичный ключ был представлен строкой в виде хешкода, а приватный ключ — массивом строк». Верно: это другое представление ключей. Давайте разберем, как мы можем преобразовать ключи, которые только что создали  в тот вид, к которому мы привыкли. 

Публичный ключ

Напомню: это адрес вашего кошелька. Когда вы хотите, чтобы кто-то перевел вам криптовалюту, то даете ему свой публичный ключ. 

Выведем в консоль публичный ключ, используя метод to.String():

console.log('Адрес кошелька:', newWallet.publicKey.toString());

// Адрес кошелька: 5g9RDPLPjHv8R3DvTNtdevDkNErNe4RKLby25bzBs6PT

Получаем адрес кошелька, это его стандартный вид.
Теперь давайте посмотрим, как мы можем получить seed-фразу для приватного ключа, к которой мы привыкли.

Приватный ключ

Мнемоническая фраза — это удобочитаемое представление приватного ключа, которое кодирует его в виде слов. 

Преобразуем приватный ключ в мнемоническую фразу:

import * as bip39 from 'bip39';

const mnemonic = bip39.entropyToMnemonic(secretKey.toString('hex'));

Здесь вам понадобится библиотека bip39. Приватный ключ отмечен как secretKey. В переменной mnemonic находится наш приватный ключ, но теперь он в виде массива слов. Давайте выведем в консоль:

console.log('Мнемоническая фраза:', mnemonic);

Мнемоническая фраза: seek thank flush cactus fork digital gentle car rebuild modify pink travel buzz save time short turn sound next ball danger measure design laptop

Это работает в обе стороны.

Напоследок добавлю: приватный ключ содержит в себе и публичный ключ.

Операции с кошельком

Все операции с кошельком мы можем поделить на два типа:

  • Те, для которых требуется подпись приватным ключом.

  • Те, где нам она не требуется.

Разберем каждый тип операции.

Операции по публичному ключу

Это операции, для которых нужно знать лишь адрес вашего Solana-кошелька. Среди них:

  • Баланс кошелька.

  • История транзакций.

  • Детали транзакций.

  • Наличие NFT.

  • Наличие SPL-токенов.

Баланс кошелька по публичному ключу

Используем объект connection и вызываем у него метод getBalance(). 

const balance = await connection.getBalance(publicKey);

В Solana есть сервис-эксплорер Solscan. Подобные сервисы есть в разных блокчейн-сетях, и все они содержат в названии слово «scan». Solscan — веб-интерфейс, который позволяет нам отслеживать активность в сети Solana.

Web3 для JavaScript-разработчиков на примере Solana - 4

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

Совет.  Если вы кому-либо предоставляете свой публичный ключ (адрес кошелька) Solana, то человек может узнать все перечисленное выше. Поэтому иногда имеет смысл использовать новый чистый кошелек как прокси-кошелек, и уже с него переводить на свой основной кошелек.

У Solana относительно недорогие комиссии за транзакции. Поэтому, как правило, уплата дополнительных комиссий не проблема. 

Операции с приватным ключом

Это операции, для которых требуется подпись приватным ключом. Среди них:

  • Подпись транзакций.

  • Подпись сообщений.

  • Создание SPL-токенов.

  • Создание NFT.

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

Транзакции

Сначала немного теории. Транзакция в Solana — это операция, отправляемая в сеть блокчейна, которая содержит инструкции для выполнения различных действий, таких как перевод SOL, SPL, NFT-токенов, взаимодействие с программами (смарт-контрактами), создание аккаунтов или выполнение других операций.

Транзакция подписывается одним или несколькими аккаунтами, чтобы подтвердить ее подлинность, и включает информацию о комиссионных (fees) для валидаторов, которые обрабатывают ее.

Подробно разберем высокоуровневую схему работы транзакций.

Web3 для JavaScript-разработчиков на примере Solana - 5

Структура транзакции:

  • Accounts — список публичных ключей, которые участвуют в транзакции.

  • Instructions — инструкции, которые необходимо выполнить (например, перевод

  • токенов).

  • Recent Blockhash — недавний хеш блока.

  • Signatures — подписи участников транзакции.

Приведу пример. Допустим, мы хотим отправить Solana другому участнику. Создаем транзакцию, в которой участвуют два публичных ключа: публичный ключ отправителя  и публичный ключ получателя. Подписей может быть как одна, так и несколько.

Примеры транзакций:

  • Перевод SOL.

  • Создание NFT.

  • Создание SPL.

  • Перевод SPL-токенов.

  • Перевод NFT-токенов.

Все действия, такие как создание токенов, передача токенов, выполнение смарт-контрактов и изменение состояния сети, происходят через транзакции. Транзакция представляет собой набор инструкций, которые должны быть выполнены в сети.

Транзакции Solana

Самый простой вид транзакции — перевод токенов SOL между счетами. Посмотрим, как это обычно выглядит в криптокошельке:

Web3 для JavaScript-разработчиков на примере Solana - 6

 Слева я вставил адрес кошелька Solana, принадлежащего другому участнику сети Solana, которому я хочу перевести SOL. Это его публичный ключ. Указываю сумму (amount). Справа нажимаю Send для отправки.

Затем мы видим как отправляется транзакция, время ожидания может быть разным. Дальше видим результат: либо отправка прошла успешно, либо она оказалась неудачной:

Web3 для JavaScript-разработчиков на примере Solana - 7

Теперь рассмотрим, как это работает под капотом. Порядок действий:

  1. Создание транзакции.

  2. Добавление инструкции в транзакцию.

  3. Указание плательщика комиссии за транзакцию.

  4. Добавление последнего BlockHash в транзакцию.

  5. Подпись транзакции приватным ключом (KeyPair).

  6. Отправка транзакции в сеть Solana.

  7. Подтверждение транзакции.

Обычно комиссию платит инициатор транзакции. Если хотите перевести криптовалюту, то обычно вы и платите комиссию.

Создание транзакции

Создание транзакции делается парой строк:

import { Transaction } from "@solana/web3.js";

const transaction = new Transaction();

Таким же образом создаются транзакции для NFT-переводов, и для мемкоинов, и для многого другого. Сложно не столько создать транзакцию, сколько написать корректную инструкцию для нее. Напишем инструкцию по переводу SOL для нашей транзакции:

import { SystemProgram, LAMPORTS_PER_SOL} from "@solana/web3.js";

const instruction = SystemProgram.transfer({

  fromPubkey: sender.publicKey, // публичный ключ отправителя

  toPubkey: recipient.publicKey, // публичный ключ получателя

  lamports: LAMPORTS_PER_SOL * 0.1, // сумма для перевода в SOL

})

Используем метод transfer(), импортируем из библиотеки Web3.js. Обратите внимание: здесь в метод transfer мы передаем объект в котором есть всего лишь три свойства. LAMPORTS_PER_SOL — константная переменная, это позволяет нам перевести нашу сумму в корректное количество lamports. Мы переводим одну десятую Solana участнику, но не вписываем ее напрямую, а переводим в те самые lamports. Конечно, этот код будет работать одинаково как на фронтенде, так и на бэкенде.

Добавление инструкции в транзакцию

Мы создали транзакцию и написали для нее инструкцию по переводу SOL другому участнику Сети. Теперь добавим нашу инструкцию транзакции, используя метод add():

transaction.add(instruction);

Есть и другой вариант, когда у нас есть несколько инструкций, мы добавляем их в одном массиве:

transaction.add([instructions]);

Так мы можем добавлять целый массив различных инструкций в одну транзакцию. 

Указание плательщика комиссии

В свойство feePayer нашей транзакции указываем публичный ключ того, кто будет платить комиссию за выполнение этой транзакции в сети.

transaction.feePayer = sender.publicKey;

Добавление BlockHash

В транзакции нам необходим недавний валидный BlockHash. Мы берем его из нашего объекта connection, который разобрали ранее, и добавляем этот blockHash в транзакцию.

const blockHash = await connection.getLatestBlockhash();

transaction.recentBlockhash = blockHash.blockhash;

Кратко поговорим о BlockHash. Это уникальный идентификатор блока в блокчейне. Важно: BlockHash может устареть, время на устаревание — примерно две минуты. Это значит, что как только мы добавили BlockHash в транзакцию, у нас есть около двух минут на то, чтобы отправить эту транзакцию. В противном случае получим ошибку отправки транзакции, и нам придется повторять этот процесс, получая новый валидный BlockHash.

Подпись транзакции

Теперь отправитель должен подписать транзакцию. Для того, чтобы подписать транзакцию, мы передаем в нее объект Keypair который содержит в себе пару ключей: публичный и приватный. Этот тот же самый объект Keypair, который мы видели ранее при создании нового Solana кошелька. 

transaction.sign(senderKeypair);

Сериализация транзакции

Поговорим о том, как отправить и подтвердить транзакцию. Обратите внимание, что в Solana есть несколько методов для этого. Порой эти методы дублируют друг друга. Есть метод, который сразу и отправляет, и подтверждает транзакцию. Но мне больше нравится метод, при котором мы отдельно отправляем и отдельно подтверждаем транзакцию. Разберем его.

const serializedTransaction = transaction.serialize(); 
// Сериализуем транзакцию, получаем Buffer

Нам нужно отправить транзакцию в так называемом сыром виде. Для этого нам нужно ее сериализовать. Используем метод serialize(). 

У объекта connection используем метод sendRawTransaction, то есть «отправить сырую транзакцию». Передаем туда единственным аргументом нашу сериализованную транзакцию. 

const signature = connection.sendRawTransaction(serializedTransaction);

Готово — мы отправили сериализованную транзакцию в сеть! Но она не была подтверждена. Мы получаем подпись транзакции, которая записывается в переменную signature, транзакция возможно еще не была обработана, и теперь нужно ее подтвердить. Для этого используем отдельный метод. На этом этапе мы могли бы использовать signature транзакции в Solscan, и мы бы уже увидели, что транзакция существует, но она еще обрабатывается, это выглядит так:

Web3 для JavaScript-разработчиков на примере Solana - 8

Мы видим, что результат был успешным. Теперь давайте подтвердим транзакцию. 

Подтверждение транзакции

Транзакция подтверждается через метод confirmTransaction() у объекта connection.

const confirmation = await connection.confirmTransaction({

  blockhash: blockHash.blockhash,

  lastValidBlockHeight: blockHash.lastValidBlockHeight,

  signature

});

console.log('Подтверждение транзакции:', confirmation);

Здесь мы передаем в аргументы blockHash, о котором говорили ранее, а также нашу подпись транзакции. Так мы узнаем статус транзакции. В переменную confirmation записывается статус нашей транзакции. Транзакция могла как увенчаться успехом (success), так и неудачей (fail). Давайте посмотрим, что находится в переменной confirmation. 

console.log('Подтверждение транзакции:', confirmation);

{

  …

  err: TransactionError | null;

}

Там находится объект, который содержит информацию по данной транзакции, а также поле err. Это поле содержит null, если транзакция успешна, или информацию об ошибке, если транзакция завершилась неудачей. 

Альтернативный метод отправки транзакций

Теперь рассмотрим более популярный метод отправки и подтверждения транзакций. Импортируем метод sendAndConfirmTransaction в библиотеке Web3.

import { sendAndConfirmTransaction } from '@solana/web3.js';

const signature = await sendAndConfirmTransaction(

  connection,

  transaction,

  [senderKeypair],

);

В данном методе одновременно происходит отправка и подтверждение транзакции. Передаем три аргумента: объект connection, который мы ранее использовали,саму транзакцию и в массиве передаем пару ключей отправителя транзакции, который будет платить комиссию за обработку транзакции сетью Solana.
На этом примере вы увидели, что есть разные методы отправки и подтверждения транзакций, которые дублируют друг друга. 

SPL-токены

Meme coins — они же SPL-tokens, разные названия, но означают одно и тоже. На схеме ниже изображены три «типа» мемкоинов в сети Solana:

Web3 для JavaScript-разработчиков на примере Solana - 9

Блок слева — стейблкоины, привязанные к курсу реальных валют. Многие из вас знают про USDT. Он существует в разных блокчейнах. В Solana есть аналог, только называется по-другому — USDC. Существуют и другие стейблкоины в сети Solana: привязанные к курсу евро, франка и т.д..

Переходим к блоку посередине, в нем у нас есть три мемкоина: Jup, Ray и POPCAT. Они имеют определенную стоимость (на скриншоте стоимость на момент создания презентации) и используются в торгах на DEX (децентрализованных биржах). В отличие от стейблкоинов, у них плавающая цена. Существует огромное количество подобных мемкоинов и новые создаются ежедневно.

Теперь блок справа, там находятся три мемкоина: : Capy, solami, HolyJS24. Последний мемкоин я специально создал для доклада. На его примере разберем, как создаются мемкоины в сети Solana. Все мемкоины справа стоят ноль и не торгуются на децентролизованных биржах Solana. 

На первый взгляд, вам может показаться, что это в этих трех блоках находятся разные токены, но с технической точки зрения это все одно и то же, это все SPL-токены/мемкоины в сети Solana. Для работы с SPL-токенами, например для перевода их с кошелька на кошелек нам неважно, есть у них цена или нет. 

К сведению. Токен Jup со схемы выше — это токен Jupiter. Вот как он выглядит в Solscan:

Web3 для JavaScript-разработчиков на примере Solana - 10

Важный вывод: Solana позволяет создать собственную криптовалюту в своей сети, а точнее SPL-токены, они же мем коины. Для этого мы используем Solana Program Library (SPL). Разберем, как мы можем создавать SPL-токены, на примере токена HolyJS24. 

Сначала посмотрим, как это выглядит в криптокошельке:

Web3 для JavaScript-разработчиков на примере Solana - 11

Теперь посмотрим как выглядит HolyJS24 токен на Solscan: ссылка, а ниже скриншот с пояснениями. Это полноценный meme-коин и при работе с ним нет отличий от других SPL-токенов, например, USDC.

Web3 для JavaScript-разработчиков на примере Solana - 12

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

Создание SPL-токена

Минимальный конфиг:

  • Название.

  • Символ.

  • Картинка.

  • Описание.

Создание SPL-токена — ряд последовательных операций. Не нужно воспринимать создание мемкоина как некую единую операцию. 

Этапы создания:

Web3 для JavaScript-разработчиков на примере Solana - 13

Первый этап, мы создаем SPL-токен в сети Solana, это будет токен без монет и без каких-либо метаданных: названия, картинки, символа и т.д. Если мы посмотрим на подобный токен в Solscan, то увидим, что он так и будет так называться: SPL-token.

Второй этап. создаем ассоциированный токен-аккаунт (АТА). Именно в ATA-аккаунте и будут храниться наши мем-коины, а не на нашем основном Solana-кошельке.

Давайте разберемся. Все существующие мемкоины мы не храним в основном кошельке Solana. Мы храним их на асоциированных аккаунтах, и эти аккаунты связаны с нашим основным Solana-кошельком. Перед тем как мы сминтим монеты, мы должны создать ATA-аккаунт и затем мы сможем перевести  туда наши новые  монеты.

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

Четвертый этап. На последнем этапе добавляем метаданные. Повторюсь, нет единой операции для создания мемкоина, все это разные операции.

Для создания мемкоинов в Solana хорошо работает Solana CLI. Поскольку мы говорим о JavaScript, разберем, как работает создание SPL-токенов на JavaScript.

Подготовка для создания SPL-токена

Нам потребуется дополнительные библиотеки для работы и создания SPL-tokens Первая — это официальная библиотека Solana для работы с SPL-токенами, вторая — это библиотека metaplex (очень популярная библиотека, которая применяется для работы с NFT в сети Solana). 

npm install

@solana/spl-token

@metaplex-foundation/mpl-token-metadata

Создаем токен

Выполняем первый этап и получаем его минт-адрес нового токена.

import { createMint } from '@solana/spl-token';

const mint = await createMint(

  connection,

  keypair, // подписант транзакции

  keypair.publicKey, // mintAuthority права эмиссии

  null, // freezeAuthority права заморозки

  9 // количество десятичных знаков

);

Вызываем метод createMint() и передаем туда пять аргументов. Первым аргументом передается объект connection, который мы использовали ранее. Затем два раза передаем keypair. Первый раз мы передаем пару ключей кошелька, что будет подписывать транзакцию. Создание SPL-токена требует уплаты комиссии сети. Мы всегда должны указывать, кто будет платить эту комиссию. Следующий пункт — mintAuthority, то есть права эмиссии. Мы можем передавать эти права от кошелька к кошельку. Допустим, минтить монеты с одного кошелька, а потом передать право владения этим мемкоином на другой кошелек. 

Не буду подробно останавливаться, что значит freezeAuthority. Если кратко, это показывает, может ли владелец этого мемкоина «замораживать» монеты. Заморозка означает, что ни один кошелек, владеющий данным мем-коином, не сможет его перевести. Последним аргументом (9) идет количество десятичных знаков. Количество десятичных знаков для мемкоина Solana не фиксировано, и мы можем выбирать это значение по своему усмотрению. 

Выведем минт-адреса нового SPL-токена в консоль

После создания SPL-токена получаем его mint-адрес: ATkXuvSZ5PgeTsnJSoiBBLyCVuZVvandSoEqHbtQ3YsN. Можем проверить, что он уже существует в Solscan. Это минт-аккаунт нашего токена. В дальнейшем, если указано mint, то это будет означать именно этот адрес.

Перевод SPL-токенов

Посмотрим, как происходит перевод SPL-токенов в кошельке Phantom:

Web3 для JavaScript-разработчиков на примере Solana - 14

Для пользователя нет никакой разницы, он одинаково переводит как  SOL, так и мемкоины. Но под капотом это работает по-разному: мы не переводим мемкоин напрямую на Solana-кошелек, а переводим его на асоциированный аккаунт (ATA) и если этого аккаунта нет, то создаем его.

Associated Token Account (ATA) — это тип аккаунта, созданный для работы с SPL-токенами. ATA автоматически связывается с основным кошельком пользователя для хранения токенов определённого типа. То есть это отдельный аккаунт для каждого мемкоина. 

Посмотрим на схему:

Web3 для JavaScript-разработчиков на примере Solana - 15

Например, есть Solana-кошелек, и у него на балансе три мемкоина: USDC, Jup и HolyJS_24. У каждого из мемкоинов есть АТА-аккаунт. Если у пользователя нет мемкоина на балансе и мы хотим перевести ему его, также у него может не быть АТА-аккаунта для данного мем-коина, то нужно ему создать этот аккаунт перед переводом. Теория по АТА-аккаунтам — ключевая вещь для понимания работы и создания мемкоинов в сети Solana. 

Теперь напишем код:

import { getOrCreateAssociatedTokenAccount } from '@solana/spl-token';

const tokenAccount = await getOrCreateAssociatedTokenAccount(

  connection,

  payer,

  mint,

  payer.publicKey

);

Мы используем всего один метод, который импортируем из библиотеки '@solana/spl-token'. В аргументах: передаем payer, то есть keypair кошелька, который будет платить комиссию сети. Затем передаем mint адрес мем-токена, что мы ранее создали, а также публичный ключ последним аргументом. 

Выведем в консоль только что созданный ATA-аккаунт, получим хешкод-строку. 

console.log(Создан аккаунт токенов: ${tokenAccount.address.toBase58()});

// Создан аккаунт токенов: 9zqkeThZ7VQbXmd9LfJg1xJD2XbPPUh4m9YeHgXrSV2k

Мы можем увидеть, как выглядит этот Token account на Solscan: https://solscan.io/account/9zqkeThZ7VQbXmd9LfJg1xJD2XbPPUh4m9YeHgXrSV2k

Этот аккаунт связан с основным криптокошельком Solana, и именно там будут храниться монеты нашего нового мем-коина.

Теперь мы готовы сминтить наши монеты и перевести их на наш АТА-аккаунт.

Минт токенов

Используем метод mintTo, который также импортируем из библиотеки spl-token:

import { mintTo } from '@solana/spl-token';

  await mintTo(
  connection,
  payer,
  mint, // mint адрес токена, что мы ранее создали
  tokenAccount.address, // ATA аккаунт, что мы ранее создали
  payer.publicKey,
  10000000000000 // 10,000.00 токенов с учетом 9 десятичных знаков
);

Разберем код выше. Есть поле mint — адрес ранее созданного токена, АТА-аккаунт, который мы создали ранее и куда мы будем переводить монеты. Обратите внимание: одним из аргументов передается количество монет, в данном случае 10 000, с учетом девяти десятичных знаков. 

После успешного минта у нашего токена появятся монеты, и мы сможем с ними работать, например, их переводить.

Метаданные SPL-токена

На данный момент наш созданный мемкоин не содержит метаданных. В нашем примере мы добавим название, символ, картинку и описание для нашего мемкоина.
Создадим конфиг с метаданными:

const tokenMetadata = {
  name: 'HolyJS 2024',
  symbol: 'HOLYJS24',
  uri: 'https://raw.githubusercontent.com/…/spl-holyjs-24-example.json',

  sellerFeeBasisPoints: 0,
  creators: null,
  collection: null,
  uses: null,
};

Сначала обсудим конфиг с метаданными. Нас интересуют первые три поля. С первыми двумя все понятно, но что за uri? А в uri мы записываем ссылку на внешнее хранилище. Это одна из важнейших следующих тем.

У нашего мемкоина есть метаданные. Только часть данных хранится в блокчейне, а часть хранится всегда на внешнем хранилище. Это внешнее хранилище может быть как централизованным, так и децентрализованным. 

Uri

В нашем примере в uri содержится URL на GitHub. Перейдем по ссылке на гитхаб и посмотрим, что там находится.

{
"name": "HolyJS 2024",
"symbol": "HOLYJS24",
"description": "Demo SPL-token for HolyJS Autumn 2024",
"image": "https://raw.githubusercontent.com/...headsholyjs-24-logo.jpg"
}

JSON находится на внешнем хранилище, и он необходим для нашего мемкоина. Есть имя, символ, описание и картинка. Обратите внимание: сам по себе JSON находится во внешнем хранилище, и он указывает на картинку, которая в свою очередь также находится на внешнем хранилище. Мы подробно разберем, где находится картинка, в разделе про NFT. В данном случае картинка также хранится на гитхаб-аккаунте. 

Часть метаданных SPL токена хранится в самом блокчейне, а часть — на внешнем хранилище.

Создание транзакции для отправки метаданных в сеть

Создадим транзакцию, добавим в нее инструкцию с нашими метаданными SPL-токена и отправим в сеть. До этого мы рассмотрели, как создать конфиг с метаданными, но теперь нам нужно отправить в сеть, и сеть добавит эти метаданные нашему токену. Для этого нам потребуется транзакция.

import { Transaction } from '@solana/web3.js';

const transaction = new Transaction();

Все то же самое — транзакция создается в одну строчку. Но вот инструкция будет отличаться. Вот так выглядит инструкция для отправки метаданных в сеть: 

import { createCreateMetadataAccountV3Instruction } from '@metaplex-foundation/mpl-token-metadata';

const createMetadataTx = createCreateMetadataAccountV3Instruction(

… другие данные для инструкции

  {
  createMetadataAccountArgsV3: {
    collectionDetails: null,
    data: tokenMetadata, // tokenMetadata, что мы ранее создали
    isMutable: true,
    },
  },
);

Метод называется createCreateMetadataAccountV3Instruction(). Здесь нас интересует поле data, мы указываем в нем тот JSON в конфиге, о котором мы ранее говорили. Таким образом мы отправляем в транзакции наши метаданные, и они добавляются к нашему токену.

Добавим нашу инструкцию в транзакцию и отправим ее в сеть:

import { sendAndConfirmTransaction } from '@solana/web3.js';

const transactionSignature = await sendAndConfirmTransaction(
  connection,
  transaction,
  [payer],
);

Подпись транзакции

На выходе получаем подпись транзакции, которая добавила метаданные нашему SPL-токену. Выведем данные в консоль:

console.log('Метаданные токена созданы.', transactionSignature);

// Метаданные токена созданы.

3jdi82repZdEWF47Bwmgu5GtnPNn5qGzqu2BgnD4kUyij9i1CU8Y72RpfGib8ZPu3r1ki

WMcqR8HHUk95oAfQk4o

Вот как это выглядит в Solscan:

Web3 для JavaScript-разработчиков на примере Solana - 16

Метаданные SPL-токена в сети

Посмотрим, как выглядят сейчас наши метаданные в самом блокчейне. “Это страница нашего токена на Solscan:

Web3 для JavaScript-разработчиков на примере Solana - 17

Нас интересует объект data — это то, что мы и пытались загружать. А Uri по-прежнему указывает на внешнее хранилище. Можем удалить данные с внешнего хранилища, и мемкоин потеряет к ним доступ. Это непосредственно данные, которые мы видим в блокчейне.

Что в итоге:

  1. Создали токен.

  2. Создали АТА аккаунт.

  3. Сминтили монеты и отправили их на АТА-аккаунт.

  4. Создали метаданные для токена и отправили их в сеть с помощью транзакции.

Теперь посмотрим, как мы можем отправлять SPL-токен другим участникам сети.

Отправка SPL-токенов

import { Transaction } from '@solana/web3.js';

const transaction = new Transaction();

const transactionInstructions = [];

Создаем транзакцию. В данном случае у нас будет массив инструкций, которые мы добавим. План по отправке SPL-токенов:

  1. Получаем адрес ATA отправителя.

  2. Получаем адрес АТА получателя. Если его нет, то создаем АТА-аккаунт получателю.

  3. Добавляем инструкции в транзакцию, подписываем, отправляем и т. д.

Мы работаем для отправки мемкоинов всегда с АТА-аккаунтами, а не напрямую с Solana-кошельками. 

Получаем адрес АТА-аккаунта

Мы передаем адрес mint, это адрес нашего мемкоина и публичный ключ.

import { getAssociatedTokenAddress } from '@solana/spl-token';

const recipientTokenAccount = await getAssociatedTokenAddress(
  mint,
  payer.publicKey
  );

// в tokenAccount публичный ключ ATA, может быть активным или неактивным

Здесь в переменной recipientTokenAccount получаем публичный ключ АТА. Далее нам нужно проверить, активен он или неактивен. Если он неактивен, нам нужно его активировать, или по-другому говоря — создать. Мы проверяем через метод getAccountInfo() наш АТА-аккаунт. 

const tokenAccountData = await this.connection.getAccountInfo(tokenAccount);

// в tokenAccountData находится объект с данными ATA или null, если данных не

существует

То есть мы определяем, есть ли у пользователя АТА-аккаунт или нужно его создать. 

Проверяем ATA и добавляем инструкцию

Делаем простую проверку:

import {createAssociatedTokenAccountInstruction} from '@solana/spl-token';

if (tokenAccountData !== null) {

  transactionInstructions.push(

    createAssociatedTokenAccountInstruction(

      payer.publicKey, // плательщик комиссии за создание ATA

      recipientTokenAccount, // адрес ATA что мы ранее получили

      recipient.publicKey, // владелец нового ATA аккаунта

      mint // mint адрес токена для ATA

    )

  );

}

Если ATA-аккаунта нет, то пишем инструкцию, которая создает его. Будет дополнительная уплата комиссии. Используем метод для создания такой инструкции. Есть простая проверка. У нас есть массив transactionInstructions, и мы туда добавляем нашу инструкцию. 

Заметка. Теперь в нашем массиве инструкций содержится инструкция для создания АТА-аккаунта, если его нет у пользователя. Теперь создадим инструкцию для перевода SPL-токенов.

Создаем инструкцию на перевод SPL-токенов

Инструкция выглядит так:

import { createTransferInstruction } from '@solana/spl-token';

transactionInstructions.push(

  createTransferInstruction(

    senderTokenAccount, // адрес ATA отправителя

    recipientTokenAccount, // адрес ATA получателя

    fromPubkey, // ключ отправителя

    amount, // сумма для перевода

  )

);

createTransferInstruction — та же самая инструкция, что мы видели ранее. Точно так же в этой инструкции мы указываем АТА-аккаунты, публичный ключ того, кто будет оплачивать комиссию, и сумму. Вот таким образом выглядит инструкция для перевода мемкоина другому участнику сети. 

Отправка SPL-транзакции

Подписываем транзакцию, добавляем lastValidBlockHash, отправляем и подтверждаем так же, как и в регулярных SOL-транзакциях.

Примеры использования SPL-токенов

Сферы, где обычно используются мемкоины:

  • Promotion. Продвижение товара или услуги, вознаграждение в виде токенов, за выполнение пользователями определенных действий. Лучший пример — приложения-кликеры. Нам это ничего не стоит, и мы можем минтить миллиарды мемкоинов и дать обещание, что возможно, они потом будут что-то стоить (но может и нет).

  • Создание долгоиграющей монеты, которая будет торговаться на биржах.

  • Скам.

Заметка. Допустим, я создал мемкоин и теперь хочу продавать его на Solana.  Может быть, это сложно сделать или для этого нужны смарт-контракты? На самом деле — нет.

В одной транзакции мы можем совершать обмен SPL-токенов на SOL, или на другие SPL-токены, или обмен в обратную сторону. Например, мы можем продавать пользователям наш SPL токен за SOL по выбранному нами курсу. Данный обмен можно совершить в одной транзакции. По этому принципу и работает популярный скам: создаются веб-приложения, мемкоины, и пользователи пытаются его продать за реальную Solana. Я не буду показывать транзакцию для свопа, но такое есть.

NFT-токены

Теперь поговорим про NFT — токены с картинками, которые порой дорого стоят. Пример: Solana выросла в цене, есть NFT и за 6 тысяч долларов. 

Web3 для JavaScript-разработчиков на примере Solana - 18

Один из моих лидеров —кто-то пытается продать NFT за 426 SOL, что было равно 90 тысячам долларов.  

Web3 для JavaScript-разработчиков на примере Solana - 19

Поговорим о том, как работает NFT. Это токен, который содержит дополнительные метаданные, например ссылку на внешнее хранилище с картинкой. В целом это похоже на то, что есть в SPL-токенах. 

Разберем одну важную деталь. Внешнее хранилище может быть как централизованным, так и децентрализованным (Arweave). Загрузка в децентрализованное хранилище может быть платной. Разберем пример децентрализованного хранилища на примере блокчейна Arweave.

Arweave

Arweave — это децентрализованная платформа для хранения данных, которая предлагает долговечное хранение с единовременной оплатой. Отдельный блокчейн, никак не связан с Solana. Основная цель Arweave — предоставление децентрализованного и постоянного хранилища данных. 

Как это выглядит на практике? У этого блокчейна есть своя криптовалюта, которая называется AR. Если кратко: мы можем создавать транзакцию, где указываем путь к изображению и загружаем это изображение в блокчейн. Комиссией выступает криптовалюта AR. Таким образом мы можем загружать JSON и картинки на децентрализованную биржу. 

Если мы говорим о платных NFT, то конечно же, картинки для них не должны храниться в централизованном хранилище. Не должно быть возможности удалить сами картинки. 

Arweave — это отдельный блокчейн, не входящий в сеть Solana. 

Про NFT

NFT-токены пересылаются также в транзакциях, но инструкции для транзакций отличаются от обычных транзакций SOL или SPL-токенов.

Существуют сжатые Solana NFT (compressed NFT) и обычные Solana NFT. Они создаются и пересылаются по-разному. Сжатые NFT стоят дешевле при их создании.

Как создать NFT

Порядок действий: 

  1. Создаем NFT-коллекцию (например, «Супергерои»). Создание коллекции стоит SOL.

  2. Создаем любое количество NFT в коллекции (например, NFT с разными супергероями). Используем любые картинки и названия для NFT, также можно добавить дополнительные метаданные, например ссылку на веб-сайт. 

  3. При создании NFT указываем адрес коллекции, таким образом мы привяжем NFT к конкретной коллекции.

Пример NFT-коллекции, которую я создал специально для моего доклада.

Web3 для JavaScript-разработчиков на примере Solana - 20

Здесь нас интересует адрес NFT-коллекции. Укажем его, когда будем минтить наши NFT. Как вы видите, у самой коллекции тоже есть картинка. 

А  вот это уже пример самих NFT. Не перепутайте!

Web3 для JavaScript-разработчиков на примере Solana - 21

Видим страницу с нашими NFT, которые называются HolyJS_24. Можем задать любое название для NFT, создать любое количество. Можем добавить ссылку на веб-сайт. Повторюсь, что может быть любая картинка для NFT (даже неприличная).

Метаданные NFT

Вот как выглядят метаданные:

Web3 для JavaScript-разработчиков на примере Solana - 22

Есть объект data, очень похоже на то, что мы видели в SPL-токенах: name, symbol, uri. В данном случае uri указывает не на мой гитхаб, а на Arweave. Внешним хранилищем служит децентрализованная платформа Arweave. Наш JSON теперь хранится на децентрализованной платформе. На Arweave мы будем загружать наши метаданные для NFT, также можно загрузить картинки для NFT. 

План создания NFT-коллекции

Порядок действий:

  1. Создаем конфиг с метаданными коллекции.

  2. Загружаем картинку в хранилище и указываем ссылку на нее в метаданных.

  3. Загружаем конфиг с метаданными на Arweave.

  4. Создаем коллекцию, добавляя ссылку на метаданные, что хранятся на Arweave.

Установка библиотек для работы с NFT

Нам понадобятся дополнительные библиотеки от Metaplex: 

npm install @metaplex-foundation/js (v0.20.1) @metaplex-foundation/mpl-bubblegum (v0.6.2)

Настройка Metaplex

Создаем объект Metaplex, с которым будем работать. 

import { Metaplex, keypairIdentity } from '@metaplex-foundation/js';
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(yourWalletKeypair));

Подвязываем к нему наши ключи. Нас интересует приватный ключ, чтобы знать, кому принадлежит все, что будем создавать.

Создание метаданных для NFT-коллекции

Здесь пример конфига для метаданных для создания NFT-коллекции.

Web3 для JavaScript-разработчиков на примере Solana - 23

В целом все стандартно. 

Загрузка метаданных NFT-коллекции

У нас есть конфиг, теперь нужно его загрузить. 

const metadata = await metaplex.nfts().uploadMetadata(

  {

  ...collectionConfig,

  payer: yourWalletKeyPair.publicKey,

  },

  {

  commitment: 'finalized',

  },

);

Получаем ссылку на эти загруженные метаданные, которые представляют собой обычный JSON.

Метаданные после загрузки на Arweave

Получаем ссылку. Наши данные находятся в блокчейне, и они выглядят вот так:

Web3 для JavaScript-разработчиков на примере Solana - 24

Это JSON, и именно его мы указываем при создании коллекции.

Создание NFT-коллекции

Этот код создает NFT-коллекцию:

const collection = await metaplex.nfts().create({

  uri: metadata.uri,

  name: metadata.name,

  sellerFeeBasisPoints: 100,

  isCollection: true,

  updateAuthority: yourWalletKeypair,

});

Как вы видите, здесь в метаданных есть поле — uri. Указываем ссылку на наши метаданные, которые хранятся на блокчейне Arweave.

Мы создали NFT-коллекцию, пришло время создать в этой коллекции NFT.

Создание NFT

Создание NFT — непростая задача, как и их отправка. Не буду подробно объяснять, как это работает. Вот пример инструкции, это только часть кода, для транзакции по созданию сжатого NFT:

const mintWithCollectionIx = createMintToCollectionV1Instruction(

  {

    payer: payer.publicKey,

    merkleTree: treeAddress,

    treeAuthority,

    treeDelegate: payer.publicKey,

    leafOwner: destination,

    leafDelegate: destination,

    collectionAuthority: payer.publicKey,

    collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,

    collectionMint: collectionDetails.mint,

    collectionMetadata: collectionDetails.metadata,

    editionAccount: collectionDetails.masterEditionAccount,

    compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,

    logWrapper: SPL_NOOP_PROGRAM_ID,

    bubblegumSigner,

    tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,

  },

  {

    metadataArgs: Object.assign(nftMetadata, {

    collection: { key: collectionDetails.mint, verified: false },

    }),

  },

);

Здесь идут сложные инструкции. Но идея та же самая.

Создание сжатого NFT через Helius

npm install helius-sdk

Ранее мы уже говорили про Helius. Он предоставляет доступ к RPC-узлам. Также у них есть крутая SDK-библиотека, которая упростит процесс создания NFT. Вот так создается сжатый NFT через Helius:

const helius = new Helius(heliusApiKey);

const nft = await helius.mintCompressedNft({

name: 'HolyJS_24',

symbol: 'HOLYJS_24',

owner: 'yourWalletPublicKey',

collection: 'yourCollectionPublicKey',

description: 'Demo NFT for HolyJS 2024 Autumn',

sellerFeeBasisPoints: 100,

externalUrl: 'https://holyjs.ru/',

imageUrl: 'https://url-to-image/nft-image.jpg',

});

Нас интересует здесь поле collection — адрес нашей ранее созданной коллекции. Наши NFT привяжутся к нашей коллекции. Все довольно просто.

Ответ будет выглядеть так:

nft: {

  jsonrpc: '2.0',

  id: 'helius-test',

  result: {

    signature: 'wymqxn6zsjTBq...9QL5QqLKWaM',

    minted: true,

    assetId: '7TVtmGoz4HQ...KAvNXWB8P' // ваш NFT-адрес

    }

}

Видим, что есть поле minted:true, то есть у нас получилось сминтить. Поле assetId — адрес нашего NFT. То есть у каждой коллекции есть адрес и у каждого NFT.

Итак, мы научились создавать коллекцию для сжатого NFT, а также сами NFT двумя способами: нативным и с помощью Helius. Остается только переслать NFT. Давайте быстро обсудим отправку. Все так же работает через транзакции. У нас есть целая коллекция NFT, и мы хотим их отправить. Давайте перешлем участнику.

Вот так это выглядит в криптокошельке:

Web3 для JavaScript-разработчиков на примере Solana - 25

А вот так выглядит инструкция для отправки compressed NFT:

const transactionInstruction = createTransferInstruction(

  {
    merkleTree: treeAddress,
    treeAuthority,
    leafOwner,
    leafDelegate,
    newLeafOwner,
    logWrapper: SPL_NOOP_PROGRAM_ID,
    compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
    anchorRemainingAccounts: proofPathAsAccounts,
  },

{

    creatorHash: assets.compression.creator_hash,
    dataHash: assets.compression.data_hash,
    index: assets.compression.leaf_id,
    nonce: assets.compression.leaf_id,
    root: assetsProof.root,
  },
  PROGRAM_ID,
);

Отправка сжатого NFT

Мы написали инструкцию для отправки, создали транзакцию, добавили blockHash, указали плательщика комиссии:

const transaction = new Transaction().add(transactionInstruction);
const blockHash = await connection.getLatestBlockhash();

transaction.feePayer = yourWalletKeypair.publicKey;
transaction.recentBlockhash = blockHash.blockhash;
transaction.lastValidBlockHeight = blockHash.lastValidBlockHeight;

Отправили транзакцию в сеть и подтвердили.

const transactionSignature = await sendAndConfirmTransaction(
  connection,
  transaction,
  [yourWalletKeypair],
);

Транзакции работают одинаково — отличаются только инструкции. Все самое сложное позади, теперь давайте порассуждаем о последних двух разделах.

Варианты использования

Как написать свой криптокошелек Solana, Web3-бот в телеграме, мобильное Web3-приложение, Web3-игру? Для этого нужно знать, как работают кошельки и транзакции в Web3. Все это пользовательские интерфейсы, а когда и как все это работает под капотом, мы разобрали. 

Куда двигаться дальше?

Авторизация в веб-приложениях по криптокошелькам. Многие веб-приложения в сфере блокчейна, в частности Solana, не имеют никаких других способов авторизации кроме как по криптокошелькам. 

Смарт-контракты. В Solana они пишутся на Rust.

Сложные транзакции. Этого мы немного коснулись в докладе. Swap-транзакции. Кошельки в виде доменов. Memo-поле — строка, которую мы можем добавить в транзакцию, например, там можно указать номер заказа. 

Криптоботы для DEX. MEV-боты, Jito. Можно ставить более высокий приоритет для наших транзакций и манипулировать скоростью обработки транзакций.

Solana Crendel-bot

Это мой опенсорс-проект — бот на Node.js. Вот что он делает:

  • Мониторит новые SPL-токены на Raydium Dex.

  • Отфильтровывает токены.

  • Покупает одним из первых.

  • Продает через заданное время.

  • Поддерживает кастомные фильтры.

  • Удобные настройки.

  • Сложные транзакции с динамической ценой на покупку/продажу токенов.

Можете зайти и посмотреть, как торгует бот на реальной децентрализованной бирже в автоматическом режиме.

На этом все, спасибо за внимание!

Заключение от организаторов

7-8 апреля пройдёт новая HolyJS: можно поучаствовать в Москве или онлайн. Новых докладов конкретно про Solana не обещаем, а вот о всём том, что вообще интересует JS-разработчиков — вполне. На сайте уже доступны описания некоторых докладов.

Автор: phillennium

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js