
Tout savoir sur la compression sur Solana
Sommaire
- De quoi parle cet article ?
- Idées reçues courantes
- La compression sur Solana est identique à la compression traditionnelle
- Stocker des données compressées hors chaîne est risqué et crée des vulnérabilités
- Je peux perdre mon arbre de Merkle concurrent si l’indexeur ou le fournisseur RPC que j’utilise pour le stocker tombe en panne
- Les arbres de Merkle concurrents peuvent gérer des mises à jour parallèles
- Un arbre est la même chose qu’une collection
- Qu’est-ce que la compression d’état ?
- État et registre
- Que sont les NFT compressés ?
- Lecture des métadonnées de NFT compressés avec la DAS API
- Dimensionnement et coûts de création d’un arbre de Merkle concurrent
- Calcul de la taille
- Calcul des coûts
- Création d’un arbre de Merkle concurrent
- Code complet
- Décomposition du code
- Création d’un arbre de Merkle concurrent avec Umi
- Émission de cNFT en interagissant directement avec Bubblegum
- Création d’une collection
- Émission d’un NFT dans notre collection
- Code complet pour l’émission dans une collection
- Décomposition du processus d’émission
- Émission de cNFT avec Umi
- Émission sans collection
- Émission avec une collection
- Mintrer des cNFT avec Helius
- Transférer des cNFT
- Transfert en interagissant directement avec Bubblegum
- Code complet
- Analyse du code
- Transfert avec Umi
- Conclusion
- Ressources supplémentaires / Pour aller plus loin
De quoi parle cet article ?
Me croiriez-vous si je vous disais que vous pouvez créer un million de NFT dès maintenant pour moins de 150 USD ? Absurde ! Selon la blockchain, créer autant de NFT coûterait plus d’un million de dollars ! N’est-ce pas ?
La compression d’état est une nouvelle primitive qui exploite les arbres de Merkle et le registre de Solana pour réduire considérablement les coûts de stockage, tout en bénéficiant de la sécurité et de la décentralisation de la couche de base de Solana. Cet article propose une présentation complète et approfondie de la compression sur Solana. Il couvre tous les sujets, des idées reçues courantes au transfert de NFT compressés. Si vous souhaitez comprendre la compression d’état et apprendre à récupérer, créer ou transférer des NFT compressés, c’est le seul article dont vous aurez besoin pour commencer.
Cet article suppose que vous avez déjà lu notre article Outils cryptographiques 101 — Explication des fonctions de hachage et des arbres de Merkle. Il est important de le lire au préalable, car nous supposons que vous connaissez les arbres de Merkle. Le présent article développe également le sujet des arbres de Merkle concurrents et approfondit leur dimensionnement et leur création.
Cet article utilise à la fois le SDK Bubblegum et Umi pour présenter les différentes méthodes de création d’arbres de Merkle concurrents, ainsi que de création et de transfert de NFT compressés. Il est utile de connaître les deux outils, car vous les rencontrerez probablement dans différentes bases de code. Le SDK Bubblegum est inclus spécifiquement pour faciliter l’apprentissage, puisque son workflow rend les mécanismes sous-jacents plus transparents, tandis qu’Umi propose un workflow plus concis qui simplifie ces processus.
Idées reçues courantes
Avant d’aborder la compression d’état et les subtilités des NFT compressés, nous devons clarifier quelques points :
La compression sur Solana est identique à la compression traditionnelle
C’est faux. Traditionnellement, la compression sert à réduire la taille des fichiers et des données. Son objectif principal consiste à stocker ou à transmettre des données avec moins de bits que le fichier d’origine. Il existe deux grandes catégories d’algorithmes de compression :
- La compression sans perte, qui permet de reconstruire les données d’origine à partir des données compressées
- La compression avec perte, qui supprime les informations « moins importantes » afin de réduire la taille du fichier
Un NFT compressé n’est pas un NFT auquel un algorithme de compression avec ou sans perte aurait été appliqué pour réduire la taille de ses données. Il ne s’agit pas non plus de réduire la qualité ou les dimensions de l’œuvre, de la musique ou des métadonnées associées au NFT. Dans le contexte de Solana, ce concept prend une forme entièrement différente. Il s’agit plutôt d’optimiser la manière dont le registre sous-jacent de la blockchain stocke les informations relatives à ce NFT. Du point de vue des comptes, nous les compressons dans le registre en regroupant plusieurs comptes — ici, des NFT — au sein d’une seule racine de Merkle stockée dans l’état. Ce processus réduit considérablement les coûts de stockage tout en préservant la vérifiabilité.
Stocker des données compressées hors chaîne est risqué et crée des vulnérabilités
C’est faux : vous pouvez stocker des données hors chaîne en toute sécurité en les hachant et en stockant leur racine de Merkle sur la chaîne. Techniquement, les NFT compressés ne sont pas stockés hors chaîne. Les données restent sur la chaîne, car tout ce qui peut être reconstitué à partir du registre est considéré comme étant sur la chaîne. La différence est que l’état incite les validateurs à conserver les comptes en mémoire, tandis que le registre doit être consulté via des nœuds d’archivage. La compression d’état associe les deux afin de permettre la vérification des données du registre grâce à l’état d’un compte, tout en conservant la sécurité et la décentralisation propres à Solana. Nous expliquerons dans une autre section ce qu’est le registre et pourquoi il est sûr.
Je peux perdre mon arbre de Merkle concurrent si l’indexeur ou le fournisseur RPC que j’utilise pour le stocker tombe en panne
Vous ne perdrez pas votre arbre : toute personne ayant accès au registre peut le reconstruire intégralement en rejouant son historique.
Les arbres de Merkle concurrents peuvent gérer des mises à jour parallèles
Une idée reçue courante consiste à penser que le terme « concurrent » implique que plusieurs mises à jour d’un arbre de Merkle sur la chaîne peuvent avoir lieu en parallèle. Bien que les arbres de Merkle concurrents puissent accepter plusieurs remplacements de feuilles dans un même bloc, les validateurs traitent ces mises à jour de manière séquentielle. Lorsqu’un validateur reçoit un lot de transactions qui affectent un arbre de Merkle concurrent sur la chaîne, il peut les traiter dans le même slot. Toutefois, les données de chaque slot ne sont pas produites simultanément. Nous approfondissons ce sujet dans la section suivante, Qu’est-ce que la compression d’état ?
Un arbre est la même chose qu’une collection
Les arbres de Merkle concurrents ne sont pas la même chose qu’une collection. Une seule collection peut utiliser n’importe quel nombre d’arbres de Merkle concurrents. Il est important de noter que le regroupement des NFT peut être indépendant de leur stockage. Les NFT peuvent se trouver dans des comptes ou être compressés dans le registre, au sein d’un ou de plusieurs arbres. Il est toutefois recommandé de n’utiliser les arbres de Merkle concurrents que pour une seule collection afin de réduire la complexité.
Qu’est-ce que la compression d’état ?
La compression d’état optimise le stockage en créant un hachage cryptographique des données du registre et en stockant ce hachage dans un compte. Cette approche exploite la sécurité et l’immuabilité intrinsèques du registre, tout en fournissant un cadre robuste pour vérifier les données qui y sont stockées.
Il s’agit d’une solution économique pour les applications conçues sur Solana. Les développeurs peuvent désormais utiliser l’espace de stockage du registre plutôt que le stockage plus coûteux fondé sur les comptes. Ainsi, la compression d’état garantit l’intégrité des données tout en offrant une solution économique pour l’allocation des ressources sur Solana.
Le secret de la compression d’état de Solana réside dans l’utilisation d’arbres de Merkle concurrents. Ces arbres sont optimisés pour traiter plusieurs transactions en succession rapide, de sorte que leurs preuves puissent être actualisées. Ils diffèrent des arbres de Merkle traditionnels, dont les preuves sont invalidées à chaque mise à jour. Les arbres de Merkle concurrents conservent un journal sécurisé de leurs modifications les plus récentes, ainsi que leur hachage racine et la preuve nécessaire pour le calculer. Ce journal des modifications est stocké sur la chaîne dans un compte dédié à l’arbre. Chaque arbre de Merkle concurrent possède une taille maximale de tampon. Cette valeur correspond au nombre maximal de modifications qui peuvent être apportées à l’arbre tout en conservant la validité de sa racine de Merkle. Considérez-la comme le degré d’« obsolescence » qu’un ensemble de preuves calculé peut atteindre avant de devoir être mis à jour.
Ainsi, lorsqu’un validateur reçoit plusieurs requêtes visant à mettre à jour un arbre de Merkle sur la chaîne au cours du même slot, il peut utiliser le journal des modifications de l’arbre comme source de vérité. Cela autorise un nombre de modifications concurrentes de l’arbre de Merkle pouvant atteindre la taille maximale du tampon. Même si cela ne réduit pas directement le volume de données stockées sur la chaîne, l’efficacité augmente puisque plusieurs mises à jour peuvent être traitées simultanément. Le système peut ainsi préserver l’intégrité de la « preuve d’inclusion » offerte par les arbres de Merkle, même dans un environnement à haut débit. Ici, une preuve d’inclusion désigne simplement la capacité à démontrer qu’un élément de données précis appartient bien à un ensemble de données hachées conjointement pour former une racine de Merkle.
Cette combinaison ingénieuse de compression d’état et d’arbres de Merkle concurrents offre une solution extrêmement économique aux applications développées sur Solana. Pour apprécier pleinement l’impact de ces technologies, il est essentiel d’examiner la différence entre l’état de Solana et son registre.
État et registre
Le registre est un historique de toutes les transactions signées par des clients qui ont eu lieu sur Solana depuis son bloc de genèse. Sa structure de données est uniquement additive : une transaction ne peut plus être modifiée ni supprimée une fois ajoutée. Les validateurs valident les transactions ajoutées au registre. Celui-ci est stocké par plusieurs nœuds sur l’ensemble du réseau afin d’assurer la tolérance aux pannes. La copie du registre détenue par un validateur peut toutefois ne contenir que les blocs les plus récents afin de réduire les besoins de stockage, car les anciens blocs ne sont pas nécessaires à la validation des futurs blocs.
L’état représente l’instantané actuel de tous les comptes et programmes sur Solana. Il est modifiable et évolue à mesure que les transactions sont traitées. Considérez l’état comme une base de données hautement optimisée que vous pouvez interroger pour connaître les soldes de tokens, les programmes et les comptes.
Voici un moyen simple de les distinguer : supposons qu’Alice possède un solde de 100 SOL et Bob également. Alice envoie une transaction pour transférer 10 SOL à Bob. Une fois vérifiée, la transaction est ajoutée à un bloc, qui est ensuite ajouté au registre. Le registre contient désormais un enregistrement immuable indiquant qu’Alice a envoyé 10 SOL à Bob. En parallèle, l’état met à jour les comptes d’Alice et de Bob, dont les soldes passent respectivement à 90 et 110 SOL.
Les principales différences entre les deux peuvent être résumées comme suit :
- Le registre est immuable et uniquement additif, tandis que l’état est modifiable et évolue constamment
- Le registre est un historique de toutes les transactions, tandis que l’état reflète la situation actuelle de tous les comptes et programmes
- Le registre sert à la vérification, tandis que l’état sert à exécuter les transactions et les programmes
Alors que le registre agit comme un historique immuable garantissant que chaque transaction peut être vérifiée et retracée, l’état fonctionne comme un instantané dynamique du registre qui s’adapte aux opérations en temps réel, telles que les transferts et l’exécution de programmes. Il est important de noter que tous deux sont soumis au consensus de la chaîne elle-même. Ensemble, l’état et le registre forment la colonne vertébrale de Solana, lui permettant de fonctionner efficacement tout en préservant une confiance décentralisée.
Que sont les NFT compressés ?
Les NFT compressés (cNFT) utilisent la compression d’état et les arbres de Merkle concurrents pour réduire les coûts de stockage. Ils stockent leurs métadonnées dans le registre plutôt que de stocker chaque NFT dans un compte Solana classique. Cela permet de réduire les coûts de stockage tout en bénéficiant de la sécurité et de l’immuabilité du registre.
Les NFT compressés suivent exactement le même schéma de métadonnées que leurs équivalents non compressés. Les NFT et les cNFT sont donc définis de la même manière.
Les principales différences entre les NFT et les cNFT sont les suivantes :
- Un NFT compressé peut être converti en NFT classique, mais un NFT classique ne peut pas être converti en NFT compressé
- Les NFT compressés ne sont pas des tokens Solana natifs : ils ne possèdent ni compte de token, ni compte de création, ni métadonnées. Ils disposent toutefois d’un identifiant stable, l’ID de l’actif. Après décompression, le NFT conserve le même identifiant. Dans leur état compressé, les NFT ne sont donc pas des tokens natifs, mais peuvent le devenir si nécessaire
- Un seul compte d’arbre de Merkle concurrent peut contenir des millions de NFT
- Une seule collection peut être répartie sur plusieurs comptes d’arbre
- Toutes les modifications de NFT passent par le programme Bubblegum
- Un appel à la DAS API est recommandé pour lire toute information relative à un NFT compressé
Fait intéressant, nous devons utiliser la DAS API pour obtenir des informations sur un NFT compressé. Pourquoi ? Et surtout, de quoi s’agit-il ?
Lecture des métadonnées de NFT compressés avec la DAS API
Nous avons besoin de l’aide d’indexeurs, car les métadonnées d’un cNFT sont stockées dans le registre plutôt que dans un compte traditionnel. Bien que vous puissiez déterminer l’état actuel d’un NFT compressé en rejouant les transactions pertinentes, des fournisseurs comme Helius s’en chargent pour vous. Les développeurs peuvent utiliser la Digital Asset Standard (DAS) API, une spécification et un système open source permettant de récupérer les informations d’un actif. La DAS API prend en charge les NFT compressés comme les NFT traditionnels, ou non compressés. Vous pouvez donc utiliser le même endpoint pour les deux types de NFT.
Helius prend actuellement en charge les méthodes DAS API suivantes :
getAsset- récupérer un actif précis à partir de son IDgetAssetBatch- récupérer plusieurs actifs à partir de leurs IDgetAssetProof- obtenir une preuve de Merkle pour un actif compressé à partir de son IDgetAssetProofBatch- obtenir les preuves de plusieurs actifs à partir de leurs IDgetAssetsByOwner- obtenir la liste des actifs détenus par une adressegetAssetsByAuthority- obtenir la liste des actifs associés à une autorité donnéegetAssetsByCreator- obtenir la liste des actifs créés par une adressegetAssetsByGroup- obtenir la liste des actifs à partir d’une clé et d’une valeur de groupesearchAssets- rechercher des actifs selon différents paramètresgetSignaturesForAsset- obtenir la liste des signatures de transactions associées à un actif compressé- Pagination - prise en charge de la pagination par page et par jeu de clés afin de récupérer plus de 1 000 enregistrements à la fois
Consultez la documentation de la DAS API de Helius pour en savoir plus sur chaque méthode. Par exemple, si vous souhaitez récupérer la liste de tous les actifs détenus par une adresse, vous pouvez envoyer la requête POST suivante avec 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();Il est pratique de récupérer des actifs compressés, mais comment créer les vôtres ? Avant de commencer à en créer, il est essentiel de calculer la taille et le coût associé à la construction de l’arbre de Merkle concurrent qui stockera ces actifs.
Dimensionnement et coûts de création d’un arbre de Merkle concurrent
Calcul de la taille
Lors de la création d’un arbre de Merkle concurrent sur la chaîne, trois paramètres importants déterminent la taille de l’arbre, son coût de création et le nombre de modifications concurrentes qui peuvent y être apportées tout en maintenant la validité de la racine de Merkle :
- Profondeur maximale
- Taille maximale du tampon
- Profondeur de la canopée
La profondeur maximale désigne le nombre maximal d’étapes nécessaires pour aller de n’importe quelle feuille jusqu’à la racine de l’arbre. Chaque feuille n’est reliée qu’à une seule autre feuille et forme avec elle une paire pour le hachage par paires. Vous pouvez calculer le nombre maximal de nœuds feuilles qu’un arbre peut contenir avec la formule suivante : numberOfNodes = 2 ^ maxDepth. La profondeur de l’arbre doit être définie lors de sa création. Vous devez donc utiliser cette formule pour déterminer la profondeur maximale la plus faible permettant de stocker vos données. Par exemple, si vous souhaitez stocker environ 100 NFT compressés dans un arbre, une valeur maxDepth de 7 suffira, puisque 2^7 = 128 et 2^6 = 64. La profondeur maximale influe considérablement sur le coût de construction d’un arbre de Merkle concurrent sur la chaîne. Ces coûts sont engagés dès la création de l’arbre et augmentent avec les valeurs plus élevées de maxDepth.
La taille maximale du tampon désigne le nombre maximal de modifications pouvant être apportées à un arbre tout en conservant la validité de sa racine de Merkle. Pour les arbres de Merkle concurrents, le tampon du journal des modifications est dimensionné et défini lors de la création de l’arbre à l’aide de la valeur maxBufferSize. Ainsi, lorsqu’un validateur reçoit plusieurs requêtes de modification d’un arbre au cours du même slot, il peut utiliser le journal des modifications et autoriser jusqu’à maxBufferSize modifications tout en maintenant la validité de la racine.
Il est essentiel de noter qu’il n’existe qu’un nombre précis de paires maxDepth et maxBufferSize valides pour créer un nouveau compte d’arbre de Merkle concurrent. Le package @solana/spl-account-compression exporte la constante ALL_DEPTH_SIZE_PAIRS, qui est un tableau de tableaux de nombres contenant toutes les combinaisons valides. Le minimum correspond à une valeur maxDepth de 3 et à une valeur maxBufferSize de 8, tandis que le maximum correspond à une valeur maxDepth de 30 et à une valeur maxBufferSize de 2048.
La profondeur de la canopée désigne une partie de l’arbre de Merkle stockée dans un compte. Ces preuves mises en cache servent à compléter celles transmises sur le réseau, car ces dernières sont soumises aux limites des transactions. Le chemin complet doit être utilisé pour vérifier la propriété initiale d’une feuille lorsque vous cherchez à modifier ses données, par exemple lors du transfert d’un NFT. Plus la profondeur maximale de l’arbre est élevée, plus le nombre de nœuds de preuve nécessaires à la vérification augmente. La canopée réduit la taille de la preuve et évite d’utiliser une preuve de taille maxDepth pour vérifier l’arbre.
Vous pouvez calculer la profondeur de la canopée en soustrayant la taille de preuve souhaitée de la profondeur maximale. Ainsi, avec une profondeur maximale de 14 et une taille de preuve souhaitée de 4, la profondeur de la canopée serait de 10. Vous ne devriez alors fournir que 4 nœuds de preuve par transaction de mise à jour. La profondeur de la canopée influe également considérablement sur le coût de construction d’un arbre de Merkle concurrent sur la chaîne. Ces coûts sont engagés dès la création de l’arbre et augmentent avec les valeurs plus élevées de canopyDepth. Une faible valeur canopyDepth réduit le coût initial, mais elle peut limiter la composabilité. En effet, chaque transaction de mise à jour nécessitera une preuve plus volumineuse, ce qui renforcera les contraintes liées aux limites de taille des transactions. Si, par exemple, votre arbre doté d’une faible valeur canopyDepth sert à stocker des NFT compressés, une marketplace de NFT pourrait uniquement prendre en charge les transferts simples pour votre collection. En règle générale, maxDepth - canopyDepth doit être inférieur ou égal à 10 pour maximiser la composabilité. Ce point est détaillé dans la spécification de Tensor sur la longueur maximale des preuves pour les cNFT Tensor.
Calcul des coûts
Il existe différentes méthodes pour déterminer la taille et le coût d’un arbre de Merkle concurrent. La plus simple consiste à utiliser le calculateur de NFT compressés et à saisir le nombre de NFT compressés que vous souhaitez stocker dans cet arbre :
Le site fournit une ventilation détaillée de la profondeur d’arbre optimale nécessaire pour stocker le nombre d’actifs souhaité, ainsi que différentes options de coût selon le niveau de composabilité. Par exemple, l’illustration montre que la création d’un arbre hautement composable pouvant stocker 10 millions de NFT compressés ne coûterait que ~7,67 SOL. En tenant compte des coûts de transaction d’environ ~50 SOL nécessaires à la création de 10 millions de NFT, le coût total s’élèverait à environ ~57,67 SOL.
Les développeurs peuvent également utiliser le package @solana/spl-account-compression pour calculer l’espace nécessaire à un arbre d’une taille donnée, ainsi que le coût d’allocation de cet espace sur la chaîne. Le script suivant permet de le faire :
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();Ici, nous importons les modules nécessaires depuis @solana/web3.js et @solana/spl-account-compression. Nous avons besoin d’une connexion au mainnet, que nous pouvons établir avec une clé API Helius. La fonction calculateCosts affiche dans la console les valeurs maxDepth, maxBufferSize et canopy, le nombre de NFT que cet arbre peut stocker, ainsi que le coût de rent en SOL. Ainsi, lorsque nous appelons calculateCosts avec la taille de preuve souhaitée, nous pouvons voir toutes les combinaisons d’arbres possibles dans notre console.
Notez que certains journaux peuvent afficher : Impossible de récupérer le solde minimal pour l’exemption de loyer. Cela s’explique par le fait que le compte associé à la valeur maxProofSize indiquée serait trop volumineux pour être créé. Nous ne pouvons donc pas récupérer le solde minimal nécessaire pour exempter ce compte de loyer.
Création d’un arbre de Merkle concurrent
Nous devons créer deux comptes lors de la création d’un arbre de Merkle concurrent :
- Un compte d’arbre de Merkle concurrent
- Un compte de configuration d’arbre de Merkle concurrent
Le compte de l’arbre contient l’arbre de Merkle utilisé pour vérifier les données. Nous le créons avec la profondeur maximale, la taille maximale du tampon et la profondeur de la canopée souhaitées, comme indiqué dans la section précédente. Ce compte appartient au programme Account Compression, créé et maintenu par Solana. Il sert à vérifier l’authenticité des NFT compressés.
Le compte de configuration de l’arbre est un PDA dérivé de l’adresse du compte de l’arbre de Merkle concurrent. Il sert à stocker des configurations supplémentaires, comme le créateur de l’arbre et le nombre de NFT compressés émis.
Metaplex désigne les arbres de Merkle concurrents associés à un compte de configuration d’arbre sous le nom de « Bubblegum tree ».
Code complet
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}`);
}
}Décomposition du code
Voici un exemple de fonction permettant de créer un arbre de Merkle concurrent sur Solana. Pour appeler cette fonction d’exemple, createTree, les paramètres suivants doivent être transmis :
connection- une connexion à un endpoint JSON RPC de nœud complet, de typeConnectionpayer- le compte qui paiera la transaction, de typeKeypairtreeKeypair- la paire de clés correspondant à l’adresse de l’arbre, de typeKeypairmaxDepthSizePair- la paire validemaxDepthetmaxBufferSize, de typeValidDepthSizePaircanopyDepth- la profondeur de la canopée de l’arbre, de typenumberet définie par défaut sur0
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";Nous importons d’abord @solana/web3.js, @solana/spl-account-compression et @metaplex-foundation/mpl-bubblegum avec les modules nécessaires.
const createTree = async (
connection: Connection,
payer: Keypair,
treeKeypair: Keypair,
maxDepthSizePair: ValidDepthSizePair,
canopyDepth: number = 0,
) => {
// Rest of the code
}Ici, nous définissons la fonction createTree avec les paramètres précédemment mentionnés.
const allocTreeInstruction = await createAllocTreeIx(
connection,
treeKeypair.publicKey,
payer.publicKey,
maxDepthSizePair,
canopyDepth,
);createAllocTreeIx est une fonction utilitaire que nous utilisons pour créer le compte de l’arbre de Merkle concurrent. Le package SPL Account Compression recommande d’utiliser cette méthode pour initialiser un compte d’arbre de Merkle concurrent, car ces comptes sont généralement assez volumineux et peuvent dépasser la limite de ce qui peut être alloué via CPI. Ici, nous créons l’instruction qui alloue le compte de l’arbre on-chain. Elle calcule également l’espace nécessaire au stockage de l’arbre on-chain ainsi que son coût, ce qui nous évite de devoir nous en préoccuper par la suite.
const [treeAuthority, ] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
PROGRAM_ID,
);Nous devons dériver le compte de configuration de l’arbre avec une autorité détenue par le programme Bubblegum. Cela est nécessaire pour createCreateTreeInstruction, l’instruction qui crée l’arbre, car nous devons transmettre treeAuthority comme argument. Ici, nous dérivons le PDA avec la méthode findProgramAddressSync, à partir de la clé publique de l’arbre et de l’ID du programme Bubblegum. Nous devons déstructurer treeAuthority, car l’autorité et le bump sont tous deux renvoyés. J’ai omis le bump, car notre fonction n’en a pas besoin. Si nécessaire, modifiez la déstructuration en [treeAuthority, bump] pour enregistrer le 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,
);Nous utilisons createCreateTreeInstruction du SDK Bubblegum pour construire l’instruction qui crée l’arbre de Merkle concurrent. L’arbre est ainsi créé on-chain avec le programme Bubblegum comme propriétaire. createCreateTreeInstruction accepte trois paramètres. Le premier est un objet contenant les comptes nécessaires pour définir des propriétés comme le créateur de l’arbre. Le deuxième objet concerne la profondeur maximale et la taille maximale du tampon. Il comprend également un paramètre public de type boolean. Définir public sur true permet à tout le monde d’émettre des NFT compressés depuis l’arbre. Dans le cas contraire, seuls le créateur ou le délégué de l’arbre pourront émettre des NFT compressés depuis celui-ci. Un compte délégué peut effectuer des actions au nom du propriétaire de l’arbre, comme transférer ou brûler un NFT compressé. À titre indicatif, vous pouvez désigner un délégué de l’arbre à l’aide de createSetTreeDelegateInstruction du package @metaplex-foundation/mpl-bubblegum comme suit :
const changeTreeDelegateTransaction = createSetTreeDelegateInstruction({
merkleTree: treeKeypair.publicKey
newTreeDelegate: ,
treeAuthority,
treeCreator: treeCreator.publicKey // which in our script would be payer.publicKey
});Nous transmettons également l’ID du programme Bubblegum. Revenons maintenant au reste de notre code :
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}`);
}Nous ajoutons les deux instructions que nous venons de construire à une transaction, puis nous l’envoyons. Nous veillons à ce que treeKeypair et payer signent tous deux la transaction. La signature de la transaction réussie est ensuite enregistrée dans la console. Nous plaçons ce processus dans un bloc try-catch afin que toute erreur éventuelle soit consignée dans la console via console.error.
Création d’un arbre de Merkle concurrent avec Umi
L’utilisation conjointe du SDK Bubblegum, du programme de compression de comptes de Solana et des packages web3.js de Solana peut être assez déroutante pour les nouveaux développeurs et fastidieuse à configurer à chaque fois. Heureusement, le SDK Bubblegum fournit une opération createTree qui gère tout pour nous et s’intègre parfaitement à Umi. Voici le code :
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 est un framework modulaire permettant de créer et d’utiliser des clients JavaScript pour les programmes Solana. Il fournit une bibliothèque sans dépendance dotée d’un ensemble d’interfaces fondamentales sur lesquelles d’autres bibliothèques peuvent s’appuyer sans être limitées à une implémentation spécifique. Umi est fourni par Metaplex et sa documentation est disponible ici.
Nous utilisons notre instance Umi pour générer un signataire, créer notre arbre de Merkle, puis envoyer et confirmer la transaction construite. Par défaut, le créateur de l’arbre correspond à l’identité Umi et le paramètre public est défini sur false. Ces paramètres peuvent être personnalisés afin de transmettre également un créateur d’arbre personnalisé et la valeur publique true. C’est un moyen bien plus rapide de créer un arbre de Merkle concurrent on-chain.
Notez que Bubblegum ne tient pas compte de la taille de la canopée. En effet, le programme Account Compression de Solana détermine cette taille en fonction de l’espace disponible dans le compte. Il suffit d’allouer assez d’espace pour que le programme puisse déterminer avec précision la taille de canopée appropriée.
Émission de cNFT en interagissant directement avec Bubblegum
Création d’une collection
Les NFT sont traditionnellement regroupés dans une collection selon la norme Metaplex. Cela s’applique aussi bien aux NFT compressés qu’aux NFT « classiques ». Pour créer une collection :
- Créez un nouveau « mint » de token
- Créez un compte de token associé au mint
- Émettez un seul token
- Stockez les métadonnées de la collection dans un compte on-chain
Bien que cela ne soit pas directement lié à la compression d’état ou aux NFT compressés, et dépasse donc le cadre de cet article, un script est fourni comme référence pour créer votre propre collection. Vous pouvez accéder à ce script ici.
Émission d’un NFT dans notre collection
Une fois votre nouvelle collection créée, vous aurez besoin des éléments suivants pour commencer l’émission :
collectionMint- l’adresse de mint de la collectioncollectionAuthority- le compte qui détient l’autorité sur la collectioncollectionMetadata- le compte de métadonnées de la collectioneditionAccount- le compte qui contient des attributs supplémentaires, comme un compte d’édition principale
Code complet pour l’émission dans une collection
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}`);
}
}Décomposition du processus d’émission
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";Nous importons d’abord @solana/web3.js, @solana/spl-account-compression, @metaplex-foundation/mpl-bubblegum et @metaplex-foundation/mpl-token-metadata avec les modules nécessaires.
export async function mintCompressedNFT(
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
collectionMint: PublicKey,
collectionMetadata: PublicKey,
collectionMasterEditionAccount: PublicKey,
compressedNFTMetadata: MetadataArgs,
receiverAddress?: PublicKey
) {
// Rest of the code
}Nous définissons mintCompressedNFT, qui accepte un certain nombre de paramètres :
connection- l’objet de connexion utilisé pour interagir avec Solanapayer- le compte qui paiera les frais de transactiontreeAddress- le compte de l’arbre de Merkle concurrentcollectionMint- l’adresse de mint de la collectioncollectionMetadata- le compte de métadonnées de la collectioncollectionMasterEditionAccount- le compte d’édition principalecompressedNFTMetadata- les métadonnées propres au cNFT à émettrereceiverAddress- une adresse de clé publique facultative à laquelle le cNFT nouvellement émis sera envoyé
const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);
const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
[Buffer.from("collection_cpi", "utf8")],
BUBBLEGUM_PROGRAM_ID
);Ici, nous recherchons les PDA nécessaires et ignorons leurs bumps. Nous dérivons d’abord le PDA de l’autorité de l’arbre, puis un autre PDA qui servira de signataire pour l’émission compressée. Nous devons inclure collection_cpi, car il s’agit d’un préfixe personnalisé requis par le programme Bubblegum.
const mintInstructions: TransactionInstruction[] = [];Nous définissons mintInstructions sur un tableau vide de TransactionInstruction. Cela nous permet, si nécessaire, d’émettre plusieurs cNFT simultanément.
const metadataArgs = Object.assign(compressedNFTMetadata, {
collection: { key: collectionMint, verified: false },
});metadataArgs vérifie que compressedNFTMetadata est correctement formaté. Pour que la transaction réussisse lors de l’émission d’un NFT dans une collection avec createMintToCollectionV1Instruction, le champ verified doit être défini sur false, même si l’instruction vérifie automatiquement la collection.
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,
}
)
);Nous ajoutons une seule émission à notre instruction. Nous pourrions en ajouter plusieurs dans la même transaction, tant que celle-ci respecte les limites de taille en octets. Ici, nous utilisons createMintToCollectionV1Instruction pour émettre notre NFT compressé depuis notre collection. Cette instruction accepte deux objets : l’un contient les comptes nécessaires à son traitement et l’autre fournit les données de l’instruction au programme. La plupart de ces paramètres devraient vous être familiers grâce aux sections précédentes. Notez que vous pouvez définir n’importe quelle adresse de délégué lors de l’émission, mais qu’elle devrait normalement être identique à leafOwner. Dans tous les cas, le délégué est automatiquement supprimé lors du transfert du cNFT. Nous définissons le payeur comme délégué, car c’est également lui qui recevra le cNFT si aucun receiverAddress n’est fourni.
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}`);
}Nous construisons ensuite notre transaction, définissons payer comme feePayer, puis envoyons la transaction. Nous plaçons cette logique dans un bloc try-catch afin de gérer toute erreur lors de l’envoi et de la confirmation de la transaction. Si une erreur survient, nous la consignons dans la console avec console.error.
Émission de cNFT avec Umi
Le programme Bubblegum propose deux processus d’émission via Umi :
- Émettre un NFT sans l’associer à une collection
- Émettre un NFT dans une collection donnée.
Émission sans collection
L’instruction MintV1 de Bubblegum permet d’émettre des NFT compressés depuis un Bubblegum Tree sans collection. Si l’arbre est public, tout le monde peut y émettre des NFT. Dans le cas contraire, seuls le créateur ou le délégué de l’arbre peuvent utiliser cette instruction. Voici comment émettre un NFT compressé sans collection :
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);Cet extrait de code provient de la documentation Metaplex sur l’émission de cNFT avec Bubblegum. Ici, nous utilisons une instance d’Umi pour émettre un cNFT. Les autres paramètres de l’instruction mintV1 sont les suivants :
leafOwnerest le propriétaire du cNFT à émettremerkleTreeest l’adresse du compte de l’arbre de Merkle concurrent depuis lequel le cNFT sera émismetadataest un objet contenant les métadonnées du cNFT à émettre. Cela comprend le nom du cNFT, son URI, sa collection, que nous avons définie sur none, ainsi que ses créateurs. Il est possible de fournir un objet de collection, mais le champ verified des créateurs doit être défini surfalse, car l’autorité de la collection n’est pas demandée dans l’instruction. Les créateurs peuvent également se vérifier eux-mêmes en définissant le champ verified sur true et en ajoutant le créateur comme signataire dans les comptes restants.
L’instruction mintV1 contient également plusieurs champs facultatifs, car l’entrée de la fonction est de type MintV1InstructionAccounts & MintV1InstructionArgs. Ces types sont définis comme suit :
// 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 est un type difficile à repérer parmi les autres types, qui se résume à un objet doté d’un champ de métadonnées. Ce champ de métadonnées est de type MetadataArgsArgs et est défini comme suit :
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;
};La définition complète de la fonction mintV1 et de tous les types associés est disponible ici. Toutefois, avec au minimum une instance d’Umi, vous pouvez émettre un cNFT sans collection en transmettant les métadonnées requises, le propriétaire de la feuille et le compte de l’arbre de Merkle concurrent.
Émission avec une collection
Bubblegum fournit mintToCollectionV1 comme moyen pratique d’émettre directement un cNFT dans une collection donnée. L’entrée de cette instruction est de types MintToCollectionV1InstructionAccounts et MintToCollectionV1InstructionArgs, ce qui correspond au final à un objet de type MetadataArgsArgs. La définition du type MintToCollectionV1InstructionAccounts est la suivante :
// 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;
};Les paramètres clés sont le mint de la collection, l’autorité de la collection et le PDA d’enregistrement de l’autorité de la collection. Un PDA d’enregistrement de délégation doit être fourni lorsqu’une autorité de collection déléguée est utilisée, afin de garantir que cette autorité est autorisée à gérer le NFT de la collection. Le paramètre de métadonnées doit contenir un objet de collection dont le champ address correspond au paramètre de mint de la collection et dont le champ verified est défini sur false. Les créateurs peuvent également se vérifier eux-mêmes en signant la transaction et en s’ajoutant aux comptes restants.
Voici comment émettre un NFT compressé dans une collection :
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);Cet extrait de code est disponible dans la documentation Metaplex sur l’émission de cNFT avec Bubblegum. Là encore, nous utilisons une instance d’Umi pour émettre le NFT compressé. Comme avec mintV1, nous transmettons leafOwner et merkleTree. Cette fois, nous transmettons toutefois collectionMint. Dans le champ de métadonnées, nous transmettons un objet collection dont la clé correspond à collectionMint et dont le champ verified est défini sur false. Notez que l’identité Umi est définie comme autorité de collection par défaut. Vous pouvez modifier ce comportement en définissant le champ facultatif collectionAuthority sur une autorité de collection personnalisée.
Mintrer des cNFT avec Helius
Chez Helius, nous proposons une API Mint qui vous permet de mintrer des NFT compressés sans complications supplémentaires. Nous prenons en charge les frais Solana et la création de l’arbre de Merkle, et nous téléversons vos métadonnées hors chaîne vers Arweave. Nous vérifions également que la transaction a bien été soumise et confirmée par le réseau, pour vous éviter d’avoir à l’interroger vous-même. Enfin, nous extrayons l’identifiant de l’actif depuis la transaction afin que vous puissiez l’utiliser immédiatement avec l’API DAS.
Pour que Helius puisse mintrer un NFT dans votre collection, l’autorité de la collection doit lui être déléguée. Selon votre cluster, l’autorité doit être déléguée à l’un des comptes suivants :
- Devnet :
2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC - Mainnet :
HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R
Voici comment mintrer un cNFT avec l’API Mint de Helius :
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();Cet extrait de code et une description plus détaillée du schéma de la requête sont disponibles dans notre documentation.
Notez que si vous ne renseignez pas le champ uri, nous créerons un fichier JSON et le téléverserons vers Arweave pour vous. Le fichier respectera la norme JSON Metaplex v1.0 et sera téléversé via Irys (anciennement Bundlr).
Transférer des cNFT
Voici les étapes générales du transfert d’un NFT compressé :
- Récupérer les données de l’actif cNFT depuis l’indexeur
- Récupérer la preuve du cNFT depuis l’indexeur
- Récupérer le compte de l’arbre de Merkle concurrent depuis Solana
- Préparer la preuve de l’actif
- Créer et envoyer la transaction de transfert
Umi et Metaplex simplifient considérablement ce processus, mais cette section explique ce qui se passe en coulisses. Nous verrons ci-dessous comment transférer un NFT compressé avec web3.js et Metaplex.
Transfert en interagissant directement avec Bubblegum
Avant d’utiliser notre script pour exécuter le transfert, nous devons récupérer certaines informations sur notre NFT compressé. Nous devons d’abord utiliser la méthode getAsset de l’API DAS afin de récupérer les métadonnées du NFT compressé. Nous recherchons ici data_hash, creator_hash, owner, delegate et 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();Voici à quoi ressemblera une partie de la réponse en cas de réussite :
{
...
},
"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",
...
}
}Une fois les informations nécessaires obtenues, nous devons utiliser la méthode getAssetProof pour récupérer proof et tree_id (l’adresse de l’arbre). Voici un exemple d’appel :
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();Voici à quoi ressemblera la réponse en cas de réussite :
{
"root": "string",
"proof": [
"string"
],
"node_index": 0,
"leaf": "string",
"tree_id": "string"
}Maintenant que nous disposons de la racine, de la preuve et de tree_id, nous pouvons passer à notre script de transfert.
Code complet
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}`);
}
};Analyse du code
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";Nous importons @solana/web3.js, @metaplex-foundation/mpl-bubblegum et @solana/spl-account-compression avec les modules nécessaires.
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
}Nous définissons notre fonction transferCompressedNFT, dans laquelle nous analyserons le chemin de preuve, créerons l’instruction de transfert et l’exécuterons.
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);
const treeAuthority = treeAccount.getAuthority();
const canopyDepth = treeAccount.getCanopyDepth();Nous récupérons le compte de l’arbre de Merkle concurrent depuis la blockchain et en extrayons l’autorité de l’arbre ainsi que la profondeur de la canopée. Ces valeurs sont nécessaires pour créer l’instruction de transfert.
const proofPath: AccountMeta[] = proof
.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}))
.slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));Pour faire simple, nous convertissons la liste des adresses de preuve en un tableau valide de type AccountMeta. AccountMeta correspond aux métadonnées de compte utilisées pour définir les transactions. Elles comprennent la clé publique du compte, indiquent si une instruction nécessite une signature de transaction correspondant à la clé publique et précisent si la clé publique peut être chargée comme compte en lecture-écriture.
Nous prenons une portion de notre preuve complète à partir du début du tableau et veillons à ne conserver que proof.length - canopyDepth valeurs de preuve. Cela permet de supprimer la partie de l’arbre déjà mise en cache dans la canopée on-chain. Nous structurons ensuite chaque valeur de preuve restante sous la forme d’un AccountMeta valide. Cette opération est nécessaire, car la preuve est soumise on-chain sous forme de « comptes supplémentaires » dans l’instruction de transfert.
const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);Nous définissons ensuite leafOwner avec le paramètre owner et leafDelegate avec le paramètre 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
);Nous créons transferInstruction à l’aide de la fonction utilitaire createTransferInstruction du SDK Bubblegum. Notez que root, dataHash et creatorHash sont renvoyés par l’API DAS sous forme de chaînes de caractères. Nous devons donc les convertir en type PublicKey, puis en tableau d’octets.
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}`);
}Une fois notre instruction créée, nous l’ajoutons à une nouvelle transaction et l’envoyons à Solana. En cas d’erreur, nous l’affichons dans la console avec console.error.
Si vous rencontrez des erreurs liées à l’arbre de Merkle concurrent, il est possible que votre RPC fournisse des données obsolètes ou incorrectes pour la preuve de cet arbre. Cela peut parfois se produire en raison de problèmes de cache. Pour y remédier, vous pouvez essayer de vérifier côté client la preuve fournie par le 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());Notez que vous devrez également utiliser la valeur leaf renvoyée par notre appel getAssetProof à l’API DAS. Ce n’est pas obligatoire, car la validation effective de la preuve est effectuée on-chain. Cela peut toutefois faciliter la gestion des erreurs.
Vous pouvez alors effectuer un nouvel appel getAsset pour vérifier que leafDelegate est vide et que la feuille a un nouveau propriétaire !
Transfert avec 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);Ce code provient de la documentation de Metaplex sur le transfert de NFT compressés.
Bubblegum fournit une instruction transfer très simple à utiliser. Elle reçoit d’abord une instance d’Umi, puis un objet contenant l’actif avec les informations sur sa preuve, le propriétaire de la feuille et le nouveau propriétaire de la feuille. Pour obtenir l’actif avec la preuve requise, nous pouvons utiliser la méthode getAssetWithProof, également fournie par Bubblegum. Notez que le délégué de la feuille peut remplacer son propriétaire : seul un compte disposant de l’autorité nécessaire pour autoriser le transfert est requis. Avec la méthode .sendAndConfirm(), nous envoyons la transaction qui lance le transfert, puis nous la confirmons avec notre instance d’Umi.
Conclusion
Félicitations ! Nous avons exploré en détail la compression d’état et les NFT compressés sur Solana. Nous avons abordé les complexités des arbres de Merkle concurrents, clarifié les idées reçues courantes et plongé au cœur du registre de Solana. Au-delà de la théorie, nous avons appris à récupérer, mintrer et transférer des cNFT en exploitant toute la puissance de web3.js de Solana, de Metaplex et de Helius !
La compression d’état de Solana est révolutionnaire dans un environnement où les coûts de transaction et de stockage peuvent être prohibitifs. La compression réduit considérablement les coûts sans compromettre la sécurité ni la décentralisation. Ce changement de paradigme ouvre des possibilités inédites aux artistes, aux collectionneurs et aux développeurs.
Si vous êtes arrivé jusqu’ici, merci, anon ! Vous êtes désormais parfaitement équipé pour contribuer à cette nouvelle frontière passionnante. Lancez-vous : mintrez une collection de dix millions de NFT pour votre MMORPG on-chain, créez une application décentralisée qui exploite la puissance du registre ou partagez simplement vos nouvelles connaissances avec la communauté. La meilleure façon de prédire l’avenir est de le créer.
Ressources supplémentaires / Pour aller plus loin
- Documentation de Solana sur la compression d’état
- Programme de compression de comptes
- Documentation de Metaplex sur Bubblegum
- Les NFT compressés sur Solana sont l’avenir • Explications de Helius
- Étude de cas sur Dialect et les cNFT
- Angela : un arbre de Merkle creux, distribué et hautement concurrent
- Implémentation C++ parallélisée d’un arbre de Merkle
Articles associés
Abonnez-vous à Helius
Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication


