
Token2022にMetadataを付与する — TokenMetadataとMetadataPointer【TokenExtension解説】
Solana Token2022のTokenMetadataとMetadataPointerを使い、Mint内にメタデータを直接埋め込む方法を解説。@solana/kitによるTypeScript実装をアンカー付きコードで紹介します。
公開日2026.01.29
更新日2026.01.29
はじめに
Token2022(Token Extensions Program)では、TokenMetadataとMetadataPointerという2つの拡張を使うことで、Mintアカウント内にトークン名・シンボル・画像URLなどのメタデータを直接埋め込めます。
従来はMetaplexなどの外部プログラムに依存していたメタデータ管理が、Solanaネイティブで実現できるようになりました。
この記事では、Token2022のMintにMetadataを付与し、メタデータ付きトークンをTypeScriptで発行するまでの手順を、コード付きで解説します。
📖 公式ドキュメント
本記事はSolana公式ドキュメントを参考に解説しています。 Metadata & Metadata Pointer Extensions | Solana
目次
対象読者
- Solanaの基本的な概念(アカウント、トランザクション)を理解している方
- TypeScriptの基礎知識がある方
- トークン発行に興味がある方
環境構築
プロジェクトの作成
mkdir token-extension-examples
cd token-extension-examples
pnpm initパッケージのインストール
3つのパッケージを使用します。
pnpm add @solana-program/system @solana-program/token-2022 @solana/kit
pnpm add -D tsx@solana/kit— Solana開発の基盤。RPC接続、トランザクション作成、署名、送信などの基本機能を含むパッケージです@solana-program/system— System Programのクライアント。アカウント作成に使います@solana-program/token-2022— Token-2022 Programのクライアント。Token2022やExtensionの操作で利用しますtsx— TypeScriptファイルの実行ツール
💡 @solana/kit について
ネット上にはweb3.jsを使用した例が多く存在しますが、現在は後継ライブラリである@solana/kitの利用が推奨されています。本記事ではkitを使用します。
ファイルの作成とローカルバリデータの起動
mkdir src
touch src/metadata.ts
surfpool start実装の全体像
この記事では3つのアカウントが登場します。
- Authority — Mintアカウントを生成する管理者アカウント(手数料支払者も兼ねる)
- Mint — Token2022のMintアカウント
- TokenAccount — ミントしたトークンを受け取るAuthority所有のトークンアカウント
実装は大きく3つのフェーズに分かれます。
事前準備 → RPC接続、Authority作成、Mintキーペア生成
Token2022命令作成 → Extension定義、Space/Rent計算、各種命令の作成
トランザクション実行 → 命令をまとめてトランザクションとして送信完成コード
まず完成コードの全体像を示します。コード内の [1]〜[10] のアンカー番号は、後続のセクションで詳しく解説します。
import { getCreateAccountInstruction } from '@solana-program/system'
import {
extension,
getInitializeAccountInstruction,
getInitializeMetadataPointerInstruction,
getInitializeMintInstruction,
getInitializeTokenMetadataInstruction,
getMintSize,
getMintToInstruction,
getTokenSize,
TOKEN_2022_PROGRAM_ADDRESS,
} from '@solana-program/token-2022'
import {
airdropFactory,
appendTransactionMessageInstructions,
assertIsTransactionWithBlockhashLifetime,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
some,
} from '@solana/kit'
// --- [1] RPC接続 ---
const rpc = createSolanaRpc('http://localhost:8899')
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://localhost:8900')
// --- [2] Authorityの作成とSOL付与 ---
const authority = await generateKeyPairSigner()
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: authority.address,
lamports: lamports(5_000_000_000n), // 5 SOL
commitment: 'confirmed',
})
// --- [3] Mintキーペア生成 ---
const mint = await generateKeyPairSigner()
// --- [4] Token Extensionの定義 ---
const metadataExtension = extension('TokenMetadata', {
updateAuthority: some(authority.address),
mint: mint.address,
name: 'OPOS',
symbol: 'OPS',
uri: 'https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json',
additionalMetadata: new Map().set('description', 'Only possible on Solana'),
})
const metadataPointerExtension = extension('MetadataPointer', {
authority: some(authority.address),
metadataAddress: some(mint.address),
})
// --- [5] SpaceとRentの計算 ---
const spaceWithoutTokenMetadataExtension = BigInt(getMintSize([metadataPointerExtension]))
const spaceWithTokenMetadataExtension = BigInt(
getMintSize([metadataPointerExtension, metadataExtension]),
)
const rent = await rpc.getMinimumBalanceForRentExemption(spaceWithTokenMetadataExtension).send()
// --- [6] MintAccount作成命令 ---
const createMintAccountInstruction = getCreateAccountInstruction({
payer: authority,
newAccount: mint,
lamports: rent,
space: spaceWithoutTokenMetadataExtension,
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
})
const initializeMetadataPointerInstruction = getInitializeMetadataPointerInstruction({
mint: mint.address,
authority: authority.address,
metadataAddress: mint.address,
})
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 9,
mintAuthority: authority.address,
freezeAuthority: authority.address,
})
const initializeMetadataInstruction = getInitializeTokenMetadataInstruction({
metadata: mint.address,
updateAuthority: authority.address,
mint: mint.address,
mintAuthority: authority,
name: 'OPOS',
symbol: 'OPS',
uri: 'https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json',
})
// --- [7] TokenAccount作成命令 ---
const tokenAccount = await generateKeyPairSigner()
const tokenAccountLen = BigInt(getTokenSize([]))
const tokenAccountRent = await rpc.getMinimumBalanceForRentExemption(tokenAccountLen).send()
const createTokenAccountInstruction = getCreateAccountInstruction({
payer: authority,
newAccount: tokenAccount,
lamports: tokenAccountRent,
space: tokenAccountLen,
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
})
const initializeTokenAccountInstruction = getInitializeAccountInstruction({
account: tokenAccount.address,
mint: mint.address,
owner: authority.address,
})
// --- [8] トークン発行命令 ---
const mintAmount = 1_000_000_000_000n // 1,000 tokens
const mintToInstruction = getMintToInstruction({
mint: mint.address,
token: tokenAccount.address,
mintAuthority: authority,
amount: mintAmount,
})
// --- [9] トランザクション構築・署名・送信 ---
const instructions = [
createMintAccountInstruction,
initializeMetadataPointerInstruction,
initializeMintInstruction,
initializeMetadataInstruction,
createTokenAccountInstruction,
initializeTokenAccountInstruction,
mintToInstruction,
]
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(authority, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx),
)
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage)
assertIsTransactionWithBlockhashLifetime(signedTransaction)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'confirmed',
skipPreflight: true,
})
// --- [10] 結果の出力 ---
const transactionSignature = getSignatureFromTransaction(signedTransaction)
console.log('【Explorer URL】')
console.log(
`https://explorer.solana.com/tx/${transactionSignature}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`,
)
console.log(
`https://explorer.solana.com/address/${mint.address}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`,
)
console.log(
`https://explorer.solana.com/address/${tokenAccount.address}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`,
)コード解説
以降、各アンカー番号に沿って解説していきます。
[1] RPC接続
const rpc = createSolanaRpc('http://localhost:8899')
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://localhost:8900')@solana/kitの2つの関数でローカルバリデータに接続します。
createSolanaRpc— HTTPでトランザクション送信やデータ取得に使用createSolanaRpcSubscriptions— WebSocketでトランザクション監視に使用。ポーリング不要でトランザクションの完了を検知できます
[2] Authorityの作成とSOL付与
const authority = await generateKeyPairSigner()
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: authority.address,
lamports: lamports(5_000_000_000n),
commitment: 'confirmed',
})generateKeyPairSignerで署名オブジェクト(秘密鍵と公開鍵のペア)を生成します。Authorityはトークンの管理者であり、トランザクション手数料の支払者も兼ねます。
lamportsはSOLの最小単位です。1 SOL = 1,000,000,000 lamportsなので、5 SOLは5_000_000_000nです。lamports()関数で型安全にラップします。
commitment: "confirmed"は、バリデータの過半数が確認した状態を指します。速度と信頼性のバランスが良い設定です。
[3] Mintキーペア生成
const mint = await generateKeyPairSigner()Mintアカウントのアドレスとして使うキーペアを生成します。Authorityと同様にgenerateKeyPairSignerを使います。
[4] Token Extensionの定義
const metadataExtension = extension('TokenMetadata', {
updateAuthority: some(authority.address),
mint: mint.address,
name: 'OPOS',
symbol: 'OPS',
uri: 'https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json',
additionalMetadata: new Map().set('description', 'Only possible on Solana'),
})
const metadataPointerExtension = extension('MetadataPointer', {
authority: some(authority.address),
metadataAddress: some(mint.address),
})@solana-program/token-2022のextension関数で、利用する拡張機能を定義します。
TokenMetadata
updateAuthority— メタデータを更新できる権限者。some()はシリアライズ時に「値がある」ことを明示する関数です。ここで書くTSは最終的にSolanaブロックチェーンのトランザクションとしてシリアライズされて送信されます。その際に値の有無を明確に伝えますmint— このメタデータが属するMintアカウントのアドレスname— トークン名(USDCなら「USD Coin」に相当)symbol— ティッカーシンボル(USDC、USDTのような呼称)uri— オフチェーンに置く詳細メタデータ(画像、description等)のJSON URLadditionalMetadata— オンチェーンに置く追加データ。大きなデータを置くとRentが高くなるため、少量の重要データだけに留めます
MetadataPointer
メタデータがどこに格納されているかを示すポインタです。
authority— ポインタの参照先を変更できる権限者metadataAddress— メタデータの格納先。Mint自身に格納する場合はmint.addressを指定しますが、別のアカウントを指定することも可能です
[5] SpaceとRentの計算
const spaceWithoutTokenMetadataExtension = BigInt(getMintSize([metadataPointerExtension]))
const spaceWithTokenMetadataExtension = BigInt(
getMintSize([metadataPointerExtension, metadataExtension]),
)
const rent = await rpc.getMinimumBalanceForRentExemption(spaceWithTokenMetadataExtension).send()Solanaではアカウントを永続化するために、必要なサイズに応じたSOL(Rent)を預け入れる必要があります。
ここで2種類のサイズを計算しているのは、TokenMetadataが可変長データであることに起因します。
spaceWithoutTokenMetadataExtension— アカウント作成時のSpaceに使う。TokenMetadataは可変長なので事前に容量を確保しないspaceWithTokenMetadataExtension— Rent計算に使う。Rentが不足するとアカウントが消滅するリスクがあるため、全Extensionを含めて計算する
💡 TokenMetadataだけ特殊な計算が必要
TokenMetadataは可変長データのため、Space計算とRent計算で扱いが異なります。他のExtension(Transfer Fee、Permanent Delegateなど)は固定長なので、このような使い分けは不要です。
Rent額はネットワークやアップグレードで変わる可能性があるため、ハードコーディングせずRPCから取得するのが推奨です。
[6] MintAccount作成命令
// アカウント作成
const createMintAccountInstruction = getCreateAccountInstruction({
payer: authority,
newAccount: mint,
lamports: rent,
space: spaceWithoutTokenMetadataExtension, // TokenMetadataを除いたサイズ
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
})
// MetadataPointer初期化
const initializeMetadataPointerInstruction = getInitializeMetadataPointerInstruction({
mint: mint.address,
authority: authority.address,
metadataAddress: mint.address,
})
// Mint初期化
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 9,
mintAuthority: authority.address,
freezeAuthority: authority.address,
})
// TokenMetadata初期化
const initializeMetadataInstruction = getInitializeTokenMetadataInstruction({
metadata: mint.address,
updateAuthority: authority.address,
mint: mint.address,
mintAuthority: authority,
name: 'OPOS',
symbol: 'OPS',
uri: 'https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/DeveloperPortal/metadata.json',
})4つの命令を順に作成します。
getCreateAccountInstruction
Solanaに新しいアカウントを作る命令です。programAddressにはToken2022のアドレスを指定します。Token2022はSolanaの標準プログラムなので、localhost/dev/mainどの環境でもアドレスは変わりません。
getInitializeMetadataPointerInstruction
MetadataPointerを初期化します。「メタデータはMint自身の中にある」と指定しています。
getInitializeMintInstruction
Mintの基本情報を設定します。decimalsはトークンの最小桁数で、SOLは9、USDCは6が一般的です。mintAuthorityはトークン発行権限、freezeAuthorityはトークン凍結権限を持つアカウントです。
getInitializeTokenMetadataInstruction
メタデータを書き込む命令です。mintAuthorityには署名オブジェクト(authority)を渡す点に注意してください(アドレスではなくオブジェクトを渡します)。
[7] TokenAccount作成命令
const tokenAccount = await generateKeyPairSigner()
const tokenAccountLen = BigInt(getTokenSize([]))
const tokenAccountRent = await rpc.getMinimumBalanceForRentExemption(tokenAccountLen).send()
const createTokenAccountInstruction = getCreateAccountInstruction({
payer: authority,
newAccount: tokenAccount,
lamports: tokenAccountRent,
space: tokenAccountLen,
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
})
const initializeTokenAccountInstruction = getInitializeAccountInstruction({
account: tokenAccount.address,
mint: mint.address,
owner: authority.address,
})ミントしたトークンを受け取るTokenAccountを作成します。getTokenSize([])でサイズを計算し、Rent分のSOLを確保してアカウントを作成・初期化します。
ownerにはこのTokenAccountの所有者を指定します。今回はauthorityを指定していますが、別のアカウントを所有者にすることも可能です。
[8] トークン発行命令
const mintAmount = 1_000_000_000_000n // 1,000 tokens
const mintToInstruction = getMintToInstruction({
mint: mint.address,
token: tokenAccount.address,
mintAuthority: authority,
amount: mintAmount,
})1,000トークンを発行します。decimals=9なので、1,000トークン = 1,000 × 10^9 = 1_000_000_000_000nです。
[9] トランザクション構築・署名・送信
const instructions = [
createMintAccountInstruction,
initializeMetadataPointerInstruction,
initializeMintInstruction,
initializeMetadataInstruction,
createTokenAccountInstruction,
initializeTokenAccountInstruction,
mintToInstruction,
]
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(authority, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx),
)
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage)
assertIsTransactionWithBlockhashLifetime(signedTransaction)
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'confirmed',
skipPreflight: true,
})命令の順序
命令は順番が重要です。7つの命令を配列にまとめます。
pipe によるトランザクション構築
pipeで複数の処理をチェーンします。
createTransactionMessage— バージョン0の空のトランザクションメッセージを作成setTransactionMessageFeePayerSigner— 手数料支払者を設定setTransactionMessageLifetimeUsingBlockhash— ブロックハッシュを埋め込み有効期限を設定。これにより未処理トランザクションの自動廃棄やリプレイ攻撃の防止が実現されますappendTransactionMessageInstructions— 命令リストを追加
署名と送信
signTransactionMessageWithSignersでトランザクションに含まれるすべての署名者(authority, mint, tokenAccount)で一括署名します。
assertIsTransactionWithBlockhashLifetimeは、TypeScriptの型レベルで「このトランザクションはブロックハッシュ方式の有効期限を持っている」ことを保証するアサーション関数です。@solana/kit v4から必要になった書き方で、送信関数に渡す前に型を確定させます。
[10] 結果の出力
const transactionSignature = getSignatureFromTransaction(signedTransaction)
console.log('【Explorer URL】')
console.log(
`https://explorer.solana.com/tx/${transactionSignature}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`,
)SolanaではトランザクションIDとして署名(Signature)を使用します。署名はローカルで生成されるため、送信前でも取得可能です。
?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899を付けると、Solana Explorerがローカルバリデータにアクセスして結果をWeb上に表示します。
実行と確認
pnpm tsx src/metadata.tsExplorerで確認すると、7つの命令が時系列順に表示されます。
- System Program: Create Account — Mintアカウント作成(234バイトの空間を確保)
- Token-2022: Initialize Metadata Pointer — メタデータの参照先をMint自身に設定
- Token-2022: Initialize Mint — Decimals: 9、MintAuthority、FreezeAuthorityを設定
- Token-2022: Initialize Token Metadata — Name: OPOS、Symbol: OPS、Uriを書き込み
- System Program: Create Account — TokenAccount作成(166バイト)
- Token-2022: Initialize Account — どのMint用か、誰の財布かを設定
- Token-2022: Mint To — 1,000トークンをTokenAccountへ発行
まとめ
Token MetadataとMetadata Pointerを使うことで、外部プログラムに依存せずにSolanaネイティブでメタデータ付きトークンを発行できます。
実装のポイントは以下です。
extension()関数でToken Extensionを定義する- TokenMetadataは可変長のため、Space計算とRent計算で扱いが異なる
- 命令の順序が重要(アカウント作成 → Extension初期化 → Mint初期化 → Metadata初期化)
pipeでトランザクションメッセージを組み立て、署名して送信する
