Ethereum pro webové vývojáře: Smart kontrakty bez hype
Koncepty Etherea, které každý webový vývojář potřebuje: účty, transakce, smart kontrakty, ABI kódování, ethers.js, WAGMI a čtení on-chain dat bez vlastního nodu.
Většina obsahu „Ethereum pro vývojáře" spadá do dvou kategorií: přehnaně zjednodušené analogie, které vám nepomohou nic postavit, nebo hluboké specifikace protokolu, které předpokládají, že už víte, co je Merkle Patricia Trie. Ani jedno není užitečné, pokud jste webový vývojář, který chce přečíst zůstatek tokenů, nechat uživatele podepsat transakci nebo zobrazit NFT metadata v React aplikaci.
Tento článek je praktický střed. Přesně vysvětlím, co se děje, když váš frontend komunikuje s Ethereem, jaké jsou pohyblivé části a jak moderní nástroje (ethers.js, viem, WAGMI) mapují koncepty, které už znáte ze stavění webových aplikací.
Žádné metafory o automatech. Žádné „představte si svět, kde..." Pouze technický model a kód.
Mentální model#
Ethereum je replikovaný stavový automat. Každý uzel v síti udržuje identickou kopii stavu — obrovský key-value store, který mapuje adresy na data účtů. Když „odešlete transakci", navrhujete přechod stavu. Pokud dostatečný počet validátorů souhlasí, že je platná, stav se aktualizuje. To je vše.
Samotný stav je přímočarý. Je to mapování z 20bajtových adres na objekty účtů. Každý účet má čtyři pole:
- nonce: Kolik transakcí tento účet odeslal (pro EOA) nebo kolik kontraktů vytvořil (pro kontraktní účty). Toto zabraňuje replay útokům.
- balance: Množství ETH, vyjádřené ve wei (1 ETH = 10^18 wei). Vždy velké celé číslo.
- codeHash: Hash EVM bytekódu. Pro běžné peněženky (EOA) je to hash prázdných bajtů. Pro kontrakty je to hash nasazeného kódu.
- storageRoot: Kořenový hash storage trie účtu. Pouze kontrakty mají smysluplné úložiště.
Existují dva typy účtů a tento rozdíl je důležitý pro vše, co následuje:
Externally Owned Accounts (EOA) jsou řízeny privátním klíčem. To je to, co spravuje MetaMask. Mohou iniciovat transakce. Nemají kód. Když někdo řekne „peněženka", myslí EOA.
Contract Accounts jsou řízeny svým kódem. Nemohou iniciovat transakce — mohou se pouze spustit jako reakce na zavolání. Mají kód a úložiště. Když někdo řekne „smart kontrakt", myslí toto. Kód je po nasazení neměnný (s některými výjimkami přes proxy patterny, což je úplně jiné téma).
Klíčový poznatek: každá změna stavu na Ethereu začíná podepsáním transakce z EOA. Kontrakty mohou volat jiné kontrakty, ale řetězec vykonávání vždy začíná člověkem (nebo botem) s privátním klíčem.
Gas: Výpočet má svou cenu#
Každá operace v EVM stojí gas. Sečtení dvou čísel stojí 3 gas. Uložení 32bajtového slova stojí 20 000 gas (poprvé) nebo 5 000 gas (aktualizace). Čtení úložiště stojí 2 100 gas (cold) nebo 100 gas (warm, již přístupné v této transakci).
Gas neplatíte v „gas jednotkách". Platíte v ETH. Celkové náklady jsou:
totalCost = gasUsed * gasPrice
Po EIP-1559 (London upgrade) se ceny gasu staly dvousložkovým systémem:
totalCost = gasUsed * (baseFee + priorityFee)
- baseFee: Nastavuje protokol podle vytížení sítě. Spaluje se (ničí).
- priorityFee (tip): Jde validátorovi. Vyšší tip = rychlejší začlenění.
- maxFeePerGas: Maximum, které jste ochotni zaplatit za jednotku gasu.
- maxPriorityFeePerGas: Maximální tip za jednotku gasu.
Pokud baseFee + priorityFee > maxFeePerGas, vaše transakce čeká, dokud baseFee neklesne. Proto transakce „uvíznou" při vysokém vytížení.
Praktický důsledek pro webové vývojáře: čtení dat je zdarma. Zápis dat stojí peníze. Toto je nejdůležitější architekturální rozdíl mezi Web2 a Web3. Každý SELECT je zdarma. Každý INSERT, UPDATE, DELETE stojí reálné peníze. Navrhujte své dApps podle toho.
Transakce#
Transakce je podepsaná datová struktura. Zde jsou pole, na kterých záleží:
interface Transaction {
// Kdo přijímá tuto transakci — EOA adresa nebo adresa kontraktu
to: string; // 20bajtová hex adresa, nebo null pro nasazení kontraktu
// Kolik ETH poslat (ve wei)
value: bigint; // Může být 0n pro čistá volání kontraktů
// Zakódovaná data volání funkce, nebo prázdné pro prosté ETH převody
data: string; // Hex-kódované bajty, "0x" pro jednoduché převody
// Sekvenční počítadlo, zabraňuje replay útokům
nonce: number; // Musí přesně odpovídat aktuálnímu nonce odesílatele
// Gas limit — maximum gasu, které tato tx může spotřebovat
gasLimit: bigint;
// EIP-1559 parametry poplatků
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
// Identifikátor řetězce (1 = mainnet, 11155111 = Sepolia, 137 = Polygon)
chainId: number;
}Životní cyklus transakce#
-
Konstrukce: Vaše aplikace vytvoří objekt transakce. Pokud voláte funkci kontraktu, pole
dataobsahuje ABI-kódované volání funkce (více o tom níže). -
Podepsání: Privátní klíč podepíše RLP-kódovanou transakci a vytvoří
v,r,skomponenty podpisu. Toto dokazuje, že odesílatel autorizoval tuto konkrétní transakci. Adresa odesílatele je odvozena z podpisu — není explicitně v transakci. -
Odeslání: Podepsaná transakce se odešle do RPC uzlu přes
eth_sendRawTransaction. Uzel ji ověří (správný nonce, dostatečný zůstatek, platný podpis) a přidá ji do svého mempoolu. -
Mempool: Transakce čeká v poolu čekajících transakcí. Validátoři vybírají transakce k začlenění do dalšího bloku, obecně preferují vyšší tipy. Právě zde dochází k front-runningu — jiní aktéři mohou vidět vaši čekající transakci a odeslat svou vlastní s vyšším tipem, aby se provedla před vaší.
-
Začlenění: Validátor zahrne vaši transakci do bloku. EVM ji provede. Pokud uspěje, změny stavu se aplikují. Pokud se vrátí, změny stavu se odrolují — ale stále platíte za gas spotřebovaný do bodu revertu.
-
Finalita: Na proof-of-stake Ethereu se blok stane „finalizovaným" po dvou epochách (~12,8 minut). Před finalitou jsou reorganizace řetězce teoreticky možné (i když vzácné). Většina aplikací považuje 1-2 potvrzení bloků za „dostatečné" pro nekritické operace.
Takto vypadá odeslání jednoduchého ETH převodu s ethers.js v6:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const wallet = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider);
const tx = await wallet.sendTransaction({
to: "0xRecipientAddress...",
value: ethers.parseEther("0.1"), // Převede "0.1" na wei (100000000000000000n)
});
console.log("Tx hash:", tx.hash);
// Čekání na začlenění do bloku
const receipt = await tx.wait();
console.log("Block number:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Status:", receipt.status); // 1 = úspěch, 0 = revertA totéž s viem:
import { createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const client = createWalletClient({
account,
chain: mainnet,
transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});
const hash = await client.sendTransaction({
to: "0xRecipientAddress...",
value: parseEther("0.1"),
});
console.log("Tx hash:", hash);Všimněte si rozdílu: ethers vrací objekt TransactionResponse s metodou .wait(). Viem vrací pouze hash — použijete samostatné volání publicClient.waitForTransactionReceipt({ hash }) pro čekání na potvrzení. Toto oddělení zodpovědností je záměrné v designu viem.
Smart kontrakty#
Smart kontrakt je nasazený bytekód plus persistentní úložiště na konkrétní adrese. Když „zavoláte" kontrakt, posíláte transakci (nebo provádíte read-only volání) s polem data nastaveným na zakódované volání funkce.
Bytekód a ABI#
Bytekód je zkompilovaný EVM kód. Nepracujete s ním přímo. Je to to, co EVM spouští.
ABI (Application Binary Interface) je JSON popis rozhraní kontraktu. Říká vaší klientské knihovně, jak kódovat volání funkcí a dekódovat návratové hodnoty. Představte si to jako OpenAPI specifikaci pro kontrakt.
Zde je fragment ABI ERC-20 tokenu:
const ERC20_ABI = [
// Read-only funkce (view/pure — bez nákladů na gas při externím volání)
"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)",
// Funkce měnící stav (vyžadují transakci, stojí gas)
"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)",
// Události (emitované během vykonávání, uložené v logách transakcí)
"event Transfer(address indexed from, address indexed to, uint256 value)",
"event Approval(address indexed owner, address indexed spender, uint256 value)",
] as const;Ethers.js přijímá tento „human-readable ABI" formát. Viem ho také může používat, ale často budete pracovat s plným JSON ABI generovaným kompilátorem Solidity. Obě varianty jsou ekvivalentní — human-readable formát je jen pohodlnější pro běžná rozhraní.
Jak se kódují volání funkcí#
Toto je ta část, kterou většina tutoriálů přeskakuje, a je to ta část, která vám ušetří hodiny debugování.
Když zavoláte transfer("0xBob...", 1000000), pole data transakce se nastaví na:
0xa9059cbb // Function selector
0000000000000000000000000xBob...000000000000000000000000 // address, doplněná na 32 bajtů
00000000000000000000000000000000000000000000000000000000000f4240 // uint256 amount (1000000 v hex)
Function selector jsou první 4 bajty Keccak-256 hashe signatury funkce:
keccak256("transfer(address,uint256)") = 0xa9059cbb...
selector = první 4 bajty = 0xa9059cbb
Zbývající bajty jsou ABI-kódované argumenty, každý doplněný na 32 bajtů. Toto kódovací schéma je deterministické — stejné volání funkce vždy produkuje stejná calldata.
Proč je to důležité? Protože když vidíte surová transakční data na Etherscanu a začínají 0xa9059cbb, víte, že je to volání transfer. Když vaše transakce revertuje a chybová zpráva je jen hex blob, můžete ji dekódovat pomocí ABI. A když stavíte dávky transakcí nebo interagujete s multicall kontrakty, budete kódovat calldata ručně.
Takto ručně kódujete a dekódujete s ethers.js:
import { ethers } from "ethers";
const iface = new ethers.Interface(ERC20_ABI);
// Zakódování volání funkce
const calldata = iface.encodeFunctionData("transfer", [
"0xBobAddress...",
1000000n,
]);
console.log(calldata);
// 0xa9059cbb000000000000000000000000bob...000000000000000000000000000f4240
// Dekódování calldat zpět na název funkce a argumenty
const decoded = iface.parseTransaction({ data: calldata });
console.log(decoded.name); // "transfer"
console.log(decoded.args[0]); // "0xBobAddress..."
console.log(decoded.args[1]); // 1000000n (BigInt)
// Dekódování návratových dat funkce
const returnData = "0x0000000000000000000000000000000000000000000000000000000000000001";
const result = iface.decodeFunctionResult("transfer", returnData);
console.log(result[0]); // trueStorage sloty#
Úložiště kontraktu je key-value store, kde klíče i hodnoty jsou 32 bajtů. Solidity přiřazuje storage sloty sekvenčně počínaje od 0. První deklarovaná stavová proměnná jde do slotu 0, další do slotu 1 a tak dále. Mapping a dynamická pole používají schéma založené na hashování.
Můžete číst úložiště libovolného kontraktu přímo, i když je proměnná označena jako private v Solidity. „Private" pouze znamená, že jiné kontrakty ji nemohou číst — kdokoli ji může přečíst přes eth_getStorageAt:
// Čtení storage slotu 0 kontraktu
const slot0 = await provider.getStorage(
"0xContractAddress...",
0
);
console.log(slot0); // Surová 32bajtová hex hodnotaTakto prohlížeče bloků zobrazují „interní" stav kontraktu. Na čtení úložiště neexistuje žádné řízení přístupu. Soukromí na veřejném blockchainu je ze své podstaty omezené.
Události a logy#
Události jsou způsob, jakým kontrakt emituje strukturovaná data, která se ukládají do logů transakcí, ale ne do úložiště kontraktu. Jsou levnější než zápisy do úložiště (375 gas pro první topic + 8 gas za bajt dat, oproti 20 000 gas za zápis do úložiště) a jsou navrženy pro efektivní dotazování.
Událost může mít až 3 indexed parametry (uložené jako „topics") a libovolný počet neindexovaných parametrů (uložených jako „data"). Na indexované parametry lze filtrovat — můžete se zeptat „dej mi všechny Transfer události, kde to je tato adresa." Neindexované parametry filtrovat nelze; musíte načíst všechny odpovídající události a filtrovat na straně klienta.
// Naslouchání Transfer událostem v reálném čase s ethers.js
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
contract.on("Transfer", (from, to, value, event) => {
console.log(`${from} -> ${to}: ${ethers.formatUnits(value, 18)} tokens`);
console.log("Block:", event.log.blockNumber);
console.log("Tx hash:", event.log.transactionHash);
});
// Dotazování historických událostí
const filter = contract.filters.Transfer(null, "0xMyAddress..."); // from=jakýkoli, to=konkrétní
const events = await contract.queryFilter(filter, 19000000, 19100000); // rozsah bloků
for (const event of events) {
console.log("From:", event.args.from);
console.log("Value:", event.args.value.toString());
}Totéž s viem:
import { createPublicClient, http, parseAbiItem } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});
// Historické logy
const logs = await client.getLogs({
address: "0xTokenAddress...",
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
args: {
to: "0xMyAddress...",
},
fromBlock: 19000000n,
toBlock: 19100000n,
});
for (const log of logs) {
console.log("From:", log.args.from);
console.log("To:", log.args.to);
console.log("Value:", log.args.value);
}
// Sledování v reálném čase
const unwatch = client.watchEvent({
address: "0xTokenAddress...",
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
onLogs: (logs) => {
for (const log of logs) {
console.log(`Transfer: ${log.args.from} -> ${log.args.to}`);
}
},
});
// Voláním unwatch() ukončíte nasloucháníČtení on-chain dat#
Tady se Ethereum stává praktickým pro webové vývojáře. Nemusíte provozovat uzel. Nemusíte těžit. Nepotřebujete ani peněženku. Čtení dat z Etherea je zdarma, bez oprávnění a funguje přes jednoduché JSON-RPC API.
JSON-RPC: HTTP API Etherea#
Každý uzel Etherea vystavuje JSON-RPC API. Je to doslova HTTP POST s JSON těly. Na transportní vrstvě není nic specifického pro blockchain.
// Toto je to, co vaše knihovna dělá pod kapotou
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: "eth_call",
params: [
{
to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
data: "0x70a08231000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
// balanceOf(vitalik.eth)
},
"latest",
],
}),
});
const result = await response.json();
console.log(result);
// { jsonrpc: "2.0", id: 1, result: "0x000000000000000000000000000000000000000000000000000000174876e800" }To je surový eth_call. Simuluje vykonání transakce bez jejího skutečného odeslání. Bez nákladů na gas. Bez změny stavu. Pouze přečte návratovou hodnotu. Takto fungují view a pure funkce zvenčí — používají eth_call místo eth_sendRawTransaction.
Dvě klíčové RPC metody#
eth_call: Simuluje vykonání. Zdarma. Bez změny stavu. Používá se pro všechny operace čtení — kontrola zůstatků, čtení cen, volání view funkcí. Lze volat na libovolném historickém bloku zadáním čísla bloku místo „latest".
eth_sendRawTransaction: Odešle podepsanou transakci k začlenění do bloku. Stojí gas. Mění stav (při úspěchu). Používá se pro všechny operace zápisu — převody, schválení, swapy, minty.
Vše ostatní v JSON-RPC API je buď varianta těchto dvou, nebo pomocná metoda (eth_blockNumber, eth_getTransactionReceipt, eth_getLogs atd.).
Provideři: Vaše brána k řetězci#
Neprovozujete vlastní uzel. Téměř nikdo to pro vývoj aplikací nedělá. Místo toho používáte službu providera:
- Alchemy: Nejpopulárnější. Skvělý dashboard, podpora webhooků, rozšířená API pro NFT a metadata tokenů. Bezplatná úroveň: ~300M výpočetních jednotek/měsíc.
- Infura: Originál. Vlastněný ConsenSys. Spolehlivý. Bezplatná úroveň: 100K požadavků/den.
- QuickNode: Dobrý pro multi-chain. Mírně odlišný cenový model.
- Veřejné RPC endpointy:
https://rpc.ankr.com/eth,https://cloudflare-eth.com. Zdarma, ale s omezením rychlosti a občas nespolehlivé. Pro vývoj v pořádku, pro produkci nebezpečné. - Tenderly: Výborný pro simulaci a debugování. Jejich RPC zahrnuje vestavěný simulátor transakcí.
Pro produkci vždy nakonfigurujte alespoň dva providery jako fallbacky. Výpadky RPC jsou reálné a stanou se v nejhorší možný čas.
import { ethers } from "ethers";
// ethers.js v6 fallback provider
const provider = new ethers.FallbackProvider([
{
provider: new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/KEY1"),
priority: 1,
stallTimeout: 2000,
weight: 2,
},
{
provider: new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/KEY2"),
priority: 2,
stallTimeout: 2000,
weight: 1,
},
]);Čtení stavu kontraktu zdarma#
Toto je silný tah, o kterém si většina Web2 vývojářů neuvědomuje: můžete číst jakákoli veřejná data z jakéhokoli kontraktu na Ethereu bez placení čehokoli, bez peněženky a bez jakékoli autentifikace kromě API klíče pro vašeho RPC providera.
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
// ERC-20 rozhraní — pouze read funkce
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 // Poznámka: provider, ne signer. Pouze pro čtení.
);
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 (NE 18!)
console.log(`Total supply: ${ethers.formatUnits(totalSupply, decimals)}`);
// Kontrola zůstatku konkrétní adresy
const balance = await erc20.balanceOf("0xSomeAddress...");
console.log(`Balance: ${ethers.formatUnits(balance, decimals)} USDC`);Bez peněženky. Bez gasu. Bez transakce. Pod kapotou je to jen JSON-RPC eth_call. Je to konceptuálně identické s vytvořením GET požadavku na REST API. Blockchain je databáze, kontrakt je API a eth_call je váš SELECT dotaz.
ethers.js v6#
ethers.js je jQuery Web3 — byla to první knihovna, kterou se většina vývojářů naučila, a stále je nejrozšířenější. Verze 6 je výrazné zlepšení oproti v5, s nativní podporou BigInt (konečně), ESM moduly a čistším API.
Tři hlavní abstrakce#
Provider: Read-only připojení k blockchainu. Může volat view funkce, načítat bloky, číst logy. Nemůže podepisovat ani odesílat transakce.
import { ethers } from "ethers";
// Připojení k uzlu
const provider = new ethers.JsonRpcProvider("https://...");
// Základní dotazy
const blockNumber = await provider.getBlockNumber();
const balance = await provider.getBalance("0xAddress...");
const block = await provider.getBlock(blockNumber);
const txCount = await provider.getTransactionCount("0xAddress...");Signer: Abstrakce nad privátním klíčem. Může podepisovat transakce a zprávy. Signer je vždy připojen k Provideru.
// Z privátního klíče (server-side, skripty)
const wallet = new ethers.Wallet("0xPrivateKey...", provider);
// Z prohlížečové peněženky (client-side)
const browserProvider = new ethers.BrowserProvider(window.ethereum);
const signer = await browserProvider.getSigner();
// Získání adresy
const address = await signer.getAddress();Contract: JavaScript proxy pro nasazený kontrakt. Metody na objektu Contract odpovídají funkcím v ABI. View funkce vracejí hodnoty. Funkce měnící stav vracejí TransactionResponse.
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);
// Čtení (zdarma, vrací hodnotu přímo)
const balance = await usdc.balanceOf("0xSomeAddress...");
// balance je bigint: 1000000000n (1000 USDC se 6 desetinnými místy)
// Pro zápis připojte se signerem
const usdcWithSigner = usdc.connect(signer);
// Zápis (stojí gas, vrací TransactionResponse)
const tx = await usdcWithSigner.transfer("0xRecipient...", 1000000n);
const receipt = await tx.wait(); // Čekání na začlenění do bloku
if (receipt.status === 0) {
throw new Error("Transaction reverted");
}TypeChain pro typovou bezpečnost#
Surové ABI interakce jsou řetězcově typované. Můžete překlepnout název funkce, předat špatné typy argumentů nebo špatně interpretovat návratové hodnoty. TypeChain generuje TypeScript typy z vašich ABI souborů:
// Bez TypeChain — žádná kontrola typů
const balance = await contract.balanceOf("0x...");
// balance je 'any'. Žádné autocomplete. Snadno se zneužije.
// S TypeChain — plná typová bezpečnost
import { USDC__factory } from "./typechain";
const usdc = USDC__factory.connect(USDC_ADDRESS, provider);
const balance = await usdc.balanceOf("0x...");
// balance je BigNumber. Autocomplete funguje. Typové chyby zachyceny při kompilaci.Pro nové projekty zvažte použití vestavěné typové inference viem z ABI. Dosahuje stejného výsledku bez samostatného kroku generování kódu.
Naslouchání událostem#
Streamování událostí v reálném čase je klíčové pro responzivní dApps. ethers.js k tomu používá WebSocket providery:
// WebSocket pro události v reálném čase
const wsProvider = new ethers.WebSocketProvider("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const contract = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, wsProvider);
// Naslouchání všem Transfer událostem
contract.on("Transfer", (from, to, value, event) => {
console.log(`Transfer: ${from} -> ${to}`);
console.log(`Amount: ${ethers.formatUnits(value, 6)} USDC`);
});
// Naslouchání převodům NA konkrétní adresu
const filter = contract.filters.Transfer(null, "0xMyAddress...");
contract.on(filter, (from, to, value) => {
console.log(`Incoming transfer: ${ethers.formatUnits(value, 6)} USDC from ${from}`);
});
// Úklid po dokončení
contract.removeAllListeners();WAGMI + Viem: Moderní stack#
WAGMI (We're All Gonna Make It) je knihovna React hooků pro Ethereum. Viem je základní TypeScript klient, který používá. Společně do značné míry nahradili ethers.js + web3-react jako standardní stack pro frontendový vývoj dApps.
Proč ten posun? Tři důvody: plná TypeScript inference z ABI (bez potřeby generování kódu), menší velikost bundlu a React hooky, které řeší chaotickou asynchronní správu stavu peněženkových interakcí.
Nastavení#
// 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"),
},
});// 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>
);
}Čtení dat kontraktu#
useReadContract je hook, který budete používat nejčastěji. Obaluje eth_call s cachováním React Query, refetchem a stavy loading/error:
"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>Načítání...</span>;
if (error) return <span>Chyba: {error.message}</span>;
// balance je typováno jako bigint, protože ABI říká uint256
return <span>{formatUnits(balance ?? 0n, 6)} USDC</span>;
}Všimněte si as const na ABI. Toto je zásadní. Bez toho TypeScript ztratí literální typy a balance se stane unknown místo bigint. Celý systém typové inference závisí na const assertions.
Zápis do kontraktů#
useWriteContract řeší celý životní cyklus: výzvu peněženky, podepsání, odeslání a sledování potvrzení.
"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 ? "Potvrďte v peněžence..." : "Odeslat 100 USDC"}
</button>
{hash && <p>Transakce: {hash}</p>}
{isConfirming && <p>Čekání na potvrzení...</p>}
{isSuccess && <p>Převod potvrzen!</p>}
{error && <p>Chyba: {error.message}</p>}
</div>
);
}Sledování událostí#
useWatchContractEvent nastavuje WebSocket předplatné pro sledování událostí v reálném čase:
"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>
);
}Připojení peněženky#
Připojení uživatelovy peněženky je „přihlášení" Web3. Jenže to není přihlášení. Neexistuje žádná session, žádný cookie, žádný stav na straně serveru. Připojení peněženky dává vaší aplikaci oprávnění číst adresu uživatele a požadovat podpisy transakcí. To je vše.
Rozhraní providera EIP-1193#
Každá peněženka vystavuje standardní rozhraní definované EIP-1193. Je to objekt s metodou request:
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 to injektuje jako window.ethereum. Jiné peněženky buď injektují vlastní vlastnost, nebo také používají window.ethereum (což způsobuje konflikty — problém „peněženkových válek", částečně vyřešený EIP-6963).
// Nízkoúrovňová interakce s peněženkou (neměli byste to dělat přímo, ale je užitečné tomu rozumět)
// Žádost o přístup k účtu
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected address:", accounts[0]);
// Získání aktuálního řetězce
const chainId = await window.ethereum.request({
method: "eth_chainId",
});
console.log("Chain ID:", parseInt(chainId, 16)); // "0x1" -> 1 (mainnet)
// Naslouchání změnám účtu (uživatel přepne účty v MetaMasku)
window.ethereum.on("accountsChanged", (accounts: string[]) => {
if (accounts.length === 0) {
console.log("Wallet disconnected");
} else {
console.log("Switched to:", accounts[0]);
}
});
// Naslouchání změnám řetězce (uživatel přepne sítě)
window.ethereum.on("chainChanged", (chainId: string) => {
// Doporučený přístup je znovu načíst stránku
window.location.reload();
});EIP-6963: Objevování více peněženek#
Starý přístup window.ethereum selhává, když mají uživatelé nainstalovaných více peněženek. Která dostane window.ethereum? Poslední injektovaná? První? Je to race condition.
EIP-6963 to opravuje pomocí protokolu objevování založeného na prohlížečových událostech:
// Objevování všech dostupných peněženek
interface EIP6963ProviderDetail {
info: {
uuid: string;
name: string;
icon: string;
rdns: string; // Reverzní doménové jméno, např. "io.metamask"
};
provider: EIP1193Provider;
}
const wallets: EIP6963ProviderDetail[] = [];
window.addEventListener("eip6963:announceProvider", (event: CustomEvent) => {
wallets.push(event.detail);
});
// Požádat všechny peněženky, aby se představily
window.dispatchEvent(new Event("eip6963:requestProvider"));
// Nyní 'wallets' obsahuje všechny nainstalované peněženky s jejich názvy a ikonami
// Můžete zobrazit UI pro výběr peněženkyWAGMI toto vše řeší za vás. Když použijete konektor injected(), automaticky použije EIP-6963, pokud je k dispozici, a přepne se na window.ethereum jako zálohu.
WalletConnect#
WalletConnect je protokol, který připojuje mobilní peněženky k desktopovým dApps přes relay server. Uživatel naskenuje QR kód svou mobilní peněženkou a naváže šifrované spojení. Požadavky na transakce se přenášejí z vaší dApp na jejich telefon.
S WAGMI je to jen další konektor:
import { walletConnect } from "wagmi/connectors";
const connector = walletConnect({
projectId: "YOUR_PROJECT_ID", // Získat z cloud.walletconnect.com
showQrModal: true,
});Přepínání řetězců#
Uživatelé jsou často na špatné síti. Vaše dApp je na Mainnetu, oni jsou připojeni k Sepolii. Nebo jsou na Polygonu a vy potřebujete Mainnet. WAGMI poskytuje useSwitchChain:
"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>Prosím připojte svou peněženku</p>;
if (chain.id !== mainnet.id) {
return (
<div>
<p>Prosím přepněte na Ethereum Mainnet</p>
<button
onClick={() => switchChain({ chainId: mainnet.id })}
disabled={isPending}
>
{isPending ? "Přepínání..." : "Přepnout síť"}
</button>
</div>
);
}
return <>{children}</>;
}IPFS a metadata#
NFT neukládají obrázky on-chain. Blockchain ukládá URI, které odkazuje na JSON soubor s metadaty, který zase obsahuje URL obrázku. Standardní vzor, definovaný funkcí tokenURI ERC-721:
Contract.tokenURI(42) → "ipfs://QmXyz.../42.json"
Tento JSON soubor má standardní schéma:
{
"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 adresy používají Content Identifiers (CID) — hashe samotného obsahu. ipfs://QmXyz... znamená „obsah, jehož hash je QmXyz...". Toto je content-addressed úložiště: URI je odvozeno z obsahu, takže obsah se nikdy nemůže změnit bez změny URI. To je garance neměnnosti, na které NFT spoléhají (když skutečně používají IPFS — mnohé používají centralizované URL, což je varovný signál).
Pro zobrazení IPFS obsahu v prohlížeči potřebujete bránu, která překládá IPFS URI na HTTP:
function ipfsToHttp(uri: string): string {
if (uri.startsWith("ipfs://")) {
const cid = uri.replace("ipfs://", "");
return `https://ipfs.io/ipfs/${cid}`;
// Nebo použijte dedikovanou bránu:
// return `https://YOUR_PROJECT.mypinata.cloud/ipfs/${cid}`;
}
return uri;
}
// Načtení NFT metadat
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 služby#
IPFS je peer-to-peer síť. Obsah zůstává dostupný, pouze pokud ho někdo hostuje („pinuje"). Pokud nahrajete obrázek NFT na IPFS a poté vypnete svůj uzel, obsah zmizí.
Pinning služby udržují váš obsah dostupný:
- Pinata: Nejpopulárnější. Jednoduché API. Štědrá bezplatná úroveň (1GB). Dedikované brány pro rychlejší načítání.
- NFT.Storage: Zdarma, podporovaný Protocol Labs (tvůrci IPFS). Navržený speciálně pro NFT metadata. Používá Filecoin pro dlouhodobé uchovávání.
- Web3.Storage: Podobný NFT.Storage, univerzálnější.
// Nahrání na Pinatu
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}`; // Vrátí CID
}Problém indexování#
Tady je špinavé tajemství vývoje pro blockchain: nemůžete efektivně dotazovat historická data z RPC uzlu.
Chcete všechny Transfer události pro token za poslední rok? Budete muset skenovat miliony bloků pomocí eth_getLogs, stránkovat po kouscích 2 000-10 000 bloků (maximum se liší podle providera). To jsou tisíce RPC volání. Bude to trvat minuty až hodiny a vyčerpá vaši API kvótu.
Chcete všechny tokeny vlastněné konkrétní adresou? Neexistuje žádné jediné RPC volání pro toto. Museli byste skenovat každou Transfer událost pro každý ERC-20 kontrakt a sledovat zůstatky. To není proveditelné.
Chcete všechna NFT v peněžence? Stejný problém. Musíte skenovat každou ERC-721 Transfer událost napříč každým NFT kontraktem.
Blockchain je datová struktura optimalizovaná pro zápis. Je vynikající ve zpracování nových transakcí. Je mizerný v odpovídání na historické dotazy. To je fundamentální nesoulad mezi tím, co UI dApps potřebují, a tím, co řetězec nativně poskytuje.
Protokol The Graph#
The Graph je decentralizovaný indexovací protokol. Napíšete „subgraph" — schéma a sadu event handlerů — a The Graph indexuje řetězec a servíruje data přes GraphQL API.
# Schéma subgrafu (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")
}// Dotazování subgrafu z vašeho frontendu
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;
}Kompromis: The Graph přidává latenci (typicky 1-2 bloky za hlavou řetězce) a další závislost. Decentralizovaná síť má také náklady na indexování (platíte v tokenech GRT). Pro menší projekty je hostovaná služba (Subgraph Studio) zdarma.
Rozšířená API od Alchemy a Moralis#
Pokud nechcete spravovat subgraph, Alchemy i Moralis nabízejí předindexovaná API, která odpovídají na běžné dotazy přímo:
// Alchemy: Získání všech ERC-20 zůstatků tokenů pro adresu
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"],
}),
}
);
// Vrátí VŠECHNY ERC-20 zůstatky tokenů v jednom volání
// oproti skenování balanceOf() každého možného ERC-20 kontraktu// Alchemy: Získání všech NFT vlastněných adresou
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}`);
}Tato API jsou proprietární a centralizovaná. Vyměňujete decentralizaci za developer experience. Pro většinu dApps je to smysluplný kompromis. Vaše uživatele nezajímá, zda jejich přehled portfolia pochází ze subgrafu nebo z databáze Alchemy. Zajímá je, že se načte za 200ms místo 30 sekund.
Běžné nástrahy#
Po dodání několika produkčních dApps a debugování kódu jiných týmů jsou toto chyby, které vidím opakovaně. Každá z nich mě osobně potkala.
BigInt všude#
Ethereum pracuje s velmi velkými čísly. Zůstatky ETH jsou ve wei (10^18). Celkové zásoby tokenů mohou být 10^27 nebo více. JavaScriptový Number dokáže bezpečně reprezentovat celá čísla pouze do 2^53 - 1 (přibližně 9 * 10^15). To nestačí pro wei částky.
// ŠPATNĚ — tichá ztráta přesnosti
const balance = 1000000000000000000; // 1 ETH ve wei
const double = balance * 2;
console.log(double); // 2000000000000000000 — vypadá správně, ale...
const largeBalance = 99999999999999999999; // ~100 ETH
console.log(largeBalance); // 100000000000000000000 — ŠPATNĚ! Zaokrouhleno nahoru.
console.log(largeBalance === 100000000000000000000); // true — poškození dat
// SPRÁVNĚ — použijte BigInt
const balance = 1000000000000000000n;
const double = balance * 2n;
console.log(double.toString()); // "2000000000000000000" — správně
const largeBalance = 99999999999999999999n;
console.log(largeBalance.toString()); // "99999999999999999999" — správněPravidla pro BigInt v kódu dApps:
- Nikdy nepřevádějte wei částky na
Number. PoužívejteBigIntvšude, převádějte na čitelné řetězce pouze pro zobrazení. - Nikdy nepoužívejte
Math.floor,Math.roundatd. na BigInty. Nefungují. Použijte celočíselné dělení:amount / 10n ** 6n. - JSON nepodporuje BigInt. Pokud serializujete stav obsahující BigInty, potřebujete vlastní serializátor:
JSON.stringify(data, (_, v) => typeof v === "bigint" ? v.toString() : v). - Používejte formátovací funkce knihoven.
ethers.formatEther(),ethers.formatUnits(),formatEther()aformatUnits()z viem. Řeší konverzi správně.
import { formatUnits, parseUnits } from "viem";
// Zobrazení: BigInt → čitelný řetězec
const weiAmount = 1500000000000000000n; // 1.5 ETH
const display = formatUnits(weiAmount, 18); // "1.5"
// Vstup: čitelný řetězec → BigInt
const userInput = "1.5";
const wei = parseUnits(userInput, 18); // 1500000000000000000n
// USDC má 6 desetinných míst, ne 18
const usdcAmount = 100000000n; // 100 USDC
const usdcDisplay = formatUnits(usdcAmount, 6); // "100.0"Asynchronní operace peněženky#
Každá interakce s peněženkou je asynchronní a může selhat způsoby, které vaše aplikace musí zvládnout elegantně:
// Uživatel může odmítnout jakoukoli výzvu peněženky
try {
const tx = await writeContract({
address: contractAddress,
abi: ERC20_ABI,
functionName: "approve",
args: [spenderAddress, amount],
});
} catch (error) {
if (error.code === 4001) {
// Uživatel odmítl transakci ve své peněžence
// Toto je normální — není to chyba k hlášení
showToast("Transakce zrušena");
} else if (error.code === -32603) {
// Interní JSON-RPC chyba — často znamená, že transakce by revertovala
showToast("Transakce by selhala. Zkontrolujte svůj zůstatek.");
} else {
// Neočekávaná chyba
console.error("Transaction error:", error);
showToast("Něco se pokazilo. Zkuste to prosím znovu.");
}
}Klíčové asynchronní nástrahy:
- Výzvy peněženky blokují na straně uživatele.
awaitve vašem kódu může trvat 30 sekund, zatímco uživatel čte detaily transakce v MetaMasku. Nezobrazujte loading spinner, který by je přiměl myslet, že se něco rozbilo. - Uživatel může uprostřed interakce přepnout účty. Žádáte schválení z Účtu A, uživatel přepne na Účet B, pak schválí. Nyní Účet B schválil, ale vy se chystáte odeslat transakci z Účtu A. Vždy znovu zkontrolujte připojený účet před kritickými operacemi.
- Dvoukrokové vzory zápisu jsou běžné. Mnoho DeFi operací vyžaduje
approve+execute. Uživatel musí podepsat dvě transakce. Pokud schválí ale neprovede, musíte zkontrolovat stav allowance a příště krok schválení přeskočit.
Chyby nesouhlasu sítí#
Tato chyba plýtvá více času na debugování než jakýkoli jiný problém. Váš kontrakt je na Mainnetu. Vaše peněženka je na Sepolii. Váš RPC provider ukazuje na Polygon. Tři různé sítě, tři různé stavy, tři úplně nesouvisející blockchainy. A chybová zpráva je obvykle neužitečná — „execution reverted" nebo „contract not found".
// Obranná kontrola řetězce
import { useAccount, useChainId } from "wagmi";
function useRequireChain(requiredChainId: number) {
const chainId = useChainId();
const { isConnected } = useAccount();
if (!isConnected) {
return { ready: false, error: "Prosím připojte svou peněženku" };
}
if (chainId !== requiredChainId) {
return {
ready: false,
error: `Prosím přepněte na ${getChainName(requiredChainId)}. Jste na ${getChainName(chainId)}.`,
};
}
return { ready: true, error: null };
}Front-running v DeFi#
Když odešlete swap na DEXu, vaše čekající transakce je viditelná v mempoolu. Bot může vidět váš obchod, provést front-run tím, že vytlačí cenu nahoru, nechá váš obchod provést za horší cenu a poté okamžitě prodá se ziskem. Tomu se říká „sandwich útok".
Jako frontendový vývojář tomu nemůžete zcela zabránit, ale můžete to zmírnit:
// Nastavení tolerance skluzu na swapu ve stylu Uniswap
const amountOutMin = expectedOutput * 995n / 1000n; // 0,5% tolerance skluzu
// Použití deadline pro zabránění dlouhodobým čekajícím transakcím
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20); // 20 minut
await router.swapExactTokensForTokens(
amountIn,
amountOutMin, // Minimální přijatelný výstup — revert, pokud bychom dostali méně
[tokenA, tokenB],
userAddress,
deadline, // Revert, pokud se neprovede do 20 minut
);Pro transakce vysoké hodnoty zvažte použití Flashbots Protect RPC, které posílá transakce přímo tvůrcům bloků místo do veřejného mempoolu. Toto kompletně zabraňuje sandwich útokům, protože boty nikdy nevidí vaši čekající transakci:
// Použití Flashbots Protect jako vašeho RPC endpointu
const provider = new ethers.JsonRpcProvider("https://rpc.flashbots.net");Záměna desetinných míst#
Ne všechny tokeny mají 18 desetinných míst. USDC a USDT mají 6. WBTC má 8. Některé tokeny mají 0, 2 nebo libovolný počet desetinných míst. Vždy si přečtěte decimals() z kontraktu před formátováním částek:
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"Selhání odhadu gasu#
Když estimateGas selže, obvykle to znamená, že transakce by revertovala. Ale chybová zpráva je často jen „cannot estimate gas" bez uvedení důvodu. Použijte eth_call k simulaci transakce a získání skutečného důvodu revertu:
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; // Žádná chyba — transakce by uspěla
} catch (error) {
// Dekódování důvodu revertu
if (error.data) {
// Běžné revert řetězce
if (error.data.startsWith("0x08c379a0")) {
// Error(string) — standardní revert se zprávou
const reason = decodeAbiParameters(
[{ type: "string" }],
`0x${error.data.slice(10)}`
);
return `Revert: ${reason[0]}`;
}
}
return error.message;
}
}Vše dohromady#
Zde je kompletní, minimální React komponenta, která připojí peněženku, přečte zůstatek tokenu a odešle převod. Toto je kostra každé dApp:
"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("");
// Čtení zůstatku — spouští se pouze když je adresa definována
const { data: balance, refetch: refetchBalance } = useReadContract({
address: USDC_ADDRESS,
abi: USDC_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});
// Zápis: převod tokenů
const {
writeContract,
data: txHash,
isPending: isSigning,
error: writeError,
} = useWriteContract();
// Čekání na potvrzení
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash: txHash,
});
// Aktualizace zůstatku po potvrzení
if (isSuccess) {
refetchBalance();
}
if (!isConnected) {
return (
<button onClick={() => connect({ connector: injected() })}>
Připojit peněženku
</button>
);
}
return (
<div>
<p>Připojeno: {address}</p>
<p>
USDC zůstatek:{" "}
{balance !== undefined ? formatUnits(balance, 6) : "Načítání..."}
</p>
<div>
<input
placeholder="Adresa příjemce (0x...)"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
<input
placeholder="Částka (např. 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
? "Potvrďte v peněžence..."
: isConfirming
? "Potvrzování..."
: "Odeslat USDC"}
</button>
</div>
{writeError && <p style={{ color: "red" }}>{writeError.message}</p>}
{isSuccess && <p style={{ color: "green" }}>Převod potvrzen!</p>}
{txHash && (
<a
href={`https://etherscan.io/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
Zobrazit na Etherscanu
</a>
)}
<button onClick={() => disconnect()}>Odpojit</button>
</div>
);
}Kam dál#
Tento článek pokryl základní koncepty a nástroje pro webové vývojáře vstupující do Etherea. V každé oblasti je mnohem více hloubky:
- Solidity: Pokud chcete psát kontrakty, ne jen s nimi interagovat. Oficiální dokumentace a kurzy Patricka Collinse jsou nejlepší výchozí body.
- ERC standardy: ERC-20 (zaměnitelné tokeny), ERC-721 (NFT), ERC-1155 (multi-token), ERC-4626 (tokenizované trezory). Každý definuje standardní rozhraní, které implementují všechny kontrakty v dané kategorii.
- Layer 2: Arbitrum, Optimism, Base, zkSync. Stejný vývojářský zážitek, nižší náklady na gas, mírně odlišné předpoklady důvěry. Váš kód ethers.js a viem funguje identicky — stačí změnit chain ID a RPC URL.
- Account Abstraction (ERC-4337): Další evoluce UX peněženek. Smart kontraktní peněženky podporující sponzorování gasu, sociální obnovu a dávkové transakce. Tímto směrem míří vzor „připojit peněženku".
- MEV a řazení transakcí: Pokud stavíte DeFi, pochopení Maximal Extractable Value není volitelné. Dokumentace Flashbots je kanonický zdroj.
Ekosystém blockchainu se vyvíjí rychle, ale základy v tomto článku — účty, transakce, ABI kódování, RPC volání, indexování událostí — se nezměnily od roku 2015 a v dohledné době se nezmění. Naučte se je dobře a vše ostatní je jen API povrch.