
Всё, что нужно знать о сжатии в Solana
Содержание
- О чём эта статья?
- Распространённые заблуждения
- Сжатие в Solana аналогично традиционному сжатию
- Хранить сжатые данные вне блокчейна рискованно: это создаёт уязвимости
- Я могу потерять параллельное дерево Меркла, если индексатор или RPC-провайдер, который хранит моё дерево, перестанет работать
- Параллельные деревья Меркла могут обрабатывать обновления параллельно
- Дерево и коллекция — одно и то же
- Что такое сжатие состояния?
- Состояние и реестр
- Что такое сжатые NFT?
- Чтение метаданных сжатых NFT с помощью DAS API
- Размер и стоимость создания параллельного дерева Меркла
- Расчёт размера
- Расчёт стоимости
- Создание конкурентного дерева Меркла
- Полный код
- Разбор кода
- Создание конкурентного дерева Меркла с помощью Umi
- Выпуск cNFT через прямое взаимодействие с Bubblegum
- Создание коллекции
- Выпуск NFT в нашу коллекцию
- Полный код выпуска в коллекцию
- Разбор процесса выпуска
- Выпуск cNFT с помощью Umi
- Выпуск без коллекции
- Выпуск в коллекцию
- Минтинг cNFT с Helius
- Передача cNFT
- Передача через прямое взаимодействие с Bubblegum
- Полный код
- Разбор кода
- Передача с помощью Umi
- Заключение
- Дополнительные ресурсы
О чём эта статья?
Поверили бы вы, если бы я сказал, что прямо сейчас можно выпустить миллион NFT менее чем за 150 долларов США? Абсурд! В зависимости от блокчейна выпуск такого количества NFT обошёлся бы более чем в миллион долларов! Или нет?
Сжатие состояния — это новый примитив, который использует деревья Меркла и реестр Solana, чтобы значительно снизить расходы на хранение данных, сохраняя безопасность и децентрализацию базового уровня Solana. Эта статья представляет собой подробное руководство по сжатию в Solana. В ней рассмотрено всё: от распространённых заблуждений до передачи сжатых NFT. Если вы хотите разобраться в сжатии состояния и научиться получать, выпускать или передавать сжатые NFT, то для начала вам понадобится только эта статья.
В этой статье предполагается, что вы уже прочитали нашу статью Основы криптографических инструментов: хеш-функции и деревья Меркла. Важно прочитать её заранее, поскольку мы предполагаем, что вы знакомы с деревьями Меркла. Здесь мы также подробнее рассмотрим параллельные деревья Меркла, их размеры и создание.
В этой статье используются Bubblegum SDK и Umi, чтобы показать разные подходы к созданию параллельных деревьев Меркла, а также к выпуску и передаче сжатых NFT. Полезно знать оба инструмента, поскольку вы, скорее всего, встретите каждый из них в разных кодовых базах. Bubblegum SDK включён специально для обучения: его рабочий процесс делает базовые механизмы более прозрачными. Umi предлагает более лаконичный рабочий процесс и упрощает эти операции.
Распространённые заблуждения
Прежде чем разбираться в сжатии состояния и тонкостях сжатых NFT, нужно прояснить несколько моментов:
Сжатие в Solana аналогично традиционному сжатию
Это неверно. Традиционно сжатие используют для уменьшения размера файлов и данных. Его основная цель — хранить или передавать данные с помощью меньшего количества битов, чем в исходном файле. Существует два основных типа алгоритмов сжатия:
- Сжатие без потерь, при котором исходные данные можно восстановить из сжатых
- Сжатие с потерями, при котором «менее важная» информация удаляется для уменьшения размера файла
Сжатый NFT — это не NFT, обработанный алгоритмом сжатия без потерь или с потерями для уменьшения объёма данных. Речь также не идёт о снижении качества или размера изображения, музыки либо метаданных, связанных с NFT. В контексте Solana это понятие имеет совершенно иной смысл. Оно связано с оптимизацией хранения информации об NFT в реестре блокчейна. На уровне аккаунтов мы сжимаем данные в реестр, объединяя несколько аккаунтов — в данном случае NFT — в один корень Меркла, который хранится в состоянии. Этот процесс значительно снижает расходы на хранение, сохраняя возможность проверки.
Хранить сжатые данные вне блокчейна рискованно: это создаёт уязвимости
Это неверно: данные можно безопасно хранить вне блокчейна, если хешировать их и записывать корень Меркла в блокчейн. Строго говоря, сжатые NFT не хранятся вне блокчейна. Данные по-прежнему находятся в блокчейне, поскольку всё, что можно повторно получить из реестра, считается ончейн-данными. Разница в том, что экономические стимулы побуждают валидаторов хранить состояние аккаунтов в памяти, тогда как доступ к реестру обеспечивают архивные ноды. Сжатие состояния объединяет оба подхода и позволяет проверять данные реестра через состояние аккаунта, сохраняя безопасность и децентрализацию самой Solana. В другом разделе мы рассмотрим, что такое реестр и почему он безопасен.
Я могу потерять параллельное дерево Меркла, если индексатор или RPC-провайдер, который хранит моё дерево, перестанет работать
Вы не потеряете дерево: любой пользователь с доступом к реестру может полностью восстановить его, воспроизведя историю дерева.
Параллельные деревья Меркла могут обрабатывать обновления параллельно
Распространено заблуждение, что слово «параллельное» означает возможность одновременно вносить несколько обновлений в ончейн-дерево Меркла. Хотя параллельные деревья Меркла допускают замену нескольких листьев в одном блоке, валидаторы обрабатывают эти обновления последовательно. Когда валидатор получает пакет транзакций, затрагивающих ончейн-параллельное дерево Меркла, он может обработать их в одном слоте. Однако данные в пределах слота не создаются параллельно. Подробнее об этом рассказывается в следующем разделе Что такое сжатие состояния?
Дерево и коллекция — одно и то же
Параллельные деревья Меркла и коллекции — не одно и то же. Одна коллекция может использовать любое количество параллельных деревьев Меркла. Важно понимать, что группировка NFT не зависит от способа их хранения. NFT могут находиться в аккаунтах или быть сжаты в реестр с использованием любого количества деревьев — одного или нескольких. Однако для снижения сложности рекомендуется использовать каждое параллельное дерево Меркла только для одной коллекции.
Что такое сжатие состояния?
Сжатие состояния оптимизирует хранение: для данных реестра создаётся криптографический хеш, который затем сохраняется в аккаунте. Этот подход использует встроенные механизмы безопасности и неизменности реестра и предоставляет надёжную основу для проверки хранящихся в нём данных.
Это экономичное решение для приложений, созданных на Solana. Теперь разработчики могут использовать пространство реестра вместо более дорогого хранилища на основе аккаунтов. Таким образом, сжатие состояния не только гарантирует целостность данных, но и позволяет экономично распределять ресурсы в Solana.
Секрет сжатия состояния в Solana заключается в использовании параллельных деревьев Меркла. Они оптимизированы для быстрой последовательной обработки нескольких транзакций с возможностью актуализировать их доказательства. В традиционных деревьях Меркла каждое обновление делает доказательства дерева недействительными. Параллельные деревья Меркла хранят защищённый журнал последних изменений вместе с корневым хешем и доказательством, необходимым для его получения. Этот журнал хранится в блокчейне, в выделенном для дерева аккаунте. У каждого параллельного дерева Меркла есть максимальный размер буфера. Это значение определяет наибольшее количество изменений, которые можно внести в дерево, пока его корень Меркла остаётся действительным. Иными словами, оно показывает, насколько может «устареть» рассчитанный набор доказательств, прежде чем его потребуется обновить.
Поэтому, когда валидатор получает несколько запросов на обновление ончейн-дерева Меркла в одном слоте, он может использовать журнал изменений дерева как источник достоверных данных. Это позволяет внести в дерево Меркла количество параллельных изменений, не превышающее максимальный размер буфера. Такой подход напрямую не уменьшает объём данных, хранящихся в блокчейне, но повышает эффективность благодаря одновременной обработке нескольких обновлений. Это означает, что система может сохранять целостность «доказательства включения», предоставляемого деревьями Меркла, даже при высокой пропускной способности. Здесь доказательство включения означает возможность подтвердить, что конкретный элемент действительно входит в набор данных, совместно хешированный в корень Меркла.
Это изобретательное сочетание сжатия состояния и параллельных деревьев Меркла предлагает чрезвычайно экономичное решение для приложений на Solana. Чтобы в полной мере оценить влияние этих технологий, важно рассмотреть различия между состоянием Solana и её реестром.
Состояние и реестр
Реестр — это историческая запись всех подписанных клиентами транзакций, выполненных в Solana с момента создания генезис-блока. Это структура данных с последовательным добавлением: после добавления транзакцию нельзя изменить или удалить. Валидаторы проверяют транзакции, добавляемые в реестр. Реестр хранится на нескольких нодах сети для обеспечения отказоустойчивости. Однако копия реестра у валидатора может содержать только новые блоки, чтобы сократить требования к хранилищу, поскольку старые блоки не нужны для проверки будущих.
Состояние представляет собой текущий снимок всех аккаунтов и программ в Solana. Оно изменяемо и обновляется при обработке транзакций. Состояние можно представить как высокооптимизированную базу данных, в которой можно запрашивать балансы токенов, программы и аккаунты.
Вот простой способ различать эти понятия. Предположим, что у Алисы есть 100 SOL и у Боба тоже есть 100 SOL. Алиса отправляет транзакцию, чтобы передать Бобу 10 SOL. После проверки транзакция добавляется в блок, а блок — в реестр. Теперь в реестре есть неизменяемая запись о том, что Алиса отправила Бобу 10 SOL. Одновременно состояние обновляет балансы аккаунтов Алисы и Боба до 90 и 110 SOL соответственно.
Основные различия можно обобщить следующим образом:
- Реестр неизменяем и допускает только добавление данных, тогда как состояние изменяемо и постоянно обновляется
- Реестр содержит историческую запись всех транзакций, а состояние отражает текущий статус всех аккаунтов и программ
- Реестр используется для проверки, а состояние — для выполнения транзакций и запуска программ
Реестр служит неизменяемой исторической записью и позволяет проверять и отслеживать каждую транзакцию. Состояние работает как динамический снимок реестра и обновляется в реальном времени при переводах и выполнении программ. Важно, что и реестр, и состояние подчиняются консенсусу самой сети. Вместе они образуют основу Solana, обеспечивая эффективную работу и сохраняя децентрализованное доверие.
Что такое сжатые NFT?
Сжатые NFT (cNFT) используют сжатие состояния и параллельные деревья Меркла для снижения расходов на хранение. Их метаданные хранятся в реестре, а не в отдельном стандартном аккаунте Solana для каждого NFT. Это снижает расходы на хранение, сохраняя безопасность и неизменность реестра.
Сжатые NFT используют точно такую же схему метаданных, как и их несжатые аналоги. Поэтому NFT и cNFT определяются одинаково.
Основные различия между NFT и cNFT:
- Сжатый NFT можно преобразовать в обычный NFT, но обычный NFT нельзя преобразовать в сжатый
- Сжатые NFT не являются нативными токенами Solana: у них нет аккаунта токена, аккаунта выпуска или метаданных. Однако у них есть стабильный идентификатор — ID актива. После распаковки NFT сохраняет тот же идентификатор. Таким образом, NFT в сжатом состоянии не являются нативными токенами, но при необходимости их можно в них преобразовать
- Один аккаунт параллельного дерева Меркла может содержать миллионы NFT
- Одна коллекция может охватывать несколько аккаунтов деревьев
- Все изменения NFT выполняются через программу Bubblegum
- Для чтения любой информации о сжатом NFT рекомендуется использовать вызов DAS API
Интересно, что для получения информации о сжатом NFT необходимо использовать DAS API. Почему? И что ещё важнее — что это такое?
Чтение метаданных сжатых NFT с помощью DAS API
Нам нужна помощь индексаторов, поскольку метаданные cNFT хранятся в реестре, а не в традиционном аккаунте. Текущее состояние сжатого NFT можно получить, воспроизведя соответствующие транзакции, но такие провайдеры, как Helius, делают это за вас. Разработчики могут использовать API стандарта цифровых активов (DAS) — спецификацию и систему с открытым исходным кодом для получения информации об активе. DAS API поддерживает как сжатые, так и традиционные, то есть несжатые, NFT. Поэтому для обоих типов NFT можно использовать одну и ту же конечную точку.
Сейчас Helius поддерживает следующие методы DAS API:
getAsset— получить конкретный актив по его IDgetAssetBatch— получить несколько активов по их IDgetAssetProof— получить доказательство Меркла для сжатого актива по его IDgetAssetProofBatch— получить доказательства для нескольких активов по их IDgetAssetsByOwner— получить список активов, принадлежащих указанному адресуgetAssetsByAuthority— получить список активов с указанным центром полномочийgetAssetsByCreator— получить список активов, созданных указанным адресомgetAssetsByGroup— получить список активов по ключу и значению группыsearchAssets— найти активы по различным параметрамgetSignaturesForAsset— получить список подписей транзакций, связанных со сжатым активом- Пагинация — поддержка постраничной пагинации и пагинации по ключам для получения более 1000 записей за один раз
Подробнее о каждом методе можно узнать в документации Helius по DAS API. Например, чтобы получить список всех активов, принадлежащих указанному адресу, можно отправить следующий POST-запрос с помощью getAssetsByOwner:
const url = `https://mainnet.helius-rpc.com/?api-key=`
const getAssetsByOwner = async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAssetsByOwner',
params: {
ownerAddress: '86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY',
page: 1, // Starts at 1
limit: 1000
},
}),
});
const { result } = await response.json();
console.log("Assets by Owner: ", result.items);
};
getAssetsByOwner();Получать сжатые активы удобно, но что делать, если мы хотим создать собственные? Прежде чем приступать к выпуску, важно рассчитать размер и стоимость создания параллельного дерева Меркла, в котором будут храниться эти активы.
Размер и стоимость создания параллельного дерева Меркла
Расчёт размера
При создании ончейн-параллельного дерева Меркла есть три важных параметра. Они определяют размер дерева, стоимость его создания и количество параллельных изменений, которые можно внести, пока корень Меркла остаётся действительным:
- Максимальная глубина
- Максимальный размер буфера
- Глубина кроны
Максимальная глубина — это наибольшее количество переходов от любого листа до корня дерева. Каждый лист связан только с одним другим листом, образуя с ним пару для попарного хеширования. Максимальное количество листовых узлов в дереве можно рассчитать по формуле: numberOfNodes = 2 ^ maxDepth. Глубину дерева необходимо задать при создании, поэтому с помощью этой формулы нужно определить минимально возможную максимальную глубину для хранения ваших данных. Например, если вы планируете хранить в дереве около 100 сжатых NFT, будет достаточно значения maxDepth, равного 7, поскольку 2^7 = 128 и 2^6 = 64. Максимальная глубина существенно влияет на стоимость создания ончейн-параллельного дерева Меркла. Эти расходы возникают сразу при создании дерева и растут с увеличением значения maxDepth.
Максимальный размер буфера — это наибольшее количество изменений, которые можно внести в дерево, пока его корень Меркла остаётся действительным. Размер буфера журнала изменений задаётся при создании параллельного дерева Меркла с помощью значения maxBufferSize. Поэтому, когда валидатор получает несколько запросов на изменение дерева в одном слоте, он может использовать журнал изменений и допустить до maxBufferSize изменений, сохраняя действительность корня.
Важно отметить, что для создания нового аккаунта параллельного дерева Меркла существует только определённый набор допустимых пар maxDepth и maxBufferSize. Пакет @solana/spl-account-compression экспортирует константу ALL_DEPTH_SIZE_PAIRS — массив числовых массивов, содержащий все допустимые комбинации. Минимальная комбинация — значение maxDepth, равное 3, и maxBufferSize, равное 8. Максимальная — maxDepth, равное 30, и maxBufferSize, равное 2048.
Глубина кроны — это часть дерева Меркла, которая хранится в аккаунте. Эти кешированные доказательства дополняют доказательства, передаваемые по сети, поскольку на них распространяются ограничения транзакций. Для подтверждения первоначального владения листом при попытке изменить его данные, например при передаче NFT, необходимо использовать полный путь. Чем больше максимальная глубина дерева, тем больше узлов доказательства требуется для проверки. Крона позволяет уменьшить размер доказательства и не использовать для проверки дерева доказательство размером maxDepth.
Глубину кроны можно рассчитать, вычтя желаемый размер доказательства из максимальной глубины. Например, если максимальная глубина равна 14, а желаемый размер доказательства — 4, глубина кроны составит 10. Это означает, что для каждой транзакции обновления потребуется отправить только 4 узла доказательства. Глубина кроны также существенно влияет на стоимость создания ончейн-параллельного дерева Меркла. Эти расходы возникают сразу при создании дерева и растут с увеличением значения canopyDepth. Меньшее значение canopyDepth снижает первоначальную стоимость, но может ограничить компонуемость. Причина в том, что для каждой транзакции обновления потребуется доказательство большего размера, а значит, ограничения на размер транзакции станут строже. Например, если дерево с низким значением canopyDepth используется для сжатых NFT, маркетплейс NFT может поддерживать для вашей коллекции только простые переводы. Для максимальной компонуемости maxDepth - canopyDepth обычно должно быть меньше или равно 10. Это указано в спецификации Tensor для максимальной длины доказательства cNFT в Tensor.
Расчёт стоимости
Размер и стоимость параллельного дерева Меркла можно определить разными способами. Самый простой — использовать калькулятор сжатых NFT и указать количество сжатых NFT, которые планируется хранить в этом дереве:
Сайт показывает оптимальную глубину дерева для хранения желаемого количества активов и предлагает несколько вариантов стоимости в зависимости от компонуемости. Например, на рисунке видно, что создание дерева с высокой компонуемостью для хранения 10 миллионов сжатых NFT обойдётся всего примерно в ~7,67 SOL. Если учесть расходы на транзакции в размере около ~50 SOL для выпуска 10 миллионов NFT, общая стоимость составит примерно ~57,67 SOL.
Разработчики также могут использовать пакет @solana/spl-account-compression, чтобы рассчитать необходимое пространство для дерева заданного размера и стоимость выделения этого пространства в блокчейне. Это можно сделать с помощью следующего скрипта:
import {
Connection,
LAMPORTS_PER_SOL
} from "@solana/web3.js";
import {
getConcurrentMerkleTreeAccountSize,
ALL_DEPTH_SIZE_PAIRS
} from "@solana/spl-account-compression";
const connection = new Connection();
const calculateCosts = async (maxProofSize: number) => {
await Promise.all(ALL_DEPTH_SIZE_PAIRS.map(async (pair) => {
const canopy = pair.maxDepth - maxProofSize;
const size = getConcurrentMerkleTreeAccountSize(pair.maxDepth, pair.maxBufferSize, canopy);
const numberOfNfts = Math.pow(2, pair.maxDepth);
const rent = (await connection.getMinimumBalanceForRentExemption(size)) / LAMPORTS_PER_SOL;
console.log(`maxDepth: ${pair.maxDepth}, maxBufferSize: ${pair.maxBufferSize}, canopy: ${canopy}, numberOfNfts: ${numberOfNfts}, rent: ${rent}`);
}));
}
await calculateCosts();Здесь мы импортируем необходимые модули из @solana/web3.js и @solana/spl-account-compression. Нам необходимо подключение к mainnet, которое можно установить с помощью ключа Helius API. Функция calculateCosts выводит в консоль maxDepth, maxBufferSize, canopy, количество NFT, которые можно хранить в этом дереве, а также стоимость rent в SOL. Поэтому, вызвав calculateCosts с желаемым размером доказательства, мы увидим в консоли все возможные комбинации параметров дерева.
Обратите внимание, что некоторые записи журнала могут содержать сообщение: Unable to fetch minimum balance for rent exemption. Это связано с тем, что аккаунт с указанным значением maxProofSize был бы слишком большим для создания, поэтому получить минимальный баланс, освобождающий аккаунт от арендной платы, невозможно.
Создание конкурентного дерева Меркла
При создании конкурентного дерева Меркла необходимо создать два аккаунта:
- Аккаунт конкурентного дерева Меркла
- Аккаунт конфигурации конкурентного дерева Меркла
Аккаунт дерева содержит дерево Меркла, которое используется для проверки данных. Мы создаем его с нужной максимальной глубиной, максимальным размером буфера и глубиной кроны, как описано в предыдущем разделе. Этот аккаунт принадлежит программе Account Compression, которую создает и поддерживает Solana. Она используется для проверки подлинности сжатых NFT.
Аккаунт конфигурации дерева — это PDA, производный от адреса аккаунта конкурентного дерева Меркла. Он используется для хранения дополнительных настроек, таких как создатель дерева и количество выпущенных сжатых NFT.
Metaplex называет конкурентные деревья Меркла со связанным аккаунтом конфигурации дерева «деревьями Bubblegum».
Полный код
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
ValidDepthSizePair,
createAllocTreeIx,
SPL_NOOP_PROGRAM_ID,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
} from "@solana/spl-account-compression";
import {
PROGRAM_ID,
createCreateTreeInstruction
} from "@metaplex-foundation/mpl-bubblegum";
const createTree = async (
connection: Connection,
payer: Keypair,
treeKeypair: Keypair,
maxDepthSizePair: ValidDepthSizePair,
canopyDepth: number = 0,
) => {
const allocTreeInstruction = await createAllocTreeIx(
connection,
treeKeypair.publicKey,
payer.publicKey,
maxDepthSizePair,
canopyDepth,
);
const [treeAuthority, ] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
PROGRAM_ID,
);
const createTreeInstruction = createCreateTreeInstruction(
{
payer: payer.publicKey,
treeCreator: payer.publicKey,
treeAuthority,
merkleTree: treeKeypair.publicKey,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
maxBufferSize: maxDepthSizePair.maxBufferSize,
maxDepth: maxDepthSizePair.maxDepth,
public: false,
},
PROGRAM_ID,
);
try {
const transaction = new Transaction().add(allocTreeInstruction).add(createTreeInstruction);
transaction.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[treeKeypair, payer],
{
commitment: "confirmed",
skipPreflight: true,
},
);
console.log(`Successfully created a Merkle tree with txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to create a Merkle tree with error: ${error}`);
}
}Разбор кода
Это пример функции для создания конкурентного дерева Меркла в Solana. Чтобы вызвать эту функцию, createTree, необходимо передать следующие параметры:
connection— подключение к JSON RPC-эндпоинту полной ноды типаConnectionpayer— аккаунт, который оплачивает транзакцию, типаKeypairtreeKeypair— адрес пары ключей дерева типаKeypairmaxDepthSizePair— допустимая параmaxDepthиmaxBufferSizeтипаValidDepthSizePaircanopyDepth— глубина кроны дерева типаnumber, для которой по умолчанию задано значение0
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
ValidDepthSizePair,
createAllocTreeIx,
SPL_NOOP_PROGRAM_ID,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
} from "@solana/spl-account-compression";
import {
PROGRAM_ID,
createCreateTreeInstruction
} from "@metaplex-foundation/mpl-bubblegum";Сначала мы импортируем @solana/web3.js, @solana/spl-account-compression и @metaplex-foundation/mpl-bubblegum с необходимыми модулями.
const createTree = async (
connection: Connection,
payer: Keypair,
treeKeypair: Keypair,
maxDepthSizePair: ValidDepthSizePair,
canopyDepth: number = 0,
) => {
// Rest of the code
}Здесь мы определяем функцию createTree с указанными выше параметрами.
const allocTreeInstruction = await createAllocTreeIx(
connection,
treeKeypair.publicKey,
payer.publicKey,
maxDepthSizePair,
canopyDepth,
);createAllocTreeIx — вспомогательная функция, которую мы используем для создания аккаунта конкурентного дерева Меркла. Пакет SPL Account Compression рекомендует использовать этот метод для инициализации аккаунта конкурентного дерева Меркла, поскольку такие аккаунты обычно довольно велики и могут превышать лимит выделения памяти через CPI. Здесь мы создаем инструкцию для выделения места под аккаунт дерева ончейн. При этом также рассчитываются место, необходимое для хранения дерева ончейн, и стоимость, поэтому нам не придется заниматься этим позднее.
const [treeAuthority, ] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
PROGRAM_ID,
);Нам нужно получить аккаунт конфигурации дерева, полномочия над которым принадлежат программе Bubblegum. Это необходимо для createCreateTreeInstruction — инструкции, создающей дерево, поскольку в качестве аргумента нужно передать treeAuthority. Здесь мы получаем PDA с помощью метода findProgramAddressSync, используя публичный ключ дерева и ID программы Bubblegum. Необходимо деструктурировать treeAuthority, поскольку возвращаются и полномочия, и bump. Мы опустили bump, так как он не нужен нашей функции. Если bump требуется сохранить, измените деструктуризацию на [treeAuthority, bump].
const createTreeInstruction = createCreateTreeInstruction(
{
payer: payer.publicKey,
treeCreator: payer.publicKey,
treeAuthority,
merkleTree: treeKeypair.publicKey,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
maxBufferSize: maxDepthSizePair.maxBufferSize,
maxDepth: maxDepthSizePair.maxDepth,
public: false,
},
PROGRAM_ID,
);Мы используем createCreateTreeInstruction из Bubblegum SDK, чтобы сформировать инструкцию для создания конкурентного дерева Меркла. Она создает дерево ончейн, владельцем которого становится программа Bubblegum. createCreateTreeInstruction принимает три параметра. Первый — объект с аккаунтами для настройки таких свойств, как создатель дерева. Второй объект относится к максимальной глубине и максимальному размеру буфера. Он также включает параметр public типа boolean. Если задать для public значение true, любой пользователь сможет выпускать сжатые NFT из дерева. В противном случае это сможет делать только создатель дерева или его делегат. Делегированный аккаунт может выполнять действия от имени владельца дерева, например передавать или сжигать сжатый NFT. Кроме того, назначить делегата дерева можно с помощью createSetTreeDelegateInstruction из пакета @metaplex-foundation/mpl-bubblegum следующим образом:
const changeTreeDelegateTransaction = createSetTreeDelegateInstruction({
merkleTree: treeKeypair.publicKey
newTreeDelegate: ,
treeAuthority,
treeCreator: treeCreator.publicKey // which in our script would be payer.publicKey
});Мы также передаем ID программы Bubblegum. Теперь вернемся к остальному коду:
try {
const transaction = new Transaction().add(allocTreeInstruction).add(createTreeInstruction);
transaction.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[treeKeypair, payer],
{
commitment: "confirmed",
skipPreflight: true,
},
);
console.log(`Successfully created a Merkle tree with txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to create a Merkle tree with error: ${error}`);
}Мы добавляем две только что созданные инструкции в транзакцию и отправляем ее. Мы проверяем, что и treeKeypair, и payer подписывают транзакцию. Затем сигнатура успешно выполненной транзакции выводится в консоль. Мы помещаем этот процесс в блок try-catch, чтобы при возникновении ошибки по любой причине она выводилась в консоль через console.error.
Создание конкурентного дерева Меркла с помощью Umi
Использование Bubblegum SDK, программы сжатия аккаунтов Solana и пакетов web3.js от Solana может запутать начинающих разработчиков, а повторная настройка каждый раз отнимает много времени. К счастью, Bubblegum SDK предоставляет операцию createTree, которая выполняет всю работу и отлично сочетается с Umi. Код выглядит так:
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { generateSigner } from '@metaplex-foundation/umi'
import { createTree } from '@metaplex-foundation/mpl-bubblegum'
const umi = createUmi();
const merkleTree = generateSigner(umi);
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
});
await builder.sendAndConfirm(umi);Umi — модульный фреймворк для создания и использования JavaScript-клиентов для программ Solana. Это библиотека без зависимостей с набором базовых интерфейсов, на которые могут опираться другие библиотеки без привязки к конкретной реализации. Umi предоставляется Metaplex, а документация доступна здесь.
Мы используем экземпляр Umi, чтобы создать подписанта и дерево Меркла, а затем отправить и подтвердить сформированную транзакцию. По умолчанию создателем дерева назначается идентификатор Umi, а параметру public присваивается значение false. Эти параметры можно настроить: указать другого создателя дерева и передать публичное значение true. Это значительно более быстрый способ создать конкурентное дерево Меркла ончейн.
Обратите внимание, что Bubblegum не зависит от размера кроны. Это связано с тем, что программа Account Compression от Solana определяет размер кроны по доступному пространству аккаунта. Нужно лишь выделить достаточно места, чтобы программа могла точно определить подходящий размер кроны.
Выпуск cNFT через прямое взаимодействие с Bubblegum
Создание коллекции
По стандарту Metaplex NFT традиционно объединяются в коллекции. Это относится как к сжатым, так и к «обычным» NFT. Чтобы создать коллекцию:
- Создайте новый «минт» токена
- Создайте связанный аккаунт токена для минта
- Выпустите один токен
- Сохраните метаданные коллекции в аккаунте ончейн
Это не связано напрямую со сжатием состояния или сжатыми NFT и потому выходит за рамки статьи, однако для справки мы предоставили скрипт создания собственной коллекции. Он доступен здесь.
Выпуск NFT в нашу коллекцию
Чтобы начать выпуск в только что созданную коллекцию, вам потребуется следующее:
collectionMint— адрес минта коллекцииcollectionAuthority— аккаунт с полномочиями над коллекциейcollectionMetadata— аккаунт метаданных коллекцииeditionAccount— аккаунт с дополнительными атрибутами, например аккаунт главного издания
Полный код выпуска в коллекцию
import {
Keypair,
PublicKey,
Connection,
Transaction,
sendAndConfirmTransaction,
TransactionInstruction,
} from "@solana/web3.js";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
MetadataArgs,
createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
export async function mintCompressedNFT(
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
collectionMint: PublicKey,
collectionMetadata: PublicKey,
collectionMasterEditionAccount: PublicKey,
compressedNFTMetadata: MetadataArgs,
receiverAddress?: PublicKey
) {
const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);
const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
[Buffer.from("collection_cpi", "utf8")],
BUBBLEGUM_PROGRAM_ID
);
const mintInstructions: TransactionInstruction[] = [];
const metadataArgs = Object.assign(compressedNFTMetadata, {
collection: { key: collectionMint, verified: false },
});
mintInstructions.push(
createMintToCollectionV1Instruction(
{
payer: payer.publicKey,
merkleTree: treeAddress,
treeAuthority,
treeDelegate: payer.publicKey,
leafOwner: receiverAddress || payer.publicKey,
leafDelegate: payer.publicKey,
collectionAuthority: payer.publicKey,
collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
collectionMint: collectionMint,
collectionMetadata: collectionMetadata,
editionAccount: collectionMasterEditionAccount,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumSigner: bubblegumSigner,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
},
{
metadataArgs,
}
)
);
try {
const txt = new Transaction().add(...mintInstructions);
txt.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
console.log(`Successfully minted a cNFT with the txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to mint cNFT with error: ${error}`);
}
}Разбор процесса выпуска
import {
Keypair,
PublicKey,
Connection,
Transaction,
sendAndConfirmTransaction,
TransactionInstruction,
} from "@solana/web3.js";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
MetadataArgs,
createMintToCollectionV1Instruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";Сначала мы импортируем @solana/web3.js, @solana/spl-account-compression, @metaplex-foundation/mpl-bubblegum и @metaplex-foundation/mpl-token-metadata с необходимыми модулями.
export async function mintCompressedNFT(
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
collectionMint: PublicKey,
collectionMetadata: PublicKey,
collectionMasterEditionAccount: PublicKey,
compressedNFTMetadata: MetadataArgs,
receiverAddress?: PublicKey
) {
// Rest of the code
}Мы определяем mintCompressedNFT, который принимает довольно много параметров:
connection— объект подключения, используемый для взаимодействия с Solanapayer— аккаунт, который оплачивает комиссии за транзакциюtreeAddress— аккаунт конкурентного дерева МерклаcollectionMint— адрес минта коллекцииcollectionMetadata— аккаунт метаданных коллекцииcollectionMasterEditionAccount— аккаунт главного изданияcompressedNFTMetadata— метаданные, относящиеся к выпускаемому cNFTreceiverAddress— необязательный адрес публичного ключа, на который будет отправлен новый cNFT
const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);
const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
[Buffer.from("collection_cpi", "utf8")],
BUBBLEGUM_PROGRAM_ID
);Здесь мы находим необходимые PDA и игнорируем их bump-значения. Сначала мы получаем PDA для полномочий дерева, а затем PDA, который будет выступать подписантом при сжатом выпуске. Необходимо включить collection_cpi, поскольку это специальный префикс, необходимый программе Bubblegum.
const mintInstructions: TransactionInstruction[] = [];Мы присваиваем mintInstructions пустой массив TransactionInstruction. Это позволяет при необходимости выпускать несколько cNFT одновременно.
const metadataArgs = Object.assign(compressedNFTMetadata, {
collection: { key: collectionMint, verified: false },
});metadataArgs обеспечивает правильное форматирование compressedNFTMetadata. Для успешного выпуска NFT в коллекцию с помощью createMintToCollectionV1Instruction поле verified должно иметь значение false, несмотря на то что коллекция проверяется автоматически.
mintInstructions.push(
createMintToCollectionV1Instruction(
{
payer: payer.publicKey,
merkleTree: treeAddress,
treeAuthority,
treeDelegate: payer.publicKey,
leafOwner: receiverAddress || payer.publicKey,
leafDelegate: payer.publicKey,
collectionAuthority: payer.publicKey,
collectionAuthorityRecordPda: BUBBLEGUM_PROGRAM_ID,
collectionMint: collectionMint,
collectionMetadata: collectionMetadata,
editionAccount: collectionMasterEditionAccount,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumSigner: bubblegumSigner,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
},
{
metadataArgs,
}
)
);Мы добавляем в инструкцию один выпуск. В одной транзакции можно добавить несколько выпусков, если она не превышает ограничение по размеру в байтах. Здесь мы используем createMintToCollectionV1Instruction, чтобы выпустить сжатый NFT из нашей коллекции. Эта инструкция принимает два объекта: один содержит аккаунты, необходимые для обработки инструкции, а другой передает данные инструкции программе. Большинство этих параметров уже знакомы по предыдущим разделам. Обратите внимание, что при выпуске можно задать любой адрес делегата, но обычно он должен совпадать с leafOwner. В любом случае при передаче cNFT делегат автоматически удаляется. Мы назначаем плательщика делегатом, поскольку он также получит cNFT, если receiverAddress не указан.
try {
const txt = new Transaction().add(...mintInstructions);
txt.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
console.log(`Successfully minted a cNFT with the txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to mint cNFT with error: ${error}`);
}Затем мы формируем транзакцию, задаем payer в качестве feePayer и отправляем транзакцию. Мы помещаем эту логику в блок try-catch на случай ошибок при отправке и подтверждении транзакции. Если возникает ошибка, мы выводим ее в консоль с помощью console.error.
Выпуск cNFT с помощью Umi
Программа Bubblegum предлагает два способа выпуска через Umi:
- Выпуск NFT без привязки к коллекции
- Выпуск NFT в указанную коллекцию.
Выпуск без коллекции
Инструкция MintV1 в Bubblegum позволяет выпускать сжатые NFT из дерева Bubblegum без коллекции. Если дерево публичное, выпускать в нем может любой пользователь. В противном случае использовать эту инструкцию может только создатель или делегат дерева. Вот как выпустить сжатый NFT без коллекции:
import { none } from '@metaplex-foundation/umi'
import { mintV1 } from '@metaplex-foundation/mpl-bubblegum'
await mintV1(umi, {
leafOwner,
merkleTree,
metadata: {
name: 'My Compressed NFT',
uri: 'https://example.com/my-cnft.json',
sellerFeeBasisPoints: 500, // 5%
collection: none(),
creators: [
{ address: umi.identity.publicKey, verified: false, share: 100 },
],
},
}).sendAndConfirm(umi);Этот фрагмент кода взят из документации Metaplex о выпуске cNFT с помощью Bubblegum. Здесь мы используем экземпляр Umi для выпуска cNFT. Остальные параметры инструкции mintV1:
leafOwner— владелец выпускаемого cNFTmerkleTree— адрес аккаунта конкурентного дерева Меркла, из которого будет выпущен cNFTmetadata— объект с метаданными выпускаемого cNFT. Он включает название cNFT, его URI, коллекцию, для которой мы указали отсутствие значения, а также создателей. Можно передать объект коллекции, но для поля verified создателей необходимо задать значениеfalse, поскольку инструкция не запрашивает полномочия над коллекцией. Создатели также могут самостоятельно подтвердить себя, задав для поля verified значение true и передав создателя как подписанта в остальных аккаунтах.
Инструкция mintV1 также содержит несколько необязательных полей, поскольку входные данные функции имеют тип MintV1InstructionAccounts & MintV1InstructionArgs. Эти типы определены следующим образом:
// Accounts
export type MintV1InstructionAccounts = {
treeConfig?: PublicKey | Pda;
leafOwner: PublicKey | Pda;
leafDelegate?: PublicKey | Pda;
merkleTree: PublicKey | Pda;
payer?: Signer;
treeCreatorOrDelegate?: Signer;
logWrapper?: PublicKey | Pda;
compressionProgram?: PublicKey | Pda;
systemProgram?: PublicKey | Pda;
};MintV1InstructionArgs — неочевидный составной тип, который в итоге представляет собой объект с полем metadata. Это поле metadata имеет тип MetadataArgsArgs и определяется следующим образом:
export type MetadataArgsArgs = {
/** The name of the asset */
name: string;
/** The symbol for the asset */
symbol?: string;
/** URI pointing to JSON representing the asset */
uri: string;
/** Royalty basis points that goes to creators in secondary sales (0-10000) */
sellerFeeBasisPoints: number;
primarySaleHappened?: boolean;
isMutable?: boolean;
/** nonce for easy calculation of editions, if present */
editionNonce?: OptionOrNullable;
/** Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. */
tokenStandard?: OptionOrNullable;
/** Collection */
collection: OptionOrNullable;
/** Uses */
uses?: OptionOrNullable;
tokenProgramVersion?: TokenProgramVersionArgs;
creators: Array;
};Полное определение функции mintV1 и всех связанных с ней типов доступно здесь. Но как минимум при наличии экземпляра Umi вы можете выпустить cNFT без коллекции, если передадите необходимые метаданные, владельца листа и аккаунт конкурентного дерева Меркла.
Выпуск в коллекцию
Bubblegum предоставляет mintToCollectionV1 как удобный способ напрямую выпустить cNFT в указанную коллекцию. Входные данные этой инструкции имеют типы MintToCollectionV1InstructionAccounts и MintToCollectionV1InstructionArgs, которые в итоге образуют объект типа MetadataArgsArgs. Тип MintToCollectionV1InstructionAccounts определяется следующим образом:
// Accounts
export type MintToCollectionV1InstructionAccounts = {
treeConfig?: PublicKey | Pda;
leafOwner: PublicKey | Pda;
leafDelegate?: PublicKey | Pda;
merkleTree: PublicKey | Pda;
payer?: Signer;
treeCreatorOrDelegate?: Signer;
collectionAuthority?: Signer;
/**
* If there is no collecton authority record PDA then
* this must be the Bubblegum program address.
*/
collectionAuthorityRecordPda?: PublicKey | Pda;
collectionMint: PublicKey | Pda;
collectionMetadata?: PublicKey | Pda;
collectionEdition?: PublicKey | Pda;
bubblegumSigner?: PublicKey | Pda;
logWrapper?: PublicKey | Pda;
compressionProgram?: PublicKey | Pda;
tokenMetadataProgram?: PublicKey | Pda;
systemProgram?: PublicKey | Pda;
};Ключевые параметры — минт коллекции, полномочия над коллекцией и PDA записи полномочий над коллекцией. При использовании делегированных полномочий над коллекцией необходимо указать PDA записи делегата, чтобы подтвердить право управлять NFT коллекции. Параметр metadata обязательно должен содержать объект коллекции, в котором поле address совпадает с параметром минта коллекции, а полю verified присвоено значение false. Создатели также могут самостоятельно подтвердить себя, подписав транзакцию и добавив себя в остальные аккаунты.
Вот как выпустить сжатый NFT в коллекцию:
import { none } from '@metaplex-foundation/umi'
import { mintToCollectionV1 } from '@metaplex-foundation/mpl-bubblegum'
await mintToCollectionV1(umi, {
leafOwner,
merkleTree,
collectionMint,
metadata: {
name: 'My Compressed NFT',
uri: 'https://example.com/my-cnft.json',
sellerFeeBasisPoints: 500, // 5%
collection: { key: collectionMint, verified: false },
creators: [
{ address: umi.identity.publicKey, verified: false, share: 100 },
],
},
}).sendAndConfirm(umi);Этот фрагмент кода можно найти в документации Metaplex о выпуске cNFT с помощью Bubblegum. Мы снова используем экземпляр Umi для выпуска сжатого NFT. Как и в случае с mintV1, мы передаем leafOwner и merkleTree. Однако на этот раз мы также передаем collectionMint. В поле metadata мы передаем объект collection, в котором ключ совпадает с collectionMint, а полю verified присвоено значение false. Обратите внимание, что идентификатор Umi по умолчанию получает полномочия над коллекцией. Это можно изменить, задав в необязательном поле collectionAuthority пользовательские полномочия над коллекцией.
Минтинг cNFT с Helius
Helius предоставляет Mint API, который позволяет минтить сжатые NFT без лишних сложностей. Мы оплачиваем комиссии Solana, создаём дерево Меркла и загружаем ваши офчейн-метаданные в Arweave. Мы также проверяем, что транзакция успешно отправлена и подтверждена сетью, поэтому вам не нужно самостоятельно опрашивать её статус. Кроме того, мы извлекаем ID актива из транзакции, чтобы вы могли сразу использовать его с DAS API.
Чтобы Helius мог минтить NFT в вашу коллекцию, ему необходимо делегировать полномочия управления коллекцией. В зависимости от кластера полномочия нужно делегировать одной из следующих учётных записей:
- Devnet:
2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC - Mainnet:
HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R
Вот как минтить cNFT с помощью Helius Mint API:
const url = `https://mainnet.helius-rpc.com/?api-key=`;
const mintCompressedNft = async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'helius-test',
method: 'mintCompressedNft',
params: {
name: 'Exodia the Forbidden One',
symbol: 'ETFO',
owner: 'DCQnfUH6mHA333mzkU22b4hMvyqcejUBociodq8bB5HF',
description:
'Exodia the Forbidden One is a powerful, legendary creature composed of five parts: ' +
'the Right Leg, Left Leg, Right Arm, Left Arm, and the Head. When all five parts are assembled, Exodia becomes an unstoppable force.',
attributes: [
{
trait_type: 'Type',
value: 'Legendary',
},
{
trait_type: 'Power',
value: 'Infinite',
},
{
trait_type: 'Element',
value: 'Dark',
},
{
trait_type: 'Rarity',
value: 'Mythical',
},
],
imageUrl:
'https://cdna.artstation.com/p/assets/images/images/052/118/830/large/julie-almoneda-03.jpg?1658992401',
externalUrl: 'https://www.yugioh-card.com/en/',
sellerFeeBasisPoints: 6900,
},
}),
});
const { result } = await response.json();
console.log('Minted asset: ', result.assetId);
};
mintCompressedNft();Этот фрагмент кода и подробное описание схемы запроса доступны в нашей документации.
Обратите внимание: если вы не заполните поле uri, мы создадим JSON-файл и загрузим его в Arweave от вашего имени. Файл будет соответствовать стандарту Metaplex JSON v1.0 и будет загружен через Irys (ранее Bundlr).
Передача cNFT
Передача сжатого NFT обычно включает следующие шаги:
- Получить данные актива cNFT из индексатора
- Получить доказательство cNFT из индексатора
- Получить учётную запись конкурентного дерева Меркла из Solana
- Подготовить доказательство актива
- Создать и отправить транзакцию передачи
Umi и Metaplex значительно упрощают этот процесс, но в этом разделе мы покажем, что происходит за кулисами. Ниже описано, как передать сжатый NFT с помощью web3.js и Metaplex.
Передача через прямое взаимодействие с Bubblegum
Прежде чем выполнить передачу с помощью скрипта, необходимо получить информацию о нашем сжатом NFT. Сначала нужно вызвать метод getAsset в DAS API, чтобы получить метаданные сжатого NFT. Нам нужны data_hash, creator_hash, owner, delegate и leaf_id:
// Example getAsset call:
const url = `https://mainnet.helius-rpc.com/?api-key=`
const getAsset = async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAsset',
params: {
id: ''
},
}),
});
const { result } = await response.json();
console.log("Asset: ", result);
};
getAsset();Часть успешного ответа будет выглядеть так:
{
...
},
"compression": {
"eligible": true,
"compressed": true,
"data_hash": "string",
"creator_hash": "string",
"asset_hash": "string",
"tree": "string",
"seq": 0,
"leaf_id": 0
...
"ownership": {
...
"delegate": "string",
"ownership_model": "string",
"owner": "string",
...
}
}Получив необходимую информацию, вызовите метод getAssetProof, чтобы получить proof и tree_id (адрес дерева). Пример вызова:
const url = `https://mainnet.helius-rpc.com/?api-key=`
const getAssetProof = async () => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'my-id',
method: 'getAssetProof',
params: {
id: ''
},
}),
});
const { result } = await response.json();
console.log("Assets Proof: ", result);
};
getAssetProof();Успешный ответ будет выглядеть так:
{
"root": "string",
"proof": [
"string"
],
"node_index": 0,
"leaf": "string",
"tree_id": "string"
}Теперь, получив root, proof и tree_id, можно перейти к скрипту передачи.
Полный код
import { Connection, Keypair, AccountMeta, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createTransferInstruction, PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
import {
ConcurrentMerkleTreeAccount,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";
const transferCompressedNFT = async (
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
proof: string[],
root: string,
dataHash: string,
creatorHash: string,
leafId: number,
owner: string,
newLeafOwner: PublicKey,
delegate: string
) => {
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);
const treeAuthority = treeAccount.getAuthority();
const canopyDepth = treeAccount.getCanopyDepth();
const proofPath: AccountMeta[] = proof
.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}))
.slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));
const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);
const transferInstruction = createTransferInstruction(
{
merkleTree: treeAddress,
treeAuthority,
leafOwner,
leafDelegate,
newLeafOwner,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
anchorRemainingAccounts: proofPath,
},
{
root: [...new PublicKey(root.trim()).toBytes()],
dataHash: [...new PublicKey(dataHash.trim()).toBytes()],
creatorHash: [...new PublicKey(creatorHash.trim()).toBytes()],
nonce: leafId,
index: leafId,
},
PROGRAM_ID
);
try {
const txt = new Transaction().add(transferInstruction);
txt.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
console.log(`Successfully transfered the cNFT with txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to transfer cNFT with error: ${error}`);
}
};Разбор кода
import { Connection, Keypair, AccountMeta, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
import { createTransferInstruction, PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
import {
ConcurrentMerkleTreeAccount,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
} from "@solana/spl-account-compression";Мы импортируем @solana/web3.js, @metaplex-foundation/mpl-bubblegum и @solana/spl-account-compression с необходимыми модулями.
const transferCompressedNFT = async (
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
proof: string[],
root: string,
dataHash: string,
creatorHash: string,
leafId: number,
owner: string,
newLeafOwner: PublicKey,
delegate: string
) => {
// Rest of the code
}Мы определяем функцию transferCompressedNFT, в которой разбираем путь доказательства, создаём инструкцию передачи и выполняем её.
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);
const treeAuthority = treeAccount.getAuthority();
const canopyDepth = treeAccount.getCanopyDepth();Мы получаем из блокчейна учётную запись конкурентного дерева Меркла и извлекаем полномочия дерева и глубину кроны. Эти значения нужны для создания инструкции передачи.
const proofPath: AccountMeta[] = proof
.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}))
.slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));Проще говоря, мы преобразуем список адресов доказательства в допустимый массив типа AccountMeta. AccountMeta — это метаданные учётной записи, которые используются для определения транзакций. Они включают публичный ключ учётной записи, требование инструкции к подписи транзакции, соответствующей публичному ключу, и возможность загрузить публичный ключ как учётную запись для чтения и записи.
Мы берём срез полного доказательства от начала массива и проверяем, что он содержит только proof.length - canopyDepth значений доказательства. Так мы удаляем часть дерева, уже закэшированную в ончейн-кроне. Затем преобразуем каждое оставшееся значение доказательства в допустимый AccountMeta. Это необходимо, поскольку доказательство отправляется ончейн в виде «дополнительных учётных записей» внутри инструкции передачи.
const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);Затем мы задаём для leafOwner значение параметра owner, а для leafDelegate — значение параметра delegate.
const transferInstruction = createTransferInstruction(
{
merkleTree: treeAddress,
treeAuthority,
leafOwner,
leafDelegate,
newLeafOwner,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
anchorRemainingAccounts: proofPath,
},
{
root: [...new PublicKey(root.trim()).toBytes()],
dataHash: [...new PublicKey(dataHash.trim()).toBytes()],
creatorHash: [...new PublicKey(creatorHash.trim()).toBytes()],
nonce: leafId,
index: leafId,
},
PROGRAM_ID
);Мы создаём transferInstruction с помощью вспомогательной функции createTransferInstruction из Bubblegum SDK. Обратите внимание: DAS API возвращает root, dataHash и creatorHash в виде строк, поэтому их необходимо преобразовать в тип PublicKey, а затем в массив байтов.
try {
const txt = new Transaction().add(transferInstruction);
txt.feePayer = payer.publicKey;
const transactionSignature = await sendAndConfirmTransaction(connection, txt, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
console.log(`Successfully transfered the cNFT with txt sig: ${transactionSignature}`);
} catch (error: any) {
console.error(`Failed to transfer cNFT with error: ${error}`);
}Созданную инструкцию мы добавляем в новую транзакцию и отправляем в Solana. При возникновении ошибок выводим их в консоль с помощью console.error.
Если возникают ошибки, связанные с конкурентным деревом Меркла, возможно, ваш RPC предоставляет устаревшие или некорректные данные доказательства для этого дерева. Такое иногда происходит из-за проблем с кэшированием. Чтобы устранить проблему, попробуйте проверить предоставленное RPC доказательство на стороне клиента:
const merkleTreeProof: MerkleTreeProof = {
leafIndex: leafId,
leaf: new PublicKey(leaf).toBuffer(),
root: new PublicKey(root).toBuffer(),
proof: proof.map((node: string) => new PublicKey(node).toBuffer()),
};
const currentRoot = treeAccount.getCurrentRoot();
const rpcRoot = new PublicKey(root).toBuffer();
console.log(new PublicKey(currentRoot).toBase58() === new PublicKey(rpcRoot).toBase58());Вам также потребуется значение leaf, возвращённое вызовом getAssetProof в DAS API. Это необязательно, поскольку фактическая проверка доказательства выполняется ончейн, однако такой подход может помочь при обработке ошибок.
Теперь можно снова вызвать getAsset и убедиться, что leafDelegate содержит пустое значение, а у листа появился новый владелец!
Передача с помощью Umi
import { getAssetWithProof, transfer } from '@metaplex-foundation/mpl-bubblegum'
const assetWithProof = await getAssetWithProof(umi, assetId)
await transfer(umi, {
...assetWithProof,
leafOwner: currentLeafOwner,
newLeafOwner: newLeafOwner.publicKey,
}).sendAndConfirm(umi);Этот код взят из документации Metaplex по передаче сжатых NFT.
Bubblegum предоставляет инструкцию transfer, которую очень просто использовать. Сначала она принимает экземпляр Umi. Затем — объект с активом, данными о его доказательстве, владельце листа и новом владельце листа. Чтобы получить актив с необходимым доказательством, можно использовать метод getAssetWithProof, также предоставляемый Bubblegum. Обратите внимание: вместо владельца листа можно указать делегата листа — нужна лишь учётная запись с полномочиями для авторизации передачи. Метод .sendAndConfirm() отправляет транзакцию, которая запускает передачу, а затем подтверждает её с помощью нашего экземпляра Umi.
Заключение
Поздравляем! Мы подробно рассмотрели сжатие состояния и сжатые NFT в Solana. Мы разобрались в сложностях конкурентных деревьев Меркла, развеяли распространённые заблуждения и глубоко изучили реестр Solana. Помимо теории, мы научились получать, минтить и передавать cNFT с помощью web3.js от Solana, Metaplex и Helius!
Сжатие состояния Solana меняет правила игры в сфере, где стоимость транзакций и хранения может создавать серьёзные ограничения. Сжатие радикально сокращает расходы без ущерба для безопасности и децентрализации. Это смена парадигмы, которая открывает беспрецедентные возможности для художников, коллекционеров и разработчиков.
Если вы дочитали до этого места, спасибо, анон! Теперь у вас достаточно знаний, чтобы внести вклад в развитие этого нового направления. Действуйте: создайте коллекцию из десяти миллионов NFT для своей ончейн-MMORPG, разработайте децентрализованное приложение, использующее возможности реестра, или просто поделитесь новыми знаниями с сообществом. Лучший способ предсказать будущее — создать его.
Дополнительные ресурсы
- Документация Solana по сжатию состояния
- Программа сжатия учётных записей
- Документация Metaplex по Bubblegum
- Сжатые NFT в Solana — это будущее • Объясняет Helius
- Практический пример Dialect и cNFT
- Angela: разреженное, распределённое и высококонкурентное дерево Меркла
- Параллельная реализация дерева Меркла на C++
Похожие статьи
Подпишитесь на Helius
Следите за новостями разработки на Solana и получайте уведомления о наших публикациях


