Builder 05 of 06
Read and Write from a dApp
Put a frontend in front of your contract: connect a wallet, show its state, and change it from a button.
18 min read, then one transaction
You have deployed contracts and called them from Remix. Remix is a development tool, though — a real user will never see it. This module puts a page in front of your contract, which is all a dApp has ever been.
There is a working one at the end of this lesson: a single HTML file, no build
step, no framework. Open it, connect a wallet, and it reads and writes the
MessageContract you deployed in module two.
A dApp is three connections
- 01
Page to wallet
The browser exposes window.ethereum when a wallet extension is installed. Your page asks it for an address and, later, for a signature. It never sees the private key.
- 02
Page to RPC
Reading contract state needs a node, not a wallet. Your page calls an RPC endpoint directly, which costs nothing and requires no permission.
- 03
Wallet to chain
When you write, the wallet signs and broadcasts. Your page does not send the transaction — it hands over a request and waits.
That split is the thing to internalise. Reads and writes take different paths. A read goes page to RPC and back, with no wallet involved at all. A write goes page to wallet to chain, and your page is a spectator for most of it.
Reading
- Public client, RPC only
- No wallet, no signature
- Free, and instant
- Fails loudly and immediately
Writing
- Wallet client, user signs
- Costs gas in zkLTC
- Takes a block to confirm
- Can fail after you have paid
The ABI is a description, not a connection
To call a contract you need its address and its ABI — a JSON description of the functions. Nothing links the two. Nothing checks that the ABI you hold matches the contract at that address.
const abi = [
{ type: "function", name: "message", stateMutability: "view",
inputs: [], outputs: [{ type: "string" }] },
];
Give a correct address with the wrong ABI and the call encodes a function selector the contract does not have. The contract falls through to its fallback or reverts, and you get an unhelpful error about decoding rather than a clear "no such function".
Simulate before you ask for a signature
This is the single habit that separates a dApp that feels solid from one that feels broken:
const { request } = await publicClient.simulateContract({
address, abi, functionName: "setMessage", args: [text], account,
});
const hash = await walletClient.writeContract(request);
simulateContract runs the call against current state on a node, for free,
without signing anything. If it would revert, you find out now — before the
wallet popup, before the user pays for gas.
Skip it and the failure mode is: user confirms, waits, pays, and then learns their call was never going to work. That is the origin of most "this dApp is broken" complaints, and it is one function call away from being fixed.
Transaction states are your interface
A write is not an event, it is a sequence, and each step can stall or fail:
simulating → waiting for signature → pending → confirmed
↓ ↓ ↓
rejected dropped reverted
A user staring at an unchanged button has no idea which of those they are in. Say it out loud at every step. The starter file below writes a plain sentence for each one, which is genuinely all it takes.
Errors worth naming
Three failures account for nearly everything a beginner hits. Each deserves its own sentence rather than a stack trace:
- Code
4001— the user rejected the request in their wallet. Not an error in your code. Say so, and offer the button again. insufficient funds— no zkLTC for gas. Link the faucet right there.- Wrong chain — the wallet is connected but pointed elsewhere. Detect it
with
getChainId()and offeraddChainthenswitchChain. A wallet that has never seen LiteForge cannot switch to it, so add first.
The wrong-chain case is worth extra care. A connected wallet on the wrong network makes every read return nothing and every write fail confusingly, and the user has no way to guess what is wrong.
Build it
- 01
Open the starter
The link below serves a single self-contained HTML file. Save it, or open it straight from this site — either works.
- 02
Connect your wallet
It checks the chain and offers to add or switch to LiteForge 4441 if you are elsewhere.
- 03
Paste your contract
The MessageContract address from module two — the panel below finds it for you if you no longer have it. Press Read: that call never touches your wallet.
- 04
Change the message
Type something and send. Watch the status line move through simulating, signing, pending, confirmed.
- 05
Submit the hash
The check below wants that transaction: a call to a contract that emitted an event.
Closed Remix days ago and lost the address? It was never only in Remix. Every contract you have ever deployed is recorded in the transaction that created it, so the chain can be asked which ones are yours:
Find a contract you deployed
Connect the wallet you deployed from, or paste its address. Everything below is read from the chain: the explorer says what your address created, and each contract is asked what it is.
Give it to somebody else
The starter is one HTML file with no backend, so sharing it is sharing a file: put it on any static host, or send it as an attachment and let them open it locally. Both work, because the only servers involved are an RPC endpoint and whatever their wallet talks to.
- 01
They open the page
No account, no sign-up. The page is inert until a wallet connects to it.
- 02
They connect their own wallet
Your dApp never sees a key and never holds a session. The wallet signs, the page only asks.
- 03
They paste the same contract address
Your MessageContract. Their read hits the same storage yours does — shared state with no database behind it.
- 04
They write, you refresh
Their message, their address as author, visible from your copy of the page a block later.
This is the part that does not exist off chain: two strangers running the same frontend against the same state, with no server between them, and neither one needing to trust the other or you.
Read the file. It is about a hundred lines, and every line is one of the ideas above: two clients, one ABI, a chain check, a simulation, four status messages, three named errors. Everything a framework would add on top of that is convenience, not substance.
What to remember
- 01Reads go page to RPC with no wallet; writes go page to wallet to chain. Different paths, different failure modes.
- 02An ABI is an unverified description — a wrong one produces a decoding error rather than a missing-function error.
- 03simulateContract catches a revert before the user signs, which removes most of what makes a dApp feel broken.
- 04A confirmed transaction can still have reverted: read receipt.status.
- 05Rejection, insufficient funds, and wrong chain deserve named messages; a wallet must be given a chain with addChain before it can switch to it.
Primary sources
Write from your dApp
Paste the hash of a transaction your frontend sent to your contract. The check reads the transaction straight from LiteForge, so it has to be one your own wallet sent.