Created
July 17, 2025 07:34
-
-
Save 15august/7e083c085f3629e7e2cd2cd91663dc95 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { createClient, getClient, MAINNET_RELAY_API, configureDynamicChains, AdaptedWallet, Execute, SvmReceipt, TransactionStepItem, convertViemChainToRelayChain, RelayChain, } from '@reservoir0x/relay-sdk'; | |
| import { parseUnits, formatUnits } from 'viem'; | |
| import { mainnet, base, arbitrum, optimism, polygon } from 'viem/chains'; | |
| // Constants | |
| const SOLANA_USDC_ADDRESS = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; | |
| const SOLANA_CHAIN_ID = 792703809; | |
| const DEFAULT_FEE_PAYER = 'a23tFfciWmKgTmLhtTGS1NPg8eqUWmqQNyoUF16UBdF'; | |
| const DEFAULT_SPONSOR_ENDPOINT = 'http://localhost:3000/api/solana/sponsor'; | |
| const DEFAULT_SLIPPAGE = '100'; // 1% | |
| const APP_FEE_RECIPIENT = '5MApngvd5TkmLXbhCzpyi5jBASE3GgDMeqxosMdPbPZs'; | |
| const DEFAULT_APP_FEE = '50'; // 0.5% | |
| const SMALL_TRADE_APP_FEE = '25'; // 0.25% | |
| // Chain ID mapping | |
| const CHAIN_ID_MAPPING: { [key: number]: number } = { | |
| 1399811149: 792703809, | |
| 792703809: 792703809, | |
| }; | |
| export function mapToRelayChainId(apiNetworkId: number): number { | |
| return CHAIN_ID_MAPPING[apiNetworkId] || apiNetworkId; | |
| } | |
| export interface QuoteOptions { | |
| amount: string; | |
| fromChainId?: number; | |
| toChainId?: number; | |
| fromCurrency?: string; | |
| toCurrency?: string; | |
| slippageTolerance?: string; | |
| recipient?: string; | |
| } | |
| export interface ExecuteOptions { | |
| onProgress?: (data: any) => void; | |
| depositGasLimit?: string; | |
| } | |
| export class RelayTrader { | |
| private wallet: any; | |
| private userAddress: string; | |
| private chainId: number; | |
| constructor(wallet: any, userAddress: string, chainId: number = SOLANA_CHAIN_ID) { | |
| this.wallet = createSolanaAdapter(wallet); | |
| this.userAddress = userAddress; | |
| this.chainId = mapToRelayChainId(chainId); | |
| this.initializeRelayClient(); | |
| } | |
| private async initializeRelayClient() { | |
| try { | |
| const solanaRelayChain: RelayChain = { | |
| id: 792703809, | |
| name: "solana", | |
| displayName: "Solana", | |
| httpRpcUrl: "https://api.mainnet-beta.solana.com", | |
| wsRpcUrl: "", // or "wss://api.mainnet-beta.solana.com" if needed | |
| explorerUrl: "https://solscan.io", | |
| explorerQueryParams: {}, | |
| iconUrl: "https://assets.relay.link/icons/792703809/light.png", | |
| currency: { | |
| id: "sol", | |
| symbol: "SOL", | |
| name: "Solana", | |
| address: "11111111111111111111111111111111", | |
| decimals: 9, | |
| supportsBridging: false, | |
| }, | |
| depositEnabled: true, | |
| blockProductionLagging: false, | |
| erc20Currencies: [ | |
| { | |
| id: "pengu", | |
| symbol: "PENGU", | |
| name: "Pudgy Penguins", | |
| address: "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", | |
| decimals: 6, | |
| supportsBridging: true, | |
| withdrawalFee: 0, | |
| depositFee: 0, | |
| surgeEnabled: false, | |
| }, | |
| { | |
| id: "usdc", | |
| symbol: "USDC", | |
| name: "USD Coin", | |
| address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", | |
| decimals: 6, | |
| supportsBridging: true, | |
| supportsPermit: true, | |
| withdrawalFee: 5, | |
| depositFee: 2, | |
| surgeEnabled: false, | |
| } | |
| ], | |
| featuredTokens: [ | |
| { | |
| id: "sol", | |
| symbol: "SOL", | |
| name: "Solana", | |
| address: "11111111111111111111111111111111", | |
| decimals: 9, | |
| supportsBridging: false, | |
| metadata: { | |
| logoURI: "https://upload.wikimedia.org/wikipedia/en/b/b9/Solana_logo.png", | |
| }, | |
| }, | |
| { | |
| id: "usdc", | |
| symbol: "USDC", | |
| name: "USD Coin", | |
| address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", | |
| decimals: 6, | |
| supportsBridging: true, | |
| metadata: { | |
| logoURI: "https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694", | |
| }, | |
| }, | |
| ], | |
| tags: [], | |
| vmType: "svm", | |
| baseChainId: 1, | |
| }; | |
| // Use manual chain configuration instead of dynamic | |
| const relayChains = [ | |
| convertViemChainToRelayChain(mainnet), | |
| convertViemChainToRelayChain(base), | |
| convertViemChainToRelayChain(arbitrum), | |
| convertViemChainToRelayChain(optimism), | |
| convertViemChainToRelayChain(polygon), | |
| solanaRelayChain, | |
| ]; | |
| createClient({ | |
| baseApiUrl: MAINNET_RELAY_API, | |
| source: "yolo.meme", | |
| chains: relayChains | |
| }); | |
| console.log('Relay client initialized successfully'); | |
| } catch (error) { | |
| console.error('Failed to initialize Relay client:', error); | |
| } | |
| } | |
| async buyWithUSDC(tokenAddress: string, usdcAmount: string, fromChainId: number = this.chainId, options: Partial<QuoteOptions> = {}) { | |
| const mappedFromChainId = mapToRelayChainId(fromChainId); | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| const client = getClient(); | |
| const amountInWei = parseUnits(usdcAmount, 6).toString(); | |
| const isSmallTrade = parseFloat(usdcAmount) < 1; | |
| // First try with normal settings | |
| try { | |
| return await client.actions.getQuote({ | |
| chainId: mappedFromChainId, | |
| toChainId: this.chainId, | |
| currency: this.getUSDCAddress(), | |
| toCurrency: tokenAddress, | |
| amount: amountInWei, | |
| tradeType: 'EXACT_INPUT' as const, | |
| user: this.userAddress, | |
| recipient: options.recipient || this.userAddress, | |
| wallet: this.wallet, | |
| options: { | |
| slippageTolerance: options.slippageTolerance || DEFAULT_SLIPPAGE, | |
| appFees: [{ | |
| recipient: APP_FEE_RECIPIENT, | |
| fee: isSmallTrade ? SMALL_TRADE_APP_FEE : DEFAULT_APP_FEE | |
| }] | |
| }, | |
| }); | |
| } catch (error) { | |
| // If quote fails, try with optimized settings for smaller transactions | |
| console.log('Retrying with optimized settings for transaction size...'); | |
| return await client.actions.getQuote({ | |
| chainId: mappedFromChainId, | |
| toChainId: this.chainId, | |
| currency: this.getUSDCAddress(), | |
| toCurrency: tokenAddress, | |
| amount: amountInWei, | |
| tradeType: 'EXACT_INPUT' as const, | |
| user: this.userAddress, | |
| recipient: options.recipient || this.userAddress, | |
| wallet: this.wallet, | |
| options: { | |
| slippageTolerance: '500', // Higher slippage = smaller transaction | |
| appFees: [], // Remove app fees to reduce transaction size | |
| }, | |
| }); | |
| } | |
| } | |
| async executeQuote(quote: any, options: ExecuteOptions = {}) { | |
| const client = getClient(); | |
| return client.actions.execute({ | |
| quote, | |
| wallet: this.wallet, | |
| depositGasLimit: options.depositGasLimit, | |
| onProgress: options.onProgress || ((data) => console.log('Progress:', data)) | |
| }); | |
| } | |
| async getPrice(fromToken: string, toToken: string, amount: string, fromDecimals: number = 9, tradeType: 'EXACT_INPUT' | 'EXACT_OUTPUT' = 'EXACT_INPUT') { | |
| const client = getClient(); | |
| const amountInWei = parseUnits(amount, fromDecimals).toString(); | |
| return client.actions.getQuote({ | |
| chainId: this.chainId, | |
| toChainId: this.chainId, | |
| currency: fromToken, | |
| toCurrency: toToken, | |
| amount: amountInWei, | |
| tradeType, | |
| user: this.userAddress, | |
| recipient: this.userAddress, | |
| wallet: this.wallet, | |
| options: { | |
| appFees: [{ | |
| recipient: APP_FEE_RECIPIENT, | |
| fee: DEFAULT_APP_FEE | |
| }] | |
| } | |
| }); | |
| } | |
| formatAmount(amount: string, decimals: number = 9): string { | |
| return formatUnits(BigInt(amount), decimals); | |
| } | |
| parseAmount(amount: string, decimals: number = 9): string { | |
| return parseUnits(amount, decimals).toString(); | |
| } | |
| getUSDCAddress(): string { | |
| return SOLANA_USDC_ADDRESS; | |
| } | |
| getChainId(): number { | |
| return this.chainId; | |
| } | |
| getBaseUSDCAddress(): string { | |
| return this.getUSDCAddress(); | |
| } | |
| getBaseChainId(): number { | |
| return this.getChainId(); | |
| } | |
| } | |
| export const createRelayTrader = (wallet: any, userAddress: string, chainId: number = SOLANA_CHAIN_ID) => { | |
| return new RelayTrader(wallet, userAddress, chainId); | |
| }; | |
| const createSolanaAdapter = (embeddedWallet: any): AdaptedWallet => { | |
| return { | |
| vmType: 'svm', | |
| async getChainId() { | |
| return SOLANA_CHAIN_ID; | |
| }, | |
| async handleSignMessageStep(stepItem, step) { | |
| throw new Error('Message signing not implemented for Solana'); | |
| }, | |
| async handleSendTransactionStep(chainId: number, stepItem: TransactionStepItem, step: Execute['steps'][0]) { | |
| const txData = stepItem.data; | |
| const solanaWeb3 = await import('@solana/web3.js'); | |
| const { Buffer } = await import('buffer'); | |
| const { TransactionInstruction, TransactionMessage, VersionedTransaction, PublicKey, Connection } = solanaWeb3; | |
| const connection = new Connection('https://api.mainnet-beta.solana.com'); | |
| const instructions = txData.instructions?.map((i: any) => { | |
| return new TransactionInstruction({ | |
| keys: i.keys.map((k: any) => ({ | |
| isSigner: k.isSigner, | |
| isWritable: k.isWritable, | |
| pubkey: new PublicKey(k.pubkey) | |
| })), | |
| programId: new PublicKey(i.programId), | |
| data: Buffer.from(i.data, 'hex') | |
| }); | |
| }) ?? []; | |
| const { blockhash } = await connection.getLatestBlockhash(); | |
| const addressLookupTableAccounts = await Promise.all( | |
| txData.addressLookupTableAddresses?.map( | |
| async (address: string) => | |
| await connection | |
| .getAddressLookupTable(new PublicKey(address)) | |
| .then((res: any) => res.value) | |
| ) ?? [] | |
| ); | |
| const messageV0 = new TransactionMessage({ | |
| payerKey: new PublicKey(DEFAULT_FEE_PAYER), | |
| recentBlockhash: blockhash, | |
| instructions | |
| }).compileToV0Message(addressLookupTableAccounts); | |
| const transaction = new VersionedTransaction(messageV0); | |
| // Check transaction size before signing to avoid size limits | |
| const preliminarySize = Buffer.from(transaction.message.serialize()).length; | |
| console.log('Transaction size before signing:', preliminarySize); | |
| if (preliminarySize > 1000) { // Leave larger buffer for signatures (can add 500+ bytes) | |
| console.warn('Transaction too large, trying to remove unnecessary instructions...'); | |
| // Strategy: Filter out optional instructions to reduce size | |
| console.log('π Original transaction instructions:'); | |
| txData.instructions?.forEach((instruction: any, index: number) => { | |
| console.log(` Instruction ${index + 1}:`, { | |
| programId: instruction.programId, | |
| dataLength: instruction.data?.length || 0, | |
| keysCount: instruction.keys?.length || 0, | |
| firstDataByte: instruction.data?.[0], | |
| keys: instruction.keys?.map((k: any) => ({ | |
| pubkey: k.pubkey, | |
| isSigner: k.isSigner, | |
| isWritable: k.isWritable | |
| })) | |
| }); | |
| }); | |
| const essentialInstructions = txData.instructions?.filter((instruction: any, index: number) => { | |
| const programId = instruction.programId; | |
| // Keep essential instructions only | |
| if (programId === '11111111111111111111111111111111') { | |
| console.log(`β Keeping instruction ${index + 1}: System program`); | |
| return true; // System program | |
| } | |
| if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') { | |
| console.log(`β Keeping instruction ${index + 1}: Token program`); | |
| return true; // Token program | |
| } | |
| if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') { | |
| console.log(`β Keeping instruction ${index + 1}: Associated token program`); | |
| return true; // Associated token | |
| } | |
| if (programId === 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4') { | |
| console.log(`β Keeping instruction ${index + 1}: Jupiter program`); | |
| return true; // Jupiter | |
| } | |
| // Skip memo and other optional instructions | |
| if (programId === 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr') { | |
| console.log(`β Removing instruction ${index + 1}: Memo program`); | |
| return false; // Memo | |
| } | |
| if (programId === 'ComputeBudget111111111111111111111111111111') { | |
| console.log(`β Removing instruction ${index + 1}: Compute Budget program`); | |
| return false; // Compute budget | |
| } | |
| console.log(`β Keeping instruction ${index + 1}: Unknown program (${programId})`); | |
| return true; // Keep other instructions by default | |
| }) || []; | |
| console.log(`Reduced instructions from ${txData.instructions?.length || 0} to ${essentialInstructions.length}`); | |
| // Rebuild transaction with fewer instructions | |
| const optimizedInstructions = essentialInstructions.map((i: any) => { | |
| return new TransactionInstruction({ | |
| keys: i.keys.map((k: any) => ({ | |
| isSigner: k.isSigner, | |
| isWritable: k.isWritable, | |
| pubkey: new PublicKey(k.pubkey) | |
| })), | |
| programId: new PublicKey(i.programId), | |
| data: Buffer.from(i.data, 'hex') | |
| }); | |
| }); | |
| const optimizedMessageV0 = new TransactionMessage({ | |
| payerKey: new PublicKey(DEFAULT_FEE_PAYER), | |
| recentBlockhash: blockhash, | |
| instructions: optimizedInstructions | |
| }).compileToV0Message(addressLookupTableAccounts); | |
| const optimizedTransaction = new VersionedTransaction(optimizedMessageV0); | |
| const optimizedSize = Buffer.from(optimizedTransaction.message.serialize()).length; | |
| console.log(`Optimized transaction size: ${optimizedSize} bytes`); | |
| if (optimizedSize > 1230) { | |
| throw new Error(`Transaction still too large after optimization: ${optimizedSize} bytes. Try reducing trade amount or using a different token pair.`); | |
| } | |
| // Use optimized transaction | |
| const serializedMessage = Buffer.from(optimizedTransaction.message.serialize()).toString('base64'); | |
| const provider = await embeddedWallet.getProvider(); | |
| const { signature: serializedUserSignature } = await provider.request({ | |
| method: 'signMessage', | |
| params: { message: serializedMessage } | |
| }); | |
| const userSignature = Buffer.from(serializedUserSignature, 'base64'); | |
| optimizedTransaction.addSignature(new PublicKey(embeddedWallet.address), userSignature); | |
| const serializedTransaction = Buffer.from(optimizedTransaction.serialize()).toString('base64'); | |
| console.log('Optimized serializedTransaction length: ', serializedTransaction.length); | |
| const response = await fetch(DEFAULT_SPONSOR_ENDPOINT, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ transaction: serializedTransaction }) | |
| }); | |
| const { transactionHash } = await response.json(); | |
| return transactionHash; | |
| } | |
| const serializedMessage = Buffer.from(transaction.message.serialize()).toString('base64'); | |
| const provider = await embeddedWallet.getProvider(); | |
| const { signature: serializedUserSignature } = await provider.request({ | |
| method: 'signMessage', | |
| params: { message: serializedMessage } | |
| }); | |
| const userSignature = Buffer.from(serializedUserSignature, 'base64'); | |
| transaction.addSignature(new PublicKey(embeddedWallet.address), userSignature); | |
| const serializedTransaction = Buffer.from(transaction.serialize()).toString('base64'); | |
| console.log('serializedTransaction length: ', serializedTransaction.length); | |
| // Check final size after signing | |
| if (serializedTransaction.length > 1644) { | |
| throw new Error(`Transaction too large: ${serializedTransaction.length} bytes (max: 1644). Consider reducing transaction complexity.`); | |
| } | |
| const response = await fetch(DEFAULT_SPONSOR_ENDPOINT, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ transaction: serializedTransaction }) | |
| }); | |
| const { transactionHash } = await response.json(); | |
| return transactionHash; | |
| }, | |
| async handleConfirmTransactionStep(tx: string, chainId: number, onReplaced?: (replacementTxHash: string) => void, onCancelled?: () => void): Promise<SvmReceipt> { | |
| const solanaWeb3 = await import('@solana/web3.js'); | |
| const { Connection } = solanaWeb3; | |
| const connection = new Connection('https://api.mainnet-beta.solana.com'); | |
| const confirmation = await connection.confirmTransaction(tx, 'confirmed'); | |
| return { | |
| txHash: tx, | |
| blockNumber: confirmation.context.slot, | |
| blockHash: '', | |
| } as SvmReceipt; | |
| }, | |
| async address() { | |
| return embeddedWallet.address; | |
| }, | |
| async switchChain(chainId: number): Promise<void> { | |
| throw new Error('Chain switching not supported for Solana wallets'); | |
| } | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment