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:
- Update the code: Paste the new code into
Counter.sol
in Remix - Compile: Ensure EVM version ‚Paris‘ is selected
- Deploy: Go to „Deploy & Run Transactions“ tab
- Connect: Ensure „DEV-GANACHE PROVIDER“ is selected and connected
- Deploy: Click the „Deploy“ button
- Save details: Copy the new
CONTRACT_ADDRESS
and updatedCONTRACT_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
- Open your Ganache Desktop application.
- Navigate to the „ACCOUNTS“ tab, you’ll see a list of pre-funded accounts.
- 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:
- 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:
- web3.py Documentation — Comprehensive API reference
- Ethereum Gas Explained — Deep dive into gas mechanics
- Smart Contract Security — Security best practices
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.