-
Notifications
You must be signed in to change notification settings - Fork 124
add Jupiter Ultra Swap #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
740621b
eb234c7
e9376aa
186e6c0
55c7350
b367c6b
f6374d4
cc9ed1e
123d65c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # QuickNode RPC URL | ||
| # Get your QuickNode RPC endpoint from https://www.quicknode.com/ | ||
| QUICKNODE_RPC_URL=your_quicknode_rpc_endpoint_here | ||
|
|
||
| # Jupiter API Key (optional but recommended for better rate limits) | ||
| # Get your Jupiter API key from https://portal.jup.ag/ | ||
| JUPITER_API_KEY=your_jupiter_api_key_here |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
|
||
| # dependencies | ||
| /node_modules | ||
| /.pnp | ||
| .pnp.js | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
| # next.js | ||
| /.next/ | ||
| /out/ | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| # misc | ||
| .DS_Store | ||
| *.pem | ||
|
|
||
| # debug | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
|
|
||
| # local env files | ||
| .env*.local | ||
| .env | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts | ||
|
|
||
| # cursor | ||
| /.cursor/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Jupiter Ultra Swap Demo | ||
|
|
||
| A single-page Solana swap UI demonstrating token swaps using Jupiter Ultra API, QuickNode RPC, Solana Wallet Adapter, and Solana Kit. | ||
|
|
||
| ## Features | ||
|
|
||
| - Connect Solana wallet (Phantom, Backpack, Solflare) | ||
| - Select From and To tokens | ||
| - Enter swap amount | ||
| - View token balances | ||
| - Execute swaps via Jupiter Ultra | ||
| - View transaction status and explorer links | ||
|
|
||
| ## Setup | ||
|
|
||
| 1. Install dependencies: | ||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| 2. Create `.env.local` file (copy from `.env.example`): | ||
| ```bash | ||
| cp .env.example .env.local | ||
| ``` | ||
|
|
||
| Then edit `.env.local` and add your API keys: | ||
| ```bash | ||
| QUICKNODE_RPC_URL=your_quicknode_rpc_endpoint_here | ||
| JUPITER_API_KEY=your_jupiter_api_key_here | ||
| ``` | ||
|
|
||
| **Note:** | ||
| - Get your QuickNode RPC endpoint from [QuickNode](https://www.quicknode.com/) | ||
| - Get your Jupiter API key from [Jupiter API Portal](https://portal.jup.ag/). The API key is optional but recommended for better rate limits and reliability. | ||
| - These API keys are stored server-side and never exposed to the client. They are used in Next.js API routes only. | ||
|
|
||
| 3. Run the development server: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| 4. Open [http://localhost:3000](http://localhost:3000) in your browser. | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - **Framework:** Next.js 14 (App Router) | ||
| - **UI:** React + Tailwind CSS | ||
| - **Wallets:** Solana Wallet Adapter for React | ||
| - **Blockchain:** Solana mainnet via QuickNode RPC | ||
| - **Swap API:** Jupiter Ultra API | ||
| - **Solana SDK:** @solana/kit v5 | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| ├── app/ | ||
| │ ├── api/ # Next.js API routes (server-side) | ||
| │ │ ├── tokens/ # Token list endpoint | ||
| │ │ ├── balances/ # Token balances endpoint | ||
| │ │ ├── quote/ # Swap quote endpoint | ||
| │ │ ├── execute/ # Swap execution endpoint | ||
| │ │ ├── rpc/ # RPC proxy endpoint | ||
| │ │ └── rpc-endpoint/ # RPC URL endpoint | ||
| │ ├── layout.tsx # Root layout with WalletProvider | ||
| │ ├── page.tsx # Main swap page | ||
| │ ├── globals.css # Global styles | ||
| │ └── providers/ | ||
| │ └── WalletProvider.tsx | ||
| ├── components/ | ||
| │ ├── SwapCard.tsx # Main card container | ||
| │ ├── WalletButton.tsx # Wallet connect button | ||
| │ ├── TokenSelector.tsx # Token dropdown | ||
| │ ├── TokenInput.tsx # Token input row | ||
| │ ├── SwapButton.tsx # Swap action button | ||
| │ └── StatusMessage.tsx # Status/error messages | ||
| ├── hooks/ | ||
| │ ├── useTokenBalances.ts # Token balance management | ||
| │ ├── useTokenList.ts # Token list fetching | ||
| │ └── useSwap.ts # Swap execution logic | ||
| └── lib/ | ||
| ├── jupiter.ts # Jupiter API client (calls API routes) | ||
| ├── solana-client.ts # Solana Kit RPC client | ||
| └── types.ts # TypeScript types | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| 1. Connect your Solana wallet | ||
| 2. Select a "From" token (default: SOL) | ||
| 3. Select a "To" token (default: USDC) | ||
| 4. Enter the amount you want to swap | ||
| 5. Click "Swap" and approve the transaction in your wallet | ||
| 6. Wait for confirmation and view the transaction link | ||
|
|
||
| ## Notes | ||
|
|
||
| - This app uses Solana mainnet only | ||
| - All RPC calls go through QuickNode endpoint (proxied via API routes) | ||
| - Swaps are executed via Jupiter Ultra API (proxied via API routes) | ||
| - API keys are stored server-side and never exposed to the client | ||
| - Transaction signing uses Solana Wallet Adapter | ||
| - Solana Kit is used for RPC operations | ||
|
|
||
| ## License | ||
|
|
||
| ISC | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { assertIsAddress } from "@solana/kit"; | ||
|
|
||
| const JUPITER_ULTRA_BASE = "https://api.jup.ag/ultra"; | ||
| const JUPITER_API_KEY = process.env.JUPITER_API_KEY; | ||
|
|
||
| // Validate Solana wallet address using @solana/kit to prevent SSRF attacks | ||
| function isValidWalletAddress(address: string): boolean { | ||
| try { | ||
| assertIsAddress(address); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Helper function to create request headers with API key | ||
| function getHeaders(): HeadersInit { | ||
| const headers: HeadersInit = { | ||
| "Accept": "application/json", | ||
| "Content-Type": "application/json", | ||
| }; | ||
|
|
||
| if (JUPITER_API_KEY) { | ||
| headers["x-api-key"] = JUPITER_API_KEY; | ||
| } | ||
|
|
||
| return headers; | ||
| } | ||
|
|
||
| export async function GET(request: Request) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const walletAddress = searchParams.get("walletAddress"); | ||
|
|
||
| if (!walletAddress) { | ||
| return NextResponse.json( | ||
| { error: "walletAddress parameter is required" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Validate wallet address to prevent SSRF attacks | ||
| if (!isValidWalletAddress(walletAddress)) { | ||
| return NextResponse.json( | ||
| { error: "Invalid walletAddress parameter" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| if (!JUPITER_API_KEY) { | ||
| return NextResponse.json([], { status: 200 }); | ||
| } | ||
|
|
||
| const response = await fetch(`${JUPITER_ULTRA_BASE}/v1/holdings/${walletAddress}`, { | ||
| headers: getHeaders(), | ||
| cache: "no-store", | ||
| }); | ||
|
||
|
|
||
| if (!response.ok) { | ||
| return NextResponse.json([], { status: 200 }); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| const balances: any[] = []; | ||
|
|
||
| // Add SOL balance (native Solana) | ||
| if (data.amount && parseInt(data.amount) > 0) { | ||
| balances.push({ | ||
| mint: "So11111111111111111111111111111111111111112", // SOL mint address | ||
| balance: parseInt(data.amount), | ||
|
||
| decimals: 9, | ||
| }); | ||
| } | ||
|
|
||
| // Add SPL token balances | ||
| if (data.tokens && typeof data.tokens === "object") { | ||
| for (const [mint, tokenAccounts] of Object.entries(data.tokens)) { | ||
| if (Array.isArray(tokenAccounts) && tokenAccounts.length > 0) { | ||
| // Sum up all token accounts for this mint | ||
| let totalAmount = 0; | ||
| let decimals = 0; | ||
|
|
||
| for (const account of tokenAccounts as any[]) { | ||
| if (account.amount) { | ||
| totalAmount += parseInt(account.amount); | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| decimals = account.decimals || 0; | ||
| } | ||
| } | ||
|
|
||
| if (totalAmount > 0) { | ||
| balances.push({ | ||
| mint, | ||
| balance: totalAmount, | ||
| decimals, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return NextResponse.json(balances); | ||
| } catch (error) { | ||
| console.error("Error fetching token balances:", error); | ||
| return NextResponse.json( | ||
| { error: "Failed to fetch token balances" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| const JUPITER_ULTRA_BASE = "https://api.jup.ag/ultra"; | ||
| const JUPITER_API_KEY = process.env.JUPITER_API_KEY; | ||
|
|
||
| // Helper function to create request headers with API key | ||
| function getHeaders(): HeadersInit { | ||
| const headers: HeadersInit = { | ||
| "Accept": "application/json", | ||
| "Content-Type": "application/json", | ||
| }; | ||
|
|
||
| if (JUPITER_API_KEY) { | ||
| headers["x-api-key"] = JUPITER_API_KEY; | ||
| } | ||
|
|
||
| return headers; | ||
| } | ||
|
|
||
| // Helper function to parse error messages from API responses | ||
| async function parseErrorResponse(response: Response): Promise<string> { | ||
| const responseText = await response.text(); | ||
| try { | ||
| const errorData = JSON.parse(responseText); | ||
| return errorData.error || errorData.message || `Request failed: ${response.status}`; | ||
| } catch { | ||
| return responseText || `Request failed: ${response.status}`; | ||
| } | ||
| } | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const body = await request.json(); | ||
| const { signedTransaction, requestId } = body; | ||
|
|
||
| if (!signedTransaction) { | ||
| return NextResponse.json( | ||
| { error: "signedTransaction is required" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| if (!JUPITER_API_KEY) { | ||
| return NextResponse.json( | ||
| { error: "Jupiter Ultra API requires an API key." }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| const response = await fetch(`${JUPITER_ULTRA_BASE}/v1/execute`, { | ||
| method: "POST", | ||
| headers: getHeaders(), | ||
| body: JSON.stringify({ | ||
| signedTransaction, | ||
| requestId, | ||
| }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorMessage = await parseErrorResponse(response); | ||
| return NextResponse.json( | ||
| { error: errorMessage }, | ||
| { status: response.status } | ||
| ); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| return NextResponse.json(result); | ||
| } catch (error) { | ||
| console.error("Error executing swap:", error); | ||
| return NextResponse.json( | ||
| { error: error instanceof Error ? error.message : "Failed to execute swap" }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of the weird things about JS is that you can throw anything - an array, an object, a string, a number - but in reality, nobody ever does. So now everyone has to write |
||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.