Web3 Integration Guide Part 2: Smart Contract Interaction

Learn how to read from and write to Ethereum smart contracts using Ethers.js and Vue 3. Complete guide with practical examples including ERC-20 tokens, contract events, and transaction handling.

By GodFake Team17 min read
Web3Smart ContractsEthereumSolidityVue 3Ethers.jsBlockchain
Web3 Integration Guide

Web3 Integration Guide Part 2: Smart Contract Interaction

Introduction

Now that we can connect wallets (Part 1), it's time to interact with smart contracts—the core of Web3 applications. Smart contracts are self-executing programs on the blockchain that handle everything from token transfers to complex decentralized applications (dApps).

In this lesson, you'll learn how to:

  • Read data from smart contracts
  • Send transactions to modify contract state
  • Work with ERC-20 tokens
  • Handle contract events and logs
  • Build a token transfer interface
  • Estimate gas costs and handle transaction errors

Understanding Smart Contracts

What is a Smart Contract?

A smart contract is a program deployed on a blockchain that:

  • Stores data in variables (like balances, mappings, arrays)
  • Exposes functions that can read or modify this data
  • Emits events to notify off-chain applications
  • Is immutable once deployed (code cannot be changed)

Contract ABI (Application Binary Interface)

To interact with a contract, you need its ABI—a JSON file that describes:

  • Function names and parameters
  • Return types
  • Event signatures
  • Whether functions are view (read-only) or require transactions

Example ABI snippet:

[
  {
    "constant": true,
    "inputs": [{"name": "account", "type": "address"}],
    "name": "balanceOf",
    "outputs": [{"name": "", "type": "uint256"}],
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {"name": "recipient", "type": "address"},
      {"name": "amount", "type": "uint256"}
    ],
    "name": "transfer",
    "outputs": [{"name": "", "type": "bool"}],
    "type": "function"
  }
]

Reading from Smart Contracts

Reading contract data is free and doesn't require a transaction. Let's create a composable to interact with an ERC-20 token contract.

Create src/composables/useContract.js

import { ref, computed } from 'vue'
import { Contract } from 'ethers'
import { useWallet } from './useWallet'

// Standard ERC-20 ABI (simplified - includes most common functions)
const ERC20_ABI = [
  // Read-only functions
  'function name() view returns (string)',
  'function symbol() view returns (string)',
  'function decimals() view returns (uint8)',
  'function totalSupply() view returns (uint256)',
  'function balanceOf(address owner) view returns (uint256)',
  'function allowance(address owner, address spender) view returns (uint256)',
  
  // State-changing functions
  'function transfer(address to, uint256 amount) returns (bool)',
  'function approve(address spender, uint256 amount) returns (bool)',
  'function transferFrom(address from, address to, uint256 amount) returns (bool)',
  
  // Events
  'event Transfer(address indexed from, address indexed to, uint256 value)',
  'event Approval(address indexed owner, address indexed spender, uint256 value)'
]

export function useContract() {
  const { provider, account } = useWallet()
  
  const contractAddress = ref(null)
  const contract = ref(null)
  const tokenInfo = ref({
    name: '',
    symbol: '',
    decimals: 18,
    totalSupply: '0'
  })
  const loading = ref(false)
  const error = ref(null)

  /**
   * Initialize contract instance
   */
  const initContract = async (address, abi = ERC20_ABI) => {
    try {
      loading.value = true
      error.value = null

      if (!provider.value) {
        throw new Error('Wallet not connected')
      }

      contractAddress.value = address
      
      // Create contract instance
      // For reading only, we use the provider
      // For writing, we'll get the signer later
      contract.value = new Contract(address, abi, provider.value)
      
      console.log('✅ Contract initialized:', address)
      
    } catch (err) {
      error.value = err.message
      console.error('Contract initialization error:', err)
    } finally {
      loading.value = false
    }
  }

  /**
   * Read token information
   */
  const fetchTokenInfo = async () => {
    if (!contract.value) {
      throw new Error('Contract not initialized')
    }

    try {
      loading.value = true
      error.value = null

      // Call multiple read-only functions in parallel
      const [name, symbol, decimals, totalSupply] = await Promise.all([
        contract.value.name(),
        contract.value.symbol(),
        contract.value.decimals(),
        contract.value.totalSupply()
      ])

      tokenInfo.value = {
        name,
        symbol,
        decimals: Number(decimals),
        totalSupply: totalSupply.toString()
      }

      console.log('Token info:', tokenInfo.value)
      return tokenInfo.value
      
    } catch (err) {
      error.value = err.message
      console.error('Error fetching token info:', err)
      throw err
    } finally {
      loading.value = false
    }
  }

  /**
   * Get token balance for an address
   */
  const getBalance = async (address) => {
    if (!contract.value) {
      throw new Error('Contract not initialized')
    }

    try {
      const balance = await contract.value.balanceOf(address)
      return balance
    } catch (err) {
      console.error('Error getting balance:', err)
      throw err
    }
  }

  /**
   * Get formatted token balance (accounts for decimals)
   */
  const getFormattedBalance = async (address) => {
    try {
      const balance = await getBalance(address)
      const decimals = tokenInfo.value.decimals || 18
      
      // Convert from wei-like units to human readable
      const formatted = Number(balance) / Math.pow(10, decimals)
      return formatted.toFixed(4)
    } catch (err) {
      console.error('Error formatting balance:', err)
      return '0.0000'
    }
  }

  /**
   * Check allowance (how much spender can spend on behalf of owner)
   */
  const getAllowance = async (owner, spender) => {
    if (!contract.value) {
      throw new Error('Contract not initialized')
    }

    try {
      const allowance = await contract.value.allowance(owner, spender)
      return allowance
    } catch (err) {
      console.error('Error getting allowance:', err)
      throw err
    }
  }

  // Computed property for user's balance
  const userBalance = computed(async () => {
    if (!account.value || !contract.value) {
      return '0.0000'
    }
    return await getFormattedBalance(account.value)
  })

  return {
    // State
    contract,
    contractAddress,
    tokenInfo,
    loading,
    error,
    
    // Computed
    userBalance,
    
    // Methods
    initContract,
    fetchTokenInfo,
    getBalance,
    getFormattedBalance,
    getAllowance
  }
}

Key Concepts Explained

  • Contract Instance: Created with address, ABI, and provider/signer
  • View Functions: Read-only, don't cost gas, return immediately
  • Human-Readable ABI: Ethers.js allows simplified ABI format (strings instead of full JSON)
  • BigInt Handling: Ethereum uses 256-bit integers; Ethers.js returns BigInt objects
  • Decimals: Most tokens use 18 decimals (like ETH), but some use different values

Writing to Smart Contracts

Writing to contracts requires sending transactions, which cost gas and need user approval.

Extend useContract.js with Write Functions

// Add to useContract.js

import { parseUnits } from 'ethers'

export function useContract() {
  // ... existing code ...

  const txPending = ref(false)
  const txHash = ref(null)

  /**
   * Get contract with signer (for write operations)
   */
  const getContractWithSigner = async () => {
    if (!provider.value) {
      throw new Error('Wallet not connected')
    }

    const signer = await provider.value.getSigner()
    return contract.value.connect(signer)
  }

  /**
   * Transfer tokens to another address
   */
  const transfer = async (toAddress, amount) => {
    try {
      txPending.value = true
      error.value = null
      txHash.value = null

      // Get contract instance with signer
      const contractWithSigner = await getContractWithSigner()

      // Parse amount to proper units (account for decimals)
      const decimals = tokenInfo.value.decimals || 18
      const amountInWei = parseUnits(amount.toString(), decimals)

      console.log('Sending transaction...')
      console.log('To:', toAddress)
      console.log('Amount:', amount, tokenInfo.value.symbol)
      console.log('Amount (wei):', amountInWei.toString())

      // Send transaction
      const tx = await contractWithSigner.transfer(toAddress, amountInWei)
      txHash.value = tx.hash

      console.log('Transaction sent:', tx.hash)
      console.log('Waiting for confirmation...')

      // Wait for transaction to be mined
      const receipt = await tx.wait()

      console.log('✅ Transaction confirmed!')
      console.log('Block:', receipt.blockNumber)
      console.log('Gas used:', receipt.gasUsed.toString())

      return receipt

    } catch (err) {
      console.error('Transfer error:', err)
      
      // Parse error message
      if (err.code === 'ACTION_REJECTED') {
        error.value = 'Transaction rejected by user'
      } else if (err.message.includes('insufficient funds')) {
        error.value = 'Insufficient funds for transaction'
      } else {
        error.value = err.message
      }
      
      throw err
    } finally {
      txPending.value = false
    }
  }

  /**
   * Approve spender to spend tokens on behalf of user
   */
  const approve = async (spenderAddress, amount) => {
    try {
      txPending.value = true
      error.value = null
      txHash.value = null

      const contractWithSigner = await getContractWithSigner()
      const decimals = tokenInfo.value.decimals || 18
      const amountInWei = parseUnits(amount.toString(), decimals)

      console.log('Approving spender...')
      const tx = await contractWithSigner.approve(spenderAddress, amountInWei)
      txHash.value = tx.hash

      const receipt = await tx.wait()
      console.log('✅ Approval confirmed!')

      return receipt

    } catch (err) {
      console.error('Approve error:', err)
      error.value = err.message
      throw err
    } finally {
      txPending.value = false
    }
  }

  /**
   * Estimate gas for a transfer
   */
  const estimateTransferGas = async (toAddress, amount) => {
    try {
      const contractWithSigner = await getContractWithSigner()
      const decimals = tokenInfo.value.decimals || 18
      const amountInWei = parseUnits(amount.toString(), decimals)

      // Estimate gas needed
      const gasEstimate = await contractWithSigner.transfer.estimateGas(
        toAddress,
        amountInWei
      )

      // Get current gas price
      const feeData = await provider.value.getFeeData()
      const gasPrice = feeData.gasPrice

      // Calculate total cost in ETH
      const gasCost = gasEstimate * gasPrice
      const gasCostInEth = Number(gasCost) / 1e18

      return {
        gasLimit: gasEstimate.toString(),
        gasPrice: gasPrice.toString(),
        totalCost: gasCostInEth.toFixed(6)
      }

    } catch (err) {
      console.error('Gas estimation error:', err)
      throw err
    }
  }

  return {
    // ... existing returns ...
    txPending,
    txHash,
    transfer,
    approve,
    estimateTransferGas
  }
}

Transaction Lifecycle

  1. User initiates: Call contract function (e.g., transfer)
  2. Gas estimation: Ethers.js estimates gas needed
  3. Wallet popup: User sees transaction details and approves/rejects
  4. Transaction sent: Tx submitted to network, returns tx hash
  5. Pending state: Tx in mempool, waiting to be mined
  6. Mined: Tx included in a block
  7. Confirmed: Block finalized (after a few more blocks)

Building a Token Transfer Component

Create src/components/TokenTransfer.vue

<template>
  <div class="token-transfer">
    <h2>💸 Token Transfer</h2>

    <!-- Contract Input -->
    <div class="section">
      <label>Token Contract Address:</label>
      <div class="input-group">
        <input 
          v-model="contractAddr"
          placeholder="0x..."
          :disabled="!!contract"
          class="address-input"
        />
        <button 
          @click="loadContract"
          :disabled="loading || !!contract"
          class="btn-primary"
        >
          {{ contract ? '✓ Loaded' : 'Load Contract' }}
        </button>
      </div>
      <p class="hint">
        Example (Sepolia USDC): 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
      </p>
    </div>

    <!-- Token Info Display -->
    <div v-if="tokenInfo.name" class="token-info">
      <h3>Token Information</h3>
      <div class="info-grid">
        <div class="info-item">
          <span class="label">Name:</span>
          <span class="value">{{ tokenInfo.name }}</span>
        </div>
        <div class="info-item">
          <span class="label">Symbol:</span>
          <span class="value">{{ tokenInfo.symbol }}</span>
        </div>
        <div class="info-item">
          <span class="label">Your Balance:</span>
          <span class="value">{{ balance }} {{ tokenInfo.symbol }}</span>
        </div>
        <div class="info-item">
          <span class="label">Total Supply:</span>
          <span class="value">{{ formattedSupply }} {{ tokenInfo.symbol }}</span>
        </div>
      </div>
    </div>

    <!-- Transfer Form -->
    <div v-if="contract" class="section transfer-form">
      <h3>Send Tokens</h3>
      
      <label>Recipient Address:</label>
      <input 
        v-model="recipientAddress"
        placeholder="0x..."
        class="address-input"
        :disabled="txPending"
      />

      <label>Amount:</label>
      <div class="amount-input-group">
        <input 
          v-model="amount"
          type="number"
          step="0.01"
          min="0"
          placeholder="0.00"
          class="amount-input"
          :disabled="txPending"
        />
        <span class="token-symbol">{{ tokenInfo.symbol }}</span>
      </div>

      <!-- Gas Estimate -->
      <div v-if="gasEstimate" class="gas-estimate">
        <span>⛽ Estimated Gas Cost:</span>
        <span>{{ gasEstimate.totalCost }} ETH</span>
      </div>

      <button 
        @click="handleEstimateGas"
        :disabled="!canEstimate || txPending"
        class="btn-secondary"
      >
        Estimate Gas Cost
      </button>

      <button 
        @click="handleTransfer"
        :disabled="!canTransfer || txPending"
        class="btn-primary btn-large"
      >
        {{ txPending ? 'Processing...' : 'Send Tokens' }}
      </button>

      <!-- Transaction Status -->
      <div v-if="txHash" class="tx-status">
        <p>✅ Transaction sent!</p>
        <a 
          :href="`https://sepolia.etherscan.io/tx/${txHash}`"
          target="_blank"
          class="tx-link"
        >
          View on Etherscan →
        </a>
      </div>
    </div>

    <!-- Error Display -->
    <div v-if="error" class="error-message">
      ⚠️ {{ error }}
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { useWallet } from '../composables/useWallet'
import { useContract } from '../composables/useContract'
import { isAddress } from 'ethers'

const { account, isConnected } = useWallet()
const {
  contract,
  tokenInfo,
  loading,
  error,
  txPending,
  txHash,
  initContract,
  fetchTokenInfo,
  getFormattedBalance,
  transfer,
  estimateTransferGas
} = useContract()

const contractAddr = ref('')
const recipientAddress = ref('')
const amount = ref('')
const balance = ref('0.0000')
const gasEstimate = ref(null)

// Load contract
const loadContract = async () => {
  if (!contractAddr.value || !isAddress(contractAddr.value)) {
    alert('Please enter a valid contract address')
    return
  }

  await initContract(contractAddr.value)
  await fetchTokenInfo()
  await updateBalance()
}

// Update user balance
const updateBalance = async () => {
  if (!account.value || !contract.value) return
  balance.value = await getFormattedBalance(account.value)
}

// Format total supply for display
const formattedSupply = computed(() => {
  if (!tokenInfo.value.totalSupply) return '0'
  const decimals = tokenInfo.value.decimals || 18
  const supply = Number(tokenInfo.value.totalSupply) / Math.pow(10, decimals)
  return supply.toLocaleString()
})

// Validation
const canEstimate = computed(() => {
  return (
    isConnected.value &&
    contract.value &&
    recipientAddress.value &&
    isAddress(recipientAddress.value) &&
    amount.value &&
    Number(amount.value) > 0
  )
})

const canTransfer = computed(() => {
  return (
    canEstimate.value &&
    Number(amount.value) <= Number(balance.value)
  )
})

// Estimate gas
const handleEstimateGas = async () => {
  if (!canEstimate.value) return

  try {
    gasEstimate.value = await estimateTransferGas(
      recipientAddress.value,
      amount.value
    )
  } catch (err) {
    console.error('Gas estimation failed:', err)
  }
}

// Send transfer
const handleTransfer = async () => {
  if (!canTransfer.value) return

  try {
    await transfer(recipientAddress.value, amount.value)
    
    // Reset form on success
    recipientAddress.value = ''
    amount.value = ''
    gasEstimate.value = null
    
    // Update balance
    await updateBalance()
    
  } catch (err) {
    console.error('Transfer failed:', err)
  }
}

// Watch for account changes
watch(account, async () => {
  if (contract.value) {
    await updateBalance()
  }
})

// Auto-estimate when inputs change
watch([recipientAddress, amount], () => {
  gasEstimate.value = null
})
</script>

<style scoped>
.token-transfer {
  max-width: 600px;
  margin: 0 auto;
}

h2 {
  font-size: 1.8rem;
  margin-bottom: 1.5rem;
  color: #333;
}

h3 {
  font-size: 1.2rem;
  margin-bottom: 1rem;
  color: #555;
}

.section {
  margin-bottom: 2rem;
}

label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
  color: #555;
}

.input-group {
  display: flex;
  gap: 0.5rem;
}

.address-input {
  flex: 1;
  padding: 0.75rem;
  border: 2px solid #ddd;
  border-radius: 8px;
  font-family: monospace;
  font-size: 0.9rem;
}

.address-input:focus {
  outline: none;
  border-color: #667eea;
}

.hint {
  margin-top: 0.5rem;
  font-size: 0.85rem;
  color: #888;
}

.btn-primary, .btn-secondary {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-primary {
  background: #667eea;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background: #5568d3;
}

.btn-secondary {
  background: #f0f0f0;
  color: #333;
  margin-bottom: 0.5rem;
  width: 100%;
}

.btn-secondary:hover:not(:disabled) {
  background: #e0e0e0;
}

.btn-large {
  width: 100%;
  padding: 1rem;
  font-size: 1.1rem;
  margin-top: 1rem;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.token-info {
  background: #f8f9fa;
  padding: 1.5rem;
  border-radius: 12px;
  margin-bottom: 2rem;
}

.info-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

.info-item {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.info-item .label {
  font-size: 0.85rem;
  color: #888;
  font-weight: 500;
}

.info-item .value {
  font-size: 1.1rem;
  color: #333;
  font-weight: 600;
}

.transfer-form {
  background: #ffffff;
  padding: 1.5rem;
  border: 2px solid #e0e0e0;
  border-radius: 12px;
}

.amount-input-group {
  display: flex;
  align-items: center;
  border: 2px solid #ddd;
  border-radius: 8px;
  padding-right: 1rem;
  margin-bottom: 1rem;
}

.amount-input {
  flex: 1;
  padding: 0.75rem;
  border: none;
  font-size: 1.2rem;
}

.amount-input:focus {
  outline: none;
}

.token-symbol {
  font-weight: 600;
  color: #667eea;
}

.gas-estimate {
  display: flex;
  justify-content: space-between;
  padding: 0.75rem;
  background: #fff3cd;
  border-radius: 6px;
  margin-bottom: 1rem;
  font-size: 0.9rem;
}

.tx-status {
  margin-top: 1rem;
  padding: 1rem;
  background: #d4edda;
  border-radius: 8px;
  text-align: center;
}

.tx-link {
  color: #28a745;
  font-weight: 600;
  text-decoration: none;
}

.tx-link:hover {
  text-decoration: underline;
}

.error-message {
  margin-top: 1rem;
  padding: 1rem;
  background: #f8d7da;
  color: #721c24;
  border-radius: 8px;
}
</style>

Listening to Contract Events

Smart contracts emit events when important actions occur. Let's add event listening to our composable:

Add Event Listeners to useContract.js

// Add to useContract.js

export function useContract() {
  // ... existing code ...

  const events = ref([])

  /**
   * Listen for Transfer events
   */
  const listenToTransfers = (callback) => {
    if (!contract.value) {
      throw new Error('Contract not initialized')
    }

    // Listen to Transfer events
    contract.value.on('Transfer', (from, to, amount, event) => {
      const eventData = {
        type: 'Transfer',
        from,
        to,
        amount: amount.toString(),
        blockNumber: event.log.blockNumber,
        transactionHash: event.log.transactionHash
      }

      console.log('Transfer event:', eventData)
      events.value.unshift(eventData)

      if (callback) {
        callback(eventData)
      }
    })

    console.log('👂 Listening for Transfer events...')
  }

  /**
   * Stop listening to events
   */
  const stopListening = () => {
    if (contract.value) {
      contract.value.removeAllListeners()
      console.log('👋 Stopped listening to events')
    }
  }

  /**
   * Get past events (event history)
   */
  const getPastTransfers = async (fromBlock = -10000, toBlock = 'latest') => {
    if (!contract.value) {
      throw new Error('Contract not initialized')
    }

    try {
      // Query past Transfer events
      const filter = contract.value.filters.Transfer()
      const pastEvents = await contract.value.queryFilter(filter, fromBlock, toBlock)

      const formattedEvents = pastEvents.map(event => ({
        type: 'Transfer',
        from: event.args.from,
        to: event.args.to,
        amount: event.args.value.toString(),
        blockNumber: event.blockNumber,
        transactionHash: event.transactionHash
      }))

      events.value = formattedEvents
      return formattedEvents

    } catch (err) {
      console.error('Error fetching past events:', err)
      throw err
    }
  }

  return {
    // ... existing returns ...
    events,
    listenToTransfers,
    stopListening,
    getPastTransfers
  }
}

Working with Different Contract Types

ERC-721 (NFTs)

const ERC721_ABI = [
  'function balanceOf(address owner) view returns (uint256)',
  'function ownerOf(uint256 tokenId) view returns (address)',
  'function tokenURI(uint256 tokenId) view returns (string)',
  'function transferFrom(address from, address to, uint256 tokenId)',
  'function safeTransferFrom(address from, address to, uint256 tokenId)',
  'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'
]

// Example: Get NFT metadata
async function getNFTMetadata(contract, tokenId) {
  const tokenURI = await contract.tokenURI(tokenId)
  const response = await fetch(tokenURI)
  const metadata = await response.json()
  return metadata // { name, description, image, attributes }
}

Custom Contract Example

// Example: Simple storage contract
const STORAGE_ABI = [
  'function store(uint256 num)',
  'function retrieve() view returns (uint256)',
  'event ValueChanged(uint256 newValue)'
]

// Read stored value
const value = await contract.retrieve()
console.log('Stored value:', value.toString())

// Write new value
const tx = await contractWithSigner.store(42)
await tx.wait()
console.log('Value updated!')

Error Handling Best Practices

Common Error Codes

Error Code Meaning Solution
ACTION_REJECTED User rejected transaction Show friendly message, allow retry
INSUFFICIENT_FUNDS Not enough ETH for gas Display balance, suggest smaller amount
UNPREDICTABLE_GAS_LIMIT Transaction would fail Check contract logic, inputs
NONCE_EXPIRED Nonce already used Refresh and retry
REPLACEMENT_UNDERPRICED Gas price too low for replacement Increase gas price

Robust Error Handler

function parseTransactionError(error) {
  // User rejection
  if (error.code === 'ACTION_REJECTED' || error.code === 4001) {
    return {
      title: 'Transaction Rejected',
      message: 'You cancelled the transaction',
      canRetry: true
    }
  }

  // Insufficient funds
  if (error.message.includes('insufficient funds')) {
    return {
      title: 'Insufficient Funds',
      message: 'You don\'t have enough ETH to pay for gas',
      canRetry: false
    }
  }

  // Contract execution failed
  if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
    return {
      title: 'Transaction Would Fail',
      message: 'This transaction would fail. Check your inputs and try again.',
      canRetry: true
    }
  }

  // Network error
  if (error.message.includes('network')) {
    return {
      title: 'Network Error',
      message: 'Failed to connect to blockchain. Check your connection.',
      canRetry: true
    }
  }

  // Generic error
  return {
    title: 'Transaction Failed',
    message: error.message || 'An unknown error occurred',
    canRetry: true
  }
}

Testing Your Smart Contract Integration

Using Test Networks

  1. Get testnet ETH:
  2. Deploy a test ERC-20 token: Use Remix IDE
  3. Test all functions:
    • Read balance, name, symbol
    • Transfer tokens
    • Approve and transferFrom
    • Listen to events

Example Test Token Contracts

  • Sepolia USDC: 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
  • Sepolia DAI: 0x68194a729C2450ad26072b3D33ADaCbcef39D574
  • Mumbai USDC: 0x0FA8781a83E46826621b3BC094Ea2A0212e71B23

Performance Optimization

Batch Read Calls

// ❌ Slow: Multiple separate calls
const name = await contract.name()
const symbol = await contract.symbol()
const decimals = await contract.decimals()

// ✅ Fast: Parallel calls
const [name, symbol, decimals] = await Promise.all([
  contract.name(),
  contract.symbol(),
  contract.decimals()
])

Cache Contract Instances

// Store contracts in a Map to avoid re-creating
const contractCache = new Map()

function getOrCreateContract(address, abi, provider) {
  const key = `${address}-${provider}`
  
  if (!contractCache.has(key)) {
    contractCache.set(key, new Contract(address, abi, provider))
  }
  
  return contractCache.get(key)
}

What's Next?

You now know how to read from and write to smart contracts! In the next part, we'll focus on accepting cryptocurrency payments:

  • Part 3: Building payment flows, handling ETH and ERC-20 tokens, payment confirmation UI
  • Part 4: Sending payments to users, batch transactions, gas optimization
  • Part 5: Security auditing, testing strategies, production deployment

Resources