
Alles, was du über Komprimierung auf Solana wissen musst
Inhaltsverzeichnis
- Worum geht es in diesem Artikel?
- Häufige Missverständnisse
- Komprimierung auf Solana entspricht herkömmlicher Komprimierung
- Komprimierte Daten off-chain zu speichern ist riskant und führt zu Sicherheitslücken
- Ich kann meinen nebenläufigen Merkle-Baum verlieren, wenn der Indexer oder RPC-Anbieter ausfällt, bei dem ich meinen Baum speichere
- Nebenläufige Merkle-Bäume können parallele Aktualisierungen verarbeiten
- Ein Baum ist dasselbe wie eine Collection
- Was ist Zustandskomprimierung?
- Zustand vs. Ledger
- Was sind komprimierte NFTs?
- Metadaten komprimierter NFTs mit der DAS API lesen
- Größe und Kosten eines nebenläufigen Merkle-Baums
- Größe berechnen
- Kosten berechnen
- Einen nebenläufigen Merkle-Baum erstellen
- Vollständiger Code
- Aufschlüsselung des Codes
- Einen nebenläufigen Merkle-Baum mit Umi erstellen
- cNFTs durch direkte Interaktion mit Bubblegum minten
- Eine Collection erstellen
- Ein NFT für unsere Collection minten
- Vollständiger Code zum Minten für eine Collection
- Aufschlüsselung des Minting-Prozesses
- cNFTs mit Umi minten
- Ohne Collection minten
- Mit einer Collection minten
- cNFTs mit Helius minten
- cNFTs übertragen
- Übertragung durch direkte Interaktion mit Bubblegum
- Vollständiger Code
- Aufschlüsselung des Codes
- Übertragung mit Umi
- Fazit
- Zusätzliche Ressourcen und weiterführende Literatur
Worum geht es in diesem Artikel?
Würdest du mir glauben, wenn ich dir sage, dass du jetzt eine Million NFTs für weniger als 150 USD minten könntest? Absurd! Je nach Blockchain würde es mehr als eine Million Dollar kosten, so viele NFTs zu minten! Oder etwa nicht?
Zustandskomprimierung ist ein neuartiges Primitive, das Merkle-Bäume und Solanas Ledger nutzt, um die Speicherkosten drastisch zu senken. Gleichzeitig übernimmt es die Sicherheit und Dezentralisierung der Solana-Basisschicht. Dieser Artikel bietet dir einen umfassenden Einblick in die Komprimierung auf Solana. Er behandelt alles – von häufigen Missverständnissen bis zur Übertragung komprimierter NFTs. Wenn du mehr über Zustandskomprimierung erfahren und lernen möchtest, wie du komprimierte NFTs abrufst, mintest oder überträgst, ist dies der einzige Artikel, den du für den Einstieg brauchst.
Dieser Artikel setzt voraus, dass du bereits unseren Artikel Kryptografische Werkzeuge 101 – Hash-Funktionen und Merkle-Bäume erklärt gelesen hast. Lies ihn unbedingt vor diesem Artikel, da wir Kenntnisse über Merkle-Bäume voraussetzen. Dieser Artikel baut außerdem auf nebenläufigen Merkle-Bäumen auf und behandelt ihre Dimensionierung und Erstellung ausführlicher.
Dieser Artikel verwendet sowohl das Bubblegum SDK als auch Umi, um verschiedene Ansätze zum Erstellen nebenläufiger Merkle-Bäume sowie zum Minten und Übertragen komprimierter NFTs zu zeigen. Kenntnisse beider Tools sind hilfreich, da dir beide in verschiedenen Codebasen begegnen werden. Das Bubblegum SDK ist speziell für Lernzwecke enthalten, weil sein Workflow die zugrunde liegenden Mechanismen transparenter macht. Umi bietet dagegen einen kompakteren Workflow, der diese Prozesse vereinfacht.
Häufige Missverständnisse
Bevor wir uns mit Zustandskomprimierung und den Feinheiten komprimierter NFTs befassen, müssen wir einige Dinge klarstellen:
Komprimierung auf Solana entspricht herkömmlicher Komprimierung
Das ist falsch. Herkömmliche Komprimierung reduziert die Größe von Dateien und Daten. Ihr Hauptziel besteht darin, Daten mit weniger Bits zu speichern oder zu übertragen als in der ursprünglichen Datei. Es gibt zwei allgemeine Arten von Komprimierungsalgorithmen:
- Verlustfreie Komprimierung, bei der sich die ursprünglichen Daten aus den komprimierten Daten rekonstruieren lassen
- Verlustbehaftete Komprimierung, bei der „weniger wichtige“ Informationen entfernt werden, um die Dateigröße zu reduzieren
Ein komprimiertes NFT ist kein NFT, dessen Daten durch einen verlustfreien oder verlustbehafteten Komprimierungsalgorithmus verkleinert wurden. Es geht auch nicht darum, die Qualität oder Abmessungen der mit dem NFT verbundenen Kunst, Musik oder Metadaten zu reduzieren. Im Kontext von Solana hat das Konzept eine völlig andere Bedeutung. Es geht vielmehr darum, zu optimieren, wie das zugrunde liegende Blockchain-Ledger Informationen zu diesem NFT speichert. Aus Account-Sicht komprimieren wir diese Informationen in das Ledger, indem wir mehrere Accounts – in diesem Fall NFTs – in einer einzigen Merkle-Root bündeln, die im Zustand gespeichert wird. Dieser Prozess senkt die Speicherkosten erheblich und erhält zugleich die Verifizierbarkeit.
Komprimierte Daten off-chain zu speichern ist riskant und führt zu Sicherheitslücken
Das ist falsch – du kannst Daten sicher off-chain speichern, indem du sie hashst und ihre Merkle-Root on-chain speicherst. Technisch gesehen werden komprimierte NFTs nicht off-chain gespeichert. Die Daten befinden sich weiterhin on-chain, da alles als on-chain gilt, was sich aus dem Ledger erneut ableiten lässt. Der Unterschied besteht darin, dass der Zustand durch Anreize so gestaltet ist, dass Validatoren Accounts im Arbeitsspeicher halten, während der Zugriff auf das Ledger über Archiv-Nodes erfolgen muss. Zustandskomprimierung verbindet beides und ermöglicht es, Ledger-Daten über den Zustand in einem Account zu verifizieren. Dabei bleiben die Sicherheit und Dezentralisierung von Solana selbst erhalten. In einem späteren Abschnitt erklären wir, was das Ledger ist und warum es sicher ist.
Ich kann meinen nebenläufigen Merkle-Baum verlieren, wenn der Indexer oder RPC-Anbieter ausfällt, bei dem ich meinen Baum speichere
Du verlierst deinen Baum nicht – jeder mit Zugriff auf das Ledger kann den gesamten Baum rekonstruieren, indem er dessen Verlauf wiedergibt.
Nebenläufige Merkle-Bäume können parallele Aktualisierungen verarbeiten
Ein häufiges Missverständnis ist, dass das Wort „nebenläufig“ bedeutet, mehrere Aktualisierungen eines On-chain-Merkle-Baums könnten parallel erfolgen. Nebenläufige Merkle-Bäume können zwar mehrere Blätter innerhalb desselben Blocks ersetzen, Validatoren verarbeiten diese Aktualisierungen jedoch nacheinander. Wenn ein Validator einen Batch von Transaktionen empfängt, die einen nebenläufigen On-chain-Merkle-Baum betreffen, kann er sie im selben Slot verarbeiten. Die Daten pro Slot werden jedoch nicht gleichzeitig erzeugt. Darauf gehen wir im folgenden Abschnitt Was ist Zustandskomprimierung? näher ein.
Ein Baum ist dasselbe wie eine Collection
Nebenläufige Merkle-Bäume sind nicht dasselbe wie eine Collection. Eine einzelne Collection kann beliebig viele nebenläufige Merkle-Bäume verwenden. Gruppierungen von NFTs können unabhängig von ihrer Speicherung sein. NFTs können sich in Accounts befinden oder in das Ledger komprimiert sein – verteilt über beliebig viele Bäume, einen oder mehrere. Dennoch empfiehlt es sich, nebenläufige Merkle-Bäume jeweils nur für eine Collection zu verwenden, um die Komplexität zu reduzieren.
Was ist Zustandskomprimierung?
Zustandskomprimierung optimiert die Speicherung, indem sie einen kryptografischen Hash von Ledger-Daten erzeugt und diesen Hash in einem Account speichert. Dieser Ansatz nutzt die inhärente Sicherheit und Unveränderlichkeit des Ledgers und bietet zugleich ein robustes Framework zur Verifizierung der darin gespeicherten Daten.
Das ist eine kostengünstige Lösung für Anwendungen, die auf Solana aufbauen. Entwickler können jetzt Speicherplatz im Ledger statt des teureren Account-basierten Speichers nutzen. Zustandskomprimierung stellt daher nicht nur die Datenintegrität sicher, sondern ermöglicht auch eine kostengünstige Ressourcenzuweisung auf Solana.
Das Geheimnis hinter Solanas Zustandskomprimierung ist der Einsatz nebenläufiger Merkle-Bäume. Nebenläufige Merkle-Bäume sind darauf optimiert, mehrere Transaktionen schnell hintereinander zu verarbeiten, sodass sich ihre Proofs vorspulen lassen. Das unterscheidet sie von herkömmlichen Merkle-Bäumen, deren Proofs bei jeder Aktualisierung ungültig werden. Nebenläufige Merkle-Bäume speichern ein sicheres Änderungsprotokoll ihrer neuesten Änderungen zusammen mit ihrem Root-Hash und dem für dessen Ableitung erforderlichen Proof. Dieses Änderungsprotokoll wird on-chain in einem Account gespeichert, der dem Baum zugeordnet ist. Jeder nebenläufige Merkle-Baum hat eine maximale Puffergröße. Dieser Wert gibt die höchste Anzahl an Änderungen an, die am Baum vorgenommen werden können, während seine Merkle-Root weiterhin gültig bleibt. Stell dir diesen Wert als Maß dafür vor, wie „veraltet“ eine berechnete Menge von Proofs sein darf, bevor sie aktualisiert werden muss.
Wenn ein Validator also innerhalb desselben Slots mehrere Anfragen zur Aktualisierung eines On-chain-Merkle-Baums erhält, kann er das Änderungsprotokoll des Baums als maßgebliche Datenquelle verwenden. Dadurch sind bis zur maximalen Puffergröße entsprechend viele nebenläufige Änderungen am Merkle-Baum möglich. Das reduziert zwar nicht direkt die Menge der on-chain gespeicherten Daten, erhöht aber die Effizienz, da sich mehrere Aktualisierungen gleichzeitig verarbeiten lassen. So kann das System die von Merkle-Bäumen gebotene Integrität des „Inklusionsnachweises“ auch in einer Umgebung mit hohem Durchsatz aufrechterhalten. Inklusionsnachweis bedeutet hier lediglich, belegen zu können, dass ein bestimmtes Datenelement tatsächlich zu einer Datenmenge gehört, die gemeinsam zu einer Merkle-Root gehasht wurde.
Diese geniale Kombination aus Zustandskomprimierung und nebenläufigen Merkle-Bäumen bietet eine äußerst kostengünstige Lösung für Anwendungen, die auf Solana aufbauen. Um die Auswirkungen dieser Technologien vollständig zu verstehen, müssen wir den Unterschied zwischen Solanas Zustand und seinem Ledger betrachten.
Zustand vs. Ledger
Das Ledger ist ein historischer Datensatz aller von Clients signierten Transaktionen, die seit Solanas Genesis-Block stattgefunden haben. Es ist eine reine Append-Datenstruktur. Das bedeutet, dass eine Transaktion nach dem Hinzufügen weder geändert noch entfernt werden kann. Validatoren validieren die Transaktionen, die dem Ledger hinzugefügt werden. Mehrere Nodes im Netzwerk speichern das Ledger, um Fehlertoleranz sicherzustellen. Die Ledger-Kopie eines Validators enthält jedoch möglicherweise nur neuere Blöcke, um Speicherplatz zu sparen, da ältere Blöcke für die Validierung zukünftiger Blöcke nicht erforderlich sind.
Der Zustand bildet den aktuellen Snapshot aller Accounts und Programme auf Solana ab. Er ist veränderlich und ändert sich bei der Verarbeitung von Transaktionen. Stell dir den Zustand als hochoptimierte Datenbank vor, die sich nach Token-Guthaben, Programmen und Accounts abfragen lässt.
So kannst du beide einfach unterscheiden: Alice hat ein Guthaben von 100 SOL und Bob ebenfalls. Alice sendet eine Transaktion, um Bob 10 SOL zu geben. Nach der Verifizierung wird die Transaktion einem Block hinzugefügt und der Block an das Ledger angehängt. Das Ledger enthält jetzt einen unveränderlichen Datensatz, laut dem Alice 10 SOL an Bob gesendet hat. Gleichzeitig aktualisiert der Zustand die Accounts von Alice und Bob auf 90 beziehungsweise 110 SOL.
Die wichtigsten Unterschiede lassen sich wie folgt zusammenfassen:
- Das Ledger ist unveränderlich und wird ausschließlich ergänzt, während der Zustand veränderlich ist und sich ständig ändert
- Das Ledger ist ein historischer Datensatz aller Transaktionen, während der Zustand den aktuellen Status aller Accounts und Programme widerspiegelt
- Das Ledger dient zur Verifizierung, während der Zustand zur Ausführung von Transaktionen und Programmen verwendet wird
Das Ledger dient als unveränderlicher historischer Datensatz und stellt sicher, dass jede Transaktion verifizierbar und nachvollziehbar ist. Der Zustand fungiert dagegen als dynamischer Snapshot des Ledgers und passt sich an Echtzeitvorgänge wie Übertragungen und Programmausführungen an. Wichtig ist, dass beide dem Konsens der Chain selbst unterliegen. Zusammen bilden Zustand und Ledger das Rückgrat von Solana. Dadurch kann Solana effizient arbeiten und zugleich dezentrales Vertrauen gewährleisten.
Was sind komprimierte NFTs?
Komprimierte NFTs (cNFTs) nutzen Zustandskomprimierung und nebenläufige Merkle-Bäume, um Speicherkosten zu reduzieren. Statt jedes NFT in einem typischen Solana-Account zu speichern, legen komprimierte NFTs ihre Metadaten im Ledger ab. Das senkt die Speicherkosten und übernimmt zugleich die Sicherheit und Unveränderlichkeit des Ledgers.
Komprimierte NFTs folgen weiterhin exakt demselben Metadatenschema wie ihre unkomprimierten Gegenstücke. NFTs und cNFTs werden daher auf dieselbe Weise definiert.
Die wichtigsten Unterschiede zwischen NFTs und cNFTs sind:
- Ein komprimiertes NFT kann in ein reguläres NFT umgewandelt werden, ein reguläres NFT jedoch nicht in ein komprimiertes NFT
- Komprimierte NFTs sind keine nativen Solana-Token – sie besitzen weder einen Token-Account noch einen Mint-Account oder Metadaten. Sie haben jedoch eine stabile Kennung, die Asset-ID. Nach der Dekomprimierung behält das NFT dieselbe Kennung. NFTs sind in ihrem komprimierten Zustand also keine nativen Token, können aber bei Bedarf in solche umgewandelt werden
- Ein einzelner Account eines nebenläufigen Merkle-Baums kann Millionen von NFTs enthalten
- Eine einzelne Collection kann sich über mehrere Baum-Accounts erstrecken
- Alle NFT-Änderungen erfolgen über das Bubblegum-Programm
- Zum Lesen von Informationen über ein komprimiertes NFT empfiehlt sich ein DAS API-Aufruf
Interessanterweise benötigen wir die DAS API, um Informationen über ein komprimiertes NFT abzurufen. Warum ist das so? Und noch wichtiger: Was ist die DAS API?
Metadaten komprimierter NFTs mit der DAS API lesen
Wir benötigen Indexer, da die Metadaten eines cNFTs im Ledger statt in einem herkömmlichen Account gespeichert werden. Du kannst den aktuellen Zustand eines komprimierten NFTs zwar ableiten, indem du relevante Transaktionen wiedergibst, aber Anbieter wie Helius übernehmen das für dich. Entwickler können die Digital Asset Standard (DAS) API verwenden. Diese Open-Source-Spezifikation und das zugehörige System rufen Informationen zu einem Asset ab. Die DAS API unterstützt sowohl komprimierte als auch herkömmliche, also unkomprimierte, NFTs. Du kannst daher für beide NFT-Typen denselben Endpoint verwenden.
Helius unterstützt derzeit die folgenden DAS API-Methoden:
getAsset– ein bestimmtes Asset anhand seiner ID abrufengetAssetBatch– mehrere Assets anhand ihrer IDs abrufengetAssetProof– einen Merkle-Proof für ein komprimiertes Asset anhand seiner ID abrufengetAssetProofBatch– mehrere Asset-Proofs anhand ihrer IDs abrufengetAssetsByOwner– eine Liste der Assets abrufen, die einer Adresse gehörengetAssetsByAuthority– eine Liste der Assets mit einer bestimmten Authority abrufengetAssetsByCreator– eine Liste der von einer Adresse erstellten Assets abrufengetAssetsByGroup– eine Liste der Assets anhand eines Gruppenschlüssels und -werts abrufensearchAssets– anhand verschiedener Parameter nach Assets suchengetSignaturesForAsset– eine Liste der Transaktionssignaturen abrufen, die mit einem komprimierten Asset zusammenhängen- Paginierung – Unterstützung für seitenbasierte und Keyset-Paginierung, um mehr als 1000 Datensätze auf einmal abzurufen
Weitere Informationen zu den einzelnen Methoden findest du in der Helius-Dokumentation zur DAS API. Wenn du beispielsweise eine Liste aller Assets abrufen möchtest, die einer Adresse gehören, kannst du mit getAssetsByOwner die folgende POST-Anfrage senden:
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();Komprimierte Assets lassen sich bequem abrufen. Aber was, wenn wir eigene erstellen möchten? Bevor wir mit dem Minten beginnen, müssen wir die Größe und die damit verbundenen Kosten eines nebenläufigen Merkle-Baums berechnen, in dem diese Assets gespeichert werden.
Größe und Kosten eines nebenläufigen Merkle-Baums
Größe berechnen
Wenn du on-chain einen nebenläufigen Merkle-Baum erstellst, bestimmen drei wichtige Kennzahlen die Größe des Baums, seine Erstellungskosten und die Anzahl der nebenläufigen Änderungen, die am Baum vorgenommen werden können, während die Merkle-Root gültig bleibt:
- Maximale Tiefe
- Maximale Puffergröße
- Canopy-Tiefe
Die maximale Tiefe bezeichnet die maximale Anzahl von Schritten, die erforderlich sind, um von einem beliebigen Blatt zur Root des Baums zu gelangen. Jedes Blatt ist nur mit einem anderen Blatt verbunden und bildet mit ihm ein Blattpaar für paarweises Hashing. Mit der folgenden Formel kannst du die maximale Anzahl an Blatt-Nodes berechnen, die ein Baum aufnehmen kann: numberOfNodes = 2 ^ maxDepth. Die Baumtiefe muss bei der Erstellung festgelegt werden. Verwende daher diese Formel, um die niedrigstmögliche maximale Tiefe zum Speichern deiner Daten zu ermitteln. Wenn du beispielsweise etwa 100 komprimierte NFTs in einem Baum speichern möchtest, reicht ein maxDepth von 7 aus, da 2^7 = 128 und 2^6 = 64. Die maximale Tiefe beeinflusst die Kosten beim Erstellen eines nebenläufigen On-chain-Merkle-Baums erheblich. Diese Kosten fallen bei der Erstellung des Baums im Voraus an und steigen mit höheren Werten für maxDepth.
Die maximale Puffergröße bezeichnet die maximale Anzahl an Änderungen, die an einem Baum vorgenommen werden können, während seine Merkle-Root gültig bleibt. Bei nebenläufigen Merkle-Bäumen wird der Puffer des Änderungsprotokolls bei der Baumerstellung anhand des Werts maxBufferSize dimensioniert und festgelegt. Wenn ein Validator also im selben Slot mehrere Änderungsanfragen für einen Baum erhält, kann er das Änderungsprotokoll verwenden und bis zu maxBufferSize Änderungen zulassen, während die Root weiterhin gültig bleibt.
Beachte unbedingt, dass für die Erstellung eines neuen Accounts für einen nebenläufigen Merkle-Baum nur eine bestimmte Anzahl gültiger Paare aus maxDepth und maxBufferSize existiert. Das Paket @solana/spl-account-compression exportiert die Konstante ALL_DEPTH_SIZE_PAIRS. Dabei handelt es sich um ein Array von Zahlen-Arrays, das alle gültigen Kombinationen enthält. Das Minimum ist ein maxDepth von 3 und ein maxBufferSize von 8. Das Maximum ist ein maxDepth von 30 und ein maxBufferSize von 2048.
Die Canopy-Tiefe bezeichnet einen Teil des Merkle-Baums, der in einem Account gespeichert wird. Diese zwischengespeicherten Proofs ergänzen die über das Netzwerk übertragenen Proofs, da diese Transaktionslimits unterliegen. Der vollständige Pfad ist erforderlich, um den ursprünglichen Besitz eines Blatts zu verifizieren, wenn du dessen Daten ändern möchtest, etwa beim Übertragen eines NFTs. Je größer die maximale Tiefe des Baums ist, desto mehr Proof-Nodes sind für die Verifizierung erforderlich. Das Canopy reduziert die Proof-Größe und vermeidet, dass für die Verifizierung des Baums eine Proof-Größe von maxDepth verwendet werden muss.
Du kannst die Canopy-Tiefe berechnen, indem du die gewünschte Proof-Größe von der maximalen Tiefe abziehst. Bei einer maximalen Tiefe von 14 und einer gewünschten Proof-Größe von 4 ergibt sich also eine Canopy-Tiefe von 10. Das bedeutet, dass du pro Aktualisierungstransaktion nur 4 Proof-Nodes übermitteln musst. Auch die Canopy-Tiefe beeinflusst die Kosten beim Erstellen eines nebenläufigen On-chain-Merkle-Baums erheblich. Diese Kosten fallen bei der Erstellung des Baums im Voraus an und steigen mit höheren Werten für canopyDepth. Ein niedrigeres canopyDepth senkt zwar die anfänglichen Kosten, kann aber die Komponierbarkeit einschränken. Das liegt daran, dass jede Aktualisierungstransaktion eine größere Proof-Größe benötigt, was durch die Limits der Transaktionsgröße eingeschränkt wird. Wenn dein Baum mit einem niedrigen canopyDepth beispielsweise für komprimierte NFTs verwendet wird, kann ein NFT-Marktplatz für deine Collection möglicherweise nur einfache Übertragungen unterstützen. Im Allgemeinen sollte maxDepth - canopyDepth für maximale Komponierbarkeit kleiner oder gleich 10 sein. Dies wird in Tensors Spezifikation der maximalen Proof-Länge für Tensor-cNFTs beschrieben.
Kosten berechnen
Es gibt verschiedene Methoden, um Größe und Kosten eines nebenläufigen Merkle-Baums zu bestimmen. Am einfachsten verwendest du den Rechner für komprimierte NFTs und gibst die Anzahl der komprimierten NFTs ein, die du in diesem Baum speichern möchtest:
Die Website zeigt detailliert die optimale Baumtiefe für die gewünschte Anzahl zu speichernder Assets sowie verschiedene Kostenoptionen auf Grundlage der Komponierbarkeit. Die Abbildung zeigt beispielsweise, dass ein hochgradig komponierbarer Baum für 10 Millionen komprimierte NFTs nur ~7,67 SOL kosten würde. Unter Berücksichtigung der Transaktionskosten von ~50 SOL für das Minten von 10 Millionen NFTs lägen die Gesamtkosten bei etwa ~57,67 SOL.
Entwickler können auch das Paket @solana/spl-account-compression verwenden, um den erforderlichen Speicherplatz für eine bestimmte Baumgröße und die Kosten für dessen On-chain-Zuweisung zu berechnen. Das funktioniert mit dem folgenden Skript:
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();Hier importieren wir die erforderlichen Module aus @solana/web3.js und @solana/spl-account-compression. Wir benötigen eine Verbindung zum Mainnet, die wir mit einem Helius API-Schlüssel herstellen können. Die Funktion calculateCosts gibt maxDepth, maxBufferSize, canopy, die Anzahl der NFTs, die in diesem Baum gespeichert werden können, sowie die Kosten von rent in SOL in der Konsole aus. Wenn wir calculateCosts mit unserer gewünschten Proof-Größe aufrufen, sehen wir daher alle möglichen Baumkombinationen in der Konsole.
Beachte, dass einige Logs möglicherweise Folgendes ausgeben: Mindestguthaben für Mietbefreiung kann nicht abgerufen werden. Das liegt daran, dass der Account mit deinem angegebenen maxProofSize zu groß wäre, um ihn zu erstellen. Daher können wir kein Mindestguthaben abrufen, durch das der Account von der Miete befreit wäre.
Einen nebenläufigen Merkle-Baum erstellen
Beim Erstellen eines nebenläufigen Merkle-Baums müssen wir zwei Accounts anlegen:
- Einen Account für den nebenläufigen Merkle-Baum
- Einen Konfigurations-Account für den nebenläufigen Merkle-Baum
Der Baum-Account enthält den Merkle-Baum, der zur Datenverifizierung dient. Wir erstellen ihn mit der gewünschten maximalen Tiefe, maximalen Puffergröße und Canopy-Tiefe, wie im vorherigen Abschnitt beschrieben. Dieser Account gehört dem von Solana entwickelten und verwalteten Account Compression-Programm. Er dient dazu, die Echtheit komprimierter NFTs zu verifizieren.
Der Baumkonfigurations-Account ist eine PDA, die von der Adresse des Accounts für den nebenläufigen Merkle-Baum abgeleitet wird. Er speichert zusätzliche Konfigurationen wie den Ersteller des Baums und die Anzahl der geminteten komprimierten NFTs.
Metaplex bezeichnet nebenläufige Merkle-Bäume mit einem zugehörigen Baumkonfigurations-Account als „Bubblegum-Baum“.
Vollständiger Code
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}`);
}
}Aufschlüsselung des Codes
Dies ist eine Beispielfunktion zum Erstellen eines nebenläufigen Merkle-Baums auf Solana. Um diese Beispielfunktion, createTree, aufzurufen, musst du die folgenden Parameter übergeben:
connection- eine Verbindung zu einem JSON-RPC-Endpunkt eines Full Nodes vom TypConnectionpayer- der Account, der für die Transaktion bezahlt, vom TypKeypairtreeKeypair- die Schlüsselpaar-Adresse für den Baum vom TypKeypairmaxDepthSizePair- das gültige Paar ausmaxDepthundmaxBufferSizevom TypValidDepthSizePaircanopyDepth- die Canopy-Tiefe des Baums vom Typnumbermit dem Standardwert0
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";Zuerst importieren wir @solana/web3.js, @solana/spl-account-compression und @metaplex-foundation/mpl-bubblegum mit den erforderlichen Modulen.
const createTree = async (
connection: Connection,
payer: Keypair,
treeKeypair: Keypair,
maxDepthSizePair: ValidDepthSizePair,
canopyDepth: number = 0,
) => {
// Rest of the code
}Hier definieren wir die Funktion createTree mit den zuvor genannten Parametern.
const allocTreeInstruction = await createAllocTreeIx(
connection,
treeKeypair.publicKey,
payer.publicKey,
maxDepthSizePair,
canopyDepth,
);createAllocTreeIx ist eine Hilfsfunktion, mit der wir den Account für den nebenläufigen Merkle-Baum erstellen. Das SPL Account Compression-Paket empfiehlt diese Methode zur Initialisierung eines solchen Accounts, da diese Accounts meist recht groß sind und die Grenze dessen überschreiten können, was sich über CPI zuweisen lässt. Hier erstellen wir die Anweisung, die Speicherplatz für den Baum-Account on-chain reserviert. Dabei werden auch der erforderliche On-Chain-Speicherplatz und die Kosten berechnet, sodass wir uns später nicht mehr darum kümmern müssen.
const [treeAuthority, ] = PublicKey.findProgramAddressSync(
[treeKeypair.publicKey.toBuffer()],
PROGRAM_ID,
);Wir müssen den Baumkonfigurations-Account ableiten, dessen Authority dem Bubblegum-Programm gehört. Dies ist für createCreateTreeInstruction erforderlich, also die Anweisung, die den Baum erstellt, da wir treeAuthority als Argument übergeben müssen. Hier leiten wir die PDA mit der Methode findProgramAddressSync aus dem öffentlichen Schlüssel des Baums und der Programm-ID von Bubblegum ab. Wir müssen treeAuthority destrukturieren, da sowohl die Authority als auch der Bump zurückgegeben werden. Den Bump habe ich weggelassen, weil unsere Funktion ihn nicht benötigt. Ändere die Destrukturierung bei Bedarf in [treeAuthority, bump], um den Bump zu speichern.
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,
);Wir verwenden createCreateTreeInstruction aus dem Bubblegum SDK, um die Anweisung zum Erstellen des nebenläufigen Merkle-Baums zu bauen. Dadurch wird der Baum on-chain erstellt und dem Bubblegum-Programm zugeordnet. createCreateTreeInstruction hat drei Parameter. Der erste ist ein Objekt mit Accounts, über die Eigenschaften wie der Ersteller des Baums festgelegt werden. Das zweite Objekt enthält die maximale Tiefe und die maximale Puffergröße. Es enthält außerdem den Parameter public vom Typ boolean. Wenn du public auf true setzt, kann jeder komprimierte NFTs aus dem Baum minten. Andernfalls können dies nur der Ersteller oder der Delegate des Baums. Ein delegierter Account kann im Namen des Baumeigentümers Aktionen ausführen, etwa ein komprimiertes NFT übertragen oder verbrennen. Nebenbei bemerkt kannst du mit createSetTreeDelegateInstruction aus dem Paket @metaplex-foundation/mpl-bubblegum wie folgt einen Baum-Delegate zuweisen:
const changeTreeDelegateTransaction = createSetTreeDelegateInstruction({
merkleTree: treeKeypair.publicKey
newTreeDelegate: ,
treeAuthority,
treeCreator: treeCreator.publicKey // which in our script would be payer.publicKey
});Außerdem übergeben wir die Programm-ID des Bubblegum-Programms. Nun zurück zum restlichen 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}`);
}Wir fügen die beiden soeben erstellten Anweisungen einer Transaktion hinzu und senden sie ab. Dabei stellen wir sicher, dass sowohl treeKeypair als auch payer die Transaktion signieren. Anschließend geben wir die Signatur der erfolgreichen Transaktion in der Konsole aus. Wir fassen diesen Prozess in einen try-catch-Block ein, damit ein auftretender Fehler über console.error in der Konsole ausgegeben wird.
Einen nebenläufigen Merkle-Baum mit Umi erstellen
Die gemeinsame Verwendung des Bubblegum SDK, des Account Compression-Programms von Solana und der web3.js-Pakete von Solana kann neue Entwickler schnell verwirren und ist bei jeder Einrichtung aufwendig. Glücklicherweise stellt das Bubblegum SDK eine createTree-Operation bereit, die alles für uns übernimmt und sich gut mit Umi kombinieren lässt. Der Code sieht so aus:
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 ist ein modulares Framework zum Erstellen und Verwenden von JavaScript-Clients für Solana-Programme. Es bietet eine Bibliothek ohne Abhängigkeiten sowie eine Reihe zentraler Schnittstellen, auf die sich andere Bibliotheken stützen können, ohne an eine bestimmte Implementierung gebunden zu sein. Umi wird von Metaplex bereitgestellt. Die Dokumentation findest du hier.
Mit unserer Umi-Instanz generieren wir einen Signer, erstellen unseren Merkle-Baum und senden und bestätigen die erstellte Transaktion. Standardmäßig wird die Umi-Identität als Ersteller des Baums festgelegt und der Parameter public auf false gesetzt. Du kannst diese Parameter anpassen, um einen benutzerdefinierten Ersteller und für „public“ den Wert true zu übergeben. So lässt sich ein nebenläufiger Merkle-Baum on-chain deutlich schneller erstellen.
Beachte, dass die Canopy-Größe für Bubblegum keine Rolle spielt. Das liegt daran, dass das Account Compression-Programm von Solana die Canopy-Größe anhand des verfügbaren Account-Speicherplatzes bestimmt. Du musst lediglich genug Speicherplatz reservieren, damit das Programm die passende Canopy-Größe korrekt ermitteln kann.
cNFTs durch direkte Interaktion mit Bubblegum minten
Eine Collection erstellen
NFTs werden nach dem Metaplex-Standard üblicherweise in einer Collection zusammengefasst. Das gilt sowohl für komprimierte als auch für „reguläre“ NFTs. So erstellst du eine Collection:
- Erstelle einen neuen Token-„Mint“
- Erstelle einen zugehörigen Token-Account für den Mint
- Minte einen einzelnen Token
- Speichere die Metadaten der Collection in einem Account on-chain
Dies steht zwar nicht direkt mit State Compression oder komprimierten NFTs in Zusammenhang und geht daher über den Rahmen dieses Artikels hinaus. Als Referenz steht jedoch ein Skript bereit, mit dem du deine eigene Collection erstellen kannst. Du findest es hier.
Ein NFT für unsere Collection minten
Für deine neu erstellte Collection benötigst du Folgendes, bevor du mit dem Minten beginnen kannst:
collectionMint- die Mint-Adresse der CollectioncollectionAuthority- der Account mit Authority über die CollectioncollectionMetadata- der Metadaten-Account der CollectioneditionAccount- der Account, der zusätzliche Attribute enthält, etwa einen Master-Edition-Account
Vollständiger Code zum Minten für eine 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}`);
}
}Aufschlüsselung des Minting-Prozesses
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";Zuerst importieren wir @solana/web3.js, @solana/spl-account-compression, @metaplex-foundation/mpl-bubblegum und @metaplex-foundation/mpl-token-metadata mit den erforderlichen Modulen.
export async function mintCompressedNFT(
connection: Connection,
payer: Keypair,
treeAddress: PublicKey,
collectionMint: PublicKey,
collectionMetadata: PublicKey,
collectionMasterEditionAccount: PublicKey,
compressedNFTMetadata: MetadataArgs,
receiverAddress?: PublicKey
) {
// Rest of the code
}Wir definieren mintCompressedNFT. Die Funktion nimmt einige Parameter entgegen:
connection- das Verbindungsobjekt für die Interaktion mit Solanapayer- der Account, der die Transaktionsgebühren bezahlttreeAddress- der Account des nebenläufigen Merkle-BaumscollectionMint- die Mint-Adresse der CollectioncollectionMetadata- der Metadaten-Account der CollectioncollectionMasterEditionAccount- der Master-Edition-AccountcompressedNFTMetadata- die Metadaten des zu mintenden cNFTsreceiverAddress- eine optionale Adresse mit öffentlichem Schlüssel, an die das neu gemintete cNFT gesendet wird
const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);
const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
[Buffer.from("collection_cpi", "utf8")],
BUBBLEGUM_PROGRAM_ID
);Hier ermitteln wir die erforderlichen PDAs und ignorieren ihre Bumps. Zuerst leiten wir die PDA für die Authority des Baums ab. Anschließend leiten wir eine PDA ab, die als Signer für das komprimierte Minting dient. Wir müssen collection_cpi einbinden, da das Bubblegum-Programm dieses benutzerdefinierte Präfix benötigt.
const mintInstructions: TransactionInstruction[] = [];Wir setzen mintInstructions auf ein leeres Array vom Typ TransactionInstruction. So können wir bei Bedarf mehrere cNFTs gleichzeitig minten.
const metadataArgs = Object.assign(compressedNFTMetadata, {
collection: { key: collectionMint, verified: false },
});metadataArgs stellt sicher, dass compressedNFTMetadata korrekt formatiert ist. Wenn du mit createMintToCollectionV1Instruction ein NFT für eine Collection mintest, muss das Feld „verified“ auf false gesetzt sein, damit die Transaktion erfolgreich ist, obwohl die Collection automatisch verifiziert wird.
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,
}
)
);Wir fügen unserer Anweisung einen einzelnen Mint hinzu. Wir könnten mehrere Mints in dieselbe Transaktion aufnehmen, solange die Transaktion innerhalb der Größenbeschränkung in Byte bleibt. Hier verwenden wir createMintToCollectionV1Instruction, um unser komprimiertes NFT aus unserer Collection zu minten. Diese Anweisung nimmt zwei Objekte entgegen: eines mit den zur Verarbeitung erforderlichen Accounts und eines mit den Anweisungsdaten für das Programm. Die meisten dieser Parameter sollten dir aus den vorherigen Abschnitten bekannt vorkommen. Du kannst beim Minten eine beliebige Delegate-Adresse festlegen. Normalerweise sollte sie jedoch mit leafOwner übereinstimmen. Unabhängig davon wird der Delegate bei der Übertragung des cNFTs automatisch entfernt. Wir legen den Zahler als Delegate fest, da er auch das cNFT erhält, wenn kein receiverAddress angegeben wurde.
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}`);
}Anschließend erstellen wir unsere Transaktion, setzen payer als feePayer und senden die Transaktion ab. Wir fassen diese Logik in einen try-catch-Block ein, falls beim Senden und Bestätigen der Transaktion Fehler auftreten. Solche Fehler geben wir mit console.error in der Konsole aus.
cNFTs mit Umi minten
Das Bubblegum-Programm bietet über Umi zwei Minting-Verfahren:
- Ein NFT minten, ohne es einer Collection zuzuordnen
- Ein NFT für eine bestimmte Collection minten.
Ohne Collection minten
Mit der Anweisung MintV1 von Bubblegum kannst du komprimierte NFTs ohne Collection aus einem Bubblegum-Baum minten. Ist der Baum öffentlich, kann jeder darin minten. Andernfalls können nur der Ersteller oder der Delegate des Baums diese Anweisung verwenden. So mintest du ein komprimiertes NFT ohne 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);Dieser Codeausschnitt stammt aus der Metaplex-Dokumentation zum Minten von cNFTs mit Bubblegum. Hier verwenden wir eine Umi-Instanz, um ein cNFT zu minten. Die weiteren Parameter der Anweisung mintV1 lauten wie folgt:
leafOwnerist der Eigentümer des zu mintenden cNFTsmerkleTreeist die Account-Adresse des nebenläufigen Merkle-Baums, aus dem das cNFT gemintet wirdmetadataist ein Objekt mit den Metadaten des zu mintenden cNFTs. Dazu gehören der Name des cNFTs, seine URI, seine Collection, für die wir „none“ angegeben haben, sowie seine Ersteller. Du kannst ein Collection-Objekt angeben, musst aber das Feld „verified“ der Ersteller auffalsesetzen, da die Collection-Authority in der Anweisung nicht angefordert wird. Ersteller können sich auch selbst verifizieren, indem sie das Feld „verified“ auf true setzen und den Ersteller in den verbleibenden Accounts als Signer angeben.
Die Anweisung mintV1 enthält außerdem mehrere optionale Felder, da die Eingabe der Funktion vom Typ MintV1InstructionAccounts & MintV1InstructionArgs ist. Diese Typen sind wie folgt definiert:
// 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 ist ein schwer greifbarer verschachtelter Typ, der letztlich einem Objekt mit einem Metadatenfeld entspricht. Dieses Metadatenfeld ist vom Typ MetadataArgsArgs und wird wie folgt definiert:
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;
};Die vollständige Funktionsdefinition für mintV1 und alle zugehörigen Typen findest du hier. Mit einer Umi-Instanz kannst du ein cNFT jedoch mindestens dann ohne Collection minten, wenn du die erforderlichen Metadaten, den Leaf-Eigentümer und den Account des nebenläufigen Merkle-Baums übergibst.
Mit einer Collection minten
Bubblegum bietet mintToCollectionV1 als bequeme Möglichkeit, ein cNFT direkt für eine bestimmte Collection zu minten. Die Eingabe dieser Anweisung hat die Typen MintToCollectionV1InstructionAccounts und MintToCollectionV1InstructionArgs und ist letztlich ein Objekt vom Typ MetadataArgsArgs. Die Typdefinition für MintToCollectionV1InstructionAccounts sieht wie folgt aus:
// 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;
};Die wichtigsten Parameter sind der Collection-Mint, die Collection-Authority und die PDA des Collection-Authority-Datensatzes. Wenn du eine delegierte Collection-Authority verwendest, musst du eine PDA für den Delegate-Datensatz angeben. So stellst du sicher, dass die Authority das Collection-NFT verwalten darf. Der Metadatenparameter muss ein Collection-Objekt enthalten, dessen Adressfeld mit dem Collection-Mint-Parameter übereinstimmt und dessen Feld „verified“ auf false gesetzt ist. Ersteller können sich auch selbst verifizieren, indem sie die Transaktion signieren und sich den verbleibenden Accounts hinzufügen.
So mintest du ein komprimiertes NFT mit einer 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);Diesen Codeausschnitt findest du in der Metaplex-Dokumentation zum Minten von cNFTs mit Bubblegum. Auch hier verwenden wir eine Umi-Instanz, um das komprimierte NFT zu minten. Wie bei mintV1 übergeben wir leafOwner und merkleTree. Dieses Mal übergeben wir jedoch zusätzlich collectionMint. Im Metadatenfeld übergeben wir ein collection-Objekt, dessen Schlüssel mit collectionMint übereinstimmt und dessen Feld „verified“ auf false gesetzt ist. Beachte, dass die Umi-Identität als standardmäßige Collection-Authority festgelegt wird. Du kannst dies ändern, indem du im optionalen Feld collectionAuthority eine benutzerdefinierte Collection-Authority angibst.
cNFTs mit Helius minten
Helius bietet eine Mint API, mit der du komprimierte NFTs ohne zusätzlichen Aufwand minten kannst. Wir übernehmen die Solana-Gebühren und die Erstellung des Merkle-Baums und laden deine Off-Chain-Metadaten auf Arweave hoch. Außerdem stellen wir sicher, dass die Transaktion erfolgreich übermittelt und vom Netzwerk bestätigt wurde. Du musst den Status also nicht selbst abfragen. Zudem lesen wir die Asset-ID aus der Transaktion aus, sodass du sie sofort mit der DAS API verwenden kannst.
Damit Helius ein NFT in deine Collection minten kann, musst du die Collection Authority delegieren. Je nach Cluster musst du die Authority an eines der folgenden Konten delegieren:
- Devnet:
2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC - Mainnet:
HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R
So mintest du ein cNFT mit der 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();Dieses Codebeispiel und eine detailliertere Beschreibung des Request-Schemas findest du in unserer Dokumentation.
Wenn du das Feld uri nicht ausfüllst, erstellen wir für dich eine JSON-Datei und laden sie auf Arweave hoch. Die Datei entspricht dem Metaplex-JSON-Standard v1.0 und wird über Irys (ehemals Bundlr) hochgeladen.
cNFTs übertragen
Eine komprimierte NFT überträgst du grundsätzlich in folgenden Schritten:
- Asset-Daten des cNFTs vom Indexer abrufen
- Proof des cNFTs vom Indexer abrufen
- Konto des Concurrent Merkle Tree von Solana abrufen
- Asset-Proof vorbereiten
- Übertragungstransaktion erstellen und senden
Umi und Metaplex vereinfachen diesen Prozess erheblich. In diesem Abschnitt zeigen wir jedoch, was im Hintergrund geschieht. Im Folgenden erklären wir, wie du eine komprimierte NFT sowohl mit web3.js als auch mit Metaplex überträgst.
Übertragung durch direkte Interaktion mit Bubblegum
Bevor wir die Übertragung mit unserem Skript ausführen, müssen wir einige Informationen über unsere komprimierte NFT abrufen. Zuerst rufen wir mit der Methode getAsset der DAS API die Metadaten der komprimierten NFT ab. Dabei benötigen wir data_hash, creator_hash, owner, delegate und 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();Ein Teil der erfolgreichen Response sieht so aus:
{
...
},
"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",
...
}
}Sobald wir die erforderlichen Informationen haben, rufen wir mit der Methode getAssetProof den Wert proof und tree_id (die Adresse des Baums) ab. Hier ist ein Beispielaufruf:
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();Die erfolgreiche Response sieht so aus:
{
"root": "string",
"proof": [
"string"
],
"node_index": 0,
"leaf": "string",
"tree_id": "string"
}Mit Root, Proof und tree_id können wir jetzt zu unserem Übertragungsskript wechseln.
Vollständiger 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";
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}`);
}
};Aufschlüsselung des Codes
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";Wir importieren @solana/web3.js, @metaplex-foundation/mpl-bubblegum und @solana/spl-account-compression mit den benötigten Modulen.
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
}Wir definieren die Funktion transferCompressedNFT. Darin parsen wir den Proof-Pfad, erstellen die Übertragungsanweisung und führen sie aus.
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);
const treeAuthority = treeAccount.getAuthority();
const canopyDepth = treeAccount.getCanopyDepth();Wir rufen das Konto des Concurrent Merkle Tree von der Blockchain ab und extrahieren die Tree Authority und die Canopy-Tiefe. Diese Werte benötigen wir, um die Übertragungsanweisung zu erstellen.
const proofPath: AccountMeta[] = proof
.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}))
.slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));Vereinfacht gesagt parsen wir die Liste der Proof-Adressen in ein gültiges Array vom Typ AccountMeta. AccountMeta enthält die Kontometadaten, mit denen Transaktionen definiert werden. Dazu gehören der öffentliche Schlüssel des Kontos, die Angabe, ob eine Anweisung eine zum öffentlichen Schlüssel passende Transaktionssignatur erfordert, und die Angabe, ob der öffentliche Schlüssel als Konto mit Lese- und Schreibzugriff geladen werden kann.
Wir nehmen einen Ausschnitt unseres vollständigen Proofs vom Anfang des Arrays und stellen sicher, dass er nur proof.length - canopyDepth Proof-Werte enthält. So entfernen wir den Teil des Baums, der bereits im On-Chain-Canopy zwischengespeichert ist. Anschließend strukturieren wir jeden verbleibenden Proof-Wert als gültigen AccountMeta. Das ist erforderlich, weil der Proof innerhalb der Übertragungsanweisung als „zusätzliche Konten“ on-chain übermittelt wird.
const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);Anschließend setzen wir leafOwner auf den Parameter owner und leafDelegate auf den Parameter 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
);Wir erstellen transferInstruction mit der Hilfsfunktion createTransferInstruction aus dem Bubblegum SDK. Beachte, dass root, dataHash und creatorHash von der DAS API als String zurückgegeben werden. Deshalb müssen wir sie zuerst in den Typ PublicKey und anschließend in ein Byte-Array konvertieren.
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}`);
}Wir fügen die erstellte Anweisung einer neuen Transaktion hinzu und senden sie an Solana. Treten Fehler auf, protokollieren wir sie mit console.error in der Konsole.
Wenn Fehler im Zusammenhang mit dem Concurrent Merkle Tree auftreten, liefert dein RPC möglicherweise veraltete oder falsche Daten für den Proof des Concurrent Merkle Tree. Das kann gelegentlich durch Caching-Probleme passieren. Um das zu beheben, kannst du den vom RPC bereitgestellten Proof clientseitig prüfen:
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());Du benötigst außerdem den Wert leaf, den unser DAS-API-Aufruf getAssetProof zurückgibt. Das ist nicht erforderlich, da die eigentliche Proof-Validierung on-chain erfolgt. Es kann jedoch bei der Fehlerbehandlung helfen.
Anschließend kannst du getAsset erneut aufrufen. Dabei siehst du, dass leafDelegate leer ist und das Leaf einen neuen Owner hat!
Übertragung mit 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);Dieser Code stammt aus der Metaplex-Dokumentation zur Übertragung komprimierter NFTs.
Bubblegum bietet eine Anweisung namens transfer, die sehr einfach zu verwenden ist. Zuerst nimmt sie eine Umi-Instanz entgegen. Danach akzeptiert sie ein Objekt, das das Asset mit Informationen zu seinem Proof, dem Leaf Owner und dem neuen Leaf Owner enthält. Das Asset mit dem erforderlichen Proof erhalten wir über die ebenfalls von Bubblegum bereitgestellte Methode getAssetWithProof. Beachte, dass du statt des Leaf Owner auch den Leaf Delegate verwenden kannst. Du benötigst lediglich ein Konto, das die Übertragung autorisieren darf. Mit der Methode .sendAndConfirm() senden wir die Transaktion, die die Übertragung startet, und bestätigen sie anschließend mit unserer Umi-Instanz.
Fazit
Glückwunsch! Wir haben State Compression und komprimierte NFTs auf Solana umfassend untersucht. Wir haben uns durch die Komplexität von Concurrent Merkle Trees gearbeitet, verbreitete Missverständnisse aufgeklärt und einen tiefen Einblick in das Solana-Ledger gewonnen. Neben der Theorie haben wir gelernt, wie du cNFTs mit Solanas web3.js, Metaplex und Helius abrufst, mintest und überträgst!
Solanas State Compression ist in einer Umgebung revolutionär, in der Transaktions- und Speicherkosten erhebliche Einschränkungen verursachen können. Komprimierung senkt die Kosten drastisch, ohne Sicherheit oder Dezentralisierung zu beeinträchtigen. Dieser Paradigmenwechsel eröffnet Künstlern, Sammlern und Entwicklern völlig neue Möglichkeiten.
Wenn du bis hierher gelesen hast: Danke, Anon! Du bist bestens vorbereitet, um zu diesem spannenden neuen Bereich beizutragen. Also leg los: Minte eine Collection mit zehn Millionen NFTs für dein On-Chain-MMORPG, entwickle eine dezentrale App, die die Leistung des Ledgers nutzt, oder teile dein neues Wissen einfach mit der Community. Die Zukunft lässt sich am besten vorhersagen, indem du sie selbst gestaltest.
Zusätzliche Ressourcen und weiterführende Literatur
- Solana-Dokumentation zu State Compression
- Programm zur Kontokomprimierung
- Bubblegum-Dokumentation von Metaplex
- Komprimierte NFTs auf Solana sind die Zukunft • Helius erklärt
- Fallstudie zu Dialect und cNFTs
- Angela: Ein dünn besetzter, verteilter und hochgradig paralleler Merkle-Baum
- Parallelisierte C++-Implementierung eines Merkle-Baums
Ähnliche Artikel
Helius abonnieren
Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen


