소스 검색

feat(jupiter): 升级 swap API 到 Ultra API

lushdog@outlook.com 1 개월 전
부모
커밋
63ee31e7b7
1개의 변경된 파일164개의 추가작업 그리고 62개의 파일을 삭제
  1. 164 62
      src/lib/jupiter.ts

+ 164 - 62
src/lib/jupiter.ts

@@ -38,6 +38,75 @@ export interface JupiterSwapResponse {
 	swapTransaction: string
 }
 
+export interface UltraOrderResponse {
+	mode: string
+	inputMint: string
+	outputMint: string
+	inAmount: string
+	outAmount: string
+	inUsdValue?: number
+	outUsdValue?: number
+	priceImpact?: number
+	otherAmountThreshold: string
+	swapMode: string
+	slippageBps: number
+	priceImpactPct: string
+	routePlan: Array<{
+		swapInfo: {
+			ammKey: string
+			label: string
+			inputMint: string
+			outputMint: string
+			inAmount: string
+			outAmount: string
+		}
+		percent: number
+		bps: number
+		usdValue?: number
+	}>
+	feeMint?: string
+	feeBps?: number
+	platformFee?: {
+		amount: string
+		feeBps: number
+	}
+	signatureFeeLamports: number
+	signatureFeePayer: string | null
+	prioritizationFeeLamports: number
+	prioritizationFeePayer: string | null
+	rentFeeLamports: number
+	rentFeePayer: string | null
+	router: string
+	transaction: string | null
+	gasless: boolean
+	requestId: string
+	totalTime: number
+	taker: string | null
+	quoteId?: string
+	maker?: string
+	expireAt?: string
+	errorCode?: number
+	errorMessage?: string
+}
+
+export interface UltraExecuteResponse {
+	status: 'Success' | 'Failed'
+	signature?: string
+	slot?: string
+	error?: string
+	code: number
+	totalInputAmount?: string
+	totalOutputAmount?: string
+	inputAmountResult?: string
+	outputAmountResult?: string
+	swapEvents?: Array<{
+		inputMint: string
+		inputAmount: string
+		outputMint: string
+		outputAmount: string
+	}>
+}
+
 /**
  * 获取代币余额(兼容 Token-2022 和标准 SPL Token)
  * 使用 Token.detectTokenTypeAndGetBalance,避免 Token-2022 代币查询返回 0
@@ -95,7 +164,9 @@ export async function getTokenPricesFromJupiter(
 			)
 			return null
 		}
-		const ids = [mintA, mintB].filter((m, i, arr) => arr.indexOf(m) === i).join(',')
+		const ids = [mintA, mintB]
+			.filter((m, i, arr) => arr.indexOf(m) === i)
+			.join(',')
 		const data = await ky
 			.get(`${JUPITER_PRICE_API_URL}?ids=${ids}`, {
 				headers: getJupiterHeaders(),
@@ -201,9 +272,68 @@ export async function executeJupiterSwap(
 	return response
 }
 
+const ULTRA_API_URL = 'https://api.jup.ag/ultra/v1/'
+
+/**
+ * 获取 Jupiter Ultra Order(返回 unsigned transaction 和 requestId)
+ */
+export async function fetchUltraOrder(
+	inputMint: string,
+	outputMint: string,
+	amount: string | number,
+	taker: string,
+	slippageBps: number = 200
+): Promise<UltraOrderResponse> {
+	const rawAmount =
+		typeof amount === 'string' ? amount : String(Math.floor(Number(amount)))
+
+	const searchParams = new URLSearchParams({
+		inputMint,
+		outputMint,
+		amount: rawAmount,
+		taker,
+		slippageBps: String(slippageBps),
+	})
+
+	const response = await ky
+		.get(`${ULTRA_API_URL}/order`, {
+			searchParams,
+			timeout: 30000,
+			headers: getJupiterHeaders(),
+		})
+		.json<UltraOrderResponse>()
+
+	return response
+}
+
 /**
- * 按 USD 金额执行 swap(ExactIn:花掉指定美元 USDC,换回目标代币)
- * 不使用 ExactOut,不使用代币数量 amount,仅使用 valueUsd。
+ * 执行 Jupiter Ultra swap
+ */
+export async function executeUltraSwap(
+	signedTransaction: string,
+	requestId: string
+): Promise<UltraExecuteResponse> {
+	const headers = {
+		...getJupiterHeaders(),
+		'Content-Type': 'application/json',
+	}
+
+	const response = await ky
+		.post(`${ULTRA_API_URL}/execute`, {
+			json: {
+				signedTransaction,
+				requestId,
+			},
+			timeout: 30000,
+			headers,
+		})
+		.json<UltraExecuteResponse>()
+
+	return response
+}
+
+/**
+ * 按 USD 金额执行 swap(使用 Jupiter Ultra API)
  * @param usdValue - 要换入的美元数(例如 5.5 表示花 $5.5 USDC)
  */
 export async function swapIfNeeded(
@@ -221,8 +351,7 @@ export async function swapIfNeeded(
 	}
 
 	const mint = String(outputMint ?? '').trim()
-	const isStablecoin =
-		STABLECOIN_MINTS.includes(mint) || mint === USDC_MINT
+	const isStablecoin = STABLECOIN_MINTS.includes(mint) || mint === USDC_MINT
 	if (isStablecoin) {
 		console.log(
 			`Skip swap: output is stablecoin (USDC/USDT), no need to swap USDC -> same`
@@ -237,84 +366,57 @@ export async function swapIfNeeded(
 	}
 
 	console.log(
-		`Swap: $${usdValue} USDC -> ${outputMint.slice(0, 8)}... (ExactIn)`
+		`Swap (Ultra): $${usdValue} USDC -> ${outputMint.slice(0, 8)}... (ExactIn)`
 	)
 
-	const slippageBps = 200 // 2%
-
-	const fetchQuoteWithRetry = async (
-		restrictIntermediate: boolean
-	): Promise<JupiterQuote> => {
-		try {
-			return await fetchJupiterQuote(
-				inputMint,
-				outputMint,
-				inputAmountRaw,
-				'ExactIn',
-				slippageBps,
-				restrictIntermediate
-			)
-		} catch (e) {
-			const msg = e instanceof Error ? e.message : String(e)
-			if (
-				msg.includes('No routes found') &&
-				restrictIntermediate
-			) {
-				console.log(
-					'No routes with restrictIntermediate=true, retrying with allow all intermediates...'
-				)
-				return fetchJupiterQuote(
-					inputMint,
-					outputMint,
-					inputAmountRaw,
-					'ExactIn',
-					slippageBps,
-					false
-				)
-			}
-			throw e
-		}
-	}
+	const slippageBps = 200
 
 	try {
-		const quoteData = await fetchQuoteWithRetry(true)
+		const orderData = await fetchUltraOrder(
+			inputMint,
+			outputMint,
+			inputAmountRaw,
+			walletAddress.toBase58(),
+			slippageBps
+		)
+
+		if (!orderData.transaction) {
+			const errorMsg = orderData.errorMessage || 'No transaction returned'
+			console.error(`Ultra order error: ${errorMsg}`)
+			return { success: false, error: errorMsg }
+		}
 
-		if (quoteData.routePlan && quoteData.routePlan.length > 0) {
-			const routeLabels = quoteData.routePlan
+		if (orderData.routePlan && orderData.routePlan.length > 0) {
+			const routeLabels = orderData.routePlan
 				.map((r) => r.swapInfo?.label || 'Unknown')
 				.join(' -> ')
 			console.log(`Route: ${routeLabels}`)
 			console.log(
-				`Expected output: ${quoteData.outAmount} (ExactIn), price impact: ${quoteData.priceImpactPct}%`
+				`Expected output: ${orderData.outAmount}, price impact: ${orderData.priceImpactPct}%`
 			)
+			console.log(`Router: ${orderData.router}, gasless: ${orderData.gasless}`)
 		}
 
-		const swapData = await executeJupiterSwap(
-			quoteData,
-			walletAddress.toBase58()
-		)
-
-		const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64')
+		const swapTransactionBuf = Buffer.from(orderData.transaction, 'base64')
 		const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
 		transaction.sign([keypair])
 
-		const signature = await connection.sendTransaction(transaction, {
-			maxRetries: 3,
-			skipPreflight: false,
-		})
-
-		console.log(`Swap transaction sent: ${signature}`)
+		const signedTransaction = Buffer.from(transaction.serialize()).toString(
+			'base64'
+		)
 
-		const confirmation = await connection.confirmTransaction(
-			signature,
-			'confirmed'
+		const executeResult = await executeUltraSwap(
+			signedTransaction,
+			orderData.requestId
 		)
 
-		if (confirmation.value.err) {
-			throw new Error(`Transaction failed: ${confirmation.value.err}`)
+		if (executeResult.status !== 'Success') {
+			throw new Error(executeResult.error || 'Execute failed')
 		}
 
+		const signature = executeResult.signature!
 		console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
+
 		const newBalance = await getTokenBalance(
 			connection,
 			walletAddress,