关于 dapp
面向使用 JavaScript 构建浏览器或 Node.js dApp 的 Stellar 前端开发技能,涵盖钱包连接及交易全流程,包括构建、合约调用、签名和提交。
主要功能
- 01初始化 Stellar JavaScript SDK
- 02连接 Freighter 或多个钱包
- 03构建并提交交易
- 04调用智能合约
- 05处理确认状态与交易错误
适用场景
- 01为 dApp 添加钱包连接
- 02构建并提交 Stellar 支付
- 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 / local)
相关技能
- 编写要调用的合约 →
../smart-contracts/SKILL.md - 发行资产并管理信任线 →
../assets/SKILL.md - 通过 RPC / Horizon 查询链上状态 →
../data/SKILL.md - 构建收费 API 或 Agent 支付客户端 →
../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 进行免 Gas 交易 | smart-accounts.md |
| 生产环境 UX 检查清单 | 交易 UX 检查清单(下文) |
推荐依赖
需要 Node.js 22+。 从 SDK v16 开始,Node 22 是最低版本(较旧的 Node 会产生
EBADENGINE警告)。v16 还将@stellar/stellar-base合并到了@stellar/stellar-sdk中,优先采用 ESM,并使用原生fetch替代 axios。如果仍直接导入@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供 Agent 使用)。钱包集成(Freighter、Stellar Wallets Kit)、通行密钥智能账户以及 OpenZeppelin 中继器属于独立的软件包,并非 JS SDK 的组成部分,请依据各自的上游文档进行核验。
SDK 初始化
完整的 API 参考(RPC 方法、Horizon 端点、迁移指南)请参阅 data 技能。
基本设置
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;
环境配置
使用特定提供商的主网 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 钱包套件(多钱包)
// 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 发布在 npm 上,使用带点的作用域
@creit.tech/stellar-wallets-kit,以及new StellarWalletsKit({...})、allowAllModules()和openModal({ onWalletSelected })。v2 迁移到了 JSR,使用@creit-tech/stellar-wallets-kit,将套件改为完全静态,并以defaultModules()替代allowAllModules(),同时将钱包选择和地址获取合并到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.Client,而不是手动构建 Contract.call + assembleTransaction。客户端会从网络读取合约接口,因此可以按名称调用每个方法,并返回一个 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 还支持精细控制(将 { 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 手续费不足
- 账户未注资
- 网络不匹配(钱包所处网络错误)
- 交易超时/已过期
- 处理期间禁止重复提交
- 签名前显示目标地址和金额