UNPKG

orca-clmm-agent

Version:

Orca Whirlpool clmm library for automated position management

267 lines (266 loc) 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.manuallExecuteLIFISwap = exports.swapAssets = exports.getLIFITransactionLinks = exports.getLIFISwapQuote = void 0; const sdk_1 = require("@lifi/sdk"); const solana_1 = require("./solana"); const utils_1 = require("./utils"); const dotenv_1 = __importDefault(require("dotenv")); const kit_1 = require("@solana/kit"); const bs58_1 = __importDefault(require("bs58")); const web3_js_1 = require("@solana/web3.js"); const orca_types_1 = require("./orca.types"); const orca_1 = require("./orca"); dotenv_1.default.config(); const rpcUrl = process.env.RPC_URL || "https://api.mainnet-beta.solana.com"; (0, sdk_1.createConfig)({ integrator: process.env.LIFI_INTEGRATOR || "orca-clmm-agent", preloadChains: true, apiKey: process.env.LIFI_API_KEY, }); const NoQuoteWithinPriceImpactError = (msg) => new sdk_1.SDKError(new sdk_1.BaseError(sdk_1.ErrorName.NotFoundError, 404, msg)); const getLIFISwapQuote = async ({ fromAmount, fromTokenAddress, toTokenAddress, privateKey, slippage = 0.005, // 0.5% maxPriceImpact = 0.03, // hides with price impact >= 3% maxGasUSD = 0.1, }) => { const [fromTokenUSDPrice, toTokenUSDPrice] = await Promise.all([ (0, utils_1.getUSDPrice)({ mintAddress: fromTokenAddress }), (0, utils_1.getUSDPrice)({ mintAddress: toTokenAddress }), ]); console.log("fromTokenUSDPrice", fromTokenUSDPrice); console.log("toTokenUSDPrice", toTokenUSDPrice); if (fromTokenAddress === solana_1.SOL_MINT_ADDRESS) fromTokenAddress = (0, kit_1.address)("11111111111111111111111111111111"); if (toTokenAddress === solana_1.SOL_MINT_ADDRESS) toTokenAddress = (0, kit_1.address)("11111111111111111111111111111111"); const walletAdapter = new sdk_1.KeypairWalletAdapter(privateKey); sdk_1.config.setProviders([ (0, sdk_1.Solana)({ getWalletAdapter: async () => walletAdapter, }), ]); const walletAddress = walletAdapter.publicKey?.toString(); if (!walletAddress) { throw new Error("Failed to get wallet address"); } const quote = await (0, sdk_1.getQuote)({ fromChain: sdk_1.ChainId.SOL, toChain: sdk_1.ChainId.SOL, fromAmount, fromToken: fromTokenAddress, toToken: toTokenAddress, fromAddress: walletAddress, toAddress: walletAddress, //maxPriceImpact, slippage, skipSimulation: true, }); const { estimate, action } = quote; if (!estimate) throw new Error("[getLIFISwapQuote] Failed to get quote"); //const { toAmountUSD, fromAmountUSD } = estimate; const { toAmount } = estimate; const gasCostsUSD = estimate.gasCosts?.[0].amountUSD || 0; if (+gasCostsUSD > maxGasUSD) { throw new Error(`[getLIFISwapQuote] Gas costs exceed max ${maxGasUSD} (${gasCostsUSD})`); } // if (!toAmountUSD || !fromAmountUSD) { // throw new Error("[getLIFISwapQuote] No toAmountUSD or fromAmountUSD"); // } console.log(`[LIFI] Price Impact:`, +estimate.toAmountUSD - +estimate.fromAmountUSD); const { fromToken, toToken } = action; const toAmountUSD = (0, utils_1.convertRawToDecimal)(BigInt(toAmount), toToken.decimals) * toTokenUSDPrice; const fromAmountUSD = (0, utils_1.convertRawToDecimal)(BigInt(fromAmount), fromToken.decimals) * fromTokenUSDPrice; console.log("Difference in toAmountUSD", +quote.estimate.toAmountUSD / toAmountUSD); quote.estimate.fromAmountUSD = fromAmountUSD.toString(); quote.estimate.toAmountUSD = toAmountUSD.toString(); const priceImpact = (+toAmountUSD - +fromAmountUSD) / +fromAmountUSD; if (priceImpact < maxPriceImpact * -1) { const maxPriceImpactPct = (maxPriceImpact * 100).toFixed(3); const priceImpactPct = (priceImpact * 100).toFixed(3); console.error(`Price impact ${priceImpactPct}% exceeds max ${maxPriceImpactPct}%`); console.log(`From: $${fromAmountUSD} -> To: $${toAmountUSD}`); throw NoQuoteWithinPriceImpactError(`Price impact exceeds max ${maxPriceImpactPct}%`); } const fees = quote.estimate.feeCosts?.filter((fee) => fee.amount !== "0") || []; //console.log("Fees:", fees); if (fees.some((fee) => fee.name !== "Rent Exemption Deposit")) { console.warn("Quote includes non-rent exemption deposit fees"); } return quote; }; exports.getLIFISwapQuote = getLIFISwapQuote; const getLIFITransactionLinks = (route) => { const transactionLinks = []; route.steps.forEach((step, index) => { step.execution?.process.forEach((process) => { if (process.txHash) { transactionLinks.push(process.txHash); } }); }); return transactionLinks; }; exports.getLIFITransactionLinks = getLIFITransactionLinks; const swapAssets = async ({ rpc, fromAmount, fromTokenAddress, toTokenAddress, walletByteArray, slippage = 0.005, // 0.5% maxPriceImpact = 0.02, // 2% maxRetries = 5, confirmation = "finalized", maxGasUSD, }) => { let retries = 0; while (retries < maxRetries) { try { console.log(`[swapAssets] Getting quote to swap ${fromAmount} with max price impact ${maxPriceImpact}`); //console.log(`From token: ${fromTokenAddress}`); //console.log(`To token: ${toTokenAddress}`); const privateKey = bs58_1.default.encode(walletByteArray); const wallet = await (0, kit_1.createKeyPairSignerFromBytes)(walletByteArray); const quote = await (0, exports.getLIFISwapQuote)({ fromAmount, fromTokenAddress, toTokenAddress, privateKey, slippage, maxPriceImpact, maxGasUSD, }); if (!quote.transactionRequest) throw new Error("[swapAssets] No transaction request"); const gasCostsUSD = quote.estimate.gasCosts?.[0].amountUSD; console.log("gasCosts for swap in USD:", gasCostsUSD, quote.estimate.gasCosts?.[0].amount); //TODO: simluate trx to properly calculate price impact? const signature = await (0, exports.manuallExecuteLIFISwap)(quote.transactionRequest, privateKey); const route = (0, sdk_1.convertQuoteToRoute)(quote); const fromUSD = +(quote.estimate.fromAmountUSD || 0); const toUSD = +(quote.estimate.toAmountUSD || 0); const priceImpact = (toUSD - fromUSD) / fromUSD; console.log(`${route.fromToken.symbol} -> ${route.toToken.symbol}:`); console.log(`From: $${fromUSD} -> To: $${toUSD}`); //console.log("Expected to amount", quote.estimate.toAmount); console.log(`Price Impact: $${toUSD - fromUSD}`); console.log(`Price Impact: ${(priceImpact * 100).toFixed(3)}%`); console.log("txHash: ", signature); await (0, solana_1.awaitTransactionStatus)(rpc, signature, confirmation); const details = await (0, solana_1.getTransactionDetails)(rpc, signature); const toTokenChange = details.changes.find((c) => c.mint === toTokenAddress && c.owner === wallet.address); if (!toTokenChange) throw new Error("No change in to token found"); const actualToAmount = toTokenChange.changeDecimal; const toTokenUSDPrice = await (0, utils_1.getUSDPrice)({ mintAddress: toTokenAddress }); const actualToUSD = actualToAmount * toTokenUSDPrice; console.log("toTokenUSDPrice", toTokenUSDPrice); //console.log("Actual to amount", actualToAmount); console.log(`From: $${fromUSD} -> To: $${actualToUSD}`); console.log("Actual Price Impact: $", actualToUSD - fromUSD, " token received", actualToAmount); const feesUSD = quote.estimate.feeCosts?.reduce((acc, fee) => acc + +fee.amountUSD, 0) || 0; console.log("Fees for swap: $", feesUSD); details.feeUSD += feesUSD; return { ...details, valueLoss: actualToUSD - fromUSD, priceImpact, }; } catch (error) { retries++; if (error instanceof sdk_1.SDKError) { if (error.message.includes("No available quotes")) { console.log(`[swapAssets] No quote found`); } else if (error.message.includes("Price impact exceeds max")) { console.log(`[swapAssets] Price impact exceeds max`); } else if (error.message.includes("Too Many Requests")) { console.log(`[swapAssets] Too many requests`); const minutes = error.message.match(/retry in (\d+) minute/); const hours = error.message.match(/retry in (\d+) hour/); if (minutes) { console.log(`[swapAssets] Retrying in ${minutes[1]} minutes`); await (0, utils_1.sleep)(1000 * 60 * Number(minutes[1])); continue; } if (hours) { console.log(`[swapAssets] Retrying in ${hours[1]} hours`); await (0, utils_1.sleep)(1000 * 60 * 60 * Number(hours[1])); continue; } } else console.error(`[swapAssets] Failed to execute swap: ${error}`); await (0, utils_1.sleep)(1000 * 25); continue; } if (error instanceof Error) { if (error.message.includes("Gas costs exceed max")) { console.log(error.message); await (0, utils_1.sleep)(1000 * 5); continue; } if (error.message.includes("Transaction timed out")) { continue; } } if (error instanceof kit_1.SolanaError) { const code = error.context?.code; if (code === solana_1.SLIPPAGE_EXCEEDED_ERROR || code === Number(solana_1.SLIPPAGE_EXCEEDED_ERROR)) { console.log("[swapAssets] Slippage exceeded error"); await (0, utils_1.sleep)(1000 * 5); continue; } if (code === solana_1.COMPUTATIONAL_BUDGET_EXCEEDED_ERROR) { console.log("[swapAssets] Computational budget exceeded error"); await (0, utils_1.sleep)(1000 * 5); continue; } if (error.message.includes("Program failed to complete")) { // probably missing priority fee console.log("[swapAssets] Program failed to complete"); await (0, utils_1.sleep)(1000 * 5); continue; } } if (error instanceof orca_types_1.OrcaError) { if (error.code === orca_1.INVALID_START_TICK_ERROR) { console.log("[swapAssets] Orca Invalid start tick error"); await (0, utils_1.sleep)(1000 * 5); continue; } } console.error("unhandled error in swapAssets", error); throw error; } } throw new Error(`[swapAssets] Failed to execute swap after ${maxRetries} retries, seems no quotes available`); }; exports.swapAssets = swapAssets; const manuallExecuteLIFISwap = async (transactionRequest, privateKey) => { try { const keypair = web3_js_1.Keypair.fromSecretKey(bs58_1.default.decode(privateKey)); // get Solana keypair if (!transactionRequest.data) throw new Error("[manuallExecuteLIFISwap] No transaction data"); const connection = new web3_js_1.Connection(rpcUrl, "confirmed"); // create a connection const decodedTx = (0, kit_1.getBase64Encoder)().encode(transactionRequest.data); const deserializedTx = web3_js_1.VersionedTransaction.deserialize(decodedTx); // deserialize decoded tx data into VersionedTransaction deserializedTx.sign([keypair]); // sign the tx with your keypair // Abort if total required fee exceeds user-defined max // const MAX_FEE_LAMPORTS = 150_000; // ≈ $0.01 at $150/SOL // try { // const feeCheckResp = await connection.getFeeForMessage(deserializedTx.message, "confirmed"); // const totalFeeLamports = feeCheckResp.value ?? 0; // if (totalFeeLamports > MAX_FEE_LAMPORTS) { // throw new Error(`[manuallExecuteLIFISwap] Required fee ${totalFeeLamports} lamports exceeds max ${MAX_FEE_LAMPORTS}`); // } // } catch (feeErr) { // console.warn("[manuallExecuteLIFISwap] Fee check failed:", feeErr); // } const signature = await connection.sendTransaction(deserializedTx, { skipPreflight: true, preflightCommitment: "confirmed", }); return signature; } catch (error) { console.error(`[manuallExecuteLIFISwap] Failed to execute swap: ${error}`); throw new kit_1.SolanaError(kit_1.SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, error); } }; exports.manuallExecuteLIFISwap = manuallExecuteLIFISwap;