Sending Transactions — Web3

Sending Transactions Menulis data ke blockchain membutuhkan transaksi. Transaksi harus di-sign oleh private key, dikirim ke network, dan ditunggu sampai…

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 redeem untuk 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

  1. Build: isi field-nya (tabel di bawah).
  2. Sign: wallet menandatangani dengan private key. Di browser itu wallet pengguna; di chain lokal kita, node Hardhat yang memegang akun test.
  3. Send: transaksi bertanda tangan dikirim ke node dan menunggu di mempool.
  4. Mined: validator memasukkannya ke block.
  5. 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

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

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.

Yang akan kamu pelajari