Browse Source

examples: Chat (#211)

Armani Ferrante 4 years ago
parent
commit
ab509b6580

+ 1 - 0
.travis.yml

@@ -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

+ 2 - 0
examples/chat/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/chat/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 12 - 0
examples/chat/migrations/deploy.js

@@ -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 - 0
examples/chat/programs/chat/Cargo.toml

@@ -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 - 0
examples/chat/programs/chat/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 100 - 0
examples/chat/programs/chat/src/lib.rs

@@ -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 - 0
examples/chat/tests/chat.js

@@ -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))
+        );
+      }
+    });
+  });
+});