|
|
@@ -214,6 +214,7 @@ export class JupiterSwapper {
|
|
|
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
|
|
|
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // USDT
|
|
|
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB', // USD1
|
|
|
+ 'So11111111111111111111111111111111111111112', // SOL
|
|
|
];
|
|
|
|
|
|
/**
|
|
|
@@ -344,6 +345,188 @@ export class JupiterSwapper {
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Analyze close transaction and swap excess tokens to USDC
|
|
|
+ * @param {string} txSignature - Transaction signature from close position
|
|
|
+ * @param {number} keepUsdValue - USD value to keep (default: 10)
|
|
|
+ * @returns {Promise<boolean>}
|
|
|
+ */
|
|
|
+ async analyzeCloseTxAndSwapRemains(txSignature, keepUsdValue = 10) {
|
|
|
+ const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
|
+
|
|
|
+ if (!txSignature) {
|
|
|
+ logger.warn('No tx signature provided, skipping swap analysis');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info(`\n═══════════════════════════════════════════`);
|
|
|
+ logger.info(`🔍 Analyzing close transaction: ${txSignature.slice(0, 20)}...`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const tx = await this.connection.getTransaction(txSignature, {
|
|
|
+ commitment: 'confirmed',
|
|
|
+ maxSupportedTransactionVersion: 0
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!tx || !tx.meta) {
|
|
|
+ logger.error('Transaction not found or invalid');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const changedTokens = this._extractBalanceChanges(tx);
|
|
|
+ if (changedTokens.length === 0) {
|
|
|
+ logger.info('No token balance changes found');
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info(`Found ${changedTokens.length} tokens with balance changes:`);
|
|
|
+ for (const token of changedTokens) {
|
|
|
+ logger.info(` ${token.mint.slice(0, 8)}... : ${token.amount.toFixed(6)}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const mints = changedTokens.map(t => t.mint);
|
|
|
+ const prices = await this.getTokenPrices(mints);
|
|
|
+
|
|
|
+ for (const token of changedTokens) {
|
|
|
+ const mint = token.mint;
|
|
|
+ const isStablecoin = JupiterSwapper.STABLECOIN_MINTS.includes(mint);
|
|
|
+
|
|
|
+ if (isStablecoin) {
|
|
|
+ logger.info(` ${mint.slice(0, 8)}... is stablecoin/SOL, skipping swap`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentBalance = await this.getTokenBalance(mint);
|
|
|
+ if (currentBalance <= 0) {
|
|
|
+ logger.info(` ${mint.slice(0, 8)}... balance is 0, skipping`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const priceUsd = prices[mint]?.price || 0;
|
|
|
+ if (priceUsd <= 0) {
|
|
|
+ logger.warn(` ${mint.slice(0, 8)}... price not available, skipping`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const totalValueUsd = currentBalance * priceUsd;
|
|
|
+ logger.info(` ${mint.slice(0, 8)}... : balance=${currentBalance.toFixed(4)}, price=$${priceUsd.toFixed(4)}, value=$${totalValueUsd.toFixed(2)}`);
|
|
|
+
|
|
|
+ if (totalValueUsd <= keepUsdValue) {
|
|
|
+ logger.info(` Value $${totalValueUsd.toFixed(2)} <= $${keepUsdValue}, keeping all`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const swapValueUsd = totalValueUsd - keepUsdValue;
|
|
|
+ logger.info(` Swapping $${swapValueUsd.toFixed(2)} worth to USDC (keeping $${keepUsdValue})`);
|
|
|
+
|
|
|
+ const swapAmountRaw = Math.floor(swapValueUsd / priceUsd * Math.pow(10, 6));
|
|
|
+ const decimals = 6;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const tryQuote = async (restrictIntermediate = true) => {
|
|
|
+ try {
|
|
|
+ return await this.fetchQuote(mint, USDC_MINT, swapAmountRaw, 2, 'ExactIn', restrictIntermediate);
|
|
|
+ } catch (e) {
|
|
|
+ if (e.message && e.message.includes('No routes found') && restrictIntermediate) {
|
|
|
+ logger.warn('No routes with restrictIntermediate=true, retrying with allow all intermediates...');
|
|
|
+ return await this.fetchQuote(mint, USDC_MINT, swapAmountRaw, 2, 'ExactIn', false);
|
|
|
+ }
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const quoteData = await tryQuote();
|
|
|
+
|
|
|
+ if (quoteData.routePlan && quoteData.routePlan.length > 0) {
|
|
|
+ const routeLabels = quoteData.routePlan.map(r => r.swapInfo?.label || 'Unknown').join(' -> ');
|
|
|
+ logger.info(` Route: ${routeLabels}`);
|
|
|
+ logger.info(` Expected output: ${quoteData.outAmount} USDC, price impact: ${quoteData.priceImpactPct}%`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const swapData = await this.executeSwap(quoteData);
|
|
|
+ const swapTransactionBuf = Buffer.from(swapData.swapTransaction, 'base64');
|
|
|
+ const transaction = VersionedTransaction.deserialize(swapTransactionBuf);
|
|
|
+ transaction.sign([this.keypair]);
|
|
|
+
|
|
|
+ const signature = await this.connection.sendTransaction(transaction, {
|
|
|
+ maxRetries: 3,
|
|
|
+ skipPreflight: false,
|
|
|
+ });
|
|
|
+ logger.info(` Swap transaction sent: ${signature}`);
|
|
|
+
|
|
|
+ const confirmation = await this.connection.confirmTransaction(signature, 'confirmed');
|
|
|
+ if (confirmation.value.err) {
|
|
|
+ throw new Error(`Transaction failed: ${confirmation.value.err}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.success(` Swap confirmed: https://solscan.io/tx/${signature}`);
|
|
|
+ } catch (swapError) {
|
|
|
+ logger.error(` Swap failed for ${mint.slice(0, 8)}...: ${swapError.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info(`═══════════════════════════════════════════\n`);
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Error analyzing close transaction:', error.message);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Extract token balance changes from transaction
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _extractBalanceChanges(tx) {
|
|
|
+ const changedTokens = [];
|
|
|
+
|
|
|
+ if (!tx.meta?.preTokenBalances || !tx.meta?.postTokenBalances) {
|
|
|
+ return changedTokens;
|
|
|
+ }
|
|
|
+
|
|
|
+ const myWallet = this.walletAddress;
|
|
|
+ const preBalances = {};
|
|
|
+
|
|
|
+ tx.meta.preTokenBalances.forEach(balance => {
|
|
|
+ if (balance.owner === myWallet) {
|
|
|
+ const key = `${balance.accountIndex}-${balance.mint}`;
|
|
|
+ preBalances[key] = {
|
|
|
+ amount: parseFloat(balance.uiTokenAmount?.uiAmountString || 0),
|
|
|
+ decimals: balance.decimals
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ tx.meta.postTokenBalances.forEach(balance => {
|
|
|
+ if (balance.owner === myWallet) {
|
|
|
+ const key = `${balance.accountIndex}-${balance.mint}`;
|
|
|
+ const pre = preBalances[key];
|
|
|
+ const postAmount = parseFloat(balance.uiTokenAmount?.uiAmountString || 0);
|
|
|
+
|
|
|
+ if (pre) {
|
|
|
+ const diff = postAmount - pre.amount;
|
|
|
+ if (Math.abs(diff) > 0.000001) {
|
|
|
+ changedTokens.push({
|
|
|
+ mint: balance.mint,
|
|
|
+ amount: postAmount,
|
|
|
+ change: diff,
|
|
|
+ decimals: balance.decimals
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else if (postAmount > 0.000001) {
|
|
|
+ changedTokens.push({
|
|
|
+ mint: balance.mint,
|
|
|
+ amount: postAmount,
|
|
|
+ change: postAmount,
|
|
|
+ decimals: balance.decimals
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return changedTokens;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export default JupiterSwapper;
|