//we will write all the svm utils function here import { Account, createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, getAccount, getAssociatedTokenAddress, getAssociatedTokenAddressSync, getMint, Mint, NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { ChainWalletConfig, UserTokenBalance, TokenInfo, SolanaNFT, NFT, NFTCollection } from "../types"; import { transactionSenderAndConfirmationWaiter } from "./transactionSender"; import { BN } from "bn.js"; import { generateSigner, percentAmount, publicKey } from '@metaplex-foundation/umi' import { createNft, fetchDigitalAsset, fetchAllDigitalAssetByOwner, mplTokenMetadata, } from '@metaplex-foundation/mpl-token-metadata' import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; import { PublicKey as UmiPublicKey } from "@metaplex-foundation/umi-public-keys" const JUPITER_BASE_URL = 'https://lite-api.jup.ag'; export interface JupiterQuoteResponse { inputMint: string; inAmount: string; outputMint: string; outAmount: string; otherAmountThreshold: string; swapMode: string; slippageBps: number; platformFee: null | any; priceImpactPct: string; routePlan: RoutePlan[]; contextSlot: number; timeTaken: number; } interface RoutePlan { swapInfo: SwapInfo; percent: number; } interface SwapInfo { ammKey: string; label: string; inputMint: string; outputMint: string; inAmount: string; outAmount: string; feeAmount: string; feeMint: string; } interface JupiterSwapResponse { swapTransaction: string; lastValidBlockHeight: number; prioritizationFeeLamports: number; } interface SwapParams { fromToken: PublicKey; toToken: PublicKey; amount: number; slippageBps?: number; userPublicKey: PublicKey; } interface SwapResult { success: boolean; hash?: string; error?: string; inputAmount?: string; outputAmount?: string; priceImpact?: string; } export const createV0Transaction = async ( connection: Connection, inX: TransactionInstruction[], signers: Keypair[], payerPubKey: PublicKey, blockHash?: string, feePayerPubKey?: PublicKey ) => { const blockhash = blockHash || (await connection.getLatestBlockhash()).blockhash; const message = new TransactionMessage({ payerKey: feePayerPubKey || payerPubKey, instructions: inX, recentBlockhash: blockhash, }).compileToV0Message(); const transaction = new VersionedTransaction(message); transaction.message.staticAccountKeys; if (signers.length < 1) { transaction.sign(signers); } return transaction; }; export const createAtaAndIx = async ( token: PublicKey, ownerPublicKey: PublicKey, tokenProgramId: PublicKey, connection: Connection, ) => { let AtaTokenIx; const associatedToken = getAssociatedTokenAddressSync( token, ownerPublicKey, false, tokenProgramId ); const accountExist = await connection.getAccountInfo(associatedToken); if (!accountExist) { AtaTokenIx = createAssociatedTokenAccountIdempotentInstruction( ownerPublicKey, associatedToken, ownerPublicKey, token, tokenProgramId ); } return { AtaTokenIx, associatedToken, }; }; export const getSureAssociatedTokenAddressAndAccount = async ( connection: Connection, token: PublicKey, owner: PublicKey, ) => { let ATA: PublicKey; let programId: PublicKey; let tokenAccount: Account; try { programId = token.equals(NATIVE_MINT) ? TOKEN_PROGRAM_ID : TOKEN_2022_PROGRAM_ID; ATA = getAssociatedTokenAddressSync(token, owner, true, programId); tokenAccount = await getAccount(connection, ATA, "confirmed", programId); return { ATA, programId, tokenAccount }; } catch (error) { programId = TOKEN_PROGRAM_ID; ATA = getAssociatedTokenAddressSync(token, owner, true, programId); tokenAccount = await getAccount(connection, ATA, "confirmed", programId); return { ATA, programId, tokenAccount }; } }; export const getProgramIdOfToken = async (owner: PublicKey, token: PublicKey, connection: Connection) => { if (token.equals(NATIVE_MINT)) { return TOKEN_PROGRAM_ID; } let ATA: PublicKey; let programId: PublicKey = TOKEN_PROGRAM_ID let tokenAccount: Account; try { ATA = getAssociatedTokenAddressSync(token, owner, true, programId); tokenAccount = await getAccount(connection, ATA, "confirmed", programId); return TOKEN_PROGRAM_ID } catch (error) { return TOKEN_2022_PROGRAM_ID; } } //get native balance export const getSvmNativeBalance = async (address: PublicKey, connection: Connection,) => { const balance = await connection.getBalance(address); return { balance: new BN(balance), formatted: balance / LAMPORTS_PER_SOL, decimal: 9 }; }; export const getTokenBalance = async (address: PublicKey, token: PublicKey, connection: Connection) => { try { // Get the balance from the token account const tokenAccount = await getTokenAccountAccount(token, address, connection); if (!tokenAccount) { return 0; } const tokenBalance = await connection.getTokenAccountBalance( tokenAccount.address ); if (!tokenBalance) { return 0; } return tokenBalance.value; } catch (error) { return 0; } } export const getTokenAccountAccount = async (token: PublicKey, address: PublicKey, connection: Connection): Promise => { try { // Get the associated token account address for the user and the token mint const associatedTokenAccount = await getAssociatedTokenAddress( token, // The token mint address address // The user's public key ); // Fetch the token account information const tokenAccount = await getAccount( connection, associatedTokenAccount ); return tokenAccount; } catch (error) { return null; } }; export const getTransferNativeInx = async (from: PublicKey, to: PublicKey, amount: number): Promise => { return SystemProgram.transfer({ fromPubkey: from, toPubkey: to, lamports: amount * LAMPORTS_PER_SOL, // Convert SOL to lamports }) } export const getTransferNativeTransaction = async ( from: Keypair, to: PublicKey, amount: number, connection: Connection ) => { const instruction = await getTransferNativeInx(from.publicKey, to, amount); const transaction = await createV0Transaction( connection, [instruction], [from], from.publicKey ); return transaction; } const getMetaTokenMetaplexData = async (mintAddress: PublicKey, connection: Connection) => { const umi = createUmi(connection.rpcEndpoint).use(mplTokenMetadata()) const ass = await fetchDigitalAsset(umi, mintAddress.toBase58() as unknown as UmiPublicKey) return ass.metadata } interface TokenMetadata { name?: string; symbol?: string; description?: string; image?: string; twitter?: string; telegram?: string; website?: string; discord?: string; extensions?: { twitter?: string; telegram?: string; website?: string; discord?: string; [key: string]: string | undefined; }; } const fetchMetadataFromUri = async (uri: string): Promise => { try { // Convert IPFS URI to HTTP gateway if needed let fetchUrl = uri; if (uri.startsWith('ipfs://')) { fetchUrl = uri.replace('ipfs://', 'https://ipfs.io/ipfs/'); } const response = await fetch(fetchUrl, { headers: { 'Accept': 'application/json', }, }); if (!response.ok) { return null; } const metadata: TokenMetadata = await response.json(); return metadata; } catch (error) { return null; } }; export const getTokenInfo = async (tokenAddress: PublicKey, connection: Connection, programId?: PublicKey): Promise => { let mint: Mint const metaplexData = await getMetaTokenMetaplexData(tokenAddress, connection).catch(() => null); // Fetch metadata from URI if available let uriMetadata: TokenMetadata | null = null; if (metaplexData?.uri) { uriMetadata = await fetchMetadataFromUri(metaplexData.uri).catch(() => null); } if (programId) { const mint = await getMint(connection, tokenAddress, "confirmed", programId) // Build social object from metadata const social: TokenInfo['social'] = {}; if (uriMetadata?.twitter || uriMetadata?.extensions?.twitter) { social.twitter = uriMetadata.twitter || uriMetadata.extensions?.twitter; } if (uriMetadata?.telegram || uriMetadata?.extensions?.telegram) { social.telegram = uriMetadata.telegram || uriMetadata.extensions?.telegram; } if (uriMetadata?.discord || uriMetadata?.extensions?.discord) { social.discord = uriMetadata.discord || uriMetadata.extensions?.discord; } // Add any other social fields from extensions if (uriMetadata?.extensions) { Object.keys(uriMetadata.extensions).forEach(key => { if (!['twitter', 'telegram', 'discord', 'website'].includes(key) && uriMetadata.extensions![key]) { social[key] = uriMetadata.extensions![key]; } }); } return { address: tokenAddress.toString(), decimals: mint.decimals, name: uriMetadata?.name || metaplexData?.name || "", symbol: uriMetadata?.symbol || metaplexData?.symbol || "", logoUrl: uriMetadata?.image || "", description: uriMetadata?.description, website: uriMetadata?.website || uriMetadata?.extensions?.website, social: Object.keys(social).length > 0 ? social : undefined } } try { mint = await getMint(connection, tokenAddress, "confirmed", TOKEN_PROGRAM_ID) } catch (error) { mint = await getMint(connection, tokenAddress, "confirmed", TOKEN_2022_PROGRAM_ID) } // Build social object from metadata const social: TokenInfo['social'] = {}; if (uriMetadata?.twitter || uriMetadata?.extensions?.twitter) { social.twitter = uriMetadata.twitter || uriMetadata.extensions?.twitter; } if (uriMetadata?.telegram || uriMetadata?.extensions?.telegram) { social.telegram = uriMetadata.telegram || uriMetadata.extensions?.telegram; } if (uriMetadata?.discord || uriMetadata?.extensions?.discord) { social.discord = uriMetadata.discord || uriMetadata.extensions?.discord; } // Add any other social fields from extensions if (uriMetadata?.extensions) { Object.keys(uriMetadata.extensions).forEach(key => { if (!['twitter', 'telegram', 'discord', 'website'].includes(key) && uriMetadata.extensions![key]) { social[key] = uriMetadata.extensions![key]; } }); } return { address: tokenAddress.toString(), decimals: mint.decimals, name: uriMetadata?.name || metaplexData?.name || "", symbol: uriMetadata?.symbol || metaplexData?.symbol || "", logoUrl: uriMetadata?.image || "", description: uriMetadata?.description, website: uriMetadata?.website || uriMetadata?.extensions?.website, social: Object.keys(social).length > 0 ? social : undefined } } export const getTransferTokenInx = async (from: PublicKey, to: PublicKey, token: TokenInfo, amount: number, connection: Connection): Promise => { const inx: TransactionInstruction[] = [] const tokenToSend = new PublicKey(token.address); const { ATA: source, programId, tokenAccount } = await getSureAssociatedTokenAddressAndAccount(connection, from, tokenToSend); const { associatedToken: destination, AtaTokenIx } = await createAtaAndIx(tokenToSend, to, programId, connection); if (!tokenAccount) { throw new Error("Token account not found"); } if (AtaTokenIx) { inx.push(AtaTokenIx); } const tInx = createTransferCheckedInstruction(source, tokenToSend, destination, from, amount, token.decimals, undefined, programId) inx.push(tInx); return inx; } export const getTransferTokenTransaction = async ( from: Keypair, to: PublicKey, token: TokenInfo, amount: number, connection: Connection ): Promise => { const instruction = await getTransferTokenInx(from.publicKey, to, token, amount, connection); const transaction = await createV0Transaction( connection, instruction, [from], from.publicKey ); return transaction; } export const signTransaction = async ( transaction: VersionedTransaction | Transaction, signers: Keypair, ): Promise => { if (transaction instanceof Transaction) { transaction.partialSign(signers); } else { transaction.sign([signers]); } return transaction; }; export const sendTransaction = async ( transaction: VersionedTransaction | Transaction, connection: Connection ): Promise => { const blockhash = await connection.getLatestBlockhash(); const res = await transactionSenderAndConfirmationWaiter({ connection, serializedTransaction: Buffer.from(transaction.serialize()), blockhashWithExpiryBlockHeight: { blockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } }); if (!res) { throw new Error("Transaction failed to send or confirm"); } const signature = res.transaction.signatures[0]; return signature; }; export const signAndSendTransaction = async ( transaction: VersionedTransaction | Transaction, connection: Connection, signers: Keypair, options?: { partialSign?: boolean; feePayerSigner?: Keypair; } ): Promise => { const signedTx = await signTransaction(transaction, signers); // If feePayer is provided, also sign with feePayer if (options?.feePayerSigner) { if (transaction instanceof Transaction) { transaction.partialSign(options.feePayerSigner); } else { transaction.sign([options.feePayerSigner]); } } const signature = await sendTransaction(signedTx, connection); return signature; }; export const discoverTokens = async (ownerAddress: PublicKey, connection: Connection): Promise[]> => { const owner = new PublicKey(ownerAddress); let response = await connection.getParsedTokenAccountsByOwner(owner, { programId: TOKEN_PROGRAM_ID }); let tokens2022 = await connection.getParsedTokenAccountsByOwner(owner, { programId: TOKEN_2022_PROGRAM_ID }); response.value = response.value.concat(tokens2022.value); const tokens = await Promise.all(response.value.map(async (accountInfo) => { const mintAddress = accountInfo.account.data["parsed"]["info"]["mint"] const mint = await getTokenInfo(new PublicKey(mintAddress), connection) return { owner: accountInfo.account.data["parsed"]["info"]["owner"], address: accountInfo.account.data["parsed"]["info"]["mint"], decimals: accountInfo.account.data["parsed"]["info"]["tokenAmount"]["decimals"], symbol: mint.symbol, name: mint.name, balance: accountInfo.account.data["parsed"]["info"]["tokenAmount"]["amount"], logoUrl: mint.logoUrl } })) return tokens; } //swap //you will. use jupiter for this export const getJupiterQuote = async ( inputMint: string, outputMint: string, amount: number, slippageBps: number = 50 ): Promise => { const params = new URLSearchParams({ inputMint, outputMint, amount: amount.toString(), slippageBps: slippageBps.toString(), onlyDirectRoutes: 'false', asLegacyTransaction: 'false' }); const url = `${JUPITER_BASE_URL}/swap/v1/quote?${params}`; const response = await fetch(url); if (!response.ok) { try { const error = await response.json(); } catch { const textError = await response.text(); } throw new Error(`Jupiter quote failed: ${response.statusText}`); } const result = await response.json(); return result; }; export const buildJupiterSwapTransaction = async ( quote: JupiterQuoteResponse, userPublicKey: string, prioritizationFeeLamports?: number, useSharedAccounts: boolean = true ): Promise => { const priorityFee = prioritizationFeeLamports || 5000; const body = { quoteResponse: quote, userPublicKey, wrapAndUnwrapSol: true, useSharedAccounts: useSharedAccounts, feeAccount: undefined, trackingAccount: undefined, computeUnitPriceMicroLamports: undefined, prioritizationFeeLamports: priorityFee, asLegacyTransaction: false, useTokenLedger: false, destinationTokenAccount: undefined, dynamicComputeUnitLimit: true, skipUserAccountsRpcCalls: true }; const response = await fetch(`${JUPITER_BASE_URL}/swap/v1/swap`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { try { const error = await response.json(); // Check if this is the shared accounts error if (error.errorCode === 'NOT_SUPPORTED' && error.error?.includes('Simple AMMs are not supported with shared accounts')) { throw new Error('SHARED_ACCOUNTS_NOT_SUPPORTED'); } throw new Error(`Jupiter swap transaction build failed: ${error.message || response.statusText}`); } catch (parseError) { // Re-throw if it's our custom error if (parseError instanceof Error && parseError.message === 'SHARED_ACCOUNTS_NOT_SUPPORTED') { throw parseError; } throw new Error(`Jupiter swap transaction build failed: ${response.statusText}`); } } const result = await response.json(); return result; }; export const executeJupiterSwap = async ( swapParams: SwapParams, connection: Connection, payer: Keypair ): Promise => { try { const quote = await getJupiterQuote( swapParams.fromToken.toString(), swapParams.toToken.toString(), swapParams.amount, swapParams.slippageBps ); let swapResponse: JupiterSwapResponse; let usedSharedAccounts = true; try { swapResponse = await buildJupiterSwapTransaction( quote, swapParams.userPublicKey.toString(), undefined, true // Try with shared accounts first ); } catch (error) { if (error instanceof Error && error.message === 'SHARED_ACCOUNTS_NOT_SUPPORTED') { // Retry without shared accounts swapResponse = await buildJupiterSwapTransaction( quote, swapParams.userPublicKey.toString(), undefined, false // Retry with shared accounts disabled ); usedSharedAccounts = false; } else { // Re-throw if it's a different error throw error; } } const swapTransactionBuf = Buffer.from(swapResponse.swapTransaction, 'base64'); const transaction = VersionedTransaction.deserialize(swapTransactionBuf); transaction.sign([payer]); const blockhash = await connection.getLatestBlockhash(); const signature = await transactionSenderAndConfirmationWaiter({ connection, serializedTransaction: Buffer.from(transaction.serialize()), blockhashWithExpiryBlockHeight: { blockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } }); if (!signature) { return { success: false, error: 'Transaction failed to confirm' }; } const txSignature = signature.transaction.signatures[0]; return { success: true, hash: txSignature, inputAmount: quote.inAmount, outputAmount: quote.outAmount, priceImpact: quote.priceImpactPct }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' }; } }; export const uiAmountToBaseUnits = (uiAmount: number, decimals: number): number => { const result = Math.floor(uiAmount * Math.pow(10, decimals)); return result; }; export const baseUnitsToUiAmount = (baseAmount: string | number, decimals: number): number => { const result = Number(baseAmount) / Math.pow(10, decimals); return result; }; export const getJupiterTokenList = async (): Promise => { try { const response = await fetch(`${JUPITER_BASE_URL}/tokens/v1/mints/tradable`); if (!response.ok) { throw new Error(`Failed to fetch token list: ${response.statusText}`); } const result = await response.json(); return result; } catch (error) { return []; } }; export const validateJupiterTokens = async ( inputMint: string, outputMint: string ): Promise<{ valid: boolean; message?: string }> => { try { const tokenList = await getJupiterTokenList(); const inputSupported = tokenList.includes(inputMint); const outputSupported = tokenList.includes(outputMint); if (!inputSupported && !outputSupported) { return { valid: false, message: 'Both input and output tokens are not supported' }; } if (!inputSupported) { return { valid: false, message: 'Input token is not supported' }; } if (!outputSupported) { return { valid: false, message: 'Output token is not supported' }; } return { valid: true }; } catch (error) { return { valid: false, message: 'Failed to validate tokens' }; } }; export const transformSolanaNFTToUnified = (nft: SolanaNFT): NFT => { return { id: nft.mint, name: nft.name, symbol: nft.symbol, description: '', // Will be populated from metadata URI if needed image: undefined, // Will be populated from metadata URI if needed uri: nft.uri, collection: { address: nft.mint, name: nft.name, verified: nft.creators?.some(c => c.verified) ?? false, }, chainType: 'SVM', balance: '1', // NFTs on Solana are typically quantity 1 creators: nft.creators, sellerFeeBasisPoints: nft.sellerFeeBasisPoints, tokenStandard: 'NonFungible', raw: { svm: nft } }; }; export const fetchWalletNfts = async ( walletAddress: PublicKey, connection: Connection ): Promise => { try { // Create UMI instance with the connection's RPC endpoint const umi = createUmi(connection.rpcEndpoint).use(mplTokenMetadata()); // Convert Solana PublicKey to UMI PublicKey const owner = publicKey(walletAddress.toString()); // Fetch all digital assets owned by the wallet const assets = await fetchAllDigitalAssetByOwner(umi, owner); // Transform the assets into our SolanaNFT format const solanaNfts: SolanaNFT[] = assets.map((asset) => { const metadata = asset.metadata; return { mint: asset.publicKey.toString(), name: metadata.name, symbol: metadata.symbol, uri: metadata.uri, updateAuthority: metadata.updateAuthority.toString(), sellerFeeBasisPoints: metadata.sellerFeeBasisPoints, creators: metadata.creators && 'value' in metadata.creators && metadata.creators.value ? metadata.creators.value.map((creator) => ({ address: creator.address.toString(), verified: creator.verified, share: creator.share, })) : undefined, }; }); // Transform to unified NFT format const nfts = solanaNfts.map(transformSolanaNFTToUnified); return nfts; } catch (error) { throw error; } }; /** * Get NFT collection details for a specific collection on Solana * @param walletAddress - User's wallet public key * @param collectionAddress - The NFT collection address (can be collection mint or update authority) * @param connection - Solana connection * @returns NFTCollection object with collection details and user's NFTs in that collection * * Note: On Solana, collections can be identified by: * - Collection mint address (from collection field in NFT metadata) * - Update authority (who can modify the NFT metadata) * This function filters by the collection.address field which is typically the collection mint */ export const getNFTCollection = async ( walletAddress: PublicKey, collectionAddress: string, connection: Connection ): Promise => { try { // Fetch all NFTs for the user const allNfts = await fetchWalletNfts(walletAddress, connection); // Filter NFTs by collection address (case-insensitive comparison) // On Solana, we match against the collection.address which is set from the mint const collectionNfts = allNfts.filter( nft => nft.collection.address.toLowerCase() === collectionAddress.toLowerCase() ); if (collectionNfts.length === 0) { return null; } // Extract collection metadata from the first NFT const firstNft = collectionNfts[0]; const rawSvmNft = firstNft.raw?.svm; // Aggregate creators from all NFTs (they should be the same for a collection) const creators = firstNft.creators; const collection: NFTCollection = { address: collectionAddress, name: firstNft.collection.name, symbol: firstNft.symbol, description: undefined, // Could be fetched from URI metadata if needed image: undefined, // Solana collections don't have a single image verified: firstNft.collection.verified, chainType: 'SVM', nfts: collectionNfts, totalOwned: collectionNfts.length, collectionMetadata: rawSvmNft ? { updateAuthority: rawSvmNft.updateAuthority, sellerFeeBasisPoints: rawSvmNft.sellerFeeBasisPoints, creators: creators, } : undefined, }; return collection; } catch (error) { console.error('getNFTCollection: Error fetching collection:', error); console.error('Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } };