Sending Complex Transactions in Web3 Backend with Go

·

In modern Web3 development, backend services often need to interact directly with smart contracts—sending tokens, swapping assets on decentralized exchanges, or executing complex on-chain logic. While basic Ethereum transactions are straightforward, handling call data for contract interactions introduces significant complexity, especially when dealing with multiple parameters and nested types.

This article walks you through how to use Go bindings and the abigen tool to simplify sending advanced transactions in Go-based backends. We'll demonstrate practical implementations using real-world examples: transferring UNI tokens and performing a swap on Uniswap V2 via the Sepolia testnet.

Understanding Call Data in Ethereum Transactions

When you send a transaction that interacts with a smart contract, you're not just transferring value—you're invoking specific functions. This requires encoding function calls into a format Ethereum understands: ABI-encoded call data.

👉 Discover how easy blockchain interaction can be with the right tools and infrastructure.

For example, calling transfer(address,uint256) on an ERC-20 contract involves:

  1. Taking the function signature and computing its Keccak-256 hash.
  2. Extracting the first 4 bytes (function selector).
  3. ABI-encoding the arguments (address padded to 64 hex characters, amount as hex with zero-padding).
  4. Concatenating everything into a single byte array.

Doing this manually is error-prone, especially with arrays, structs, or dynamic types like address[]. That’s where Go bindings come in.

What Are Go Bindings for Smart Contracts?

Go bindings are auto-generated Go interfaces that wrap Ethereum smart contracts, allowing developers to interact with them as if they were native Go objects. These bindings handle all ABI encoding/decoding automatically, enforce type safety, and expose contract methods as regular Go functions.

They are part of the broader concept of language bindings, which bridge systems written in different languages—like enabling Go to call Python functions without shell scripts or RPC overhead.

In Ethereum development, these bindings are typically generated from a contract’s Application Binary Interface (ABI) using tools like abigen.

Using ERC-20 Go Binding for Token Transfers

Instead of manually crafting call data for token transfers, we can leverage pre-built Go bindings for standard interfaces like ERC-20.

The eth-go-bindings library provides ready-to-use implementations for common standards including ERC-20, ERC-721, and more.

Setting Up the Environment

First, connect to an Ethereum node via JSON-RPC:

client, err := ethclient.Dial("https://eth-sepolia.g.alchemy.com/v2/" + os.Getenv("ALCHEMY_API_KEY"))
if err != nil {
    log.Fatal(err)
}

Then initialize the UNI token contract using its address:

const uniTokenContractAddress = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
uniToken, err := erc20.NewErc20(common.HexToAddress(uniTokenContractAddress), client)
if err != nil {
    log.Fatal(err)
}

Querying Token Balance

With the binding in place, querying state becomes trivial:

balance, err := uniToken.BalanceOf(&bind.CallOpts{}, ownerAddress)
if err != nil {
    log.Fatalf("Failed to retrieve token balance: %v", err)
}

Sending a Token Transfer

To send tokens, use the Transfer method:

chainID := big.NewInt(11155111)
tx, err := uniToken.Transfer(
    &bind.TransactOpts{
        From: common.HexToAddress(address.Hex()),
        Signer: func(_ common.Address, tx *types.Transaction) (*types.Transaction, error) {
            return types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
        },
        Value:    big.NewInt(0),
        GasPrice: gasPrice,
    },
    common.HexToAddress("0xE2Dc3214f7096a94077E71A3E218243E289F1067"),
    big.NewInt(1000000), // 1 UNI (6 decimals)
)
fmt.Printf("tx sent: %s\n", tx.Hash().Hex())

Note: You don’t need to specify GasLimit or Noncego-ethereum automatically estimates gas and fetches the correct nonce via eth_estimateGas and eth_getTransactionCount.

Generating Custom Go Bindings with abigen

Not all contracts follow standard patterns. For custom logic—like swapping tokens on Uniswap—you’ll need your own binding.

Enter abigen: a powerful tool included in the Geth suite that generates Go wrappers from ABI files.

Step-by-Step: Create Binding for Uniswap V2

  1. Get the contract ABI from Sepolia Etherscan.
  2. Save it as uniswapv2.abi.json.
  3. Run:
abigen --abi uniswapv2.abi.json --pkg uniswap --type UniswapV2 --out UniswapV2.go

Now import and instantiate:

uniswapV2, err := uniswap.NewUniswapV2(common.HexToAddress(uniswapV2ContractAddress), client)
if err != nil {
    log.Fatal(err)
}

Executing a Swap on Uniswap V2

Uniswap V2 supports various swap functions. We’ll use swapExactETHForTokens, which swaps a fixed amount of ETH for at least a minimum amount of another token.

Function Parameters

Example: Swap ETH for ZKSlove Token

We’ll swap a small amount of ETH along the path: ETH → WETH → ZKSlove, using this token: ZKSlove.

tx, err := uniswapV2.SwapExactETHForTokens(
    &bind.TransactOpts{
        From: common.HexToAddress(address.Hex()),
        Signer: func(_ common.Address, tx *types.Transaction) (*types.Transaction, error) {
            return types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
        },
        Value:    big.NewInt(100000), // Amount of ETH to send
        GasPrice: gasPrice,
    },
    big.NewInt(0), // amountOutMin – set to 0 for demo
    []common.Address{
        common.HexToAddress("0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9"), // WETH
        common.HexToAddress("0xbd429ad5456385bf86042358ddc81c57e72173d3"), // ZKSlove
    },
    common.HexToAddress("0x32e0556aeC41a34C3002a264f4694193EBCf44F7"), // recipient
    big.NewInt(999999999999999999), // far-future deadline
)
fmt.Printf("tx sent: %s\n", tx.Hash().Hex())

👉 See how seamless DeFi interactions can be when powered by robust backend integrations.

This transaction will appear on Sepolia Etherscan:

Frequently Asked Questions (FAQ)

Q: What is call data in Ethereum transactions?
A: Call data is the encoded payload sent with a transaction that specifies which function to call on a smart contract and with what arguments. It follows the ABI encoding rules defined by Ethereum.

Q: Why should I use Go bindings instead of manual ABI encoding?
A: Manual encoding is error-prone and hard to maintain. Go bindings automate encoding/decoding, ensure type safety, improve readability, and reduce bugs—especially with complex parameter types.

Q: Can I generate Go bindings for any smart contract?
A: Yes, as long as you have the contract’s ABI. Tools like abigen can generate bindings for any EVM-compatible contract deployed on any network.

Q: Do I need to set GasLimit and Nonce when using TransactOpts?
A: No. The go-ethereum library automatically fills in missing fields like GasLimit (via eth_estimateGas) and Nonce (via eth_getTransactionCount) unless explicitly overridden.

Q: Is it safe to set amountOutMin to zero in Uniswap swaps?
A: For testing, yes—but in production, always calculate a reasonable minimum based on current market rates to prevent loss due to slippage or front-running.

Q: Where can I find reliable RPC endpoints for testnets?
A: Providers like Alchemy, Infura, or public nodes offer stable access. You can also run your own node or use services like OKX’s Web3 APIs for scalable infrastructure.


By leveraging Go bindings and tools like abigen, backend developers can securely and efficiently interact with decentralized protocols without getting bogged down in low-level details.

Whether you're building automated market makers, NFT mints, or cross-chain bridges, mastering these techniques empowers your backend to become a powerful gateway into the Web3 ecosystem.

👉 Start building smarter Web3 backends today—explore powerful tools and resources now.