新着:HeliusがLight Protocolを買収
Solana Frames:FarcasterでcNFTをミントする
ブログ/開発

Solana Frames:FarcasterでcNFTをミントする

Developer Experience EngineerXの0xIchigoLinkedInの0xIchigoGitHubの0xIchigo
読了時間:12分

この記事で解説する内容

分散型ソーシャルメディアは、オンラインデータに対するプライバシーとコントロールをユーザーにより多く委ねるという、大きな転換をもたらします。従来のソーシャルメディアプラットフォームとは異なり、分散型ネットワークでは個人が自身のソーシャルグラフとデジタルアイデンティティを直接管理できます。Farcasterのようなプラットフォームはその最前線に立ち、さまざまなプラットフォーム間でオープンかつ相互運用可能なコミュニケーションを促進する、検閲耐性のあるネットワークを構築しています。Farcasterにおける重要なイノベーションの1つがFramesです。通常のソーシャルメディア投稿を本格的なアプリケーションへと変え、オンラインでのソーシャル体験を豊かにします。

この記事では、Farcasterと、最近追加されたSolanaサポートについて解説します。Farcasterとは何か、Framesとは何か、Solanaアドレスを認証する方法を取り上げます。その後、Helius Mint APIを使用して、ユーザーが認証したSolanaアドレスにcNFTをミントするFrameを作成する短いチュートリアルを紹介します。このチュートリアルでは、TypeScriptとNext.jsの使用経験があることを前提とします。

Farcasterとは?

Farcasterは、Ethereumネットワークを活用した十分に分散化されたソーシャルネットワークです。TwitterやRedditなどの人気ソーシャルネットワークに似た公開プラットフォームを提供し、ユーザーはプロフィールを作成し、投稿(castと呼ばれます)を共有し、ほかのユーザーをフォローできます。Farcasterの特長は、ユーザーの所有権とポータビリティを重視している点です。ユーザーは自身のアカウントや他者との関係を所有します。デジタルアイデンティティとソーシャルグラフを維持したまま、異なるアプリケーション間を移動できます。

Framesとは?

Framesを使用すると、開発者は任意のcastをインタラクティブなアプリケーションに変換できます。OpenGraph標準を拡張することで、静的な埋め込みを動的でインタラクティブな体験へと変えます。これにより、ユーザーはWarpcasterを含む任意のFarcasterクライアント内で、投票、ライブフィード、インタラクティブギャラリーなどの機能を作成できます。Frameの中核となるのは、HTMLページの<head>とともに返される一連の<meta>タグです。ページに必要なFrameプロパティがすべて含まれている場合、FarcasterアプリはそのページをFrameとしてレンダリングします。Frame仕様には、これらの必須プロパティが記載されています。たとえば、初期Frameは、次の要件を満たす必要があります。

  • HTMLの<head>セクションで有効なFrameを返す
  • ユーザーがブラウザでFrameをクリックして開いた場合に備え、有効なHTMLの<body>を返す
  • Farcasterクライアントによってキャッシュされるため、初期Frameに動的コンテンツを含めない
  • fc:frame:stateタグを含めない

FarcasterでSolanaアドレスを認証する

Farcasterは最近、Solanaアドレスのサポートを追加しました。これにより、開発者はユーザーのSolanaウォレットと連携するFrameを構築できます。サポート開始から24時間も経たないうちに、9,328を超える一意のFarcaster FIDがSolanaアドレスを認証しました。

ユーザーは次の手順でSolanaアドレスを認証できます。

  • Settingsに移動します
  • Verified Addressesをクリックし、「Verify an address」ボタンをクリックします
  • Solanaウォレットを接続し、所有権を証明するメッセージに署名します

現在サポートされているウォレットはPhantomのみです。

チュートリアル:cNFTをミントするFrameを作成する

この記事では、認証済みのSolanaアドレスを持つユーザーが圧縮NFT(cNFT)をミントできるFrameを作成します。Framesの構築とデバッグに使用できるNext.jsテンプレート、frames.jsを使ってプロジェクトを立ち上げます。CoinbaseのOnchainKit、特にFrame Kitを使用し、FarcasterユーザーがFrameを操作した際にそのユーザー情報を取得します。その後、このデータをHelius Mint APIに渡し、ユーザーのSolanaウォレットに次のcNFTをミントします。

Frameをセットアップする

frame.jsでFrameを作成するのは、スターターテンプレートをクローンするだけなので簡単です。次のコマンドを実行し、<name>を、テンプレートのクローン先として作成する新しいフォルダー名に置き換えます。

コード
npx degit github:framesjs/frames.js/examples/framesjs-starter#main <name>

ここではディレクトリ名をhelius-frameとしたため、<name>を<helius-frame>に置き換えました。次に、新しいディレクトリ(例:cd helius-frame)へ移動し、yarn installコマンドで必要な依存関係をインストールします。ダウンロードが完了したら、yarn devを実行して開発サーバーを起動します。ブラウザでhttp://localhost:3000にアクセスすると、次の画面が表示されます。

Debugハイパーリンクをクリックすると、http://localhost:3000/debugに移動します。

デプロイせずにFrameをローカルでテストできるため便利です。たとえば、サインインしてボタンをテストしたり、ほかのユーザーになりすましたり、ハブの状態をモックしたりできます。Warpcasterには、Frameを検証できる優れたFrame Validatorツールもあります。以降は、個人的な好みからFrame Validatorツールを使用してテストします。 

次に、yarn installを実行して、テンプレートに必要な依存関係をインストールします。さらに、yarn install @coinbase/onchainkitを実行してOnchainKitをインストールします。

プロジェクトのフォルダーを次のように再構成します。

  • examplesフォルダーを削除します。サンプルからはFramesの構築方法について有益な知見を得られますが、今回の目的には不要です。このフォルダーを削除すると、プロジェクトの肥大化を抑えられます。ただし、一般的なFramesを構築する別の方法を知るために、内容を確認することを強くおすすめします
  • appディレクトリにapiフォルダーを作成します。この手順により、アプリケーションのフロントエンドとバックエンドが明確に分かれ、APIルートに関するNext.jsの規約に沿った構成になります。次に、framesフォルダーを、その内容であるroute.tsとともに新しいapiフォルダーへ移動します。
  • appディレクトリにutilsフォルダーを作成します。この新しいフォルダー内にmint.tsファイルを作成します。ここにcNFTのミント処理を記述します

これで、いよいよFrameの構築を始められます!

Frameを編集する

layout.tsx

まず、layout.tsxファイルから始めます。内容をすべて削除し、次のコードに置き換えます。

コード
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    
      {children}
    
  );
}

RootLayoutコンポーネントは、アプリケーション全体を一貫したレイアウト構造でラップする、ミニマルなアプローチを採用しています。柔軟性があり、再利用できるように設計されています。このミニマルなレイアウトを出発点として、開発者はこのファイルをテンプレートにし、必要に応じて独自のFramesを構築、カスタマイズできます。

layout.tsxファイルを変更すると、ローカルでデバッガーを使用する際に問題が発生する点に注意してください。layout.tsxの編集は必須ではありません。ここで編集するのは、アプリケーションのメタデータをpage.tsxに導入するためです。回避策として、このファイルにメタデータを導入することも可能です。 

page.tsx

page.tsxの内容を削除し、次のコードに置き換えます。

コード
import type { Metadata } from "next";
import { getFrameMetadata } from "@coinbase/onchainkit";

const baseURL = "https://helius-frame.vercel.app/";

const frameMetadata = getFrameMetadata({
  image: {
    src: `${baseURL}/default.jpg`,
    aspectRatio: "1:1",
  },
  buttons: [
    {
      label: "Click to mint a cNFT on Solana!",
      action: "post",
    },
  ],
  postUrl: `${baseURL}api/frames`,
});

export const metadata: Metadata = {
  title: "Mint a cNFT on Solana",
  description: "Mint a cNFT to your verified Solana address using the Helius Mint API",
  openGraph: {
    title: "Mint a cNFT on Solana",
    description: "Mint a cNFT to your verified Solana address using the Helius Mint API",
    images: ["https://helius-frame.vercel.app/default.jpg"],
  },
  other: {
    ...frameMetadata,
  },
};

export default function Page() {
  return (
    
      Mint a cNFT on Farcaster using the Helius Mint API
      
        Built by 0xIchigo
      
      
        GitHub Repo
      
    
  );
}

上記のコードでは、Webサーバー上のURLで公開する初期Frameをセットアップしています。ここでは、初期FrameをVercel上のhttps://helius-frame.vercel.app/でホストしています。このチュートリアルの後半で、Vercelを使ったデプロイ方法を解説します。  

まず、このファイルのmetadataエクスポートに型を付けるため、Next.jsからMetadata型をインポートします。また、@coinbase/onchainkitからgetFrameMetadataをインポートします。次に、コードの重複を減らすため、アプリケーションのベースURLを定義します。

続いてgetFrameMetadataを呼び出し、初期Frameのメタデータを設定します。このメタデータには、次の項目が含まれます。

  • アスペクト比1:1で、Frameの視覚要素となる画像(つまりsrc)
  • 「Click to mint a cNFT on Solana!」というラベルのボタン。クリックすると、指定されたpostUrl(つまり${baseURL}api/frames)にPOSTリクエストを送信します。この操作によってミント処理が開始されます

次に、ページのタイトルと説明、OpenGraphメタデータ(つまりopenGraph)、およびotherプロパティに展開された、先ほど定義したframeMetadataを含むmetadataオブジェクトをエクスポートします。

最後に、pageでデフォルトのページコンポーネントを定義してエクスポートします。cNFTをミントするよう促す基本的なh1を返します。これは、Farcasterでは何らかのHTMLを返す必要があるためです。主な操作はサイト自体へ直接アクセスするのではなく、Framesを通じて行われます。ただし、ユーザーがクリックしてサイトを開いた場合に備え、サイト上にも何らかの内容が必要です。 

mint.ts 

mint.tsに次のコードを貼り付けます。

コード
const url = `Get a URL at dev.helius.xyz`;

export const mintCompressedNFT = async (address: string) => {
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            jsonrpc: "2.0",
            id: "frames-sol-mint",
            method: "mintCompressedNft",
            params: {
                name: "Solana Soldier",
                symbol: "HELIUS",
                owner: address,
                description: "A decentralized warrior backed by Solana's most loved RPC Nodes, APIs, Webhooks, and Developer Tooling",
                attributes: [
                    {
                        trait_type: "RPCs",
                        value: "Fast",
                    },
                    {
                        trait_type: "Company",
                        value: "Helius"
                    },
                ],
                imageUrl: "https://shdw-drive.genesysgo.net/HmvVwYAy7cxWECLbu2cjWBSHysVoriTqgdugyePVg3rY/cnftmint.jpg",
                externalUrl: "https://www.helius.dev/",
                sellerFeeBasisPoints: 6900,
            },
        }),
    });

    const data = await response.json();
    console.log(`data: ${JSON.stringify(data, null, 2)}`);

    if (!response.ok) {
        throw new Error(`The response was not okay - ${response.status}`);
    }

    if (!data.result || !data.result.assetId) {
        throw new Error(`The response did not include the expected data.result.assetId field`);
    }

    return data.result;
};

mintCompressedNFTユーティリティ関数は、指定したアドレスに事前定義済みのcNFTをミントするため、アプリケーションにとって重要です。Helius Mint APIを使用し、cNFTのミント処理をカプセル化した、シンプルで再利用可能な非同期関数を作成しています。urlとして定義するHelius General RPC URLが必要です。まだ取得していない場合は、Heliusダッシュボードにアクセスし、urlで使用する無料のRPC URLを生成してください。

このリポジトリではSecureRPC URLを使用しています。APIキーが漏洩しないよう注意し、プロキシの運用を検討してください。リクエスト自体については、リクエストスキーマの詳細な説明をこちらで確認できます。

また、執筆時点ではFarcasterがトランザクションを完全にはサポートしていないため、cNFTの画像はすでにShdwDriveへアップロードしてあります。署名者を指定すれば、Helius Mint APIがオフチェーンメタデータのArweaveへのアップロードを処理します。

この関数には堅牢なエラー処理も含まれており、ミント処理中に発生した問題を確実に捕捉して報告します。また、デバッグ用にレスポンスデータをログへ出力します。

route.ts 

api/frames/route.tsに次のコードを貼り付けます。

コード
import { NextRequest, NextResponse} from "next/server";
import { getFrameMessage, getFrameHtmlResponse } from "@coinbase/onchainkit/frame";
import { mintCompressedNFT } from "../../utils/mint";

const baseURL = "https://helius-frame.vercel.app/";

export async function POST(req: NextRequest): Promise {
    const body = await req.json();

    // Validate the frame message and extract the user ID
    const { isValid, message } = await getFrameMessage(body, {
        neynarApiKey: "NEYNAR_ONCHAIN_KIT",
    });

    console.log(JSON.stringify(message, null, 2));

    // Make sure they're following us before they can mint the cNFT
    if (isValid && !message.following) {
        return new NextResponse(getFrameHtmlResponse({
            image: {
                src: `${baseURL}/error.jpg`,
                aspectRatio: "1:1",
            },
            buttons: [
                {
                  label: "You need to be following to mint! Click to retry",
                  action: "post",
                },
            ],
            postUrl: `${baseURL}api/frames`,
        }));
    // Make sure they have a verified Solana address
    } else if (isValid && message.interactor.verified_addresses.sol_addresses) {
        const solanaAddresses = message.interactor.verified_addresses.sol_addresses;
        console.log(`SOLANA ADDRESSES: ${solanaAddresses}`);

        if (solanaAddresses.length !== 0) {
            try {
                console.log(`MINTING to ${solanaAddresses[0]}`);
                // We take the first address address, in case there's more than one
                const mintResult = await mintCompressedNFT(solanaAddresses[0]!);
              
                return new NextResponse(getFrameHtmlResponse({
                    image: {
                        src: `${baseURL}/success.jpg`,
                        aspectRatio: "1:1",
                    },
                    buttons: [
                        {
                            label: "Success! View your cNFT on XRAY",
                            action: "link",
                            target: `https://xray.helius.xyz/token/${mintResult.assetId}?network=mainnet`
                        },
                    ],
                }));
            } catch (e: any) {
                console.log(`Minting failed: ${e}`);
                return new NextResponse(getFrameHtmlResponse({
                    image: {
                        src: `${baseURL}/error.jpg`,
                        aspectRatio: "1:1",
                    },
                    buttons: [
                        {
                          label: "Minting failed. Click to retry",
                          action: "post",
                        },
                    ],
                    postUrl: `${baseURL}api/frames`,
                }));
            }
        } else {
            console.log(`No Solana address found`);
            return new NextResponse(getFrameHtmlResponse({
                image: {
                    src: `${baseURL}/error.jpg`,
                    aspectRatio: "1:1",
                },
                buttons: [
                    {
                      label: "No Solana address found. Click to retry",
                      action: "post",
                    },
                ],
                postUrl: `${baseURL}api/frames`
            }));
        }
    } else {
        return new NextResponse(getFrameHtmlResponse({
            image: {
                src: `${baseURL}/error.jpg`,
                aspectRatio: "1:1",
            },
            buttons: [
                {
                  label: "Invalid frame message. Click to retry",
                  action: "post",
                },
            ],
            postUrl: `${baseURL}api/frames`,
        }));
    }
}

このファイルは、FrameへのすべてのPOSTリクエストを処理するNext.js APIルートです。まず、受信したリクエストの処理とレスポンスの送信に使用するNextRequestとNextResponseをnext/serverからインポートします。また、Frameメッセージの検証とFrame操作用HTMLレスポンスの生成に使用するgetFrameMessageとgetFrameHtmlResponseを**@coinbase/onchainkit/frame**からインポートします。さらに、mintCompressedNFTユーティリティ関数もインポートします。

コードの重複を減らすため、baseURLを定義しています。これは、リソースやAPIエンドポイントへのパスを構築するために使用します。

このファイルはPOST関数をエクスポートします。この関数は、受信したリクエスト本文を解析してFrameメッセージを抽出します。getFrameMessageを使用し、受信したFrame操作とメッセージのペイロードが有効かどうかを確認します。そのために、リクエスト本文をJSONとして渡し、デフォルトのNeynar APIキー(つまり**「NEYNAR_ONCHAIN_KIT」**)を渡します。Neynarは、Farcaster開発の簡素化を目的とした開発者向けプラットフォームです。getFrameMessageは、FrameValidationResponseに解決されるPromiseを返します。定義は次のとおりです。

コード
export type FrameValidationResponse =
  | { isValid: true; message: FrameValidationData }
  | { isValid: false; message: undefined };

したがって、Frame操作が有効な場合は、FrameValidationData型のメッセージを返します。定義は次のとおりです。

コード
export interface FrameValidationData {
  button: number; // Number of the button clicked
  following: boolean; // Indicates if the viewer clicking the frame follows the cast author
  input: string; // Text input from the viewer typing in the frame
  interactor: {
    fid: number; // Viewer Farcaster ID
    custody_address: string; // Viewer custody address
    verified_accounts: string[]; // Viewer account addresses
    verified_addresses: {
      eth_addresses: string[] | null;
      sol_addresses: string[] | null;
    };
  };
  liked: boolean; // Indicates if the viewer clicking the frame liked the cast
  raw: NeynarFrameValidationInternalModel;
  recasted: boolean; // Indicates if the viewer clicking the frame recasted the cast
  valid: boolean; // Indicates if the frame is valid
}

これには、ユーザーのFarcaster ID、認証済みのEthereumまたはSolanaアドレスを持っているかどうか、castに「いいね」したかどうかなど、関連データが含まれるため非常に有用です。このチュートリアルでは、ミントを試みているユーザーが私のプロフィールをフォローしているか確認します。フォローしている場合は、cNFTのミントを許可します。また、cNFTのミント先となる認証済みSolanaアドレスを持っていることも確認します。

まず、操作が有効か、ユーザーが私のアカウントをフォローしているかを確認します。条件を満たさない場合は、エラーメッセージと再試行を促すボタンを表示します。次に、ユーザーが認証済みのSolanaアドレスを持っているか確認します。1つ以上のアドレスが存在する場合は、ミント処理へ進みます。最初の認証済みSolanaアドレスを取得し、mintCompressedNFTに渡します。成功すると、成功を示す画像と、OrbでcNFTを表示するためのリンクを返します。失敗した場合はエラーをログに記録し、再試行を促すメッセージを返します。認証済みのSolanaアドレスが見つからない場合は、その旨を示すエラーメッセージを返し、再試行を促します。最後に、Frameメッセージが無効な場合は、Frameが無効であることを示すエラーメッセージを返し、再試行を促します。

このコードでは、getFrameHtmlResponseを使用して、ミント処理や検証チェックの結果に応じたHTMLレスポンスを生成します。これには、特定のユーザー操作に応じたレスポンス画像、アスペクト比、ボタンの設定が含まれます。

本番環境へプッシュする

コードが完成したら、新しいGitHubリポジトリを作成し、そこへコードをプッシュします。コマンドラインで次のコマンドを実行し、コードをGitHubへプッシュしてください。

コード
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/<username>/<new repo name>.git
git branch -M main
git push -u origin main

コードをGitHubへプッシュしたら、次はアプリケーションをデプロイします。VercelはGitHubとシームレスに統合でき、デプロイプロセス全体を簡素化できるため、今回はVercelを使用します。次の手順で進めます。

  • VercelのWebサイトでアカウントにサインインします。アカウントがない場合は作成してください。十分に利用できるHobbyプラン(つまり無料)があり、今回の目的にはそれで対応できます
  • ログインしたらダッシュボードへ移動し、New Projectボタンをクリックします。VercelからGitHubプロジェクトのインポートを求められます。前の手順で作成したリポジトリを選択します
  • Vercelは使用しているフレームワークを自動検出し、ビルド設定を提案します。VercelはNext.jsの開発元によって作られており、最上位のサポートを提供しているため、複雑な設定をあまり心配する必要はありません
  • プロジェクトの設定が完了したら、Deployボタンをクリックします。Vercelが自動的にデプロイプロセスを開始し、プロジェクトをビルドして一意のVercel URLへデプロイします。 
  • デプロイが完了すると、稼働中のアプリケーションへアクセスするための一意のURLがVercelから提供されます。このプロジェクトはhttps://helius-frame.vercel.app/にビルドされました。GitHubリポジトリ名の後に**.vercel.app**が続く形式です。Vercelでは、プロジェクト設定からカスタムドメインも構成できます

以上です!これらの手順により、Farcaster FrameをGitHubへプッシュし、Vercelでホストできました。前述したFrames validatorなどのテストツールを使用して、Frameの機能をテストできます。あとはFrameのURLを含むcastを作成するだけです!

まとめ

ブロックチェーンとソーシャルメディアの融合は、デジタル空間の捉え方や、その中での交流方法を大きく進化させます。FarcasterとFramesの人気により、従来のソーシャルメディアプラットフォーム上で静的だった投稿が、インタラクティブな体験へと変わります。この記事では、FarcasterとFramesについて解説しました。チュートリアルでは、Farcasterに最近追加されたSolanaサポートを活用し、ボタンを1回クリックするだけでユーザーにcNFTをミントする方法を紹介しました。Farcaster上で開発したい方にも、新しく刺激的な方法でソーシャルメディアのフィードを閲覧したい方にも、可能性は無限に広がっています。

ここまでお読みいただき、ありがとうございます!Farcasterに参加したい方は、次の招待リンクをご利用ください!

Solanaの最新情報を見逃さないよう、以下にメールアドレスを入力してください。さらに深く学ぶ準備はできましたか?Heliusブログで最新記事を読み、今日からSolanaの旅を続けましょう。

その他のリソース

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます

拡大画像