Build a Messaging System
What we're building
A minimal on-chain messaging contract: any address can send a short message to any other address, the recipient can read their inbox, and both can prove the message existed at a given block. Not a chat app — a primitive that real chat apps could be built on top of. We'll touch the same patterns you'd use for any append-only public ledger: a per-recipient mapping, indexed events, and gas-conscious storage.
By the end you'll have a deployed contract on testnet and an SDK snippet that reads from it.
The contract
Open the Playground and paste this:
import { Address } from '@valdium/std';
export class Messages {
// inbox[recipient] = list of message indices
#inbox = new Map();
// messages[i] = { from, to, text, blockNumber }
#messages = [];
send(to, text) {
if (typeof to !== 'string' || !Address.isValid(to)) {
throw new Error('bad recipient');
}
if (typeof text !== 'string' || text.length === 0 || text.length > 280) {
throw new Error('text must be 1..280 chars');
}
const idx = this.#messages.length;
this.#messages.push({
from: ctx.sender,
to,
text,
blockNumber: ctx.blockNumber,
});
const inbox = this.#inbox.get(to) ?? [];
inbox.push(idx);
this.#inbox.set(to, inbox);
emit('Sent', { from: ctx.sender, to, index: idx });
}
inboxOf(addr) {
const idxs = this.#inbox.get(addr) ?? [];
return idxs.map(i => this.#messages[i]);
}
count() {
return this.#messages.length;
}
}Three things to point at in this code:
ctx.senderandctx.blockNumberare the only ways to get caller identity and the current height inside a contract. The runtime injects them on every call; you don't import them.emit('Sent', ...)writes a log entry that indexers and dapps can subscribe to. Cheaper than reading state; the right channel for "something happened" notifications.- The 280-char limit is a gas-saving choice — every character of
textcosts bytes-of-storage gas, and an open-ended limit invites spammers to write a megabyte for a few VLD.
Compile and deploy
Click Compile. If you see OK · 0 errors, click Deploy. Sign in the wallet. Within 2 seconds you'll have an address like 0xA12cE…F92e. Copy it.
Send your first message
In the Interact tab, click send. Fill in:
- to: any other address you control (or a friend's)
- text:
hello world
Sign. The right panel will show the receipt with the Sent event decoded. Click inboxOf with the recipient's address and you'll see the message you just sent.
Reading from a frontend
Now let's pull this from a webpage. Install the SDK:
npm i @valdium/sdkAnd drop in this code:
import { ValdiumClient, Contract } from '@valdium/sdk';
import abi from './messages.abi.json'; // generated by the compiler
const client = new ValdiumClient('https://testnet.valdium.xyz/rpc');
const messages = new Contract(MY_DEPLOYED_ADDRESS, abi, client);
// read
const inbox = await messages.inboxOf(myAddress);
inbox.forEach(m => console.log(\`from \${m.from}: \${m.text}\`));
// subscribe to new messages
messages.on('Sent', (event) => {
if (event.to === myAddress) {
console.log('new message from', event.from);
}
});Reads are free and return immediately. Subscriptions hold a WebSocket open and fire as new messages land. Combined, this is the entire stack for a minimal mailbox UI.
Gas considerations
The send function does three storage writes: append to #messages, lookup-and-update #inbox.get(to), and emit an event. On testnet that's around 65,000 gas — roughly 0.0001 VLD at current base fee. Worth knowing because a chat app that sends a tx per keystroke would burn a real wallet quickly; batching sends is the obvious optimization.
A batched variant that takes an array of { to, text } in one call would amortize the per-tx overhead and drop the per-message cost by about 40%. Left as an exercise.
What's missing
This contract is a primitive, not a product. Real messaging adds:
- Encryption. Anything written to chain is public. To send a private message you'd encrypt the plaintext to the recipient's Dilithium3 key off-chain and store the ciphertext.
- Spam control. Anyone can write to anyone's inbox. Real systems add a per-sender stake, a per-recipient allowlist, or a fee paid to the recipient.
- Read receipts and deletion. Append-only is a constraint, not a feature. To "delete" a message you'd mark it deleted in a parallel map; storage doesn't shrink.