examples: Chat (#211)
This commit is contained in:
parent
016e2c3d4c
commit
ab509b6580
|
@ -63,6 +63,7 @@ jobs:
|
||||||
- <<: *examples
|
- <<: *examples
|
||||||
name: Runs the examples 2
|
name: Runs the examples 2
|
||||||
script:
|
script:
|
||||||
|
- pushd examples/chat && yarn && anchor test && popd
|
||||||
- pushd examples/tutorial/basic-0 && anchor test && popd
|
- pushd examples/tutorial/basic-0 && anchor test && popd
|
||||||
- pushd examples/tutorial/basic-1 && anchor test && popd
|
- pushd examples/tutorial/basic-1 && anchor test && popd
|
||||||
- pushd examples/tutorial/basic-2 && anchor test && popd
|
- pushd examples/tutorial/basic-2 && anchor test && popd
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
cluster = "localnet"
|
||||||
|
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
|
@ -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.
|
||||||
|
}
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -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,
|
||||||
|
}
|
|
@ -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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue