185 lines
5.4 KiB
Go
185 lines
5.4 KiB
Go
package ethereum
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
)
|
|
|
|
const explorerTxPrefix = "https://etherscan.io/tx/"
|
|
|
|
// UniswapV2 wraps an Ethereum Client and provides Uniswap V2 Router swap methods.
|
|
type UniswapV2 struct {
|
|
client *Client
|
|
routerAddr common.Address
|
|
wethAddr common.Address
|
|
quoteAddr common.Address
|
|
quoteSymbol string
|
|
quoteDec int
|
|
slippagePct float64
|
|
routerABI abi.ABI
|
|
erc20ABI abi.ABI
|
|
}
|
|
|
|
func NewUniswapV2(
|
|
client *Client,
|
|
routerAddr, wethAddr, quoteAddr string,
|
|
quoteSymbol string,
|
|
quoteDecimals int,
|
|
slippagePct float64,
|
|
) (*UniswapV2, error) {
|
|
rABI, err := abi.JSON(mustRouterABI())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse router ABI: %w", err)
|
|
}
|
|
eABI, err := abi.JSON(mustERC20ABI())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse ERC20 ABI: %w", err)
|
|
}
|
|
return &UniswapV2{
|
|
client: client,
|
|
routerAddr: common.HexToAddress(routerAddr),
|
|
wethAddr: common.HexToAddress(wethAddr),
|
|
quoteAddr: common.HexToAddress(quoteAddr),
|
|
quoteSymbol: quoteSymbol,
|
|
quoteDec: quoteDecimals,
|
|
slippagePct: slippagePct,
|
|
routerABI: rABI,
|
|
erc20ABI: eABI,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UniswapV2) ExplorerURL(txHash string) string {
|
|
return explorerTxPrefix + txHash
|
|
}
|
|
|
|
// TokenBalance returns the ERC20 token balance as a human-readable float.
|
|
func (u *UniswapV2) TokenBalance(ctx context.Context) (float64, error) {
|
|
data, err := u.erc20ABI.Pack("balanceOf", u.client.wallet)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
result, err := u.client.CallContract(ctx, u.quoteAddr, data)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("balanceOf call: %w", err)
|
|
}
|
|
bal := new(big.Int).SetBytes(result)
|
|
divisor := math.Pow10(u.quoteDec)
|
|
f, _ := new(big.Float).Quo(new(big.Float).SetInt(bal), new(big.Float).SetFloat64(divisor)).Float64()
|
|
return f, nil
|
|
}
|
|
|
|
// ETHBalance returns wallet ETH balance as a human-readable float.
|
|
func (u *UniswapV2) ETHBalance(ctx context.Context) (float64, error) {
|
|
bal, err := u.client.ETHBalance(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
f, _ := new(big.Float).Quo(new(big.Float).SetInt(bal), new(big.Float).SetFloat64(1e18)).Float64()
|
|
return f, nil
|
|
}
|
|
|
|
// EnsureAllowance checks the router's allowance for the quote token and approves max if needed.
|
|
func (u *UniswapV2) EnsureAllowance(ctx context.Context, requiredAmount float64) error {
|
|
data, err := u.erc20ABI.Pack("allowance", u.client.wallet, u.routerAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := u.client.CallContract(ctx, u.quoteAddr, data)
|
|
if err != nil {
|
|
return fmt.Errorf("allowance call: %w", err)
|
|
}
|
|
current := new(big.Int).SetBytes(result)
|
|
|
|
requiredWei := toTokenWei(requiredAmount*2, u.quoteDec)
|
|
if current.Cmp(requiredWei) >= 0 {
|
|
return nil
|
|
}
|
|
|
|
fmt.Printf("Setting %s allowance for Uniswap Router...\n", u.quoteSymbol)
|
|
maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
|
|
approveData, err := u.erc20ABI.Pack("approve", u.routerAddr, maxUint256)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txHash, err := u.client.SignAndSend(ctx, u.quoteAddr, big.NewInt(0), approveData)
|
|
if err != nil {
|
|
return fmt.Errorf("approve tx: %w", err)
|
|
}
|
|
fmt.Printf("Allowance TX confirmed: %s\n", u.ExplorerURL(txHash))
|
|
return nil
|
|
}
|
|
|
|
// SwapUSDCForETH executes swapExactTokensForETH on the Uniswap V2 Router.
|
|
// Returns the transaction hash.
|
|
func (u *UniswapV2) SwapUSDCForETH(ctx context.Context, usdcAmount, minETHOut float64) (string, error) {
|
|
if err := u.EnsureAllowance(ctx, usdcAmount); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
path := []common.Address{u.quoteAddr, u.wethAddr}
|
|
deadline := big.NewInt(time.Now().Unix() + 20*60)
|
|
amountIn := toTokenWei(usdcAmount, u.quoteDec)
|
|
// Apply slippage to minETHOut
|
|
minOutWei := toEthWei(minETHOut * (1 - u.slippagePct/100))
|
|
|
|
data, err := u.routerABI.Pack("swapExactTokensForETH",
|
|
amountIn, minOutWei, path, u.client.wallet, deadline)
|
|
if err != nil {
|
|
return "", fmt.Errorf("pack swapExactTokensForETH: %w", err)
|
|
}
|
|
|
|
return u.client.SignAndSend(ctx, u.routerAddr, big.NewInt(0), data)
|
|
}
|
|
|
|
// SwapETHForUSDC executes swapExactETHForTokens on the Uniswap V2 Router.
|
|
// Returns the transaction hash.
|
|
func (u *UniswapV2) SwapETHForUSDC(ctx context.Context, ethAmount float64) (string, error) {
|
|
path := []common.Address{u.wethAddr, u.quoteAddr}
|
|
deadline := big.NewInt(time.Now().Unix() + 20*60)
|
|
value := toEthWei(ethAmount)
|
|
|
|
data, err := u.routerABI.Pack("swapExactETHForTokens",
|
|
big.NewInt(0), path, u.client.wallet, deadline)
|
|
if err != nil {
|
|
return "", fmt.Errorf("pack swapExactETHForTokens: %w", err)
|
|
}
|
|
|
|
return u.client.SignAndSend(ctx, u.routerAddr, value, data)
|
|
}
|
|
|
|
// GasCostETH estimates the gas cost for a transaction in ETH.
|
|
func (u *UniswapV2) GasCostETH(ctx context.Context) (float64, error) {
|
|
gasPrice, err := u.client.GasPrice(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
cost := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(u.client.GasLimit()))
|
|
f, _ := new(big.Float).Quo(new(big.Float).SetInt(cost), new(big.Float).SetFloat64(1e18)).Float64()
|
|
return f, nil
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func toEthWei(eth float64) *big.Int {
|
|
// eth * 1e18
|
|
f := new(big.Float).Mul(new(big.Float).SetFloat64(eth), new(big.Float).SetFloat64(1e18))
|
|
i, _ := f.Int(nil)
|
|
return i
|
|
}
|
|
|
|
func toTokenWei(amount float64, decimals int) *big.Int {
|
|
f := new(big.Float).Mul(
|
|
new(big.Float).SetFloat64(amount),
|
|
new(big.Float).SetFloat64(math.Pow10(decimals)),
|
|
)
|
|
i, _ := f.Int(nil)
|
|
return i
|
|
}
|