dapp
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStellar dApp / Frontend
Stellar dApp/前端开发
Client-side development with , wallet connection, signing, and submitting transactions. Covers both classic Stellar operations and Soroban contract invocation from the browser or Node.js.
@stellar/stellar-sdk使用进行客户端开发,包括钱包连接、签名和交易提交。涵盖传统Stellar操作以及从浏览器或Node.js调用Soroban合约的内容。
@stellar/stellar-sdkWhen to use this skill
何时使用本技能
- Connecting Freighter or other wallets via Stellar Wallets Kit
- Building, simulating, signing, and submitting transactions
- Invoking Soroban contracts from a frontend
- Implementing smart accounts with passkeys
- Handling network passphrases (Mainnet / Testnet / local)
- 通过Stellar Wallets Kit连接Freighter或其他钱包
- 构建、模拟、签名并提交交易
- 从前端调用Soroban合约
- 实现基于密钥的智能账户
- 处理网络密码(主网/测试网/本地网络)
Related skills
相关技能
- Writing the contract being invoked →
../soroban/SKILL.md - Issuing assets and managing trustlines →
../assets/SKILL.md - Querying chain state via RPC / Horizon →
../data/SKILL.md - Building paid APIs or agent payment clients →
../agentic-payments/SKILL.md - SEPs the wallet/anchor flows depend on →
../standards/SKILL.md
- 编写被调用的合约 →
../soroban/SKILL.md - 发行资产并管理信任线 →
../assets/SKILL.md - 通过RPC/Horizon查询链上状态 →
../data/SKILL.md - 构建付费API或代理支付客户端 →
../agentic-payments/SKILL.md - 钱包/锚点流程依赖的SEP标准 →
../standards/SKILL.md
Goals
目标
- Single SDK instance for the app (RPC/Horizon + transaction building)
- Freighter wallet integration (or multi-wallet via Stellar Wallets Kit)
- Clean separation of client/server in Next.js
- Transaction sending with proper confirmation handling
- 应用中使用单一SDK实例(RPC/Horizon + 交易构建)
- Freighter钱包集成(或通过Stellar Wallets Kit实现多钱包支持)
- 在Next.js中清晰分离客户端与服务端
- 交易发送并处理确认逻辑
Quick Navigation
快速导航
- SDK setup and env config: SDK Initialization
- Wallet integrations: Wallet Integration
- Tx build/send patterns: Transaction Building, Transaction Submission
- React + Next.js patterns: React Components, Next.js App Router Setup
- Smart wallets/passkeys: Smart Accounts (Passkey Wallets)
- Production UX checklist: Transaction UX Checklist
- SDK设置与环境配置:SDK初始化
- 钱包集成:钱包集成
- 交易构建/发送模式:交易构建、交易提交
- React + Next.js模式:React组件、Next.js App Router配置
- 智能钱包/密钥:智能账户(密钥钱包)
- 生产环境UX检查清单:交易UX检查清单
Recommended Dependencies
推荐依赖
Requires Node.js 20+ — the Stellar SDK dropped Node 18 support.
bash
npm install @stellar/stellar-sdk @stellar/freighter-api要求Node.js 20+ — Stellar SDK已不再支持Node 18。
bash
npm install @stellar/stellar-sdk @stellar/freighter-apiOr for multi-wallet support:
如需多钱包支持:
npm install @stellar/stellar-sdk @creit.tech/stellar-wallets-kit
undefinednpm install @stellar/stellar-sdk @creit.tech/stellar-wallets-kit
undefinedSDK Initialization
SDK初始化
For the full API reference (RPC methods, Horizon endpoints, migration guide), see api-rpc-horizon.md.
完整API参考(RPC方法、Horizon端点、迁移指南)请查看api-rpc-horizon.md。
Basic Setup
基础设置
typescript
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;typescript
import * as StellarSdk from "@stellar/stellar-sdk";
// 测试网配置
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;
// 主网配置
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); // 从你选择的RPC服务商获取
const mainnetNetworkPassphrase = StellarSdk.Networks.PUBLIC;Environment Configuration
环境配置
Use a provider-specific mainnet RPC URL (see: https://developers.stellar.org/docs/data/apis/rpc/providers).
typescript
// 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;
};
export const config = {
testnet: {
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: StellarSdk.Networks.TESTNET,
friendbotUrl: "https://friendbot.stellar.org",
},
mainnet: {
horizonUrl: "https://horizon.stellar.org",
rpcUrl: requireEnv("NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL"),
networkPassphrase: StellarSdk.Networks.PUBLIC,
friendbotUrl: null,
},
}[NETWORK]!;
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);使用服务商提供的主网RPC URL(参考:https://developers.stellar.org/docs/data/apis/rpc/providers)。
typescript
// 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;
};
export const config = {
testnet: {
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: StellarSdk.Networks.TESTNET,
friendbotUrl: "https://friendbot.stellar.org",
},
mainnet: {
horizonUrl: "https://horizon.stellar.org",
rpcUrl: requireEnv("NEXT_PUBLIC_STELLAR_MAINNET_RPC_URL"),
networkPassphrase: StellarSdk.Networks.PUBLIC,
friendbotUrl: null,
},
}[NETWORK]!;
export const horizon = new StellarSdk.Horizon.Server(config.horizonUrl);
export const rpc = new StellarSdk.rpc.Server(config.rpcUrl);Wallet Integration
钱包集成
Freighter (Primary Browser Wallet)
Freighter(主流浏览器钱包)
typescript
// hooks/useFreighter.ts
import { useState, useEffect, useCallback } from "react";
import {
isConnected,
isAllowed,
setAllowed,
getPublicKey,
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 freighterConnected = await isConnected();
if (!freighterConnected) return;
const allowed = await isAllowed();
if (allowed) {
const pubKey = await getPublicKey();
const net = await getNetwork();
setConnected(true);
setAddress(pubKey);
setNetwork(net);
}
};
const connect = useCallback(async () => {
const freighterConnected = await isConnected();
if (!freighterConnected) {
throw new Error("Freighter extension not installed");
}
await setAllowed();
const pubKey = await getPublicKey();
const net = await getNetwork();
setConnected(true);
setAddress(pubKey);
setNetwork(net);
return pubKey;
}, []);
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");
return signTransaction(xdr, { networkPassphrase });
},
[connected]
);
return { connected, address, network, connect, disconnect, sign };
}typescript
// hooks/useFreighter.ts
import { useState, useEffect, useCallback } from "react";
import {
isConnected,
isAllowed,
setAllowed,
getPublicKey,
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 freighterConnected = await isConnected();
if (!freighterConnected) return;
const allowed = await isAllowed();
if (allowed) {
const pubKey = await getPublicKey();
const net = await getNetwork();
setConnected(true);
setAddress(pubKey);
setNetwork(net);
}
};
const connect = useCallback(async () => {
const freighterConnected = await isConnected();
if (!freighterConnected) {
throw new Error("Freighter扩展未安装");
}
await setAllowed();
const pubKey = await getPublicKey();
const net = await getNetwork();
setConnected(true);
setAddress(pubKey);
setNetwork(net);
return pubKey;
}, []);
const disconnect = useCallback(() => {
setConnected(false);
setAddress(null);
setNetwork(null);
}, []);
const sign = useCallback(
async (xdr: string, networkPassphrase: string) => {
if (!connected) throw new Error("钱包未连接");
return signTransaction(xdr, { networkPassphrase });
},
[connected]
);
return { connected, address, network, connect, disconnect, sign };
}Stellar Wallets Kit (Multi-Wallet)
Stellar Wallets Kit(多钱包支持)
typescript
// hooks/useStellarWallet.ts
import { useState, useCallback } from "react";
import {
StellarWalletsKit,
WalletNetwork,
allowAllModules,
FREIGHTER_ID,
LOBSTR_ID,
XBULL_ID,
} from "@creit.tech/stellar-wallets-kit";
const kit = new StellarWalletsKit({
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
modules: allowAllModules(),
});
export function useStellarWallet() {
const [address, setAddress] = useState<string | null>(null);
const connect = useCallback(async () => {
await kit.openModal({
onWalletSelected: async (option) => {
kit.setWallet(option.id);
const { address } = await kit.getAddress();
setAddress(address);
},
});
}, []);
const disconnect = useCallback(() => {
setAddress(null);
}, []);
const sign = useCallback(async (xdr: string) => {
const { signedTxXdr } = await kit.signTransaction(xdr);
return signedTxXdr;
}, []);
return { address, connect, disconnect, sign, kit };
}typescript
// hooks/useStellarWallet.ts
import { useState, useCallback } from "react";
import {
StellarWalletsKit,
WalletNetwork,
allowAllModules,
FREIGHTER_ID,
LOBSTR_ID,
XBULL_ID,
} from "@creit.tech/stellar-wallets-kit";
const kit = new StellarWalletsKit({
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
modules: allowAllModules(),
});
export function useStellarWallet() {
const [address, setAddress] = useState<string | null>(null);
const connect = useCallback(async () => {
await kit.openModal({
onWalletSelected: async (option) => {
kit.setWallet(option.id);
const { address } = await kit.getAddress();
setAddress(address);
},
});
}, []);
const disconnect = useCallback(() => {
setAddress(null);
}, []);
const sign = useCallback(async (xdr: string) => {
const { signedTxXdr } = await kit.signTransaction(xdr);
return signedTxXdr;
}, []);
return { address, connect, disconnect, sign, kit };
}Transaction Building
交易构建
Basic Payment
基础支付
typescript
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();
}typescript
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();
}Soroban Contract Invocation
Soroban合约调用
typescript
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);
let transaction = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: config.networkPassphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(180)
.build();
// Simulate to get resource estimates
const simulation = await rpc.simulateTransaction(transaction);
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
throw new Error(`Simulation failed: ${simulation.error}`);
}
// Assemble with proper resources
transaction = StellarSdk.rpc.assembleTransaction(
transaction,
simulation
).build();
return transaction.toXDR();
}typescript
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);
let transaction = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: config.networkPassphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(180)
.build();
// 模拟交易获取资源预估
const simulation = await rpc.simulateTransaction(transaction);
if (StellarSdk.rpc.Api.isSimulationError(simulation)) {
throw new Error(`模拟失败: ${simulation.error}`);
}
// 使用预估资源组装交易
transaction = StellarSdk.rpc.assembleTransaction(
transaction,
simulation
).build();
return transaction.toXDR();
}Building ScVal Arguments
构建ScVal参数
typescript
import * as StellarSdk from "@stellar/stellar-sdk";
// Common conversions
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
const vecVal = StellarSdk.nativeToScVal([1, 2, 3], { type: "i128" });typescript
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" });
// 结构体
const structVal = StellarSdk.nativeToScVal(
{ name: "Token", decimals: 7 },
{
type: {
name: ["symbol", null],
decimals: ["u32", null],
},
}
);
// 数组
const vecVal = StellarSdk.nativeToScVal([1, 2, 3], { type: "i128" });Transaction Submission
交易提交
Submit and Wait for Confirmation
提交并等待确认
typescript
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 Soroban 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(`Transaction failed: ${response.errorResult}`);
}
// Poll for completion
let getResponse = await rpc.getTransaction(response.hash);
while (getResponse.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 1000));
getResponse = await rpc.getTransaction(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,
};
}typescript
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
);
// Soroban交易使用RPC提交
if (transaction.operations.some(op => op.type === "invokeHostFunction")) {
return submitSorobanTransaction(signedXdr);
}
// 传统交易使用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(`交易失败: ${response.errorResult}`);
}
// 轮询等待交易完成
let getResponse = await rpc.getTransaction(response.hash);
while (getResponse.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 1000));
getResponse = await rpc.getTransaction(response.hash);
}
if (getResponse.status === "SUCCESS") {
return {
hash: response.hash,
result: getResponse.returnValue,
};
}
throw new Error(`交易失败: ${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,
};
}React Components
React组件
Connect Wallet Button
连接钱包按钮
tsx
// components/ConnectButton.tsx
"use client";
import { useFreighter } from "@/hooks/useFreighter";
export function ConnectButton() {
const { connected, address, connect, disconnect } = useFreighter();
if (connected && address) {
return (
<div className="flex items-center gap-2">
<span className="text-sm">
{address.slice(0, 4)}...{address.slice(-4)}
</span>
<button
onClick={disconnect}
className="px-4 py-2 bg-red-500 text-white rounded"
>
Disconnect
</button>
</div>
);
}
return (
<button
onClick={connect}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Connect Wallet
</button>
);
}tsx
// components/ConnectButton.tsx
"use client";
import { useFreighter } from "@/hooks/useFreighter";
export function ConnectButton() {
const { connected, address, connect, disconnect } = useFreighter();
if (connected && address) {
return (
<div className="flex items-center gap-2">
<span className="text-sm">
{address.slice(0, 4)}...{address.slice(-4)}
</span>
<button
onClick={disconnect}
className="px-4 py-2 bg-red-500 text-white rounded"
>
断开连接
</button>
</div>
);
}
return (
<button
onClick={connect}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
连接钱包
</button>
);
}Send Payment Form
发送支付表单
tsx
// components/SendPayment.tsx
"use client";
import { useState } from "react";
import { useFreighter } from "@/hooks/useFreighter";
import { buildPaymentTx, submitTransaction } from "@/lib/transactions";
export function SendPayment() {
const { address, sign } = useFreighter();
const [destination, setDestination] = useState("");
const [amount, setAmount] = useState("");
const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!address) return;
setLoading(true);
setStatus("Building transaction...");
try {
const xdr = await buildPaymentTx(address, destination, amount);
setStatus("Please sign in your wallet...");
const signedXdr = await sign(xdr, config.networkPassphrase);
setStatus("Submitting transaction...");
const result = await submitTransaction(signedXdr);
setStatus(`Success! Hash: ${result.hash}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
placeholder="Destination Address"
value={destination}
onChange={(e) => setDestination(e.target.value)}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="Amount (XLM)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full p-2 border rounded"
/>
<button
type="submit"
disabled={loading || !address}
className="w-full p-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{loading ? "Processing..." : "Send"}
</button>
{status && <p className="text-sm">{status}</p>}
</form>
);
}tsx
// components/SendPayment.tsx
"use client";
import { useState } from "react";
import { useFreighter } from "@/hooks/useFreighter";
import { buildPaymentTx, submitTransaction } from "@/lib/transactions";
import { config } from "@/lib/stellar";
export function SendPayment() {
const { address, sign } = useFreighter();
const [destination, setDestination] = useState("");
const [amount, setAmount] = useState("");
const [status, setStatus] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!address) return;
setLoading(true);
setStatus("正在构建交易...");
try {
const xdr = await buildPaymentTx(address, destination, amount);
setStatus("请在钱包中完成签名...");
const signedXdr = await sign(xdr, config.networkPassphrase);
setStatus("正在提交交易...");
const result = await submitTransaction(signedXdr);
setStatus(`交易成功!哈希值: ${result.hash}`);
} catch (error) {
setStatus(`错误: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
placeholder="目标地址"
value={destination}
onChange={(e) => setDestination(e.target.value)}
className="w-full p-2 border rounded"
/>
<input
type="text"
placeholder="金额(XLM)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="w-full p-2 border rounded"
/>
<button
type="submit"
disabled={loading || !address}
className="w-full p-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{loading ? "处理中..." : "发送"}
</button>
{status && <p className="text-sm">{status}</p>}
</form>
);
}Next.js App Router Setup
Next.js App Router配置
Provider Component
提供者组件
tsx
// app/providers.tsx
"use client";
import { ReactNode } from "react";
// Add any context providers here
export function Providers({ children }: { children: ReactNode }) {
return <>{children}</>;
}tsx
// app/providers.tsx
"use client";
import { ReactNode } from "react";
// 在此添加上下文提供者
export function Providers({ children }: { children: ReactNode }) {
return <>{children}</>;
}Layout
布局文件
tsx
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}tsx
// app/layout.tsx
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Data Fetching
数据获取
Account Balance
账户余额
typescript
import { horizon } from "@/lib/stellar";
export async function getBalance(address: string) {
try {
const account = await horizon.loadAccount(address);
const nativeBalance = account.balances.find(
(b) => b.asset_type === "native"
);
return nativeBalance?.balance || "0";
} catch (error) {
if (error.response?.status === 404) {
return "0"; // Account not funded
}
throw error;
}
}typescript
import { horizon } from "@/lib/stellar";
export async function getBalance(address: string) {
try {
const account = await horizon.loadAccount(address);
const nativeBalance = account.balances.find(
(b) => b.asset_type === "native"
);
return nativeBalance?.balance || "0";
} catch (error) {
if (error.response?.status === 404) {
return "0"; // 账户未充值
}
throw error;
}
}Contract State
合约状态
typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { rpc } from "@/lib/stellar";
export async function getContractData(
contractId: string,
key: StellarSdk.xdr.ScVal
) {
const ledgerKey = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: new StellarSdk.Address(contractId).toScAddress(),
key: key,
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
})
);
const entries = await rpc.getLedgerEntries(ledgerKey);
if (entries.entries.length === 0) {
return null;
}
return StellarSdk.scValToNative(
entries.entries[0].val.contractData().val()
);
}typescript
import * as StellarSdk from "@stellar/stellar-sdk";
import { rpc } from "@/lib/stellar";
export async function getContractData(
contractId: string,
key: StellarSdk.xdr.ScVal
) {
const ledgerKey = StellarSdk.xdr.LedgerKey.contractData(
new StellarSdk.xdr.LedgerKeyContractData({
contract: new StellarSdk.Address(contractId).toScAddress(),
key: key,
durability: StellarSdk.xdr.ContractDataDurability.persistent(),
})
);
const entries = await rpc.getLedgerEntries(ledgerKey);
if (entries.entries.length === 0) {
return null;
}
return StellarSdk.scValToNative(
entries.entries[0].val.contractData().val()
);
}Smart Accounts (Passkey Wallets)
智能账户(密钥钱包)
For passwordless authentication using WebAuthn passkeys, use Smart Account Kit.
如需使用WebAuthn密钥实现无密码认证,请使用Smart Account Kit。
Installation
安装
bash
npm install smart-account-kitbash
npm install smart-account-kitQuick Start
快速开始
typescript
import { SmartAccountKit, IndexedDBStorage } from 'smart-account-kit';
const kit = new SmartAccountKit({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
accountWasmHash: 'YOUR_ACCOUNT_WASM_HASH',
webauthnVerifierAddress: 'CWEBAUTHN_VERIFIER_ADDRESS',
storage: new IndexedDBStorage(),
});
// On page load - silent restore from stored session
const result = await kit.connectWallet();
if (!result) {
showConnectButton(); // No stored session
}
// Create new wallet with passkey
const { contractId, credentialId } = await kit.createWallet(
'My App',
'user@example.com',
{ autoSubmit: true }
);
// Connect to existing wallet (prompts for passkey)
await kit.connectWallet({ prompt: true });
// Sign and submit transactions
const result = await kit.signAndSubmit(transaction);
// Transfer tokens
await kit.transfer(tokenContract, recipient, amount);typescript
import { SmartAccountKit, IndexedDBStorage } from 'smart-account-kit';
const kit = new SmartAccountKit({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
accountWasmHash: 'YOUR_ACCOUNT_WASM_HASH',
webauthnVerifierAddress: 'CWEBAUTHN_VERIFIER_ADDRESS',
storage: new IndexedDBStorage(),
});
// 页面加载时 - 从存储会话中静默恢复
const result = await kit.connectWallet();
if (!result) {
showConnectButton(); // 无存储会话
}
// 使用密钥创建新钱包
const { contractId, credentialId } = await kit.createWallet(
'My App',
'user@example.com',
{ autoSubmit: true }
);
// 连接现有钱包(提示输入密钥)
await kit.connectWallet({ prompt: true });
// 签名并提交交易
const result = await kit.signAndSubmit(transaction);
// 转账
await kit.transfer(tokenContract, recipient, amount);Key Features
核心特性
- Session Management: Automatic credential persistence and silent reconnection
- Multiple Signer Types: Passkeys (secp256r1), Ed25519 keys, policies
- Context Rules: Fine-grained authorization for different operations
- Policy Support: Threshold multisig, spending limits, custom policies
- External Wallet Support: Connect Freighter, LOBSTR via adapters
- Gasless Transactions: Optional relayer integration for fee sponsoring
- 会话管理:自动凭证持久化与静默重连
- 多签名类型:密钥(secp256r1)、Ed25519密钥、策略
- 上下文规则:针对不同操作的细粒度授权
- 策略支持:阈值多签、消费限额、自定义策略
- 外部钱包支持:通过适配器连接Freighter、LOBSTR
- 无Gas交易:可选中继器集成实现费用赞助
Fee Sponsorship with OpenZeppelin Relayer
使用OpenZeppelin中继器实现费用赞助
The OpenZeppelin Relayer (also called Stellar Channels Service) handles gasless transaction submission. It replaces the deprecated Launchtube service and uses Stellar's native fee bump mechanism so users don't need XLM for fees.
typescript
import * as RPChannels from "@openzeppelin/relayer-plugin-channels";
const client = new RPChannels.ChannelsClient({
baseUrl: "https://channels.openzeppelin.com/testnet",
apiKey: "your-api-key",
});
// Submit a Soroban contract call with fee sponsorship
const response = await client.submitSorobanTransaction({
func: contractFunc,
auth: contractAuth,
});- Testnet hosted instance: (API keys at
https://channels.openzeppelin.com/testnet)/gen - Production: Self-host via Docker (GitHub)
- Stellar docs: https://developers.stellar.org/docs/tools/openzeppelin-relayer
OpenZeppelin中继器(也称为Stellar Channels Service)处理无Gas交易提交。它替代了已弃用的Launchtube服务,使用Stellar原生的费用 bump机制,用户无需XLM支付手续费。
typescript
import * as RPChannels from "@openzeppelin/relayer-plugin-channels";
const client = new RPChannels.ChannelsClient({
baseUrl: "https://channels.openzeppelin.com/testnet",
apiKey: "your-api-key",
});
// 提交带有费用赞助的Soroban合约调用
const response = await client.submitSorobanTransaction({
func: contractFunc,
auth: contractAuth,
});- 测试网托管实例:(API密钥在
https://channels.openzeppelin.com/testnet路径获取)/gen - 生产环境:通过Docker自行部署(GitHub)
- Stellar文档:https://developers.stellar.org/docs/tools/openzeppelin-relayer
Resources
资源
- GitHub: https://github.com/kalepail/smart-account-kit
- OpenZeppelin Contracts: https://github.com/OpenZeppelin/stellar-contracts
- Legacy SDK: https://github.com/kalepail/passkey-kit (for simpler use cases)
Transaction UX Checklist
交易UX检查清单
- Show loading state during wallet signing
- Display transaction hash immediately after submission
- Track confirmation status (pending → success/failed)
- Handle common errors with clear messages:
- Wallet not connected
- User rejected signing
- Insufficient XLM for fees
- Account not funded
- Network mismatch (wallet on wrong network)
- Transaction timeout/expired
- Prevent double-submission while processing
- Show destination and amount before signing
- 钱包签名过程中显示加载状态
- 提交后立即显示交易哈希
- 跟踪确认状态(待处理→成功/失败)
- 清晰展示常见错误信息:
- 钱包未连接
- 用户拒绝签名
- 手续费XLM不足
- 账户未充值
- 网络不匹配(钱包使用错误网络)
- 交易超时/过期
- 处理中防止重复提交
- 签名前展示目标地址和金额