821 lines
28 KiB
Go
821 lines
28 KiB
Go
package arbitrum
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
"github.com/fraktal/mev-beta/internal/logger"
|
|
"github.com/fraktal/mev-beta/pkg/oracle"
|
|
"github.com/fraktal/mev-beta/pkg/pools"
|
|
)
|
|
|
|
// RawL2Transaction represents a raw Arbitrum L2 transaction
|
|
type RawL2Transaction struct {
|
|
Hash string `json:"hash"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Value string `json:"value"`
|
|
Gas string `json:"gas"`
|
|
GasPrice string `json:"gasPrice"`
|
|
Input string `json:"input"`
|
|
Nonce string `json:"nonce"`
|
|
TransactionIndex string `json:"transactionIndex"`
|
|
Type string `json:"type"`
|
|
ChainID string `json:"chainId,omitempty"`
|
|
V string `json:"v,omitempty"`
|
|
R string `json:"r,omitempty"`
|
|
S string `json:"s,omitempty"`
|
|
}
|
|
|
|
// RawL2Block represents a raw Arbitrum L2 block
|
|
type RawL2Block struct {
|
|
Hash string `json:"hash"`
|
|
Number string `json:"number"`
|
|
Timestamp string `json:"timestamp"`
|
|
Transactions []RawL2Transaction `json:"transactions"`
|
|
}
|
|
|
|
// RawL2BlockWithLogs represents a raw Arbitrum L2 block with logs
|
|
type RawL2BlockWithLogs struct {
|
|
Hash string `json:"hash"`
|
|
Number string `json:"number"`
|
|
Timestamp string `json:"timestamp"`
|
|
Transactions []RawL2TransactionWithLogs `json:"transactions"`
|
|
}
|
|
|
|
// RawL2TransactionWithLogs includes transaction logs for pool discovery
|
|
type RawL2TransactionWithLogs struct {
|
|
Hash string `json:"hash"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Value string `json:"value"`
|
|
Gas string `json:"gas"`
|
|
GasPrice string `json:"gasPrice"`
|
|
Input string `json:"input"`
|
|
Logs []interface{} `json:"logs"`
|
|
TransactionIndex string `json:"transactionIndex"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// DEXFunctionSignature represents a DEX function signature
|
|
type DEXFunctionSignature struct {
|
|
Signature string
|
|
Name string
|
|
Protocol string
|
|
Description string
|
|
}
|
|
|
|
// ArbitrumL2Parser handles parsing of Arbitrum L2 transactions
|
|
type ArbitrumL2Parser struct {
|
|
client *rpc.Client
|
|
logger *logger.Logger
|
|
oracle *oracle.PriceOracle
|
|
|
|
// DEX contract addresses on Arbitrum
|
|
dexContracts map[common.Address]string
|
|
|
|
// DEX function signatures
|
|
dexFunctions map[string]DEXFunctionSignature
|
|
|
|
// Pool discovery system
|
|
poolDiscovery *pools.PoolDiscovery
|
|
}
|
|
|
|
// NewArbitrumL2Parser creates a new Arbitrum L2 transaction parser
|
|
func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger, priceOracle *oracle.PriceOracle) (*ArbitrumL2Parser, error) {
|
|
client, err := rpc.Dial(rpcEndpoint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %v", err)
|
|
}
|
|
|
|
parser := &ArbitrumL2Parser{
|
|
client: client,
|
|
logger: logger,
|
|
oracle: priceOracle,
|
|
dexContracts: make(map[common.Address]string),
|
|
dexFunctions: make(map[string]DEXFunctionSignature),
|
|
}
|
|
|
|
// Initialize DEX contracts and functions
|
|
parser.initializeDEXData()
|
|
|
|
// Initialize pool discovery system
|
|
parser.poolDiscovery = pools.NewPoolDiscovery(client, logger)
|
|
logger.Info(fmt.Sprintf("Pool discovery system initialized - %d pools, %d exchanges loaded",
|
|
parser.poolDiscovery.GetPoolCount(), parser.poolDiscovery.GetExchangeCount()))
|
|
|
|
return parser, nil
|
|
}
|
|
|
|
// initializeDEXData initializes known DEX contracts and function signatures
|
|
func (p *ArbitrumL2Parser) initializeDEXData() {
|
|
// Official Arbitrum DEX contracts
|
|
p.dexContracts[common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")] = "UniswapV2Factory"
|
|
p.dexContracts[common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")] = "UniswapV3Factory"
|
|
p.dexContracts[common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4")] = "SushiSwapFactory"
|
|
p.dexContracts[common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24")] = "UniswapV2Router02"
|
|
p.dexContracts[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3Router"
|
|
p.dexContracts[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router02"
|
|
p.dexContracts[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwapRouter"
|
|
p.dexContracts[common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88")] = "UniswapV3PositionManager"
|
|
|
|
// CORRECT DEX function signatures verified for Arbitrum (first 4 bytes of keccak256(function_signature))
|
|
|
|
// Uniswap V2 swap functions
|
|
p.dexFunctions["0x38ed1739"] = DEXFunctionSignature{
|
|
Signature: "0x38ed1739",
|
|
Name: "swapExactTokensForTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact tokens for tokens",
|
|
}
|
|
p.dexFunctions["0x8803dbee"] = DEXFunctionSignature{
|
|
Signature: "0x8803dbee",
|
|
Name: "swapTokensForExactTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap tokens for exact tokens",
|
|
}
|
|
p.dexFunctions["0x7ff36ab5"] = DEXFunctionSignature{
|
|
Signature: "0x7ff36ab5",
|
|
Name: "swapExactETHForTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact ETH for tokens",
|
|
}
|
|
p.dexFunctions["0x4a25d94a"] = DEXFunctionSignature{
|
|
Signature: "0x4a25d94a",
|
|
Name: "swapTokensForExactETH",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap tokens for exact ETH",
|
|
}
|
|
p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{
|
|
Signature: "0x18cbafe5",
|
|
Name: "swapExactTokensForETH",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact tokens for ETH",
|
|
}
|
|
p.dexFunctions["0x791ac947"] = DEXFunctionSignature{
|
|
Signature: "0x791ac947",
|
|
Name: "swapExactTokensForETHSupportingFeeOnTransferTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact tokens for ETH supporting fee-on-transfer tokens",
|
|
}
|
|
p.dexFunctions["0xb6f9de95"] = DEXFunctionSignature{
|
|
Signature: "0xb6f9de95",
|
|
Name: "swapExactETHForTokensSupportingFeeOnTransferTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact ETH for tokens supporting fee-on-transfer tokens",
|
|
}
|
|
p.dexFunctions["0x5c11d795"] = DEXFunctionSignature{
|
|
Signature: "0x5c11d795",
|
|
Name: "swapExactTokensForTokensSupportingFeeOnTransferTokens",
|
|
Protocol: "UniswapV2",
|
|
Description: "Swap exact tokens for tokens supporting fee-on-transfer tokens",
|
|
}
|
|
|
|
// Uniswap V2 liquidity functions
|
|
p.dexFunctions["0xe8e33700"] = DEXFunctionSignature{
|
|
Signature: "0xe8e33700",
|
|
Name: "addLiquidity",
|
|
Protocol: "UniswapV2",
|
|
Description: "Add liquidity to pool",
|
|
}
|
|
p.dexFunctions["0xf305d719"] = DEXFunctionSignature{
|
|
Signature: "0xf305d719",
|
|
Name: "addLiquidityETH",
|
|
Protocol: "UniswapV2",
|
|
Description: "Add liquidity with ETH",
|
|
}
|
|
p.dexFunctions["0xbaa2abde"] = DEXFunctionSignature{
|
|
Signature: "0xbaa2abde",
|
|
Name: "removeLiquidity",
|
|
Protocol: "UniswapV2",
|
|
Description: "Remove liquidity from pool",
|
|
}
|
|
p.dexFunctions["0x02751cec"] = DEXFunctionSignature{
|
|
Signature: "0x02751cec",
|
|
Name: "removeLiquidityETH",
|
|
Protocol: "UniswapV2",
|
|
Description: "Remove liquidity with ETH",
|
|
}
|
|
|
|
// Uniswap V3 swap functions
|
|
p.dexFunctions["0x414bf389"] = DEXFunctionSignature{
|
|
Signature: "0x414bf389",
|
|
Name: "exactInputSingle",
|
|
Protocol: "UniswapV3",
|
|
Description: "Exact input single swap",
|
|
}
|
|
p.dexFunctions["0xc04b8d59"] = DEXFunctionSignature{
|
|
Signature: "0xc04b8d59",
|
|
Name: "exactInput",
|
|
Protocol: "UniswapV3",
|
|
Description: "Exact input multi-hop swap",
|
|
}
|
|
p.dexFunctions["0xdb3e2198"] = DEXFunctionSignature{
|
|
Signature: "0xdb3e2198",
|
|
Name: "exactOutputSingle",
|
|
Protocol: "UniswapV3",
|
|
Description: "Exact output single swap",
|
|
}
|
|
p.dexFunctions["0xf28c0498"] = DEXFunctionSignature{
|
|
Signature: "0xf28c0498",
|
|
Name: "exactOutput",
|
|
Protocol: "UniswapV3",
|
|
Description: "Exact output multi-hop swap",
|
|
}
|
|
p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{
|
|
Signature: "0xac9650d8",
|
|
Name: "multicall",
|
|
Protocol: "UniswapV3",
|
|
Description: "Batch multiple function calls",
|
|
}
|
|
|
|
// Uniswap V3 position management functions
|
|
p.dexFunctions["0x88316456"] = DEXFunctionSignature{
|
|
Signature: "0x88316456",
|
|
Name: "mint",
|
|
Protocol: "UniswapV3",
|
|
Description: "Mint new liquidity position",
|
|
}
|
|
p.dexFunctions["0xfc6f7865"] = DEXFunctionSignature{
|
|
Signature: "0xfc6f7865",
|
|
Name: "collect",
|
|
Protocol: "UniswapV3",
|
|
Description: "Collect fees from position",
|
|
}
|
|
p.dexFunctions["0x219f5d17"] = DEXFunctionSignature{
|
|
Signature: "0x219f5d17",
|
|
Name: "increaseLiquidity",
|
|
Protocol: "UniswapV3",
|
|
Description: "Increase liquidity in position",
|
|
}
|
|
p.dexFunctions["0x0c49ccbe"] = DEXFunctionSignature{
|
|
Signature: "0x0c49ccbe",
|
|
Name: "decreaseLiquidity",
|
|
Protocol: "UniswapV3",
|
|
Description: "Decrease liquidity in position",
|
|
}
|
|
}
|
|
|
|
// GetBlockByNumber fetches a block with full transaction details using raw RPC
|
|
func (p *ArbitrumL2Parser) GetBlockByNumber(ctx context.Context, blockNumber uint64) (*RawL2Block, error) {
|
|
var block RawL2Block
|
|
|
|
blockNumHex := fmt.Sprintf("0x%x", blockNumber)
|
|
err := p.client.CallContext(ctx, &block, "eth_getBlockByNumber", blockNumHex, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get block %d: %v", blockNumber, err)
|
|
}
|
|
|
|
p.logger.Debug(fmt.Sprintf("Retrieved L2 block %d with %d transactions", blockNumber, len(block.Transactions)))
|
|
return &block, nil
|
|
}
|
|
|
|
// ParseDEXTransactions analyzes transactions in a block for DEX interactions
|
|
func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL2Block) []DEXTransaction {
|
|
var dexTransactions []DEXTransaction
|
|
|
|
for _, tx := range block.Transactions {
|
|
if dexTx := p.parseDEXTransaction(tx); dexTx != nil {
|
|
dexTransactions = append(dexTransactions, *dexTx)
|
|
}
|
|
}
|
|
|
|
if len(dexTransactions) > 0 {
|
|
p.logger.Info(fmt.Sprintf("Block %s: Found %d DEX transactions", block.Number, len(dexTransactions)))
|
|
}
|
|
|
|
return dexTransactions
|
|
}
|
|
|
|
// SwapDetails contains detailed information about a DEX swap
|
|
type SwapDetails struct {
|
|
AmountIn *big.Int
|
|
AmountOut *big.Int
|
|
AmountMin *big.Int
|
|
TokenIn string
|
|
TokenOut string
|
|
Fee uint32
|
|
Deadline uint64
|
|
Recipient string
|
|
IsValid bool
|
|
}
|
|
|
|
// DEXTransaction represents a parsed DEX transaction
|
|
type DEXTransaction struct {
|
|
Hash string
|
|
From string
|
|
To string
|
|
Value *big.Int
|
|
FunctionSig string
|
|
FunctionName string
|
|
Protocol string
|
|
InputData []byte
|
|
ContractName string
|
|
BlockNumber string
|
|
SwapDetails *SwapDetails // Detailed swap information
|
|
}
|
|
|
|
// parseDEXTransaction checks if a transaction is a DEX interaction
|
|
func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransaction {
|
|
// Skip transactions without recipient (contract creation)
|
|
if tx.To == "" || tx.To == "0x" {
|
|
return nil
|
|
}
|
|
|
|
// Skip transactions without input data
|
|
if tx.Input == "" || tx.Input == "0x" || len(tx.Input) < 10 {
|
|
return nil
|
|
}
|
|
|
|
toAddr := common.HexToAddress(tx.To)
|
|
|
|
// Check if transaction is to a known DEX contract
|
|
contractName, isDEXContract := p.dexContracts[toAddr]
|
|
|
|
// Extract function signature (first 4 bytes of input data)
|
|
functionSig := tx.Input[:10] // "0x" + 8 hex chars = 10 chars
|
|
|
|
// Check if function signature matches known DEX functions
|
|
if funcInfo, isDEXFunction := p.dexFunctions[functionSig]; isDEXFunction {
|
|
|
|
// Parse value
|
|
value := big.NewInt(0)
|
|
if tx.Value != "" && tx.Value != "0x" && tx.Value != "0x0" {
|
|
value.SetString(strings.TrimPrefix(tx.Value, "0x"), 16)
|
|
}
|
|
|
|
// Parse input data
|
|
inputData, err := hex.DecodeString(strings.TrimPrefix(tx.Input, "0x"))
|
|
if err != nil {
|
|
p.logger.Debug(fmt.Sprintf("Failed to decode input data for transaction %s: %v", tx.Hash, err))
|
|
inputData = []byte{}
|
|
}
|
|
|
|
// Decode function parameters based on function type
|
|
swapDetails := p.decodeFunctionDataStructured(funcInfo, inputData)
|
|
|
|
// Use detailed opportunity logging if swap details are available
|
|
if swapDetails != nil && swapDetails.IsValid && swapDetails.AmountIn != nil {
|
|
amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18))
|
|
amountOutFloat := float64(0)
|
|
if swapDetails.AmountOut != nil {
|
|
amountOutFloat, _ = new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountOut), big.NewFloat(1e18)).Float64()
|
|
}
|
|
amountMinFloat := float64(0)
|
|
if swapDetails.AmountMin != nil {
|
|
amountMinFloat, _ = new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountMin), big.NewFloat(1e18)).Float64()
|
|
}
|
|
amountInFloatVal, _ := amountInFloat.Float64()
|
|
|
|
// Calculate estimated profit using price oracle
|
|
estimatedProfitUSD, err := p.calculateProfitWithOracle(swapDetails)
|
|
if err != nil {
|
|
p.logger.Debug(fmt.Sprintf("Failed to calculate profit with oracle: %v", err))
|
|
estimatedProfitUSD = 0.0
|
|
}
|
|
|
|
additionalData := map[string]interface{}{
|
|
"tokenIn": swapDetails.TokenIn,
|
|
"tokenOut": swapDetails.TokenOut,
|
|
"fee": swapDetails.Fee,
|
|
"deadline": swapDetails.Deadline,
|
|
"recipient": swapDetails.Recipient,
|
|
"contractName": contractName,
|
|
"functionSig": functionSig,
|
|
}
|
|
|
|
p.logger.Opportunity(tx.Hash, tx.From, tx.To, funcInfo.Name, funcInfo.Protocol,
|
|
amountInFloatVal, amountOutFloat, amountMinFloat, estimatedProfitUSD, additionalData)
|
|
} else {
|
|
// Fallback to simple logging
|
|
swapDetailsStr := p.decodeFunctionData(funcInfo, inputData)
|
|
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH%s",
|
|
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol,
|
|
new(big.Float).Quo(new(big.Float).SetInt(value), big.NewFloat(1e18)).String(), swapDetailsStr))
|
|
}
|
|
|
|
return &DEXTransaction{
|
|
Hash: tx.Hash,
|
|
From: tx.From,
|
|
To: tx.To,
|
|
Value: value,
|
|
FunctionSig: functionSig,
|
|
FunctionName: funcInfo.Name,
|
|
Protocol: funcInfo.Protocol,
|
|
InputData: inputData,
|
|
ContractName: contractName,
|
|
BlockNumber: "", // Will be set by caller
|
|
SwapDetails: swapDetails,
|
|
}
|
|
}
|
|
|
|
// Check if it's to a known DEX contract but unknown function
|
|
if isDEXContract {
|
|
p.logger.Debug(fmt.Sprintf("Unknown DEX function call: %s -> %s (%s), Function: %s",
|
|
tx.From, tx.To, contractName, functionSig))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// decodeFunctionData extracts parameters from transaction input data
|
|
func (p *ArbitrumL2Parser) decodeFunctionData(funcInfo DEXFunctionSignature, inputData []byte) string {
|
|
if len(inputData) < 4 {
|
|
return ""
|
|
}
|
|
|
|
// Skip the 4-byte function selector
|
|
params := inputData[4:]
|
|
|
|
switch funcInfo.Name {
|
|
case "swapExactTokensForTokens":
|
|
return p.decodeSwapExactTokensForTokens(params)
|
|
case "swapTokensForExactTokens":
|
|
return p.decodeSwapTokensForExactTokens(params)
|
|
case "swapExactETHForTokens":
|
|
return p.decodeSwapExactETHForTokens(params)
|
|
case "swapExactTokensForETH":
|
|
return p.decodeSwapExactTokensForETH(params)
|
|
case "exactInputSingle":
|
|
return p.decodeExactInputSingle(params)
|
|
case "exactInput":
|
|
return p.decodeExactInput(params)
|
|
case "exactOutputSingle":
|
|
return p.decodeExactOutputSingle(params)
|
|
case "multicall":
|
|
return p.decodeMulticall(params)
|
|
default:
|
|
return fmt.Sprintf(", Raw input: %d bytes", len(inputData))
|
|
}
|
|
}
|
|
|
|
// decodeSwapExactTokensForTokens decodes UniswapV2 swapExactTokensForTokens parameters
|
|
// function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
|
|
func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokens(params []byte) string {
|
|
if len(params) < 160 { // 5 parameters * 32 bytes each
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
// Decode parameters (simplified - real ABI decoding would be more robust)
|
|
amountIn := new(big.Int).SetBytes(params[0:32])
|
|
amountOutMin := new(big.Int).SetBytes(params[32:64])
|
|
|
|
// Convert to readable format
|
|
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
|
|
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountIn: %s tokens, MinOut: %s tokens",
|
|
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeSwapTokensForExactTokens decodes UniswapV2 swapTokensForExactTokens parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokens(params []byte) string {
|
|
if len(params) < 160 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
amountOut := new(big.Int).SetBytes(params[0:32])
|
|
amountInMax := new(big.Int).SetBytes(params[32:64])
|
|
|
|
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
|
|
amountInMaxEth := new(big.Float).Quo(new(big.Float).SetInt(amountInMax), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountOut: %s tokens, MaxIn: %s tokens",
|
|
amountOutEth.Text('f', 6), amountInMaxEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeSwapExactETHForTokens decodes UniswapV2 swapExactETHForTokens parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapExactETHForTokens(params []byte) string {
|
|
if len(params) < 32 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
amountOutMin := new(big.Int).SetBytes(params[0:32])
|
|
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", MinOut: %s tokens", amountOutMinEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeSwapExactTokensForETH decodes UniswapV2 swapExactTokensForETH parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapExactTokensForETH(params []byte) string {
|
|
if len(params) < 64 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
amountIn := new(big.Int).SetBytes(params[0:32])
|
|
amountOutMin := new(big.Int).SetBytes(params[32:64])
|
|
|
|
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
|
|
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountIn: %s tokens, MinETH: %s",
|
|
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeExactInputSingle decodes UniswapV3 exactInputSingle parameters
|
|
func (p *ArbitrumL2Parser) decodeExactInputSingle(params []byte) string {
|
|
if len(params) < 160 { // ExactInputSingleParams struct
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
// Simplified decoding - real implementation would parse the struct properly
|
|
amountIn := new(big.Int).SetBytes(params[128:160]) // approximation
|
|
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountIn: %s tokens", amountInEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeExactInput decodes UniswapV3 exactInput parameters
|
|
func (p *ArbitrumL2Parser) decodeExactInput(params []byte) string {
|
|
if len(params) < 128 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
amountIn := new(big.Int).SetBytes(params[64:96]) // approximation
|
|
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountIn: %s tokens (multi-hop)", amountInEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeExactOutputSingle decodes UniswapV3 exactOutputSingle parameters
|
|
func (p *ArbitrumL2Parser) decodeExactOutputSingle(params []byte) string {
|
|
if len(params) < 160 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
amountOut := new(big.Int).SetBytes(params[160:192]) // approximation
|
|
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
|
|
|
|
return fmt.Sprintf(", AmountOut: %s tokens", amountOutEth.Text('f', 6))
|
|
}
|
|
|
|
// decodeMulticall decodes UniswapV3 multicall parameters
|
|
func (p *ArbitrumL2Parser) decodeMulticall(params []byte) string {
|
|
if len(params) < 32 {
|
|
return ", Invalid parameters"
|
|
}
|
|
|
|
// Multicall contains an array of encoded function calls
|
|
// This is complex to decode without full ABI parsing
|
|
return fmt.Sprintf(", Multicall with %d bytes of data", len(params))
|
|
}
|
|
|
|
// decodeFunctionDataStructured extracts structured parameters from transaction input data
|
|
func (p *ArbitrumL2Parser) decodeFunctionDataStructured(funcInfo DEXFunctionSignature, inputData []byte) *SwapDetails {
|
|
if len(inputData) < 4 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
// Skip the 4-byte function selector
|
|
params := inputData[4:]
|
|
|
|
switch funcInfo.Name {
|
|
case "swapExactTokensForTokens":
|
|
return p.decodeSwapExactTokensForTokensStructured(params)
|
|
case "swapTokensForExactTokens":
|
|
return p.decodeSwapTokensForExactTokensStructured(params)
|
|
case "swapExactETHForTokens":
|
|
return p.decodeSwapExactETHForTokensStructured(params)
|
|
case "swapExactTokensForETH":
|
|
return p.decodeSwapExactTokensForETHStructured(params)
|
|
case "exactInputSingle":
|
|
return p.decodeExactInputSingleStructured(params)
|
|
case "exactInput":
|
|
return p.decodeExactInputStructured(params)
|
|
case "exactOutputSingle":
|
|
return p.decodeExactOutputSingleStructured(params)
|
|
case "multicall":
|
|
return p.decodeMulticallStructured(params)
|
|
default:
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
}
|
|
|
|
// decodeSwapExactTokensForTokensStructured decodes UniswapV2 swapExactTokensForTokens parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokensStructured(params []byte) *SwapDetails {
|
|
if len(params) < 160 { // 5 parameters * 32 bytes each
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountIn: new(big.Int).SetBytes(params[0:32]),
|
|
AmountMin: new(big.Int).SetBytes(params[32:64]),
|
|
TokenIn: "unknown", // Would need to decode path array
|
|
TokenOut: "unknown", // Would need to decode path array
|
|
Deadline: new(big.Int).SetBytes(params[128:160]).Uint64(),
|
|
Recipient: fmt.Sprintf("0x%x", params[96:128]), // address is last 20 bytes
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeSwapExactTokensForETHStructured decodes UniswapV2 swapExactTokensForETH parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapExactTokensForETHStructured(params []byte) *SwapDetails {
|
|
if len(params) < 64 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountIn: new(big.Int).SetBytes(params[0:32]),
|
|
AmountMin: new(big.Int).SetBytes(params[32:64]),
|
|
TokenIn: "unknown",
|
|
TokenOut: "ETH",
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeExactInputSingleStructured decodes UniswapV3 exactInputSingle parameters
|
|
func (p *ArbitrumL2Parser) decodeExactInputSingleStructured(params []byte) *SwapDetails {
|
|
if len(params) < 160 { // ExactInputSingleParams struct
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
// Simplified decoding - real implementation would parse the struct properly
|
|
return &SwapDetails{
|
|
AmountIn: new(big.Int).SetBytes(params[128:160]),
|
|
TokenIn: fmt.Sprintf("0x%x", params[0:32]), // tokenIn
|
|
TokenOut: fmt.Sprintf("0x%x", params[32:64]), // tokenOut
|
|
Fee: uint32(new(big.Int).SetBytes(params[64:96]).Uint64()), // fee
|
|
Recipient: fmt.Sprintf("0x%x", params[96:128]), // recipient
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeSwapTokensForExactTokensStructured decodes UniswapV2 swapTokensForExactTokens parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokensStructured(params []byte) *SwapDetails {
|
|
if len(params) < 160 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountOut: new(big.Int).SetBytes(params[0:32]),
|
|
AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in
|
|
TokenIn: "unknown",
|
|
TokenOut: "unknown",
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeSwapExactETHForTokensStructured decodes UniswapV2 swapExactETHForTokens parameters
|
|
func (p *ArbitrumL2Parser) decodeSwapExactETHForTokensStructured(params []byte) *SwapDetails {
|
|
if len(params) < 32 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountMin: new(big.Int).SetBytes(params[0:32]),
|
|
TokenIn: "ETH",
|
|
TokenOut: "unknown",
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeExactInputStructured decodes UniswapV3 exactInput parameters
|
|
func (p *ArbitrumL2Parser) decodeExactInputStructured(params []byte) *SwapDetails {
|
|
if len(params) < 128 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountIn: new(big.Int).SetBytes(params[64:96]),
|
|
TokenIn: "unknown", // Would need to decode path
|
|
TokenOut: "unknown", // Would need to decode path
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeExactOutputSingleStructured decodes UniswapV3 exactOutputSingle parameters
|
|
func (p *ArbitrumL2Parser) decodeExactOutputSingleStructured(params []byte) *SwapDetails {
|
|
if len(params) < 160 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
return &SwapDetails{
|
|
AmountOut: new(big.Int).SetBytes(params[160:192]),
|
|
TokenIn: fmt.Sprintf("0x%x", params[0:32]),
|
|
TokenOut: fmt.Sprintf("0x%x", params[32:64]),
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// decodeMulticallStructured decodes UniswapV3 multicall parameters
|
|
func (p *ArbitrumL2Parser) decodeMulticallStructured(params []byte) *SwapDetails {
|
|
if len(params) < 32 {
|
|
return &SwapDetails{IsValid: false}
|
|
}
|
|
|
|
// For multicall, we'd need to decode the individual calls
|
|
// This is a placeholder
|
|
return &SwapDetails{
|
|
TokenIn: "unknown",
|
|
TokenOut: "unknown",
|
|
IsValid: true,
|
|
}
|
|
}
|
|
|
|
// calculateProfitWithOracle calculates profit estimation using the price oracle
|
|
func (p *ArbitrumL2Parser) calculateProfitWithOracle(swapDetails *SwapDetails) (float64, error) {
|
|
if p.oracle == nil {
|
|
return 0.0, fmt.Errorf("price oracle not available")
|
|
}
|
|
|
|
// Skip calculation for invalid swaps
|
|
if !swapDetails.IsValid || swapDetails.AmountIn == nil || swapDetails.AmountIn.Sign() == 0 {
|
|
return 0.0, nil
|
|
}
|
|
|
|
// Convert token addresses
|
|
var tokenIn, tokenOut common.Address
|
|
var err error
|
|
|
|
switch v := swapDetails.TokenIn.(type) {
|
|
case string:
|
|
if !common.IsHexAddress(v) {
|
|
return 0.0, fmt.Errorf("invalid tokenIn address: %s", v)
|
|
}
|
|
tokenIn = common.HexToAddress(v)
|
|
case common.Address:
|
|
tokenIn = v
|
|
default:
|
|
return 0.0, fmt.Errorf("unsupported tokenIn type: %T", v)
|
|
}
|
|
|
|
switch v := swapDetails.TokenOut.(type) {
|
|
case string:
|
|
if !common.IsHexAddress(v) {
|
|
return 0.0, fmt.Errorf("invalid tokenOut address: %s", v)
|
|
}
|
|
tokenOut = common.HexToAddress(v)
|
|
case common.Address:
|
|
tokenOut = v
|
|
default:
|
|
return 0.0, fmt.Errorf("unsupported tokenOut type: %T", v)
|
|
}
|
|
|
|
// Create price request
|
|
priceReq := &oracle.PriceRequest{
|
|
TokenIn: tokenIn,
|
|
TokenOut: tokenOut,
|
|
AmountIn: swapDetails.AmountIn,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// Get price from oracle with timeout
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
priceResp, err := p.oracle.GetPrice(ctx, priceReq)
|
|
if err != nil {
|
|
return 0.0, fmt.Errorf("oracle price lookup failed: %w", err)
|
|
}
|
|
|
|
if !priceResp.Valid {
|
|
return 0.0, fmt.Errorf("oracle returned invalid price")
|
|
}
|
|
|
|
// Convert amounts to float for USD calculation (assuming 18 decimals)
|
|
amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18))
|
|
amountOutFloat := new(big.Float).Quo(new(big.Float).SetInt(priceResp.AmountOut), big.NewFloat(1e18))
|
|
|
|
amountInVal, _ := amountInFloat.Float64()
|
|
amountOutVal, _ := amountOutFloat.Float64()
|
|
|
|
// Estimate profit based on price difference
|
|
// This is a simplified calculation - in reality you'd need:
|
|
// 1. USD prices for both tokens
|
|
// 2. Gas cost estimation
|
|
// 3. Market impact analysis
|
|
// 4. Arbitrage opportunity assessment
|
|
|
|
// For now, calculate potential arbitrage profit as percentage of swap value
|
|
profitPercentage := 0.0
|
|
if amountInVal > 0 {
|
|
// Simple profit estimation based on price impact
|
|
slippageBps := priceResp.SlippageBps.Int64()
|
|
if slippageBps > 0 {
|
|
// Higher slippage = higher potential arbitrage profit
|
|
profitPercentage = float64(slippageBps) / 10000.0 * 0.1 // 10% of slippage as profit estimate
|
|
}
|
|
}
|
|
|
|
// Convert to USD estimate (simplified - assumes token has $1 base value)
|
|
estimatedProfitUSD := amountInVal * profitPercentage
|
|
|
|
p.logger.Debug(fmt.Sprintf("Calculated profit for swap %s->%s: amountIn=%.6f, amountOut=%.6f, slippage=%d bps, profit=$%.2f",
|
|
tokenIn.Hex()[:8], tokenOut.Hex()[:8], amountInVal, amountOutVal, slippageBps, estimatedProfitUSD))
|
|
|
|
return estimatedProfitUSD, nil
|
|
}
|
|
|
|
// Close closes the RPC connection
|
|
func (p *ArbitrumL2Parser) Close() {
|
|
if p.client != nil {
|
|
p.client.Close()
|
|
}
|
|
}
|