On-chain-data i produktion: Det ingen berattar for dig
Blockkedjedata ar inte ren, pålitlig eller enkel. RPC-hastighetsbegränsningar, chain reorgs, BigInt-buggar och indexeringskompromisser — hårda lärdomar fran att leverera riktiga DeFi-produkter.
Det finns en fantasi om att on-chain-data ar innebordes trovardigt. Oforanderligt register. Transparent tillstand. Las det bara sa ar du klar.
Jag trodde pa det också. Sedan levererade jag en DeFi-dashboard till produktion och tillbringade tre veckor med att försöka forsta varför vara tokensaldon var felaktiga, var haendelsehistorik hade luckor och var databas inneholl transaktioner fran block som inte langre existerade.
On-chain-data ar ra, fientlig och full av edge cases som kommer att forstora din applikation pa satt du inte marker forran en anvandare rapporterar en bugg. Det har inlagget tar upp allt jag lärde mig den hårda vagen.
Illusionen av pålitlig data#
Har ar det forsta ingen berattar for dig: blockkedjan ger dig inte data. Den ger dig tillstandsovergangar. Det finns ingen SELECT * FROM transfers WHERE user = '0x...'. Det finns loggar, kvitton, lagringsplatser och call traces — allt kodat i format som kraver kontext for att avkoda.
En Transfer-handelsologg ger dig from, to och value. Den berattar inte tokensymbolen. Den berattar inte decimalerna. Den berattar inte om detta ar en legitim overfooring eller en fee-on-transfer-token som tar 3% fran toppen. Den berattar inte om detta block fortfarande kommer att existera om 30 sekunder.
Den "oforanderliga" delen stammer — nar det ar finaliserat. Men finalisering ar inte omedelbar. Och datan du far tillbaka fran en RPC-nod ar inte nodvandigtvis fran ett finaliserat block. De flesta utvecklare fragar latest och behandlar det som sanning. Det ar en bugg, inte en funktion.
Sedan ar det kodningen. Allt ar hex. Adresser ar blandade versaler med checksumma (eller inte). Tokenbelopp ar heltal multiplicerade med 10^decimals. En USDC-overfooring pa $100 ser ut som 100000000 on-chain for att USDC har 6 decimaler, inte 18. Jag har sett produktionskod som antog 18 decimaler for varje ERC-20-token. De resulterande saldona var fel med en faktor pa 10^12.
RPC-hastighetsbegränsningar forstoor din helg#
Varje produktions-Web3-app kommunicerar med en RPC-endpunkt. Och varje RPC-endpunkt har hastighetsbegränsningar som ar langt mer aggressiva an du forvatar dig.
Har ar siffrorna som spelar roll:
- Alchemy Free: ~30M compute units/manad, 40 requests/minut. Det later generost tills du inser att ett enda
eth_getLogs-anrop över ett brett blockintervall kan forbruka hundratals CU:er. Du branner igenom din manadskvot pa en dag av indexering. - Infura Free: 100K requests/dag, ungefar 1,15 req/sek. Forsok paginera genom 500K block av handelsologgar i den takten.
- QuickNode Free: Liknande Infura — 100K requests/dag.
De betalda nivderna hjalper, men de eliminerar inte problemet. Även med $200/manad pa Alchemys Growth-plan kommer ett tungt indexeringsjobb att traffa genomstromningsbegransningar. Och nar du traffar dem far du inte en gracios nedbrytning. Du far 429-fel, ibland med ohjalpsamna meddelanden, ibland utan retry-after-header.
Losningen ar en kombination av fallback-leverantorer, retry-logik och att vara mycket medveten om vilka anrop du gor. Sa har ser en robust RPC-uppsättning ut med viem:
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 }
),
});Alternativet rank: true ar kritiskt. Det talar om for viem att mata latens och framgangsfrekvens for varje transport och automatiskt foredra den snabbaste och mest palitliga. Om Alchemy börjar hastighetsbegransa dig skiftar viem trafiken till Infura. Om Infura gar ner faller den tillbaka till Ankr.
Men det finns en subtilitet: viems standard-retry-logik anvander exponentiell backoff, vilket vanligtvis ar det du vill ha. Dock finns det i borjan av 2025 ett kant problem dar retryCount inte korrekt forsaker RPC-nivafel (som 429:or) nar batch-lage ar aktiverat. Om du batchanrop, testa ditt retry-beteende uttryckligen. Lita inte pa att det fungerar.
Reorgs: Buggen du inte ser komma#
En kedjereorganisation sker nar natverket tillfalligt ar oenigt om vilket block som ar kanoniskt. Nod A ser block 1000 med transaktioner [A, B, C]. Nod B ser ett annat block 1000 med transaktioner [A, D]. Slutligen konvergerar natverket och en version vinner.
Pa proof-of-work-kedjor var detta vanligt — reorgs pa 1-3 block hande flera ganger per dag. Ethereum efter sammanslagningen ar bättre. En lyckad reorg-attack kraver nu samordning av nara 50% av validerarna. Men "bättre" ar inte "omojligt." Det var en anmarkningsvard 7-blocks reorg pa Beacon Chain i maj 2022, orsakad av inkonsekventa klientimplementationer av proposer boost-forken.
Och det spelar ingen roll hur sallsynta reorgs ar pa Ethereum mainnet. Om du bygger pa L2:er eller sidokedjor — Polygon, Arbitrum, Optimism — ar reorgs vanligare. Polygon har historiskt haft reorgs pa 10+ block.
Har ar det praktiska problemet: du indexerade block 18 000 000. Du skrev handelser till din databas. Sedan blev block 18 000 000 reorganiserat. Nu har din databas handelser fran ett block som inte existerar pa den kanoniska kedjan. Dessa handelser kan referera till transaktioner som aldrig hande. Dina anvandare ser fantomoverforingar.
Losningen beror pa din arkitektur:
Alternativ 1: Bekraftelseforsening. Indexera inte data forran N block av bekraftelser har passerat. For Ethereum mainnet ger 64 block (tva epoker) finalitetsgarantier. For L2:er, kontrollera den specifika kedjans finalitetsmodell. Det har ar enkelt men lagger till latens — ungefar 13 minuter pa Ethereum.
Alternativ 2: Reorg-detektion och aterkallning. Indexera aggressivt men sparar blockhash. For varje nytt block, verifiera att foraldrahash matchar det foregaende blocket du indexerade. Om det inte gor det har du upptackt en reorg: radera allt fran de overgivna blocken och omindexera den kanoniska kedjan.
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 };
}
// Ga baklanges for att hitta var kedjan divergerade
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); // din DB-uppslag
if (onChain.hash === inDb?.hash) {
return { reorged: true, depth };
}
depth++;
checkNumber--;
}
return { reorged: true, depth };
}Det har ar inte hypotetiskt. Jag hade ett produktionssystem dar vi indexerade handelser vid kedjespetsen utan reorg-detektion. I tre veckor fungerade det bra. Sedan orsakade en 2-blocks reorg pa Polygon en duplicerad NFT-minthhandelse i var databas. Frontenden visade att en anvandare agde en token de inte hade. Det tog tva dagar att debugga for att ingen letade efter reorgs som grundorsak.
Indexeringsproblemet: Valj din smartpunkt#
Du har tre riktiga alternativ for att fa strukturerad on-chain-data in i din applikation.
Direkta RPC-anrop#
Anropa bara getLogs, getBlock, getTransaction direkt. Det fungerar for smakalig lasning — kontrollera en anvandares saldo, hamta senaste handelserna for ett enda kontrakt. Det fungerar inte for historisk indexering eller komplexa fragor över kontrakt.
Problemet ar kombinatoriskt. Vill du ha alla Uniswap V3-swappar de senaste 30 dagarna? Det ar ~200K block. Med Alchemys grans pa 2K block per getLogs-anrop ar det minst 100 paginerade forfragan. Varje en raknas mot din hastighetsbegränsning. Och om något anrop misslyckas behover du retry-logik, cursorhantering och ett satt att ateruppta dar du slutade.
The Graph (Subgraphs)#
The Graph var den ursprungliga losningen. Definiera ett schema, skriv mappningar i AssemblyScript, deploya och fraga med GraphQL. Hosted Service fasades ut — allt ar nu pa det decentraliserade Graph Network, vilket innebar att du betalar med GRT-tokens for forfragan.
Det positiva: standardiserat, valdokumenterat, stort ekosystem av befintliga subgraphs du kan forka.
Det negativa: AssemblyScript ar smartsamt. Debugging ar begransad. Deployment tar minuter till timmar. Om din subgraph har en bugg, omdistribuerar du och vantar pa att den synkar om fran borjan. Det decentraliserade natverket lagger till latens och ibland ligger indexerare efter kedjespetsen.
Jag har anvant The Graph for lastunga dashboards dar dataaktualitet pa 30-60 sekunder ar acceptabel. Det fungerar bra dar. Jag skulle inte anvanda det for något som kraver realtidsdata eller komplex affarslogik i mappningarna.
Anpassade indexerare (Ponder, Envio)#
Det ar har ekosystemet har mognat avsevert. Ponder och Envio later dig skriva indexeringslogik i TypeScript (inte AssemblyScript), kora lokalt under utveckling och deploya som fristaende tjänster.
Ponder ger dig maximal kontroll. Du definierar handelsehanterare i TypeScript, den hanterar indexeringspipelinen och du far en SQL-databas som utdata. Kompromissen: du ager infrastrukturen. Skalning, övervakning, reorg-hantering — det ar pa dig.
Envio optimerar for synkhastighet. Deras benchmarks visar avsevart snabbare initiala synktider jamfort med The Graph. De hanterar reorgs inbyggt och stodjer HyperSync, ett specialiserat protokoll for snabbare datahamtning. Kompromissen: du koper in dig i deras infrastruktur och API.
Min rekommendation: om du bygger en produktions-DeFi-app och du har ingenjorskapacitet, anvand Ponder. Om du behover snabbast mojliga synk och inte vill hantera infrastruktur, utvardera Envio. Om du behover en snabb prototyp eller vill ha communityunderhallna subgraphs ar The Graph fortfarande bra.
getLogs ar farligare an det ser ut#
RPC-metoden eth_getLogs ar bedrägligt enkel. Ge den ett blockintervall och några filter, fa tillbaka matchande handelseloggar. Har ar vad som faktiskt hander i produktion:
Blockintervallgrenser varierar per leverantor. Alchemy begransar till 2K block (obegransade loggar) eller obegransade block (max 10K loggar). Infura har andra granser. QuickNode har andra granser. En publik RPC kan begransa till 1K block. Din kod maste hantera alla dessa.
Svarsstorleksgransen finns. Även inom blockintervallet, om ett populart kontrakt emitterar tusentals handelser per block, kan ditt svar överskrida leverantorens payloadgrans (150MB pa Alchemy). Anropet returnerar inte partiella resultat. Det misslyckas.
Tomma intervall ar inte gratis. Även om det finns noll matchande loggar skannar leverantoren fortfarande blockintervallet. Detta raknas mot dina compute units.
Har ar ett pagineringsverktyg som hanterar dessa begränsningar:
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) {
// Om intervallet ar for stort (for manga resultat), dela det pa halften
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")
);
}Den viktiga insikten ar den binara uppdelningen vid misslyckande. Om ett 2K-blockintervall returnerar for många loggar, dela det i tva 1K-intervall. Om 1K fortfarande ar for mycket, dela igen. Detta anpassar sig automatiskt till hogaktiva kontrakt utan att du behover kanna till handelsetateheten i forvag.
BigInt kommer att odmiuka dig#
JavaScripts Number-typ ar en 64-bitars float. Den kan representera heltal upp till 2^53 - 1 — ungefar 9 biljarder. Det later som mycket tills du inser att ett tokenbelopp pa 1 ETH i wei ar 1000000000000000000 — ett tal med 18 nollor. Det ar 10^18, langt bortom Number.MAX_SAFE_INTEGER.
Om du av misstag konverterar en BigInt till en Number någon stans i din pipeline — JSON.parse, en databasdrivrutin, ett loggningsbibliotek — far du tyst precisionsforlust. Talet ser ungefar ratt ut men de sista siffrorna ar fel. Du fanger inte detta i testning for att dina testbelopp ar sma.
Har ar buggen jag levererade till produktion:
// BUGGEN: Ser harmlost ut, ar det inte
function formatTokenAmount(amount: bigint, decimals: number): string {
return (Number(amount) / Math.pow(10, decimals)).toFixed(4);
}
// For sma belopp fungerar det bra:
formatTokenAmount(1000000n, 6); // "1.0000" -- korrekt
// For stora belopp gar det sonder tyst:
formatTokenAmount(123456789012345678n, 18);
// Returnerar "0.1235" -- FEL, verklig precision ar forlorad
// Number(123456789012345678n) === 123456789012345680
// De tva sista siffrorna avrundades av IEEE 754Losningen: konvertera aldrig till Number fore division. Anvand viems inbyggda verktyg som opererar pa strangar och BigInts:
import { formatUnits, parseUnits } from "viem";
// Korrekt: opererar pa BigInt, returnerar strang
function formatTokenAmount(
amount: bigint,
decimals: number,
displayDecimals: number = 4
): string {
const formatted = formatUnits(amount, decimals);
// formatUnits returnerar full precisionsstrang som "0.123456789012345678"
// Trunkera (avrunda inte) till onskat antal visade decimaler
const [whole, fraction = ""] = formatted.split(".");
const truncated = fraction.slice(0, displayDecimals).padEnd(displayDecimals, "0");
return `${whole}.${truncated}`;
}
// Ocksa kritiskt: anvand parseUnits for anvandarinmatning, aldrig parseFloat
function parseTokenInput(input: string, decimals: number): bigint {
// parseUnits hanterar strang-till-BigInt-konverteringen korrekt
return parseUnits(input, decimals);
}Observera att jag trunkerar istallet for att avrunda. Det ar medvetet. I finansiella sammanhang ar det bättre att visa "1.0001 ETH" nar det verkliga vardet ar "1.00009999..." an att visa "1.0001" nar det verkliga vardet ar "1.00005001..." och avrundades updat. Anvandare fattar beslut baserat pa visade belopp. Trunkering ar det konservativa valet.
En annan falla: JSON.stringify vet inte hur man serialiserar BigInt. Den kastar ett fel. Varje enskilt svar fran ditt API som inkluderar tokenbelopp behover en serialiseringsstrategi. Jag anvander strangkonvertering vid API-gransen:
// API-svarsserialiserare
function serializeForApi(data: Record<string, unknown>): string {
return JSON.stringify(data, (_, value) =>
typeof value === "bigint" ? value.toString() : value
);
}Cachningsstrategi: Vad, hur lange och nar man invaliderar#
Inte all on-chain-data har samma aktualitetskrav. Har ar hierarkin jag anvander:
Cacha for evigt (oforanderlig):
- Transaktionskvitton (nar de ar utvunna ändras de inte)
- Finaliserad blockdata (blockhash, tidsstampel, transaktionslista)
- Kontraktsbytekod
- Historiska handelseloggar fran finaliserade block
Cacha i minuter till timmar:
- Tokenmetadata (namn, symbol, decimaler) — tekniskt sett oforanderlig for de flesta tokens, men proxyuppgraderingar kan andra implementationen
- ENS-upplosningar — 5 minuters TTL fungerar bra
- Tokenpriser — beror pa dina noggrannhetskrav, 30 sekunder till 5 minuter
Cacha i sekunder eller inte alls:
- Aktuellt blocknummer
- Kontosaldon och nonce
- Vantande transaktionsstatus
- Ofinaliserade handelseloggar (reorg-problemet igen)
Implementationen behover inte vara komplex. En tvanivacache med in-memory LRU och Redis täcker de flesta fall:
import { LRUCache } from "lru-cache";
const memoryCache = new LRUCache<string, unknown>({
max: 10_000,
ttl: 1000 * 60, // 1 minut standard
});
type CacheTier = "immutable" | "short" | "volatile";
const TTL_MAP: Record<CacheTier, number> = {
immutable: 1000 * 60 * 60 * 24, // 24 timmar i minnet, permanent i Redis
short: 1000 * 60 * 5, // 5 minuter
volatile: 1000 * 15, // 15 sekunder
};
async function cachedRpcCall<T>(
key: string,
tier: CacheTier,
fetcher: () => Promise<T>
): Promise<T> {
// Kontrollera minnet forst
const cached = memoryCache.get(key) as T | undefined;
if (cached !== undefined) return cached;
// Sedan Redis (om du har det)
// const redisCached = await redis.get(key);
// if (redisCached) { ... }
const result = await fetcher();
memoryCache.set(key, result, { ttl: TTL_MAP[tier] });
return result;
}
// Anvandning:
const receipt = await cachedRpcCall(
`receipt:${txHash}`,
"immutable",
() => client.getTransactionReceipt({ hash: txHash })
);Den kontraintuitiva lardomen: den storsta prestandavinsten ar inte att cacha RPC-svar. Det ar att undvika RPC-anrop helt. Varje gang du ar pa vag att anropa getBlock, fraga dig själv: behover jag verkligen data fran kedjan just nu, eller kan jag harleda det fran data jag redan har? Kan jag lyssna pa handelser via WebSocket istallet for att polla? Kan jag batcha flera lasningar till ett enda multicall?
TypeScript och kontrakts-ABI:er: Ratt satt#
Viems typsystem, drivet av ABIType, ger fullstandig end-to-end-typinferens fran ditt kontrakts-ABI till din TypeScript-kod. Men bara om du ställer in det korrekt.
Fel satt:
// Ingen typinferens — args ar unknown[], retur ar unknown
const result = await client.readContract({
address: "0x...",
abi: JSON.parse(abiString), // parsad vid korning = ingen typinfo
functionName: "balanceOf",
args: ["0x..."],
});Ratt satt:
// Definiera ABI som const for fullstandig typinferens
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;
// Nu vet TypeScript:
// - functionName autokompletterar till "balanceOf" | "transfer"
// - args for balanceOf ar [address: `0x${string}`]
// - returtyp for balanceOf ar bigint
const balance = await client.readContract({
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
abi: erc20Abi,
functionName: "balanceOf",
args: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
});
// typeof balance = bigint -- fullt typadSatsen as const ar det som far det att fungera. Utan den breddar TypeScript ABI-typen till { name: string, type: string, ... }[] och all inferensmaskineri kollapsar. Det har ar det enskilt vanligaste misstaget jag ser i Web3 TypeScript-kodbaser.
For större projekt, anvand @wagmi/cli for att generera typade kontraktsbindningar direkt fran ditt Foundry- eller Hardhat-projekt. Den laser dina kompilerade ABI:er och producerar TypeScript-filer med as const-satser redan applicerade. Ingen manuell ABI-kopiering, ingen typdrift.
Den obekvama sanningen#
Blockkedjedata ar ett distribuerat systemproblem forklagt som ett databasproblem. I samma ogonblick du behandlar det som "bara ytterligare ett API" börjar du ackumulera buggar som ar osynliga under utveckling och intermittenta i produktion.
Verktygen har blivit dramatiskt bättre. Viem ar en massiv förbättring jamfort med ethers.js for typsaker och utvecklarupplevelse. Ponder och Envio har gjort anpassad indexering tillgänglig. Men de fundamentala utmaningarna — reorgs, hastighetsbegränsningar, kodning, finalitet — ar pa protokollniva. Inget bibliotek abstraherar bort dem.
Bygg med antagandet att din RPC kommer att ljuga for dig, dina block kommer att reorganiseras, dina tal kommer att svamna över och din cache kommer att servera inaktuell data. Hantera sedan varje fall uttryckligen.
Det ar sa produktionsredo on-chain-data ser ut.