İçeriğe geç
·11 dk okuma

Üretimde Zincir Üstü Veri: Kimsenin Size Söylemediği Şeyler

Blok zinciri verisi temiz, güvenilir veya kolay değildir. RPC hız limitleri, zincir yeniden düzenlemeleri, BigInt hataları ve indeksleme ödünleşimleri — gerçek DeFi ürünleri gönderirken öğrenilen zor dersler.

Paylaş:X / TwitterLinkedIn

Zincir üstü verinin doğası gereği güvenilir olduğuna dair bir fantezi var. Değiştirilemez defter. Şeffaf durum. Sadece oku ve işin bitti.

Ben de buna inanıyordum. Sonra bir DeFi kontrol panelini üretime gönderdim ve token bakiyelerimizin neden yanlış olduğunu, olay geçmişimizde neden boşluklar olduğunu ve veritabanımızın neden artık var olmayan bloklardan işlemler içerdiğini anlamak için üç hafta harcadım.

Zincir üstü veri ham, düşmanca ve uygulamanızı bir kullanıcı hata raporu gönderene kadar fark etmeyeceğiniz şekillerde bozacak uç durumlarla doludur. Bu yazı, zor yoldan öğrendiğim her şeyi kapsıyor.

Güvenilir Veri Yanılsaması#

İşte kimsenin size söylemediği ilk şey: blok zinciri size veri vermez. Size durum geçişleri verir. SELECT * FROM transfers WHERE user = '0x...' diye bir şey yoktur. Loglar, makbuzlar, depolama yuvaları ve çağrı izleri vardır — hepsi çözümlemek için bağlam gerektiren formatlarda kodlanmıştır.

Bir Transfer olay logu size from, to ve value verir. Token sembolünü söylemez. Ondalık sayıyı söylemez. Bunun meşru bir transfer mi yoksa üstten %3 kesen bir transfer-ücreti-olan token mu olduğunu söylemez. Bu bloğun 30 saniye sonra hâlâ var olup olmayacağını söylemez.

"Değiştirilemez" kısmı doğrudur — kesinleştikten sonra. Ama kesinleşme anlık değildir. Ve bir RPC düğümünden geri aldığınız veri mutlaka kesinleşmiş bir bloktan değildir. Çoğu geliştirici latest'i sorgular ve gerçek olarak kabul eder. Bu bir özellik değil, bir hatadır.

Bir de kodlama var. Her şey hex'tir. Adresler büyük-küçük harf karma sağlama toplamına sahiptir (veya değildir). Token miktarları 10^decimals ile çarpılmış tam sayılardır. 100 dolarlık bir USDC transferi zincir üstünde 100000000 gibi görünür çünkü USDC'nin 18 değil 6 ondalık basamağı vardır. Her ERC-20 token için 18 ondalık basamak varsayan üretim kodu gördüm. Sonuçtaki bakiyeler 10^12 kat hatalıydı.

RPC Hız Limitleri Hafta Sonunuzu Mahvedecek#

Her üretim Web3 uygulaması bir RPC uç noktasıyla konuşur. Ve her RPC uç noktasının beklediğinizden çok daha agresif hız limitleri vardır.

İşte önemli olan sayılar:

  • Alchemy Ücretsiz: ~30M hesaplama birimi/ay, 40 istek/dakika. Tek bir geniş blok aralığındaki eth_getLogs çağrısının yüzlerce hesaplama birimi yiyebileceğini fark edene kadar cömert görünür. Bir günlük indekslemede aylık kotanızı tüketirsiniz.
  • Infura Ücretsiz: 100K istek/gün, kabaca 1,15 istek/saniye. Bu hızda 500K blok olay logunu sayfalamayı deneyin.
  • QuickNode Ücretsiz: Infura'ya benzer — 100K istek/gün.

Ücretli katmanlar yardımcı olur ama sorunu ortadan kaldırmaz. Alchemy'nin Growth planında ayda 200 dolar bile ödüyor olsanız, yoğun bir indeksleme işi verim limitlerine ulaşacaktır. Ve limitlere ulaştığınızda, zarif bir performans düşüşü elde etmezsiniz. 429 hataları alırsınız, bazen yararsız mesajlarla, bazen retry-after başlığı olmadan.

Çözüm, yedek sağlayıcılar, yeniden deneme mantığı ve hangi çağrıları yaptığınız konusunda çok bilinçli olmanın bir kombinasyonudur. İşte viem ile sağlam bir RPC kurulumunun nasıl göründüğü:

typescript
import { createPublicClient, fallback, http } from "viem";
import { mainnet } from "viem/chains";
 
const client = createPublicClient({
  chain: mainnet,
  transport: fallback(
    [
      http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY", {
        retryCount: 3,
        retryDelay: 1500,
        timeout: 15_000,
      }),
      http("https://mainnet.infura.io/v3/YOUR_KEY", {
        retryCount: 3,
        retryDelay: 1500,
        timeout: 15_000,
      }),
      http("https://rpc.ankr.com/eth", {
        retryCount: 2,
        retryDelay: 2000,
        timeout: 20_000,
      }),
    ],
    { rank: true }
  ),
});

rank: true seçeneği kritiktir. viem'e her transport için gecikme ve başarı oranını ölçmesini ve otomatik olarak en hızlı, en güvenilir olanı tercih etmesini söyler. Alchemy sizi hız sınırlamaya başlarsa, viem trafiği Infura'ya kaydırır. Infura çökerse, Ankr'a düşer.

Ama bir incelik var: viem'in varsayılan yeniden deneme mantığı üstel geri çekilme kullanır, bu genellikle istediğiniz şeydir. Ancak 2025 başı itibarıyla, toplu mod etkinleştirildiğinde retryCount'un RPC düzeyindeki hataları (429'lar gibi) düzgün şekilde yeniden denemediği bilinen bir sorun var. İstekleri topluyorsanız, yeniden deneme davranışınızı açıkça test edin. Çalıştığına güvenmeyin.

Yeniden Düzenlemeler: Göremeyeceğiniz Hata#

Bir zincir yeniden düzenlemesi, ağın hangi bloğun kanonik olduğu konusunda geçici olarak anlaşamadığında gerçekleşir. Düğüm A, [A, B, C] işlemlerine sahip blok 1000'i görür. Düğüm B, [A, D] işlemlerine sahip farklı bir blok 1000 görür. Sonunda ağ yakınsar ve bir sürüm kazanır.

İş ispatı zincirlerinde bu yaygındı — 1-3 blok yeniden düzenlemeleri günde birden fazla kez gerçekleşiyordu. Birleşme sonrası Ethereum daha iyidir. Başarılı bir yeniden düzenleme saldırısı artık doğrulayıcıların yaklaşık %50'sinin koordinasyonunu gerektirir. Ama "daha iyi" "imkansız" değildir. Mayıs 2022'de, önerici güçlendirme çatalının tutarsız istemci uygulamalarından kaynaklanan dikkat çekici bir 7 blok yeniden düzenleme oldu.

Ve yeniden düzenlemelerin Ethereum ana ağında ne kadar nadir olduğu önemli değil. L2'ler veya yan zincirler üzerinde inşa ediyorsanız — Polygon, Arbitrum, Optimism — yeniden düzenlemeler daha sıktır. Polygon tarihsel olarak 10+ blok yeniden düzenlemelerine sahipti.

İşte pratik sorun: blok 18.000.000'ı indekslediniz. Olayları veritabanınıza yazdınız. Sonra blok 18.000.000 yeniden düzenlendi. Artık veritabanınızda kanonik zincirde var olmayan bir bloktan olaylar var. Bu olaylar hiç gerçekleşmemiş işlemlere atıfta bulunabilir. Kullanıcılarınız hayalet transferler görür.

Çözüm mimarinize bağlıdır:

Seçenek 1: Onay gecikmesi. N blok onay geçene kadar veri indekslemeyin. Ethereum ana ağı için 64 blok (iki dönem) kesinlik garantisi verir. L2'ler için ilgili zincirin kesinlik modelini kontrol edin. Bu basittir ama gecikme ekler — Ethereum'da yaklaşık 13 dakika.

Seçenek 2: Yeniden düzenleme tespiti ve geri alma. Agresif şekilde indeksleyin ama blok hash'lerini takip edin. Her yeni blokta, üst hash'in daha önce indekslediğiniz blokla eşleştiğini doğrulayın. Eşleşmiyorsa, bir yeniden düzenleme tespit etmişsinizdir: yetim bloklardan her şeyi silin ve kanonik zinciri yeniden indeksleyin.

typescript
interface IndexedBlock {
  number: bigint;
  hash: `0x${string}`;
  parentHash: `0x${string}`;
}
 
async function detectReorg(
  client: PublicClient,
  lastIndexed: IndexedBlock
): Promise<{ reorged: boolean; depth: number }> {
  const currentBlock = await client.getBlock({
    blockNumber: lastIndexed.number,
  });
 
  if (currentBlock.hash === lastIndexed.hash) {
    return { reorged: false, depth: 0 };
  }
 
  // Zincirin nerede ayrıldığını bulmak için geriye doğru yürü
  let depth = 1;
  let checkNumber = lastIndexed.number - 1n;
 
  while (checkNumber > 0n && depth < 128) {
    const onChain = await client.getBlock({ blockNumber: checkNumber });
    const inDb = await getIndexedBlock(checkNumber); // veritabanı sorgunuz
 
    if (onChain.hash === inDb?.hash) {
      return { reorged: true, depth };
    }
 
    depth++;
    checkNumber--;
  }
 
  return { reorged: true, depth };
}

Bu varsayımsal değil. Yeniden düzenleme tespiti olmadan zincir ucunda olayları indekslediğim bir üretim sistemim vardı. Üç hafta sorunsuz çalıştı. Sonra Polygon'daki 2 blokluk bir yeniden düzenleme veritabanımızda yinelenen bir NFT basım olayına neden oldu. Ön yüz bir kullanıcının sahip olmadığı bir token'a sahip olduğunu gösterdi. Kimse kök neden olarak yeniden düzenlemelere bakmadığı için bunun hatası ayıklanması iki gün sürdü.

İndeksleme Sorunu: Acınızı Seçin#

Yapılandırılmış zincir üstü veriyi uygulamanıza almak için üç gerçek seçeneğiniz var.

Doğrudan RPC Çağrıları#

Doğrudan getLogs, getBlock, getTransaction çağırın. Bu küçük ölçekli okumalar için çalışır — bir kullanıcının bakiyesini kontrol etmek, tek bir sözleşme için son olayları getirmek. Tarihsel indeksleme veya sözleşmeler arası karmaşık sorgular için çalışmaz.

Sorun kombinatoriktir. Son 30 gündeki tüm Uniswap V3 takaslarını mı istiyorsunuz? Bu yaklaşık 200K bloktur. Alchemy'nin getLogs çağrısı başına 2K blok aralık limitiyle, bu en az 100 sayfalanmış istektir. Her biri hız limitinize sayılır. Ve herhangi bir çağrı başarısız olursa, yeniden deneme mantığı, imleç takibi ve kaldığınız yerden devam etme yoluna ihtiyacınız var.

The Graph (Alt Çizgeler)#

The Graph orijinal çözümdü. Bir şema tanımlayın, AssemblyScript ile eşlemeler yazın, dağıtın ve GraphQL ile sorgulayın. Barındırılan Hizmet kullanımdan kaldırıldı — artık her şey merkeziyetsiz Graph Ağı'nda, bu da sorgular için GRT token'larıyla ödeme yaptığınız anlamına geliyor.

İyi tarafı: standartlaştırılmış, iyi belgelenmiş, çatallayabileceğiniz geniş bir mevcut alt çizge ekosistemi.

Kötü tarafı: AssemblyScript acı vericidir. Hata ayıklama sınırlıdır. Dağıtım dakikalar ila saatler sürer. Alt çizgenizde bir hata varsa, yeniden dağıtırsınız ve sıfırdan yeniden senkronize olmasını beklersiniz. Merkeziyetsiz ağ gecikme ekler ve bazen indeksleyiciler zincir ucunun gerisinde kalır.

The Graph'i, 30-60 saniyelik veri tazeliğinin kabul edilebilir olduğu okuma yoğun kontrol panelleri için kullandım. Orada iyi çalışır. Gerçek zamanlı veri veya eşlemelerde karmaşık iş mantığı gerektiren herhangi bir şey için kullanmazdım.

Özel İndeksleyiciler (Ponder, Envio)#

Ekosistemin önemli ölçüde olgunlaştığı yer burası. Ponder ve Envio, indeksleme mantığını TypeScript'te (AssemblyScript değil) yazmanıza, geliştirme sırasında yerel olarak çalıştırmanıza ve bağımsız hizmetler olarak dağıtmanıza olanak tanır.

Ponder size maksimum kontrol verir. TypeScript'te olay işleyicileri tanımlarsınız, indeksleme hattını yönetir ve çıktı olarak bir SQL veritabanı elde edersiniz. Ödünleşim: altyapı sizin sorumluluğunuzdadır. Ölçekleme, izleme, yeniden düzenleme yönetimi — hepsi size ait.

Envio senkronizasyon hızı için optimize eder. Karşılaştırma testleri, The Graph'a kıyasla önemli ölçüde daha hızlı başlangıç senkronizasyon süreleri gösterir. Yeniden düzenlemeleri yerel olarak yönetir ve daha hızlı veri çekme için özelleştirilmiş bir protokol olan HyperSync'i destekler. Ödünleşim: onların altyapısına ve API'sine bağımlı olursunuz.

Önerim: üretim bir DeFi uygulaması inşa ediyorsanız ve mühendislik kapasiteniz varsa, Ponder kullanın. Mümkün olan en hızlı senkronizasyona ihtiyacınız varsa ve altyapı yönetmek istemiyorsanız, Envio'yu değerlendirin. Hızlı bir prototipe veya topluluk tarafından sürdürülen alt çizgelere ihtiyacınız varsa, The Graph hâlâ uygundur.

getLogs Göründüğünden Daha Tehlikelidir#

eth_getLogs RPC yöntemi aldatıcı derecede basittir. Bir blok aralığı ve bazı filtreler verin, eşleşen olay loglarını geri alın. İşte üretimde gerçekte olan:

Blok aralığı limitleri sağlayıcıya göre değişir. Alchemy 2K blokla (sınırsız log) veya sınırsız blokla (en fazla 10K log) sınırlar. Infura'nın farklı limitleri var. QuickNode'un farklı limitleri var. Herkese açık bir RPC 1K blokla sınırlayabilir. Kodunuz bunların hepsini yönetmelidir.

Yanıt boyutu limitleri mevcuttur. Blok aralığı içinde bile, popüler bir sözleşme blok başına binlerce olay yayarsa, yanıtınız sağlayıcının yük limitini (Alchemy'de 150MB) aşabilir. Çağrı kısmi sonuç döndürmez. Başarısız olur.

Boş aralıklar ücretsiz değildir. Sıfır eşleşen log olsa bile, sağlayıcı yine de blok aralığını tarar. Bu hesaplama birimlerinize sayılır.

İşte bu kısıtlamaları yöneten bir sayfalama yardımcı fonksiyonu:

typescript
import type { PublicClient, Log, AbiEvent } from "viem";
 
async function fetchLogsInChunks<T extends AbiEvent>(
  client: PublicClient,
  params: {
    address: `0x${string}`;
    event: T;
    fromBlock: bigint;
    toBlock: bigint;
    maxBlockRange?: bigint;
  }
): Promise<Log<bigint, number, false, T, true>[]> {
  const { address, event, fromBlock, toBlock, maxBlockRange = 2000n } = params;
  const allLogs: Log<bigint, number, false, T, true>[] = [];
 
  let currentFrom = fromBlock;
 
  while (currentFrom <= toBlock) {
    const currentTo =
      currentFrom + maxBlockRange - 1n > toBlock
        ? toBlock
        : currentFrom + maxBlockRange - 1n;
 
    try {
      const logs = await client.getLogs({
        address,
        event,
        fromBlock: currentFrom,
        toBlock: currentTo,
      });
 
      allLogs.push(...logs);
      currentFrom = currentTo + 1n;
    } catch (error) {
      // Aralık çok genişse (çok fazla sonuç), ikiye böl
      if (isRangeTooLargeError(error) && currentTo > currentFrom) {
        const mid = currentFrom + (currentTo - currentFrom) / 2n;
        const firstHalf = await fetchLogsInChunks(client, {
          address,
          event,
          fromBlock: currentFrom,
          toBlock: mid,
          maxBlockRange,
        });
        const secondHalf = await fetchLogsInChunks(client, {
          address,
          event,
          fromBlock: mid + 1n,
          toBlock: currentTo,
          maxBlockRange,
        });
        allLogs.push(...firstHalf, ...secondHalf);
        currentFrom = currentTo + 1n;
      } else {
        throw error;
      }
    }
  }
 
  return allLogs;
}
 
function isRangeTooLargeError(error: unknown): boolean {
  const message = error instanceof Error ? error.message : String(error);
  return (
    message.includes("Log response size exceeded") ||
    message.includes("query returned more than") ||
    message.includes("exceed maximum block range")
  );
}

Önemli bakış açısı, başarısızlık durumunda ikili bölmedir. 2K blokluk bir aralık çok fazla log döndürürse, iki 1K aralığa bölün. 1K hâlâ çok fazlaysa, tekrar bölün. Bu, olay yoğunluğunu önceden bilmenize gerek kalmadan yüksek aktiviteli sözleşmelere otomatik olarak uyum sağlar.

BigInt Sizi Alçakgönüllü Kılacak#

JavaScript'in Number tipi 64 bit kayan noktalı sayıdır. 2^53 - 1'e kadar tam sayıları temsil edebilir — yaklaşık 9 katrilyon. Bu, 1 ETH'nin wei cinsinden token miktarının 1000000000000000000 — 18 sıfırlı bir sayı — olduğunu fark edene kadar çok gibi gelir. Bu 10^18'dir, Number.MAX_SAFE_INTEGER'ın çok ötesinde.

Hattınızın herhangi bir yerinde — JSON.parse, veritabanı sürücüsü, loglama kütüphanesi — bir BigInt'i yanlışlıkla Number'a dönüştürürseniz, sessiz hassasiyet kaybı elde edersiniz. Sayı kabaca doğru görünür ama son birkaç rakam yanlıştır. Bunu testlerde yakalayamazsınız çünkü test miktarlarınız küçüktür.

İşte üretime gönderdiğim hata:

typescript
// HATA: Zararsız görünür, değildir
function formatTokenAmount(amount: bigint, decimals: number): string {
  return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
 
// Küçük miktarlar için sorunsuz çalışır:
formatTokenAmount(1000000n, 6); // "1.0000" -- doğru
 
// Büyük miktarlar için sessizce bozulur:
formatTokenAmount(123456789012345678n, 18);
// "0.1235" döndürür -- YANLIŞ, gerçek hassasiyet kayboldu
// Number(123456789012345678n) === 123456789012345680
// Son iki rakam IEEE 754 tarafından yuvarlandı

Çözüm: bölmeden önce asla Number'a dönüştürmeyin. viem'in dizeler ve BigInt'ler üzerinde çalışan yerleşik yardımcı fonksiyonlarını kullanın:

typescript
import { formatUnits, parseUnits } from "viem";
 
// Doğru: BigInt üzerinde çalışır, dize döndürür
function formatTokenAmount(
  amount: bigint,
  decimals: number,
  displayDecimals: number = 4
): string {
  const formatted = formatUnits(amount, decimals);
 
  // formatUnits tam hassasiyetli dize döndürür, örn. "0.123456789012345678"
  // İstenen görüntüleme hassasiyetine kırp (yuvarlama)
  const [whole, fraction = ""] = formatted.split(".");
  const truncated = fraction.slice(0, displayDecimals).padEnd(displayDecimals, "0");
 
  return `${whole}.${truncated}`;
}
 
// Ayrıca kritik: kullanıcı girişi için parseUnits kullanın, asla parseFloat değil
function parseTokenInput(input: string, decimals: number): bigint {
  // parseUnits dize-BigInt dönüşümünü doğru şekilde yönetir
  return parseUnits(input, decimals);
}

Yuvarlama yerine kırptığıma dikkat edin. Bu bilinçli bir seçimdir. Finansal bağlamlarda, gerçek değer "1.00009999..." iken "1.0001 ETH" göstermek, gerçek değer "1.00005001..." iken yukarı yuvarlanmış "1.0001" göstermekten daha iyidir. Kullanıcılar görüntülenen miktarlara göre karar verir. Kırpma muhafazakar tercihtir.

Bir tuzak daha: JSON.stringify BigInt'i nasıl serileştireceğini bilmez. Hata fırlatır. API'nizden gelen token miktarları içeren her yanıtın bir serileştirme stratejisine ihtiyacı var. API sınırında dize dönüşümü kullanıyorum:

typescript
// API yanıt serileştiricisi
function serializeForApi(data: Record<string, unknown>): string {
  return JSON.stringify(data, (_, value) =>
    typeof value === "bigint" ? value.toString() : value
  );
}

Önbellek Stratejisi: Neyi, Ne Kadar Süre ve Ne Zaman Geçersiz Kılmalı#

Tüm zincir üstü verilerin aynı tazelik gereksinimleri yoktur. İşte kullandığım hiyerarşi:

Sonsuza kadar önbellekle (değiştirilemez):

  • İşlem makbuzları (bir kez mayınlandıktan sonra değişmezler)
  • Kesinleşmiş blok verileri (blok hash'i, zaman damgası, işlem listesi)
  • Sözleşme bayt kodu
  • Kesinleşmiş bloklardan tarihsel olay logları

Dakikalar ila saatler önbellekle:

  • Token meta verileri (ad, sembol, ondalık) — teknik olarak çoğu token için değiştirilemez, ama proxy yükseltmeleri uygulamayı değiştirebilir
  • ENS çözümlemeleri — 5 dakikalık TTL iyi çalışır
  • Token fiyatları — doğruluk gereksinimlerinize bağlıdır, 30 saniye ila 5 dakika

Saniyeler veya hiç önbellekleme:

  • Mevcut blok numarası
  • Hesap bakiyeleri ve nonce
  • Bekleyen işlem durumu
  • Kesinleşmemiş olay logları (yine yeniden düzenleme sorunu)

Uygulama karmaşık olmak zorunda değil. Bellek içi LRU ve Redis ile iki katmanlı bir önbellek çoğu durumu kapsar:

typescript
import { LRUCache } from "lru-cache";
 
const memoryCache = new LRUCache<string, unknown>({
  max: 10_000,
  ttl: 1000 * 60, // Varsayılan 1 dakika
});
 
type CacheTier = "immutable" | "short" | "volatile";
 
const TTL_MAP: Record<CacheTier, number> = {
  immutable: 1000 * 60 * 60 * 24, // Bellekte 24 saat, Redis'te kalıcı
  short: 1000 * 60 * 5,            // 5 dakika
  volatile: 1000 * 15,             // 15 saniye
};
 
async function cachedRpcCall<T>(
  key: string,
  tier: CacheTier,
  fetcher: () => Promise<T>
): Promise<T> {
  // Önce belleği kontrol et
  const cached = memoryCache.get(key) as T | undefined;
  if (cached !== undefined) return cached;
 
  // Sonra Redis (varsa)
  // const redisCached = await redis.get(key);
  // if (redisCached) { ... }
 
  const result = await fetcher();
  memoryCache.set(key, result, { ttl: TTL_MAP[tier] });
 
  return result;
}
 
// Kullanım:
const receipt = await cachedRpcCall(
  `receipt:${txHash}`,
  "immutable",
  () => client.getTransactionReceipt({ hash: txHash })
);

Sezgisel olmayan ders: en büyük performans kazancı RPC yanıtlarını önbellemek değildir. RPC çağrılarından tamamen kaçınmaktır. Her getBlock çağırmak üzereyken kendinize sorun: şu anda zincirden gerçekten veriye ihtiyacım var mı, yoksa zaten sahip olduğum veriden türetebilir miyim? Yoklama yerine WebSocket aracılığıyla olayları dinleyebilir miyim? Birden fazla okumayı tek bir multicall'da toplu hale getirebilir miyim?

TypeScript ve Sözleşme ABI'leri: Doğru Yol#

Viem'in tip sistemi, ABIType tarafından desteklenir ve sözleşme ABI'nizden TypeScript kodunuza kadar uçtan uca tam tip çıkarımı sağlar. Ama yalnızca doğru kurarsanız.

Yanlış yol:

typescript
// Tip çıkarımı yok — args unknown[], dönüş unknown
const result = await client.readContract({
  address: "0x...",
  abi: JSON.parse(abiString), // çalışma zamanında ayrıştırılmış = tip bilgisi yok
  functionName: "balanceOf",
  args: ["0x..."],
});

Doğru yol:

typescript
// Tam tip çıkarımı için ABI'yi const olarak tanımlayın
const erc20Abi = [
  {
    name: "balanceOf",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ name: "balance", type: "uint256" }],
  },
  {
    name: "transfer",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    outputs: [{ name: "success", type: "bool" }],
  },
] as const;
 
// Artık TypeScript şunları bilir:
// - functionName "balanceOf" | "transfer" olarak otomatik tamamlanır
// - balanceOf için args [address: `0x${string}`]
// - balanceOf için dönüş tipi bigint
const balance = await client.readContract({
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  abi: erc20Abi,
  functionName: "balanceOf",
  args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// typeof balance = bigint -- tamamen tipli

İşi yapan as const ifadesidir. Onsuz TypeScript, ABI tipini { name: string, type: string, ... }[] olarak genişletir ve tüm çıkarım mekanizması çöker. Bu, Web3 TypeScript kod tabanlarında gördüğüm en yaygın hatadır.

Daha büyük projeler için, @wagmi/cli kullanarak doğrudan Foundry veya Hardhat projenizden tipli sözleşme bağlamları oluşturun. Derlenmiş ABI'lerinizi okur ve as const ifadeleri zaten uygulanmış TypeScript dosyaları üretir. Manuel ABI kopyalama yok, tip kayması yok.

Rahatsız Edici Gerçek#

Blok zinciri verisi, veritabanı sorunu gibi görünen bir dağıtık sistem sorunudur. Onu "sadece başka bir API" olarak ele aldığınız an, geliştirmede görünmez ve üretimde aralıklı olan hatalar biriktirmeye başlarsınız.

Araçlar çarpıcı biçimde iyileşti. Viem, tip güvenliği ve geliştirici deneyimi açısından ethers.js'ye göre büyük bir gelişmedir. Ponder ve Envio özel indekslemeyi erişilebilir kıldı. Ama temel zorluklar — yeniden düzenlemeler, hız limitleri, kodlama, kesinlik — protokol düzeyindedir. Hiçbir kütüphane bunları soyutlamaz.

RPC'nizin size yalan söyleyeceği, bloklarınızın yeniden düzenleneceği, sayılarınızın taşacağı ve önbelleğinizin eski veri sunacağı varsayımıyla inşa edin. Sonra her durumu açıkça ele alın.

Üretim kalitesinde zincir üstü veri böyle görünür.

İlgili Yazılar