Building a Chat smart contract: simplifying Solana Development with Typescript

dvrvsimi
8 min readJan 7, 2025

--

· 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:

  1. 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!

toph beifong locking in!

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!

Sign up to discover human stories that deepen your understanding of the world.

--

--

dvrvsimi
dvrvsimi

Written by dvrvsimi

bme | 🦀 | ml/ai | tw | web3

No responses yet

Write a response