import type { ParsedAccountData, AccountInfo } from '@solana/web3.js'; import { Connection, PublicKey, SystemProgram, Transaction, TransactionExpiredTimeoutError, TransactionInstruction, } from '@solana/web3.js'; import { type Market, Network, Pair } from '@invariant-labs/sdk-eclipse'; import { FEE_TIERS, getTokenProgramAddress, } from '@invariant-labs/sdk-eclipse/lib/utils.js'; import { parsePool, TOKEN_2022_PROGRAM_ID, } from '@invariant-labs/sdk-eclipse/lib/market.js'; import type { RawPoolStructure } from '@invariant-labs/sdk-eclipse/lib/market.js'; import { ECLIPSE_RPC_URL } from '../../../utils/eclipse.js'; import { BN } from '@project-serum/anchor'; import axios from 'axios'; import { Metaplex } from '@metaplex-foundation/js'; import { getMint, type Mint, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddress, } from '@solana/spl-token'; export const findPairs = ( tokenFrom: PublicKey, tokenTo: PublicKey, pairs: any[], ) => { return pairs.filter( (pool) => (tokenFrom.equals(pool.tokenX) && tokenTo.equals(pool.tokenY)) || (tokenFrom.equals(pool.tokenY) && tokenTo.equals(pool.tokenX)), ); }; const getPoolsFromAddresses = async ( addresses: PublicKey[], marketProgram: Market, ): Promise => { try { const pools = (await marketProgram.program.account.pool.fetchMultiple( addresses, )) as Array; return pools .filter((pool) => !!pool) .map((pool, index) => { return { ...parsePool(pool), address: addresses[index], }; }); } catch (error) { console.error(error); return []; } }; const getPools = async (pairs: Pair[], market: Market): Promise => { try { const addresses: PublicKey[] = await Promise.all( pairs.map( async (pair) => await pair.getAddress(market.program.programId), ), ); return await getPoolsFromAddresses(addresses, market); } catch (error) { console.error(error); return []; } }; const fetchAllPoolsForPairData = async (pairs: any, market: any) => { try { const pools = await getPools(pairs, market); return pools; } catch (error) { console.error(error); return []; } }; export const getAllPools = async ( tokenFromPublicKey: PublicKey, tokenToPublicKey: PublicKey, market: Market, ) => { const pairs = FEE_TIERS.map( (fee) => new Pair(tokenFromPublicKey, tokenToPublicKey, fee), ); const listPools = await fetchAllPoolsForPairData(pairs, market); return listPools; }; export const MAX_CROSSES_IN_SINGLE_TX = 11; export const WRAPPED_ETH_ADDRESS = 'So11111111111111111111111111111111111111112'; function buildAssociatedTokenAccountInstruction( payer: PublicKey, associatedToken: PublicKey, owner: PublicKey, mint: PublicKey, instructionData: Buffer, programId = TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, ): TransactionInstruction { const keys = [ { pubkey: payer, isSigner: true, isWritable: true }, { pubkey: associatedToken, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: false, isWritable: false }, { pubkey: mint, isSigner: false, isWritable: false }, { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, { pubkey: programId, isSigner: false, isWritable: false }, ]; return new TransactionInstruction({ keys, programId: associatedTokenProgramId, data: instructionData, }); } function createAssociatedTokenAccountInstruction( payer: PublicKey, associatedToken: PublicKey, owner: PublicKey, mint: PublicKey, programId = TOKEN_PROGRAM_ID, associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID, ): TransactionInstruction { return buildAssociatedTokenAccountInstruction( payer, associatedToken, owner, mint, Buffer.alloc(0), programId, associatedTokenProgramId, ); } async function signAndSend(wallet: any, tx: any, rpcUrl: string) { const connection = new Connection(rpcUrl); const { blockhash } = await connection.getLatestBlockhash({ commitment: 'finalized', }); (tx as any).feePayer = wallet.publicKey; (tx as any).recentBlockhash = blockhash; const signedTx = (await wallet.signTransaction(tx)) as Transaction; const signature = await connection.sendRawTransaction(signedTx.serialize()); return signature; } export async function createAccount( tokenAddress: PublicKey, wallet: any, connection: any, ) { const programId = await getTokenProgramAddress( connection, new PublicKey(tokenAddress), ); const associatedAccount = await getAssociatedTokenAddress( tokenAddress, wallet.publicKey, false, programId, ASSOCIATED_TOKEN_PROGRAM_ID, ); const ix = createAssociatedTokenAccountInstruction( wallet.publicKey, associatedAccount, wallet.publicKey, tokenAddress, programId, ASSOCIATED_TOKEN_PROGRAM_ID, ); await signAndSend(wallet, new Transaction().add(ix), ECLIPSE_RPC_URL); return associatedAccount; } interface TokenAccountInfo { pubkey: PublicKey; account: AccountInfo; } interface ITokenAccount { programId: PublicKey; balance: BN; address: PublicKey; decimals: number; } interface IparsedTokenInfo { mint: string; owner: string; tokenAmount: { amount: string; decimals: number; uiAmount: number; }; } export async function getAllTokenAccounts(wallet: any, connection: any) { const [splTokensAccounts, token2022TokensAccounts] = await Promise.all([ connection.getParsedTokenAccountsByOwner(wallet.publicKey, { programId: TOKEN_PROGRAM_ID, }), connection.getParsedTokenAccountsByOwner(wallet.publicKey, { programId: TOKEN_2022_PROGRAM_ID, }), ]); const mergedAccounts: TokenAccountInfo[] = [ ...splTokensAccounts.value, ...token2022TokensAccounts.value, ]; const newAccounts: ITokenAccount[] = []; for (const account of mergedAccounts) { const info: IparsedTokenInfo = account.account.data.parsed.info; newAccounts.push({ programId: new PublicKey(info.mint), balance: new BN(info.tokenAmount.amount), address: account.pubkey, decimals: info.tokenAmount.decimals, }); } return newAccounts; } //// Get token onchain info const getTokenProgramId = async ( connection: Connection, address: PublicKey, ): Promise => { return await getTokenProgramAddress(connection, address); }; async function getTokenMetadata( connection: Connection, address: string, decimals: number, tokenProgram?: PublicKey, ) { const mintAddress = new PublicKey(address); try { const metaplex = new Metaplex(connection); const nft = await metaplex.nfts().findByMint({ mintAddress }); const irisTokenData = await axios.get(nft.uri).then((res) => res.data); return { tokenProgram, address: mintAddress, decimals, symbol: nft?.symbol || irisTokenData?.symbol || `${address.slice(0, 2)}...${address.slice(-4)}`, name: nft?.name || irisTokenData?.name || address, logoURI: nft?.json?.image || irisTokenData?.image || '/unknownToken.svg', isUnknown: true, }; } catch (error) { return { tokenProgram, address: mintAddress, decimals, symbol: `${address.slice(0, 2)}...${address.slice(-4)}`, name: address, logoURI: '/unknownToken.svg', isUnknown: true, }; } } export const getFullNewTokensData = async ( addresses: PublicKey[], connection: Connection, ) => { const promises: Promise<[PublicKey, Mint]>[] = addresses.map( async (address) => { const programId = await getTokenProgramId(connection, address); return [ programId, await getMint(connection, address, undefined, programId), ] as [PublicKey, Mint]; }, ); const tokens: Record = {}; const results = await Promise.allSettled(promises); for (const [index, result] of results.entries()) { const [programId, decimals] = result.status === 'fulfilled' ? [result.value[0], result.value[1].decimals] : [undefined, 6]; tokens[addresses[index].toString()] = await getTokenMetadata( connection, addresses[index].toString(), decimals, programId, ); } return tokens; }; export enum NetworkType { Local = 'Local', Testnet = 'Testnet', Devnet = 'Devnet', Mainnet = 'Mainnet', } export const networkTypetoProgramNetwork = (type: NetworkType): Network => { switch (type) { case NetworkType.Devnet: return Network.DEV; case NetworkType.Local: return Network.LOCAL; case NetworkType.Testnet: return Network.TEST; case NetworkType.Mainnet: return Network.MAIN; default: return Network.DEV; } }; export async function isSwapWithETH( tokenFrom: any, tokenTo: any, connection: any, ) { const allTokens = await getFullNewTokensData( [tokenFrom, tokenTo], connection, ); return ( allTokens[tokenFrom.toString()].address.toString() === WRAPPED_ETH_ADDRESS || allTokens[tokenTo.toString()].address.toString() === WRAPPED_ETH_ADDRESS ); } // cook trx swap export async function handleSwap( allPools: any, tickmaps: any, marketProgram: any, wallet: any, connection: any, tokenFrom: any, tokenTo: any, byAmountIn: boolean, slippage: any, estimatedPriceAfterSwap: any, poolIndex: number, swapAmount: any, ) { try { const allTokens = await getFullNewTokensData( [tokenFrom, tokenTo], connection, ); const tokenFromFormat = tokenFrom.toString(); const tokenToFormat = tokenTo.toString(); const tokensAccountList = await getAllTokenAccounts(wallet, connection); const tokensAccounts = tokensAccountList.reduce((acc: any, item: any) => { acc[item.programId] = item; return acc; }, {}); // Find the correct pool based on tokens and fee tier const selectedPool = allPools[poolIndex]; if (!selectedPool) { throw new Error('Pool not found'); } // Ensure tokens are in correct order as per pool configuration const isXtoY = tokenFrom.equals(selectedPool.tokenX); const [tokenX, tokenY] = isXtoY ? [tokenFrom, tokenTo] : [tokenTo, tokenFrom]; // Create pair with correct token order and fee tier const pair = new Pair(tokenX, tokenY, { fee: selectedPool.fee, tickSpacing: selectedPool.tickSpacing, }); // Get or create token accounts let fromAddress = tokensAccounts[tokenFromFormat] ? tokensAccounts[tokenFromFormat].address : await createAccount(tokenFrom, wallet, connection); let toAddress = tokensAccounts[tokenToFormat] ? tokensAccounts[tokenToFormat].address : await createAccount(tokenTo, wallet, connection); // Build swap instruction with verified pool and token accounts const swapTx = await marketProgram.swapIx( { pair, owner: wallet.publicKey, xToY: isXtoY, amount: swapAmount, estimatedPriceAfterSwap, slippage, byAmountIn, accountX: isXtoY ? fromAddress : toAddress, accountY: isXtoY ? toAddress : fromAddress, }, { pool: selectedPool, tickmap: tickmaps[selectedPool.tickmap.toString()], tokenXProgram: allTokens[selectedPool.tokenX.toString()].tokenProgram, tokenYProgram: allTokens[selectedPool.tokenY.toString()].tokenProgram, }, { tickCrosses: MAX_CROSSES_IN_SINGLE_TX, }, ); return swapTx; } catch (error: any) { console.error('Swap error:', error); if (error?.message?.includes('custom program error: 0x7d6')) { const errorLogs = error.logs || []; console.error('Transaction logs:', errorLogs); throw new Error( 'Invalid pool configuration. Please verify token pair and fee tier.', ); } if (error instanceof TransactionExpiredTimeoutError) { throw new Error( 'Transaction has timed out. Check the details to confirm success.', ); } throw new Error(error?.message || 'Failed to send. Please try again.'); } }