👏

AI Agentを使ってSolanaでNFTを自動生成してくれるチャットを開発してみる

2025/02/24に公開

はじめに

先日、Solanaで急激にAI Agentのシェアを伸ばしている【SendAI】の日本コミュニティをゆうきさんをはじめとしたSolanaに強い方達と立ち上げました。
https://x.com/sendaifunjapan/status/1890919938094719013

そしてその2週間後にメンバー第1号としてSolana Developer Hubで登壇することになりました!
https://lu.ma/soldevhub-14

今回のSolana Developer HubはRWA×DePinがテーマということなので、SendAIを使うとこんなに簡単にNFTを発行することができます、ということをお伝えできたらと思います。もし詰まるようなことがありましたら、Githubに作成したコードが記載されていますので、ご参照ください。
https://github.com/Mameta29/sendai-nft

目次

  1. Solanaトークンの基礎
  2. NFTとMetadata
  3. AI Agentを使用したNFTミントのハンズオン

Solanaトークンの基礎

そもそも私がSolana初心者ということもあり、SolanaにおけるNFTの概念、さらにいうとトークンの概念、もっというとそもそものアカウントモデルについてよくわかっておらず理解するのに少々手こずりました。この記事ではそれを全て説明することは目標としておらず、「SendAIを使ったNFTのミントができるようになる」ということに趣旨をおいていることをご理解ください。

SolanaのNFTを理解する上で参考になった記事です。

SolanaにおけるNFTについて

SolanaにおけるNFTの定義

Solanaでは全てがアカウントという前提があります。プログラム(Ethereumでいうスマートコントラクト)もSolana上で発行したトークンも全てアカウントとして表現されます。
特にトークンにおけるアカウント(ミントアカウントという)では下記の情報を持っています。
https://solana.com/ja/docs/core/tokens

上図にあるようにMint AccountにはAccount Dataが紐づいていますが、それぞれのデータは下記のようになっています。

pub struct Mint {
    /// ミント権限者(オプション)
    pub mint_authority: COption<Pubkey>,
    /// トークン供給量
    pub supply: u64,
    /// 小数点以下の桁数
    pub decimals: u8,
    /// 初期化されたかどうか
    pub is_initialized: bool,
    /// Token Accountをフリーズできる権限者(オプション)
    pub freeze_authority: COption<Pubkey>,
}

このデータ構造は全てのトークンで同じようですが、以下の特徴を持つものがNFTとして認識できます。

  1. 供給量が1
pub supply: 1
  1. 小数点以下の桁数が0
pub decimals: 0
  1. ミント権限が無効化(追加発行不可)
pub mint_authority: ///これの設定なし

1つしか発行されてなくて、mint権限者が誰もいなければ永遠とsupplyは1になるので、なるほどです。

SendAIのAI Agent Kitを使用したNFTの自動ミント

そもそも、SendAIとは何かというとSolanaに特化したAI開発ラボと言えると思います。特に代表的なのが、Solana Agent Kitといって、AIエージェントとSolanaを統合するためのOSSのツールキットです。Solanaのデプロイコードなどは全く分からなくてもミントできちゃうところがすごいです!

それではSendAIを使っていきましょう!

依存関係のインストール

Node.jsのバージョン23.x.xが必要です。エディタのターミナルを開いて下記のコマンドでバージョンの確認と依存関係のインストールをしていきましょう。

# 現在のバージョンを確認
node --version
# Node.js 23をインストール(必要な場合)
nvm install 23
# Node.js 23に切り替え
nvm use 23

# 必要なパッケージのインストール
pnpm install solana-agent-kit
pnpm add @langchain/core @langchain/openai @langchain/langgraph dotenv

Solana Agent Kit とは別にlangchainのライブラリも使っていきます。これは、大規模言語モデル(LLM)を使用したアプリケーション開発を簡単にするためのフレームワークです。特にAIを使ったアプリケーション開発において、複雑な操作を抽象化することで、開発者が効率的にLLMの機能を活用できるようにしてくれます。

IDEがpackage.jsonファイルを自動設定しますが、設定されない場合は以下のような内容にしてください:

{
  "dependencies": {
    "@langchain/core": "^0.3.33",
    "@langchain/langgraph": "^0.2.41",
    "@langchain/openai": "^0.3.17",
    "dotenv": "^16.4.7",
    "solana-agent-kit": "^1.4.3"
  }
}

環境設定

プロジェクトのルートに.envファイルを作成し、以下の内容を追加してください:

OPENAI_API_KEY=OpenAI APIキーを取得してください。
RPC_URL=https://api.devnet.solana.com
SOLANA_PRIVATE_KEY=あなたの秘密鍵

私はPhantomというウォレットをブラウザ拡張機能に入れて秘密鍵を取得しています。

OPENAI_API_KEYはOpenAIプラットフォームで取得できます。

RPC URLは現時点ではdevnetのままにしておきます。

エージェントスクリプトの作成

agent.tsという新しいファイルを作成し、以下の内容を記述してください:

// 必要なライブラリのインポート
// Solanaブロックチェーンとの対話に必要なツールキット
import { SolanaAgentKit, createSolanaTools } from "solana-agent-kit";
// LangChainフレームワークのメッセージング機能
import { HumanMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
// エージェントの会話履歴を保存するためのメモリ機能
import { MemorySaver } from "@langchain/langgraph";
import * as dotenv from "dotenv";
// コンソールでの対話的な入力を扱うためのNode.jsの標準ライブラリ
import * as readline from "readline";

dotenv.config();

// エージェントの初期化
async function initializeAgent() {
  // temperatureは1->0に近づくほど定型分になる
  const llm = new ChatOpenAI({
    modelName: "gpt-4-turbo-preview",
    temperature: 0.7,
  });

  // 秘密鍵を.envから取得
  const privateKeyBase58 = process.env.SOLANA_PRIVATE_KEY!;
  
  // SolanaAgentKitのインスタンスを作成
  const solanaKit = new SolanaAgentKit(privateKeyBase58, process.env.RPC_URL!, {
    OPENAI_API_KEY: process.env.OPENAI_API_KEY!,
  });

  // Solana関連のツールセットを作成
  const tools = createSolanaTools(solanaKit);
  
  // エージェントの会話履歴を保存するためのメモリインスタンスを作成
  const memory = new MemorySaver();

  // ReActエージェントを作成して返す
  // (ReActは「Reasoning and Acting」の略で、AIの推論と行動を組み合わせたアプローチ)
  return createReactAgent({
    llm,
    tools,
    checkpointSaver: memory,  // 会話履歴の保存機能
  });
}

// チャットを実行する関数
async function runInteractiveChat() {
  // エージェントを初期化
  const agent = await initializeAgent();
  
  // スレッドIDを設定
  const config = { configurable: { thread_id: "Solana Agent Kit!" } };

  // コンソールでの入出力インターフェースを設定
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  // コンソールをクリアしてチャットを開始(初期化メッセージを消去するため)
  setTimeout(() => {
    console.clear();
    console.log("Chat with Solana Agent (type 'exit' to quit)");
    console.log("--------------------------------------------");
    askQuestion();
  }, 100);

  // ユーザーからの入力を受け付ける関数
  const askQuestion = () => {
    rl.question("You: ", async (input) => {
      // exitと入力された場合はチャットを終了
      if (input.toLowerCase() === "exit") {
        rl.close();
        return;
      }

      // エージェントとの対話をストリーム形式で開始
      const stream = await agent.stream(
        {
          messages: [new HumanMessage(input)],
        },
        config,
      );

      // エージェントの応答を表示
      process.stdout.write("Agent: ");
      // ストリームからチャンクを受け取って処理
      for await (const chunk of stream) {
        if ("agent" in chunk) {
          // エージェントからのメッセージを表示
          process.stdout.write(chunk.agent.messages[0].content);
        } else if ("tools" in chunk) {
          // ツールからの出力を表示
          process.stdout.write(chunk.tools.messages[0].content);
        }
      }

      // 区切り線を表示して次の質問を受け付ける
      console.log("\n--------------------------------------------");
      askQuestion();
    });
  };
}

// チャットを開始
runInteractiveChat().catch(console.error);

*ストリーム形式
ストリーム形式(ストリーミング)とは、データを一度に全て送受信するのではなく、小さな単位(チャンク)に分けて順次送受信する方式

エージェントの実行

以下のコマンドでスクリプトを実行できます:

node agent.ts

これによりエージェントとの簡単なチャットが開始されます。

基本機能のテスト

Solanaの残高確認とdevnet SOLのリクエストができます。

Please show me my wallet address and request some devnet sol

アドレスを表示してfaucetも取得してきてもらいました!

NFTコレクションの作成

エージェントに以下のように依頼します

Please create me a NFT collection called trains with symbol TRN using this uri: https://scarlet-fancy-minnow-617.mypinata.cloud/ipfs/bafkreif43sp62yuy3sznrvqesk23tfnhpdck4npqowdwrhrzhsrgf5ao2e

「trainsという名前でシンボルTRNのNFTコレクションを作成してください。uri: https://scarlet-fancy-minnow-617.mypinata.cloud/ipfs/bafkreif43sp62yuy3sznrvqesk23tfnhpdck4npqowdwrhrzhsrgf5ao2e

いい感じです!
エクスプローラーでも確認できました!
https://explorer.solana.com/address/9KZFiAQg1FrmDhGh7qk7vZqqV9hnyhoLN7Vts4pjUJC?cluster=devnet

NFTのミント

コレクション作成後、NFTをミントします

Please mint me an NFT into that collection using the name: Train1 and using this URI: https://scarlet-fancy-minnow-617.mypinata.cloud/ipfs/bafkreif43sp62yuy3sznrvqesk23tfnhpdck4npqowdwrhrzhsrgf5ao2e

「そのコレクションに名前:Train1、URI: https://scarlet-fancy-minnow-617.mypinata.cloud/ipfs/bafkreif43sp62yuy3sznrvqesk23tfnhpdck4npqowdwrhrzhsrgf5ao2e でNFTをミントしてください」
これにより、Train1という名前と列車の画像を持つNFTがミントされます。

エクスプローラーでも確認できました!
https://explorer.solana.com/address/39Lx2iUrGQ8y9rQ3ozwBQP9bEBrx5tByVs44hC1zaUqQ?cluster=devnet

Pinataやその他のストレージプロバイダーを使用してアップロードした任意のメタデータを使用することもできます。

NFTミント挙動の中で何が行われているか

node_modulesの中のsolana-agent-kitライブラリを眺めていると下記の Metaplex のNFTミント実装していることがわかりました。
solana-agent-kit/src/langchain/metaplex/mint_nft.ts

// Solanaブロックチェーンの公開鍵(アドレス)操作に必要なクラスをインポート
import { PublicKey } from "@solana/web3.js";
// LangChainのツール基底クラスをインポート(AIエージェントがこのツールを使用できるようにするため)
import { Tool } from "langchain/tools";
// 独自のSolanaAgentKitクラスをインポート(Solanaとの対話を簡素化するためのラッパー)
import { SolanaAgentKit } from "../../agent";

// NFTをミント(発行)するための専用ツールクラス
export class SolanaMintNFTTool extends Tool {
 // ツールの名前(エージェントがこの名前で呼び出す)
 name = "solana_mint_nft";
 // ツールの説明(エージェントがこのツールの使い方を理解するため)
 description = `Mint a new NFT in a collection on Solana blockchain.

   Inputs (input is a JSON string):
   collectionMint: string, eg "J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w" (required) - The address of the collection to mint into
   name: string, eg "My NFT" (required)
   uri: string, eg "https://example.com/nft.json" (required)
   recipient?: string, eg "9aUn5swQzUTRanaaTwmszxiv89cvFwUCjEBv1vZCoT1u" (optional) - The wallet to receive the NFT, defaults to agent's wallet which is ${this.solanaKit.wallet_address.toString()}`;

 // コンストラクタ:SolanaAgentKitインスタンスを受け取り、privateフィールドとして保存
 constructor(private solanaKit: SolanaAgentKit) {
   // 親クラス(Tool)のコンストラクタを呼び出す
   super();
 }

 // ツールが実際に呼び出される際に実行されるメソッド
 // 入力はJSONフォーマットの文字列として渡される
 protected async _call(input: string): Promise<string> {
   try {
     // 入力文字列をJSONオブジェクトにパース
     const parsedInput = JSON.parse(input);

     // SolanaAgentKitのmintNFTメソッドを呼び出してNFTをミント
     const result = await this.solanaKit.mintNFT(
       // コレクションのミントアドレスをPublicKeyオブジェクトに変換
       new PublicKey(parsedInput.collectionMint),
       // NFTのメタデータ(名前とURI)を指定
       {
         name: parsedInput.name,
         uri: parsedInput.uri,
       },
       // 受取人のアドレスが指定されていればそれを使用、そうでなければエージェントのウォレットアドレスを使用
       parsedInput.recipient
         ? new PublicKey(parsedInput.recipient)
         : this.solanaKit.wallet_address,
     );

     // 成功時のレスポンスをJSON形式で返す
     return JSON.stringify({
       status: "success",
       message: "NFT minted successfully",
       mintAddress: result.mint.toString(), // 新しくミントされたNFTのアドレス
       metadata: {
         name: parsedInput.name,
         symbol: parsedInput.symbol, // 注意: 入力にsymbolが含まれてない場合はundefinedになる
         uri: parsedInput.uri,
       },
       recipient: parsedInput.recipient || result.mint.toString(),
     });
   } catch (error: any) {
     // エラー発生時のレスポンスをJSON形式で返す
     return JSON.stringify({
       status: "error",
       message: error.message,
       code: error.code || "UNKNOWN_ERROR", // エラーコードがあれば使用、なければデフォルト値
     });
   }
 }
}

まとめ

Solanaの簡単なトークンやNFTの概念から、SendAIを使ってAI AgentにNFTを自動でミントしてもらうコードを実装できまいた!裏側をみてみるときちんとmintするコードがあったりしたので、自然言語からAIでユーザーがどのようなことをしたいかを割り振って、該当の関数を呼び出すみたいなことをしてそうでした。
ツールを使えば簡単にブロックチェーン開発ができる未来もすぐそこですね〜。

Discussion