読了時間:18分
概要
Solanaへの需要が高まるにつれ、ストレージコストの削減ニーズも高まっています。Solana 上で数千個のNFTをミントする場合、レンタル料金として数千ドルかかるSolana もしビジネスケースで数百万個以上のNFTが必要になった場合はどうでしょうか?ステート圧縮は、多数のアカウントを単一のアカウントに格納し、ストレージコストを削減できるツールです。 実質的に、Solanaを利用して、オフチェーンに保存されたデータを検証することが可能になります。これは、「マークルツリー」と呼ばれる暗号技術の概念を活用することで実現できます。このガイドでは、Solana 圧縮についてSolana 圧縮されたNFTのミントおよび取得方法Solana 解説します。
主な業務内容
- SolanaにおけるNFTの圧縮について学ぶ
- Solanaの開発用ネットワーク上で、圧縮されたNFTを発行し、取得するスクリプトを作成してください
必要なもの
- Solana に関する経験(ガイド:Solana 徹底解説)
- Node.js(バージョン 16.15 以降)がインストールされていること
- TypeScriptの使用経験があり、ts-nodeがインストールされている
- Quicknode Endpoint with DAS Add-on Installed:
DASアドオンを入手する
If you do not already have one, you will need a Quicknode account with a Solana endpoint. You can sign up for an account here.

To use the DAS API, you'll need to use a Solana endpoint with the DAS Add-on installed. You can install the DAS Add-on to your endpoint on your endpoint page (https://dashboard.quicknode.com/endpoints/YOUR_ENDPOINT_ID/add-ons).

Make sure to add the Digital Asset Standard Add-on to your endpoint before proceeding.
このガイドで使用されている依存関係
| 依存関係 | バージョン |
|---|---|
| @metaplex-foundation/デジタル資産標準API | ^1.0.0 |
| @metaplex-foundation/umi | ^0.8.10 |
| @metaplex-foundation/umi-bundle-defaults | ^0.8.10 |
| @metaplex-foundation/mpl-bubblegum | ^3.1.2 |
| @metaplex-foundation/mpl-token-metadata | ^3.1.2 |
| solana | ^0.2.0 |
| solana | ^0.3.11 |
| @solana/web3.js | ^1.87.6 |
さあ、始めましょう!
「圧縮NFT」とは何ですか?
Compressed NFTs leverage cryptographic techniques to efficiently store and verify large amounts of data on the blockchain. Two key concepts in this process are hashing and Merkle trees.
-
ハッシュ化:これは、NFTのメタデータや関連するメディアファイルなどの入力を、ハッシュと呼ばれる固定長のバイト列に変換するプロセスです。各ハッシュは一意であり、入力がわずかに変更されただけでもハッシュは大きく異なるため、ハッシュのみから元の入力を推測することはほぼ不可能です。この一意性により、NFTのデータが変更された場合、即座に気づくことができます。
-
マークル木 大規模なデータセットに含まれる個々のデータのハッシュ値を、データセットの内容を効率的かつ安全に検証できるように保存するために使用されるデータ構造です。個々のデータ(これを
葉) はハッシュ化され、別のハッシュと組み合わせられて新しいハッシュが生成されます。この処理は、ハッシュが1つだけ残るまで繰り返され、そのハッシュはルート. ルートハッシュは、データの完全性を検証するために使用されます。
出典:Solana Metaplex Foundation
上の簡単な例の画像で、それぞれの葉(例えば、 X8, X9, X10, X11) を個別のNFTとして。ルートハッシュは、 X2, はNFTのコレクション全体をコンパクトに表現したものです。ご覧の通り、 ルート はハッシュ処理から派生したものです X4 そして X5:
X4はハッシュ処理から派生したものですX8そしてX9、およびX5はハッシュ処理から派生したものですX10そしてX11.
単一のNFTを検証するには、コレクション全体ではなく、ルートまで遡る少数のハッシュさえあれば十分です。これは、数百や数千ものNFTを含むコレクションにおいて特に有用であり、譲渡や検証といった操作にかかるリソース負荷を軽減します。
圧縮NFTの主要な概念
圧縮NFTを作成するには、まずNFTデータを格納できるマークルツリーを作成する必要があります。そのためには、いくつかの重要なパラメータについて理解しておく必要があります:
-
深さ: これは、ルートからリーフに至るまでのマークルツリー内の階層数を表しており、各リーフはNFTとなる可能性があります。これは最終的に、ツリー内に保存したいNFTの数によって決まります。ツリーが深ければ深いほど、より多くのNFTを保存できますが、1つのNFTを検証するために必要なハッシュの数も増えます。 -
最大バッファサイズ: Since users on Solana might be modifying multiple NFTs in the same tree at once, we need to be able to support changes to the tree without one change invalidating another. Solana uses a special type of Merkle tree, called a concurrent Merkle tree, to support this. ThemaxBufferSizeこれにより、ツリーの証明に対する更新や変更を管理するための変更履歴が事実上設定される。 -
キャノピーの深さ: キャノピーとは、チェーン上にキャッシュされ保存されているプルーフノードの数を指します。キャノピーが大きいほど、NFTを検証するために取得しなければならないプルーフの数を減らすことができます。ここには、コストと構成可能性のバランスがあります。キャノピーを大きくすれば、取得が必要なプルーフの数は減り(その結果、プログラムがNFTとやり取りしやすくなります)、一方で、チェーン上にツリーを保存するためのコストも増加します。
それでは、さっそく作成してみましょう!
新しいプロジェクトの設定
mkdir compressed-nft && cd compressed-nft && echo > app.ts
圧縮されたNFTを作成および取得Solana 、MetaplexおよびSolana のいくつかのパッケージを使用します:
| 依存関係 | 説明 |
|---|---|
@metaplex-foundation/umi-bundle-defaults | Metaplex 用の事前設定済み Umi バンドル。 |
@metaplex-foundation/umi | Solana 向けのCore Umiフレームワーク。 |
@metaplex-foundation/mpl-token-metadata | Metaplex トークンのメタデータ契約ライブラリ。 |
@metaplex-foundation/mpl-bubblegum | NFTの圧縮用Metaplexライブラリ。 |
solana | アカウント圧縮のためのSolana ライブラリ。 |
@solana/web3.js | Solana's JavaScript API for interacting with the blockchain. |
@metaplex-foundation/デジタル資産標準API | Solana 資産データを取得するためのJS API。 |
必要な依存関係をインストールします:
yarn init -y
yarn add @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-bubblegum @solana/spl-account-compression @solana/web3.js@1 @metaplex-foundation/digital-asset-standard-api
または
npm init -y
npm i @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-bubblegum @solana/spl-account-compression @solana/web3.js@1 @metaplex-foundation/digital-asset-standard-api
依存関係のインポート
お好みのコードエディタでapp.tsを開き、1 行目に以下の import 文を追加してください:
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { none } from '@metaplex-foundation/umi';
import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata';
import {
mplBubblegum,
createTree,
fetchTreeConfigFromSeeds,
MetadataArgsArgs,
mintV1,
findLeafAssetIdPda
} from '@metaplex-foundation/mpl-bubblegum';
import {
getConcurrentMerkleTreeAccountSize,
ALL_DEPTH_SIZE_PAIRS,
} from "@solana/spl-account-compression";
import {
PublicKey,
Umi,
createSignerFromKeypair,
generateSigner,
keypairIdentity
} from '@metaplex-foundation/umi';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { DasApiAsset, dasApi } from '@metaplex-foundation/digital-asset-standard-api';
これらのインポートにより、 Umi (MetaplexのJSフレームワーク)のインスタンスを作成し、Solana プログラムを使用して圧縮されたNFTを作成・取得できるようになります。
Solanaへの接続を確立する
Connect to a Solana Cluster with Your Quicknode Endpoint
To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us.
なぜ、のプロジェクトの50%以上が Solana Quicknode を選んでQuicknode 無料トライアルを始めましょう こちら. ここでは、 Solana Devnet endpoint.
をコピーして HTTP プロバイダーのリンク:
Define your Solana endpoint (make sure to replace the endpoint with your own), and add it to below your imports:
const endpoint = "https://example.solana-devnet.quiknode.pro/123456/";
Umiインスタンスを作成する
To use Metaplex's Bubblegum program for minting Compressed NFTs, we must create an instance of Umi with our endpoint. Umi is a JS framework that makes interacting with the Solana blockchain easy. It provides a set of tools to help you build on Solana. First, you will need a keypair to sign transactions. You can create a new keypair using the solana コマンドラインツールを作成するか、既存のツールを使用します:
solana-keygen new --no-bip39-passphrase --outfile ./my-keypair.json
次のような出力が表示されるはずです:
Generating a new keypair
Wrote new keypair to ./my-keypair.json
========================================================================
pubkey: E9tb...NzT7 # 👈 THIS IS YOUR PUBLIC KEY
========================================================================
Head over to Quicknode Multi-chain Faucet and get some Devnet SOL to fund your account.
Below your endpoint, establish an instance of Umi with your secret key:
const umi = createUmi(endpoint)
.use(mplTokenMetadata())
.use(mplBubblegum());
.use(dasApi());
const secret = new Uint8Array(/* 📋 Paste your secret from my-keypair.json (e.g., [0, 0, ... 0, 0]) key here */);
const myKeypair = umi.eddsa.createKeypairFromSecretKey(secret);
const wallet = createSignerFromKeypair(umi, myKeypair);
umi.use(keypairIdentity(wallet));
この「Umi」インスタンスは、devnetクラスターへのトランザクション送信やDAS APIへのクエリ実行に使用されます。ここでは、以下の4つのプラグインを使用しています:
mplTokenMetadata(): このプラグインは、Metaplex Token Metadata プログラムと連携するための一連のメソッドを提供します。mplBubblegum(): このプラグインは、Metaplex Bubblegum プログラム(圧縮 NFT)と連携するための一連のメソッドを提供します。dasApi(): このプラグインは、Digital Asset Standard API と連携するための一連のメソッドを提供します。keypairIdentity(): This plug-in provides a set of methods to sign and send transctions with the Solana blockchain using our keypair.
NFTのメタデータを定義する
独自のメタデータを作成しても、以下の例を使用しても構いません。コード内に、 メタデータ オブジェクト:
const metadata: MetadataArgsArgs = {
name: 'QN Pixel',
symbol: 'QNPIX',
uri: "https://qn-shared.quicknode-ipfs.com/ipfs/QmQFh6WuQaWAMLsw9paLZYvTsdL5xJESzcoSxzb6ZU3Gjx",
sellerFeeBasisPoints: 500,
collection: none(),
creators: [],
};
独自のメタデータを使用したい場合は、 メタデータ カスタムメタデータを含むオブジェクト。をアップロードするには、 .json および画像ファイルをIPFSにアップロードするには、 Quicknode IPFS.
ヘルパー関数の定義
Merkleツリーの深さやバッファサイズを計算したり、アセットの詳細を出力したりするのに役立つヘルパー関数をいくつか作成しました。app.tsファイルに以下のコードを追加してください:
function calculateDepthForNFTs(nftCount: number): number {
let depth = 0;
while (2 ** depth < nftCount) {
depth++;
}
return depth;
}
function calcuateMaxBufferSize(nodes: number): number {
let defaultDepthPair = ALL_DEPTH_SIZE_PAIRS[0];
let maxDepth = defaultDepthPair.maxDepth;
const allDepthSizes = ALL_DEPTH_SIZE_PAIRS.flatMap(
(pair) => pair.maxDepth,
).filter((item, pos, self) => self.indexOf(item) == pos);
for (let i = 0; i <= allDepthSizes.length; i++) {
if (Math.pow(2, allDepthSizes[i]) >= nodes) {
maxDepth = allDepthSizes[i];
break;
}
}
return ALL_DEPTH_SIZE_PAIRS.filter((pair) => pair.maxDepth == maxDepth)?.[0]
?.maxBufferSize ?? defaultDepthPair.maxBufferSize;
}
async function printAsset(umi: Umi, assetId: PublicKey<string>, retries = 5, retryDelay = 5000) {
while (retries > 0) {
try {
const asset = await umi.rpc.getAsset(assetId);
printAssetDetails(asset, true, false);
return;
} catch (e) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
retries--;
}
}
}
function printAssetDetails(asset: DasApiAsset, showAttributes = true, showJson = false): void {
const { name, token_standard: standard, attributes } = asset.content.metadata;
const { compressed } = asset.compression;
const { json_uri, files } = asset.content;
const imgUrl = files?.find(file => file.mime === 'image/png' || file.mime === 'image/jpeg')?.uri;
console.table({
name,
standard,
compressed,
json_uri,
imgUrl
});
if (showAttributes && attributes) {
console.table(attributes);
}
if (showJson) {
console.log(JSON.stringify(asset, null, 2));
}
}
これらについて一つひとつ詳しく説明するわけではありませんが、各関数の機能について簡単に概要を説明します:
calculateDepthForNFTs(): この関数は、Merkleツリーに格納したいNFTの数に基づいて、そのツリーの深さを計算します。これは、NFTの数以上となる最小の2の累乗を求めることで計算されます。calcuateMaxBufferSize(): この関数は、マークルツリーのノード数に基づいて、そのツリーの最大バッファサイズを計算します。これは、あらかじめ定義された「深さ-サイズ」のペアのリスト(spl-account-compressionパッケージ),ALL_DEPTH_SIZE_PAIRS.printAssetDetails(): この関数は、アセットの詳細情報を解析し、読みやすい形式でコンソールに記録します。printAsset(): この関数は、リトライロジックを用いてDAS APIからアセットを取得します。アセットの取得は、各試行の間に5秒の間隔を空けて、最大5回までリトライされます。これは、アセットがミントされ、インデックスに登録されるのを待つ際に役立ちます。アセットが見つかったら、printAssetDetails()資産情報をコンソールに記録する。
主な機能を概説する
以下のコードをあなたの app.ts ファイルに、私たちの概要を記載する メイン これから作成する関数:
const main = async ({ nftCount, umi, metadata }: { nftCount: number, umi: Umi, metadata: MetadataArgsArgs}) => {
// 0 - Check Costs
console.log(`👾 Initializing a Merkle Tree for ${nftCount.toLocaleString()} Compressed NFTs.`);
// 1 - Create a Merkle Tree
console.log(` Creating Merkle Tree...${merkleTree.publicKey.toString()}`);
// 2 - Mint NFT
console.log(`🎨 Minting an example NFT`);
// 3 - Fetch NFT
console.log(` Fetching from the chain (this may take a few moments)...`);
}
main({ nftCount: 10_000, umi, metadata }).catch(console.error);
この関数は、このスクリプトのメインのエントリポイントとなります。この関数の役割は以下の通りです:
- マークルツリーの作成、
- NFTの発行、および
- チェーンからNFTを取得する。
さあ、作ってみよう!
事前確認
マークルツリーを作成する前に、その作成と保存にかかるコストを確認しておきましょう。これには、ツリーの深さとバッファサイズを計算すればよいです。以下のコードを、 メイン app.ts ファイルの関数:
// 0 - Check Costs
console.log(`👾 Initializing a Merkle Tree for ${nftCount.toLocaleString()} Compressed NFTs.`);
const balance = await umi.rpc.getBalance(umi.payer.publicKey);
console.log(` Wallet Balance: ◎${(Number(balance.basisPoints) / LAMPORTS_PER_SOL).toLocaleString()}`);
const merkleStructure = {
maxDepth: calculateDepthForNFTs(nftCount),
maxBufferSize: calcuateMaxBufferSize(nftCount),
canopyDepth: 0,
}
const canopyDepth = merkleStructure.maxDepth > 20 ? merkleStructure.maxDepth - 10 :
merkleStructure.maxDepth > 10 ? 10 :
Math.floor(merkleStructure.maxDepth / 2);
merkleStructure.canopyDepth = canopyDepth;
console.log(` Max Depth: ${merkleStructure.maxDepth}`);
console.log(` Max Buffer Size: ${merkleStructure.maxBufferSize}`);
console.log(` Canopy Depth: ${merkleStructure.canopyDepth}`);
const requiredSpace = getConcurrentMerkleTreeAccountSize(
merkleStructure.maxDepth,
merkleStructure.maxBufferSize,
merkleStructure.canopyDepth,
);
console.log(` Total size: ${requiredSpace.toLocaleString()} bytes.`);
const { basisPoints } = await umi.rpc.getRent(requiredSpace);
const storageCost = Number(basisPoints);
if (Number(balance.basisPoints) < storageCost) {
throw new Error(`Insufficient funds. Need at least ◎${(storageCost / LAMPORTS_PER_SOL).toLocaleString(undefined)} for storage`);
}
console.log(` Total cost: ◎ ${(storageCost / LAMPORTS_PER_SOL).toLocaleString(undefined)}`);
ここでは、いくつかのことを行っています:
- まず、私たちは
umi.rpc.getBalance()ウォレットの残高を確認するには。 - 次に、Merkle構造を定義します。
merkleStructure、これには当社のマークルツリーの深さ、バッファサイズ、およびキャノピーの深さが含まれます。キャノピーの深さとバッファサイズは、お客様がミントを希望するNFTの数に基づいて計算します。この計算には、当社のヘルパー関数を利用しています。 - また、ツリーの深さに基づいてキャノピーの深さを決定するためのロジックも作成します。これはコストと構成可能性のバランスを考慮したものであり、最終的にはプロジェクトの要件によって決まります。
- 最後に、以下の式を用いて、仕様に基づいたマークルツリーのサイズと必要なリースを算出します。
getConcurrentMerkleTreeAccountSize()そしてumi.rpc.getRent(). 次に、保管費用を賄うのに十分な資金があるかどうかを確認します。
それでは、マークルツリーの作成に移りましょう。
マークルツリーを作成する
次に、クラスターに対してツリーを作成するリクエストを送信する機能を追加しましょう。そして、ツリーが作成されたら、その設定を取得します。以下のコードを メイン 関数:
// 1 - Create a Merkle Tree
const merkleTree = generateSigner(umi);
console.log(` Creating Merkle Tree...${merkleTree.publicKey.toString()}`);
const builder = await createTree(umi, {
merkleTree,
maxDepth: merkleStructure.maxDepth,
maxBufferSize: merkleStructure.maxBufferSize,
canopyDepth: merkleStructure.canopyDepth,
});
console.log(` Sending request (this may take a few moments)...`);
const { blockhash, lastValidBlockHeight } = await umi.rpc.getLatestBlockhash();
await builder.sendAndConfirm(umi, {
send: { commitment: 'finalized' },
confirm: { strategy: { type: 'blockhash', blockhash, lastValidBlockHeight } },
});
let treeFound = false;
while (!treeFound) {
try {
const treeConfig = await fetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
treeFound = true;
console.log(`🌲 Merkle Tree created: ${merkleTree.publicKey.toString()}. Config:`)
console.log(` - Total Mint Capacity ${Number(treeConfig.totalMintCapacity).toLocaleString()}`);
console.log(` - Number Minted: ${Number(treeConfig.numMinted).toLocaleString()}`);
console.log(` - Is Public: ${treeConfig.isPublic}`);
console.log(` - Is Decompressible: ${treeConfig.isDecompressible}`);
} catch (error) {
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
これを詳しく見ていきましょう:
- まず、以下を使用して、マークルツリー用の新しい署名者を生成します。
generateSigner(). これが、マークルツリーを格納するアカウントとなります。 - そこで、Umiの
createTree()新しいマークルツリー命令を作成するための命令ビルダー。ここに、私たちのmerkleStructureビルダーへの引数として。 - その後、以下のコマンドを使用して、その指示をクラスタに送信します。
sendAndConfirm(). This will create the Merkle tree on the Solana blockchain. - 最後に、以下のコードを使用してツリーの設定を取得します。
fetchTreeConfigFromSeeds(). これにより、ツリーの構成(容量、鋳造済みNFTの数、公開されているか、展開可能かなど)が返されます。私たちは、一方、ツリーのインデックス作成には少し時間がかかる可能性があるため、見つからない場合は再試行するループです。
NFTを発行する
よし、Merkleツリーができたので、これでNFTを発行できます。以下のコードをあなたの メイン 関数:
// 2 - Mint NFT
console.log(`🎨 Minting an example NFT`);
const leafOwner = generateSigner(umi).publicKey;
await mintV1(umi, { leafOwner, merkleTree: merkleTree.publicKey, metadata }).sendAndConfirm(umi);
const assetId = findLeafAssetIdPda(umi, { merkleTree: merkleTree.publicKey, leafIndex: 0 });
console.log(`🍃 NFT Minted: ${assetId[0].toString()}`);
ここでは、新しいリーフ(NFTが保存される場所)用の新しいアカウントを作成し、以下を使用してNFTをミントします。 mintV1(). なお、私たちの leafOwner そして MerkleTree 公開鍵、および メタデータ NFTについて。ミントが完了したら、次に、以下の方法を使用してNFTのミントアドレス(アセットID)を取得します。 findLeafAssetIdPda().
NFTを取得する
最後に、DAS API を使ってチェーンから NFT を取得しましょう。これまでに多くの設定を済ませているため、あとは単に以下を呼び出すだけです。 printAsset() 当社の assetId. 以下のコードを メイン 関数:
// 3 - Fetch NFT
console.log(` Fetching from the chain (this may take a few moments)...`);
await printAsset(umi, assetId[0]);
DAS APIについてさらに詳しく知りたい場合は、DAS APIドキュメントおよびDAS APIガイドをご覧ください。
よくできました!このスクリプトの完全なコードはGitHubでご覧いただけます。
スクリプトを実行する
スクリプトを実行するには、ターミナルで次のコマンドを実行してください:
ts-node app.ts
次のような出力が表示されるはずです:
qn@guides compressed-nft % ts-node app
👾 Initializing a Merkle Tree for 10,000 Compressed NFTs.
Wallet Balance: ◎4.533
Max Depth: 14
Max Buffer Size: 64
Canopy Depth: 10
Total size: 97,272 bytes.
Total cost: ◎ 0.678
Creating Merkle Tree...H4tsJtvJqGaPAXkUJXqAfe3vsWp8zLewyKoscRkNyGpw
Sending request (this may take a few moments)...
🌲 Merkle Tree created: H4tsJtvJqGaPAXkUJXqAfe3vsWp8zLewyKoscRkNyGpw. Config:
- Total Mint Capacity 16,384
- Number Minted: 0
- Is Public: false
- Is Decompressible: 1
🎨 Minting an example NFT
🍃 NFT Minted: 5BS5Tk2N7516RK5ZdUBqJRYHVNofexjLk6qdfTKEWuCx
Fetching from the chain (this may take a few moments)...
┌────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ Values │
├────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ name │ 'QN Pixel' │
│ standard │ 'NonFungible' │
│ compressed │ true │
│ json_uri │ 'https://qn-shared.quicknode-ipfs.com/ipfs/QmQFh6WuQaWAMLsw9paLZYvTsdL5xJESzcoSxzb6ZU3Gjx' │
│ imgUrl │ 'https://qn-shared.quicknode-ipfs.com/ipfs/QmZkvx76VSidDznVhyRoPsRkJY6ujqrEMKte25ppAp9YV4' │
└────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────┬────────┬──────────────┐
│ (index) │ value │ trait_type │
├─────────┼────────┼──────────────┤
│ 0 │ 'Blue' │ 'Background' │
└─────────┴────────┴──────────────┘
🔥 よくやった!
これからも作り続けよう
Solana圧縮NFTの作成と取得に成功しました!このスクリプトを土台として、独自の圧縮NFTミントアプリケーションを構築することができます。
行き詰まってしまった場合や、質問がある場合、あるいは単に開発中のプロジェクトについて話したい場合は、DiscordやTwitterでぜひご連絡ください!
