如何使用 Jupiter Ultra API 交换 Token

本文介绍了如何使用 Jupiter Ultra API 在 Solana 上构建 Token 交换 UI。通过使用 Jupiter Ultra API,开发者可以简化 Token 发现、报价、滑点处理和交易执行的过程,从而创建一个更可靠的交换 UI。文章提供了一个示例应用程序,展示了如何使用 quote → order → sign → execute 的生命周期来实现 Token 交换。

概述

在 Solana 上构建一个 Swap UI 通常意味着将 token 发现、报价、滑点处理、交易构建、发送、重试和状态更新拼接在一起。这种复杂性很容易导致交付的东西能工作,但在实际用户条件下会崩溃。

Jupiter Ultra Swap 通过为你提供清晰的执行路径来简化这一点。 你可以获得一个报价来预览 swap,然后请求一个订单,该订单返回一个交易就绪的 payload 供用户签名,最后执行签名的交易。

在本指南结束时,你将在 Solana 上拥有一个由 Jupiter Ultra API 驱动的可工作的 token swap UI,用户可以选择 token、获得报价、签名一次并端到端执行 swap。

你将做什么

你将连接一个先报价的流程,将 swap 限制为仅经过验证的 token 列表,并连接 Ultra 订单/签名/执行生命周期,以便你的 Swap UI 可以可靠地完成 swap 并显示最终签名。

  • 克隆并运行配套的示例应用程序
  • 加载并强制执行经过验证的 token 列表,以便用户只能 swap 经过验证的 token
  • 添加一个先报价的步骤,以便用户可以在提交之前预览预期的输出
  • 集成 Ultra swap 生命周期:订单 → 签名 → 执行 → 状态
  • 验证成功和常见的失败路径

你需要什么

本指南假设你对构建 TypeScript dApp、DeFi 和使用 Solana 钱包有基本的了解。

如果你需要复习,请参考:

你还需要:

danger

Jupiter Ultra Swap 仅在 Solana mainnet-beta 上可用。 这意味着,如果你按照本指南执行 swap,你将交易真实的 token 并支付真实的网络费用,并且你可能会因价格变动、滑点或选择错误的 token 对而损失价值。

运行示例应用程序

首先,克隆示例应用程序存储库并打开 solana/jupiter-ultra-swap 文件夹。

git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/solana/jupiter-ultra-swap
npm install
npm run dev

在本地运行它后,快速浏览一下代码库,熟悉其结构,然后返回本指南,详细了解每个集成步骤,并了解 Jupiter Ultra API 方法如何协同工作。

了解 Jupiter Ultra Swap 生命周期

你无需自己构建和发送 swap 交易,而是向 Ultra 请求一个订单,其中包括一个交易就绪的 payload。用户使用他们的钱包对其进行签名,然后将其发送回 Jupiter Ultra 以执行。

以下是你的 Swap UI 在本指南中遵循的生命周期:

将 Token 选择限制为已验证的 Token

为了保持 UX 更安全和可预测,你可以从仅经过验证的集合构建 token 选择。 Jupiter 的 Tokens API v2 支持使用 verified 标签查询 token。

/tokens/v2/tag?query=verified 检索可用于 swap 的经过验证的 token 列表。 响应经过过滤,仅显示经过验证的 token 以确保安全。

请求 Ultra 订单

在用户提交之前,你需要获取报价以显示预期的输出和 swap 条件。 Ultra 返回一个未签名的 swap 交易。 然后,你的 UI 提示连接的钱包对返回的交易 payload 进行签名。

当用户单击 Swap 生成带有 requestId 以供执行的未签名交易时,将调用 /ultra/v1/order。 此端点返回定价信息、路由详细信息和准备好签名的 base64 编码交易。

通过 Ultra 执行并跟踪状态

签名的交易被发送到 Jupiter 的基础设施,其中包括处理滑点、优先级费用和交易确认,并返回执行状态,以便你的 UI 可以显示进度和完成情况。

用户在其钱包中签署交易后,将调用 /ultra/v1/execute。 此端点提交签名的交易以及订单响应中的 requestId

可选的 UX 助手

Ultra 还公开了可以改善 UI 体验的支持端点,例如 token 搜索、持有量和 token 告。 这些对于核心 swap 生命周期不是必需的,但它们通常用于使 swap UI 感觉更完整和更安全。

/ultra/v1/holdings/{walletAddress} 检索钱包地址的 token 余额,包括原生 SOL 和 SPL token。

示例 Swap UI

此示例应用程序是一个围绕报价 → 订单 → 签名 → 执行 → 状态生命周期构建的 Swap UI。 UI 组件和状态已在调用 Jupiter API 并协调 swap 流程的模块中就位。

此应用程序使用多个 Jupiter API 端点来启用 token swap、获取 token 列表和检索钱包余额。 所有 API 调用都通过 Next.js API 路由(如 /api/quote/api/execute/api/balances/api/tokens)代理,而不是直接从客户端调用 Jupiter 的 API。 这种代理模式可以将你的 Jupiter API 密钥安全地保存在服务器上,在那里它不会在客户端代码中公开,并让你可以控制请求处理、速率限制和错误管理。

获取余额

该应用程序使用 Jupiter Ultra 的 holdings/{walletAddress} 方法来获取钱包余额。 当钱包连接时,应用程序调用 Jupiter Ultra API 密钥来检索钱包的原生 SOL 和 SPL token,每个 mint 的余额都会聚合。 这些余额用于在 swap 输入旁边显示可用的 token、将 token 选择器过滤为具有非零余额的 token、在 swap 之前验证是否有足够的余额,并启用“最大”按钮以使用完整余额填充输入。

lib/jupiter.ts

/**
 * 使用 Jupiter Ultra API 通过 API 路由获取 token 余额
 * 返回钱包的 SOL 和 SPL token 余额
 * @param walletAddress - 要获取余额的钱包地址
 * @param signal - 可选的 AbortSignal 以取消请求
 */
export async function fetchTokenBalances(
  walletAddress: string,
  signal?: AbortSignal
): Promise<TokenBalance[]> {
  if (!walletAddress) {
    return [];
  }

  try {
    const response = await fetch(`/api/balances?walletAddress=${encodeURIComponent(walletAddress)}`, {
      cache: "no-store",
      signal,
    });

    if (!response.ok) {
      return [];
    }

    const balances: TokenBalance[] = await response.json();
    return balances;
  } catch (error) {
    // 如果请求被中止,则不要抛出错误
    if (error instanceof Error && error.name === "AbortError") {
      throw error;
    }
    console.error("Error fetching token balances:", error);
    return [];
  }
}

hooks/useTokenBalances.ts

"use client";

import { useState, useEffect, useCallback, useRef } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { fetchTokenBalances } from "@/lib/jupiter";
import type { TokenBalance } from "@/lib/types";

export function useTokenBalances() {
  const { publicKey } = useWallet();
  const [balances, setBalances] = useState<TokenBalance[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  // 跟踪当前 publicKey 以防止 refreshBalances 中出现陈旧的更新
  const publicKeyRef = useRef<string | null>(null);

  useEffect(() => {
    // 更新 ref 以跟踪当前 publicKey
    publicKeyRef.current = publicKey?.toBase58() || null;

    // 当 publicKey 更改时重置余额
    setBalances([]);
    setError(null);

    if (!publicKey) {
      setLoading(false);
      return;
    }

    // 创建 AbortController 以取消正在进行的请求
    const abortController = new AbortController();
    let isCancelled = false;
    const currentPublicKey = publicKey.toBase58();

    setLoading(true);

    const fetchBalances = async () => {
      try {
        const tokenBalances = await fetchTokenBalances(
          currentPublicKey,
          abortController.signal
        );

        // 在更新状态之前检查请求是否已取消或 publicKey 是否已更改
        if (isCancelled || publicKeyRef.current !== currentPublicKey) {
          return;
        }

        setBalances(tokenBalances);
        setError(null);
      } catch (err) {
        // 如果请求被中止,则不要更新状态
        if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
          return;
        }

        // 在设置错误之前再次检查是否已取消或 publicKey 是否已更改
        if (isCancelled || publicKeyRef.current !== currentPublicKey) {
          return;
        }

        setError(err instanceof Error ? err.message : "Failed to fetch balances");
        console.error("Error fetching balances:", err);
      } finally {
        // 仅当未取消且 publicKey 未更改时才更新加载状态
        if (!isCancelled && publicKeyRef.current === currentPublicKey) {
          setLoading(false);
        }
      }
    };

    fetchBalances();

    // 清理:当 publicKey 更改时中止正在进行的请求
    return () => {
      isCancelled = true;
      abortController.abort();
    };
  }, [publicKey]); // 仅当 publicKey 更改时才重新创建

  const refreshBalances = useCallback(async () => {
    if (!publicKey) {
      setBalances([]);
      return;
    }

    const currentPublicKey = publicKey.toBase58();
    const abortController = new AbortController();
    let isCancelled = false;

    setLoading(true);
    setError(null);
    try {
      const tokenBalances = await fetchTokenBalances(
        currentPublicKey,
        abortController.signal
      );

      // 在更新状态之前检查请求是否已取消或 publicKey 是否已更改
      if (isCancelled || publicKeyRef.current !== currentPublicKey) {
        return;
      }

      setBalances(tokenBalances);
      setError(null);
    } catch (err) {
      // 如果请求被中止,则不要更新状态
      if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
        return;
      }

      // 在设置错误之前再次检查是否已取消或 publicKey 是否已更改
      if (isCancelled || publicKeyRef.current !== currentPublicKey) {
        return;
      }

      setError(err instanceof Error ? err.message : "Failed to fetch balances");
      console.error("Error fetching balances:", err);
    } finally {
      // 仅当未取消且 publicKey 未更改时才更新加载状态
      if (!isCancelled && publicKeyRef.current === currentPublicKey) {
        setLoading(false);
      }
    }
  }, [publicKey]);

  const getBalance = useCallback((mint: string): number => {
    const balance = balances.find((b) => b.mint === mint);
    return balance ? balance.balance / Math.pow(10, balance.decimals) : 0;
  }, [balances]); // 仅当余额更改时才重新创建

  return {
    balances,
    loading,
    error,
    refreshBalances,
    getBalance,
  };
}

加载钱包余额后,你接下来需要获取经过验证的 token 列表以填充 swap token 选择器。

获取 Token 列表(已验证)

该应用程序使用 Jupiter 的 GET /tokens/v2/tag?query=verified 来获取经过验证的 token。 token 列表填充两个 token 选择器下拉菜单(从和到),使用户可以选择 swap token。 这确保了用户只能看到合法的 token,并且可以选择任何经过验证的 token 作为 swap 目的地。

lib/jupiter.ts

/**
 * 通过 API 路由从 Jupiter Token API v2 获取 token 列表
 * 使用 verified 标签端点获取所有经过验证的 token
 * 将 v2 API 响应格式映射到我们的 Token 接口
 */
export async function fetchTokenList(): Promise<Token[]> {
  try {
    const response = await fetch("/api/tokens", {
      cache: "no-store",
    });

    if (!response.ok) {
      // 尝试从响应中解析错误消息
      let errorMessage = `Failed to fetch tokens: ${response.statusText}`;
      try {
        const errorData = await response.json();
        if (errorData.error) {
          errorMessage = errorData.error;
        }
      } catch {
        // 如果响应不是 JSON,则使用默认消息
      }
      throw new Error(errorMessage);
    }

    const tokens: Token[] = await response.json();
    console.log(`Successfully loaded ${tokens.length} verified tokens`);
    return tokens;
  } catch (error) {
    console.error("Error fetching token list:", error);
    // 重新抛出错误,以便 hook 可以处理它
    throw error;
  }
}

hooks/useTokenList.ts

"use client";

import { useState, useEffect } from "react";
import { fetchTokenList } from "@/lib/jupiter";
import type { Token } from "@/lib/types";

const COMMON_TOKENS: Token[] = [
  {
    address: "So11111111111111111111111111111111111111112",
    symbol: "SOL",
    name: "Solana",
    decimals: 9,
  },
  {
    address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    symbol: "USDC",
    name: "USD Coin",
    decimals: 6,
  },
  {
    address: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
    symbol: "USDT",
    name: "Tether USD",
    decimals: 6,
  },
];

export function useTokenList() {
  const [tokens, setTokens] = useState<Token[]>(COMMON_TOKENS);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadTokens = async () => {
      setLoading(true);
      setError(null);
      try {
        const tokenList = await fetchTokenList();
        // 将常用 token 与获取的列表合并,优先考虑常用 token
        const commonAddresses = new Set(COMMON_TOKENS.map((t) => t.address));
        const otherTokens = tokenList.filter((t) => !commonAddresses.has(t.address));
        setTokens([...COMMON_TOKENS, ...otherTokens]);
      } catch (err) {
        console.error("Error loading token list:", err);
        setError(err instanceof Error ? err.message : "Failed to load tokens");
        // 回退到仅常用 token
        setTokens(COMMON_TOKENS);
      } finally {
        setLoading(false);
      }
    };

    loadTokens();
  }, []);

  const searchTokens = (query: string): Token[] => {
    if (!query) return tokens.slice(0, 20); // 返回前 20 个 token
    const lowerQuery = query.toLowerCase();
    return tokens.filter(
      (token) =>
        token.symbol.toLowerCase().includes(lowerQuery) ||
        token.name.toLowerCase().includes(lowerQuery) ||
        token.address.toLowerCase().includes(lowerQuery)
    );
  };

  return {
    tokens,
    loading,
    error,
    searchTokens,
  };
}

在用户选择 token 并输入金额后,下一步是获取报价以在执行 swap 之前向他们展示他们将收到的内容。

获取 Swap 报价

报价步骤在提交 swap 之前显示预期的输出和路由详细信息。 报价检索是在一个 hook 中实现的,并更新一个报价状态对象以供 UI 渲染。

lib/jupiter.ts

export interface JupiterQuoteResponse {
  inputMint: string;
  outputMint: string;
  inAmount: string;
  outAmount: string;
  otherAmountThreshold: string;
  swapMode: string;
  slippageBps: number;
  priceImpactPct: string;
  routePlan: any[];
  _ultraTransaction?: string; // Ultra API: base64 编码的交易
  _ultraRequestId?: string; // Ultra API: 执行端点的请求 ID
}

export interface JupiterSwapResponse {
  swapTransaction: string; // base64 编码的交易
  lastValidBlockHeight: number;
  priorityFeeLamports: string;
  _ultraOrder?: boolean; // Ultra API 的标志
  _ultraRequestId?: string; // Ultra 执行端点的请求 ID
}

/**
 * 通过 API 路由从 Jupiter Ultra API 获取 swap 报价
 * @param inputMint - 从中交换的 token 的地址
 * @param outputMint - 交换到的 token 的地址
 * @param amount - 以最小单位表示的金额(例如,SOL 的 lamport)
 * @param slippageBps - 以基点表示的滑点容差(50 = 0.5%)
 * @param taker - 可选的钱包地址(Ultra API 需要生成交易)
 * @param signal - 可选的 AbortSignal 以取消请求
 */
export async function getSwapQuote(
  inputMint: string,
  outputMint: string,
  amount: number,
  slippageBps: number = 50,
  taker?: string,
  signal?: AbortSignal
): Promise<JupiterQuoteResponse> {
  // 构建查询参数
  const params = new URLSearchParams({
    inputMint,
    outputMint,
    amount: Math.floor(amount).toString(),
    slippageBps: slippageBps.toString(),
  });

  if (taker) {
    params.append("taker", taker);
  }

  const response = await fetch(`/api/quote?${params.toString()}`, {
    signal,
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.error || `Request failed: ${response.statusText}`);
  }

  const quote = await response.json();
  console.log("Jupiter Ultra API Response:", quote);

  return quote as JupiterQuoteResponse;
}

hooks/useQuote.ts

export function useQuote(
  fromToken: Token | null,
  toToken: Token | null,
  amount: string
) {
  const { publicKey } = useWallet();
  const [quoteInfo, setQuoteInfo] = useState<QuoteInfo>(createEmptyQuoteInfo());

  useEffect(() => {
    // 当依赖项更改时重置报价信息
    setQuoteInfo(createEmptyQuoteInfo());

    // 如果缺少必需的值,则不要获取
    if (
      !fromToken ||
      !toToken ||
      !amount ||
      !publicKey ||
      fromToken.address === toToken.address
    ) {
      return;
    }

    const amountNum = parseFloat(amount);
    if (amountNum <= 0 || isNaN(amountNum)) {
      return;
    }

    // 设置加载状态
    setQuoteInfo(createEmptyQuoteInfo(true));

    // 创建 AbortController 以取消正在进行的请求
    const abortController = new AbortController();
    let isCancelled = false;

    // 防抖:等待 500 毫秒后再获取以避免过多的 API 调用
    const timeoutId = setTimeout(async () => {
      // 在开始获取之前检查是否已取消
      if (isCancelled) {
        return;
      }

      try {
        // 将金额转换为最小单位(例如,SOL 的 lamport)
        const amountInSmallestUnit = Math.floor(
          amountNum * Math.pow(10, fromToken.decimals)
        );

        // 使用中止信号从 Jupiter Ultra API 获取报价
        const quote = await getSwapQuote(
          fromToken.address,
          toToken.address,
          amountInSmallestUnit,
          50, // 0.5% 滑点容差
          publicKey.toBase58(),
          abortController.signal
        );

        // 在更新状态之前检查请求是否已取消
        if (isCancelled) {
          return;
        }

        // 将输出金额转换回可读格式
        // 使用 BigInt 在解析大量金额时保留精度(例如,具有 18 位小数的 token)
        const outAmountBigInt = BigInt(quote.outAmount);
        const decimalsBigInt = BigInt(10 ** toToken.decimals);
        // 对于显示,乘以 10^6 以保留 6 位小数,然后除以
        // 这比先将两者转换为 Number 更好地保留了精度
        const displayPrecision = BigInt(1000000); // 10^6 用于 6 位小数
        const scaledAmount = (outAmountBigInt * displayPrecision) / decimalsBigInt;
        const outAmountNative = (Number(scaledAmount) / Number(displayPrecision)).toFixed(6);

        // 计算汇率
        const exchangeRate =
          amountNum > 0
            ? (parseFloat(outAmountNative) / amountNum).toFixed(6)
            : "0";

        // 提取路由标签
        const routeLabels = extractRouteLabels(quote.routePlan || []);

        // 使用结果更新报价信息
        setQuoteInfo({
          outAmount: outAmountNative,
          priceImpactPct: quote.priceImpactPct || "0",
          slippageBps: quote.slippageBps || 50,
          exchangeRate,
          routeCount: routeLabels.length,
          routeLabels,
          loading: false,
          error: null,
        });
      } catch (err) {
        // 如果请求被中止,则不要更新状态
        if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
          return;
        }

        // 在设置错误之前再次检查是否已取消
        if (isCancelled) {
          return;
        }

        setQuoteInfo({
          ...createEmptyQuoteInfo(),
          error: err instanceof Error ? err.message : "Failed to fetch quote",
        });
      }
    }, 500);

    // 清理:取消超时并中止正在进行的请求
    return () => {
      isCancelled = true;
      clearTimeout(timeoutId);
      abortController.abort();
    };
  }, [fromToken, toToken, amount, publicKey]);

  return quoteInfo;
}

签名 Swap 交易

然后,当用户单击 Swap 时,应用程序会请求一个 Ultra 订单,接收未签名的交易 payload,提示连接的钱包签名,然后将其序列化回适合提交的 base64 payload。

lib/jupiter.ts

export interface JupiterUltraOrderResponse {
  inAmount: string;
  outAmount: string;
  priceImpactPct?: string; // 在某些响应中可能称为 priceImpact
  priceImpact?: string; // 备用字段名称
  transaction: string; // base64 编码的交易(如果没有 taker 或资金不足,则为空)
  requestId: string; // 执行端点的请求 ID
  swapMode: string;
  slippageBps: number;
  routePlan?: any[];
  error?: string; // 如果有任何错误消息
  errorCode?: number; // 如果有任何错误代码
}

/**
 * 从订单中获取 swap 交易
 * 对于 Ultra API,交易已包含在订单响应中
 */
export async function getSwapTransaction(
  quote: JupiterQuoteResponse,
  userPublicKey: string
): Promise<JupiterSwapResponse> {
  if (!(quote as any)._ultraTransaction) {
    throw new Error("Transaction not found in quote");
  }

  return {
    swapTransaction: (quote as any)._ultraTransaction,
    lastValidBlockHeight: 0,
    priorityFeeLamports: "0",
    _ultraOrder: true,
    _ultraRequestId: (quote as any)._ultraRequestId,
  } as any;
}

一旦用户批准了报价并单击 Swap,应用程序就会从订单中检索交易并提示钱包签名。

执行 Swap 交易

执行是通过将签名的交易连同订单响应中的请求标识符一起发送回 Jupiter Ultra 来处理的。 当执行成功时,应用程序会捕获返回的签名并进入成功状态,以便 UI 可以显示结果。

lib/jupiter.ts

export interface JupiterUltraExecuteResponse {
  signature: string;
  status: string;
}

//**
 * 通过 API 路由执行 Ultra swap 订单
 * 将签名的交易发送到 Jupiter Ultra API 以供执行
 */
export async function executeUltraSwap(
  signedTransaction: string,
  requestId?: string
): Promise<JupiterUltraExecuteResponse> {
  const response = await fetch("/api/execute", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      signedTransaction,
      requestId,
    }),
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(errorData.error || `Request failed: ${response.statusText}`);
  }

  return await response.json();
}

hooks/useSwap.ts

"use client";

import { useState, useRef } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import {
  getSwapQuote,
  getSwapTransaction,
  executeUltraSwap,
} from "@/lib/jupiter";
import type { Token, SwapStatus } from "@/lib/types";

/**
 * 用于管理 swap 执行流程的 Hook
 * 处理:报价 → 交易 → 签名 → 执行
 */
export function useSwap() {
  const { publicKey, signTransaction } = useWallet();
  const [status, setStatus] = useState<SwapStatus>("idle");
  const [error, setError] = useState<string | null>(null);
  const [txSignature, setTxSignature] = useState<string | null>(null);
  const [estimatedOutput, setEstimatedOutput] = useState<string | null>(null);

  // 同步防护装置以防止并发 swap 执行
  // 此 ref 会立即(同步地)更新,与批量处理的状态不同
  const isExecutingRef = useRef(false);

  const executeSwap = async (
    fromToken: Token,
    toToken: Token,
    amount: number
  ) => {
    if (!publicKey || !signTransaction) {
      throw new Error("Wallet not connected");
    }

    // 同步防护装置:防止并发执行
    // 此检查会立即发生,在任何异步操作或状态更新之前
    if (isExecutingRef.current) {
      throw new Error("Swap already in progress");
    }

    // 同步设置防护装置以防止竞争条件
    isExecutingRef.current = true;

    setStatus("quoting");
    setError(null);
    setTxSignature(null);

    try {
      // 步骤 1:将金额转换为最小单位(例如,SOL 的 lamport)
      const amountInSmallestUnit = Math.floor(
        amount * Math.pow(10, fromToken.decimals)
      );

      // 步骤 2:从 Jupiter Ultra API 获取 swap 报价
      const quote = await getSwapQuote(
        fromToken.address,
        toToken.address,
        amountInSmallestUnit,
        50, // 0.5% 滑点容差
        publicKey.toBase58()
      );

      // 存储估计的输出以供显示
      // 使用 BigInt 在解析大量金额时保留精度(例如,具有 18 位小数的 token)
      const outAmountBigInt = BigInt(quote.outAmount);
      const decimalsBigInt = BigInt(10 ** toToken.decimals);
      // 对于显示,乘以 10^6 以保留 6 位小数,然后除以
      // 这比先将两者转换为 Number 更好地保留了精度
      const displayPrecision = BigInt(1000000); // 10^6 用于 6 位小数
      const scaledAmount = (outAmountBigInt * displayPrecision) / decimalsBigInt;
      setEstimatedOutput(
        (Number(scaledAmount) / Number(displayPrecision)).toFixed(6)
      );
      // 步骤 3:从报价中获取交易(Ultra API 将其包含在报价中)
      setStatus("signing");
      const swapResponse = await getSwapTransaction(quote, publicKey.toBase58());

      // 步骤 4:反序列化并签署交易
      // 注意:这是唯一使用 @solana/web3.js v1 的地方。 需要,因为:
      // - @solana/wallet-adapter-react 期望 v1 VersionedTransaction 用于 signTransaction()
      // - Jupiter Ultra API 以 v1 格式返回交易
      // - @solana/kit (v2) 使用与钱包适配器不兼容的不同交易模型
      const { VersionedTransaction } = await import("@solana/web3.js");
      const transaction = VersionedTransaction.deserialize(
        Buffer.from(swapResponse.swapTransaction, "base64")
      );

      const signedTransaction = await signTransaction(transaction);

      // 步骤 5:通过 Jupiter Ultra API 执行签名的交易
      setStatus("executing");
      const signedBuffer = signedTransaction.serialize();
      const executeResponse = await executeUltraSwap(
        Buffer.from(signedBuffer).toString("base64"),
        (swapResponse as any)._ultraRequestId
      );

      // 成功!
      setTxSignature(executeResponse.signature);
      setStatus("success");
    } catch (err) {
      setStatus("error");
      setError(err instanceof Error ? err.message : "Swap failed");
      throw err;
    } finally {
      // 始终重置防护装置,即使发生错误也是如此
      isExecutingRef.current = false;
    }
  };

  const reset = () => {
    setStatus("idle");
    setError(null);
    setTxSignature(null);
    setEstimatedOutput(null);
    // 在手动重置时重置执行防护装置
    isExecutingRef.current = false;
  };

  return {
    executeSwap,
    status,
    error,
    txSignature,
    estimatedOutput,
    reset,
  };
}

在交易签名和执行后,swap 完成,UI 显示交易签名,允许用户验证他们在链上的 swap。 这完成了完整的 Ultra swap 生命周期:报价 → 订单 → 签名 → 执行。

Ultra 与 Metis

Ultra 是 Jupiter 集成 swap 的较新方式。 当你想要最简洁、最快的应用程序集成,并且你很乐意将更多 swap 执行复杂性委派给 Jupiter 时,请选择 Ultra。

Metis 是原始的、较低级别的路由引擎,可以更精细地控制 swap 构建和执行。 当你想要最大程度地控制组装原始 swap 指令、添加自定义指令/CPI 或拥有自己的交易发送和确认策略时,请选择 Metis API。

截至 2025 年底,Jupiter 已将 Metis 过渡到独立的公共产品,同时继续尽最大努力提供支持和维护。

总结

你的 Swap UI 已准备好将 token 发现、路由、滑点处理和交易管理的复杂性处理为由 Jupiter Ultra 驱动的精简 swap 体验,而 报价 → 订单 → 签名 → 执行 的清晰生命周期为你提供了坚实的基础来扩展。

常见问题

什么是 Jupiter Ultra API?

Jupiter Ultra API 是 Solana 上的 DEX 聚合器,可为 token swap 提供跨多个流动性来源的最佳 swap 路由。

什么是 Jupiter Ultra Swap,它是如何工作的?

Jupiter Ultra Swap 是 Jupiter 的简化 API,用于将 token swap 集成到 Solana dApp 中。 Ultra 提供了一个简化的生命周期,你可以在其中请求报价以预览 swap 详细信息,并接收准备好签名的预构建的交易 payload,而不是手动构建 swap 交易,从而更容易构建在真实条件下工作的可靠 swap UI。

Jupiter Ultra 和 Metis API 有什么区别?

Jupiter Ultra 是较新的、更高级别的集成选项,专为想要最快、最简洁的应用程序集成并且可以舒适地将 swap 执行复杂性委派给 Jupiter 的开发人员而设计。 Metis 是原始的、较低级别的路由引擎,可提供对 swap 构建、自定义指令、CPI 调用和交易发送策略的精细控制。

Jupiter Ultra Swap 是否在 Solana testnet 上可用,还是仅在 mainnet 上可用?

Jupiter Ultra Swap 仅在 Solana mainnet-beta 上可用。 这意味着使用 Ultra 执行的任何 swap 都将交易真实的 token 并产生真实的网络费用。 开发人员应该意识到,遵循本指南并执行 swap 将涉及实际的金融交易,并且可能因价格波动、滑点或选择不正确的 token 对而导致价值损失。 我需要什么来将 Jupiter Ultra Swap 集成到我的 Solana dApp 中?

要集成 Jupiter Ultra Swap,你需要一个 Jupiter API 密钥、一个 Quicknode Solana RPC 端点、对 TypeScript dApp 开发的基本理解,以及少量 mainnet SOL 和代币用于测试。

什么是已验证的代币,Jupiter Ultra 如何使用它们?

已验证的代币是指已经通过 Jupiter 的代币验证系统验证的代币,以确保它们满足一定的合法性和安全性标准。Jupiter Ultra 使用 Jupiter 的 Tokens API v2 按状态进行过滤,并仅获取已验证的代币用于你的 swap UI,从而保护用户免受诈骗、假代币和恶意合约的侵害。

资源

我们 ❤️ 反馈!

如果你有任何反馈或对新主题的请求,请告诉我们。我们很乐意听到你的声音。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。