dapp について
ブラウザーや Node.js で dApp を開発する JavaScript 開発者向けの Stellar フロントエンドスキルです。ウォレット接続から、トランザクションの構築、コントラクト呼び出し、署名、送信までを扱います。
主な機能
- 01Stellar JavaScript SDK の初期化
- 02Freighter または複数ウォレットへの接続
- 03トランザクションの構築と送信
- 04スマートコントラクトの呼び出し
- 05確認状態とトランザクションエラーの処理
活用シーン
- 01dApp へのウォレット接続の追加
- 02Stellar 決済の構築と送信
- 03フロントエンドからのスマートコントラクト呼び出し
- 04パスキー対応スマートアカウントの実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ stellar/ stellar-dev-skill --skill dappインストール後、Claude Code で/dappを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
data-fetching.mdreact.mdsmart-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 が不足している
- アカウントに資金がない
- ネットワークが一致しない(ウォレットが別のネットワークに接続されている)
- トランザクションがタイムアウトした/期限切れになった
- 処理中の二重送信を防ぐ
- 署名前に送信先と金額を表示する