package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"math/big"
"net/http"
"net/url"
"os"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
var (
usdc = common.HexToAddress("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
weth = common.HexToAddress("0x4200000000000000000000000000000000000006")
sellAmount = big.NewInt(100_000_000) // 100 USDC (6 decimals)
)
const approveABI = `[{"type":"function","name":"approve","stateMutability":"nonpayable",
"inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],
"outputs":[{"name":"","type":"bool"}]}]`
type Quote struct {
LiquidityAvailable bool `json:"liquidityAvailable"`
BuyAmount string `json:"buyAmount"`
MinBuyAmount string `json:"minBuyAmount"`
Issues struct {
Allowance *struct {
Spender common.Address `json:"spender"`
} `json:"allowance"`
Balance *struct {
Actual string `json:"actual"`
Expected string `json:"expected"`
} `json:"balance"`
SimulationIncomplete bool `json:"simulationIncomplete"`
} `json:"issues"`
Transaction *struct {
To common.Address `json:"to"`
Data hexutil.Bytes `json:"data"`
Value *string `json:"value"`
} `json:"transaction"`
}
func required(name string) string {
value := os.Getenv(name)
if value == "" {
log.Fatalf("Missing %s", name)
}
return value
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func getQuote(user common.Address) (*Quote, error) {
params := url.Values{
"chainId": {"8453"}, "sellToken": {usdc.Hex()}, "buyToken": {weth.Hex()},
"sellAmount": {sellAmount.String()}, "taker": {user.Hex()}, "slippageBps": {"50"},
}
req, err := http.NewRequest("GET", "https://api.0x.org/swap/allowance-holder/quote?"+params.Encode(), nil)
if err != nil {
return nil, err
}
req.Header.Set("0x-api-key", required("ZERO_EX_API_KEY"))
req.Header.Set("0x-version", "v2")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode >= 300 {
return nil, fmt.Errorf("0x quote failed: %d %s", res.StatusCode, body)
}
var quote Quote
return "e, json.Unmarshal(body, "e)
}
func main() {
ctx := context.Background()
client, err := ethclient.Dial("https://mainnet.base.org")
check(err)
key, err := crypto.HexToECDSA(strings.TrimPrefix(required("PRIVATE_KEY"), "0x"))
check(err)
user := crypto.PubkeyToAddress(key.PublicKey)
opts, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(8453))
check(err)
quote, err := getQuote(user)
check(err)
if !quote.LiquidityAvailable {
log.Fatal("No route is currently available")
}
if quote.Issues.Balance != nil {
log.Fatal("Insufficient USDC balance")
}
if quote.Issues.Allowance != nil {
erc20, err := abi.JSON(strings.NewReader(approveABI))
check(err)
data, err := erc20.Pack("approve", quote.Issues.Allowance.Spender, sellAmount)
check(err)
_, err = client.CallContract(ctx, ethereum.CallMsg{From: user, To: &usdc, Data: data}, nil) // simulate
check(err)
approval, err := bind.NewBoundContract(usdc, erc20, client, client, client).RawTransact(opts, data)
check(err)
_, err = bind.WaitMined(ctx, client, approval)
check(err)
quote, err = getQuote(user)
check(err)
}
if quote.Issues.Allowance != nil {
log.Fatal("Token allowance is still insufficient")
}
if quote.Issues.SimulationIncomplete {
log.Fatal("0x could not complete its simulation")
}
if quote.Transaction == nil {
log.Fatal("Quote did not include transaction data")
}
tx := quote.Transaction
value := new(big.Int)
if tx.Value != nil {
value.SetString(*tx.Value, 10)
}
_, err = client.CallContract(ctx, ethereum.CallMsg{From: user, To: &tx.To, Data: tx.Data, Value: value}, nil)
check(err)
opts.Value = value
sent, err := bind.NewBoundContract(tx.To, abi.ABI{}, client, client, client).RawTransact(opts, tx.Data)
check(err)
_, err = bind.WaitMined(ctx, client, sent)
check(err)
fmt.Printf("Swap confirmed: %s\n", sent.Hash())
minBuy, _ := new(big.Int).SetString(quote.MinBuyAmount, 10)
fmt.Printf("Minimum WETH output: %s WETH\n", new(big.Rat).SetFrac(minBuy, big.NewInt(1e18)).FloatString(18))
}