Mengirim transaksi artinya meminta jaringan mengubah state: menukar POIN, memindahkan ETH, memanggil fungsi yang menulis. Karena mengubah state, transaksi harus ditandatangani pemilik akun dan membayar gas. Bayangin mengirim paket lewat ekspedisi. Kamu mengisi formulir (tujuan, isi, ongkir maksimal), tanda tangan, lalu menyerahkannya ke loket. Paket menunggu di gudang (mempool) sampai diangkut truk berikutnya (block), dan kamu dapat resi (receipt) yang bilang terkirim atau ditolak. Ongkirnya tetap dibayar walau paketnya ditolak. Paket yang masih di gudang bisa diganti formulirnya; paket yang sudah berangkat gak bisa ditarik lagi.
🧪 Tebakan cepat: Yoga punya 115 POIN dan nekat mengirim
redeemuntuk 200 POIN dengan batas gas 100.000, tanpa simulasi. Transaksinya masuk block dan revert. Berapa gas yang Yoga bayar: 0, 24.263, atau 100.000 penuh? Tebak dulu, jawabannya ada di bagian "Receipt: status 1 atau 0".
Lima langkah sebuah transaksi
- Build: isi field-nya (tabel di bawah).
- Sign: wallet menandatangani dengan private key. Di browser itu wallet pengguna; di chain lokal kita, node Hardhat yang memegang akun test.
- Send: transaksi bertanda tangan dikirim ke node dan menunggu di mempool.
- Mined: validator memasukkannya ke block.
- Receipt: hasilnya bisa dibaca: berhasil atau revert, gas terpakai, log.
Ini transaksi Dinda yang menukar 100 POIN (diskon Rp 5.000), dibangun viem 2.57.4 di chain lokal yang sama dengan lesson sebelumnya (block 1 sampai 5):
| Field | Nilai | Artinya |
|---|---|---|
from |
Dinda 0x7099...79C8 |
yang menandatangani |
to |
PoinRaka 0x5FbD...0aa3 |
contract yang dipanggil |
value |
0 | gak ada ETH yang dikirim |
data |
0xdb006a75...056bc75e2d63100000 |
redeem(100 POIN) dalam calldata |
nonce |
0 | nomor urut transaksi Dinda; ini transaksi pertamanya |
gas |
33.946 | batas gas, dari eth_estimateGas |
maxPriorityFeePerGas |
1.000.000.000 wei (1 gwei) | tip untuk validator |
maxFeePerGas |
1.617.957.952 wei | harga tertinggi per gas yang rela dibayar |
chainId |
31337 | supaya tanda tangannya gak berlaku di chain lain |
type |
2 (EIP-1559) | format fee base + tip |
Nonce adalah nomor antrean pribadi tiap akun, mulai dari 0. Node cuma memproses nonce 1 setelah nonce 0 selesai, dan satu nonce cuma bisa dipakai sekali. Itu yang mencegah transaksi yang sama diputar ulang.
Membangun, mengirim, menunggu dengan viem
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits, encodeFunctionData } from 'viem'
import { hardhat } from 'viem/chains'
const abi = parseAbi([
'function redeem(uint256 amount)',
'event Redeemed(address indexed customer, uint256 amount)',
'error NotEnoughPoin(uint256 balance, uint256 needed)',
])
const publicClient = createPublicClient({ chain: hardhat, transport: http() })
const dinda = createWalletClient({ account: DINDA, chain: hardhat, transport: http() })
const request = await dinda.prepareTransactionRequest({
to: POIN,
data: encodeFunctionData({ abi, functionName: 'redeem', args: [parseUnits('100', 18)] }),
})
console.log(request.nonce, request.gas, request.maxFeePerGas, request.maxPriorityFeePerGas)
const hash = await dinda.sendTransaction(request)
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(receipt.status, receipt.blockNumber, receipt.gasUsed, receipt.effectiveGasPrice)
0 33946n 1617957952n 1000000000n
success 6n 33946n 1450674733n
prepareTransactionRequestmengisi nonce (eth_getTransactionCount), gas (eth_estimateGas) dan fee. viem menghitungmaxFeePerGas= base fee block terakhir × 1,2 + tip: 514.964.960 × 1,2 + 1.000.000.000 = 1.617.957.952 wei.sendTransactionmengirimeth_sendTransaction; node menandatangani untuk akun test-nya dan langsung mengembalikan hash. Hash itu cuma tanda terima, belum bukti berhasil.waitForTransactionReceiptmenunggu sampai transaksinya masuk block. Di ethers v6 sama saja:const tx = await poin.connect(dinda).redeem(amount), laluconst receipt = await tx.wait(), danreceipt.statusbernilai1.
Yang ditandatangani adalah semua field tadi. Hasilnya 147 byte, diawali 0x02 (tipe EIP-1559), dan hash transaksi = keccak256 dari byte itu (dipotong):
0x02f890827a6980843b9aca00846070144082849a945fbdb2315678afecb367f032d93f642f64180aa380a4db006a75...c080a0504461bb...a070506f2c...
Receipt: status 1 atau 0
Dinda, redeem(100) |
Yoga, redeem(200) dengan batas gas 100.000 |
|
|---|---|---|
status |
success (1) |
reverted (0) |
| block | 6 | 7 |
gasUsed |
33.946 | 24.263 |
effectiveGasPrice |
1.450.674.733 wei | 1.394.404.136 wei |
| biaya | 0,000049244604486418 ETH | 0,000033832427551768 ETH |
logs |
Redeemed(Dinda, 100 POIN) |
kosong |
| POIN sesudahnya | 60 | tetap 115 |
effectiveGasPrice = base fee block itu + tip: block 6 punya base fee 450.674.733, ditambah tip 1 gwei. Transaksi Yoga revert, jadi semua perubahannya dibatalkan dan gak ada log. Tapi kerja EVM sampai titik revert tetap dibayar: 24.263 gas, bukan 0 dan bukan 100.000. Itu jawaban tebakannya. Sisa batas gas dikembalikan; yang hangus seluruhnya cuma kalau gas-nya habis di tengah jalan (out of gas). Nonce Yoga juga tetap terpakai: transaksi berikutnya memakai nonce 1.
Revert dengan custom error
Sebelum mengirim, simulasikan dulu. simulateContract menjalankan transaksinya lewat eth_call, jadi revert ketahuan tanpa tanda tangan dan tanpa gas:
await publicClient.simulateContract({ account: YOGA, address: POIN, abi, functionName: 'redeem', args: [parseUnits('200', 18)] })
The contract function "redeem" reverted.
Error: NotEnoughPoin(uint256 balance, uint256 needed)
(115000000000000000000, 200000000000000000000)
Node sebenarnya cuma mengembalikan byte revert data. viem bisa membacanya karena error NotEnoughPoin ada di abi:
0xb6029ee7 selector NotEnoughPoin(uint256,uint256)
0000000000000000000000000000000000000000000000063bf212b431ec0000 115 × 10^18, saldo Yoga
00000000000000000000000000000000000000000000000ad78ebc5ac6200000 200 × 10^18, yang diminta
Selector error dihitung persis seperti selector fungsi. Di ethers v6, error dari pengiriman bisa datang sebagai execution reverted (unknown custom error); ubah jadi nama dan argumen dengan poin.interface.parseError(error.data).
Menunggu konfirmasi
Begitu masuk block 6, transaksi Dinda punya 1 konfirmasi. Setiap block baru di atasnya menambah satu. Setelah block 7 ditambang, waitForTransactionReceipt({ hash, confirmations: 2 }) selesai (di ethers: tx.wait(2)). Makin banyak konfirmasi, makin kecil peluang block-nya diganti. Untuk yang benar-benar penting, tunggu sampai block-nya finalized (lesson sebelumnya).
Transaksi macet dan cara menggantinya
Yoga mencoba lagi untuk 100 POIN, tapi memasang maxFeePerGas 0,1 gwei (100.000.000 wei), di bawah base fee block 7 (394.404.136 wei). Transaksinya diterima ke mempool tapi gak bisa masuk block: block 8 ditambang tanpa dia. Selama belum masuk block, transaksi bisa diganti: kirim transaksi baru dengan nonce yang sama dan fee yang lebih tinggi. Node Hardhat menolak kenaikan yang terlalu kecil:
Replacement transaction underpriced. A gasPrice/maxFeePerGas of at least 110000000 is necessary to replace the existing transaction with nonce 1.
Dengan maxFeePerGas 2 gwei dan tip 1 gwei, penggantinya masuk block 9 (status success, 33.946 gas), Yoga tinggal 15 POIN, dan hash lama hilang: Transaction with hash "0x5250727da7169bd826b23c2f8abd09a34d6bfa89536394709c0683b3a6cca8d1" could not be found. viem memberi tahu lewat onReplaced di waitForTransactionReceipt, dengan reason: 'repriced'. Untuk membatalkan, ganti dengan transaksi 0 ETH ke alamat sendiri dengan nonce sama. Tapi begitu transaksi sudah masuk block, gak ada lagi yang bisa dibatalkan: blockchain gak punya tombol undo.
⚠️ Jebakan yang sering ketemu
- Menganggap hash berarti berhasil. Hash cuma tanda terima. Tunggu receipt, lalu cek
status. - Mengirim tanpa simulasi. Revert yang bisa ketahuan gratis lewat
simulateContractatauestimateGasjadi revert yang dibayar. - Mengatur nonce sendiri. Nonce yang loncat bikin semua transaksi sesudahnya menunggu. Biarkan wallet atau library yang mengisinya; pakai nonce manual cuma untuk mengganti transaksi yang macet.
- Fee terlalu rendah. Kalau
maxFeePerGasdi bawah base fee, transaksinya diam di mempool. Ganti dengan nonce sama dan fee lebih tinggi.
TL;DR: Transaksi melewati build, sign, send, mined, receipt. Field-nya: to, value, data, nonce (urutan per akun), batas gas, maxFeePerGas dan maxPriorityFeePerGas (EIP-1559), chainId, lalu tanda tangan dari private key. viem: prepareTransactionRequest, sendTransaction, waitForTransactionReceipt; ethers: tx.wait(). Hash bukan bukti: cek receipt.status (1 berhasil, 0 revert). Revert membatalkan perubahan tapi gas terpakai tetap dibayar (Yoga: 24.263 gas). Simulasikan dulu: NotEnoughPoin(115, 200) terbaca dari revert data 0xb6029ee7.... Konfirmasi bertambah tiap block. Transaksi macet diganti dengan nonce sama dan fee lebih tinggi; yang sudah masuk block gak bisa dibatalkan.