İçeriğe geç
·24 dk okuma

Web Geliştiriciler İçin Ethereum: Abartı Olmadan Smart Contract

Her web geliştiricinin bilmesi gereken Ethereum kavramları: hesaplar, transaction'lar, smart contract'lar, ABI encoding, ethers.js, WAGMI ve kendi node'unu çalıştırmadan on-chain veri okuma.

Paylaş:X / TwitterLinkedIn

"Geliştiriciler için Ethereum" içeriklerinin çoğu iki kategoriye düşer: bir şey inşa etmene yardımcı olmayan basitleştirilmiş analojiler ya da Merkle Patricia Trie'nin ne olduğunu zaten bildiğini varsayan derin protokol spesifikasyonları. Token bakiyesi okumak, bir kullanıcının transaction imzalamasını sağlamak veya React uygulamasında NFT metadata'sı göstermek isteyen bir web geliştiriciysen ikisi de işe yaramaz.

Bu yazı pratik orta yol. Frontend'inin Ethereum ile konuştuğunda tam olarak ne olduğunu, hareketli parçaların ne olduğunu ve modern araçların (ethers.js, viem, WAGMI) web uygulamaları inşa etmekten zaten anladığın kavramlarla nasıl eşleştiğini açıklayacağım.

Otomat makinesi metaforları yok. "Bir dünya hayal edin..." yok. Sadece teknik model ve kod.

Zihinsel Model#

Ethereum çoğaltılmış bir durum makinesi. Ağdaki her düğüm durumun aynı kopyasını tutar — adresleri hesap verilerine eşleyen devasa bir anahtar-değer deposu. "Transaction gönderdiğinde", bir durum geçişi öneriyorsun. Yeterli doğrulayıcı geçerli olduğuna katılırsa, durum güncellenir. Hepsi bu.

Durumun kendisi basit. 20-byte adreslerden hesap nesnelerine bir eşleme. Her hesabın dört alanı var:

  • nonce: Bu hesabın kaç transaction gönderdiği (EOA'lar için) veya kaç contract oluşturduğu (contract hesapları için). Replay saldırılarını önler.
  • balance: Wei cinsinden ETH miktarı (1 ETH = 10^18 wei). Her zaman büyük tam sayı.
  • codeHash: EVM bytecode'unun hash'i. Normal cüzdanlar (EOA'lar) için boş byte'ların hash'i. Contract'lar için deploy edilen kodun hash'i.
  • storageRoot: Hesabın depolama trie'sinin kök hash'i. Sadece contract'ların anlamlı depolaması var.

İki tür hesap var ve ayrım bundan sonraki her şey için önemli:

Harici Sahipli Hesaplar (EOA'lar) private key tarafından kontrol edilir. MetaMask'ın yönettiği şey bunlar. Transaction başlatabilirler. Kodları yok.

Contract Hesapları kodları tarafından kontrol edilir. Transaction başlatamazlar — sadece çağrılmalarına yanıt olarak çalışabilirler. Kodları ve depolamaları var. Deploy edildiğinde kod değişmez (proxy kalıpları hariç).

Kritik içgörü: Ethereum'daki her durum değişikliği bir EOA'nın transaction imzalamasıyla başlar. Contract'lar başka contract'ları çağırabilir ama çalıştırma zinciri her zaman private key'e sahip bir insanla (veya botla) başlar.

Gas: Hesaplamanın Bir Fiyatı Var#

EVM'deki her operasyon gas maliyetine sahip. İki sayı toplamak 3 gas. 32-byte bir kelime saklamak 20.000 gas (ilk sefer) veya 5.000 gas (güncelleme). Depolama okumak 2.100 gas (soğuk) veya 100 gas (sıcak).

EIP-1559'dan sonra gas fiyatlandırması iki parçalı bir sistem oldu:

toplamMaliyet = gasKullanımı * (baseFee + priorityFee)
  • baseFee: Ağ tıkanıklığına göre protokol tarafından belirlenir. Yakılır (yok edilir).
  • priorityFee (bahşiş): Doğrulayıcıya gider. Daha yüksek bahşiş = daha hızlı dahil edilme.

Web geliştiriciler için pratik anlam: veri okumak bedava. Veri yazmak para maliyeti. Bu Web2 ile Web3 arasındaki en önemli mimari fark. Her SELECT bedava. Her INSERT, UPDATE, DELETE gerçek paraya mal olur.

Transaction'lar#

Transaction imzalanmış bir veri yapısı:

typescript
interface Transaction {
  to: string;      // 20-byte hex adres, veya contract deploy için null
  value: bigint;   // Gönderilecek ETH miktarı (wei cinsinden)
  data: string;    // Kodlanmış fonksiyon çağrısı verisi
  nonce: number;   // Sıralı sayaç, replay saldırılarını önler
  gasLimit: bigint;
  maxFeePerGas: bigint;
  maxPriorityFeePerGas: bigint;
  chainId: number; // 1 = mainnet, 11155111 = Sepolia, 137 = Polygon
}

Bir Transaction'ın Yaşam Döngüsü#

  1. Oluşturma: Uygulamanın transaction nesnesini inşa eder.
  2. İmzalama: Private key RLP-kodlanmış transaction'ı imzalar.
  3. Yayınlama: İmzalı transaction eth_sendRawTransaction ile bir RPC node'una gönderilir.
  4. Mempool: Transaction bekleyen transaction'lar havuzunda oturur. Doğrulayıcılar dahil edilecek transaction'ları seçer.
  5. Dahil Edilme: Bir doğrulayıcı transaction'ını bir bloğa dahil eder. EVM çalıştırır.
  6. Kesinleşme: Proof-of-stake Ethereum'da bir blok iki epoch sonra (~12.8 dakika) "kesinleşir".

Smart Contract Etkileşimi#

ABI: Contract'ın API Tanımı#

ABI (Application Binary Interface) contract'ın fonksiyonlarını ve verilerini tanımlar — REST API'ın OpenAPI spec'i gibi düşün:

typescript
const ERC20_ABI = [
  "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 transfer(address to, uint256 amount) returns (bool)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "event Transfer(address indexed from, address indexed to, uint256 value)",
];

Fonksiyon Seçiciler ve Calldata#

transfer(address,uint256) çağırdığında aslında blockchain'e raw byte gönderiyorsun. Calldata'nın ilk 4 byte'ı fonksiyon seçicisi — fonksiyon imzasının Keccak-256 hash'inin ilk 4 byte'ı:

transfer(address,uint256) → keccak256 → 0xa9059cbb...
İlk 4 byte: 0xa9059cbb

Kalan byte'lar her biri 32 byte'a doldurulmuş ABI-kodlanmış argümanlar. Bu kodlama şeması deterministik — aynı fonksiyon çağrısı her zaman aynı calldata'yı üretir.

Bu neden önemli? Çünkü Etherscan'de raw transaction verisini gördüğünde ve 0xa9059cbb ile başladığında, bunun bir transfer çağrısı olduğunu bilirsin. Transaction'ın revert olduğunda ve hata mesajı sadece bir hex blob olduğunda, ABI kullanarak decode edebilirsin. Ve transaction batch'leri oluştururken veya multicall contract'larla etkileşirken calldata'yı manuel olarak kodlayacaksın.

ethers.js ile manuel olarak nasıl encode ve decode yapılır:

typescript
import { ethers } from "ethers";
 
const iface = new ethers.Interface(ERC20_ABI);
 
// Encode a function call
const calldata = iface.encodeFunctionData("transfer", [
  "0xBobAddress...",
  1000000n,
]);
console.log(calldata);
// 0xa9059cbb000000000000000000000000bob...000000000000000000000000000f4240
 
// Decode calldata back to function name and args
const decoded = iface.parseTransaction({ data: calldata });
console.log(decoded.name);       // "transfer"
console.log(decoded.args[0]);    // "0xBobAddress..."
console.log(decoded.args[1]);    // 1000000n (BigInt)
 
// Decode a function's return data
const returnData = "0x0000000000000000000000000000000000000000000000000000000000000001";
const result = iface.decodeFunctionResult("transfer", returnData);
console.log(result[0]); // true

Depolama Slot'ları#

Contract depolaması hem anahtarların hem değerlerin 32 byte olduğu bir anahtar-değer deposu. Solidity depolama slot'larını 0'dan başlayarak sırayla atar. İlk bildirilen durum değişkeni slot 0'a, sonraki slot 1'e gider ve devam eder. Mapping'ler ve dinamik diziler hash tabanlı bir şema kullanır.

Depolamayı doğrudan okuyabilirsin — contract'ın bir view fonksiyonu sunmasını beklemeye gerek yok:

typescript
import { ethers } from "ethers";
 
// Bir contract'ın slot 0'ını oku
const value = await provider.getStorage("0xContractAddress...", 0);
console.log(value); // 32 byte hex
 
// Bir mapping girişi oku (slot hesaplaması gerekir)
// mapping(address => uint256) slot 3'te ise:
const slot = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ["address", "uint256"],
    ["0xUserAddress...", 3] // anahtar, mapping'in slot numarası
  )
);
const balance = await provider.getStorage("0xContractAddress...", slot);

Bu gelişmiş ama güçlü. Bir contract bir public getter sunmasa bile verilerini doğrudan depolamadan okuyabilirsin. Etherscan'in contract depolamayı nasıl gösterdiği ve "decompile" araçlarının nasıl çalıştığı budur.

Statik Çağrılar vs Transaction'lar#

Tekrarlığa değer çünkü bu web geliştiriciler için en yaygın kafa karışıklığı noktası:

Statik çağrı (eth_call): Blockchain'i değiştirmeden bir fonksiyonu simüle eder. Bedava. Anında geri döner. Sonucu doğrudan alırsın. Bu, view veya pure fonksiyonları okumak için kullandığın şey. Bir REST API'a GET isteği yapmak gibi düşün.

Transaction (eth_sendRawTransaction): Blockchain durumunu değiştirir. Gas maliyeti. Blok dahil etme gerektirir (saniyeler ila dakikalar). Sadece bir transaction hash'i geri döner, gerçek sonucu değil. Sonucu almak için receipt'i beklemelisin. POST isteği gibi düşün ama işlemin tamamlanması dakikalar süren bir POST.

typescript
// Statik çağrı — anında, bedava
const balance = await contract.balanceOf("0x..."); // Değeri döndürür
 
// Transaction — gas maliyeti, onay bekler
const tx = await contract.transfer("0x...", 1000n); // Hash döndürür
const receipt = await tx.wait();                     // Dahil edilmeyi bekler
console.log(receipt.status); // 1 = başarılı, 0 = revert

RPC Provider'ları Anlamak#

RPC (Remote Procedure Call) provider'ı uygulaman ile Ethereum ağı arasındaki geçit. Kendi Ethereum node'unu çalıştırmadığın sürece (ki çoğu dApp çalıştırmaz), birinin node'u üzerinden JSON-RPC istekleri yapıyorsun.

Büyük Provider'lar#

ProviderÜcretsiz KatmanGüçlü Yönler
Alchemy300M işlem birimi/ayEn iyi geliştirici araçları, gelişmiş API'lar
Infura100K istek/günEn eski, en yaygın
QuickNode10M API kredisi/ayEn hızlı yanıt süreleri
Ankr30 istek/saniyeİyi fiyat/performans oranı

JSON-RPC, İstisna Yerine Hata Döndürür#

Bu önemli bir dikkat noktası. JSON-RPC çağrıları HTTP hatası fırlatmaz — response gövdesinde hata döndürür:

typescript
// eth_call "Yetersiz bakiye" ile başarısız olursa
// HTTP yanıtı yine 200 OK gelir
// Hata response.result yerine response.error'dadır
 
// ethers.js ve viem bunu senin için halleder ve uygun şekilde fırlatır
// Ama raw fetch yapıyorsan dikkat et:
const response = await fetch(RPC_URL, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "eth_call",
    params: [{ to: "0x...", data: "0x..." }, "latest"],
  }),
});
 
const json = await response.json();
if (json.error) {
  // İşte gerçek hata!
  console.error(json.error.message);
}

İlk Pratik Adım: Cüzdan Olmadan Token Verisi Okumak#

Hepsi bir araya geldiğinde en basit kullanışlı şey: bir ERC-20 token'ı hakkında on-chain veri okumak. Cüzdan gerekmez. Gas gerekmez. Transaction gerekmez.

typescript
import { ethers } from "ethers";
 
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
 
// ERC-20 arayüzü — sadece okuma fonksiyonları
const erc20 = new ethers.Contract(
  "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
  [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function decimals() view returns (uint8)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint256)",
  ],
  provider // Not: provider, signer değil. Salt okunur.
);
 
const [name, symbol, decimals, totalSupply] = await Promise.all([
  erc20.name(),
  erc20.symbol(),
  erc20.decimals(),
  erc20.totalSupply(),
]);
 
console.log(`${name} (${symbol})`);           // "USD Coin (USDC)"
console.log(`Decimals: ${decimals}`);          // 6 (18 DEĞİL!)
console.log(`Total supply: ${ethers.formatUnits(totalSupply, decimals)}`);
 
// Belirli bir adresin bakiyesini kontrol et
const balance = await erc20.balanceOf("0xSomeAddress...");
console.log(`Balance: ${ethers.formatUnits(balance, decimals)} USDC`);

Cüzdan yok. Gas yok. Transaction yok. Perde arkasında sadece bir JSON-RPC eth_call. Bu kavramsal olarak REST API'a GET isteği yapmakla aynı. Blockchain veritabanı, contract API ve eth_call SELECT sorgun.

ethers.js v6#

ethers.js Web3'ün jQuery'si — çoğu geliştiricinin ilk öğrendiği kütüphane ve hala en yaygın kullanılan. Sürüm 6, native BigInt desteği (sonunda), ESM modülleri ve daha temiz bir API ile v5'e göre önemli bir gelişme.

Üç Temel Soyutlama#

Provider: Blockchain'e salt okunur bağlantı. View fonksiyonları çağırabilir, bloklar getirebilir, log'ları okuyabilir. İmzalama veya transaction gönderme yapamaz.

typescript
import { ethers } from "ethers";
 
// Bir node'a bağlan
const provider = new ethers.JsonRpcProvider("https://...");
 
// Temel sorgular
const blockNumber = await provider.getBlockNumber();
const balance = await provider.getBalance("0xAddress...");
const block = await provider.getBlock(blockNumber);
const txCount = await provider.getTransactionCount("0xAddress...");

Signer: Private key üzerinde bir soyutlama. Transaction ve mesaj imzalayabilir. Bir Signer her zaman bir Provider'a bağlıdır.

typescript
// Private key'den (sunucu tarafı, script'ler)
const wallet = new ethers.Wallet("0xPrivateKey...", provider);
 
// Tarayıcı cüzdanından (istemci tarafı)
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
 
// Adresi al
const address = await signer.getAddress();

Contract: Deploy edilmiş bir contract için JavaScript proxy'si. Contract nesnesindeki metotlar ABI'deki fonksiyonlara karşılık gelir. View fonksiyonları değer döndürür. Durum değiştiren fonksiyonlar TransactionResponse döndürür.

typescript
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
 
// Okuma (bedava, değeri doğrudan döndürür)
const balance = await usdc.balanceOf("0xSomeAddress...");
// balance bir bigint: 1000000000n (6 ondalıklı 1000 USDC)
 
// Yazmak için signer ile bağla
const usdcWithSigner = usdc.connect(signer);
 
// Yazma (gas maliyeti, TransactionResponse döndürür)
const tx = await usdcWithSigner.transfer("0xRecipient...", 1000000n);
const receipt = await tx.wait(); // Blok dahil edilmesini bekle
 
if (receipt.status === 0) {
  throw new Error("Transaction revert oldu");
}

Tip Güvenliği İçin TypeChain#

Raw ABI etkileşimleri string tipli. Fonksiyon adını yanlış yazabilir, yanlış argüman tipleri geçirebilir veya dönüş değerlerini yanlış yorumlayabilirsin. TypeChain, ABI dosyalarından TypeScript tipleri üretir:

typescript
// TypeChain olmadan — tip kontrolü yok
const balance = await contract.balanceOf("0x...");
// balance 'any'. Otocompletion yok. Yanlış kullanım kolay.
 
// TypeChain ile — tam tip güvenliği
import { USDC__factory } from "./typechain";
 
const usdc = USDC__factory.connect(USDC_ADDRESS, provider);
const balance = await usdc.balanceOf("0x...");
// balance BigNumber. Otocompletion çalışır. Tip hataları derleme zamanında yakalanır.

Yeni projeler için, ayrı bir kod üretim adımı olmadan aynı sonucu elde eden viem'in ABI'lerden yerleşik tip çıkarımını kullanmayı düşün.

Event Dinleme#

Gerçek zamanlı event akışı duyarlı dApp'ler için kritik. ethers.js bunun için WebSocket provider'ları kullanır:

typescript
// Gerçek zamanlı event'ler için WebSocket
const wsProvider = new ethers.WebSocketProvider("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
 
const contract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, wsProvider);
 
// Tüm Transfer event'lerini dinle
contract.on("Transfer", (from, to, value, event) => {
  console.log(`Transfer: ${from} -> ${to}`);
  console.log(`Miktar: ${ethers.formatUnits(value, 6)} USDC`);
});
 
// Belirli bir adrese yapılan transfer'leri dinle
const filter = contract.filters.Transfer(null, "0xMyAddress...");
contract.on(filter, (from, to, value) => {
  console.log(`Gelen transfer: ${ethers.formatUnits(value, 6)} USDC, gönderen: ${from}`);
});
 
// İşin bittiğinde temizle
contract.removeAllListeners();

WAGMI + Viem: Modern Yığın#

WAGMI (We're All Gonna Make It) Ethereum için React hooks kütüphanesi. Viem altında kullandığı TypeScript istemcisi. Birlikte, frontend dApp geliştirmede standart yığın olarak ethers.js + web3-react'ın yerini büyük ölçüde aldılar.

Neden geçiş? Üç neden: ABI'lerden tam TypeScript çıkarımı (codegen gerekmez), daha küçük bundle boyutu ve cüzdan etkileşimlerinin karmaşık asenkron durum yönetimini idare eden React hook'ları.

Kurulum#

typescript
// wagmi.config.ts
import { createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { injected, walletConnect } from "wagmi/connectors";
 
export const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    injected(),
    walletConnect({ projectId: "YOUR_WALLETCONNECT_PROJECT_ID" }),
  ],
  transports: {
    [mainnet.id]: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
    [sepolia.id]: http("https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY"),
  },
});
typescript
// app/providers.tsx
"use client";
 
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "./wagmi.config";
 
const queryClient = new QueryClient();
 
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Contract Verisi Okuma#

useReadContract en çok kullanacağın hook. eth_call'ı React Query önbellekleme, yeniden getirme ve yükleme/hata durumlarıyla sarar:

typescript
"use client";
 
import { useReadContract } from "wagmi";
import { formatUnits } from "viem";
 
const ERC20_ABI = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
] as const;
 
function TokenBalance({ address }: { address: `0x${string}` }) {
  const { data: balance, isLoading, error } = useReadContract({
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
    abi: ERC20_ABI,
    functionName: "balanceOf",
    args: [address],
  });
 
  if (isLoading) return <span>Yükleniyor...</span>;
  if (error) return <span>Hata: {error.message}</span>;
 
  // balance bigint olarak tipli çünkü ABI uint256 diyor
  return <span>{formatUnits(balance ?? 0n, 6)} USDC</span>;
}

ABI üzerindeki as const'a dikkat et. Bu kritik. Onsuz TypeScript literal tipleri kaybeder ve balance bigint yerine unknown olur. Tüm tip çıkarım sistemi const assertion'lara bağlı.

Contract'lara Yazma#

useWriteContract tam yaşam döngüsünü yönetir: cüzdan istemi, imzalama, yayınlama ve onay takibi.

typescript
"use client";
 
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseUnits } from "viem";
 
function SendTokens() {
  const { writeContract, data: hash, isPending, error } = useWriteContract();
 
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash,
  });
 
  function handleSend() {
    writeContract({
      address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      abi: ERC20_ABI,
      functionName: "transfer",
      args: [
        "0xRecipientAddress...",
        parseUnits("100", 6), // 100 USDC
      ],
    });
  }
 
  return (
    <div>
      <button onClick={handleSend} disabled={isPending}>
        {isPending ? "Cüzdanda onaylayın..." : "100 USDC Gönder"}
      </button>
 
      {hash && <p>Transaction: {hash}</p>}
      {isConfirming && <p>Onay bekleniyor...</p>}
      {isSuccess && <p>Transfer onaylandı!</p>}
      {error && <p>Hata: {error.message}</p>}
    </div>
  );
}

Event İzleme#

useWatchContractEvent gerçek zamanlı event izleme için bir WebSocket aboneliği kurar:

typescript
"use client";
 
import { useWatchContractEvent } from "wagmi";
import { useState } from "react";
import { formatUnits } from "viem";
 
interface TransferEvent {
  from: string;
  to: string;
  value: string;
}
 
function LiveTransfers() {
  const [transfers, setTransfers] = useState<TransferEvent[]>([]);
 
  useWatchContractEvent({
    address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    abi: ERC20_ABI,
    eventName: "Transfer",
    onLogs(logs) {
      const newTransfers = logs.map((log) => ({
        from: log.args.from as string,
        to: log.args.to as string,
        value: formatUnits(log.args.value as bigint, 6),
      }));
      setTransfers((prev) => [...newTransfers, ...prev].slice(0, 50));
    },
  });
 
  return (
    <ul>
      {transfers.map((t, i) => (
        <li key={i}>
          {t.from.slice(0, 8)}... → {t.to.slice(0, 8)}...: {t.value} USDC
        </li>
      ))}
    </ul>
  );
}

Cüzdan Bağlama Kalıpları#

Kullanıcının cüzdanını bağlamak Web3'ün "giriş yapma"sıdır. Ancak gerçekte giriş değildir. Oturum yok, çerez yok, sunucu tarafı durum yok. Cüzdan bağlantısı uygulamana kullanıcının adresini okuma ve transaction imza isteği gönderme izni verir. Hepsi bu.

EIP-1193 Provider Arayüzü#

Her cüzdan EIP-1193 tarafından tanımlanan standart bir arayüz sunar. request metodu olan bir nesne:

typescript
interface EIP1193Provider {
  request(args: { method: string; params?: unknown[] }): Promise<unknown>;
  on(event: string, handler: (...args: unknown[]) => void): void;
  removeListener(event: string, handler: (...args: unknown[]) => void): void;
}

MetaMask bunu window.ethereum olarak enjekte eder. Diğer cüzdanlar ya kendi property'lerini enjekte eder ya da window.ethereum'ı kullanır (bu çakışmalara neden olur — "cüzdan savaşları" sorunu, EIP-6963 tarafından kısmen çözüldü).

typescript
// Düşük seviye cüzdan etkileşimi (doğrudan yapmamalısın ama anlamak faydalı)
 
// Hesap erişimi iste
const accounts = await window.ethereum.request({
  method: "eth_requestAccounts",
});
console.log("Bağlı adres:", accounts[0]);
 
// Mevcut zinciri al
const chainId = await window.ethereum.request({
  method: "eth_chainId",
});
console.log("Chain ID:", parseInt(chainId, 16)); // "0x1" -> 1 (mainnet)
 
// Hesap değişikliklerini dinle (kullanıcı MetaMask'ta hesap değiştirdiğinde)
window.ethereum.on("accountsChanged", (accounts: string[]) => {
  if (accounts.length === 0) {
    console.log("Cüzdan bağlantısı kesildi");
  } else {
    console.log("Geçilen hesap:", accounts[0]);
  }
});
 
// Zincir değişikliklerini dinle (kullanıcı ağ değiştirdiğinde)
window.ethereum.on("chainChanged", (chainId: string) => {
  // Önerilen yaklaşım sayfayı yeniden yüklemektir
  window.location.reload();
});

EIP-6963: Çoklu Cüzdan Keşfi#

Eski window.ethereum yaklaşımı kullanıcının birden fazla cüzdanı kurulu olduğunda bozulur. window.ethereum'ı hangisi alır? Son enjekte eden? İlk? Bu bir yarış durumu.

EIP-6963 bunu tarayıcı event'lerine dayanan bir keşif protokolüyle çözer:

typescript
// Tüm mevcut cüzdanları keşfetme
interface EIP6963ProviderDetail {
  info: {
    uuid: string;
    name: string;
    icon: string;
    rdns: string;  // Ters alan adı, ör. "io.metamask"
  };
  provider: EIP1193Provider;
}
 
const wallets: EIP6963ProviderDetail[] = [];
 
window.addEventListener("eip6963:announceProvider", (event: CustomEvent) => {
  wallets.push(event.detail);
});
 
// Tüm cüzdanların kendilerini duyurmasını iste
window.dispatchEvent(new Event("eip6963:requestProvider"));
 
// Artık 'wallets' tüm kurulu cüzdanları adları ve ikonlarıyla içerir
// Cüzdan seçim arayüzü gösterebilirsin

WAGMI tüm bunları senin için halleder. injected() connector'ını kullandığında, mevcutsa otomatik olarak EIP-6963'ü kullanır ve window.ethereum'a geri düşer.

WalletConnect#

WalletConnect, mobil cüzdanları masaüstü dApp'lere bir relay sunucusu üzerinden bağlayan bir protokol. Kullanıcı mobil cüzdanıyla bir QR kod tarar ve şifreli bağlantı kurulur. Transaction istekleri dApp'inden telefonlarına iletilir.

WAGMI ile sadece başka bir connector:

typescript
import { walletConnect } from "wagmi/connectors";
 
const connector = walletConnect({
  projectId: "YOUR_PROJECT_ID", // cloud.walletconnect.com'dan al
  showQrModal: true,
});

Zincir Değiştirme İşleme#

Kullanıcılar genellikle yanlış ağdadır. dApp'in Mainnet'te, onlar Sepolia'ya bağlı. Ya da Polygon'dalar ve sen Mainnet'e ihtiyaç duyuyorsun. WAGMI useSwitchChain sağlar:

typescript
"use client";
 
import { useAccount, useSwitchChain } from "wagmi";
import { mainnet } from "wagmi/chains";
 
function NetworkGuard({ children }: { children: React.ReactNode }) {
  const { chain } = useAccount();
  const { switchChain, isPending } = useSwitchChain();
 
  if (!chain) return <p>Lütfen cüzdanınızı bağlayın</p>;
 
  if (chain.id !== mainnet.id) {
    return (
      <div>
        <p>Lütfen Ethereum Mainnet'e geçin</p>
        <button
          onClick={() => switchChain({ chainId: mainnet.id })}
          disabled={isPending}
        >
          {isPending ? "Geçiş yapılıyor..." : "Ağ Değiştir"}
        </button>
      </div>
    );
  }
 
  return <>{children}</>;
}

IPFS ve Metadata#

NFT'ler görselleri on-chain saklamaz. Blockchain, bir JSON metadata dosyasına işaret eden bir URI saklar ve bu dosya da görselin URL'sini içerir. ERC-721'in tokenURI fonksiyonu tarafından tanımlanan standart kalıp:

Contract.tokenURI(42) → "ipfs://QmXyz.../42.json"

Bu JSON dosyası standart bir şema izler:

json
{
  "name": "Cool NFT #42",
  "description": "A very cool NFT",
  "image": "ipfs://QmImageHash...",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Rarity", "value": "Legendary" }
  ]
}

IPFS CID vs URL#

IPFS adresleri Content Identifier (CID) kullanır — içeriğin kendisinin hash'leri. ipfs://QmXyz... "hash'i QmXyz... olan içerik" anlamına gelir. Bu içerik adresli depolama: URI içerikten türetilir, bu yüzden içerik URI'yı değiştirmeden asla değişemez. NFT'lerin güvendiği değişmezlik garantisi budur (gerçekten IPFS kullandıklarında — birçoğu bunun yerine merkezi URL'ler kullanır, bu bir kırmızı bayrak).

Tarayıcıda IPFS içeriğini görüntülemek için IPFS URI'larını HTTP'ye çeviren bir gateway gerekir:

typescript
function ipfsToHttp(uri: string): string {
  if (uri.startsWith("ipfs://")) {
    const cid = uri.replace("ipfs://", "");
    return `https://ipfs.io/ipfs/${cid}`;
    // Veya adanmış bir gateway kullan:
    // return `https://YOUR_PROJECT.mypinata.cloud/ipfs/${cid}`;
  }
  return uri;
}
 
// NFT metadata'sı getirme
async function getNftMetadata(
  contractAddress: string,
  tokenId: bigint,
  provider: ethers.Provider
) {
  const contract = new ethers.Contract(
    contractAddress,
    ["function tokenURI(uint256 tokenId) view returns (string)"],
    provider
  );
 
  const tokenUri = await contract.tokenURI(tokenId);
  const httpUri = ipfsToHttp(tokenUri);
 
  const response = await fetch(httpUri);
  const metadata = await response.json();
 
  return {
    name: metadata.name,
    description: metadata.description,
    image: ipfsToHttp(metadata.image),
    attributes: metadata.attributes,
  };
}

Pinning Servisleri#

IPFS eşler arası bir ağ. İçerik yalnızca birisi barındırdığı ("pinlediği") sürece erişilebilir kalır. IPFS'e bir NFT görseli yükler sonra node'unu kapatırsan içerik kaybolur.

Pinning servisleri içeriğini erişilebilir tutar:

  • Pinata: En popüler. Basit API. Cömert ücretsiz katman (1GB). Daha hızlı yükleme için adanmış gateway'ler.
  • NFT.Storage: Ücretsiz, Protocol Labs (IPFS'in yaratıcıları) destekli. NFT metadata'sı için özel tasarlanmış. Uzun vadeli kalıcılık için Filecoin kullanır.
  • Web3.Storage: NFT.Storage'a benzer, daha genel amaçlı.
typescript
// Pinata'ya yükleme
async function pinToIpfs(file: File): Promise<string> {
  const formData = new FormData();
  formData.append("file", file);
 
  const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${PINATA_JWT}`,
    },
    body: formData,
  });
 
  const result = await response.json();
  return `ipfs://${result.IpfsHash}`; // CID döndürür
}

İndeksleme Sorunu#

İşte blockchain geliştirmenin kirli sırrı: bir RPC node'undan geçmiş verileri verimli şekilde sorgulayamazsın.

Son bir yıldaki bir token'ın tüm Transfer event'lerini mi istiyorsun? Milyonlarca bloğu eth_getLogs ile taramak gerekir, 2.000-10.000 blok'luk parçalar halinde sayfalama yaparak (maksimum provider'a göre değişir). Bu binlerce RPC çağrısı. Dakikalar ila saatler sürecek ve API kotanı tüketecek.

Belirli bir adresin sahip olduğu tüm token'ları mı istiyorsun? Bunun için tek bir RPC çağrısı yok. Her ERC-20 contract'ın her Transfer event'ini tarayıp bakiyeleri takip etmen gerekir. Bu uygulanabilir değil.

Blockchain yazma-optimize edilmiş bir veri yapısı. Yeni transaction'ları işlemekte mükemmel. Geçmiş sorguları yanıtlamakta berbat. Bu, dApp arayüzlerinin ihtiyaç duyduğu ile zincirin doğal olarak sağladığı arasındaki temel uyumsuzluk.

The Graph Protokolü#

The Graph merkeziyetsiz bir indeksleme protokolü. Bir "subgraph" yazarsın — bir şema ve event handler seti — ve The Graph zinciri indeksler ve veriyi GraphQL API üzerinden sunar.

graphql
# Subgraph şeması (schema.graphql)
type Transfer @entity {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  blockNumber: BigInt!
  timestamp: BigInt!
}
 
type Account @entity {
  id: Bytes!
  balance: BigInt!
  transfersFrom: [Transfer!]! @derivedFrom(field: "from")
  transfersTo: [Transfer!]! @derivedFrom(field: "to")
}
typescript
// Frontend'inden bir subgraph sorgulama
const SUBGRAPH_URL =
  "https://api.studio.thegraph.com/query/YOUR_ID/YOUR_SUBGRAPH/v0.0.1";
 
async function getRecentTransfers(address: string) {
  const query = `
    query GetTransfers($address: Bytes!) {
      transfers(
        where: { from: $address }
        orderBy: blockNumber
        orderDirection: desc
        first: 100
      ) {
        id
        from
        to
        value
        blockNumber
        timestamp
      }
    }
  `;
 
  const response = await fetch(SUBGRAPH_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables: { address } }),
  });
 
  const { data } = await response.json();
  return data.transfers;
}

Takas: The Graph gecikme ekler (tipik olarak zincir başından 1-2 blok geride) ve başka bir bağımlılık. Merkeziyetsiz ağın da indeksleme maliyetleri var (GRT token'larıyla ödersin). Daha küçük projeler için barındırılan servis (Subgraph Studio) ücretsiz.

Alchemy ve Moralis Gelişmiş API'ları#

Bir subgraph bakımı yapmak istemiyorsan, hem Alchemy hem de Moralis yaygın sorguları doğrudan yanıtlayan önceden indekslenmiş API'lar sunar:

typescript
// Alchemy: Bir adresin tüm ERC-20 token bakiyelerini al
const response = await fetch(
  `https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "alchemy_getTokenBalances",
      params: ["0xAddress...", "erc20"],
    }),
  }
);
 
// Tek çağrıda TÜM ERC-20 token bakiyelerini döndürür
// vs. olası her ERC-20 contract'ın balanceOf()'unu taramak
typescript
// Alchemy: Bir adresin sahip olduğu tüm NFT'leri al
const response = await fetch(
  `https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY/getNFTs?owner=0xAddress...`
);
 
const { ownedNfts } = await response.json();
for (const nft of ownedNfts) {
  console.log(`${nft.title} - ${nft.contract.address}#${nft.tokenId}`);
}

Bu API'lar tescilli ve merkezi. Merkeziyetsizliği geliştirici deneyimi karşılığında takas ediyorsun. Çoğu dApp için bu değerli bir takas. Kullanıcıların portföy görünümünün bir subgraph'tan mı yoksa Alchemy'nin veritabanından mı geldiği umurlarında değil. 30 saniye yerine 200ms'de yüklenmesi umurlarında.

Yaygın Tuzaklar#

Birkaç production dApp gönderdikten ve diğer takımların kodunu debug ettikten sonra, bunlar sürekli gördüğüm hatalar. Her biri beni kişisel olarak ısırmıştır.

Her Yerde BigInt#

Ethereum çok büyük sayılarla çalışır. ETH bakiyeleri wei cinsindendir (10^18). Token arzları 10^27 veya daha yüksek olabilir. JavaScript Number yalnızca 2^53 - 1'e kadar (yaklaşık 9 * 10^15) tam sayıları güvenle temsil edebilir. Bu wei miktarları için yeterli değil.

typescript
// YANLIŞ — sessiz hassasiyet kaybı
const balance = 1000000000000000000; // wei cinsinden 1 ETH
const double = balance * 2;
console.log(double); // 2000000000000000000 — doğru görünüyor ama...
 
const largeBalance = 99999999999999999999; // ~100 ETH
console.log(largeBalance);          // 100000000000000000000 — YANLIŞ! Yukarı yuvarlanmış.
console.log(largeBalance === 100000000000000000000); // true — veri bozulması
 
// DOĞRU — BigInt kullan
const balance = 1000000000000000000n;
const double = balance * 2n;
console.log(double.toString()); // "2000000000000000000" — doğru
 
const largeBalance = 99999999999999999999n;
console.log(largeBalance.toString()); // "99999999999999999999" — doğru

dApp kodunda BigInt kuralları:

  1. Wei miktarlarını asla Number'a dönüştürme. Her yerde BigInt kullan, sadece görüntüleme için okunabilir string'lere dönüştür.
  2. BigInt'lerde asla Math.floor, Math.round vb. kullanma. Çalışmaz. Tamsayı bölme kullan: amount / 10n ** 6n.
  3. JSON BigInt desteklemez. BigInt içeren durumu serileştiriyorsan, özel bir serializer gerekir: JSON.stringify(data, (_, v) => typeof v === "bigint" ? v.toString() : v).
  4. Kütüphane formatlama fonksiyonlarını kullan. ethers.formatEther(), ethers.formatUnits(), viem'in formatEther(), formatUnits(). Dönüşümü doğru şekilde hallederler.
typescript
import { formatUnits, parseUnits } from "viem";
 
// Görüntüleme: BigInt → okunabilir string
const weiAmount = 1500000000000000000n; // 1.5 ETH
const display = formatUnits(weiAmount, 18); // "1.5"
 
// Girdi: okunabilir string → BigInt
const userInput = "1.5";
const wei = parseUnits(userInput, 18); // 1500000000000000000n
 
// USDC'nin 6 ondalığı var, 18 değil
const usdcAmount = 100000000n; // 100 USDC
const usdcDisplay = formatUnits(usdcAmount, 6); // "100.0"

Asenkron Cüzdan İşlemleri#

Her cüzdan etkileşimi asenkron ve uygulamanın zarif şekilde ele alması gereken yollarla başarısız olabilir:

typescript
// Kullanıcı herhangi bir cüzdan istemini reddedebilir
try {
  const tx = await writeContract({
    address: contractAddress,
    abi: ERC20_ABI,
    functionName: "approve",
    args: [spenderAddress, amount],
  });
} catch (error) {
  if (error.code === 4001) {
    // Kullanıcı cüzdanındaki transaction'ı reddetti
    // Bu normal — raporlanacak bir hata değil
    showToast("Transaction iptal edildi");
  } else if (error.code === -32603) {
    // Dahili JSON-RPC hatası — genellikle transaction'ın revert olacağı anlamına gelir
    showToast("Transaction başarısız olur. Bakiyenizi kontrol edin.");
  } else {
    // Beklenmeyen hata
    console.error("Transaction hatası:", error);
    showToast("Bir şeyler ters gitti. Lütfen tekrar deneyin.");
  }
}

Önemli asenkron tuzaklar:

  • Cüzdan istemleri kullanıcı tarafında engelleyici. Kodundaki await, kullanıcı MetaMask'ta transaction detaylarını okurken 30 saniye sürebilir. Bir şeylerin bozulduğunu düşünmelerine neden olacak bir yükleme göstergesi gösterme.
  • Kullanıcı etkileşim sırasında hesap değiştirebilir. Hesap A'dan onay istersin, kullanıcı Hesap B'ye geçer, sonra onaylar. Artık Hesap B onay vermiş ama sen Hesap A'dan transaction göndereceksin. Kritik operasyonlardan önce bağlı hesabı her zaman yeniden kontrol et.
  • İki adımlı yazma kalıpları yaygın. Birçok DeFi operasyonu approve + execute gerektirir. Kullanıcının iki transaction imzalaması gerekir. Onaylayıp çalıştırmazsa, izin durumunu kontrol etmen ve bir sonraki seferde onay adımını atlaman gerekir.

Ağ Uyumsuzluk Hataları#

Bu hata diğer tüm sorunlardan daha fazla debug zamanı harcar. Contract'ın Mainnet'te. Cüzdanın Sepolia'da. RPC provider'ın Polygon'u gösteriyor. Üç farklı ağ, üç farklı durum, üç tamamen ilgisiz blockchain. Ve hata mesajı genellikle işe yaramaz — "execution reverted" veya "contract not found."

typescript
// Savunmacı zincir kontrolü
import { useAccount, useChainId } from "wagmi";
 
function useRequireChain(requiredChainId: number) {
  const chainId = useChainId();
  const { isConnected } = useAccount();
 
  if (!isConnected) {
    return { ready: false, error: "Lütfen cüzdanınızı bağlayın" };
  }
 
  if (chainId !== requiredChainId) {
    return {
      ready: false,
      error: `Lütfen ${getChainName(requiredChainId)} ağına geçin. Şu anda ${getChainName(chainId)} ağındasınız.`,
    };
  }
 
  return { ready: true, error: null };
}

DeFi'de Front-Running#

Bir DEX'te swap gönderdiğinde, bekleyen transaction'ın mempool'da görünür. Bir bot alım satımını görebilir, fiyatı yukarı iterek önüne geçebilir, senin alım satımının daha kötü fiyattan gerçekleşmesini sağlayabilir ve ardından hemen satarak kar edebilir. Buna "sandviç saldırısı" denir.

Frontend geliştirici olarak bunu tamamen engelleyemezsin ama azaltabilirsin:

typescript
// Uniswap tarzı swap'ta slippage toleransı ayarlama
const amountOutMin = expectedOutput * 995n / 1000n; // %0,5 slippage toleransı
 
// Uzun ömürlü bekleyen transaction'ları önlemek için deadline kullanma
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 dakika
 
await router.swapExactTokensForTokens(
  amountIn,
  amountOutMin,  // Kabul edilebilir minimum çıktı — daha az alırsak revert et
  [tokenA, tokenB],
  userAddress,
  deadline,       // 20 dakika içinde çalıştırılmazsa revert et
);

Yüksek değerli transaction'lar için, transaction'ları halka açık mempool yerine doğrudan blok oluşturuculara gönderen Flashbots Protect RPC kullanmayı düşün. Bu sandviç saldırılarını tamamen önler çünkü botlar bekleyen transaction'ını asla görmez:

typescript
// RPC endpoint olarak Flashbots Protect kullanma
const provider = new ethers.JsonRpcProvider("https://rpc.flashbots.net");

Ondalık Karışıklığı#

Tüm token'ların 18 ondalığı yok. USDC ve USDT'nin 6'sı var. WBTC'nin 8'i. Bazı token'ların 0, 2 veya rastgele ondalıkları var. Miktarları biçimlendirmeden önce contract'tan her zaman decimals() değerini oku:

typescript
async function formatTokenAmount(
  tokenAddress: string,
  rawAmount: bigint,
  provider: ethers.Provider
): Promise<string> {
  const contract = new ethers.Contract(
    tokenAddress,
    ["function decimals() view returns (uint8)", "function symbol() view returns (string)"],
    provider
  );
 
  const [decimals, symbol] = await Promise.all([
    contract.decimals(),
    contract.symbol(),
  ]);
 
  return `${ethers.formatUnits(rawAmount, decimals)} ${symbol}`;
}
 
// formatTokenAmount(USDC, 1000000n, provider) → "1.0 USDC"
// formatTokenAmount(WETH, 1000000000000000000n, provider) → "1.0 WETH"
// formatTokenAmount(WBTC, 100000000n, provider) → "1.0 WBTC"

Gas Tahmini Başarısızlıkları#

estimateGas başarısız olduğunda, genellikle transaction'ın revert olacağı anlamına gelir. Ama hata mesajı çoğunlukla neden olduğuna dair hiçbir gösterge olmadan sadece "cannot estimate gas" olur. Transaction'ı simüle etmek ve gerçek revert nedenini almak için eth_call kullan:

typescript
import { createPublicClient, http, decodeFunctionResult } from "viem";
 
async function simulateAndGetError(client: ReturnType<typeof createPublicClient>, tx: object) {
  try {
    await client.call({
      account: tx.from,
      to: tx.to,
      data: tx.data,
      value: tx.value,
    });
    return null; // Hata yok — transaction başarılı olur
  } catch (error) {
    // Revert nedenini decode et
    if (error.data) {
      // Yaygın revert string'leri
      if (error.data.startsWith("0x08c379a0")) {
        // Error(string) — mesajlı standart revert
        const reason = decodeAbiParameters(
          [{ type: "string" }],
          `0x${error.data.slice(10)}`
        );
        return `Revert: ${reason[0]}`;
      }
    }
    return error.message;
  }
}

Hepsini Bir Araya Getirme#

İşte cüzdan bağlayan, token bakiyesi okuyan ve transfer gönderen eksiksiz, minimal bir React bileşeni. Bu her dApp'in iskeleti:

typescript
"use client";
 
import { useAccount, useConnect, useDisconnect, useReadContract, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { injected } from "wagmi/connectors";
import { formatUnits, parseUnits } from "viem";
import { useState } from "react";
 
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
 
const USDC_ABI = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "", type: "bool" }],
  },
] as const;
 
export function TokenDashboard() {
  const { address, isConnected } = useAccount();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();
 
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");
 
  // Bakiye oku — sadece address tanımlıyken çalışır
  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: USDC_ADDRESS,
    abi: USDC_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });
 
  // Yazma: token transfer et
  const {
    writeContract,
    data: txHash,
    isPending: isSigning,
    error: writeError,
  } = useWriteContract();
 
  // Onay bekle
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  });
 
  // Onaydan sonra bakiyeyi yeniden getir
  if (isSuccess) {
    refetchBalance();
  }
 
  if (!isConnected) {
    return (
      <button onClick={() => connect({ connector: injected() })}>
        Cüzdan Bağla
      </button>
    );
  }
 
  return (
    <div>
      <p>Bağlı: {address}</p>
      <p>
        USDC Bakiye:{" "}
        {balance !== undefined ? formatUnits(balance, 6) : "Yükleniyor..."}
      </p>
 
      <div>
        <input
          placeholder="Alıcı adresi (0x...)"
          value={recipient}
          onChange={(e) => setRecipient(e.target.value)}
        />
        <input
          placeholder="Miktar (ör. 100)"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
        />
        <button
          onClick={() => {
            writeContract({
              address: USDC_ADDRESS,
              abi: USDC_ABI,
              functionName: "transfer",
              args: [recipient as `0x${string}`, parseUnits(amount, 6)],
            });
          }}
          disabled={isSigning || isConfirming}
        >
          {isSigning
            ? "Cüzdanda onaylayın..."
            : isConfirming
              ? "Onaylanıyor..."
              : "USDC Gönder"}
        </button>
      </div>
 
      {writeError && <p style={{ color: "red" }}>{writeError.message}</p>}
      {isSuccess && <p style={{ color: "green" }}>Transfer onaylandı!</p>}
      {txHash && (
        <a
          href={`https://etherscan.io/tx/${txHash}`}
          target="_blank"
          rel="noopener noreferrer"
        >
          Etherscan'de Görüntüle
        </a>
      )}
 
      <button onClick={() => disconnect()}>Bağlantıyı Kes</button>
    </div>
  );
}

Buradan Sonra Nereye#

Bu yazı Ethereum'a giren web geliştiricileri için temel kavramları ve araçları kapsadı. Her alanda çok daha fazla derinlik var:

  • Solidity: Contract'ları sadece etkileşimde bulunmak değil, yazmak istiyorsan. Resmi dokümanlar ve Patrick Collins'in kursları en iyi başlangıç noktaları.
  • ERC standartları: ERC-20 (fungible token'lar), ERC-721 (NFT'ler), ERC-1155 (çoklu token), ERC-4626 (tokenize edilmiş vault'lar). Her biri o kategorideki tüm contract'ların uyguladığı standart bir arayüz tanımlar.
  • Layer 2'ler: Arbitrum, Optimism, Base, zkSync. Aynı geliştirici deneyimi, daha düşük gas maliyetleri, biraz farklı güven varsayımları. ethers.js ve viem kodun aynı şekilde çalışır — sadece chain ID ve RPC URL'sini değiştir.
  • Hesap Soyutlama (ERC-4337): Cüzdan UX'inin sonraki evrimi. Gas sponsorluğu, sosyal kurtarma ve toplu transaction'ları destekleyen smart contract cüzdanlar. "Cüzdan bağla" kalıbının yöneldiği yer burası.
  • MEV ve transaction sıralaması: DeFi inşa ediyorsan, Maximal Extractable Value'yu anlamak isteğe bağlı değil. Flashbots dokümanları kanonik kaynak.

Blockchain ekosistemi hızlı ilerler ama bu yazıdaki temel kavramlar — hesaplar, transaction'lar, ABI encoding, RPC çağrıları, event indeksleme — 2015'ten beri değişmedi ve yakın zamanda değişmeyecek. Bunları iyi öğren ve geri kalan her şey sadece API yüzeyi.

İlgili Yazılar