✍️ Unit 4 — Build & Broadcast Transaction
Di akhir unit ini kamu akan:
- Paham alur compose → sign → broadcast → confirm
- Bisa mengirim MsgSend dari script Node maupun dari browser
- Bisa memanggil execute contract CosmWasm dari TypeScript
- Bisa menangani kegagalan transaksi dengan benar
- ✅ Unit 3 selesai
- ✅ Ada testnet INJ di wallet-mu
🔄 Alur Sebuah Transaksi
Perhatikan bahwa ini adalah versi sisi klien dari perjalanan transaksi yang kita bahas di Phase 1 Unit 1. Sekarang kamu yang menulis kodenya.
🖥️ Cara 1 — Dari Script Node (Private Key)
Cocok untuk bot, script otomatis, dan pengujian. Tidak pernah untuk kode yang berjalan di browser.
import { Network } from "@injectivelabs/networks";
import { toChainFormat } from "@injectivelabs/utils";
import { MsgSend } from "@injectivelabs/sdk-ts/core/modules";
import { MsgBroadcasterWithPk } from "@injectivelabs/sdk-ts/core/tx";
const privateKey = process.env.PRIVATE_KEY!;
const injectiveAddress = "inj1alamat_pengirim";
const tujuan = "inj1alamat_tujuan";
async function kirimINJ() {
const amount = {
denom: "inj",
amount: toChainFormat(0.01).toFixed(), // 0.01 INJ
};
const msg = MsgSend.fromJSON({
amount,
srcInjectiveAddress: injectiveAddress,
dstInjectiveAddress: tujuan,
});
const txHash = await new MsgBroadcasterWithPk({
privateKey,
network: Network.Testnet,
}).broadcast({
msgs: msg,
});
console.log("Berhasil. Tx hash:", txHash);
}
kirimINJ().catch(console.error);
Sama seperti di Phase 2 Unit 3:
- Simpan di
.env, dan pastikan.envada di.gitignore - Pakai wallet latihan yang hanya berisi testnet INJ
- Jangan pernah memakai
MsgBroadcasterWithPkdi kode frontend — private key akan terkirim ke browser semua pengunjung
Untuk browser, pakai Cara 2 di bawah.
🌐 Cara 2 — Dari Browser (Wallet)
Di browser, private key tidak pernah menyentuh kodemu. Wallet yang menandatangani.
import { MsgSend } from "@injectivelabs/sdk-ts/core/modules";
import { MsgBroadcaster } from "@injectivelabs/wallet-core";
import { Network } from "@injectivelabs/networks";
import { toChainFormat } from "@injectivelabs/utils";
import { walletStrategy } from "../wallet";
const broadcaster = new MsgBroadcaster({
walletStrategy,
network: Network.Testnet,
});
export async function kirimINJ(dari: string, ke: string, jumlah: number) {
const msg = MsgSend.fromJSON({
amount: {
denom: "inj",
amount: toChainFormat(jumlah).toFixed(),
},
srcInjectiveAddress: dari,
dstInjectiveAddress: ke,
});
// Wallet akan memunculkan popup konfirmasi di sini
const hasil = await broadcaster.broadcast({
msgs: msg,
injectiveAddress: dari,
});
return hasil.txHash;
}
Bergantung versi SDK, broadcaster berbasis wallet bisa berada di @injectivelabs/wallet-core, @injectivelabs/wallet-ts, atau diekspor dari @injectivelabs/sdk-ts.
Kalau impor di atas gagal, jalankan npm ls | grep injectivelabs dan cek API reference. Polanya tetap sama: satu broadcaster yang menerima walletStrategy dan jaringan, lalu method broadcast yang menerima pesan.
📜 Memanggil Contract CosmWasm
Ini menyambungkan contract dari Phase 2 Jalur B ke aplikasi web-mu.
import { MsgExecuteContractCompat } from "@injectivelabs/sdk-ts/core/modules";
export async function panggilIncrement(pengirim: string, alamatContract: string) {
const msg = MsgExecuteContractCompat.fromJSON({
sender: pengirim,
contractAddress: alamatContract,
msg: {
increment: {}, // ExecuteMsg yang sama dengan di terminal
},
});
const hasil = await broadcaster.broadcast({
msgs: msg,
injectiveAddress: pengirim,
});
return hasil.txHash;
}
export async function panggilReset(
pengirim: string,
alamatContract: string,
nilaiBaru: number
) {
const msg = MsgExecuteContractCompat.fromJSON({
sender: pengirim,
contractAddress: alamatContract,
msg: {
reset: { count: nilaiBaru },
},
});
const hasil = await broadcaster.broadcast({
msgs: msg,
injectiveAddress: pengirim,
});
return hasil.txHash;
}
{ increment: {} } di sini adalah objek yang sama persis dengan:
- Varian
ExecuteMsg::Increment {}yang kamu tulis dalam Rust - String
'{"increment":{}}'yang kamu ketik di terminalinjectived
Satu definisi, tiga tempat pemakaian. Inilah kenapa memahami enum di CosmWasm Starter terbayar sekarang.
⚠️ Menangani Kegagalan Transaksi
Transaksi bisa gagal di dua tempat berbeda, dan konsekuensinya berbeda.
| Gagal di | Kapan | Gas terpotong? |
|---|---|---|
| Sebelum masuk blok | Signature salah, saldo tidak cukup, format salah | ❌ Tidak |
| Saat eksekusi | Logika contract menolak (Unauthorized, dsb) | ✅ Ya |
export async function kirimDenganPenanganan(
dari: string,
ke: string,
jumlah: number
) {
// Validasi sebelum menyentuh jaringan
if (!ke.startsWith("inj1")) {
return { sukses: false, pesan: "Alamat tujuan tidak valid" };
}
if (jumlah <= 0) {
return { sukses: false, pesan: "Jumlah harus lebih dari nol" };
}
try {
const txHash = await kirimINJ(dari, ke, jumlah);
return {
sukses: true,
txHash,
url: `https://testnet.blockscout.injective.network/tx/${txHash}`,
};
} catch (error) {
return { sukses: false, pesan: terjemahkanError(error) };
}
}
function terjemahkanError(error: unknown): string {
const pesan = error instanceof Error ? error.message : String(error);
if (pesan.includes("insufficient funds")) {
return "Saldo tidak cukup. Ambil testnet INJ dari faucet.";
}
if (pesan.includes("rejected") || pesan.includes("denied")) {
return "Transaksi dibatalkan.";
}
if (pesan.includes("out of gas")) {
return "Gas tidak cukup. Coba naikkan batas gas.";
}
if (pesan.includes("Unauthorized")) {
return "Akun ini tidak punya izin untuk aksi tersebut.";
}
if (pesan.includes("account sequence")) {
return "Ada transaksi lain yang belum selesai. Tunggu sebentar lalu coba lagi.";
}
return `Transaksi gagal: ${pesan}`;
}
Ketika transaksi berhasil, berikan pengguna tautan ke Blockscout.
Ini membangun kepercayaan (mereka bisa memverifikasi sendiri), sangat membantu saat debugging, dan menjadi bukti yang bisa kamu tunjukkan di showcase.
🎨 Pola UI Transaksi
Transaksi butuh waktu. Antarmuka harus mencerminkan itu.
type StatusTx = "idle" | "menandatangani" | "menunggu" | "sukses" | "gagal";
export function FormKirim({ alamat }: { alamat: string }) {
const [status, setStatus] = useState<StatusTx>("idle");
const [txHash, setTxHash] = useState<string | null>(null);
const [pesan, setPesan] = useState<string | null>(null);
async function handleKirim(tujuan: string, jumlah: number) {
setStatus("menandatangani");
setPesan(null);
try {
const hasil = await kirimDenganPenanganan(alamat, tujuan, jumlah);
if (hasil.sukses) {
setTxHash(hasil.txHash!);
setStatus("sukses");
} else {
setPesan(hasil.pesan!);
setStatus("gagal");
}
} catch {
setPesan("Terjadi kesalahan tak terduga");
setStatus("gagal");
}
}
return (
<div>
{status === "menandatangani" && <p>Konfirmasi di wallet-mu...</p>}
{status === "menunggu" && <p>Menunggu konfirmasi jaringan...</p>}
{status === "sukses" && txHash && (
<p>
Berhasil.{" "}
<a
href={`https://testnet.blockscout.injective.network/tx/${txHash}`}
target="_blank"
rel="noreferrer"
>
Lihat di explorer
</a>
</p>
)}
{status === "gagal" && <p role="alert">{pesan}</p>}
</div>
);
}
Pemula biasanya hanya membuat "loading" dan "selesai". Tapi pengguna perlu tahu bedanya:
- "Konfirmasi di wallet-mu" — bola ada di tangan pengguna, mereka harus bertindak
- "Menunggu konfirmasi jaringan" — bola ada di tangan chain, mereka tinggal menunggu
Tanpa perbedaan ini, pengguna akan menatap spinner sambil bertanya-tanya kenapa tidak ada yang terjadi — padahal popup wallet menunggu di balik jendela browser.
🧪 Latihan
- Kirim 0,01 testnet INJ dari script Node ke alamat keduamu
- Verifikasi transaksinya di Blockscout
- Buat halaman web sederhana dengan tombol koneksi wallet dan form kirim
- Panggil
incrementpada contract CosmWasm-mu dari browser - Sengaja picu kegagalan (kirim melebihi saldo) dan pastikan pesan errornya ramah
🎯 Rangkuman
- Alur: compose → sign → broadcast → confirm
- Private key hanya untuk script Node, tidak pernah di browser
- Di browser, wallet yang menandatangani — kodemu tidak pernah menyentuh key
- Objek pesan CosmWasm sama persis di Rust, terminal, dan TypeScript
- Gagal sebelum blok = gas tidak terpotong; gagal saat eksekusi = gas terpotong
- Terjemahkan error jadi kalimat yang bisa ditindaklanjuti
- Selalu berikan tautan explorer setelah berhasil
- Bedakan status "menandatangani" dan "menunggu jaringan" di UI
✅ Quick Check
- Kenapa
MsgBroadcasterWithPktidak boleh dipakai di frontend? - Transaksi gagal karena
Unauthorized. Apakah gas terpotong? - Apa hubungan
{ increment: {} }di TypeScript dengan kode Rust-mu? - Kenapa "menandatangani" dan "menunggu jaringan" harus jadi dua status berbeda?
🎉 Bagian TypeScript SDK selesai! Ini mencakup Learning Track Phase 6 dan sebagian Phase 7.
Lanjut: Guided Project — Overview 👉