orca-clmm-agent
Version:
Orca Whirlpool clmm library for automated position management
239 lines (238 loc) • 12.1 kB
JavaScript
;
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");
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);
console.log("fromTokenAddress", fromTokenAddress);
console.log("toTokenAddress", toTokenAddress);
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 || [];
//console.log("Fees:", fees);
if (fees.some((fee) => fee.name !== "Rent Exemption Deposit")) {
console.warn("Fees:", fees);
//throw new Error("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);
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;
}
}
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;