evm/uniswaprouter.go

151 lines
5.4 KiB
Go

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...)
}