在Solana上使用Titan元聚合API兑换代币
本教程指导你使用Titan Meta-Aggregation API(通过Quicknode提供)构建Solana兑换界面。
概述
在 Solana 上获取最佳兑换价格意味着要比较分散流动性的路径,而每个聚合器只会从其内部的黑盒路由器返回单一路径。自行操作则需要集成多个聚合器并在每次兑换时比较它们的报价。
Titan 元聚合 API 通过一次请求即可完成上述工作。Titan 是一个元聚合器:它会一次性将一笔兑换请求分发到多个流动性提供商,针对链上实时状态模拟每条路径,并返回所有竞争报价以及预期的胜出者。该 API 可通过你的 Quicknode Solana 端点使用,因此无需单独维护集成。
完成本指南后,你将拥有一个由 Titan 通过 Quicknode 驱动的 Solana 兑换交互界面,用户可以选择代币、观察各提供商竞争最佳路径,并端到端地执行一笔兑换。
TLDR
- 使用通过 Quicknode 端点提供的 Titan 元聚合 API 构建一个 Solana 兑换交互界面
- 通过
/quote/price获取轻量级指示性价格用于实时显示,然后通过/quote/swap运行完整的提供商竞争 - 使用
metadata.ExpectedWinner选择胜出路径,并显示每个竞争提供商的报价 - 根据 Titan 提供的可组合指令和地址查找表自行构建
VersionedTransaction - 使用已连接的钱包签名,并通过 Quicknode RPC 提交交易
你将完成的任务
- 克隆并运行配套示例应用
- 为未连接钱包的用户获取指示性价格,为已连接用户获取完整的提供商竞争报价
- 根据 Titan 的指令和地址查找表构建
VersionedTransaction - 通过 Quicknode RPC 签名、发送并确认兑换交易
你需要具备的条件
本指南假设你具备构建 TypeScript dApp、DeFi 以及使用 Solana 钱包的基本知识。
如需复习,请参考:
你还需要:
- 少量
mainnet-beta的 SOL 用于支付费用 - 主网上少量用于兑换的代币(建议使用 USDC)
- 一个 Quicknode Solana 主网端点。通过注册 Quicknode 获取你的端点
- 在你的端点上启用 Titan 元聚合 API
危险
Titan 元聚合 API 仅在 Solana 主网测试版(mainnet-beta)上可用。这意味着如果你按照本指南操作并执行兑换,你将交易真实代币并支付真实网络费用,并且可能因价格变动、滑点或选择错误的代币对而损失价值。
Titan 元聚合 API 是如何工作的?
Titan 元聚合 API 是一个 Solana 兑换 API,它会一次性将一笔请求分发到多个流动性提供商,包括 Titan 自己的路由器、第三方 DEX 聚合器以及询价(RFQ)场所,然后针对链上实时状态模拟每条返回的路径,并返回所有竞争报价以及预期胜出的路径。
它与普通聚合器的实际区别在于返回的内容。单引擎聚合器为一个代币对返回一条路径;Titan 则返回每个提供商的独立路径,因此你的应用可以看到完整的竞争情况并进行显示或选择,而不是一个黑盒答案。
有三个特性使得集成变得实用:
- 竞争提供商。 一次
/quote/swap调用会返回一个以提供商为键的quotes映射。每个条目都是该提供商的一条完整、可执行的路径。metadata.ExpectedWinner字段命名了 Titan 预期提供最佳执行的提供商,因此你可以自动选择一个胜出者,或向用户展示完整的竞争情况。 - 模拟验证的路径。 在返回路径之前,Titan 会针对最新的链上状态对其进行模拟。你显示的输出金额与实际执行结果非常接近,这减少了因池数据过时而导致的交易失败。
- 可组合指令。 Titan 不会返回一个已密封、可直接发送的交易。每条路径都包含原始的兑换指令以及它们引用的地址查找表(ALT)。你需要自行组装、签名和发送交易,这为你将兑换包装到自己的逻辑中留下了空间。
以下是你的兑换交互界面在本指南中遵循的生命周期:
预览价格报价
在钱包连接之前,你只需要一个指示性汇率来在用户输入时显示。GET /api/v1/quote/price 端点非常轻量:它返回预期输出和价格影响,而无需构建任何指令或运行完整的提供商竞争。这是实时价格更新的正确选择。
运行提供商竞争
一旦钱包连接且用户准备行动,你可以使用他们的 userPublicKey 调用 GET /api/v1/quote/swap。这将运行完整的元聚合过程:每个活跃的提供商都会参与竞争,响应中包含每个提供商的可执行指令、ALT、路径步骤以及预期胜出者。示例应用按输出金额从高到低排序报价,并将预期胜出者保持在顶部。
发送交易
由于 Titan 返回的是指令而非完整的交易,你需要从 RPC 解析路径的 ALT,编译一个 v0 版本的 VersionedTransaction,让钱包签名,并通过 Quicknode RPC 提交。你完全拥有交易生命周期。
发现提供商和场所
Titan 还公开了描述聚合广度的元数据端点。GET /api/v1/providers 列出了当前竞争的提供商,GET /api/v1/venues 列出了可路由的链上场所以及其程序 ID)。这些信息在会话期间很少更改,并为示例交互界面中的“提供商竞争”和“涉及场所”显示提供支持。
运行示例应用
首先,克隆示例应用仓库并打开 solana/titan-swap 文件夹。
git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/solana/titan-swap
npm install
npm run dev
该应用从 .env.local 文件中读取两个仅服务端使用的环境变量,它们都来自同一个 Quicknode Solana 端点。QUICKNODE_RPC_URL 是你的端点 URL 本身,你可以从 Quicknode 仪表板 的“端点”页面复制。TITAN_GATEWAY_URL 是该 URL 后附加 /addon/1147/ 的结果(1147 是 Titan 元聚合 API 的插件 ID),一旦你在该端点上启用了该插件,它就会生效。
.env.local
## Quicknode Solana RPC 端点(仅服务端使用,绝不暴露给浏览器)。
QUICKNODE_RPC_URL=https://your-endpoint.solana-mainnet.quiknode.pro/YOUR_TOKEN/
## Titan 元聚合 API 基础 URL:上述 RPC 端点后附加
## /addon/1147/。代理会自动为你附加尾部的 /api/v1。
TITAN_GATEWAY_URL=https://your-endpoint.solana-mainnet.quiknode.pro/YOUR_TOKEN/addon/1147/
在本地运行后,快速浏览一下代码库以熟悉结构,然后返回本指南,逐步完成每个集成步骤。
示例兑换交互界面
这个示例应用是一个兑换交互界面,围绕价格预览、多提供商兑换竞争以及构建 → 签名 → 发送 → 确认流程构建。UI 组件和状态已经就位;以下各节将介绍与 Titan 通信并组装交易的模块。
在服务端调用 Titan API
每个 Titan 请求都通过一个仅服务端的模块发出,这样你的 API 凭据永远不会到达浏览器。该 API 将响应编码为 MessagePack 二进制格式而非 JSON,因此客户端请求 application/vnd.msgpack,在配置了 bearer token 时附加它,并在将干净数据返回之前解码响应。这个单一的辅助函数是应用中使用的每个 Titan 端点的入口点。
lib/titan-server.ts
import "server-only";
import { decode } from "@msgpack/msgpack";
// TITAN_GATEWAY_URL — 你的 Titan 元聚合 API 基础 URL(带或不带尾部的 /api/v1)
// TITAN_GATEWAY_AUTH — 可选的 bearer token(如果尚未包含在 URL 中)
const RAW_BASE = process.env.TITAN_GATEWAY_URL;
const AUTH = process.env.TITAN_GATEWAY_AUTH;
function baseUrl(): string {
if (!RAW_BASE) {
throw new Error("TITAN_GATEWAY_URL 未设置。请将你的 API URL 添加到 .env.local。");
}
let b = RAW_BASE.replace(/\/+$/, "");
if (!b.endsWith("/api/v1")) b = `${b}/api/v1`;
return b;
}
type QueryValue = string | number | boolean | undefined;
async function titanGet<T = unknown>(
path: string,
params?: Record<string, QueryValue>
): Promise<T> {
const url = new URL(`${baseUrl()}${path}`);
if (params) {
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== "") url.searchParams.set(k, String(v));
}
}
const headers: Record<string, string> = { Accept: "application/vnd.msgpack" };
if (AUTH) headers.Authorization = `Bearer ${AUTH}`;
const res = await fetch(url.toString(), { headers, cache: "no-store" });
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Titan ${path} 失败: ${res.status} ${text.slice(0, 300)}`);
}
const buf = new Uint8Array(await res.arrayBuffer());
return decode(buf) as T;
}
位于 /api/titan/* 下的薄层 Next.js 路由处理器、将 Solana JSON-RPC 转发到你的 Quicknode 端点的 /api/rpc 代理,以及客户端的 fetch 封装,都是标准的框架基础设施,此处省略。
加载代币列表
Titan 元聚合 API 是一个交换聚合器,而不是代币目录,因此它不附带代币列表。示例应用仅用于显示和获取小数位,从公共注册表中提取经过验证的代币元数据(符号、名称、小数位、图标),并以常见代币(SOL、USDC、USDT)的简短列表作为后备。任何路由或定价都不来源于此列表;一切通过 Titan 完成。由于这不涉及 Titan API,代币列表加载器作为标准应用代码保留在示例仓库中。
获取钱包余额
当钱包连接时,应用通过 Quicknode RPC 获取其 SOL 和 SPL 代币余额。这些余额用于填充“来源”选择器(持有最多的排在前),验证用户是否有足够的代币进行兑换,并驱动“最大”按钮。这是普通的 RPC 基础设施,不是 Titan 调用,因此余额 Hook 作为标准应用代码保留在示例仓库中。
获取价格报价
当没有连接钱包时,应用调用 /quote/price 获取上述指示性价格,并在用户输入时进行防抖处理,以使实时显示保持响应,同时不耗尽速率限制。
lib/titan-server.ts
export async function fetchPrice(params: {
inputMint: string;
outputMint: string;
amount: string;
slippageBps?: number;
}): Promise<TitanPriceResponse> {
const raw = await titanGet<any>("/quote/price", {
inputMint: params.inputMint,
outputMint: params.outputMint,
amount: params.amount,
slippageBps: params.slippageBps,
});
return {
inputAmount: String(raw?.inputAmount ?? params.amount),
outputAmount: String(raw?.outputAmount ?? raw?.outAmount ?? "0"),
priceImpact: raw?.priceImpact != null ? Number(raw.priceImpact) : undefined,
};
}
获取交换报价
当钱包连接时,应用使用用户的公钥和 simulate 标志调用 /quote/swap。这就是元聚合发生的地方:响应包含一个以提供商为键的 quotes 映射,以及一个 metadata.ExpectedWinner 字段。应用向每个提供商请求报价(网关限制为 10 个),读取胜出者,标准化每条路径,丢弃未能生成可用路径的提供商,并按输出金额从高到低排序,同时将预期胜出者保持在顶部。
lib/titan-server.ts
export async function fetchSwap(params: {
inputMint: string;
outputMint: string;
amount: string;
userPublicKey: string;
slippageBps?: number;
simulate?: boolean;
}): Promise<TitanSwapResponse> {
const raw = await titanGet<any>("/quote/swap", {
inputMint: params.inputMint,
outputMint: params.outputMint,
amount: params.amount,
userPublicKey: params.userPublicKey,
slippageBps: params.slippageBps,
simulate: params.simulate,
// 要求每个提供商报价,以填充竞争(服务器最大值为 10)。
numQuotes: 10,
});
const quotesMap = raw?.quotes ?? {};
const expectedWinner: string | null = raw?.metadata?.ExpectedWinner ?? null;
const quotes: TitanSwapRoute[] = Object.entries(quotesMap)
.map(([provider, route]) => normRoute(provider, route))
// 丢弃未能生成可用路径的提供商。
.filter((q) => Number(q.outAmount) > 0);
// 按最佳输出优先排序;如果存在预期胜出者,则将其保持在顶部。
quotes.sort((a, b) => {
if (expectedWinner) {
if (a.provider === expectedWinner) return -1;
if (b.provider === expectedWinner) return 1;
}
return Number(b.outAmount) - Number(a.outAmount);
});
return { quotes, expectedWinner };
}
simulate 参数映射到 UI 的“精确”与“快速”切换。针对链上实时状态模拟每条路径更精确,但会增加延迟;关闭它将返回更快、但验证程度较低的报价。然后 UI 并排显示竞争报价,显示每个报价落后领先者的基点数量,标记预期胜出者,并将胜出路径传递给交换步骤。
响应中的每条路径以 MessagePack 格式到达,使用紧凑键名,公钥以原始字节缓冲区形式出现,因此需要通过标准化过程将所有内容转换为交易构建器期望的 base58/base64 格式。
lib/titan-server.ts
function toBase58(v: unknown): string {
if (typeof v === "string") return v;
if (v instanceof Uint8Array) return bs58.encode(v);
if (Array.isArray(v)) return bs58.encode(Uint8Array.from(v as number[]));
return String(v ?? "");
}
function toBase64(v: unknown): string {
if (typeof v === "string") return v;
if (v instanceof Uint8Array) return Buffer.from(v).toString("base64");
if (Array.isArray(v)) return Buffer.from(Uint8Array.from(v as number[])).toString("base64");
return "";
}
function normStep(s: any) {
return {
label: String(s.label ?? s.amm ?? "Unknown"),
ammKey: toBase58(s.ammKey),
inputMint: toBase58(s.inputMint),
outputMint: toBase58(s.outputMint),
inAmount: String(s.inAmount ?? ""),
outAmount: String(s.outAmount ?? ""),
// allocPpb 是十亿分之一;转换为百分比。
allocPct: s.allocPpb != null ? Number(s.allocPpb) / 1e7 : 0,
};
}
// API 使用紧凑键名(p/a/d, p/s/w),但我们也读取长格式,因此上游的模式调整不会静默破坏构建器。
function normInstruction(ix: any): TitanInstruction {
return {
programId: toBase58(ix.p ?? ix.programId),
accounts: (ix.a ?? ix.accounts ?? []).map((a: any) => ({
pubkey: toBase58(a.p ?? a.pubkey),
isSigner: Boolean(a.s ?? a.isSigner),
isWritable: Boolean(a.w ?? a.isWritable),
})),
data: toBase64(ix.d ?? ix.data),
};
}
function normRoute(provider: string, r: any): TitanSwapRoute {
return {
provider,
inputAmount: String(r.inputAmount ?? r.inAmount ?? ""),
outAmount: String(r.outAmount ?? r.outputAmount ?? ""),
slippageBps: Number(r.slippageBps ?? 0),
priceImpact: r.priceImpact != null ? Number(r.priceImpact) : undefined,
computeUnitsSafe:
r.computeUnitsSafe != null ? Number(r.computeUnitsSafe) : undefined,
steps: (r.steps ?? []).map(normStep),
instructions: (r.instructions ?? []).map(normInstruction),
addressLookupTables: (r.addressLookupTables ?? []).map(toBase58),
expiresAtMs: r.expiresAtMs != null ? Number(r.expiresAtMs) : undefined,
expiresAfterSlot:
r.expiresAfterSlot != null ? Number(r.expiresAfterSlot) : undefined,
};
}
构建 VersionedTransaction
这是与“为你执行”式聚合器的主要集成区别。Titan 返回指令以及它们引用的 ALT,因此你需要自行构建交易。构建器从 Quicknode RPC 解析每个 ALT,根据标准化的 base58 程序 ID 和 base64 数据重新构建每条指令,获取最新的区块哈希,并编译一个包含查找表的 v0 消息。此步骤是标准的 Solana 交易组装,而非 Titan API 调用,因此完整的构建器(lib/build-swap-tx.ts,使用 @solana/kit 构建)保留在示例仓库中供你阅读。
注意
addressLookupTables 中的地址查找表必须从 RPC 获取并在编译消息时传递。跳过此步骤会导致交易编译失败,因为路径指令中的压缩账户引用无法在没有其查找表的情况下解析。
签名并发送交易
交易构建完成后,交换 Hook 提示已连接的钱包对其签名,通过 Quicknode RPC 提交原始交易,并确认它。由于 RPC 代理仅承载 HTTP JSON-RPC(无 WebSocket),应用通过轮询 getSignatureStatuses 来确认,直到交易被确认、出错或其区块哈希过期。状态流经 building → signing → sending → confirming → success,UI 显示签名并附带一个链接,用于在链上验证兑换。
hooks/useSwap.ts
const executeSwap = async (route: TitanSwapRoute, toToken: Token) => {
if (!signer) throw new Error("钱包未连接");
const rpc = createRpc();
// 根据 Titan 的指令 + 查找表构建交易。
setStatus("building");
const { message, lastValidBlockHeight } = await buildSwapTransaction(
route,
signer,
rpc
);
// 通过附加到消息上的 Kit 签名者在钱包中签名。
setStatus("signing");
const signedTransaction = await signTransactionMessageWithSigners(message);
// 通过 Quicknode RPC 提交。
setStatus("sending");
const signature = getSignatureFromTransaction(signedTransaction);
await rpc
.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), {
encoding: "base64",
skipPreflight: false,
preflightCommitment: "confirmed",
})
.send();
setTxSignature(signature);
// 通过 HTTP 轮询确认(RPC 代理不承载 WebSocket)。
setStatus("confirming");
await confirmBySignature(rpc, signature, lastValidBlockHeight);
setStatus("success");
};
签名、发送和确认是标准的 Solana 交易基础设施,而非 Titan 调用,因此相关的 Hook 状态和 confirmBySignature 轮询器作为标准应用代码保留在示例仓库中。
交易签名、发送并确认后,兑换完成。这完成了完整的 Titan 流程:价格预览 → 兑换竞争 → 构建 → 签名 → 发送。
总结
你已经构建了一个兑换交互界面,它由 Titan 元聚合 API 驱动,将代币发现、多提供商路由、滑点处理和交易管理整合为一个统一的体验。由于 Titan 返回的是竞争报价和可组合指令,而非密封的交易,你确切地看到了哪些提供商在竞标、哪条路径获胜,以及如何自行组装和发送交易。这种对交易生命周期的掌控为你提供了坚实的基础来进行扩展,无论是添加优先费用、自定义指令,还是你自己的着陆策略。
常见问题
什么是 Titan 元聚合 API?
Titan 元聚合 API 是一个 Solana 兑换 API,它在单次请求中从多个流动性提供商获取可执行的报价。它不是通过一个引擎路由,而是将请求分发到多个提供商,针对链上实时状态模拟每条路径,并返回所有竞争报价以及预期胜出的报价。
元聚合与常规 DEX 聚合器有何不同?
常规聚合器只返回其自身的最佳路径。元聚合器则同时查询多个提供商,包括其自身的路由器、其他聚合器以及 RFQ 场所,并返回所有路径,以便你的应用进行比较或选择预期胜出者。你获得的是完整的竞争情况,而不是一个黑盒答案。
为什么我必须自己构建交易?
Titan 返回的是可组合指令以及它们引用的地址查找表,而不是密封的交易。你需要从 RPC 解析查找表,编译 v0 版本的 VersionedTransaction,用已连接的钱包签名,然后发送。这让你完全控制将兑换包装到自定义逻辑中,但意味着你必须在编译之前加载查找表,否则交易将无法构建。
我应该何时使用价格端点与交换端点?
在用户输入时,使用 /quote/price 进行轻量级的实时显示,因为它返回预期输出,无需构建指令或运行完整的提供商竞争。仅在钱包连接且用户准备行动时调用 /quote/swap,因为它会运行完整的元聚合并返回可执行指令和地址查找表。
Titan 元聚合 API 在 Solana 测试网上可用吗?还是仅限主网?
它仅在 Solana 主网测试版(mainnet-beta)上可用。你执行的任何兑换都将交易真实代币并产生真实网络费用,可能因价格变动、滑点或选择错误的代币对而导致价值损失。
集成 Titan 元聚合 API 需要什么?
你需要一个启用了 Titan 元聚合 API 的 Quicknode Solana 主网端点、基本的 TypeScript 和 Solana dApp 经验,以及少量主网 SOL 和代币用于测试。Quicknode RPC Token 和 API 凭据都应保持在服务端,位于代理路由之后。
资源
我们 ❤️ 反馈!
如果你有任何反馈或新主题的请求,请告诉我们。我们很乐意听到你的声音。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~