Building a Chat smart contract: simplifying Solana Development with Typescript

· What is Poseidon and Why Does it Matter?
· Hands-on Tutorial: Building a Chat Program
∘ Prerequisites
· Understanding the Chat Program: Features and Structure
∘ Core Features
∘ Message Structure
· Diving In!
∘ Step 1: Creating the Project
∘ Step 2: Defining Program Structure
∘ Step 3: Implementing Core Instructions
∘ Step 4: Transpiling to Rust
∘ Step 5: Writing Tests
· Understanding the Code
· Conclusion
Picture this: you’re a developer eager to venture into building onchain programs on Solana. You’re then confronted with the seemingly insurmountable wall of learning both Rust and Anchor at the same time. For many, starting Solana development feels like preparing for an uphill climb, armed with nothing but determination.
Then comes Poseidon — a new way of creating Solana programs that transforms this climb into a smooth, scenic ride. With Poseidon, developers can harness the power of Solana’s blockchain using the familiar, approachable syntax of TypeScript without compromising performance or reliability.
What is Poseidon and Why Does it Matter?
Poseidon is a TypeScript-to-Rust transpiler specifically designed for Solana development. It aims to bridge the gap between traditional web development and blockchain programming by:
- Allowing developers to write Solana programs in TypeScript
- Automatically handling complex Rust-specific concepts
- Generating Anchor-compatible Rust code with familiar commands
- Maintaining the performance of regular Rust-based Anchor programs
For web developers looking to enter the Solana ecosystem, Poseidon significantly reduces the barrier to entry while ensuring the resulting programs are of good quality. production-ready.
Hands-on Tutorial: Building a Chat Program
To demonstrate Poseidon’s capabilities, let’s build a decentralized chat program. This hands-on example will showcase how Poseidon simplifies Solana development while maintaining core blockchain concepts.
Prerequisites
This guide assumes that you have some knowledge of blockchain development and have written some code before now. Before we begin, ensure you have the following installed:
Even though we will be building in TypeScript, it is necessary to install Rust because we need cargo
to install and build the Poseidon binary.
Understanding the Chat Program: Features and Structure
Before implementing, let’s break down the program's essential features and components. This will give us a clear roadmap of what we’re building and why these elements are fundamental to the program’s functionality.
Core Features
Our chat program will have the following core capabilities, enabling users to interact seamlessly while leveraging the benefits of decentralized architecture:
- Initialize a New Chat Board
- This feature sets up the foundation of the chat program, creating a dedicated on-chain space (chat board) where messages can be posted.
- Each chat board will have an admin (authority) responsible for managing the board.
2. Post Messages
- Users can add new messages to the chat board.
- Each message will be tied to the user’s public key and stored permanently on the blockchain.
3. Edit Existing Messages
- Users can modify the content of their previously posted messages.
- This ensures flexibility while maintaining on-chain data consistency.
4. Delete Messages
- Users can remove their messages from the board if needed.
- This feature provides an additional layer of control over posted content.
Message Structure
Every message in the program will consist of the following attributes:
- Title: A concise label summarizing the message’s content (e.g., “Meeting Update”).
- Content: The body of the message, contains detailed information.
- Author’s Public Key: The unique identifier of the message’s creator, ensuring accountability and enabling access control.
- Timestamp: The exact time the message was posted, allowing users to track the chronology of conversations.
Diving In!
We have made the necessary preparations, let’s bend some seemingly insurmountable walls!

Step 1: Creating the Project
Run poseidon --version
to confirm that the binary was properly built, then create a new Poseidon project:
poseidon init chat
cd chat
Step 2: Defining Program Structure
Navigate to ts-programs/src/chat.ts
and define the program structure:
import {
Account,
Pubkey,
type Result,
i64,
u8,
Signer,
string
} from "@solanaturbine/poseidon";
export default class ChatProgram {
static PROGRAM_ID = new Pubkey("11111111111111111111111111111111");
initialize(): Result {}
postMessage(): Result {}
editMessage(): Result {}
deleteMessage(): Result {}
}
export interface Message extends Account {
author: Pubkey; // Message author's public key
title: string; // Message title
content: string; // Message content
timestamp: i64; // Posted timestamp
bump: u8; // PDA bump
}
export interface BoardState extends Account {
authority: Pubkey; // Board admin
messageCount: i64; // Total number of messages
bump: u8; // PDA bump
}
This scaffold implements the explanation made earlier. The BoardState account tracks and manages the chat board’s overall state, ensuring all operations (posting, editing, and deleting messages) are organized around a single, consistent data structure.
Step 3: Implementing Core Instructions
Let’s implement each instruction one by one:
Initialize Board
initialize(
authority: Signer,
boardState: BoardState
): Result {
boardState.derive(["board"])
.init(authority);
boardState.authority = authority.key;
boardState.messageCount = new i64(0);
boardState.bump = boardState.getBump(); // remember these three?
}
Post Message
postMessage(
author: Signer,
message: Message,
boardState: BoardState,
title: Str<64>,
content: Str<1024>
): Result {
boardState.derive(["board"]);
message.derive([
"message",
boardState.messageCount.toBytes(),
author.key
]).init(author);
message.author = author.key;
message.title = title;
message.content = content;
message.timestamp = new i64(Date.now());
message.bump = message.getBump(); // declaring types from the Message struct
boardState.messageCount = boardState.messageCount.add(1); // updating count
}
Edit Message
editMessage(
author: Signer,
message: Message,
boardState: BoardState,
newTitle: Str<64>,
newContent: Str<1024>
): Result {
message.derive(["message", message.messageIndex.toBytes(), author.key])
.has([ author ])
message.title = newTitle;
message.content = newContent;
}
Delete Message
deleteMessage(
author: Signer,
message: Message,
boardState: BoardState
): Result {
boardState.derive(["board"]);
message.derive([
"message",
message.messageIndex.toBytes(),
author.key
])
.has([ author ])
.close(author); // to close the account
}
Step 4: Transpiling to Rust

To convert the Typescript code to Rust, run poseidon build
. If everything is set up properly, you should see the output in /programs/chat/lib.rs
. See the directory structure below:
├── Anchor.toml
├── Cargo.toml
├── app
├── migrations
│ └── deploy.ts
├── package.json
├── programs
│ └── chat
│ ├── Cargo.toml
│ ├── Xargo.toml
│ └── src
│ └── lib.rs ← — — — — Output Rust file
├── target
│ └── deploy
│ └── chat_program.json
├── tests
│ └── chat.ts
├── ts-programs
│ ├── package.json
│ └── src
│ └── chat.ts ← — — — — Input Typescript file
├── tsconfig.json
└── yarn.lock
You can now build your chat program using anchor build
, the same command used to build a Rust-based Anchor program. After the build finishes, note the path to the newly created types
and idl
files in the target
folder.
Step 5: Writing Tests
Tests are not the easiest to write but they are important to verify that the logic works as expected. Go to tests/chat.ts
in your chat
folder and paste the code below. Run poseidon test
to verify that it works:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Chat } from "../target/types/chat"; // install these with your package manager
import { assert } from "chai";
describe("chat program", () => {
// Configure the client to use the local cluster
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Chat as Program<Chat>;
// Generate the program derived address for our board state
const [boardPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("board")],
program.programId
);
it("Initializes the board", async () => {
const tx = await program.methods
.initialize()
.accounts({
authority: provider.wallet.publicKey,
boardState: boardPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Initialize transaction:", tx);
// Fetch the created board state
const boardState = await program.account.boardState.fetch(boardPda);
assert.ok(boardState.authority.equals(provider.wallet.publicKey));
assert.ok(boardState.messageCount.eq(new anchor.BN(0)));
});
it("Post a message", async () => {
// Generate PDA for the message using message count as seed
const [messagePda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("message"),
new anchor.BN(0).toArrayLike(Buffer, "le", 8),
provider.wallet.publicKey.toBuffer()
],
program.programId
);
const title = "First Message";
const content = "Hello, Solana!";
const tx = await program.methods
.postMessage(title, content)
.accounts({
author: provider.wallet.publicKey,
message: messagePda,
boardState: boardPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Post message transaction:", tx);
// Fetch and verify the message
const message = await program.account.message.fetch(messagePda);
assert.ok(message.author.equals(provider.wallet.publicKey));
assert.equal(message.title, title);
assert.equal(message.content, content);
assert.ok(message.messageIndex.eq(new anchor.BN(0)));
// Verify board state was updated
const boardState = await program.account.boardState.fetch(boardPda);
assert.ok(boardState.messageCount.eq(new anchor.BN(1)));
});
it("Edit a message", async () => {
// Use same PDA from the previous test
const [messagePda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("message"),
new anchor.BN(0).toArrayLike(Buffer, "le", 8),
provider.wallet.publicKey.toBuffer()
],
program.programId
);
const newTitle = "Updated Title";
const newContent = "Updated content!";
const tx = await program.methods
.editMessage(newTitle, newContent)
.accounts({
author: provider.wallet.publicKey,
message: messagePda,
boardState: boardPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Edit message transaction:", tx);
// Verify the message was updated
const message = await program.account.message.fetch(messagePda);
assert.equal(message.title, newTitle);
assert.equal(message.content, newContent);
});
it("Delete a message", async () => {
// Use same PDA from the previous tests
const [messagePda] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("message"),
new anchor.BN(0).toArrayLike(Buffer, "le", 8),
provider.wallet.publicKey.toBuffer()
],
program.programId
);
const tx = await program.methods
.deleteMessage()
.accounts({
author: provider.wallet.publicKey,
message: messagePda,
boardState: boardPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("Delete message transaction:", tx);
// Verify the message account was closed
try {
await program.account.message.fetch(messagePda);
assert.fail("Message account should have been closed");
} catch (e) {
// Expected error - account not found
assert.ok(e);
}
});
});
Understanding the Code
The ChatProgram
leverages key Solana concepts to ensure robust and efficient functionality. PDAs are used to create deterministic addresses for the board state and messages, with the derive()
method handling their creation using appropriate seeds and storing bump values for future reference. Account management ensures that message accounts are initialized withinit()
, rent is automatically managed by Anchor, and closing accounts returns rent to the designated recipient. State management is centralized in theBoardState
, which tracks the total message count, while individual messages store content and metadata, with all changes being persistently recorded on-chain. Finally, access control ensures only message authors can edit or delete their messages through simple equality checks, effectively enforcing ownership and maintaining program integrity.
If you need further help, consult the official docs.
Poseidon is being actively developed to ensure that developers can use advanced features like you would write in vanilla Rust. Run
poseidon --help
to see other commands and what they do. You can find issues or open a PR for a new feature.
Conclusion
Poseidon represents a significant step forward in making Solana development more accessible to TypeScript developers. Through our chat program example, we’ve seen how Poseidon maintains the power of Solana’s native features while providing a familiar development experience.
The ability to write Solana programs in TypeScript without sacrificing performance or security makes Poseidon an invaluable tool for:
- Web developers transitioning to blockchain development
- Teams looking to accelerate their Solana development process
- Developers who want to leverage their existing TypeScript knowledge
As the Solana ecosystem continues to grow, tools like Poseidon will play a crucial role in lowering the barrier to entry and enabling more developers to build on the platform.
Thank you for reading! Prost!