Created
July 18, 2025 14:33
-
-
Save 15august/0eb9b1660e2942df2ffff63ba99e717b 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, adaptViemWallet, getClient, MAINNET_RELAY_API, configureDynamicChains, AdaptedWallet, Execute, SvmReceipt, TransactionStepItem, convertViemChainToRelayChain, RelayChain, } from '@reservoir0x/relay-sdk'; | |
| import { parseUnits, formatUnits, createWalletClient, createPublicClient, http } from 'viem'; | |
| import { mainnet, base, arbitrum, optimism, polygon } from 'viem/chains'; | |
| import { SmartWalletClientType } from '@privy-io/js-sdk-core/smart-wallets'; | |
| // Constants for Base network | |
| const BASE_USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; | |
| const BASE_CHAIN_ID = 8453; | |
| const DEFAULT_SLIPPAGE = '100'; // 1% | |
| const APP_FEE_RECIPIENT = '0xbB656a3ed33C26dEe7f956545DD21361D5e788dC'; | |
| 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 } = { | |
| 8453: 8453, // Base | |
| 1: 1, // Ethereum mainnet | |
| 137: 137, // Polygon | |
| 42161: 42161, // Arbitrum | |
| 10: 10, // Optimism | |
| }; | |
| 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 = BASE_CHAIN_ID) { | |
| // Handle both smart wallet clients and regular wallet clients | |
| if (wallet.account && wallet.sendTransaction) { | |
| // This is likely a smart wallet client, use it directly | |
| this.wallet = createSmartWalletClientAdapter(wallet); | |
| } else { | |
| // This is a regular wallet client, adapt it | |
| this.wallet = adaptViemWallet(wallet); | |
| } | |
| this.userAddress = userAddress; | |
| this.chainId = chainId; | |
| this.initializeRelayClient(); | |
| } | |
| private async initializeRelayClient() { | |
| try { | |
| // Use manual chain configuration | |
| const relayChains = [ | |
| convertViemChainToRelayChain(mainnet), | |
| convertViemChainToRelayChain(base), | |
| convertViemChainToRelayChain(arbitrum), | |
| convertViemChainToRelayChain(optimism), | |
| convertViemChainToRelayChain(polygon), | |
| ]; | |
| 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...'); | |
| } | |
| } | |
| async executeQuote(quote: any, options: ExecuteOptions = {}) { | |
| const client = getClient(); | |
| return client.actions.execute({ | |
| quote, | |
| wallet: this.wallet, | |
| depositGasLimit: options.depositGasLimit, | |
| onProgress: options.onProgress || ((data) => { | |
| if (data?.currentStepItem?.data) { | |
| console.log('๐ Transaction call data:', JSON.stringify(data.currentStepItem.data, null, 2)); | |
| } | |
| }) | |
| }); | |
| } | |
| formatAmount(amount: string, decimals: number = 18): string { | |
| return formatUnits(BigInt(amount), decimals); | |
| } | |
| parseAmount(amount: string, decimals: number = 18): string { | |
| return parseUnits(amount, decimals).toString(); | |
| } | |
| getUSDCAddress(): string { | |
| return BASE_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 = BASE_CHAIN_ID) => { | |
| return new RelayTrader(wallet, userAddress, chainId); | |
| }; | |
| const createSmartWalletClientAdapter = ( | |
| smartWallet: SmartWalletClientType | |
| ): AdaptedWallet => { | |
| return { | |
| vmType: 'evm', | |
| async getChainId() { | |
| return smartWallet.chain.id; | |
| }, | |
| async address() { | |
| return smartWallet.account.address; | |
| }, | |
| async handleSignMessageStep(stepItem, step) { | |
| if (!stepItem.data?.sign) { | |
| throw new Error('Invalid step item: missing sign data'); | |
| } | |
| const { message, signatureKind, domain, types, value } = stepItem.data.sign; | |
| if (signatureKind === 'eip191') { | |
| if (typeof message !== 'string') { | |
| throw new Error('Message is required for eip191 signature'); | |
| } | |
| return await smartWallet.signMessage({ message }); | |
| } | |
| if (signatureKind === 'eip712') { | |
| if (!domain || !types || !value) { | |
| throw new Error('Domain, types, and value are required for eip712 signature'); | |
| } | |
| // A more robust way to find the primaryType, excluding the domain. | |
| const primaryType = Object.keys(types).find(key => key !== 'EIP712Domain'); | |
| if (!primaryType) { | |
| throw new Error('Could not determine primaryType for EIP712 signature'); | |
| } | |
| return await smartWallet.signTypedData({ | |
| domain, | |
| types, | |
| primaryType, | |
| message: value, | |
| }); | |
| } | |
| throw new Error(`Unsupported signature kind: ${signatureKind}`); | |
| }, | |
| async handleSendTransactionStep(chainId, stepItem, step) { | |
| const txData = stepItem.data; | |
| try { | |
| const result = await smartWallet.sendTransaction({ | |
| to: txData.to as `0x${string}`, | |
| data: (txData.data as `0x${string}`) || '0x', | |
| // Gas fields are typically handled by the bundler/paymaster | |
| // and can often be omitted. | |
| }); | |
| return result; | |
| } catch (error) { | |
| console.error('Smart wallet transaction failed:', error); | |
| throw error; | |
| } | |
| }, | |
| async handleConfirmTransactionStep(hash, chainId, onReplaced, onCancelled) { | |
| return new Promise((resolve, reject) => { | |
| const checkTransaction = async () => { | |
| try { | |
| // Use viem client for read operations | |
| const publicClient = createPublicClient({ | |
| chain: smartWallet.chain, | |
| transport: http() | |
| }); | |
| const receipt = await publicClient.getTransactionReceipt({ hash: hash as `0x${string}` }); | |
| if (receipt) { | |
| resolve(receipt); | |
| } else { | |
| // Continue polling | |
| setTimeout(checkTransaction, 2000); | |
| } | |
| } catch (error) { | |
| // Transaction might still be pending | |
| setTimeout(checkTransaction, 2000); | |
| } | |
| }; | |
| checkTransaction(); | |
| // Timeout after 5 minutes | |
| setTimeout(() => { | |
| reject(new Error('Transaction confirmation timeout')); | |
| }, 300000); | |
| }); | |
| }, | |
| async switchChain(chainId) { | |
| throw new Error('Chain switching not supported by privy smart wallets. Create a new wallet for different chains.'); | |
| }, | |
| // Optional: Support for batched transactions (ERC-4337 feature) | |
| async supportsAtomicBatch(chainId) { | |
| return true; // privy smart wallets likely support batching | |
| }, | |
| async handleBatchTransactionStep(chainId, stepItems) { | |
| console.log('๐ Sending batch transaction with step items:'); | |
| // Map the step items to the structure expected by the batch sendTransaction | |
| const calls = stepItems.map(item => ({ | |
| to: item.data.to as `0x${string}`, | |
| data: (item.data.data || '0x') as `0x${string}`, | |
| value: item.data.value ? BigInt(item.data.value) : undefined, | |
| })); | |
| const result = await smartWallet.sendTransaction({ | |
| calls: calls as any // Cast to bypass strict typing for raw transaction calls | |
| }); | |
| return result; | |
| }, | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment