mirror of
https://github.com/mortdeus/legacy-cc.git
synced 2026-09-24 21:05:05 +00:00
284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
import axios from "axios";
|
|
import { Keypair, Connection, LAMPORTS_PER_SOL, VersionedTransaction, SystemProgram, PublicKey, TransactionMessage, sendAndConfirmRawTransaction } from "@solana/web3.js";
|
|
import * as anchor from "@coral-xyz/anchor";
|
|
import { readFile } from "fs/promises";
|
|
import { Wallet } from "@project-serum/anchor";
|
|
import dotenv from "dotenv";
|
|
import chalk from "chalk";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import bs58 from "bs58";
|
|
import { getSplTokenBalance } from "./fuc.js";
|
|
import { decodedPrivateKey } from "./index.js";
|
|
import { logToFile } from "./logger.js";
|
|
dotenv.config();
|
|
|
|
// SWAP_METHOD: "0slot", "nozomi", "race", "solana"
|
|
const SWAP_METHOD = (process.env.SWAP_METHOD || "solana").toLowerCase();
|
|
|
|
const NOZOMI_URL = process.env.NOZOMI_URL;
|
|
const NOZOMI_UUID = process.env.NOZOMI_UUID;
|
|
const nozomiConnection = new Connection(`${NOZOMI_URL}?c=${NOZOMI_UUID}`);
|
|
|
|
const NOZOMI_TIP_LAMPORTS = Number(process.env.NOZOMI_TIP_LAMPORTS || 200000);
|
|
const JITO_TIP_LAMPORTS = Number(process.env.JITO_TIP || 100000);
|
|
const PRIORITIZATION_FEE_LAMPORTS = Number(process.env.PRIORITIZATION_FEE_LAMPORTS || 10000);
|
|
|
|
const NOZOMI_TIP_ADDRESS = new PublicKey("TEMPaMeCRFAS9EKF53Jd6KpHxgL47uWLcpFArU1Fanq");
|
|
|
|
export const MAX_RETRIES = parseInt(process.env.MAX_RETRIES) || 3;
|
|
|
|
export const decodePrivateKey = (secretKeyString) =>{
|
|
try {
|
|
// Try base58 first
|
|
return bs58.decode(secretKeyString);
|
|
} catch (error) {
|
|
try {
|
|
// Try base64
|
|
return Buffer.from(secretKeyString, 'base64');
|
|
} catch (base64Error) {
|
|
try {
|
|
// Try JSON array (for array format)
|
|
const jsonArray = JSON.parse(secretKeyString);
|
|
return new Uint8Array(jsonArray);
|
|
} catch (jsonError) {
|
|
throw new Error('Invalid private key format. Supported formats: base58, base64, or JSON array');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const loadwallet = async () => {
|
|
const privateKey = decodedPrivateKey;
|
|
if (!privateKey) {
|
|
throw new Error("PRIVATE_KEY not found in environment variables");
|
|
}
|
|
try {
|
|
const privateKeyBytes=decodePrivateKey(privateKey)
|
|
const keypair = Keypair.fromSecretKey(privateKeyBytes);
|
|
|
|
if (!keypair) {
|
|
throw new Error("Failed to create Keypair from the provided private key");
|
|
}
|
|
|
|
const wallet = new Wallet(keypair);
|
|
wallet.keypair = keypair;
|
|
return wallet;
|
|
} catch (error) {
|
|
console.error("Error loading wallet:", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const rpc_connection = () => {
|
|
return new Connection(process.env.RPC_URL, "confirmed");
|
|
};
|
|
|
|
const getResponse = async (tokenA, tokenB, amount, slippageBps, anchorWallet) => {
|
|
const quoteResponse = (
|
|
await axios.get(
|
|
`https://quote-api.jup.ag/v6/quote?inputMint=${tokenA}&outputMint=${tokenB}&amount=${amount}&slippageBps=${slippageBps}`
|
|
)
|
|
).data;
|
|
|
|
// Build swap request body based on SWAP_METHOD
|
|
let swapRequestBody = {
|
|
quoteResponse,
|
|
userPublicKey: anchorWallet.publicKey.toString(),
|
|
wrapAndUnwrapSol: true,
|
|
dynamicComputeUnitLimit: true,
|
|
};
|
|
|
|
// Map SWAP_METHOD string to behavior
|
|
if (SWAP_METHOD === "solana" || SWAP_METHOD === "0slot") {
|
|
// Standard prioritization fee
|
|
swapRequestBody.prioritizationFeeLamports = PRIORITIZATION_FEE_LAMPORTS;
|
|
} else if (SWAP_METHOD === "race") {
|
|
// JITO tip
|
|
swapRequestBody.prioritizationFeeLamports = { jitoTipLamports: JITO_TIP_LAMPORTS };
|
|
}
|
|
// "nozomi" handled in executeTransaction
|
|
|
|
const swapResponse = await axios.post(`https://quote-api.jup.ag/v6/swap`, swapRequestBody);
|
|
return swapResponse.data;
|
|
};
|
|
|
|
const executeTransaction = async (connection, swapTransaction, anchorWallet) => {
|
|
try {
|
|
if (!anchorWallet?.keypair) {
|
|
throw new Error("Invalid anchorWallet: keypair is undefined");
|
|
}
|
|
|
|
const transaction = VersionedTransaction.deserialize(Buffer.from(swapTransaction, "base64"));
|
|
transaction.sign([anchorWallet.keypair]);
|
|
|
|
let newMessage, newTransaction, rawTransaction, txid, timestart;
|
|
let blockhash = await connection.getLatestBlockhash();
|
|
|
|
if (SWAP_METHOD === "nozomi") {
|
|
let message = transaction.message;
|
|
let addressLookupTableAccounts = await loadAddressLookupTablesFromMessage(message, connection);
|
|
let txMessage = TransactionMessage.decompile(message, { addressLookupTableAccounts });
|
|
// Add Nozomi tip instruction
|
|
let nozomiTipIx = SystemProgram.transfer({
|
|
fromPubkey: anchorWallet.publicKey,
|
|
toPubkey: NOZOMI_TIP_ADDRESS,
|
|
lamports: NOZOMI_TIP_LAMPORTS,
|
|
});
|
|
txMessage.instructions.push(nozomiTipIx);
|
|
|
|
newMessage = txMessage.compileToV0Message(addressLookupTableAccounts);
|
|
newMessage.recentBlockhash = blockhash.blockhash;
|
|
|
|
newTransaction = new VersionedTransaction(newMessage);
|
|
newTransaction.sign([anchorWallet.keypair]);
|
|
|
|
rawTransaction = newTransaction.serialize();
|
|
timestart = Date.now();
|
|
txid = await nozomiConnection.sendRawTransaction(rawTransaction, {
|
|
skipPreflight: false,
|
|
maxRetries: 2,
|
|
});
|
|
|
|
console.log("Nozomi response: txid: %s", txid);
|
|
} else {
|
|
// Standard/JITO/0slot/solana/race: send via normal connection
|
|
const currentUTC = new Date();
|
|
rawTransaction = transaction.serialize();
|
|
timestart = Date.now();
|
|
txid = await sendAndConfirmRawTransaction(connection, Buffer.from(rawTransaction), {
|
|
skipPreflight: false,
|
|
maxRetries: 1,
|
|
});
|
|
|
|
console.log("Standard/JITO/0slot/solana/race response: txid: %s", txid);
|
|
const endUTC = new Date();
|
|
const timeTaken = endUTC.getTime() - currentUTC.getTime();
|
|
console.log(`⏱️ confirm time taken: ${timeTaken}ms (${(timeTaken / 1000).toFixed(2)}s)`);
|
|
return txid;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("Transaction execution error:", error);
|
|
console.log(chalk.red("Transaction reconfirm after 1s!"));
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
};
|
|
|
|
async function loadAddressLookupTablesFromMessage(message, connection) {
|
|
let addressLookupTableAccounts = [];
|
|
for (let lookup of message.addressTableLookups) {
|
|
let lutAccounts = await connection.getAddressLookupTable(lookup.accountKey);
|
|
addressLookupTableAccounts.push(lutAccounts.value);
|
|
}
|
|
return addressLookupTableAccounts;
|
|
}
|
|
|
|
export const getBalance = async () => {
|
|
const startTime = Date.now();
|
|
const startUTC = new Date(startTime).toISOString();
|
|
const connection = rpc_connection();
|
|
const walletInstance = await loadwallet();
|
|
const balance = await connection.getBalance(walletInstance.publicKey);
|
|
const endTime = Date.now();
|
|
const endUTC = new Date(endTime).toISOString();
|
|
const diffMs = endTime - startTime;
|
|
const diffSec = (diffMs / 1000).toFixed(3);
|
|
console.log(`[${endUTC}] ( duration: ${diffMs}ms / ${diffSec}s) balance =>`, balance / LAMPORTS_PER_SOL, "SOL");
|
|
return balance / LAMPORTS_PER_SOL;
|
|
};
|
|
|
|
export const swap = async (action, mint, amount) => {
|
|
const SOL_ADDRESS = "So11111111111111111111111111111111111111112";
|
|
const RETRY_DELAY = Number(process.env.RETRY_DELAY) || 1000; // fallback to 1s if not set
|
|
|
|
try {
|
|
const currentUTC = new Date();
|
|
console.log(`⌛⌛⌚⌚Starting swap at ${currentUTC.toUTCString()} (${currentUTC.getTime()}ms)`);
|
|
const connection = rpc_connection();
|
|
const wallet = await loadwallet();
|
|
|
|
// Determine tokenA and tokenB based on action and mint
|
|
let tokenA, tokenB;
|
|
if (action === "BUY") {
|
|
tokenA = SOL_ADDRESS;
|
|
tokenB = mint;
|
|
} else if (action === "SELL") {
|
|
tokenA = mint;
|
|
tokenB = SOL_ADDRESS;
|
|
} else {
|
|
throw new Error(`Unknown action: ${action}`);
|
|
}
|
|
|
|
console.log(`Swapping ${amount} of ${tokenA} for ${tokenB}...`);
|
|
let retryCount = 0;
|
|
while (retryCount <= MAX_RETRIES) {
|
|
try {
|
|
console.log(`Attempt ${retryCount + 1}/${MAX_RETRIES + 1}`);
|
|
// If this is a sell (tokenA is not SOL and tokenB is SOL), and retryCount > 1, check tokenA balance before proceeding
|
|
if (
|
|
retryCount > 1 &&
|
|
tokenA !== SOL_ADDRESS &&
|
|
tokenB === SOL_ADDRESS
|
|
) {
|
|
const balance = await getSplTokenBalance(tokenA);
|
|
console.log(`(Retry #${retryCount}) Current tokenA (${tokenA}) balance:`, balance, "Requested amount:", amount);
|
|
if (balance <= 0) {
|
|
console.log(`No balance for tokenA (${tokenA}) to sell. Aborting swap.`);
|
|
return "stop";
|
|
}
|
|
if (amount > balance) {
|
|
console.log(`Requested amount (${amount}) exceeds available balance (${balance}) for tokenA (${tokenA}). Adjusting amount to available balance.`);
|
|
amount = balance;
|
|
}
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const quoteData = await getResponse(tokenA, tokenB, amount, process.env.SLIPPAGE_BPS || "50", wallet);
|
|
const endTime = Date.now();
|
|
console.log(chalk.blue(`getResponse took ${endTime - startTime}ms (${((endTime - startTime) / 1000).toFixed(2)}s)`));
|
|
|
|
if (!quoteData?.swapTransaction) {
|
|
throw new Error("Failed to get swap transaction data");
|
|
}
|
|
|
|
const startExecTime = Date.now();
|
|
const txid = await executeTransaction(connection, quoteData.swapTransaction, wallet);
|
|
const endExecTime = Date.now();
|
|
console.log(
|
|
chalk.blue(`executeTransaction took ${endExecTime - startExecTime}ms (${((endExecTime - startExecTime) / 1000).toFixed(2)}s)`)
|
|
);
|
|
|
|
if (!txid) {
|
|
throw new Error("Transaction was not confirmed");
|
|
}
|
|
|
|
console.log(`--------------------------------------------------------\n
|
|
✌✌✌Swap successful! ${tokenA} for ${tokenB}`);
|
|
console.log(`https://solscan.io/tx/${txid}\n`);
|
|
const endUTC = new Date();
|
|
const timeTaken = endUTC.getTime() - currentUTC.getTime();
|
|
console.log(`⏱️ Total time taken: ${timeTaken}ms (${(timeTaken / 1000).toFixed(2)}s)`);
|
|
|
|
return txid;
|
|
} catch (error) {
|
|
console.error(`Attempt ${retryCount + 1} failed:`, error.message);
|
|
retryCount++;
|
|
|
|
if (retryCount > MAX_RETRIES) {
|
|
console.error(`Transaction failed after ${MAX_RETRIES + 1} attempts.`);
|
|
throw error;
|
|
}
|
|
|
|
console.warn(`Retrying in ${RETRY_DELAY / 1000} seconds (${retryCount}/${MAX_RETRIES})...`);
|
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Swap failed:", error.message);
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
};
|