package evm import ( "context" "math/big" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/shopspring/decimal" ) type UniswapRouterContract interface { SwapExactTokensForTokens(opts *bind.TransactOpts, amountIn *big.Int, amountOutMin *big.Int, path []common.Address, to common.Address, deadline *big.Int) (*types.Transaction, error) GetAmountsOut(opts *bind.CallOpts, amountIn *big.Int, path []common.Address) ([]*big.Int, error) AddLiquidity(opts *bind.TransactOpts, tokenA common.Address, tokenB common.Address, amountADesired *big.Int, amountBDesired *big.Int, amountAMin *big.Int, amountBMin *big.Int, to common.Address, deadline *big.Int) (*types.Transaction, error) } type UniswapRouter struct { name string client Client contract UniswapRouterContract deadline time.Duration } type UniswapRouterOption func(s *UniswapRouter) *UniswapRouter func (s *UniswapRouter) WithOperationDeadline(deadline time.Duration) UniswapRouterOption { return func(s *UniswapRouter) *UniswapRouter { s.deadline = deadline return s } } func NewUniswapRouter(name string, client Client, contract UniswapRouterContract) *UniswapRouter { return &UniswapRouter{ name: name, client: client, contract: contract, deadline: 15 * time.Minute, } } func (s *UniswapRouter) Name() string { return s.name } func (s *UniswapRouter) SwapTokenToToken(ctx context.Context, path []common.Address, amount decimal.Decimal, slippage decimal.Decimal, opts ...ExecutionOption) (Transaction, error) { if len(path) < 2 { return nil, errors.Errorf("swap path must contain at least 2 addresses, currently %d", len(path)) } tokenInput, err := s.client.TokenService().TokenByAddress(path[0]) if err != nil { return nil, errors.WithStack(err) } tokenOutput, err := s.client.TokenService().TokenByAddress(path[len(path)-1]) if err != nil { return nil, errors.WithStack(err) } log.Debug().Msgf("[SWAP] swaping %s %s to %s with path %v", amount, tokenInput.Symbol(), tokenOutput.Symbol(), path) inputBalance, err := tokenInput.Balance() if err != nil { return nil, errors.Wrap(err, "getting from token balance") } log.Debug().Msgf("[SWAP] balance of token to swap : %s", inputBalance) if inputBalance.LessThan(amount) { return nil, errors.Errorf("token balance %s is less than swap amount asked %s", inputBalance, amount) } expectedout, err := s.contract.GetAmountsOut(&bind.CallOpts{}, tokenInput.ValueToBigInt(amount), path) if err != nil { return nil, errors.Wrap(err, "getting expected amount in target token") } decexp := tokenOutput.ValueFromBigInt(expectedout[len(expectedout)-1]) expectedwithslippage := decexp.Sub(decexp.Mul(slippage).Div(decimal.NewFromInt(100))) log.Debug().Msgf("[SWAP] expected amounts : %v", expectedout) log.Debug().Msgf("[SWAP] expected ratio : %s", decexp.Div(amount)) log.Debug().Msgf("[SWAP] expected out with slippage : %s (slippage %s%%)", expectedwithslippage, slippage) log.Debug().Msgf("[SWAP] swaping...") tx, err := s.client.Execute(ctx, func(ctx context.Context, options *TransactOpts) (*types.Transaction, error) { return s.contract.SwapExactTokensForTokens( options.TransactOpts, tokenInput.ValueToBigInt(amount), tokenOutput.ValueToBigInt(expectedwithslippage), path, s.client.PublicAddress(), s.getDeadline()) }, opts...) if err != nil { return nil, errors.Wrap(err, "swaping") } log.Debug().Msgf("[SWAP] swaping done") return tx, nil } func (s *UniswapRouter) getDeadline() *big.Int { return big.NewInt(time.Now().Add(s.deadline).Unix()) } func (s *UniswapRouter) GetPriceInUSDC(token Token) (decimal.Decimal, error) { return s.GetTokenQuoteInUSDC(token, decimal.NewFromInt(1)) } func (s *UniswapRouter) GetTokenQuoteInUSDC(token Token, amount decimal.Decimal) (decimal.Decimal, error) { usdc, err := s.client.TokenService().TokenBySymbol("USDC") if err != nil { return decimal.Zero, errors.Wrap(err, "getting usdc token") } return s.GetAmountRate(token, usdc, amount) } func (s *UniswapRouter) GetRate(token0 Token, token1 Token) (decimal.Decimal, error) { return s.GetAmountRate(token0, token1, decimal.NewFromInt(1)) } func (s *UniswapRouter) GetAmountRate(token0 Token, token1 Token, amount decimal.Decimal) (decimal.Decimal, error) { if amount.LessThanOrEqual(decimal.Zero) { return decimal.Zero, nil } expectedout, err := s.contract.GetAmountsOut(&bind.CallOpts{}, token0.ValueToBigInt(amount), []common.Address{token0.Address(), token1.Address()}) if err != nil { return decimal.Zero, errors.Wrap(err, "getting expected amount in quote token") } return token1.ValueFromBigInt(expectedout[len(expectedout)-1]), nil } func (s *UniswapRouter) AddLiquidity(ctx context.Context, token1 Token, token2 Token, amounttoken1 decimal.Decimal, amounttoken2 decimal.Decimal, opts ...ExecutionOption) (Transaction, error) { return s.client.Execute(ctx, func(ctx context.Context, options *TransactOpts) (*types.Transaction, error) { return s.contract.AddLiquidity( options.TransactOpts, token1.Address(), token2.Address(), token1.ValueToBigInt(amounttoken1), token2.ValueToBigInt(amounttoken2), token1.ValueToBigInt(amounttoken1.Sub(amounttoken1.Mul(decimal.NewFromFloat(0.01)))), token2.ValueToBigInt(amounttoken2.Sub(amounttoken2.Mul(decimal.NewFromFloat(0.01)))), s.client.PublicAddress(), s.getDeadline()) }, opts...) }