jupiter.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import {
  2. Connection,
  3. PublicKey,
  4. Keypair,
  5. VersionedTransaction,
  6. } from '@solana/web3.js'
  7. import ky from 'ky'
  8. import { Token } from '@/lib/byreal-clmm-sdk/src/client/token'
  9. // USDC 和 USDT 作为默认的输入代币(用于兑换)
  10. const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
  11. const USDT_MINT = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
  12. /** 稳定币 mint:用 USDC 换这些币时无需 swap */
  13. const STABLECOIN_MINTS = [
  14. USDC_MINT,
  15. USDT_MINT,
  16. 'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB', // USD1
  17. ]
  18. export interface JupiterQuote {
  19. inputMint: string
  20. inAmount: string
  21. outputMint: string
  22. outAmount: string
  23. swapMode: string
  24. slippageBps: number
  25. priceImpactPct: string
  26. routePlan: Array<{
  27. swapInfo: {
  28. label?: string
  29. [key: string]: unknown
  30. }
  31. }>
  32. }
  33. export interface JupiterSwapResponse {
  34. swapTransaction: string
  35. }
  36. /**
  37. * 获取代币余额(兼容 Token-2022 和标准 SPL Token)
  38. * 使用 Token.detectTokenTypeAndGetBalance,避免 Token-2022 代币查询返回 0
  39. */
  40. export async function getTokenBalance(
  41. connection: Connection,
  42. walletAddress: PublicKey,
  43. mintAddress: string
  44. ): Promise<number> {
  45. try {
  46. const token = new Token(connection)
  47. const result = await token.detectTokenTypeAndGetBalance(
  48. walletAddress.toBase58(),
  49. mintAddress
  50. )
  51. return result.balance
  52. } catch (error) {
  53. console.error(`Error getting balance for ${mintAddress}:`, error)
  54. return 0
  55. }
  56. }
  57. /**
  58. * 获取 Jupiter API 请求头
  59. */
  60. function getJupiterHeaders(): Record<string, string> {
  61. const headers: Record<string, string> = {
  62. Accept: 'application/json',
  63. }
  64. const apiKey = process.env.JUPITER_API_KEY
  65. if (apiKey) {
  66. headers['x-api-key'] = apiKey
  67. }
  68. return headers
  69. }
  70. /** Jupiter Price API v3 返回的单个代币价格(price.jup.ag 已弃用,请用 api.jup.ag/price/v3) */
  71. const JUPITER_PRICE_API_URL =
  72. process.env.JUPITER_PRICE_API_URL || 'https://api.jup.ag/price/v3'
  73. /**
  74. * 从 Jupiter Price API v3 获取代币 USD 价格(备用,避免使用已弃用的 price.jup.ag)
  75. * @returns 成功时返回 { tokenAPriceUsd, tokenBPriceUsd },失败返回 null
  76. */
  77. export async function getTokenPricesFromJupiter(
  78. mintA: string,
  79. mintB: string
  80. ): Promise<{ tokenAPriceUsd: number; tokenBPriceUsd: number } | null> {
  81. try {
  82. const ids = [mintA, mintB].filter((m, i, arr) => arr.indexOf(m) === i).join(',')
  83. const data = await ky
  84. .get(`${JUPITER_PRICE_API_URL}?ids=${ids}`, {
  85. headers: getJupiterHeaders(),
  86. timeout: 10000,
  87. })
  88. .json<Record<string, { price?: number; usdPrice?: number }>>()
  89. const priceOf = (mint: string): number | undefined => {
  90. const row = data[mint]
  91. if (!row) return undefined
  92. return row.usdPrice ?? row.price
  93. }
  94. const pa = priceOf(mintA)
  95. const pb = priceOf(mintB)
  96. if (pa != null && pa > 0 && pb != null && pb > 0) {
  97. return { tokenAPriceUsd: pa, tokenBPriceUsd: pb }
  98. }
  99. return null
  100. } catch (error) {
  101. console.warn('Jupiter price v3 fetch failed:', error)
  102. return null
  103. }
  104. }
  105. /**
  106. * 从 Jupiter API 获取 quote
  107. * @param restrictIntermediate - 为 false 时允许更多中间代币路由(用于 NO_ROUTES_FOUND 时重试)
  108. */
  109. export async function fetchJupiterQuote(
  110. inputMint: string,
  111. outputMint: string,
  112. amount: string | number,
  113. swapMode: 'ExactIn' | 'ExactOut' = 'ExactIn',
  114. slippageBps: number = 200, // 2%
  115. restrictIntermediate: boolean = true
  116. ): Promise<JupiterQuote> {
  117. const jupiterBaseUrl =
  118. process.env.JUPITER_API_URL || 'https://api.jup.ag/swap/v1'
  119. const rawAmount =
  120. typeof amount === 'string' ? amount : String(Math.floor(Number(amount)))
  121. const searchParams = new URLSearchParams({
  122. inputMint,
  123. outputMint,
  124. amount: rawAmount,
  125. swapMode,
  126. slippageBps: String(slippageBps),
  127. onlyDirectRoutes: 'false',
  128. restrictIntermediateTokens: restrictIntermediate ? 'true' : 'false',
  129. })
  130. const response = await ky
  131. .get(`${jupiterBaseUrl}/quote`, {
  132. searchParams,
  133. timeout: 30000,
  134. headers: getJupiterHeaders(),
  135. })
  136. .json<JupiterQuote>()
  137. return response
  138. }
  139. /**
  140. * 执行 Jupiter swap
  141. */
  142. export async function executeJupiterSwap(
  143. quoteData: JupiterQuote,
  144. walletAddress: string
  145. ): Promise<JupiterSwapResponse> {
  146. const jupiterBaseUrl =
  147. process.env.JUPITER_API_URL || 'https://api.jup.ag/swap/v1'
  148. const headers = {
  149. ...getJupiterHeaders(),
  150. 'Content-Type': 'application/json',
  151. }
  152. const response = await ky
  153. .post(`${jupiterBaseUrl}/swap`, {
  154. json: {
  155. quoteResponse: quoteData,
  156. userPublicKey: walletAddress,
  157. wrapAndUnwrapSol: true,
  158. prioritizationFeeLamports: 10000,
  159. dynamicSlippage: false,
  160. },
  161. timeout: 30000,
  162. headers,
  163. })
  164. .json<JupiterSwapResponse>()
  165. return response
  166. }
  167. /**
  168. * 按 USD 金额执行 swap(ExactIn:花掉指定美元 USDC,换回目标代币)
  169. * 不使用 ExactOut,不使用代币数量 amount,仅使用 valueUsd。
  170. * @param usdValue - 要换入的美元数(例如 5.5 表示花 $5.5 USDC)
  171. */
  172. export async function swapIfNeeded(
  173. connection: Connection,
  174. keypair: Keypair,
  175. outputMint: string,
  176. usdValue: number,
  177. inputMint: string = USDC_MINT
  178. ): Promise<{ success: boolean; txid?: string; error?: string }> {
  179. const walletAddress = keypair.publicKey
  180. if (!usdValue || Number(usdValue) <= 0) {
  181. console.log(`Skip swap: USD value is ${usdValue}`)
  182. return { success: true }
  183. }
  184. const mint = String(outputMint ?? '').trim()
  185. const isStablecoin =
  186. STABLECOIN_MINTS.includes(mint) || mint === USDC_MINT
  187. if (isStablecoin) {
  188. console.log(
  189. `Skip swap: output is stablecoin (USDC/USDT), no need to swap USDC -> same`
  190. )
  191. return { success: true }
  192. }
  193. const inputAmountRaw = Math.floor(Number(usdValue) * 1e6)
  194. if (inputAmountRaw < 1e6) {
  195. console.log(`Skip swap: USD value too small (${usdValue})`)
  196. return { success: true }
  197. }
  198. console.log(
  199. `Swap: $${usdValue} USDC -> ${outputMint.slice(0, 8)}... (ExactIn)`
  200. )
  201. const slippageBps = 200 // 2%
  202. const fetchQuoteWithRetry = async (
  203. restrictIntermediate: boolean
  204. ): Promise<JupiterQuote> => {
  205. try {
  206. return await fetchJupiterQuote(
  207. inputMint,
  208. outputMint,
  209. inputAmountRaw,
  210. 'ExactIn',
  211. slippageBps,
  212. restrictIntermediate
  213. )
  214. } catch (e) {
  215. const msg = e instanceof Error ? e.message : String(e)
  216. if (
  217. msg.includes('No routes found') &&
  218. restrictIntermediate
  219. ) {
  220. console.log(
  221. 'No routes with restrictIntermediate=true, retrying with allow all intermediates...'
  222. )
  223. return fetchJupiterQuote(
  224. inputMint,
  225. outputMint,
  226. inputAmountRaw,
  227. 'ExactIn',
  228. slippageBps,
  229. false
  230. )
  231. }
  232. throw e
  233. }
  234. }
  235. try {
  236. const quoteData = await fetchQuoteWithRetry(true)
  237. if (quoteData.routePlan && quoteData.routePlan.length > 0) {
  238. const routeLabels = quoteData.routePlan
  239. .map((r) => r.swapInfo?.label || 'Unknown')
  240. .join(' -> ')
  241. console.log(`Route: ${routeLabels}`)
  242. console.log(
  243. `Expected output: ${quoteData.outAmount} (ExactIn), price impact: ${quoteData.priceImpactPct}%`
  244. )
  245. }
  246. const swapData = await executeJupiterSwap(
  247. quoteData,
  248. walletAddress.toBase58()
  249. )
  250. const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64')
  251. const transaction = VersionedTransaction.deserialize(swapTransactionBuf)
  252. transaction.sign([keypair])
  253. const signature = await connection.sendTransaction(transaction, {
  254. maxRetries: 3,
  255. skipPreflight: false,
  256. })
  257. console.log(`Swap transaction sent: ${signature}`)
  258. const confirmation = await connection.confirmTransaction(
  259. signature,
  260. 'confirmed'
  261. )
  262. if (confirmation.value.err) {
  263. throw new Error(`Transaction failed: ${confirmation.value.err}`)
  264. }
  265. console.log(`Swap confirmed: https://solscan.io/tx/${signature}`)
  266. const newBalance = await getTokenBalance(
  267. connection,
  268. walletAddress,
  269. outputMint
  270. )
  271. console.log(`New balance: ${newBalance.toFixed(6)}`)
  272. return { success: true, txid: signature }
  273. } catch (error) {
  274. const errorMessage =
  275. error instanceof Error ? error.message : 'Unknown swap error'
  276. console.error('Swap failed:', errorMessage)
  277. return {
  278. success: false,
  279. error: errorMessage,
  280. }
  281. }
  282. }
  283. /**
  284. * 确保 LP 所需的两种代币都有足够余额
  285. * 按 valueUsd 换币:使用 ExactIn,花 $valueUsd USDC 换目标代币,不使用 amount
  286. */
  287. export async function ensureSufficientBalances(
  288. connection: Connection,
  289. keypair: Keypair,
  290. tokenA: { mint: string; valueUsd: number },
  291. tokenB: { mint: string; valueUsd: number }
  292. ): Promise<{
  293. success: boolean
  294. swapTxids: string[]
  295. error?: string
  296. }> {
  297. const swapTxids: string[] = []
  298. // Token A:按 valueUsd 换
  299. console.log(
  300. `\n--- Token A (${tokenA.mint.slice(0, 8)}...): swap $${tokenA.valueUsd} USDC ---`
  301. )
  302. const resultA = await swapIfNeeded(
  303. connection,
  304. keypair,
  305. tokenA.mint,
  306. tokenA.valueUsd,
  307. USDC_MINT
  308. )
  309. if (!resultA.success) {
  310. console.log('USDC swap failed for Token A, trying USDT...')
  311. const resultA_USDT = await swapIfNeeded(
  312. connection,
  313. keypair,
  314. tokenA.mint,
  315. tokenA.valueUsd,
  316. USDT_MINT
  317. )
  318. if (!resultA_USDT.success) {
  319. return {
  320. success: false,
  321. swapTxids,
  322. error: `Failed to get Token A: ${resultA.error || resultA_USDT.error}`,
  323. }
  324. }
  325. if (resultA_USDT.txid) swapTxids.push(resultA_USDT.txid)
  326. } else if (resultA.txid) {
  327. swapTxids.push(resultA.txid)
  328. }
  329. // Token B:按 valueUsd 换
  330. console.log(
  331. `\n--- Token B (${tokenB.mint.slice(0, 8)}...): swap $${tokenB.valueUsd} USDC ---`
  332. )
  333. const resultB = await swapIfNeeded(
  334. connection,
  335. keypair,
  336. tokenB.mint,
  337. tokenB.valueUsd,
  338. USDC_MINT
  339. )
  340. if (!resultB.success) {
  341. console.log('USDC swap failed for Token B, trying USDT...')
  342. const resultB_USDT = await swapIfNeeded(
  343. connection,
  344. keypair,
  345. tokenB.mint,
  346. tokenB.valueUsd,
  347. USDT_MINT
  348. )
  349. if (!resultB_USDT.success) {
  350. return {
  351. success: false,
  352. swapTxids,
  353. error: `Failed to get Token B: ${resultB.error || resultB_USDT.error}`,
  354. }
  355. }
  356. if (resultB_USDT.txid) swapTxids.push(resultB_USDT.txid)
  357. } else if (resultB.txid) {
  358. swapTxids.push(resultB.txid)
  359. }
  360. return { success: true, swapTxids }
  361. }