はじめに
対象読者
環境構築
プロジェクトの作成
パッケージのインストール
ファイルの作成とローカルバリデータの起動
実装の全体像
完成コード
コード解説
[1] RPC接続
[2] Authorityの作成とSOL付与
[3] Mintキーペア生成
[4] Token Extensionの定義
TokenMetadata
MetadataPointer
[5] SpaceとRentの計算
[6] MintAccount作成命令
getCreateAccountInstruction
getInitializeMetadataPointerInstruction
getInitializeMintInstruction
getInitializeTokenMetadataInstruction
[7] TokenAccount作成命令
[8] トークン発行命令
[9] トランザクション構築・署名・送信
命令の順序
pipe によるトランザクション構築
署名と送信
[10] 結果の出力
実行と確認
まとめ
https://s3.ap-northeast-1.amazonaws.com/hanzochang.com/_v2/1770796290564_g2lxp89rz4.png

Token2022にMetadataを付与する — TokenMetadataとMetadataPointer【TokenExtension解説】

Solana Token2022のTokenMetadataとMetadataPointerを使い、Mint内にメタデータを直接埋め込む方法を解説。@solana/kitによるTypeScript実装をアンカー付きコードで紹介します。

公開日2026.01.29

更新日2026.01.29

Crypto

はじめに

Token2022(Token Extensions Program)では、TokenMetadataMetadataPointerという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-2022extension関数で、利用する拡張機能を定義します。

TokenMetadata

  • updateAuthority — メタデータを更新できる権限者。some()はシリアライズ時に「値がある」ことを明示する関数です。ここで書くTSは最終的にSolanaブロックチェーンのトランザクションとしてシリアライズされて送信されます。その際に値の有無を明確に伝えます
  • mint — このメタデータが属するMintアカウントのアドレス
  • name — トークン名(USDCなら「USD Coin」に相当)
  • symbol — ティッカーシンボル(USDC、USDTのような呼称)
  • uri — オフチェーンに置く詳細メタデータ(画像、description等)のJSON URL
  • additionalMetadata — オンチェーンに置く追加データ。大きなデータを置くと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は可変長なので事前に容量を確保しない
  • spaceWithTokenMetadataExtensionRent計算に使う。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で複数の処理をチェーンします。

  1. createTransactionMessage — バージョン0の空のトランザクションメッセージを作成
  2. setTransactionMessageFeePayerSigner — 手数料支払者を設定
  3. setTransactionMessageLifetimeUsingBlockhash — ブロックハッシュを埋め込み有効期限を設定。これにより未処理トランザクションの自動廃棄やリプレイ攻撃の防止が実現されます
  4. 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.ts

Explorerで確認すると、7つの命令が時系列順に表示されます。

  1. System Program: Create Account — Mintアカウント作成(234バイトの空間を確保)
  2. Token-2022: Initialize Metadata Pointer — メタデータの参照先をMint自身に設定
  3. Token-2022: Initialize Mint — Decimals: 9、MintAuthority、FreezeAuthorityを設定
  4. Token-2022: Initialize Token Metadata — Name: OPOS、Symbol: OPS、Uriを書き込み
  5. System Program: Create Account — TokenAccount作成(166バイト)
  6. Token-2022: Initialize Account — どのMint用か、誰の財布かを設定
  7. Token-2022: Mint To — 1,000トークンをTokenAccountへ発行

まとめ

Token MetadataとMetadata Pointerを使うことで、外部プログラムに依存せずにSolanaネイティブでメタデータ付きトークンを発行できます。

実装のポイントは以下です。

  • extension()関数でToken Extensionを定義する
  • TokenMetadataは可変長のため、Space計算とRent計算で扱いが異なる
  • 命令の順序が重要(アカウント作成 → Extension初期化 → Mint初期化 → Metadata初期化)
  • pipeでトランザクションメッセージを組み立て、署名して送信する
picture
hanzochang - 半澤勇大
慶應義塾大学卒業後、Webプランナーとして勤務。 ナショナルクライアントのキャンペーンサイトの企画・演出を担当。 その後開発会社に創業メンバーとして参加。 Fintech案件や大手企業のDXプロジェクトに関わり、その後個人事業主として独立し、 2023年にWeb3に特化した開発会社として法人化しました。 現在はWeb3アプリ開発を中心にAI開発フローの整備を行っています。
また、趣味で2017年ごろより匿名アカウントでCryptoの調査等を行い、 ブロックチェーンメディアやSNSでビットコイン論文等の図解等を発信していました。
X (Twitter)

お問い合わせはこちらから

内容によっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。