import { BaseStepExecutor } from '../BaseStepExecutor.js'; import type { LiFiStepExtended, StepExecutorOptions } from '../types.js'; // import { getEclipseTokens } from '../../services/getTokens.js'; // import { ECLIPSE_NATIVE_ADDRESS } from '../../utils/constants.js'; import { ChainId } from '../../types/base.js'; import { ECLIPSE_RPC_URL } from '../../utils/eclipse.js'; import type { TransactionInstruction } from '@solana/web3.js'; import { Connection, PublicKey, SystemProgram, Transaction, TransactionExpiredTimeoutError, Keypair, } from '@solana/web3.js'; import { NATIVE_MINT } from '@solana/spl-token'; import axios from 'axios'; import { API_URLS } from 'solar-sdk'; import { getAmm, getAmountOut, getPool, getSwapInstruction, LIFINITY_ECLIPSE_PROGRAM_ID, } from '@lifinity/sdk-v2-eclipse'; import { getMarketAddress, type IWallet, Market, Pair, Network, } from '@invariant-labs/sdk-eclipse'; import { createNativeAtaInstructions, createNativeAtaWithTransferInstructions, fromFee, } from '@invariant-labs/sdk-eclipse/lib/utils.js'; import { BN, web3 } from '@project-serum/anchor'; import { getRoutesInvariant } from '../../routes/Eclipse/Invariant/index.js'; import { findPairs, getAllPools, handleSwap, isSwapWithETH, MAX_CROSSES_IN_SINGLE_TX, networkTypetoProgramNetwork, NetworkType, WRAPPED_ETH_ADDRESS, getFullNewTokensData, getAllTokenAccounts, createAccount, } from '../../routes/Eclipse/Invariant/utils.js'; import type { RoutesInvariantParams } from '../../routes/Eclipse/Invariant/types.js'; import { convertBalanceToBN } from '../../routes/Eclipse/utils.js'; import { handleGetRoutesSolar } from '../../routes/Eclipse/Solar/index.js'; import { handleGetRoutesOrca, handleTrxOrca, } from '../../routes/Eclipse/Orca/index.js'; export interface EclipseStepExecutorOptions extends StepExecutorOptions { walletAdapter: any; } export class EclipseStepExecutor extends BaseStepExecutor { private walletAdapter: any; private commissionBps?: Partial>; private commissionBpsSDK?: Partial>; private slippage?: string; constructor(options: EclipseStepExecutorOptions) { super(options); this.walletAdapter = options.walletAdapter; this.commissionBps = options.executionOptions?.commissionBps; this.commissionBpsSDK = options.executionOptions?.commissionBpsSDK; this.slippage = options.executionOptions?.slippage; } async executeStep(step: LiFiStepExtended): Promise { step.execution = this.statusManager.initExecutionObject(step); const currentProcessType = 'SWAP'; // const commissionBpsSDK = this.commissionBpsSDK?.[ChainId.ECLIPSE]; // const commissionBps = this.commissionBps?.[ChainId.ECLIPSE]; let process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, }); try { process = this.statusManager.updateProcess(step, process.type, 'STARTED'); const rpcUrl = ECLIPSE_RPC_URL; const client = new Connection(rpcUrl); const fromToken = step.action.fromToken; const toToken = step.action.toToken; const inputAmount = step.action.fromAmount; // const amountInUsd = // (Number(step.action.fromAmount) / // 10 ** step.action.fromToken.decimals) * // Number(step.action.fromToken.priceUSD); const account = this.walletAdapter; // const allTokens = await getEclipseTokens(); // const nativeToken = allTokens.find( // (token) => token.address === ECLIPSE_NATIVE_ADDRESS, // ); let commissionAmount = null; // if (commissionBpsSDK && nativeToken) { // commissionAmount = Math.floor( // (((commissionBpsSDK / 10000) * amountInUsd) / // Number(nativeToken?.priceUSD)) * // 10 ** nativeToken?.decimals, // ); // } // if (commissionBps && nativeToken) { // commissionAmount = Math.floor( // (((commissionBps / 10000) * amountInUsd) / // Number(nativeToken?.priceUSD)) * // 10 ** nativeToken?.decimals, // ); // } if (step.tool === 'solar') { const txVersion: string = 'V0'; // get statistical transaction fee from api /** * vh: very high * h: high * m: medium */ const { data } = await axios.get<{ id: string; success: boolean; data: { default: { vh: number; h: number; m: number } }; }>(`${API_URLS.BASE_HOST}${API_URLS.PRIORITY_FEE}`); const [isInputSol, isOutputSol] = [ fromToken?.address === NATIVE_MINT.toBase58(), toToken?.address === NATIVE_MINT.toBase58(), ]; const swapResponse = await handleGetRoutesSolar({ tokenIn: fromToken?.address || '', tokenOut: toToken?.address || '', amountIn: BigInt(inputAmount || 0), slippage: Number(this.slippage ? this.slippage : 1 / 100) * 1e6, }); const { data: swapTransactions } = await axios.post<{ id: string; version: string; success: boolean; data: { transaction: string }[]; }>(`${API_URLS.SWAP_HOST}/transaction/swap-base-in`, { computeUnitPriceMicroLamports: String(data.data.default.h), swapResponse, txVersion, wallet: account.publicKey.toBase58(), wrapSol: isInputSol, unwrapSol: isOutputSol, // true means output mint receive sol, false means output mint received wsol inputAccount: isInputSol ? undefined : fromToken?.address, outputAccount: isOutputSol ? undefined : toToken?.address, }); if (!swapTransactions || swapTransactions.data.length === 0) { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'Not found any route from Solar Dex', }, }, ); return step; } const transaction = Transaction.from( Buffer.from(swapTransactions.data[0].transaction, 'base64'), ) as Transaction; if (commissionAmount !== null) { const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); transaction.add(transferInstruction); } const { lastValidBlockHeight, blockhash } = await client.getLatestBlockhash({ commitment: 'finalized', }); transaction.recentBlockhash = blockhash; const signedTx = await account.signTransaction(transaction); const txHash = await client.sendRawTransaction(signedTx.serialize()); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.ECLIPSE, txHash: txHash, txLink: `https://eclipsescan.xyz/tx/${txHash}`, }, ); await client.confirmTransaction( { blockhash, lastValidBlockHeight, signature: txHash, }, 'confirmed', ); process = this.statusManager.updateProcess(step, process.type, 'DONE'); this.statusManager.updateExecution(step, 'DONE'); } else if (step.tool === 'lifinity') { const fromMint: PublicKey = new PublicKey(fromToken?.address); const toMint: PublicKey = new PublicKey(toToken?.address); const ownerAccount: PublicKey = account.publicKey; const transaction = new Transaction(); const tokensAccountList = await getAllTokenAccounts( account as IWallet, client, ); const tokensAccounts = tokensAccountList.reduce( (acc: any, item: any) => { acc[item.programId] = item; return acc; }, {}, ); let fromUserAccount = tokensAccounts[fromToken?.address] ? tokensAccounts[fromToken?.address].address : await createAccount(fromMint, account as IWallet, client); let toUserAccount = tokensAccounts[toToken?.address] ? tokensAccounts[toToken?.address].address : await createAccount(toMint, account as IWallet, client); const slippage: number = Number(this.slippage); const amountIn = Number(inputAmount) / 10 ** Number(step.action.fromToken.decimals); const pool = await getPool(client as any, fromMint, toMint); const ammData = await getAmm(client as any, pool.ammPubkey); const router = await getAmountOut( client as any, ammData, amountIn, fromMint, slippage, ); const minimumOut: number = router.amountOutWithSlippage; const swapInstruction = await getSwapInstruction( client as any, ownerAccount, amountIn, minimumOut, ammData, fromMint, toMint, fromUserAccount, toUserAccount, LIFINITY_ECLIPSE_PROGRAM_ID, ); transaction.add(swapInstruction as any); if (commissionAmount !== null) { const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); transaction.add(transferInstruction); } transaction.feePayer = account.publicKey; const { lastValidBlockHeight, blockhash } = await client.getLatestBlockhash({ commitment: 'finalized', }); transaction.recentBlockhash = blockhash; const signedTx = await account.signTransaction(transaction); const txHash = await client.sendRawTransaction(signedTx.serialize()); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.ECLIPSE, txHash: txHash, txLink: `https://eclipsescan.xyz/tx/${txHash}`, }, ); await client.confirmTransaction( { blockhash, lastValidBlockHeight, signature: txHash, }, 'confirmed', ); process = this.statusManager.updateProcess(step, process.type, 'DONE'); this.statusManager.updateExecution(step, 'DONE'); } else if (step.tool === 'invariant') { const market = await Market.build( Network.MAIN, account as IWallet, client, new PublicKey(getMarketAddress(Network.MAIN)), ); const swapAmount = convertBalanceToBN( ( Number(inputAmount) / 10 ** Number(step.action.fromToken.decimals) ).toString(), step.action.fromToken.decimals, ); const thisSlippage = Number(this.slippage); const slippage = fromFee(new BN(Number(thisSlippage * 100))); const byAmountIn = true; const allPools = await getAllPools( new PublicKey(fromToken?.address || ''), new PublicKey(toToken?.address || ''), market, ); const filteredPools = findPairs( new PublicKey(fromToken?.address || ''), new PublicKey(toToken?.address || ''), allPools, ); const allTickmaps = await Promise.all( filteredPools.map(async (pool: any) => { return await market .getTickmap( new Pair(pool.tokenX, pool.tokenY, { fee: pool.fee, tickSpacing: pool.tickSpacing, }), ) .then((res: any) => { return { [pool.tickmap.toString()]: res, }; }); }), ); const tickmaps = allTickmaps.reduce((acc: any, obj: any) => { const key = Object.keys(obj)[0]; acc[key] = obj[key]; return acc; }, {}); const simulation = await getRoutesInvariant({ fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, slippage, account, fromTokenDecimals: step.action.fromToken.decimals, toTokenDecimals: step.action.toToken.decimals, } as RoutesInvariantParams); const transaction = new Transaction(); if (simulation) { const swapWithETH = await isSwapWithETH( new PublicKey(fromToken?.address), new PublicKey(toToken?.address), client, ); if (swapWithETH) { try { const tokenFrom = new PublicKey(fromToken?.address); const tokenTo = new PublicKey(toToken?.address); const selectedPool = allPools[(simulation as any).poolIndex]; if (!selectedPool) { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'Pool not found from Invariant', }, }, ); return step; } // Swapping tokens const allTokens = await getFullNewTokensData( [tokenFrom, tokenTo], client, ); const tokensAccountList = await getAllTokenAccounts( account as IWallet, client, ); const tokensAccounts = tokensAccountList.reduce( (acc: any, item: any) => { acc[item.programId] = item; return acc; }, {}, ); // Ensure tokens are in correct order as per pool configuration const isXtoY = tokenFrom.equals(selectedPool.tokenX); const [tokenX, tokenY] = isXtoY ? [tokenFrom, tokenTo] : [tokenTo, tokenFrom]; const wrappedEthAccount = Keypair.generate(); const net = networkTypetoProgramNetwork(NetworkType.Mainnet); let initialTx: Transaction; let unwrapIx: TransactionInstruction; if ( allTokens[tokenFrom.toString()].address.toString() === WRAPPED_ETH_ADDRESS ) { const { createIx, transferIx, initIx, unwrapIx: unwrap, } = createNativeAtaWithTransferInstructions( wrappedEthAccount.publicKey, account.publicKey, net, swapAmount.toNumber(), ); unwrapIx = unwrap; initialTx = new Transaction() .add(createIx) .add(transferIx) .add(initIx); } else { const { createIx, initIx, unwrapIx: unwrap, } = createNativeAtaInstructions( wrappedEthAccount.publicKey, account.publicKey, net, ); unwrapIx = unwrap; initialTx = new Transaction().add(createIx).add(initIx); } let fromAddress = allTokens[tokenFrom.toString()].address.toString() === WRAPPED_ETH_ADDRESS ? wrappedEthAccount.publicKey : tokensAccounts[tokenFrom.toString()] ? tokensAccounts[tokenFrom.toString()].address : await createAccount( tokenFrom, account as IWallet, client, ); let toAddress = allTokens[tokenTo.toString()].address.toString() === WRAPPED_ETH_ADDRESS ? wrappedEthAccount.publicKey : tokensAccounts[tokenTo.toString()] ? tokensAccounts[tokenTo.toString()].address : await createAccount(tokenTo, account as IWallet, client); const swapTx = await market.swapIx( { pair: new Pair(tokenX, tokenY, { fee: selectedPool.fee, tickSpacing: selectedPool.tickSpacing, }), owner: account.publicKey, xToY: isXtoY, amount: swapAmount, estimatedPriceAfterSwap: (simulation as any) .estimatedPriceAfterSwap, slippage, byAmountIn, accountX: isXtoY ? fromAddress : toAddress, accountY: isXtoY ? toAddress : fromAddress, }, { pool: allPools[(simulation as any).poolIndex], tickmap: tickmaps[ allPools[(simulation as any).poolIndex].tickmap.toString() ], }, { tickCrosses: MAX_CROSSES_IN_SINGLE_TX }, ); await initialTx.add(swapTx); await initialTx.add(unwrapIx); if (commissionAmount !== null) { const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); initialTx.add(transferInstruction); } const { lastValidBlockHeight, blockhash } = await client.getLatestBlockhash({ commitment: 'finalized', }); initialTx.recentBlockhash = blockhash; initialTx.feePayer = account.publicKey; // Partially sign with the wrapped ETH account first initialTx.partialSign(wrappedEthAccount); // Then sign with the user's wallet const signedTx = await account.signTransaction(initialTx); const txHash = await client.sendRawTransaction( signedTx.serialize(), { skipPreflight: false, preflightCommitment: 'confirmed', }, ); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.ECLIPSE, txHash: txHash, txLink: `https://eclipsescan.xyz/tx/${txHash}`, }, ); await client.confirmTransaction( { blockhash, lastValidBlockHeight, signature: txHash, }, 'confirmed', ); process = this.statusManager.updateProcess( step, process.type, 'DONE', ); this.statusManager.updateExecution(step, 'DONE'); } catch (error) { console.error(error); if (error instanceof TransactionExpiredTimeoutError) { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'Transaction has timed out. Check the details to confirm success.', }, }, ); } else { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: (error as any).message, }, }, ); } return step; } } else { const trx = await handleSwap( allPools, tickmaps, market, account as IWallet, client, new PublicKey(fromToken?.address), new PublicKey(toToken?.address), byAmountIn, slippage, (simulation as any).estimatedPriceAfterSwap, (simulation as any).poolIndex, swapAmount, ).catch((e: any) => { console.error('>>> e', e); process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: e.message, }, }, ); }); transaction.add(trx); if (commissionAmount !== null) { const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); transaction.add(transferInstruction); } const { lastValidBlockHeight, blockhash } = await client.getLatestBlockhash({ commitment: 'finalized', }); transaction.recentBlockhash = blockhash; // Add fee payer transaction.feePayer = account.publicKey; const signedTx = await account.signTransaction(transaction); const txHash = await client.sendRawTransaction( signedTx.serialize(), ); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.ECLIPSE, txHash: txHash, txLink: `https://eclipsescan.xyz/tx/${txHash}`, }, ); await client.confirmTransaction( { blockhash, lastValidBlockHeight, signature: txHash, }, 'confirmed', ); process = this.statusManager.updateProcess( step, process.type, 'DONE', ); this.statusManager.updateExecution(step, 'DONE'); } } else { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'Not found any routes from Invariant', }, }, ); } } else if (step.tool === 'orca') { const payload = { fromToken: fromToken?.address || '', toToken: toToken?.address || '', fromAmount: inputAmount, slippage: this.slippage || 0, account: account.publicKey.toString(), fromTokenDecimals: step.action.fromToken.decimals, toTokenDecimals: step.action.toToken.decimals, }; const router = await handleGetRoutesOrca(payload).catch((e) => { console.error('>>> e', e); throw new Error('Not found any routes from Orca'); }); const txInstructions = await handleTrxOrca(payload, router.swap); if (!txInstructions?.instructions?.length) { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'No instructions received from Orca', }, }, ); return step; } // Create instructions array including commission transfer if needed const allInstructions = [...txInstructions.instructions]; if (commissionAmount !== null) { const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); allInstructions.push(transferInstruction); } // Safe conversion for instruction data function safeInstructionData(data: any): Buffer { if (typeof data === 'string') { try { return Buffer.from(data, 'base64'); } catch { return Buffer.from(data, 'utf8'); } } if (Array.isArray(data)) { return Buffer.from(data); } if (Buffer.isBuffer(data)) { return data; } if (data instanceof Uint8Array) { return Buffer.from(data); } // If data is null, undefined, or an object, return empty buffer return Buffer.alloc(0); } // Create a v0 transaction message const messageV0 = new web3.TransactionMessage({ payerKey: account.publicKey, recentBlockhash: (await client.getLatestBlockhash()).blockhash, instructions: allInstructions.map((ix: any) => ({ programId: new PublicKey(ix.programId), keys: ix.accounts.map((acc: any) => ({ pubkey: new PublicKey(acc.pubkey), isSigner: acc.isSigner, isWritable: acc.isWritable, })), data: safeInstructionData(ix.data), })), }).compileToV0Message( // Convert lookup table accounts to AddressLookupTableAccount objects (txInstructions as any).lookupTableAccounts.map((account: any) => ({ key: new PublicKey(account), state: { addresses: [], // The addresses will be loaded from chain authority: null, deactivationSlot: BigInt(0), lastExtendedSlot: BigInt(0), lastExtendedSlotStartIndex: 0, }, })), ); // Create v0 transaction const transaction = new web3.VersionedTransaction(messageV0); // Add all required signers if (txInstructions.signers?.length) { const signers = txInstructions.signers.map((signer: any) => { // Split the signer array into public key and secret key parts const secretKeyBytes = signer.slice(0, 32); // First 32 bytes are secret key return Keypair.fromSeed(Uint8Array.from(secretKeyBytes)); }); transaction.sign(signers); } // Sign with the main account const signedTx = await account.signTransaction(transaction); // Send transaction const txHash = await client.sendTransaction(signedTx, { skipPreflight: false, preflightCommitment: 'confirmed', maxRetries: 3, }); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.ECLIPSE, txHash: txHash, txLink: `https://eclipsescan.xyz/tx/${txHash}`, }, ); // Confirm transaction await client.confirmTransaction( { signature: txHash, blockhash: messageV0.recentBlockhash, lastValidBlockHeight: (await client.getLatestBlockhash()) .lastValidBlockHeight, }, 'confirmed', ); process = this.statusManager.updateProcess(step, process.type, 'DONE'); this.statusManager.updateExecution(step, 'DONE'); } return step; } catch (e: any) { console.error('>>> e', e); process = this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { code: e.code, message: e.message, }, }); return step; } } }