From 70350731d472fc4b91eb41c93876bb479c420866 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:30:09 -0700 Subject: [PATCH] init: example of jupiter swap with jito bundles (#80) --- solana/jupiter-jito/.env.local | 3 + solana/jupiter-jito/README.md | 52 ++++++++ solana/jupiter-jito/index.ts | 211 +++++++++++++++++++++++++++++++ solana/jupiter-jito/package.json | 20 +++ 4 files changed, 286 insertions(+) create mode 100644 solana/jupiter-jito/.env.local create mode 100644 solana/jupiter-jito/README.md create mode 100644 solana/jupiter-jito/index.ts create mode 100644 solana/jupiter-jito/package.json diff --git a/solana/jupiter-jito/.env.local b/solana/jupiter-jito/.env.local new file mode 100644 index 0000000..1ea6d2f --- /dev/null +++ b/solana/jupiter-jito/.env.local @@ -0,0 +1,3 @@ +METIS_ENDPOINT= +JITO_ENDPOINT= +WALLET_SECRET= diff --git a/solana/jupiter-jito/README.md b/solana/jupiter-jito/README.md new file mode 100644 index 0000000..fd42e47 --- /dev/null +++ b/solana/jupiter-jito/README.md @@ -0,0 +1,52 @@ +# Jito Jupiter Swap + +A TypeScript utility for executing swaps via Jupiter with Jito MEV bundles. + +## Prerequisites + +- Node.js & npm +- [QuickNode](https://quicknode.com) endpoints with [Metis API](https://marketplace.quicknode.com/add-on/metis-jupiter-v6-swap-api) and [Lil' JIT Add-ons](https://marketplace.quicknode.com/add-on/lil-jit-jito-bundles-and-transactions) +- Solana wallet with SOL +- TypeScript + +> _Note: this project utilizes Solana Web3.js v1.95.4_ + +## Setup + +1. Install dependencies: +```bash +npm install @jup-ag/api @solana/web3.js bs58 dotenv +``` + +2. Instal dev dependencies: +```bash +npm install -d @types/node +``` + +3. Create `.env` file and replace with your keys: +```env +METIS_ENDPOINT=https://public.jupiterapi.com +JITO_ENDPOINT=https://jito-mainnet.quiknode.pro/your-key +WALLET_SECRET=21,31,41,51,61,71,81,91 # Your wallet private key as comma-separated numbers +``` + +## Usage + +```typescript +const swapManager = new JitoSwapManager(); +swapManager.executeSwap().catch(console.error); +``` + +## Configuration + +Edit `CONFIG` in the script to modify: +- Input/output tokens +- Swap amount +- Tip amount +- Polling intervals + +## Safety + +- Never commit your `.env` file +- Keep your private keys secure +- Test with small amounts first - mainnet endpoints can result in irreversible loss of funds diff --git a/solana/jupiter-jito/index.ts b/solana/jupiter-jito/index.ts new file mode 100644 index 0000000..901df2d --- /dev/null +++ b/solana/jupiter-jito/index.ts @@ -0,0 +1,211 @@ +import { + QuoteGetRequest, + QuoteResponse, + createJupiterApiClient, + ResponseError, + SwapResponse +} from '@jup-ag/api'; +import { + Keypair, + LAMPORTS_PER_SOL, + Connection, + SystemProgram, + PublicKey, + Transaction, + VersionedTransaction +} from '@solana/web3.js'; +import bs58 from 'bs58'; +import dotenv from 'dotenv'; + +dotenv.config(); + +// Configuration +const CONFIG = { + METIS_ENDPOINT: process.env.METIS_ENDPOINT || 'https://public.jupiterapi.com', + JITO_ENDPOINT: process.env.JITO_ENDPOINT || '', + WALLET_SECRET: process.env.WALLET_SECRET?.split(',').map(Number) || [], + JITO_TIP_AMOUNT: 0.0005 * LAMPORTS_PER_SOL, // 500,000 lamports + POLL_TIMEOUT_MS: 30000, + POLL_INTERVAL_MS: 3000 +}; + +// Quote request configuration +const QUOTE_REQUEST: QuoteGetRequest = { + inputMint: "So11111111111111111111111111111111111111112", // SOL + outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC + amount: LAMPORTS_PER_SOL / 1000, // 0.001 SOL + restrictIntermediateTokens: true // https://station.jup.ag/docs/apis/landing-transactions#:~:text=()%3B-,restrictIntermediateTokens,-%3A%20Mkae%20sure%20that +}; + +interface BundleStatus { + bundle_id: string; + status: string; + landed_slot?: number; +} + +class JitoSwapManager { + private jupiterApi; + private wallet: Keypair; + private connection: Connection; + + constructor() { + this.jupiterApi = createJupiterApiClient({ basePath: CONFIG.METIS_ENDPOINT }); + this.wallet = Keypair.fromSecretKey(new Uint8Array(CONFIG.WALLET_SECRET)); + this.connection = new Connection(CONFIG.JITO_ENDPOINT); + } + + async getSwapQuote(): Promise { + const quote = await this.jupiterApi.quoteGet(QUOTE_REQUEST); + if (!quote) throw new Error('No quote found'); + return quote; + } + + async getSwapTransaction(quote: QuoteResponse): Promise { + const swapResult = await this.jupiterApi.swapPost({ + swapRequest: { + quoteResponse: quote, + userPublicKey: this.wallet.publicKey.toBase58(), + }, + }); + if (!swapResult) throw new Error('No swap result found'); + return swapResult; + } + + async createTipTransaction(jitoTipAccount: string): Promise { + const tipTx = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: this.wallet.publicKey, + toPubkey: new PublicKey(jitoTipAccount), + lamports: CONFIG.JITO_TIP_AMOUNT, + }) + ); + tipTx.feePayer = this.wallet.publicKey; + tipTx.recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash; + tipTx.sign(this.wallet); + return tipTx; + } + + private convertBase64ToBase58(base64String: string): string { + return bs58.encode(Buffer.from(base64String, 'base64')); + } + + async getTipAccount(): Promise { + //@ts-ignore can use _rpcRequest + const { result: tipAccounts } = await this.connection._rpcRequest("getTipAccounts", []); + return tipAccounts[Math.floor(Math.random() * tipAccounts.length)]; + } + + async simulateBundle(b64Transactions: string[]): Promise { + //@ts-ignore can use _rpcRequest + const result = await this.connection._rpcRequest( + "simulateBundle", + [{ encodedTransactions: b64Transactions }] + ); + if (result.error) throw new Error(`Simulation failed: ${result.error}`); + return result; + } + + async sendBundle(base58Transactions: string[]): Promise { + //@ts-ignore can use _rpcRequest + const { result } = await this.connection._rpcRequest( + "sendBundle", + [base58Transactions] + ); + return result; + } + + async pollBundleStatus(bundleId: string): Promise { + const startTime = Date.now(); + let lastStatus = ''; + + while (Date.now() - startTime < CONFIG.POLL_TIMEOUT_MS) { + try { + //@ts-ignore can use _rpcRequest + const response = await this.connection._rpcRequest("getInflightBundleStatuses", [[bundleId]]); + const bundleStatuses: BundleStatus[] = response.result.value; + + const status = bundleStatuses[0].status; + if (status !== lastStatus) { + lastStatus = status; + console.log(`Bundle status: ${status}`); + } + + if (status === 'Landed') { + console.log(`Bundle landed at slot: ${bundleStatuses[0].landed_slot}`); + return true; + } + + if (status === 'Failed') { + throw new Error(`Bundle failed with status: ${status}`); + } + + await new Promise(resolve => setTimeout(resolve, CONFIG.POLL_INTERVAL_MS)); + } catch (error) { + console.error('Error polling bundle status:', error); + } + } + throw new Error("Polling timeout reached without confirmation"); + } + + private async checkEnvironment(): Promise { + if (!CONFIG.WALLET_SECRET.length) { + throw new Error('No wallet secret provided'); + } + if (!CONFIG.JITO_ENDPOINT) { + throw new Error('No Jito endpoint provided'); + } + } + + async executeSwap(): Promise { + try { + await this.checkEnvironment(); + console.log(`Using Wallet: ${this.wallet.publicKey.toBase58()}`); + + // Get Jupiter quote and swap transaction + console.log('Getting Swap Quote...'); + const quote = await this.getSwapQuote(); + const swapResult = await this.getSwapTransaction(quote); + + // Process swap transaction + const swapTxBuf = Buffer.from(swapResult.swapTransaction, 'base64'); + const swapVersionedTx = VersionedTransaction.deserialize(swapTxBuf); + swapVersionedTx.sign([this.wallet]); + + // Convert swap transaction to required formats + const serializedSwapTx = swapVersionedTx.serialize(); + const b64SwapTx = Buffer.from(serializedSwapTx).toString('base64'); + const b58SwapTx = this.convertBase64ToBase58(b64SwapTx); + + // Create and process tip transaction + const jitoTipAccount = await this.getTipAccount(); + console.log(`Using JITO Tip Account: ${jitoTipAccount}`); + const tipTx = await this.createTipTransaction(jitoTipAccount); + const b64TipTx = tipTx.serialize().toString('base64'); + const b58TipTx = this.convertBase64ToBase58(b64TipTx); + + // Simulate and send bundle + console.log("Simulating Bundle..."); + await this.simulateBundle([b64SwapTx, b64TipTx]); + console.log('Simulation successful'); + + console.log("Sending Bundle..."); + const bundleId = await this.sendBundle([b58SwapTx, b58TipTx]); + console.log('Bundle ID:', bundleId); + + await this.pollBundleStatus(bundleId); + console.log(`Bundle landed successfully`); + console.log(`https://explorer.jito.wtf/bundle/${bundleId}`); + } catch (error) { + if (error instanceof ResponseError) { + console.error('API Error:', await error.response.json()); + } else { + console.error('Error:', error); + } + throw error; + } + } +} + +// Execute the swap +const swapManager = new JitoSwapManager(); +swapManager.executeSwap().catch(console.error); \ No newline at end of file diff --git a/solana/jupiter-jito/package.json b/solana/jupiter-jito/package.json new file mode 100644 index 0000000..8acbfa2 --- /dev/null +++ b/solana/jupiter-jito/package.json @@ -0,0 +1,20 @@ +{ + "name": "jito-ex", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@jup-ag/api": "^6.0.29", + "@solana/web3.js": "^1.95.4", + "base58": "^2.0.1" + }, + "devDependencies": { + "@types/node": "^22.8.0" + } +}