mcpField
D
dapp

dapp

github.com/stellar开发55371 次安装1 周前更新
代码仓库

关于 dapp

面向使用 JavaScript 构建浏览器或 Node.js dApp 的 Stellar 前端开发技能,涵盖钱包连接及交易全流程,包括构建、合约调用、签名和提交。

主要功能

  1. 01初始化 Stellar JavaScript SDK
  2. 02连接 Freighter 或多个钱包
  3. 03构建并提交交易
  4. 04调用智能合约
  5. 05处理确认状态与交易错误

适用场景

  1. 01为 dApp 添加钱包连接
  2. 02构建并提交 Stellar 支付
  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 / 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 手续费不足
    • 账户未注资
    • 网络不匹配(钱包所处网络错误)
    • 交易超时/已过期
  • 处理期间禁止重复提交
  • 签名前显示目标地址和金额

评论

支持 Markdown