@solsdk/liquidity_sdk
Version:
Orca Whirlpool clmm library for automated position management
304 lines (303 loc) • 11.6 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 utils_2 = require("@nealireverse_dev/utils");
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,
}) => {
utils_2.Keypair.from(bs58_1.default.decode(privateKey));
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 {
utils_2.Keypair.from(bs58_1.default.decode(privateKey));
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;