
「Hybrid Collection」を作成してみよう!
こちらのガイドでは「Hybrid Collection」の作成方法を解説します。必要なアセットの作成方法から、エスクローの作成、FTとNFTを相互に交換するためのパラメーター設定まで、すべての手順を説明します。
MPL-Hybridとは何か?
MPL-Hybridは、デジタルアセット・web3ゲーム・オンチェーンコミュニティのための新しいモデルです。このモデルの中核となるのは、一定数のFTとNFTを相互に交換できる「スワッププログラム」です。
前提条件
お好みのコードエディタ(Visual Studio Code推奨)
Node 18.x.x以上
初期設定
このガイドでは、JavaScriptを使用してHybrid Collectionを作成する方法を説明します。必要に応じて、関数の修正や配置を変更することができます。
プロジェクトの初期化
お好みのパッケージマネージャー(npm、yarn、pnpm、bun)を使用して新規プロジェクトを初期化します(任意)プロンプトが表示されたら、必要な情報を入力してください。
npm init
必要なパッケージ
このガイドで使用するパッケージをインストールしてください。
@metaplex-foundation/umi
@metaplex-foundation/umi-bundle-defaults
@metaplex-foundation/mpl-core
@metaplex-foundation/mpl-hybrid
@metaplex-foundation/mpl-token-metadata
npm i @metaplex-foundation/umi
npm i @metaplex-foundation/umi-bundle-defaults
npm i @metaplex-foundation/mpl-core
npm i @metaplex-foundation/mpl-hybrid
npm i @metaplex-foundation/mpl-token-metadata
準備
MPL-Hybridプログラムのエスクローをセットアップする前に、FTとNFTを相互に交換できるようにするために、Core NFTのコレクションとFTの両方が既にミントされている必要があります。
これらの前提条件が不足している場合でも心配いりません。各ステップに必要なリソースをすべて提供します。
注意:エスクローを機能させるためには、NFT、FT、またはその両方を組み合わせて資金を提供する必要があります。エスクローのバランスを維持する最も簡単な方法は、一方の種類のアセットでエスクローを完全に満たし、もう一方を配布することです。
NFTコレクションの作成
MPL-Hybridプログラムのメタデータランダム化機能を利用するには、オフチェーンのメタデータURIが一貫性のある段階的な構造に従う必要があります。このため、Turbo SDKと組み合わせてArweaveのpath manifest機能を使用します。
Manifestを使用すると、複数のトランザクションを単一のベーストランザクションIDの下にリンクし、以下のような我々が読みやすいファイル名を割り当てることができます:
deterministic URIの作成に不慣れな場合は、このガイドで詳細な手順を確認できます。また、Hybridプログラムの動作に必要なコレクションとアセットの作成手順も確認できます。
注意:現在、MPL-Hybridプログラムは提供されたURI indexの最小値と最大値の間からランダムに数値を選択しますが、そのURIが既に使用されているかどうかはチェックしません。そのため、スワップはBirthday Paradoxの影響を受けます。プロジェクトがスワップのランダム化の恩恵を受けるためには、ランダムに選択可能な最低25万個のアセットメタデータを準備し、アップロードすることを推奨します。利用可能な潜在的アセットが多いほど良いでしょう。
FTの作成
MPL-Hybridエスクローでは、NFTの償還等に使用するFTが必要です。これは、既にミントされて流通している既存のトークンでも、まったく新しいものでも構いません。
トークンの作成に不慣れな場合は、このガイドを参照してSolana上で独自のFTをミントする方法を学ぶことができます。
エスクローの作成
NFTとFTの両方を作成したら、いよいよエスクローを作成してスワップを開始する準備が整いました。
しかし、MPL-Hybridに関する情報に入る前に、このガイドで何度も使用することになるUmiインスタンスのセットアップ方法を学んでおくことをお勧めします。
Umiのセットアップ
Umiのセットアップでは、異なるソースからキーペア/ウォレットを使用または生成することができます。テスト用に新しいウォレットを作成したり、ファイルシステムから既存のウォレットをインポートしたり、ウェブサイト/dAppを作成する場合は`walletAdapter`を使用したりできます。
注意:この例では`generatedSigner()`でUmiをセットアップしますが、すべてのセットアップ方法は、以下から確認できます。
With a New Wallet
const umi = createUmi('https://api.devnet.solana.com')
const signer = generateSigner(umi)
umi.use(signerIdentity(signer))
// This will airdrop SOL on devnet only for testing.
console.log('Airdropping 1 SOL to identity')
umi.rpc.airdrop(umi.identity.publicKey, sol(1));
With an Existing Wallet
const umi = createUmi('https://api.devnet.solana.com')
// You will need to us fs and navigate the filesystem to
// load the wallet you wish to use via relative pathing.
const walletFile = fs.readFileSync('./keypair.json')
// Convert your walletFile onto a keypair.
let keypair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(walletFile));
// Load the keypair into umi.
umi.use(keypairIdentity(keypair));
With the Wallet Adapter
import { walletAdapterIdentity } from '@metaplex-foundation/umi-signer-wallet-adapters'
import { useWallet } from '@solana/wallet-adapter-react'
const wallet = useWallet()
const umi = createUmi('https://api.devnet.solana.com')
// Register Wallet Adapter to Umi
.use(walletAdapterIdentity(wallet))
注意:`walletAdapter`のセクションでは、既に`walletAdapter`がインストールおよびセットアップされていることを前提として、Umiに接続するために必要なコードのみを提供しています。包括的なガイドについては、こちらを参照してください。
パラメータの設定
Umi instanceのセットアップ後、次のステップはMPL-Hybridエスクローに必要なパラメータを設定することです。
まずはエスクローコントラクトの設定から始めましょう:
// Escrow Settings - Change these to your needs
const name = "MPL-404 Hybrid Escrow";
const uri = "https://arweave.net/manifestId";
const max = 15;
const min = 0;
const path = 0;
Name:エスクローコントラクトの名前(例:MPL-404 Hybrid Escrow)URI:NFTコレクションのベースURI。これはdeterministicなメタデータ構造に従う必要があります。
Max & Min:コレクションのメタデータに関するdeterministic URIの範囲を定義します。
Path:2つのパスから選択します:`0`はスワップ時にNFTメタデータを更新、`1`はスワップ後もメタデータを変更しないようにします。
次に、エスクローに必要な主要アカウントを設定します:
// Escrow Accounts - Change these to your needs
const collection = publicKey('<YOUR-COLLECTION-ADDRESS>');
const token = publicKey('<YOUR-TOKEN-ADDRESS>');
const feeLocation = publicKey('<YOUR-FEE-ADDRESS>');
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
]);
Collection:スワップの対象となるコレクション。これはNFTコレクションのアドレスです。
Token:スワップの対象となるトークン。これはFTのアドレスです。
Fee Location:スワップから発生する手数料の送付先アドレス。
Escrow:スワップ処理中にNFTとFTを保持する役割を担う、派生エスクローアカウント。
最後にトークン関連のパラメータを定義し、小数点のためのトークン量を調整する`addZeros()` helper関数を作成します:
// Token Swap Settings - Change these to your needs
const tokenDecimals = 6;
const amount = addZeros(100, tokenDecimals);
const feeAmount = addZeros(1, tokenDecimals);
const solFeeAmount = addZeros(0, 9);
// Function that adds zeros to a number, needed for adding the correct amount of decimals
function addZeros(num: number, numZeros: number): number {
return num * Math.pow(10, numZeros)
}
Amount:小数点を考慮した、スワップ時にユーザーが受け取るトークンの量。
Fee Amount:NFTへのスワップ時にユーザーが支払うトークン手数料の量。
Sol Fee Amount:NFTへのスワップ時に課される追加手数料(SOL単位)Solanaの9桁の小数点を考慮して調整されます。
エスクローの初期化
これで、設定したすべてのパラメータと変数を渡して`initEscrowV1()`メソッドを使用し、エスクローを初期化できます。これにより、独自のMPL-Hybridエスクローが作成されます。
const initEscrowTx = await initEscrowV1(umi, {
name,
uri,
max,
min,
path,
escrow,
collection,
token,
feeLocation,
amount,
feeAmount,
solFeeAmount,
}).sendAndConfirm(umi);
const signature = base58.deserialize(initEscrowTx.signature)[0]
console.log(`Escrow created! https://explorer.solana.com/tx/${signature}?cluster=devnet`)
注意:前述の通りエスクローを作成しただけでは、スワップの準備は整いません。NFTまたはFT(あるいはその両方)でエスクローに資金を提供する必要があります。方法は以下の通りです:
Send Assets to the Escrow
import { transfer } from '@metaplex-foundation/mpl-core'
// Derive the Escrow
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
])[0];
// Transfer Asset to it
const transferAssetTx = await transfer(umi, {
asset,
collection,
newOwner: escrow
}).sendAndConfirm(umi);
Send Fungible Tokens to the Escrow
import { transactionBuilder } from '@metaplex-foundation/umi'
import { createTokenIfMissing, transferTokens } from '@metaplex-foundation/mpl-toolbox'
// Derive the Escrow
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
])[0];
// Transfer Fungible Tokens to it (after creating the ATA if needed)
const transferTokenTx = await transactionBuilder().add(
createTokenIfMissing(umi, {
mint: token,
owner: escrow
})
).add(
transferTokens(umi, {
source: findAssociatedTokenPda(umi, { mint: token, owner: umi.identity.publicKey }),
destination: findAssociatedTokenPda(umi, { mint: token, owner: escrow }),
amount,
})
).sendAndConfirm(umi)
フルコード例
エスクロー作成のための完全なコードをコピー&ペーストで使用したい場合は、以下をご利用ください。
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { publicKey, signerIdentity, generateSigner, sol } from '@metaplex-foundation/umi'
import { mplHybrid, MPL_HYBRID_PROGRAM_ID, initEscrowV1 } from '@metaplex-foundation/mpl-hybrid'
import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'
import { string, base58, publicKey as publicKeySerializer } from '@metaplex-foundation/umi/serializers'
(async () => {
/// Step 1: Setup Umi
const umi = createUmi('https://api.devnet.solana.com')
.use(mplHybrid())
.use(mplTokenMetadata())
let signer = generateSigner(umi);
umi.use(signerIdentity(signer)).rpc.airdrop(umi.identity.publicKey, sol(1));
/// Step 2: Setup the Escrow
// Escrow Settings - Change these to your needs
const name = "MPL-404 Hybrid Escrow"; // The name of the escrow
const uri = "https://arweave.net/manifestId"; // The base URI of the collection
const max = 15; // The max URI
const min = 0; // The min URI
const path = 0; // 0: Update Nft on Swap, 1: Do not update Nft on Swap
// Escrow Accounts - Change these to your needs
const collection = publicKey('<YOUR-COLLECTION-ADDRESS>'); // The collection we are swapping to/from
const token = publicKey('<YOUR-TOKEN-ADDRESS>'); // The token we are swapping to/from
const feeLocation = publicKey('<YOUR-FEE-ADDRESS>'); // The address where the fees will be sent
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
]); // The derived escrow account
// Token Swap Settings - Change these to your needs
const tokenDecimals = 6; // The decimals of the token
const amount = addZeros(100, tokenDecimals); // The amount the user will receive when swapping
const feeAmount = addZeros(1, tokenDecimals); // The amount the user will pay as fee when swapping to NFT
const solFeeAmount = addZeros(0, 9); // Additional fee to pay when swapping to NFTs (Sol has 9 decimals)
/// Step 3: Create the Escrow
const initEscrowTx = await initEscrowV1(umi, {
name,
uri,
max,
min,
path,
escrow,
collection,
token,
feeLocation,
amount,
feeAmount,
solFeeAmount,
}).sendAndConfirm(umi);
const signature = base58.deserialize(initEscrowTx.signature)[0]
console.log(`Escrow created! https://explorer.solana.com/tx/${signature}?cluster=devnet`)
})()
// Function that adds zeros to a number, needed for adding the correct amount of decimals
function addZeros(num: number, numZeros: number): number {
return num * Math.pow(10, numZeros)
}
Capture & Release
アカウントのセットアップ
Umiのセットアップ(前のセクションで行ったように)の後、次のステップは`Capture`と`Release`プロセスに必要なアカウントを設定することです。これらのアカウントは、先ほど使用したものと似ているため馴染みがあるでしょう。また、両方の命令で同じアカウントを使用します:
// Step 2: Escrow Accounts - Change these to your needs
const collection = publicKey('<YOUR-COLLECTION-ADDRESS>');
const token = publicKey('<YOUR-TOKEN-ADDRESS>');
const feeProjectAccount = publicKey('<YOUR-FEE-ADDRESS>');
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
]);
注意:`feeProjectAccount`は、前回のスクリプトの`feeLocation`フィールドと同じものです。
Capture/Releaseするアセットの選択
CaptureとReleaseするアセットの選び方は、エスクロー作成時に選択したパスによって異なります:
パス 0:パスが`0`に設定されている場合、スワップ中にNFTメタデータが更新されるため、どのアセットでも問題ないのでエスクローからランダムに選択できます。
パス 1:パスが`1`に設定されている場合、スワップ後もNFTメタデータは変更されないため、ユーザーが特定のスワップ対象のNFTを選択できます。
Captureについて
NFTをCaptureする場合、以下の方法でエスクローが所有するアセットの中からランダムに選択することができます。
// Fetch all the assets in the collection
const assetsListByCollection = await fetchAssetsByCollection(umi, collection, {
skipDerivePlugins: false,
})
// Find the assets owned by the escrow
const asset = assetsListByCollection.filter(
(a) => a.owner === publicKey(escrow)
)[0].publicKey
Releaseについて
NFTをReleaseする場合、通常はユーザーが希望するものを選択することができます。ただし、この例では、ユーザーが所有するアセットの中からランダムに選択することができます。
// Fetch all the assets in the collection
const assetsListByCollection = await fetchAssetsByCollection(umi, collection, {
skipDerivePlugins: false,
})
// Usually the user choose what to exchange
const asset = assetsListByCollection.filter(
(a) => a.owner === umi.identity.publicKey
)[0].publicKey
Capture(FT→NFT)
最後に、Captureについて説明します。これは、FTを使ってNFTと交換するプロセスです(交換に必要なトークンの量は、エスクロー作成時に設定されます)
// Capture an NFT by swapping fungible tokens
const captureTx = await captureV1(umi, {
owner: umi.identity.publicKey,
escrow,
asset,
collection,
token,
feeProjectAccount,
amount,
}).sendAndConfirm(umi);
const signature = base58.deserialize(captureTx.signature)[0];
console.log(`Captured! Check it out: https://explorer.solana.com/tx/${signature}?cluster=devnet`);
Release(NFT→FT)
Releaseは、Captureの逆の操作です。NFTをFTと交換します。
// Release an NFT and receive fungible tokens
const releaseTx = await releaseV1(umi, {
owner: umi.payer,
escrow,
asset,
collection,
token,
feeProjectAccount,
}).sendAndConfirm(umi);
const signature = base58.deserialize(releaseTx.signature)[0];
console.log(`Released! Check it out: https://explorer.solana.com/tx/${signature}?cluster=devnet`);
フルコード例
以下が、`Capture`と`Release`のフルコードです。
Capture
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { generateSigner, signerIdentity, publicKey, sol } from '@metaplex-foundation/umi'
import { mplHybrid, MPL_HYBRID_PROGRAM_ID, captureV1 } from '@metaplex-foundation/mpl-hybrid'
import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'
import { base58, string, publicKey as publicKeySerializer } from '@metaplex-foundation/umi/serializers'
import { fetchAssetsByCollection } from '@metaplex-foundation/mpl-core'
(async () => {
/// Step 1: Setup Umi
const umi = createUmi('https://api.devnet.solana.com')
.use(mplHybrid())
.use(mplTokenMetadata())
let signer = generateSigner(umi);
umi.use(signerIdentity(signer)).rpc.airdrop(umi.identity.publicKey, sol(1));
// Step 2: Escrow Accounts - Change these to your needs
const collection = publicKey('<YOUR-COLLECTION-ADDRESS>'); // The collection we are swapping to/from
const token = publicKey('<YOUR-TOKEN-ADDRESS>'); // The token we are swapping to/from
const feeProjectAccount = publicKey('<YOUR-FEE-ADDRESS>'); // The address where the fees will be sent
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
]);
// Fetch all the assets in the collection
const assetsListByCollection = await fetchAssetsByCollection(umi, collection, {
skipDerivePlugins: false,
})
// Find the assets owned by the escrow
const asset = assetsListByCollection.filter(
(a) => a.owner === publicKey(escrow)
)[0].publicKey
/// Step 3: "Capture" (Swap from Fungible to Non-Fungible) the Asset
const captureTx = await captureV1(umi, {
owner: umi.payer,
escrow,
asset,
collection,
token,
feeProjectAccount,
}).sendAndConfirm(umi)
const signature = base58.deserialize(captureTx.signature)[0]
console.log(`Captured! https://explorer.solana.com/tx/${signature}?cluster=devnet`)})();
Release
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { generateSigner, signerIdentity, publicKey, sol } from '@metaplex-foundation/umi'
import { mplHybrid, MPL_HYBRID_PROGRAM_ID, releaseV1 } from '@metaplex-foundation/mpl-hybrid'
import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'
import { base58, string, publicKey as publicKeySerializer } from '@metaplex-foundation/umi/serializers'
import { fetchAssetsByCollection } from '@metaplex-foundation/mpl-core'
import walletFile from "/Users/leo/.config/solana/id.json";
(async () => {
/// Step 1: Setup Umi
const umi = createUmi('https://api.devnet.solana.com')
.use(mplHybrid())
.use(mplTokenMetadata())
let signer = generateSigner(umi);
umi.use(signerIdentity(signer)).rpc.airdrop(umi.identity.publicKey, sol(1));
// Step 2: Escrow Accounts - Change these to your needs
const collection = publicKey('<YOUR-COLLECTION-ADDRESS>'); // The collection we are swapping to/from
const token = publicKey('<YOUR-TOKEN-ADDRESS>'); // The token we are swapping to/from
const feeProjectAccount = publicKey('<YOUR-FEE-ADDRESS>'); // The address where the fees will be sent
const escrow = umi.eddsa.findPda(MPL_HYBRID_PROGRAM_ID, [
string({ size: 'variable' }).serialize('escrow'),
publicKeySerializer().serialize(collection),
]);
// Fetch all the assets in the collection
const assetsListByCollection = await fetchAssetsByCollection(umi, collection, {
skipDerivePlugins: false,
})
// Usually the user choose what to exchange
const asset = assetsListByCollection.filter(
(a) => a.owner === umi.identity.publicKey
)[0].publicKey
/// Step 3: "Capture" (Swap from Fungible to Non-Fungible) the Asset
const releaseTx = await releaseV1(umi, {
owner: umi.payer,
escrow,
asset,
collection,
token,
feeProjectAccount,
}).sendAndConfirm(umi)
const signature = base58.deserialize(releaseTx.signature)[0]
console.log(`Released! https://explorer.solana.com/tx/${signature}?cluster=devnet`)
})();