import { BaseStepExecutor } from '../BaseStepExecutor.js'; import type { LiFiStepExtended, StepExecutorOptions } from '../types.js'; import { ChainId } from '../../types/base.js'; import { SOLANA_RPC_URL } from '../../utils/svm.js'; import { Connection, Keypair, PublicKey, SystemProgram, Transaction, VersionedTransaction, } from '@solana/web3.js'; import axios from 'axios'; import { web3 } from '@project-serum/anchor'; import { getRoutesJup } from '../../routes/Svm/Jup/index.js'; import { getRoutesOrca } from '../../routes/Svm/Orca/index.js'; export interface SolanaStepExecutorOptions extends StepExecutorOptions { walletAdapter: any; } export class SolanaStepExecutor extends BaseStepExecutor { private walletAdapter: any; // private commissionBps?: Partial>; // private commissionBpsSDK?: Partial>; constructor(options: SolanaStepExecutorOptions) { super(options); this.walletAdapter = options.walletAdapter; // this.commissionBps = options.executionOptions?.commissionBps; // this.commissionBpsSDK = options.executionOptions?.commissionBpsSDK; } async executeStep(step: LiFiStepExtended): Promise { step.execution = this.statusManager.initExecutionObject(step); const currentProcessType = 'SWAP'; // const commissionBpsSDK = this.commissionBpsSDK?.[ChainId.SOL]; // const commissionBps = this.commissionBps?.[ChainId.SOL]; let process = this.statusManager.findOrCreateProcess({ step, type: currentProcessType, }); try { process = this.statusManager.updateProcess(step, process.type, 'STARTED'); const rpcUrl = SOLANA_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 === 'jupiter') { const formatFromTokenCA = fromToken?.address === '11111111111111111111111111111111' ? 'So11111111111111111111111111111111111111112' : fromToken?.address; const formatToTokenCA = toToken?.address === '11111111111111111111111111111111' ? 'So11111111111111111111111111111111111111112' : toToken?.address; const payload = { fromToken: formatFromTokenCA, toToken: formatToTokenCA, fromAmount: typeof inputAmount === 'object' && (inputAmount as any).c ? (inputAmount as any).c[0].toString() : inputAmount.toString(), slippage: step.action.slippage * 10000 || 0, }; const router = await getRoutesJup(payload).catch((e) => { console.error('>>> e', e); throw new Error('Not found any routes from Jupiter'); }); const swapRequestBody = { quoteResponse: router, userPublicKey: account?.publicKey?.toString(), dynamicComputeUnitLimit: true, dynamicSlippage: true, prioritizationFeeLamports: { priorityLevelWithMaxLamports: { maxLamports: 1000000, priorityLevel: 'veryHigh', }, }, }; const swapResponse = await axios.post( 'https://api.jup.ag/swap/v1/swap', swapRequestBody, ); if (swapResponse && swapResponse?.data) { const swapTransactionBuf = Buffer.from( swapResponse.data.swapTransaction, 'base64', ); const tx = VersionedTransaction.deserialize(swapTransactionBuf); if (commissionAmount !== null) { // For versioned transactions, we need to create a new regular transaction for the commission const commissionTx = new Transaction(); const transferInstruction = SystemProgram.transfer({ fromPubkey: account.publicKey, toPubkey: new PublicKey( 'Bh75eEDksaoBV9GBf7iJU9EaWQUixUyC52rMr4BH3BH6', ), lamports: commissionAmount, }); commissionTx.add(transferInstruction); const { blockhash: commissionBlockhash } = await client.getLatestBlockhash({ commitment: 'finalized' }); commissionTx.recentBlockhash = commissionBlockhash; commissionTx.feePayer = account.publicKey; const signedCommissionTx = await account.signTransaction(commissionTx); if (!signedCommissionTx) { throw new Error('Failed to sign commission transaction'); } await client.sendRawTransaction(signedCommissionTx.serialize()); } // Sign and send the main versioned transaction const signedTx = await account.signTransaction(tx); if (!signedTx) { throw new Error('Failed to sign transaction'); } const txHash = await client.sendRawTransaction(signedTx.serialize()); process = this.statusManager.updateProcess( step, process.type, 'PENDING', { chainId: ChainId.SOL, txHash: txHash, txLink: `https://solscan.io/tx/${txHash}`, }, ); const { blockhash, lastValidBlockHeight } = await client.getLatestBlockhash({ commitment: 'finalized', }); 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 Jupiter', }, }, ); } } else if (step.tool === 'orca') { // Orca const formatFromTokenCA = fromToken?.address === '11111111111111111111111111111111' ? 'So11111111111111111111111111111111111111112' : fromToken?.address; const formatToTokenCA = toToken?.address === '11111111111111111111111111111111' ? 'So11111111111111111111111111111111111111112' : toToken?.address; const payload = { fromToken: formatFromTokenCA, toToken: formatToTokenCA, fromAmount: typeof inputAmount === 'object' && (inputAmount as any).c ? (inputAmount as any).c[0].toString() : inputAmount.toString(), slippage: step.action.slippage * 10000 || 0, wallet: account?.publicKey?.toString(), }; const router = await getRoutesOrca(payload).catch((e) => { console.error('>>> e', e); throw new Error('Not found any routes from Orca'); }); const swapRequestBody = { amountIsInput: true, swap: router?.data?.swap, wallet: account?.publicKey?.toString(), slippage: step.action.slippage / 100 || '0.01', }; const swapResponse = await axios .post( 'https://pools-api.mainnet.orca.so/swap-prepare-instructions', swapRequestBody, ) .then((res) => res?.data?.data); if (!swapResponse?.instructions?.length) { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'No instructions received from Orca', }, }, ); return step; } if (swapResponse) { // Create instructions array including commission transfer if needed const allInstructions = [...swapResponse.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 (swapResponse 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 (swapResponse.signers?.length) { const signers = swapResponse.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.SOL, txHash: txHash, txLink: `https://solscan.io/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'); } else { process = this.statusManager.updateProcess( step, process.type, 'FAILED', { error: { code: -1, message: 'Not found any routes from Orca', }, }, ); } } return step; } catch (e: any) { console.error('>>> e', e); /* eslint-disable @typescript-eslint/no-unused-vars */ process = this.statusManager.updateProcess(step, process.type, 'FAILED', { error: { code: e.code, message: e.message, }, }); return step; } } }