Zum Inhalt springen

Part 3: Sending Your First Blockchain Transaction with web3.py — From Reading to Writing

Welcome back, blockchain explorers! In Part 1, we connected to our local Ganache blockchain and learned to read fundamental data like block numbers and balances. In Part 2, you deployed your Counter smart contract and read its initial state using web3.py.

Now comes the exciting part: changing the state of our smart contract. We’ll make our Counter actually increment its value on the blockchain. This is where your code interacts directly with the decentralized ledger by sending your first transaction!

Understanding Blockchain Transactions

Unlike „read“ operations (like w3.eth.block_number or contract.functions.getCount().call()), which are free and don’t alter the blockchain, „write“ operations require a transaction.

Think of it like this:

  • Reading (View Function): Asking „What’s my bank balance?“; a free inquiry
  • Writing (Transaction): Transferring money or making a payment; this changes your balance and requires verification

Key Transaction Characteristics

Every blockchain transaction has three essential components:

1. Gas Costs Every transaction on Ethereum requires a fee known as gas, paid in the native currency (ETH for Ethereum, MATIC for Polygon, etc.). Gas ensures:

  • Network validators are compensated for computational resources
  • Transaction processing and block inclusion costs are covered
  • The network remains secure and spam-resistant

2. Cryptographic Signatures To prove you authorize the transaction, it must be cryptographically signed with your account’s private key. This signature ensures authenticity and integrity.

🔒 Security Note: Your private key never leaves your machine during this process; only the signature is sent to the node.

3. Nonce Management Each transaction from a specific account must include a unique, incrementing number called a nonce (short for „number used once“). This mechanism:

  • Ensures transactions are executed in order
  • Prevents duplicate or replayed transactions
  • Must be unique for each transaction from the same address

Updating Our Counter Contract

Let’s enhance our Counter.sol contract to include functions that can actually change its state: increment() and decrement().

// Counter.sol (updated with increment/decrement functions)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
    uint256 public count;
    constructor() {
        count = 0;
    }
    // Function to increment the counter
    function increment() public {
        count++;
    }
    // Function to decrement the counter
    function decrement() public {
        count--;
    }
    // View function to get current count
    function getCount() public view returns (uint256) {
        return count;
    }
}

Redeploying Your Contract

Before proceeding, you need to redeploy this updated contract to Ganache via Remix: Follow the same steps from Part 2:

  1. Update the code: Paste the new code into Counter.sol in Remix
  2. Compile: Ensure EVM version ‚Paris‘ is selected
  3. Deploy: Go to „Deploy & Run Transactions“ tab
  4. Connect: Ensure „DEV-GANACHE PROVIDER“ is selected and connected
  5. Deploy: Click the „Deploy“ button
  6. Save details: Copy the new CONTRACT_ADDRESS and updated CONTRACT_ABI

⚠️ Important: The ABI now includes *increment* and *decrement* functions, so you must update it in your code.

Setting Up Your Sender Account (Securely!)

To send a transaction, you need an account with some Ether (or Ganache ETH) and its private key.

Getting a Private Key from Ganache

  1. Open your Ganache Desktop application.
  2. Navigate to the „ACCOUNTS“ tab, you’ll see a list of pre-funded accounts.
  3. Click the „key“ icon next to the first account (the one with the most ETH).

Image: Screenshot of Ganache Desktop, highlighting the key icon next to an account.

4. Copy the private key from the dialogue that appears.

Image: Screenshot of Ganache Desktop showing the private key dialog.

Secure Storage with Environment Variables

Never hardcode private keys directly in your script! Update your .env file:

# .env
RPC_URL="http://127.0.0.1:7545"

# Private key for your Ganache account
GANACHE_PRIVATE_KEY="0xYOUR_GANACHE_PRIVATE_KEY_HERE"

🚨 Production Warning: For real-world applications on public networks, never use private keys directly like this. Instead, use secure key management solutions (hardware wallets, KMS, etc.). For local development with Ganache, this approach is acceptable.

The Transaction Workflow: Build, Sign, Send, Wait

Sending a transaction with web3.py involves four critical steps:

  1. Build: Create the raw transaction dictionary, specifying who it’s from, who it’s to (the contract address), what function to call, its arguments, and gas parameters.
 increment_txn = contract.functions.increment().build_transaction({
        'from': sender_account.address,
        'nonce': nonce,
        'gas': 2000000,
        'gasPrice': w3.eth.gas_price
    })

2. Sign: Cryptographically sign the transaction with your private key (locally)

signed_txn = w3.eth.account.sign_transaction(increment_txn, private_key=PRIVATE_KEY)

3. Send: Send the signed transaction data to the Ethereum node

tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)

4. Wait: Wait for the transaction to be included in a block and get the receipt

tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

Now let’s implement this to increment our Counter!

Complete Implementation

Here’s the complete updated app.py script:

# app.py - Complete transaction implementation

import os
import json
from web3 import Web3, HTTPProvider
from web3.middleware import ExtraDataToPOAMiddleware
from eth_account import Account
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Configuration
RPC_URL = os.getenv("RPC_URL")
PRIVATE_KEY = os.getenv("GANACHE_PRIVATE_KEY")
# Validation
if not RPC_URL or not PRIVATE_KEY:
    raise ValueError("RPC_URL or GANACHE_PRIVATE_KEY not found in .env file.")
try:
    # Initialize Web3 connection
    w3 = Web3(HTTPProvider(RPC_URL))

    # Add PoA middleware for Ganache compatibility
    w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)

    # Verify connection
    if not w3.is_connected():
        print(f"❌ Failed to connect to Ethereum node at {RPC_URL}")
        exit()

    print(f"✅ Successfully connected to Ethereum node at {RPC_URL}")

    # Set up sender account
    sender_account = Account.from_key(PRIVATE_KEY)
    sender_balance = w3.from_wei(w3.eth.get_balance(sender_account.address), 'ether')

    print(f"n🔑 Sender Account Address: {sender_account.address}")
    print(f"💰 Sender Account Balance: {sender_balance} ETH")

    # Smart Contract Setup
    print("n--- Smart Contract Transaction ---")

    # IMPORTANT: Replace with your newly deployed contract details
    CONTRACT_ADDRESS = "0xYourNEWLYDeployedCounterContractAddressHere"
    CONTRACT_ABI = json.loads('''[
        {
            "inputs": [],
            "name": "count",
            "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "getCount",
            "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "increment",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        },
        {
            "inputs": [],
            "name": "decrement",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
        }
    ]''')

    # Create contract instance
    contract = w3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI)

    # Get current counter value
    current_count = contract.functions.getCount().call()
    print(f"🔢 Current Counter Value: {current_count}")

    # Transaction Process
    print("n--- Transaction Process ---")

    # Step 1: Get current nonce
    nonce = w3.eth.get_transaction_count(sender_account.address)
    print(f"📊 Current Nonce: {nonce}")

    # Step 2: Build transaction
    increment_txn = contract.functions.increment().build_transaction({
        'from': sender_account.address,
        'nonce': nonce,
        'gas': 100000,  # Reasonable gas limit for simple operations
        'gasPrice': w3.eth.gas_price
    })

    print(f"📄 Transaction built successfully")
    print(f"   Gas Limit: {increment_txn['gas']}")
    print(f"   Gas Price: {increment_txn['gasPrice']} wei")

    # Step 3: Sign transaction
    signed_txn = w3.eth.account.sign_transaction(increment_txn, private_key=PRIVATE_KEY)
    print(f"✍️ Transaction signed")

    # Step 4: Send transaction
    tx_hash = w3.eth.send_raw_transaction(signed_txn.raw_transaction)
    print(f"🚀 Transaction sent! Hash: {tx_hash.hex()}")

    # Step 5: Wait for confirmation
    print("⏳ Waiting for transaction to be mined...")
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    # Check transaction status
    if tx_receipt['status'] == 1:
        print("n🎉 Transaction successful!")
        print(f"   Block Number: {tx_receipt['blockNumber']}")
        print(f"   Gas Used: {tx_receipt['gasUsed']}")

        # Verify the state change
        new_count = contract.functions.getCount().call()
        print(f"🔢 New Counter Value: {new_count}")
        print(f"   Increment: {current_count}{new_count}")
    else:
        print("n❌ Transaction failed!")
        print("Check your contract address, ABI, and account balance.")
except Exception as e:
    print(f"n❌ Error occurred: {e}")
    print("nTroubleshooting checklist:")
    print("• Ensure Ganache is running on the correct port")
    print("• Verify contract address and ABI are correct")
    print("• Check that sender account has sufficient ETH")
    print("• Confirm private key is valid")
print("n--- End of Transaction Process ---")

Why ExtraDataToPOAMiddleware Matters

I’ll be honest with you; I didn’t include this middleware in my first attempts, and it cost me hours of debugging frustration. When I tried connecting to a Polygon testnet to expand my testing, I kept hitting this cryptic error:

web3.exceptions.ExtraDataLengthError: The field extraData is 97 bytes, but should be 32

I was pulling my hair out! The same code worked perfectly on Ganache but failed miserably on other networks. After struggling with ChatGPT and digging through Stack Overflow threads, I finally discovered the culprit.

Here’s what’s happening: The Ethereum Yellow Paper limits extraData to 32 bytes, but Proof-of-Authority (PoA) chains like Polygon, Goerli, Rinkeby, Sepolia, and BSC stuff additional validator metadata into this field – sometimes up to 97 bytes or more!

That innocent-looking line in our code:

w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)

This middleware patches the extraData field on the fly, allowing web3.py to parse blocks from PoA chains that would otherwise crash your application.

💡 Learn from my mistakes: Even when working with standard Ethereum, always include this middleware. It’s a small addition that saves you from potential headaches when you eventually deploy to testnets or other networks.

Running Your Transaction

Execute your script:

python app.py

You should see output similar to:

Image: Script output.

Run it again, and the count becomes 2, then 3, and so on. You’re now successfully writing data to the blockchain!

What You’ve Accomplished

This is a significant milestone! You’ve successfully:

Understood transaction concepts: Understanding gas, nonces, and cryptographic signing

Implemented the complete transaction workflow: Building, signing, sending, and confirming

Changed blockchain state: Successfully modifying your smart contract’s data

Handled network compatibility: Using middleware for different blockchain networks

This capability unlocks vast possibilities, from sending tokens to interacting with complex DeFi protocols.

Common Troubleshooting

If you encounter issues:

Connection Problems

  • Ensure Ganache is running on the correct port (usually 7545)
  • Check that your RPC_URL matches Ganache’s settings

Transaction Failures

  • Verify your contract address and ABI are correct and up-to-date
  • Ensure your sender account has sufficient ETH for gas fees
  • Check that your private key is valid and corresponds to a funded account

Gas Estimation Issues

  • Try increasing the gas limit if transactions fail
  • On testnets, gas prices can be volatile, consider using w3.eth.gas_price for current rates

What’s Next?

In Part 4, we’ll explore another critical aspect of dApp development: listening for smart contract events to get real-time updates from the blockchain. You’ll learn how to:

  • Set up event listeners for your contracts
  • Filter and process blockchain events
  • Build responsive applications that react to on-chain changes

Resources for Further Learning:

Found this tutorial helpful? Follow me for more blockchain development content and don’t forget to clap if this helped you send your first transaction! 👏

Have questions or run into issues? Drop a comment below and I’ll help you troubleshoot.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert