BARU: Helius mengakuisisi Light Protocol
kompresi Solana
Blog/Dasar-Dasar

Semua yang Perlu Anda Ketahui tentang Kompresi di Solana

Developer Experience Engineer0xIchigo di X0xIchigo di LinkedIn0xIchigo di GitHub
Bacaan 32 menit

Apa yang dibahas dalam artikel ini?

Percayakah Anda jika saya mengatakan bahwa Anda dapat mencetak satu juta NFT sekarang juga dengan biaya kurang dari $150 USD? Mustahil! Biaya untuk mencetak NFT sebanyak itu bisa mencapai lebih dari satu juta dolar, tergantung blockchain-nya! Benarkah demikian?

Kompresi state adalah primitif baru yang memanfaatkan pohon Merkle dan buku besar Solana untuk memangkas biaya penyimpanan secara drastis, sekaligus mewarisi keamanan dan desentralisasi lapisan dasar Solana. Artikel ini dirancang sebagai pembahasan mendalam dan menyeluruh tentang kompresi di Solana. Topiknya mencakup segala hal, mulai dari kesalahpahaman umum hingga transfer NFT terkompresi. Jika Anda ingin mempelajari kompresi state serta cara mengambil, mencetak, atau mentransfer NFT terkompresi, inilah satu-satunya artikel yang Anda perlukan untuk memulai.

Artikel ini mengasumsikan bahwa Anda sudah membaca artikel kami Pengantar Alat Kriptografi - Penjelasan Fungsi Hash dan Pohon Merkle. Anda perlu membacanya sebelum membaca artikel ini karena kami mengasumsikan Anda telah memahami pohon Merkle. Artikel ini juga mengembangkan pembahasan tentang pohon Merkle konkuren dan mengulas penentuan ukuran serta pembuatannya secara lebih mendalam.

Artikel ini menggunakan Bubblegum SDK dan Umi untuk menunjukkan berbagai pendekatan dalam membuat pohon Merkle konkuren serta mencetak dan mentransfer NFT terkompresi. Memahami kedua alat tersebut akan bermanfaat karena Anda kemungkinan akan menemukan masing-masing alat dalam berbagai basis kode. Bubblegum SDK disertakan secara khusus untuk memudahkan pembelajaran karena alur kerjanya membuat mekanisme yang mendasarinya lebih transparan, sedangkan Umi menawarkan alur kerja lebih ringkas yang menyederhanakan proses-proses tersebut.

Kesalahpahaman Umum

Kita perlu meluruskan beberapa hal sebelum membahas kompresi state dan seluk-beluk NFT terkompresi:

Kompresi di Solana sama dengan kompresi tradisional

Hal ini keliru. Secara tradisional, kompresi digunakan untuk mengurangi ukuran file dan data. Tujuan utamanya adalah menyimpan atau mengirimkan data menggunakan lebih sedikit bit dibandingkan file aslinya. Ada dua jenis umum algoritme kompresi:

  • Kompresi lossless, yang memungkinkan data asli direkonstruksi dari data terkompresi
  • Kompresi lossy, yang menghapus informasi “kurang penting” untuk mengurangi ukuran file

NFT terkompresi bukanlah NFT yang telah diproses dengan algoritme kompresi lossless atau lossy agar datanya lebih kecil. Konsep ini juga bukan tentang mengurangi kualitas atau dimensi karya seni, musik, maupun metadata yang terkait dengan NFT tersebut. Dalam konteks Solana, konsep ini memiliki bentuk yang sama sekali berbeda. Konsep ini berfokus pada pengoptimalan cara buku besar blockchain yang mendasarinya menyimpan informasi terkait NFT tersebut. Dari konteks akun, kita mengompresinya ke dalam buku besar dengan menggabungkan beberapa akun—dalam hal ini NFT—menjadi satu root Merkle yang disimpan dalam state. Proses ini mengurangi biaya penyimpanan secara signifikan sekaligus mempertahankan kemampuan verifikasi.

Menyimpan data terkompresi secara off-chain berisiko dan menimbulkan kerentanan

Hal ini keliru—Anda dapat menyimpan data secara aman di luar chain dengan melakukan hashing terhadapnya dan menyimpan root Merkle-nya secara on-chain. Secara teknis, NFT terkompresi tidak disimpan di luar chain. Datanya tetap berada secara on-chain karena apa pun yang dapat diperoleh kembali dari buku besar dianggap berada secara on-chain. Perbedaannya adalah akun diberi insentif oleh state agar disimpan dalam memori oleh validator, sedangkan buku besar perlu diakses melalui node arsip. Kompresi state menggabungkan keduanya untuk menyediakan verifikasi data buku besar melalui state dalam sebuah akun, yang tetap mempertahankan keamanan dan desentralisasi Solana itu sendiri. Kita akan membahas buku besar dan alasan keamanannya di bagian lain.

Saya dapat kehilangan pohon Merkle konkuren jika pengindeks atau penyedia RPC yang saya gunakan untuk menyimpan pohon tersebut tidak beroperasi

Anda tidak akan kehilangan pohon tersebut—siapa pun yang memiliki akses ke buku besar dapat merekonstruksi seluruh pohon dengan memutar ulang riwayatnya.

Pohon Merkle konkuren dapat menangani pembaruan paralel

Kesalahpahaman yang umum adalah bahwa penggunaan kata “konkuren” menyiratkan beberapa pembaruan pada pohon Merkle on-chain dapat berlangsung secara paralel. Meskipun pohon Merkle konkuren dapat mengakomodasi beberapa penggantian leaf dalam blok yang sama, pembaruan ini ditangani secara berurutan oleh validator. Saat validator menerima sekumpulan transaksi yang memengaruhi pohon Merkle konkuren on-chain, validator dapat memprosesnya dalam slot yang sama. Namun, data per slot tidak diproduksi secara konkuren. Kami membahasnya lebih lanjut dalam bagian berikut, Apa Itu Kompresi State?

Pohon sama dengan koleksi

Pohon Merkle konkuren tidak sama dengan koleksi. Satu koleksi dapat menggunakan berapa pun jumlah pohon Merkle konkuren. Penting untuk diperhatikan bahwa pengelompokan NFT dapat berdiri sendiri dari penyimpanannya. NFT dapat berada dalam akun atau dikompresi ke dalam buku besar, yang tersebar di berapa pun jumlah pohon, baik satu maupun banyak. Namun, sebaiknya satu pohon Merkle konkuren hanya digunakan untuk satu koleksi guna mengurangi kompleksitas.

Apa itu kompresi state?

Kompresi state mengoptimalkan penyimpanan dengan membuat hash kriptografis dari data buku besar dan menyimpan hash tersebut dalam sebuah akun. Pendekatan ini memanfaatkan keamanan dan kekekalan bawaan buku besar sekaligus menyediakan kerangka kerja yang tangguh untuk memverifikasi data yang tersimpan di dalamnya.

Ini merupakan solusi hemat biaya untuk aplikasi yang dibangun di atas Solana. Developer kini dapat menggunakan ruang penyimpanan buku besar sebagai pengganti penyimpanan berbasis akun yang lebih mahal. Dengan demikian, kompresi state tidak hanya menjamin integritas data, tetapi juga menjadi solusi hemat biaya untuk alokasi sumber daya di Solana.

Rahasia di balik kompresi state Solana adalah penggunaan pohon Merkle konkuren. Pohon Merkle konkuren dioptimalkan untuk memproses beberapa transaksi secara berurutan dengan cepat sehingga proof-nya dapat dipercepat ke kondisi terbaru. Hal ini berbeda dari pohon Merkle tradisional, yang proof-nya menjadi tidak valid pada setiap pembaruan. Pohon Merkle konkuren menyimpan log perubahan yang aman untuk perubahan terbarunya, beserta hash root dan proof yang diperlukan untuk memperolehnya. Log perubahan ini disimpan secara on-chain dalam akun khusus untuk pohon tersebut. Setiap pohon Merkle konkuren memiliki ukuran buffer maksimum. Nilai ini menunjukkan jumlah perubahan terbanyak yang dapat dilakukan pada pohon selama root Merkle-nya masih valid. Anggap ini sebagai ukuran seberapa “usang” sekumpulan proof yang telah dihitung sebelum perlu diperbarui.

Dengan demikian, ketika validator menerima beberapa permintaan untuk memperbarui pohon Merkle on-chain dalam slot yang sama, validator dapat menggunakan log perubahan pohon sebagai sumber kebenaran. Hal ini memungkinkan perubahan konkuren pada pohon Merkle hingga sebanyak ukuran buffer maksimum. Meskipun tidak secara langsung mengurangi jumlah data yang disimpan secara on-chain, pendekatan ini meningkatkan efisiensi dengan memungkinkan beberapa pembaruan diproses secara bersamaan. Artinya, sistem dapat mempertahankan integritas “proof of inclusion” yang ditawarkan pohon Merkle, bahkan dalam lingkungan dengan throughput tinggi. Di sini, proof of inclusion hanya berarti kemampuan untuk menunjukkan bahwa elemen data tertentu memang merupakan bagian dari sekumpulan data yang telah di-hash bersama menjadi sebuah root Merkle.

Kombinasi cerdas antara kompresi state dan pohon Merkle konkuren ini menawarkan solusi yang sangat hemat biaya bagi aplikasi yang dibangun di Solana. Untuk memahami sepenuhnya dampak teknologi ini, kita perlu membahas perbedaan antara state dan buku besar Solana.

State vs. Buku Besar

Buku besar adalah catatan historis semua transaksi yang ditandatangani oleh klien dan telah terjadi di Solana sejak blok genesis. Buku besar merupakan struktur data yang hanya dapat ditambahi, yang berarti sebuah transaksi tidak dapat diubah atau dihapus setelah ditambahkan. Validator memvalidasi transaksi yang ditambahkan ke buku besar. Buku besar disimpan oleh beberapa node di seluruh jaringan untuk memastikan toleransi kegagalan. Namun, salinan buku besar milik validator mungkin hanya memuat blok-blok yang lebih baru untuk mengurangi kebutuhan penyimpanan, karena blok lama tidak diperlukan untuk memvalidasi blok mendatang.

State merepresentasikan snapshot terkini dari semua akun dan program di Solana. State dapat berubah dan diperbarui saat transaksi diproses. Anggap state sebagai basis data yang sangat dioptimalkan dan dapat dikueri untuk mengetahui saldo token, program, dan akun.

Berikut cara mudah untuk membedakan keduanya: Misalkan Alice memiliki saldo 100 SOL dan Bob juga memiliki saldo 100 SOL. Alice mengirim transaksi untuk memberikan 10 SOL kepada Bob. Setelah diverifikasi, transaksi tersebut ditambahkan ke sebuah blok dan blok itu ditambahkan ke buku besar. Buku besar kini memiliki catatan kekal yang menyatakan bahwa Alice mengirim 10 SOL kepada Bob. Pada saat yang sama, state akan memperbarui akun Alice dan Bob masing-masing menjadi 90 dan 110 SOL.

Perbedaan utama antara keduanya dapat dirangkum sebagai berikut:

  • Buku besar bersifat kekal dan hanya dapat ditambahi, sedangkan state dapat berubah dan terus diperbarui
  • Buku besar adalah catatan historis semua transaksi, sedangkan state mencerminkan status terkini semua akun dan program
  • Buku besar digunakan untuk verifikasi, sedangkan state digunakan untuk mengeksekusi transaksi dan menjalankan program

Buku besar berfungsi sebagai catatan historis yang kekal untuk memastikan setiap transaksi dapat diverifikasi dan dilacak, sedangkan state berfungsi sebagai snapshot dinamis dari buku besar yang menyesuaikan diri dengan operasi waktu nyata, seperti transfer dan eksekusi program. Yang terpenting, keduanya tunduk pada konsensus chain itu sendiri. State dan buku besar bersama-sama membentuk tulang punggung Solana, memungkinkannya beroperasi secara efisien sekaligus mempertahankan kepercayaan terdesentralisasi.

Apa Itu NFT Terkompresi?

NFT terkompresi (cNFT) menggunakan kompresi state dan pohon Merkle konkuren untuk mengurangi biaya penyimpanan. NFT terkompresi menyimpan metadata-nya di buku besar, bukan menyimpan setiap NFT dalam akun Solana biasa. Hal ini mengurangi biaya penyimpanan sekaligus mewarisi keamanan dan kekekalan buku besar.

NFT terkompresi tetap mengikuti skema metadata yang sama persis dengan versi yang tidak terkompresi. Dengan demikian, NFT dan cNFT didefinisikan dengan cara yang sama.

Perbedaan utama antara NFT dan cNFT adalah sebagai berikut:

  • NFT terkompresi dapat dikonversi menjadi NFT biasa, tetapi NFT biasa tidak dapat dikonversi menjadi NFT terkompresi
  • NFT terkompresi bukan token native Solana—NFT tersebut tidak memiliki akun token, akun mint, atau metadata. Namun, NFT tersebut memiliki pengenal stabil (ID aset). Setelah dekompresi, NFT mempertahankan pengenal yang sama. Dengan demikian, NFT dalam kondisi terkompresi bukanlah token native, tetapi dapat diubah menjadi token native jika diperlukan
  • Satu akun pohon Merkle konkuren dapat menampung jutaan NFT
  • Satu koleksi dapat tersebar di beberapa akun pohon
  • Semua modifikasi NFT dilakukan melalui program Bubblegum
  • Panggilan DAS API disarankan untuk membaca informasi apa pun tentang NFT terkompresi

Menariknya, kita perlu menggunakan DAS API untuk mendapatkan informasi tentang NFT terkompresi. Mengapa demikian? Dan yang lebih penting, apa itu DAS API?

Membaca Metadata NFT Terkompresi dengan DAS API

Kita memerlukan bantuan pengindeks karena metadata cNFT disimpan di buku besar, bukan dalam akun tradisional. Meskipun Anda dapat memperoleh state terkini NFT terkompresi dengan memutar ulang transaksi yang relevan, penyedia seperti Helius melakukannya untuk memudahkan Anda. Developer dapat menggunakan Digital Asset Standard (DAS) API, yaitu spesifikasi dan sistem sumber terbuka untuk mengambil informasi suatu aset. DAS API mendukung NFT terkompresi maupun NFT tradisional atau tidak terkompresi. Dengan demikian, Anda dapat menggunakan endpoint yang sama untuk kedua jenis NFT.

Helius saat ini mendukung metode DAS API berikut:

  • getAsset - mengambil aset tertentu berdasarkan id-nya
  • getAssetBatch - mengambil beberapa aset berdasarkan ID-nya
  • getAssetProof - mengambil proof Merkle untuk aset terkompresi berdasarkan id-nya
  • getAssetProofBatch - mengambil beberapa proof aset berdasarkan ID-nya
  • getAssetsByOwner - mengambil daftar aset yang dimiliki oleh sebuah alamat
  • getAssetsByAuthority - mengambil daftar aset dengan otoritas tertentu
  • getAssetsByCreator - mengambil daftar aset yang dibuat oleh sebuah alamat
  • getAssetsByGroup - mengambil daftar aset berdasarkan kunci dan nilai grup
  • searchAssets - mencari aset berdasarkan berbagai parameter
  • getSignaturesForAsset - mengambil daftar tanda tangan transaksi yang terkait dengan aset terkompresi
  • Paginasi - dukungan untuk paginasi berbasis halaman dan keyset guna mengambil lebih dari 1.000 catatan sekaligus

Lihat dokumentasi Helius DAS API untuk mempelajari lebih lanjut setiap metode. Sebagai contoh, jika ingin mengambil daftar semua aset yang dimiliki oleh sebuah alamat, Anda dapat membuat permintaan POST berikut dengan getAssetsByOwner:

Kode
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();

Mengambil aset terkompresi memang mudah, tetapi bagaimana jika kita ingin membuatnya sendiri? Sebelum memulai proses pencetakan, kita perlu menghitung ukuran dan biaya terkait untuk membuat pohon Merkle konkuren yang akan menyimpan aset-aset tersebut.

Penentuan Ukuran dan Biaya Pembuatan Pohon Merkle Konkuren

Menghitung Ukuran

Saat membuat pohon Merkle konkuren secara on-chain, ada tiga metrik penting yang akan menentukan ukuran pohon, biaya pembuatannya, dan jumlah perubahan konkuren yang dapat dilakukan pada pohon selagi root Merkle tetap valid:

  • Kedalaman maksimum
  • Ukuran buffer maksimum
  • Kedalaman canopy

Kedalaman maksimum mengacu pada jumlah lompatan maksimum untuk mencapai root pohon dari leaf mana pun. Setiap leaf hanya terhubung ke satu leaf lain, membentuk pasangan leaf untuk hashing berpasangan. Anda dapat menghitung jumlah maksimum node leaf yang dapat ditampung pohon menggunakan rumus: numberOfNodes = 2 ^ maxDepth. Kedalaman pohon harus ditetapkan saat pembuatan, sehingga Anda perlu menggunakan rumus ini untuk menentukan kedalaman maksimum terendah yang dapat menyimpan data Anda. Misalnya, jika ingin menyimpan sekitar 100 NFT terkompresi dalam sebuah pohon, maxDepth sebesar 7 sudah cukup karena 2^7 = 128 dan 2^6 = 64. Kedalaman maksimum merupakan faktor biaya yang signifikan saat membuat pohon Merkle konkuren secara on-chain. Biaya ini dikeluarkan di muka saat pohon dibuat dan meningkat seiring bertambahnya nilai maxDepth.

Ukuran buffer maksimum mengacu pada jumlah perubahan maksimum yang dapat dilakukan pada sebuah pohon selagi root Merkle-nya masih valid. Ukuran buffer log perubahan ditentukan dan ditetapkan saat pohon Merkle konkuren dibuat menggunakan nilai maxBufferSize. Dengan demikian, ketika validator menerima beberapa permintaan perubahan pada sebuah pohon dalam slot yang sama, validator dapat menggunakan log perubahan dan mengizinkan hingga maxBufferSize perubahan selagi root tetap valid.

Penting untuk diperhatikan bahwa hanya ada sejumlah pasangan maxDepth dan maxBufferSize tertentu yang valid untuk membuat akun pohon Merkle konkuren baru. Paket @solana/spl-account-compression mengekspor konstanta ALL_DEPTH_SIZE_PAIRS, yaitu sebuah array berisi array angka yang mencakup semua kombinasi valid. Nilai minimumnya adalah maxDepth sebesar 3 dan maxBufferSize sebesar 8, sedangkan nilai maksimumnya adalah maxDepth sebesar 30 dan maxBufferSize sebesar 2048.

Kedalaman canopy mengacu pada subset pohon Merkle yang disimpan dalam sebuah akun. Proof yang di-cache ini digunakan untuk melengkapi proof yang dikirimkan melalui jaringan karena proof tersebut dibatasi oleh batas transaksi. Jalur lengkap harus digunakan untuk memverifikasi kepemilikan awal suatu leaf ketika mencoba mengubah datanya, seperti saat Anda mentransfer NFT. Semakin besar kedalaman maksimum pohon, semakin banyak node proof yang diperlukan untuk verifikasi. Canopy memungkinkan ukuran proof yang lebih kecil dan menghindari penggunaan ukuran proof sebesar maxDepth untuk memverifikasi pohon.

Kedalaman canopy dapat dihitung dengan mengurangi kedalaman maksimum dengan ukuran proof yang diinginkan. Jadi, jika Anda memiliki kedalaman maksimum 14 dan menginginkan ukuran proof 4, kedalaman canopy-nya adalah 10. Artinya, Anda hanya perlu mengirimkan 4 node proof per transaksi pembaruan. Kedalaman canopy juga merupakan faktor biaya yang signifikan saat membuat pohon Merkle konkuren secara on-chain. Biaya ini dikeluarkan di muka saat pohon dibuat dan meningkat seiring bertambahnya nilai canopyDepth. Meskipun canopyDepth yang lebih rendah menghasilkan biaya awal yang lebih kecil, nilai canopyDepth yang rendah dapat membatasi komposabilitas. Ini karena setiap transaksi pembaruan memerlukan ukuran proof yang lebih besar, sehingga menambah batasan pada ukuran transaksi. Sebagai contoh, jika pohon Anda dengan canopyDepth rendah digunakan untuk NFT terkompresi, marketplace NFT mungkin hanya dapat mendukung transfer sederhana untuk koleksi Anda. Secara umum, maxDepth - canopyDepth harus kurang dari atau sama dengan 10 untuk mencapai komposabilitas maksimum. Hal ini dijelaskan dalam spesifikasi Tensor mengenai panjang proof maksimum untuk cNFT Tensor.

Menghitung Biaya

Ada berbagai metode untuk menentukan ukuran dan biaya pohon Merkle konkuren. Pendekatan paling sederhana adalah menggunakan Kalkulator NFT Terkompresi dan memasukkan jumlah NFT terkompresi yang ingin disimpan dalam pohon tersebut:

Situs tersebut menyediakan perincian mendetail mengenai kedalaman pohon optimal yang diperlukan untuk menyimpan jumlah aset yang diinginkan, beserta berbagai opsi biaya berdasarkan komposabilitas. Sebagai contoh, gambar tersebut menunjukkan bahwa pembuatan pohon dengan komposabilitas tinggi yang menyimpan 10 juta NFT terkompresi hanya memerlukan biaya ~7,67 SOL. Dengan memperhitungkan biaya transaksi sebesar ~50 SOL untuk mencetak 10 juta NFT, total biayanya akan mencapai sekitar ~57,67 SOL.

Developer juga dapat menggunakan paket @solana/spl-account-compression untuk menghitung ruang yang diperlukan bagi ukuran pohon tertentu dan biaya alokasi ruang yang diperlukan untuk pohon tersebut secara on-chain. Hal ini dapat dilakukan dengan skrip berikut:

Kode
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();

Di sini, kita mengimpor modul yang diperlukan dari @solana/web3.js dan @solana/spl-account-compression. Kita memerlukan koneksi ke mainnet, yang dapat dibuat menggunakan kunci Helius API. Fungsi calculateCosts mencatat maxDepth, maxBufferSize, canopy, jumlah NFT yang dapat disimpan dalam pohon ini, serta biaya rent dalam SOL ke konsol. Dengan demikian, ketika memanggil calculateCosts menggunakan ukuran proof yang diinginkan, kita dapat melihat semua kemungkinan kombinasi pohon di konsol.

Perhatikan bahwa beberapa log mungkin menampilkan: Tidak dapat mengambil saldo minimum untuk pengecualian biaya sewa. Hal ini terjadi karena akun dengan maxProofSize yang Anda tentukan akan terlalu besar untuk dibuat. Oleh karena itu, kita tidak dapat mengambil saldo minimum yang akan membuat akun tersebut bebas biaya sewa.

Membuat Pohon Merkle Konkuren

Kita perlu membuat dua akun saat membuat pohon Merkle konkuren:

  • Akun pohon Merkle konkuren
  • Akun konfigurasi pohon Merkle konkuren

Akun pohon menyimpan pohon Merkle yang digunakan untuk verifikasi data. Kita membuatnya menggunakan kedalaman maksimum, ukuran buffer maksimum, dan kedalaman kanopi yang diinginkan seperti yang disebutkan pada bagian sebelumnya. Akun ini dimiliki oleh program Account Compression, yang dibuat dan dikelola oleh Solana. Akun ini digunakan untuk memverifikasi keaslian NFT terkompresi.

Akun konfigurasi pohon adalah PDA yang diturunkan dari alamat akun pohon Merkle konkuren. Akun ini digunakan untuk menyimpan konfigurasi tambahan seperti pembuat pohon dan jumlah NFT terkompresi yang telah dicetak.

Metaplex menyebut pohon Merkle konkuren yang memiliki akun konfigurasi pohon terkait sebagai “pohon Bubblegum”.

Kode Lengkap

Kode
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}`);
    }
}

Uraian Kode

Ini berfungsi sebagai contoh fungsi untuk membuat pohon Merkle konkuren di Solana. Untuk memanggil contoh fungsi ini, createTree, parameter berikut harus diteruskan:

  • connection - koneksi ke endpoint JSON RPC node penuh, dengan tipe Connection
  • payer - akun yang akan membayar transaksi, dengan tipe Keypair
  • treeKeypair - alamat pasangan kunci untuk pohon, dengan tipe Keypair
  • maxDepthSizePair - pasangan maxDepth dan maxBufferSize yang valid, dengan tipe ValidDepthSizePair
  • canopyDepth - kedalaman kanopi pohon, dengan tipe number dan nilai default 0
Kode
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";

Pertama, kita mengimpor @solana/web3.js, @solana/spl-account-compression, dan @metaplex-foundation/mpl-bubblegum beserta modul yang diperlukan.

Kode
const createTree = async (
    connection: Connection,
    payer: Keypair,
    treeKeypair: Keypair,
    maxDepthSizePair: ValidDepthSizePair,
    canopyDepth: number = 0,
) => {
	// Rest of the code
}

Di sini, kita mendefinisikan fungsi createTree dengan parameter yang disebutkan sebelumnya.

Kode
const allocTreeInstruction = await createAllocTreeIx(
		connection,
    treeKeypair.publicKey,
    payer.publicKey,
    maxDepthSizePair,
    canopyDepth,
);

createAllocTreeIx adalah fungsi pembantu yang digunakan untuk membuat akun pohon Merkle konkuren. Paket SPL Account Compression menyarankan penggunaan metode ini untuk menginisialisasi akun pohon Merkle konkuren karena akun tersebut cenderung cukup besar dan dapat melampaui batas alokasi melalui CPI. Di sini, kita membuat instruksi untuk mengalokasikan akun pohon secara on-chain. Tindakan ini juga menghitung ruang yang diperlukan untuk menyimpan pohon secara on-chain beserta biayanya, sehingga kita tidak perlu memikirkannya nanti.

Kode
const [treeAuthority, ] = PublicKey.findProgramAddressSync(
    [treeKeypair.publicKey.toBuffer()],
		PROGRAM_ID,
);

Kita perlu menurunkan akun konfigurasi pohon dengan otoritas yang dimiliki oleh program Bubblegum. Ini diperlukan untuk createCreateTreeInstruction, yaitu instruksi yang membuat pohon, karena treeAuthority harus diteruskan sebagai argumen. Di sini, kita menurunkan PDA dengan metode findProgramAddressSync menggunakan kunci publik pohon dan ID program Bubblegum. Kita perlu mendestrukturisasi treeAuthority karena otoritas dan bump sama-sama dikembalikan. Bump tidak disertakan karena tidak diperlukan oleh fungsi kita. Jika diperlukan, ubah destrukturisasi menjadi [treeAuthority, bump] untuk menyimpan bump.

Kode
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,
);

Kita menggunakan createCreateTreeInstruction dari Bubblegum SDK untuk menyusun instruksi yang membuat pohon Merkle konkuren. Instruksi ini membuat pohon secara on-chain dengan program Bubblegum sebagai pemiliknya. createCreateTreeInstruction memiliki tiga parameter. Parameter pertama adalah objek berisi akun untuk mengatur properti seperti pembuat pohon. Objek kedua berkaitan dengan kedalaman maksimum dan ukuran buffer maksimum. Objek ini juga mencakup parameter public bertipe boolean. Menetapkan public ke true akan memungkinkan siapa pun mencetak NFT terkompresi dari pohon. Jika tidak, hanya pembuat pohon atau delegasi pohon yang dapat mencetak NFT terkompresi dari pohon tersebut. Akun yang didelegasikan dapat melakukan tindakan atas nama pemilik pohon, seperti mentransfer atau membakar NFT terkompresi. Sebagai tambahan, Anda dapat menetapkan delegasi pohon menggunakan createSetTreeDelegateInstruction dari paket @metaplex-foundation/mpl-bubblegum seperti berikut:

Kode
const changeTreeDelegateTransaction = createSetTreeDelegateInstruction({
		merkleTree: treeKeypair.publicKey
		newTreeDelegate: ,
		treeAuthority,
		treeCreator: treeCreator.publicKey // which in our script would be payer.publicKey
});

Kita juga meneruskan ID program Bubblegum. Sekarang, kembali ke bagian kode lainnya:

Kode
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}`);
}

Kita menambahkan dua instruksi yang baru dibuat ke sebuah transaksi, lalu mengirimkannya. Kita memastikan treeKeypair dan payer menandatangani transaksi. Tanda tangan transaksi yang berhasil kemudian dicatat ke konsol. Kita membungkus proses ini dalam blok try-catch agar jika terjadi error karena alasan apa pun, error tersebut dicatat ke konsol melalui console.error.

Membuat Pohon Merkle Konkuren dengan Umi

Menggunakan Bubblegum SDK, program kompresi akun Solana, dan paket web3.js Solana dapat cukup membingungkan bagi developer baru serta merepotkan untuk disiapkan setiap kali. Untungnya, Bubblegum SDK menyediakan operasi createTree yang menangani semuanya untuk kita dan sangat cocok dipadukan dengan Umi. Kodenya adalah sebagai berikut:

Kode
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 adalah framework modular untuk membangun dan menggunakan klien JavaScript bagi program Solana. Umi menyediakan pustaka tanpa dependensi dengan sekumpulan antarmuka inti yang dapat diandalkan oleh pustaka lain tanpa dibatasi pada implementasi tertentu. Umi disediakan oleh Metaplex dan dokumentasinya dapat ditemukan di sini.

Kita menggunakan instans Umi untuk membuat penanda tangan, membuat pohon Merkle, serta mengirim dan mengonfirmasi transaksi yang telah disusun. Secara default, pembuat pohon ditetapkan ke identitas Umi dan parameter public ditetapkan ke false. Parameter ini dapat disesuaikan, sehingga pembuat pohon khusus dan nilai publik true juga dapat diteruskan. Ini adalah cara yang jauh lebih cepat untuk membuat pohon Merkle konkuren secara on-chain.

Perhatikan bahwa Bubblegum tidak bergantung pada ukuran kanopi. Ini karena Solana Account Compression Program akan menentukan ukuran kanopi berdasarkan ruang akun yang tersedia. Anda hanya perlu mengalokasikan ruang yang cukup agar program dapat menentukan ukuran kanopi yang sesuai secara akurat.

Mencetak cNFT dengan Berinteraksi Langsung dengan Bubblegum

Membuat Koleksi

Secara tradisional, NFT dikelompokkan ke dalam koleksi menggunakan standar Metaplex. Ini berlaku untuk NFT terkompresi maupun NFT “reguler”. Untuk membuat koleksi:

  • Buat “mint” token baru
  • Buat akun token terkait untuk mint tersebut
  • Cetak satu token
  • Simpan metadata koleksi dalam akun secara on-chain

Meskipun tidak berkaitan langsung dengan topik kompresi status atau NFT terkompresi sehingga berada di luar cakupan artikel ini, sebuah skrip telah disediakan sebagai referensi untuk membuat koleksi Anda sendiri. Anda dapat mengakses skrip tersebut di sini.

Mencetak NFT ke Koleksi Kita

Dengan koleksi yang baru dibuat, Anda memerlukan hal berikut untuk mulai mencetak:

  • collectionMint - alamat mint koleksi
  • collectionAuthority - akun yang memiliki otoritas atas koleksi
  • collectionMetadata - akun metadata koleksi
  • editionAccount - akun yang menyimpan atribut tambahan, seperti akun edisi master

Kode Lengkap untuk Mencetak ke Koleksi

Kode
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}`);
  }
}

Menguraikan Proses Pencetakan

Kode
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";

Pertama, kita mengimpor @solana/web3.js, @solana/spl-account-compression, @metaplex-foundation/mpl-bubblegum, dan @metaplex-foundation/mpl-token-metadata beserta modul yang diperlukan.

Kode
export async function mintCompressedNFT(
  connection: Connection,
  payer: Keypair,
  treeAddress: PublicKey,
  collectionMint: PublicKey,
  collectionMetadata: PublicKey,
  collectionMasterEditionAccount: PublicKey,
  compressedNFTMetadata: MetadataArgs,
  receiverAddress?: PublicKey
) {
	// Rest of the code
}

Kita mendefinisikan mintCompressedNFT, yang menerima cukup banyak parameter:

  • connection - objek koneksi yang digunakan untuk berinteraksi dengan Solana
  • payer - akun yang akan membayar biaya transaksi
  • treeAddress - akun pohon Merkle konkuren
  • collectionMint - alamat mint koleksi
  • collectionMetadata - akun metadata koleksi
  • collectionMasterEditionAccount - akun edisi master
  • compressedNFTMetadata - metadata khusus untuk cNFT yang akan dicetak
  • receiverAddress - alamat kunci publik opsional yang akan menerima cNFT yang baru dicetak
Kode
const [treeAuthority, ] = PublicKey.findProgramAddressSync([treeAddress.toBuffer()], BUBBLEGUM_PROGRAM_ID);

const [bubblegumSigner, ] = PublicKey.findProgramAddressSync(
    [Buffer.from("collection_cpi", "utf8")],
    BUBBLEGUM_PROGRAM_ID
  );

Di sini, kita mencari PDA yang diperlukan dan mengabaikan bump-nya. Pertama, kita menurunkan PDA untuk otoritas pohon, lalu menurunkan PDA yang akan bertindak sebagai penanda tangan untuk pencetakan terkompresi. Kita perlu menyertakan collection_cpi karena ini adalah prefiks khusus yang diwajibkan oleh program Bubblegum.

Kode
const mintInstructions: TransactionInstruction[] = [];

Kita menetapkan mintInstructions sebagai array kosong dari TransactionInstruction. Hal ini memungkinkan kita mencetak beberapa cNFT sekaligus jika diinginkan.

Kode
const metadataArgs = Object.assign(compressedNFTMetadata, {
    collection: { key: collectionMint, verified: false },
});

metadataArgs memastikan compressedNFTMetadata diformat dengan benar. Untuk mencetak NFT ke dalam koleksi menggunakan createMintToCollectionV1Instruction, field verified harus ditetapkan ke false agar transaksi berhasil, meskipun instruksi tersebut memverifikasi koleksi secara otomatis.

Kode
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,
      }
  	)
);

Kita menambahkan satu mint ke instruksi. Kita dapat menambahkan beberapa mint dalam transaksi yang sama selama transaksi tetap berada dalam batas ukuran byte. Di sini, kita menggunakan createMintToCollectionV1Instruction untuk mencetak NFT terkompresi dari koleksi. Instruksi ini menerima dua objek: satu berisi akun yang diperlukan untuk memproses instruksi dan satu lagi menyediakan data instruksi kepada program. Sebagian besar parameter ini seharusnya sudah tidak asing dari bagian sebelumnya. Perhatikan bahwa Anda dapat menetapkan alamat delegasi apa pun saat mencetak, tetapi biasanya alamat tersebut harus sama dengan leafOwner. Bagaimanapun, delegasi akan otomatis dihapus saat cNFT ditransfer. Kita menetapkan pembayar sebagai delegasi karena pembayar juga akan menerima cNFT jika receiverAddress tidak diberikan.

Kode
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}`);
  }

Kemudian, kita menyusun transaksi, menetapkan payer sebagai feePayer, lalu mengirim transaksi. Kita membungkus logika ini dalam blok try-catch untuk menangani error saat mengirim dan mengonfirmasi transaksi. Jika terjadi error, kita mencatatnya ke konsol dengan console.error.

Mencetak cNFT dengan Umi

Program Bubblegum menawarkan dua proses pencetakan melalui Umi:

  • Mencetak NFT tanpa mengaitkannya dengan koleksi
  • Mencetak NFT ke koleksi tertentu.

Mencetak Tanpa Koleksi

Instruksi MintV1 milik Bubblegum memungkinkan pencetakan NFT terkompresi dari pohon Bubblegum tanpa koleksi. Jika pohon bersifat publik, siapa pun dapat mencetak ke pohon ini. Jika tidak, hanya pembuat atau delegasi pohon yang dapat menggunakan instruksi ini. Berikut cara mencetak NFT terkompresi tanpa koleksi:

Kode
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);

Cuplikan kode ini berasal dari dokumentasi Metaplex tentang pencetakan cNFT dengan Bubblegum. Di sini, kita menggunakan instans Umi untuk mencetak cNFT. Parameter lain untuk instruksi mintV1 adalah sebagai berikut:

  • leafOwner adalah pemilik cNFT yang akan dicetak
  • merkleTree adalah alamat akun pohon Merkle konkuren yang menjadi sumber pencetakan cNFT
  • metadata adalah objek yang berisi metadata cNFT yang akan dicetak. Ini mencakup nama cNFT, URI-nya, koleksinya yang telah kita tetapkan ke none, serta pembuatnya. Anda dapat memberikan objek koleksi, tetapi field verified pada pembuat harus ditetapkan ke false karena otoritas koleksi tidak diminta dalam instruksi. Pembuat juga dapat memverifikasi dirinya sendiri dengan menetapkan field verified ke true dan menyediakan pembuat sebagai penanda tangan dalam akun yang tersisa.

Instruksi mintV1 juga memiliki sejumlah field opsional karena input fungsi bertipe MintV1InstructionAccounts & MintV1InstructionArgs. Tipe-tipe ini didefinisikan sebagai berikut:

Kode
// 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 adalah tipe yang sulit ditemukan di dalam rangkaian tipe dan pada dasarnya merupakan objek dengan field metadata. Field metadata ini bertipe MetadataArgsArgs dan didefinisikan sebagai berikut:

Kode
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;
};

Definisi fungsi lengkap untuk mintV1 beserta semua tipe terkait dapat ditemukan di sini. Namun, setidaknya dengan instans Umi, Anda dapat mencetak cNFT tanpa koleksi jika meneruskan metadata, pemilik leaf, dan akun pohon Merkle konkuren yang diperlukan.

Mencetak dengan Koleksi

Bubblegum menyediakan mintToCollectionV1 sebagai cara praktis untuk mencetak cNFT langsung ke koleksi tertentu. Input instruksi ini bertipe MintToCollectionV1InstructionAccounts dan MintToCollectionV1InstructionArgs, yang pada akhirnya merupakan objek bertipe MetadataArgsArgs. Definisi tipe untuk MintToCollectionV1InstructionAccounts adalah sebagai berikut:

Kode
// 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;
};

Parameter utamanya adalah mint koleksi, otoritas koleksi, dan PDA catatan otoritas koleksi. PDA catatan delegasi harus diberikan saat menggunakan otoritas koleksi yang didelegasikan untuk memastikan otoritas tersebut diizinkan mengelola NFT koleksi. Parameter metadata harus berisi objek koleksi dengan field alamat yang cocok dengan parameter mint koleksi dan field verified yang ditetapkan ke false. Pembuat juga dapat memverifikasi dirinya sendiri dengan menandatangani transaksi dan menambahkan dirinya sebagai akun yang tersisa.

Berikut cara mencetak NFT terkompresi dengan koleksi:

Kode
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);

Cuplikan kode ini dapat ditemukan dalam dokumentasi Metaplex tentang pencetakan cNFT dengan Bubblegum. Sekali lagi, kita menggunakan instans Umi untuk mencetak NFT terkompresi. Seperti mintV1, kita meneruskan leafOwner dan merkleTree. Namun, kali ini kita meneruskan collectionMint. Dalam field metadata, kita meneruskan objek collection dengan key yang cocok dengan collectionMint dan field verified yang ditetapkan ke false. Perhatikan bahwa identitas Umi ditetapkan sebagai otoritas koleksi default. Ini dapat diubah dengan menetapkan field opsional collectionAuthority ke otoritas koleksi khusus.

Mencetak cNFT dengan Helius

Di Helius, kami menyediakan Mint API yang memungkinkan Anda mencetak NFT terkompresi tanpa kerumitan tambahan. Kami menanggung biaya Solana, menangani pembuatan Merkle tree, dan mengunggah metadata off-chain Anda ke Arweave. Kami juga memastikan transaksi berhasil dikirim dan dikonfirmasi oleh jaringan, sehingga Anda tidak perlu melakukan polling sendiri. Selain itu, kami mengurai ID aset dari transaksi agar Anda dapat langsung menggunakannya dengan DAS API.

Agar Helius dapat mencetak NFT ke dalam koleksi Anda, otoritas koleksi harus didelegasikan kepadanya. Otoritas tersebut harus didelegasikan ke salah satu account berikut, sesuai cluster Anda:

  • Devnet: 2LbAtCJSaHqTnP9M5QSjvAMXk79RNLusFspFN5Ew67TC
  • Mainnet: HnT5KVAywGgQDhmh6Usk4bxRg4RwKxCK4jmECyaDth5R

Berikut cara mencetak cNFT menggunakan Helius Mint API:

Kode
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();

Cuplikan kode ini dan uraian lebih lanjut tentang skema permintaan dapat ditemukan dalam dokumentasi kami.

Perhatikan bahwa jika Anda tidak mengisi kolom uri, kami akan membuat file JSON dan mengunggahnya ke Arweave untuk Anda. File tersebut akan mengikuti standar JSON Metaplex v1.0 dan diunggah melalui Irys (sebelumnya dikenal sebagai Bundlr).

Mentransfer cNFT

Secara umum, langkah-langkah untuk mentransfer NFT terkompresi adalah sebagai berikut:

  • Dapatkan data aset cNFT dari indexer
  • Dapatkan bukti cNFT dari indexer
  • Dapatkan account concurrent Merkle tree dari Solana
  • Siapkan bukti aset
  • Buat dan kirim transaksi transfer

Penggunaan Umi dan Metaplex sangat menyederhanakan proses ini, tetapi bagian ini akan menunjukkan apa yang terjadi di balik layar. Di bawah ini, kami akan menjelaskan cara mentransfer NFT terkompresi menggunakan web3.js dan Metaplex.

Mentransfer dengan Berinteraksi Langsung dengan Bubblegum

Kita perlu mengambil beberapa informasi tentang NFT terkompresi sebelum menggunakan skrip untuk menjalankan transfer. Pertama, kita perlu menggunakan metode getAsset pada DAS API untuk mengambil metadata NFT terkompresi. Di sini, kita mencari data_hash, creator_hash, owner, delegate, dan leaf_id:

Kode
// 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();

Berikut tampilan sebagian respons yang berhasil:

Kode
{
  ...
  },
  "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",
    ...
  }
}

Setelah memperoleh informasi yang diperlukan, kita perlu menggunakan metode getAssetProof untuk mengambil proof dan tree_id (alamat tree). Berikut contoh pemanggilannya:

Kode
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();

Berikut tampilan respons yang berhasil:

Kode
{
  "root": "string",
  "proof": [
    "string"
  ],
  "node_index": 0,
  "leaf": "string",
  "tree_id": "string"
}

Sekarang, dengan root, proof, dan tree_id, kita dapat melanjutkan ke skrip transfer.

Kode Lengkap

Kode
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}`);
  }
};

Menguraikan Kode

Kode
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";

Kita mengimpor @solana/web3.js, @metaplex-foundation/mpl-bubblegum, dan @solana/spl-account-compression beserta modul yang diperlukan.

Kode
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
}

Kita mendefinisikan fungsi transferCompressedNFT untuk mengurai jalur bukti, membuat instruksi transfer, dan menjalankannya.

Kode
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);

  const treeAuthority = treeAccount.getAuthority();
  const canopyDepth = treeAccount.getCanopyDepth();

Kita mengambil account concurrent Merkle tree dari blockchain, lalu mengekstrak otoritas tree dan kedalaman canopy. Nilai-nilai ini diperlukan untuk membuat instruksi transfer.

Kode
const proofPath: AccountMeta[] = proof
    .map((node: string) => ({
      pubkey: new PublicKey(node),
      isSigner: false,
      isWritable: false,
    }))
    .slice(0, proof.length - (!!canopyDepth ? canopyDepth : 0));

Sederhananya, kita mengurai daftar alamat bukti menjadi array valid bertipe AccountMeta. AccountMeta adalah metadata account yang digunakan untuk mendefinisikan transaksi. Metadata ini mencakup public key account, apakah suatu instruksi memerlukan tanda tangan transaksi yang cocok dengan public key tersebut, dan apakah public key dapat dimuat sebagai account baca-tulis.

Kita mengambil sebagian dari bukti lengkap, mulai dari awal array, dan memastikan hanya ada sejumlah proof.length - canopyDepth nilai bukti. Ini dilakukan untuk menghapus bagian tree yang sudah tersimpan dalam cache canopy on-chain. Kemudian, kita menyusun setiap nilai bukti yang tersisa sebagai AccountMeta yang valid. Hal ini dilakukan karena bukti dikirimkan secara on-chain dalam bentuk “account tambahan” di dalam instruksi transfer.

Kode
const leafOwner = new PublicKey(owner);
const leafDelegate = new PublicKey(delegate);

Kemudian, kita menetapkan leafOwner ke parameter owner dan leafDelegate ke parameter delegate.

Kode
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
  );

Kita membuat transferInstruction menggunakan fungsi pembantu createTransferInstruction dari Bubblegum SDK. Perhatikan bahwa root, dataHash, dan creatorHash dikembalikan oleh DAS API sebagai string, sehingga kita harus mengonversinya menjadi tipe PublicKey, lalu menjadi array byte.

Kode
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}`);
  }

Setelah instruksi dibuat, kita menambahkannya ke transaksi baru dan mengirimkannya ke Solana. Jika terjadi error, kita mencatatnya ke konsol menggunakan console.error.

Jika Anda mengalami error terkait concurrent Merkle tree, RPC Anda mungkin memberikan data yang usang atau salah untuk bukti concurrent Merkle tree. Hal ini terkadang dapat terjadi karena masalah cache. Untuk mengatasinya, Anda dapat mencoba memverifikasi bukti yang diberikan RPC di sisi klien:

Kode
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());

Perhatikan bahwa Anda juga perlu menggunakan nilai leaf yang dikembalikan oleh pemanggilan DAS API getAssetProof. Langkah ini tidak wajib karena validasi bukti yang sebenarnya dilakukan secara on-chain. Namun, langkah ini dapat membantu menangani error.

Setelah itu, Anda dapat melakukan pemanggilan getAsset lagi untuk melihat bahwa leafDelegate memiliki nilai kosong dan leaf tersebut memiliki pemilik baru!

Mentransfer dengan Umi

Kode
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);

Kode ini berasal dari dokumentasi Metaplex tentang transfer NFT terkompresi.

Bubblegum menyediakan instruksi transfer yang sangat mudah digunakan. Pertama, instruksi ini menerima instance Umi. Kemudian, instruksi ini menerima objek yang berisi aset beserta informasi tentang buktinya, pemilik leaf, dan pemilik leaf baru. Untuk mendapatkan aset beserta bukti yang diperlukan, kita dapat menggunakan metode getAssetWithProof yang juga disediakan oleh Bubblegum. Perhatikan bahwa delegasi leaf dapat digunakan sebagai pengganti pemilik leaf—yang diperlukan hanyalah account dengan otoritas untuk mengesahkan transfer. Dengan metode .sendAndConfirm(), kita mengirimkan transaksi yang memulai transfer, lalu mengonfirmasinya dengan instance Umi.

Kesimpulan

Selamat! Kita telah membahas kompresi state dan NFT terkompresi di Solana secara sangat menyeluruh. Kita telah menelusuri kompleksitas concurrent Merkle tree, meluruskan kesalahpahaman umum, dan mendalami ledger Solana. Di luar teori, kita telah mempelajari cara mengambil, mencetak, dan mentransfer cNFT dengan memanfaatkan Solana web3.js, Metaplex, dan Helius!

Kompresi state Solana merupakan terobosan di tengah kondisi biaya transaksi dan penyimpanan yang dapat menjadi hambatan. Kompresi memangkas biaya secara drastis tanpa mengorbankan keamanan atau desentralisasi. Ini adalah perubahan paradigma yang membuka kemungkinan baru yang belum pernah ada bagi seniman, kolektor, dan developer.

Jika Anda berhasil sampai sejauh ini, terima kasih, anon! Anda kini siap berkontribusi pada ranah baru yang menarik ini. Silakan melangkah maju—cetak koleksi sepuluh juta NFT untuk MMORPG on-chain Anda, buat aplikasi terdesentralisasi yang memanfaatkan kekuatan ledger, atau cukup bagikan pengetahuan baru Anda kepada komunitas. Cara terbaik untuk memprediksi masa depan adalah dengan menciptakannya.

Referensi Tambahan / Bacaan Lebih Lanjut

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan

Gambar diperbesar