Skip to content

Commit

Permalink
examples: Chat (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
armaniferrante authored Apr 18, 2021
1 parent 016e2c3 commit ab509b6
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
- <<: *examples
name: Runs the examples 2
script:
- pushd examples/chat && yarn && anchor test && popd
- pushd examples/tutorial/basic-0 && anchor test && popd
- pushd examples/tutorial/basic-1 && anchor test && popd
- pushd examples/tutorial/basic-2 && anchor test && popd
Expand Down
2 changes: 2 additions & 0 deletions examples/chat/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cluster = "localnet"
wallet = "~/.config/solana/id.json"
4 changes: 4 additions & 0 deletions examples/chat/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]
12 changes: 12 additions & 0 deletions examples/chat/migrations/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Migrations are an early feature. Currently, they're nothing more than this
// single deploy script that's invoked from the CLI, injecting a provider
// configured from the workspace's Anchor.toml.

const anchor = require("@project-serum/anchor");

module.exports = async function (provider) {
// Configure client to use the provider.
anchor.setProvider(provider);

// Add your deploy script here.
}
18 changes: 18 additions & 0 deletions examples/chat/programs/chat/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "chat"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"

[lib]
crate-type = ["cdylib", "lib"]
name = "chat"

[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { path = "../../../../lang" }
2 changes: 2 additions & 0 deletions examples/chat/programs/chat/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
100 changes: 100 additions & 0 deletions examples/chat/programs/chat/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! A simple chat program using a ring buffer to store messages.
use anchor_lang::prelude::*;

#[program]
pub mod chat {
use super::*;

pub fn create_user(ctx: Context<CreateUser>, name: String) -> Result<()> {
ctx.accounts.user.name = name;
ctx.accounts.user.authority = *ctx.accounts.authority.key;
Ok(())
}
pub fn create_chat_room(ctx: Context<CreateChatRoom>, name: String) -> Result<()> {
let given_name = name.as_bytes();
let mut name = [0u8; 280];
name[..given_name.len()].copy_from_slice(given_name);
let mut chat = ctx.accounts.chat_room.load_init()?;
chat.name = name;
Ok(())
}
pub fn send_message(ctx: Context<SendMessage>, msg: String) -> Result<()> {
let mut chat = ctx.accounts.chat_room.load_mut()?;
chat.append({
let src = msg.as_bytes();
let mut data = [0u8; 280];
data[..src.len()].copy_from_slice(src);
Message {
from: *ctx.accounts.user.to_account_info().key,
data,
}
});
Ok(())
}
}

#[derive(Accounts)]
pub struct CreateUser<'info> {
#[account(associated = authority, space = "312")]
user: ProgramAccount<'info, User>,
#[account(signer)]
authority: AccountInfo<'info>,
rent: Sysvar<'info, Rent>,
system_program: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct CreateChatRoom<'info> {
#[account(init)]
chat_room: Loader<'info, ChatRoom>,
rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct SendMessage<'info> {
#[account(has_one = authority)]
user: ProgramAccount<'info, User>,
#[account(signer)]
authority: AccountInfo<'info>,
#[account(mut)]
chat_room: Loader<'info, ChatRoom>,
}

#[associated]
pub struct User {
name: String,
authority: Pubkey,
}

#[account(zero_copy)]
pub struct ChatRoom {
head: u64,
tail: u64,
name: [u8; 280], // Human readable name (char bytes).
messages: [Message; 33607], // Leaves the account at 10,485,680 bytes.
}

impl ChatRoom {
fn append(&mut self, msg: Message) {
self.messages[ChatRoom::index_of(self.head)] = msg;
if ChatRoom::index_of(self.head + 1) == ChatRoom::index_of(self.tail) {
self.tail += 1;
}
self.head += 1;
}
fn index_of(counter: u64) -> usize {
std::convert::TryInto::try_into(counter % 10000).unwrap()
}
}

#[zero_copy]
pub struct Message {
pub from: Pubkey,
pub data: [u8; 280],
}

#[error]
pub enum ErrorCode {
Unknown,
}
99 changes: 99 additions & 0 deletions examples/chat/tests/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const anchor = require("@project-serum/anchor");
const assert = require("assert");

describe("chat", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.Provider.env());

// Program client handle.
const program = anchor.workspace.Chat;

// Chat room account.
const chatRoom = new anchor.web3.Account();

it("Creates a chat room", async () => {
// Add your test here.

await program.rpc.createChatRoom("Test Chat", {
accounts: {
chatRoom: chatRoom.publicKey,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
},
instructions: [
await program.account.chatRoom.createInstruction(chatRoom),
],
signers: [chatRoom],
});

const chat = await program.account.chatRoom(chatRoom.publicKey);
const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
assert.ok(chat.messages.length === 33607);
assert.ok(chat.head.toNumber() === 0);
assert.ok(chat.tail.toNumber() === 0);
});

it("Creates a user", async () => {
const authority = program.provider.wallet.publicKey;
await program.rpc.createUser("My User", {
accounts: {
user: await program.account.user.associatedAddress(authority),
authority,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
systemProgram: anchor.web3.SystemProgram.programId,
},
});
const account = await program.account.user.associated(authority);
assert.ok(account.name === "My User");
assert.ok(account.authority.equals(authority));
});

it("Sends messages", async () => {
const authority = program.provider.wallet.publicKey;
const user = await program.account.user.associatedAddress(authority);

// Only send a couple messages so the test doesn't take an eternity.
const numMessages = 10;

// Generate random message strings.
const messages = new Array(numMessages).fill("").map((msg) => {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
});

// Send each message.
for (let k = 0; k < numMessages; k += 1) {
console.log("Sending message " + k);
await program.rpc.sendMessage(messages[k], {
accounts: {
user,
authority,
chatRoom: chatRoom.publicKey,
},
});
}

// Check the chat room state is as expected.
const chat = await program.account.chatRoom(chatRoom.publicKey);
const name = new TextDecoder("utf-8").decode(new Uint8Array(chat.name));
assert.ok(name.startsWith("Test Chat")); // [u8; 280] => trailing zeros.
assert.ok(chat.messages.length === 33607);
assert.ok(chat.head.toNumber() === numMessages);
assert.ok(chat.tail.toNumber() === 0);
chat.messages.forEach((msg, idx) => {
if (idx < 10) {
const data = new TextDecoder("utf-8").decode(new Uint8Array(msg.data));
console.log("Message", data);
assert.ok(msg.from.equals(user));
assert.ok(data.startsWith(messages[idx]));
} else {
assert.ok(new anchor.web3.PublicKey());
assert.ok(
JSON.stringify(msg.data) === JSON.stringify(new Array(280).fill(0))
);
}
});
});
});

0 comments on commit ab509b6

Please sign in to comment.