package main import ( "context" "fmt" "math/big" "os" "strconv" "strings" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/fraktal/mev-beta/internal/config" "github.com/fraktal/mev-beta/internal/logger" "github.com/urfave/cli/v2" ) // SwapParams represents the parameters for a swap operation type SwapParams struct { TokenIn common.Address TokenOut common.Address AmountIn *big.Int MinAmountOut *big.Int Recipient common.Address Deadline uint64 Protocol string Slippage float64 } // SwapExecutor handles the execution of swaps type SwapExecutor struct { client *ethclient.Client logger *logger.Logger config *config.Config auth *bind.TransactOpts } func main() { app := &cli.App{ Name: "swap-cli", Usage: "CLI tool for executing swaps on Arbitrum using various DEX protocols", Flags: []cli.Flag{ &cli.StringFlag{ Name: "rpc-endpoint", Usage: "Arbitrum RPC endpoint URL", EnvVars: []string{"ARBITRUM_RPC_ENDPOINT"}, Required: true, }, &cli.StringFlag{ Name: "private-key", Usage: "Private key for transaction signing (hex format without 0x)", EnvVars: []string{"PRIVATE_KEY"}, }, &cli.StringFlag{ Name: "wallet-address", Usage: "Wallet address (if using external signer)", EnvVars: []string{"WALLET_ADDRESS"}, }, &cli.BoolFlag{ Name: "dry-run", Usage: "Simulate the swap without executing", Value: false, }, &cli.StringFlag{ Name: "log-level", Usage: "Log level (debug, info, warn, error)", Value: "info", }, }, Commands: []*cli.Command{ { Name: "uniswap-v3", Usage: "Execute swap on Uniswap V3", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "uniswap-v3") }, }, { Name: "uniswap-v2", Usage: "Execute swap on Uniswap V2", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "uniswap-v2") }, }, { Name: "sushiswap", Usage: "Execute swap on SushiSwap", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "sushiswap") }, }, { Name: "camelot-v3", Usage: "Execute swap on Camelot V3", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "camelot-v3") }, }, { Name: "traderjoe-v2", Usage: "Execute swap on TraderJoe V2", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "traderjoe-v2") }, }, { Name: "kyber-elastic", Usage: "Execute swap on KyberSwap Elastic", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return executeSwap(c, "kyber-elastic") }, }, { Name: "estimate-gas", Usage: "Estimate gas cost for a swap", Flags: getSwapFlags(), Action: func(c *cli.Context) error { return estimateGas(c) }, }, { Name: "check-allowance", Usage: "Check token allowance for a protocol", Flags: []cli.Flag{ &cli.StringFlag{ Name: "token", Usage: "Token contract address", Required: true, }, &cli.StringFlag{ Name: "spender", Usage: "Spender contract address (router)", Required: true, }, &cli.StringFlag{ Name: "owner", Usage: "Owner address (defaults to wallet address)", }, }, Action: func(c *cli.Context) error { return checkAllowance(c) }, }, { Name: "approve", Usage: "Approve token spending for a protocol", Flags: []cli.Flag{ &cli.StringFlag{ Name: "token", Usage: "Token contract address", Required: true, }, &cli.StringFlag{ Name: "spender", Usage: "Spender contract address (router)", Required: true, }, &cli.StringFlag{ Name: "amount", Usage: "Amount to approve (use 'max' for maximum approval)", Required: true, }, }, Action: func(c *cli.Context) error { return approveToken(c) }, }, }, } if err := app.Run(os.Args); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } func getSwapFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ Name: "token-in", Usage: "Input token contract address", Required: true, }, &cli.StringFlag{ Name: "token-out", Usage: "Output token contract address", Required: true, }, &cli.StringFlag{ Name: "amount-in", Usage: "Amount of input tokens (in smallest unit, e.g., wei for ETH)", Required: true, }, &cli.StringFlag{ Name: "min-amount-out", Usage: "Minimum amount of output tokens (calculated from slippage if not provided)", }, &cli.StringFlag{ Name: "recipient", Usage: "Recipient address (defaults to sender)", }, &cli.Float64Flag{ Name: "slippage", Usage: "Slippage tolerance in percentage (e.g., 0.5 for 0.5%)", Value: 0.5, }, &cli.Uint64Flag{ Name: "deadline", Usage: "Transaction deadline in seconds from now", Value: 300, // 5 minutes default }, &cli.StringFlag{ Name: "pool-fee", Usage: "Pool fee tier for V3 swaps (500, 3000, 10000)", Value: "3000", }, &cli.StringFlag{ Name: "gas-price", Usage: "Gas price in gwei (optional, uses network default if not specified)", }, &cli.Uint64Flag{ Name: "gas-limit", Usage: "Gas limit (optional, estimated if not specified)", }, } } func executeSwap(c *cli.Context, protocol string) error { // Initialize logger log := logger.New(c.String("log-level"), "text", "") // Parse swap parameters params, err := parseSwapParams(c) if err != nil { return fmt.Errorf("failed to parse swap parameters: %w", err) } // Initialize swap executor executor, err := newSwapExecutor(c, log) if err != nil { return fmt.Errorf("failed to initialize swap executor: %w", err) } log.Info("Swap Parameters", "protocol", protocol, "tokenIn", params.TokenIn.Hex(), "tokenOut", params.TokenOut.Hex(), "amountIn", params.AmountIn.String(), "minAmountOut", params.MinAmountOut.String(), "recipient", params.Recipient.Hex(), "slippage", fmt.Sprintf("%.2f%%", params.Slippage), ) if c.Bool("dry-run") { return executor.simulateSwap(params, protocol) } return executor.executeSwap(params, protocol) } func parseSwapParams(c *cli.Context) (*SwapParams, error) { // Parse token addresses tokenIn := common.HexToAddress(c.String("token-in")) tokenOut := common.HexToAddress(c.String("token-out")) // Parse amount in amountInStr := c.String("amount-in") amountIn, ok := new(big.Int).SetString(amountInStr, 10) if !ok { return nil, fmt.Errorf("invalid amount-in: %s", amountInStr) } // Parse minimum amount out var minAmountOut *big.Int if minAmountOutStr := c.String("min-amount-out"); minAmountOutStr != "" { var ok bool minAmountOut, ok = new(big.Int).SetString(minAmountOutStr, 10) if !ok { return nil, fmt.Errorf("invalid min-amount-out: %s", minAmountOutStr) } } else { // Calculate from slippage (simplified - in real implementation would fetch price) slippage := c.Float64("slippage") minAmountOut = calculateMinAmountOut(amountIn, slippage) } // Parse recipient var recipient common.Address if recipientStr := c.String("recipient"); recipientStr != "" { recipient = common.HexToAddress(recipientStr) } else { // Use wallet address as recipient if walletAddr := c.String("wallet-address"); walletAddr != "" { recipient = common.HexToAddress(walletAddr) } else { return nil, fmt.Errorf("recipient address required") } } // Calculate deadline deadline := uint64(time.Now().Unix()) + c.Uint64("deadline") return &SwapParams{ TokenIn: tokenIn, TokenOut: tokenOut, AmountIn: amountIn, MinAmountOut: minAmountOut, Recipient: recipient, Deadline: deadline, Protocol: "", Slippage: c.Float64("slippage"), }, nil } func newSwapExecutor(c *cli.Context, log *logger.Logger) (*SwapExecutor, error) { // Connect to Arbitrum RPC client, err := ethclient.Dial(c.String("rpc-endpoint")) if err != nil { return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %w", err) } // Load configuration (simplified) cfg := &config.Config{ Arbitrum: config.ArbitrumConfig{ RPCEndpoint: c.String("rpc-endpoint"), ChainID: 42161, // Arbitrum mainnet }, } // Setup auth if private key is provided var auth *bind.TransactOpts if privateKeyHex := c.String("private-key"); privateKeyHex != "" { privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { return nil, fmt.Errorf("invalid private key: %w", err) } chainID := big.NewInt(42161) // Arbitrum mainnet auth, err = bind.NewKeyedTransactorWithChainID(privateKey, chainID) if err != nil { return nil, fmt.Errorf("failed to create transactor: %w", err) } // Set gas price if specified if gasPriceStr := c.String("gas-price"); gasPriceStr != "" { gasPriceGwei, err := strconv.ParseFloat(gasPriceStr, 64) if err != nil { return nil, fmt.Errorf("invalid gas price: %w", err) } gasPrice := new(big.Int).Mul( big.NewInt(int64(gasPriceGwei*1e9)), big.NewInt(1), ) auth.GasPrice = gasPrice } // Set gas limit if specified if gasLimit := c.Uint64("gas-limit"); gasLimit > 0 { auth.GasLimit = gasLimit } } return &SwapExecutor{ client: client, logger: log, config: cfg, auth: auth, }, nil } func (se *SwapExecutor) simulateSwap(params *SwapParams, protocol string) error { se.logger.Info("🔍 SIMULATION MODE - No actual transaction will be sent") ctx := context.Background() // Check balances balance, err := se.getTokenBalance(ctx, params.TokenIn, params.Recipient) if err != nil { return fmt.Errorf("failed to get token balance: %w", err) } se.logger.Info("Balance Check", "token", params.TokenIn.Hex(), "balance", balance.String(), "required", params.AmountIn.String(), "sufficient", balance.Cmp(params.AmountIn) >= 0, ) if balance.Cmp(params.AmountIn) < 0 { se.logger.Warn("⚠️ Insufficient balance for swap") return fmt.Errorf("insufficient balance: have %s, need %s", balance.String(), params.AmountIn.String()) } // Estimate gas gasEstimate, err := se.estimateSwapGas(params, protocol) if err != nil { se.logger.Warn("Failed to estimate gas", "error", err.Error()) gasEstimate = 200000 // Default estimate } se.logger.Info("Gas Estimation", "estimatedGas", gasEstimate, "protocol", protocol, ) se.logger.Info("✅ Simulation completed successfully") return nil } func (se *SwapExecutor) executeSwap(params *SwapParams, protocol string) error { if se.auth == nil { return fmt.Errorf("no private key provided - cannot execute transaction") } se.logger.Info("🚀 Executing swap transaction") ctx := context.Background() // Pre-flight checks if err := se.preFlightChecks(ctx, params); err != nil { return fmt.Errorf("pre-flight checks failed: %w", err) } // Get router address for protocol routerAddr, err := se.getRouterAddress(protocol) if err != nil { return fmt.Errorf("failed to get router address: %w", err) } se.logger.Info("Router Information", "protocol", protocol, "router", routerAddr.Hex(), ) // Execute the swap based on protocol switch protocol { case "uniswap-v3": return se.executeUniswapV3Swap(ctx, params, routerAddr) case "uniswap-v2": return se.executeUniswapV2Swap(ctx, params, routerAddr) case "sushiswap": return se.executeSushiSwap(ctx, params, routerAddr) case "camelot-v3": return se.executeCamelotV3Swap(ctx, params, routerAddr) case "traderjoe-v2": return se.executeTraderJoeV2Swap(ctx, params, routerAddr) case "kyber-elastic": return se.executeKyberElasticSwap(ctx, params, routerAddr) default: return fmt.Errorf("unsupported protocol: %s", protocol) } } func (se *SwapExecutor) preFlightChecks(ctx context.Context, params *SwapParams) error { // Check balance balance, err := se.getTokenBalance(ctx, params.TokenIn, params.Recipient) if err != nil { return fmt.Errorf("failed to get token balance: %w", err) } if balance.Cmp(params.AmountIn) < 0 { return fmt.Errorf("insufficient balance: have %s, need %s", balance.String(), params.AmountIn.String()) } se.logger.Info("✅ Balance check passed", "balance", balance.String(), "required", params.AmountIn.String(), ) return nil } // Helper functions for protocol-specific implementations func (se *SwapExecutor) executeUniswapV3Swap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing Uniswap V3 swap") // Implementation would go here - this is a placeholder return fmt.Errorf("Uniswap V3 swap implementation pending") } func (se *SwapExecutor) executeUniswapV2Swap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing Uniswap V2 swap") // Implementation would go here - this is a placeholder return fmt.Errorf("Uniswap V2 swap implementation pending") } func (se *SwapExecutor) executeSushiSwap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing SushiSwap swap") // Implementation would go here - this is a placeholder return fmt.Errorf("SushiSwap swap implementation pending") } func (se *SwapExecutor) executeCamelotV3Swap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing Camelot V3 swap") // Implementation would go here - this is a placeholder return fmt.Errorf("Camelot V3 swap implementation pending") } func (se *SwapExecutor) executeTraderJoeV2Swap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing TraderJoe V2 swap") // Implementation would go here - this is a placeholder return fmt.Errorf("TraderJoe V2 swap implementation pending") } func (se *SwapExecutor) executeKyberElasticSwap(ctx context.Context, params *SwapParams, router common.Address) error { se.logger.Info("Executing KyberSwap Elastic swap") // Implementation would go here - this is a placeholder return fmt.Errorf("KyberSwap Elastic swap implementation pending") } // Utility functions func (se *SwapExecutor) getTokenBalance(ctx context.Context, token common.Address, owner common.Address) (*big.Int, error) { // Implementation would call ERC20 balanceOf // For now, return a placeholder return big.NewInt(1000000000000000000), nil // 1 ETH worth } func (se *SwapExecutor) estimateSwapGas(params *SwapParams, protocol string) (uint64, error) { // Implementation would estimate gas for the specific protocol // For now, return reasonable estimates switch protocol { case "uniswap-v3": return 150000, nil case "uniswap-v2": return 120000, nil default: return 200000, nil } } func (se *SwapExecutor) getRouterAddress(protocol string) (common.Address, error) { // Return known router addresses for each protocol on Arbitrum switch protocol { case "uniswap-v3": return common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), nil case "uniswap-v2": return common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24"), nil case "sushiswap": return common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), nil case "camelot-v3": return common.HexToAddress("0x1a3c9B1d2F0529D97f2afC5136Cc23e58f1FD35B"), nil case "traderjoe-v2": return common.HexToAddress("0x18556DA13313f3532c54711497A8FedAC273220E"), nil case "kyber-elastic": return common.HexToAddress("0x5F1dddbf348aC2fbe22a163e30F99F9ECE3DD50a"), nil default: return common.Address{}, fmt.Errorf("unknown protocol: %s", protocol) } } func calculateMinAmountOut(amountIn *big.Int, slippage float64) *big.Int { // Simple calculation: amountOut = amountIn * (1 - slippage/100) // In a real implementation, this would fetch current prices slippageMultiplier := 1.0 - (slippage / 100.0) amountInFloat := new(big.Float).SetInt(amountIn) minAmountOutFloat := new(big.Float).Mul(amountInFloat, big.NewFloat(slippageMultiplier)) minAmountOut, _ := minAmountOutFloat.Int(nil) return minAmountOut } // Additional commands func estimateGas(c *cli.Context) error { log := logger.New(c.String("log-level"), "text", "") params, err := parseSwapParams(c) if err != nil { return err } executor, err := newSwapExecutor(c, log) if err != nil { return err } protocol := strings.TrimPrefix(c.Command.FullName(), "estimate-gas ") if protocol == "estimate-gas" { protocol = "uniswap-v3" // default } gasEstimate, err := executor.estimateSwapGas(params, protocol) if err != nil { return err } log.Info("Gas Estimation", "protocol", protocol, "estimatedGas", gasEstimate, ) return nil } func checkAllowance(c *cli.Context) error { log := logger.New(c.String("log-level"), "text", "") log.Info("Checking token allowance - implementation pending") return nil } func approveToken(c *cli.Context) error { log := logger.New(c.String("log-level"), "text", "") log.Info("Approving token - implementation pending") return nil }