|
@@ -38,6 +38,75 @@ export interface JupiterSwapResponse {
|
|
|
swapTransaction: string
|
|
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-2022 和标准 SPL Token)
|
|
|
* 使用 Token.detectTokenTypeAndGetBalance,避免 Token-2022 代币查询返回 0
|
|
* 使用 Token.detectTokenTypeAndGetBalance,避免 Token-2022 代币查询返回 0
|
|
@@ -95,7 +164,9 @@ export async function getTokenPricesFromJupiter(
|
|
|
)
|
|
)
|
|
|
return null
|
|
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
|
|
const data = await ky
|
|
|
.get(`${JUPITER_PRICE_API_URL}?ids=${ids}`, {
|
|
.get(`${JUPITER_PRICE_API_URL}?ids=${ids}`, {
|
|
|
headers: getJupiterHeaders(),
|
|
headers: getJupiterHeaders(),
|
|
@@ -201,9 +272,68 @@ export async function executeJupiterSwap(
|
|
|
return response
|
|
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)
|
|
* @param usdValue - 要换入的美元数(例如 5.5 表示花 $5.5 USDC)
|
|
|
*/
|
|
*/
|
|
|
export async function swapIfNeeded(
|
|
export async function swapIfNeeded(
|
|
@@ -221,8 +351,7 @@ export async function swapIfNeeded(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const mint = String(outputMint ?? '').trim()
|
|
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) {
|
|
if (isStablecoin) {
|
|
|
console.log(
|
|
console.log(
|
|
|
`Skip swap: output is stablecoin (USDC/USDT), no need to swap USDC -> same`
|
|
`Skip swap: output is stablecoin (USDC/USDT), no need to swap USDC -> same`
|
|
@@ -237,84 +366,57 @@ export async function swapIfNeeded(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.log(
|
|
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 {
|
|
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')
|
|
.map((r) => r.swapInfo?.label || 'Unknown')
|
|
|
.join(' -> ')
|
|
.join(' -> ')
|
|
|
console.log(`Route: ${routeLabels}`)
|
|
console.log(`Route: ${routeLabels}`)
|
|
|
console.log(
|
|
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)
|
|
const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
|
|
|
transaction.sign([keypair])
|
|
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}`)
|
|
console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
|
|
|
|
|
+
|
|
|
const newBalance = await getTokenBalance(
|
|
const newBalance = await getTokenBalance(
|
|
|
connection,
|
|
connection,
|
|
|
walletAddress,
|
|
walletAddress,
|