阅读时间 13 分钟
概述
要在 Solana 上获得最佳兑换价格,就需要在碎片化的流动性环境中比较不同路径——每个聚合器都会通过其自身的“黑匣子”路由器返回一条路径。若要自行完成这一操作,就需要集成多个聚合器,并在每次兑换时比较它们的报价。
Titan 元聚合 API仅需一次请求即可完成这一操作。Titan 是一个元聚合器:它会将一笔交易同时分发给多个流动性提供商,根据实时链上状态模拟每条路径,并返回所有竞争报价以及预期胜出方案。您可通过 Quicknode Solana 端点使用该服务,因此无需维护单独的集成。
读完本指南后,您将拥有一个由 Titan 通过 Quicknode 驱动的、可正常运行的 Solana 兑换界面,用户可以在其中选择代币、观察提供商竞相提供最佳路径,并端到端地执行兑换操作。
- 使用通过 Quicknode 端点提供的 Titan 元聚合 API 构建一个 Solana 交易对交换界面
- 使用以下方式获取一个大致的参考价格:
/报价/价格若要实时显示,请运行完整的提供程序竞赛,并使用/quote/swap - 选择制胜路线,使用
metadata.ExpectedWinner并显示所有竞争对手的报价 - 构建一个
版本化事务通过Titan的可组合指令和地址查找表来实现 - 使用已连接的钱包进行签名,并通过 Quicknode RPC 提交
您将负责的工作内容
- 克隆并运行配套的示例应用
- 为未连接的用户获取参考价格,为已连接的用户获取所有服务商的竞标结果
- 构建一个
版本化事务来自Titan的说明和地址查找表 - 通过 Quicknode RPC 对交换进行签名、发送和确认
您需要准备的物品
本指南假设您已具备构建 TypeScript dApp 和 DeFi 应用以及使用 Solana 钱包的基础知识。
如果您需要复习一下,请参阅:
您还需要:
- 少量
主网测试版费用已结清 - 主网上有少量代币可供兑换(建议使用USDC)
- 一个 Quicknode Solana 主网端点。注册Quicknode即可获取您的端点
- 您的端点已启用Titan 元聚合 API
Titan Meta-Aggregation API 仅在 Solana 主网测试版上提供。这意味着,如果您按照本指南操作并执行代币兑换,您将交易真实的代币并支付真实的网络手续费,且可能因价格波动、滑点或选择错误的代币对而造成资产贬值。
Titan 元聚合 API 是如何工作的?
Titan Meta-Aggregation API 是一款 Solana 交易 API,它能将单次请求同时分发给多个流动性提供方,包括 Titan 自有的路由器、第三方 DEX 聚合平台以及询价(RFQ)平台,随后根据实时链上状态对返回的每条路径进行模拟,并返回所有竞争报价以及预计将胜出的报价。
它与普通聚合器的实际区别在于返回的结果。单引擎聚合器针对一对目的地仅返回一条路线;而Titan会返回各服务商的独立路线,因此您的应用能够看到完整的竞争情况以便显示或供用户选择,而不是一个“黑箱”式的答案。
以下三个特性使得该功能易于集成:
- 竞争对手。 一首
/quote/swap该调用返回一个引语按提供商作为键的映射。每个条目都是该提供商的一条完整且可执行的路线。该metadata.ExpectedWinner该字段指定了 Titan 预期能提供最佳执行效果的提供商,因此您可以自动选出优胜者,或向用户展示完整的竞争情况。 - 经模拟验证的路径。在返回路径之前,Titan 会根据最新的链上状态对其进行模拟。您所看到的输出金额与实际执行的结果高度吻合,这有助于减少因池数据过时而导致的交易失败。
- 可组合的指令。Titan 不会返回一个密封且可直接发送的交易。每条路由都包含原始的交换指令及其引用的地址查找表(ALTs)。您需要自行组装、签名并发送交易,这为您提供了将交换操作封装到自有逻辑中的空间。
以下是本指南中 swap UI 遵循的生命周期:
预览报价
在连接钱包之前,只需显示一个参考汇率,以便在用户输入时显示。该 GET /api/v1/quote/price 该端点非常轻量级:它无需构建任何指令或运行完整的提供商竞标流程,即可返回预期的输出结果和价格影响。对于实时价格更新而言,这是最合适的选择。
参加“服务商竞逐”活动
一旦钱包连接成功且用户准备好进行操作,你就调用 GET /api/v1/quote/swap 与他们的 userPublicKey. 这将执行完整的元聚合:所有活跃的提供商都会参与竞争,响应结果包含每个提供商的可执行指令、替代方案(ALTs)、路由步骤以及预期胜出者。示例应用会按“最佳输出优先”的顺序对报价进行排序,并将预期胜出者置于首位。
发送交易
由于 Titan 返回的是指令而非已完成的事务,因此需要从 RPC 中解析路由的 ALTs,并编译一个 v0 版本化事务,让钱包进行签名,然后通过 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 安装
npm 运行开发环境
该应用程序从以下位置读取两个仅限服务器的环境变量: .env.local,而且两者都来自同一个 Quicknode Solana 端点。 QUICKNODE_RPC_URL 即您的端点 URL 本身,您可以从 Quicknode 仪表盘. TITAN_GATEWAY_URL 是那个相同的URL,其中包含 /addon/1147/ 附上 (1147 (这是 Titan Meta-Aggregation API 的插件 ID),在您启用该端点的插件后,该 ID 即可使用。
# 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/
在本地成功运行后,请快速浏览一下代码库,熟悉其结构,然后回到本指南,详细了解每个集成步骤。
样本交换界面
该示例应用程序是一个围绕价格预览、多提供商互换竞标以及“构建 → 签名 → 发送 → 确认”流程构建的互换用户界面。用户界面组件和状态已就绪;下文将详细介绍与 Titan 进行交互并组装交易的各个模块。
调用 Titan API(服务器端)
每个 Titan 请求都是通过仅限服务器的模块发出的,因此您的 API 凭据绝不会传送到浏览器。该 API 将响应编码为 MessagePack 二进制格式,而非 JSON,因此客户端请求 application/vnd.msgpack, 在配置了承载令牌时将其附加,并在返回干净数据之前对响应进行解码。这个单一的辅助函数是应用程序中所有 Titan 端点的入口点。
import "server-only";
import { decode } from "@msgpack/msgpack";
// TITAN_GATEWAY_URL — your Titan Meta-Aggregation API base URL (with or without trailing /api/v1)
// TITAN_GATEWAY_AUTH — optional bearer token (if not already in the 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 is not set. Add your API URL to .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} failed: ${res.status} ${text.slice(0, 300)}`);
}
const buf = new Uint8Array(await res.arrayBuffer());
return decode(buf) as T;
}
位于 /api/titan/*,该 /api/rpc 该代理将 Solana JSON-RPC 转发至您的 Quicknode 端点,而客户端的请求封装器属于框架的标准基础组件,此处不予赘述。
加载令牌列表
Titan 元聚合 API 是一个交易聚合器,而非代币目录,因此它不提供代币列表。示例应用从公共注册表中提取经过验证的代币元数据(符号、名称、小数位数、徽标),仅用于显示和确定小数位数,并提供一份包含常见代币(SOL、USDC、USDT)的简短列表作为备用方案。 该列表不涉及任何路由或定价逻辑;所有相关操作均通过 Titan 处理。由于这些操作均不涉及 Titan API,因此示例代码库中的代币列表加载器被保留为标准应用程序代码。
获取钱包余额
当钱包连接后,应用会通过 Quicknode RPC 获取其 SOL 和 SPL 代币的余额。这些余额会填充到“From”(来源)下拉菜单中(按持仓量从高到低排序),验证用户是否持有足够的代币进行兑换,并驱动“Max”按钮。这只是普通的 RPC 调用,并非 Titan 调用,因此示例代码库中将余额处理逻辑保留为标准应用代码。
获取报价
当未连接任何钱包时,该应用会调用 /报价/价格 针对上述指示速率,在用户输入时进行去抖处理,以确保实时显示保持响应性,同时不会耗尽速率限制。
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,
};
}
获取掉期报价
当钱包连接后,应用程序会调用 /quote/swap 使用用户的公钥和 模拟 flag。这就是元聚合发生的地方:响应中包含一个 引语 以提供商为键的映射,外加一个 metadata.ExpectedWinner 字段。该应用会向所有服务商请求报价(网关将此数量限制为10个),读取最优方案,对每条路线进行标准化处理,剔除未能提供可用路线的服务商,并按“最佳结果优先”的原则进行排序,同时确保预期最优方案始终位于首位。
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,
// Ask every provider to quote so the race is populated (server max is 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))
// Drop providers that failed to produce a usable route.
.filter((q) => Number(q.outAmount) > 0);
// Sort best output first; keep the expected winner on top if present.
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 };
}
该 模拟 该参数对应于用户界面的“精确”与“快速”切换按钮。根据实时链上状态对每条路径进行模拟虽然更精确,但会增加延迟;关闭该功能则会返回更快但验证程度较低的报价。随后,用户界面会将竞争报价并排显示,标明每条路径落后领先者的基点数,标记预期胜出者,并将胜出的路径传递至交换步骤。
响应中的每条路由都以 MessagePack 格式传输,其中包含紧凑键,而公钥则以原始字节缓冲区形式传输,因此需要进行一次规范化处理,将所有内容转换为交易构建器所期望的 base58 / base64 格式。
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 is parts-per-billion; convert to a percentage.
allocPct: s.allocPpb != null ? Number(s.allocPpb) / 1e7 : 0,
};
}
// The API uses compact keys (p/a/d, p/s/w) but we read long forms too so a
// schema tweak upstream doesn't silently break the builder.
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) 已保留在示例仓库中,供您查阅。
位于 地址查找表 必须从 RPC 中获取,并在编译消息时传递。跳过此步骤会导致事务编译失败,因为如果没有相应的表,就无法解析路由指令中的压缩账户引用。
签署并发送交易
交易构建完成后,swap hook 会提示已连接的钱包对其进行签名,通过 Quicknode RPC 提交原始交易,并确认该交易。由于 RPC 代理仅支持 HTTP JSON-RPC(不支持 WebSocket),因此应用通过轮询方式进行确认 getSignatureStatuses 直到交易得到确认、出现错误,或者其区块哈希过期为止。状态流经 构建 → 签名 → 发送 → 确认 → 成功,并且界面会显示该签名,并附有一个链接,用于在链上验证此次交换。
const executeSwap = async (route: TitanSwapRoute, toToken: Token) => {
if (!signer) throw new Error("Wallet not connected");
const rpc = createRpc();
// Build the transaction from Titan's instructions + lookup tables.
setStatus("building");
const { message, lastValidBlockHeight } = await buildSwapTransaction(
route,
signer,
rpc
);
// Sign in the wallet via the Kit signer attached to the message.
setStatus("signing");
const signedTransaction = await signTransactionMessageWithSigners(message);
// Submit through Quicknode RPC.
setStatus("sending");
const signature = getSignatureFromTransaction(signedTransaction);
await rpc
.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), {
encoding: "base64",
skipPreflight: false,
preflightCommitment: "confirmed",
})
.send();
setTxSignature(signature);
// Confirm by polling over HTTP (the RPC proxy carries no WebSocket).
setStatus("confirming");
await confirmBySignature(rpc, signature, lastValidBlockHeight);
setStatus("success");
};
签名、发送和确认属于 Solana 交易的标准基础操作,而非 Titan 调用,因此周围的钩子状态以及 通过签名确认 poller 作为标准应用程序代码保留在示例代码库中。
交易完成签署、发送和确认后,兑换即告完成。至此,Titan的完整流程已全部完成:价格预览 → 兑换竞速 → 构建 → 签署 → 发送。
总结
您已构建了一个交易对交换界面,借助 Titan 元聚合 API,将代币发现、多提供商路由、滑点处理和交易管理整合为一种统一的用户体验。由于 Titan 返回的是竞争性报价和可组合的指令,而非封装好的交易,因此您可以准确了解哪些提供商参与了竞价、哪条路由胜出,以及如何自行组装并发送交易。 对交易生命周期的这种掌控权,为您提供了坚实的扩展基础,无论是添加优先级费用、自定义指令,还是您自己的落单策略。
常见问题解答
什么是 Titan 元聚合 API?
Titan Meta-Aggregation API 是一款 Solana 交易 API,它能够通过单次请求从多个流动性提供商处获取可执行报价。它并非通过单一引擎进行路由,而是将请求分发给多个提供商,根据实时链上状态模拟每条路径,并返回所有竞争报价以及其预期会胜出的报价。
元聚合与普通的去中心化交易所(DEX)聚合器有何不同?
普通的聚合器会返回其自身认为的最佳单一路由。而元聚合器则会同时向多个提供商(包括其自身的路由器、其他聚合器以及询价平台)发起查询,并返回所有这些路由,以便您的应用程序可以进行比较或选出预期的优胜者。您将获得全面的竞争结果,而非一个“黑箱”式的答案。
为什么我必须自己构建事务?
Titan 返回可组合的指令及其引用的地址查找表,而非密封交易。您需要从 RPC 中解析这些查找表,编译一个 v0 版本的 VersionedTransaction,使用已连接的钱包对其进行签名,然后发送该交易。这使您能够完全掌控如何通过自定义逻辑封装该交换操作,但这也意味着您必须在编译之前加载这些查找表,否则交易将无法构建。
何时应该使用价格接口,何时又应该使用掉期接口?
在用户输入时,请使用 /quote/price 进行轻量级的实时显示,因为它无需构建指令或执行完整的提供商竞争即可返回预期结果。仅当钱包已连接且用户准备采取行动时,才调用 /quote/swap,因为该操作会执行完整的元聚合,并返回可执行的指令和地址查找表。
Titan 元聚合 API 是否已在 Solana 测试网上线,还是仅在主网可用?
该功能仅在 Solana 主网测试版上提供。您执行的任何兑换操作都将涉及真实代币的交易并产生实际网络手续费,且可能因价格波动、滑点或选择错误的代币对而导致价值损失。
要集成 Titan 元聚合 API,我需要什么?
您需要一个已启用 Titan Meta-Aggregation API 的 Quicknode Solana 主网端点,具备基本的 TypeScript 和 Solana dApp 经验,并准备少量主网 SOL 和代币用于测试。Quicknode RPC 令牌和 API 凭据均应位于代理路由后的服务器端。
资源
我们 ❤️ 您的反馈!
如果您有任何反馈或对新主题的建议,请告诉我们。我们非常期待您的来信。
