mcpField
D
dapp

dapp

github.com/stellar開発55371 回インストール1 週間前に更新
リポジトリ

dapp について

ブラウザーや Node.js で dApp を開発する JavaScript 開発者向けの Stellar フロントエンドスキルです。ウォレット接続から、トランザクションの構築、コントラクト呼び出し、署名、送信までを扱います。

主な機能

  1. 01Stellar JavaScript SDK の初期化
  2. 02Freighter または複数ウォレットへの接続
  3. 03トランザクションの構築と送信
  4. 04スマートコントラクトの呼び出し
  5. 05確認状態とトランザクションエラーの処理

活用シーン

  1. 01dApp へのウォレット接続の追加
  2. 02Stellar 決済の構築と送信
  3. 03フロントエンドからのスマートコントラクト呼び出し
  4. 04パスキー対応スマートアカウントの実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/stellar/stellar-dev-skill --skill dapp

インストール後、Claude Code で/dappを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

data-fetching.md
react.md
smart-accounts.md

詳細

Stellar dApp/フロントエンド

@stellar/stellar-sdk を使用したクライアントサイド開発、ウォレット接続、トランザクションへの署名と送信について説明します。従来の Stellar 操作と、ブラウザーまたは Node.js からのスマートコントラクト呼び出しの両方を扱います。

このスキルを使用する場面

  • Stellar Wallets Kit を使って Freighter などのウォレットを接続する場合
  • トランザクションを構築、シミュレーション、署名、送信する場合
  • フロントエンドから Stellar のスマートコントラクトを呼び出す場合
  • パスキーを使ったスマートアカウントを実装する場合
  • ネットワークのパスフレーズ(Mainnet/Testnet/ローカル)を扱う場合

関連スキル

  • 呼び出すコントラクトの作成 → ../smart-contracts/SKILL.md
  • アセットの発行とトラストラインの管理 → ../assets/SKILL.md
  • RPC/Horizon によるチェーン状態の照会 → ../data/SKILL.md
  • 有料 API やエージェントの決済クライアントの構築 → ../agentic-payments/SKILL.md
  • ウォレット/アンカーフローが依存する SEP → ../standards/SKILL.md

目標

  • アプリで SDK インスタンスを一元化する(RPC/Horizon + トランザクション構築)
  • Freighter ウォレットと連携する(または Stellar Wallets Kit を使って複数のウォレットに対応する)
  • Next.js でクライアントとサーバーを明確に分離する
  • 適切な確認処理を備えたトランザクション送信を実装する

タスクに該当するファイルを読む

このファイルでは、SDK のセットアップ、ウォレット接続、トランザクションの構築/署名/送信について説明します。詳しい解説は関連ファイルにあります。

タスクファイル
SDK のセットアップと環境設定SDK の初期化(以下)
ウォレット連携(Freighter、Wallets Kit)ウォレット連携(以下)
トランザクションの構築/送信パターントランザクションの構築、トランザクションの送信(以下)
ウォレット接続ボタン、決済フォーム、Next.js App Router の接続react.md
アカウント残高、コントラクトの読み取り(queryContract)、生のレジャーエントリーdata-fetching.md
パスキー対応スマートウォレット(Smart Account Kit)、OpenZeppelin Relayer によるガス代不要のトランザクションsmart-accounts.md
本番環境向け UX チェックリストトランザクション UX チェックリスト(以下)

推奨される依存関係

Node.js 22 以上が必要です。 SDK v16 では Node 22 が最低バージョンです(古い Node では EBADENGINE 警告が表示されます)。また、v16 では @stellar/stellar-base が @stellar/stellar-sdk に統合され、ESM ファーストになり、axios の代わりにネイティブの fetch を使用します。引き続き @stellar/stellar-base を直接インポートしている場合は、インポート先を @stellar/stellar-sdk に切り替え、base パッケージをアンインストールしてください(両方を使うと instanceof チェックが正しく動作しません)。詳しくは移行ガイドを参照してください。

npm install @stellar/stellar-sdk @stellar/freighter-api
# Or for multi-wallet support — Wallets Kit v2 is distributed on JSR, not npm:
npx jsr add @creit-tech/stellar-wallets-kit

情報源: 以下の SDK の仕組み(初期化、トランザクション構築、コントラクト呼び出し、送信、データ取得、エラー処理)は、公式の JS SDK ドキュメントに準拠しています(エージェント向けに llms.txt/llms-full.txt のバンドルも公開されています)。ウォレット連携(Freighter、Stellar Wallets Kit)、パスキー対応スマートアカウント、OpenZeppelin リレイヤーは、JS SDK には含まれない別個のパッケージです。それぞれの公式ドキュメントでご確認ください。

SDK の初期化

API リファレンス全体(RPC メソッド、Horizon エンドポイント、移行ガイド)については、データスキルを参照してください。

基本的なセットアップ
import * as StellarSdk from "@stellar/stellar-sdk";

// For Testnet
const testnetServer = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");
const testnetRpc = new StellarSdk.rpc.Server("https://soroban-testnet.stellar.org");
const testnetNetworkPassphrase = StellarSdk.Networks.TESTNET;

// For Mainnet
const mainnetServer = new StellarSdk.Horizon.Server("https://horizon.stellar.org");
const mainnetRpcUrl = process.env.NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL;
if (!mainnetRpcUrl) throw new Error("Missing NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL");
const mainnetRpc = new StellarSdk.rpc.Server(mainnetRpcUrl); // set from your chosen RPC provider
const mainnetNetworkPassphrase = StellarSdk.Networks.PUBLIC;
環境設定

Mainnet の RPC URL には、プロバイダー固有のものを使用してください(https://developers.stellar.org/docs/data/apis/rpc/providers を参照)。

// lib/stellar.ts
import * as StellarSdk from "@stellar/stellar-sdk";

const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet";

const requireEnv = (name: string): string => {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env var: ${name}`);
  return value;
};

function getConfig(network: string) {
  switch (network) {
    case "testnet":
      return {
        horizonUrl: "https://horizon-testnet.stellar.org",
        rpcUrl: "https://soroban-testnet.stellar.org",
        networkPassphrase: StellarSdk.Networks.TESTNET,
        friendbotUrl: "https://friendbot.stellar.org" as string | null,
      };
    case "mainnet":
      return {
        horizonUrl: "https://horizon.stellar.org",
        // Resolved lazily so testnet runs don't require the mainnet env var
        rpcUrl: requireEnv("NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL"),
        networkPassphrase: StellarSdk.Networks.PUBLIC,
        friendbotUrl: null,
      };
    default:
      throw new Error(`Unknown network: ${network}`);
  }
}

export const config = getConfig(NETWORK);

export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);

ウォレット連携

Freighter(主要なブラウザーウォレット)
// hooks/useFreighter.ts
import { useState, useEffect, useCallback } from "react";
import {
  isConnected,
  getAddress,
  requestAccess,
  signTransaction,
  getNetwork,
} from "@stellar/freighter-api";

export function useFreighter() {
  const [connected, setConnected] = useState(false);
  const [address, setAddress] = useState<string | null>(null);
  const [network, setNetwork] = useState<string | null>(null);

  useEffect(() => {
    checkConnection();
  }, []);

  const checkConnection = async () => {
    const { isConnected: installed, error } = await isConnected();
    if (error || !installed) return;

    // getAddress returns address: "" until the app has been granted access,
    // so a non-empty address means we're already authorized.
    const { address: addr, error: addressError } = await getAddress();
    if (addressError || !addr) return;

    const { network: net, error: networkError } = await getNetwork();
    if (networkError) return;
    setConnected(true);
    setAddress(addr);
    setNetwork(net);
  };

  const connect = useCallback(async () => {
    const { isConnected: installed, error } = await isConnected();
    if (error || !installed) {
      throw new Error("Freighter extension not installed");
    }

    // requestAccess prompts the user and returns the granted address.
    const { address: addr, error: accessError } = await requestAccess();
    if (accessError) throw new Error(accessError.message);

    const { network: net, error: networkError } = await getNetwork();
    if (networkError) throw new Error(networkError.message);
    setConnected(true);
    setAddress(addr);
    setNetwork(net);

    return addr;
  }, []);

  const disconnect = useCallback(() => {
    setConnected(false);
    setAddress(null);
    setNetwork(null);
  }, []);

  const sign = useCallback(
    async (xdr: string, networkPassphrase: string) => {
      if (!connected) throw new Error("Wallet not connected");
      const { signedTxXdr, error } = await signTransaction(xdr, {
        networkPassphrase,
      });
      if (error) throw new Error(error.message);
      return signedTxXdr;
    },
    [connected]
  );

  return { connected, address, network, connect, disconnect, sign };
}
Stellar Wallets Kit(マルチウォレット)
// hooks/useStellarWallet.ts
import { useState, useCallback } from "react";
import { StellarWalletsKit, Networks } from "@creit-tech/stellar-wallets-kit";
import { defaultModules } from "@creit-tech/stellar-wallets-kit/modules/utils";

// v2 is a static singleton: init once at module load, then call static methods —
// there is no instance to construct or pass around.
// defaultModules() loads every wallet that needs no extra setup; modules with
// prerequisites (WalletConnect, Ledger, Trezor) must be imported and added explicitly.
StellarWalletsKit.init({
  modules: defaultModules(),
  network: Networks.TESTNET,
});

export function useStellarWallet() {
  const [address, setAddress] = useState<string | null>(null);

  const connect = useCallback(async () => {
    // authModal() opens the wallet picker, sets the chosen module active,
    // and returns the address — one call replaces v1's openModal callback dance.
    const { address } = await StellarWalletsKit.authModal();
    setAddress(address);
  }, []);

  const disconnect = useCallback(async () => {
    await StellarWalletsKit.disconnect();
    setAddress(null);
  }, []);

  const sign = useCallback(async (xdr: string) => {
    const { signedTxXdr } = await StellarWalletsKit.signTransaction(xdr);
    return signedTxXdr;
  }, []);

  return { address, connect, disconnect, sign };
}

v1 から移行しますか?(2026 年 7 月時点)v1 は @creit.tech/stellar-wallets-kit というドット付きスコープで npm に公開され、new StellarWalletsKit({...})、allowAllModules()、openModal({ onWalletSelected }) を使用していました。v2 は @creit-tech/stellar-wallets-kit として JSR に移行し、キットを完全に静的にしたほか、allowAllModules() を defaultModules() に置き換え、ウォレットの選択とアドレスの取得を authModal() に統合しました。現在は npm との同等性を維持していますが、メンテナーによると、将来的に npm への更新は停止する予定です。JSR からインストールしてください。ウォレットの事前選択(setWallet(FREIGHTER_ID))は引き続き利用できます。ID 定数は、@creit-tech/stellar-wallets-kit/modules/freighter のようなウォレットごとのモジュールサブパスに移動しました。

ウォレットの統合やコントラクト呼び出しには、Blux も使用できます。例については、react.md と data-fetching.md を参照してください。

トランザクションの構築

基本的な支払い
import * as StellarSdk from "@stellar/stellar-sdk";
import { horizon, config } from "@/lib/stellar";

export async function buildPaymentTx(
  sourceAddress: string,
  destinationAddress: string,
  amount: string,
  asset: StellarSdk.Asset = StellarSdk.Asset.native()
) {
  const account = await horizon.loadAccount(sourceAddress);

  const transaction = new StellarSdk.TransactionBuilder(account, {
    fee: StellarSdk.BASE_FEE,
    networkPassphrase: config.networkPassphrase,
  })
    .addOperation(
      StellarSdk.Operation.payment({
        destination: destinationAddress,
        asset: asset,
        amount: amount,
      })
    )
    .setTimeout(180)
    .build();

  return transaction.toXDR();
}
スマートコントラクトの呼び出し(contract.Client)

JS から Stellar スマートコントラクトを呼び出す標準的な方法は、手作業で Contract.call と assembleTransaction を組み立てることではなく、contract.Client を使うことです。クライアントはネットワークからコントラクトのインターフェースを読み取るため、各メソッドを名前で呼び出せ、AssembledTransaction を返します。ネイティブな JS の結果を取得でき、ScVal を手作業で構築する必要はありません。

import { contract } from "@stellar/stellar-sdk";
import { config } from "@/lib/stellar";

// Describe just the methods you call. `Client.from<T>()` uses this to type
// the returned client, so calls are checked and autocompleted — no codegen.
// For a contract with many methods, generate this interface from its spec
// with the SDK's binding CLI instead of writing it by hand.
interface CounterContract {
  increment: (
    options?: contract.MethodOptions,
  ) => Promise<contract.AssembledTransaction<number>>;
}

// `signTransaction` comes from the wallet (e.g. Freighter/Wallets Kit in the
// browser). `contract.basicNodeSigner(keypair, networkPassphrase)` is the
// Node equivalent for scripts and tests.
export async function getCounterClient(
  contractId: string,
  publicKey: string,
  signTransaction: contract.ClientOptions["signTransaction"],
) {
  return contract.Client.from<CounterContract>({
    contractId,
    rpcUrl: config.rpcUrl,
    networkPassphrase: config.networkPassphrase,
    publicKey,
    signTransaction,
  });
}

// Preview (free simulation) then sign + send to apply on-chain.
export async function increment(client: contract.Client & CounterContract) {
  const tx = await client.increment();
  console.log("preview:", tx.result); // predicted return value, no signature
  const sent = await tx.signAndSend(); // submits and polls to completion
  return sent.result;
}

AssembledTransaction では、細かい制御(第 2 引数の { fee, simulate, timeoutInSeconds })や、tx.needsNonInvokerSigningBy() / tx.signAuthEntries() による複数当事者の認証もサポートされています。コントラクトの呼び出し と コントラクト呼び出しの承認 を参照してください。

上級者向け:クライアントを使わない低レベルの呼び出し

トランザクションを直接制御する必要がある場合(例:コントラクト呼び出しと従来のオペレーションをまとめてバッチ処理する場合)に限り、これを使用してください。それ以外の場合は、上記の contract.Client を推奨します。

import * as StellarSdk from "@stellar/stellar-sdk";
import { rpc, config } from "@/lib/stellar";

export async function invokeContract(
  sourceAddress: string,
  contractId: string,
  method: string,
  args: StellarSdk.xdr.ScVal[]
) {
  const account = await rpc.getAccount(sourceAddress);
  const contract = new StellarSdk.Contract(contractId);

  const transaction = new StellarSdk.TransactionBuilder(account, {
    fee: StellarSdk.BASE_FEE,
    networkPassphrase: config.networkPassphrase,
  })
    .addOperation(contract.call(method, ...args))
    .setTimeout(180)
    .build();

  // `prepareTransaction` simulates and applies footprint/auth/fees in one step.
  // (Equivalent to simulateTransaction + rpc.assembleTransaction.)
  const prepared = await rpc.prepareTransaction(transaction);
  return prepared.toXDR();
}

ScVal 引数を手作業で構築する(低レベルの方法でのみ必要です。contract.Client がネイティブな JS の引数を変換します):

import * as StellarSdk from "@stellar/stellar-sdk";

const addressVal = StellarSdk.Address.fromString(address).toScVal();
const i128Val = StellarSdk.nativeToScVal(BigInt(amount), { type: "i128" });
const u32Val = StellarSdk.nativeToScVal(42, { type: "u32" });
const stringVal = StellarSdk.nativeToScVal("hello", { type: "string" });
const symbolVal = StellarSdk.nativeToScVal("transfer", { type: "symbol" });

// Struct
const structVal = StellarSdk.nativeToScVal(
  { name: "Token", decimals: 7 },
  {
    type: {
      name: ["symbol", null],
      decimals: ["u32", null],
    },
  }
);

// Vec of i128 — the element type is applied to each item
const vecVal = StellarSdk.nativeToScVal(
  [1, 2, 3].map((n) => BigInt(n)),
  { type: "i128" }
);

トランザクションの送信

送信して確認を待つ
import * as StellarSdk from "@stellar/stellar-sdk";
import { rpc, horizon, config } from "@/lib/stellar";

export async function submitTransaction(signedXdr: string) {
  const transaction = StellarSdk.TransactionBuilder.fromXDR(
    signedXdr,
    config.networkPassphrase
  );

  // For smart contract transactions, use RPC
  if (transaction.operations.some(op => op.type === "invokeHostFunction")) {
    return submitSorobanTransaction(signedXdr);
  }

  // For classic transactions, use Horizon
  return submitClassicTransaction(signedXdr);
}

async function submitSorobanTransaction(signedXdr: string) {
  const transaction = StellarSdk.TransactionBuilder.fromXDR(
    signedXdr,
    config.networkPassphrase
  ) as StellarSdk.Transaction;

  const response = await rpc.sendTransaction(transaction);

  if (response.status === "ERROR") {
    throw new Error(`Send failed: ${response.errorResult}`);
  }

  // Poll for completion. pollTransaction handles the retry loop (default 5
  // attempts, 1s apart — tune with { attempts, sleepStrategy }) instead of a
  // hand-rolled while loop that can spin forever.
  const getResponse = await rpc.pollTransaction(response.hash);

  if (getResponse.status === "SUCCESS") {
    return {
      hash: response.hash,
      result: getResponse.returnValue,
    };
  }

  throw new Error(`Transaction failed: ${getResponse.status}`);
}

async function submitClassicTransaction(signedXdr: string) {
  const transaction = StellarSdk.TransactionBuilder.fromXDR(
    signedXdr,
    config.networkPassphrase
  ) as StellarSdk.Transaction;

  const response = await horizon.submitTransaction(transaction);
  return {
    hash: response.hash,
    ledger: response.ledger,
  };
}

トランザクション UX チェックリスト

  • ウォレットの署名中に読み込み状態を表示する
  • 送信直後にトランザクションハッシュを表示する
  • 確認ステータスを追跡する(保留中 → 成功/失敗)
  • よくあるエラーに対して、分かりやすいメッセージを表示する:
    • ウォレットが接続されていない
    • ユーザーが署名を拒否した
    • 手数料分の XLM が不足している
    • アカウントに資金がない
    • ネットワークが一致しない(ウォレットが別のネットワークに接続されている)
    • トランザクションがタイムアウトした/期限切れになった
  • 処理中の二重送信を防ぐ
  • 署名前に送信先と金額を表示する

コメント