From ab14876342bfc863fed47c2140db75ce717f44a4 Mon Sep 17 00:00:00 2001 From: sharmaster96 Date: Thu, 29 Aug 2019 13:49:13 -0700 Subject: [PATCH 1/3] Sync messaging tutorial --- .circleci/config.yml | 8 + hello-world/README.md | 1 + message-board/README.md | 3 + message-board/app/package.json | 39 +++ message-board/app/scripts/deploy_service.js | 18 + message-board/app/src/index.ts | 0 message-board/app/test/service.spec.ts | 21 ++ message-board/app/tsconfig.json | 13 + message-board/app/tslint.json | 11 + message-board/service/.rustfmt.toml | 64 ++++ message-board/service/Cargo.toml | 14 + message-board/service/src/main.rs | 350 ++++++++++++++++++++ 12 files changed, 542 insertions(+) create mode 100644 message-board/README.md create mode 100644 message-board/app/package.json create mode 100755 message-board/app/scripts/deploy_service.js create mode 100644 message-board/app/src/index.ts create mode 100644 message-board/app/test/service.spec.ts create mode 100644 message-board/app/tsconfig.json create mode 100644 message-board/app/tslint.json create mode 100644 message-board/service/.rustfmt.toml create mode 100644 message-board/service/Cargo.toml create mode 100644 message-board/service/src/main.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index fc81ca2..e0dc073 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,6 +41,14 @@ jobs: name: Test Secret Ballot working_directory: ballot/service command: oasis test + - run: + name: Build MessageBoard + working_directory: message-board/service + command: oasis build + - run: + name: Test MessageBoard + working_directory: message-board/service + command: oasis test build_ballot: executor: rust steps: diff --git a/hello-world/README.md b/hello-world/README.md index 6a8d9f0..d493d60 100644 --- a/hello-world/README.md +++ b/hello-world/README.md @@ -1,2 +1,3 @@ # hello-world + Check out our [Hello World Tutorial](https://docs.oasis.dev/quickstart.html)! \ No newline at end of file diff --git a/message-board/README.md b/message-board/README.md new file mode 100644 index 0000000..6e8aab3 --- /dev/null +++ b/message-board/README.md @@ -0,0 +1,3 @@ +# message-board + + Check out our [Messaging Tutorial](https://docs.oasis.dev/tutorials/messaging.html)! \ No newline at end of file diff --git a/message-board/app/package.json b/message-board/app/package.json new file mode 100644 index 0000000..87dc8d7 --- /dev/null +++ b/message-board/app/package.json @@ -0,0 +1,39 @@ +{ + "name": "message_board", + "version": "1.0.0", + "main": "dist/index.js", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "directories": { + "test": "test", + "bin": "scripts" + }, + "scripts": { + "build": "tsc", + "test": "jest", + "deploy": "node scripts/deploy_service.js", + "clean": "rimraf dist", + "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", + "lint:fix": "npm run-script lint -- --fix" + }, + "devDependencies": { + "@types/jest": "^24.0.15", + "chalk": "^2.4.2", + "jest": "^24.8.0", + "prettier": "^1.18.2", + "rimraf": "^2.6.3", + "ts-jest": "^24.0.2", + "ts-node": "^8.3.0", + "tslint": "^5.18.0", + "tslint-config-prettier": "^1.18.0", + "tslint-config-standard": "^8.0.1", + "typescript": "^3.5.3" + }, + "dependencies": { + "@oasislabs/client": "^1.0.0-rc.13" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node" + } +} diff --git a/message-board/app/scripts/deploy_service.js b/message-board/app/scripts/deploy_service.js new file mode 100755 index 0000000..4bfb2a8 --- /dev/null +++ b/message-board/app/scripts/deploy_service.js @@ -0,0 +1,18 @@ +const chalk = require('chalk'); +const oasis = require('@oasislabs/client'); + +oasis.workspace.MessageBoard.deploy({ + header: {confidential: false}, +}) + .then(res => { + let addrHex = Buffer.from(res._inner.address).toString('hex'); + console.log(` ${chalk.green('Deployed')} MessageBoard at 0x${addrHex}`); + }) + .catch(err => { + console.error( + `${chalk.red('error')}: could not deploy MessageBoard: ${err.message}`, + ); + }) + .finally(() => { + oasis.disconnect(); + }); diff --git a/message-board/app/src/index.ts b/message-board/app/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/message-board/app/test/service.spec.ts b/message-board/app/test/service.spec.ts new file mode 100644 index 0000000..fb29452 --- /dev/null +++ b/message-board/app/test/service.spec.ts @@ -0,0 +1,21 @@ +import oasis from '@oasislabs/client'; + +jest.setTimeout(20000); + +describe('MessageBoard', () => { + let service; + + beforeAll(async () => { + service = await oasis.workspace.MessageBoard.deploy({ + header: {confidential: false}, + }); + }); + + it('deployed', async () => { + expect(service).toBeTruthy(); + }); + + afterAll(() => { + oasis.disconnect(); + }); +}); diff --git a/message-board/app/tsconfig.json b/message-board/app/tsconfig.json new file mode 100644 index 0000000..322152a --- /dev/null +++ b/message-board/app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "esModuleInterop": true, + "module": "es6", + "moduleResolution": "node", + "noImplicitAny": false, + "outDir": "./dist", + "sourceMap": true, + "strict": true + }, + "include": ["src"] +} diff --git a/message-board/app/tslint.json b/message-board/app/tslint.json new file mode 100644 index 0000000..1c138f3 --- /dev/null +++ b/message-board/app/tslint.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "tslint-config-standard", + "tslint-config-prettier" + ], + "rules": { + "semicolon": [true, "always"], + "quotemark": [true, "single"], + "no-trailing-whitespace": true + } +} diff --git a/message-board/service/.rustfmt.toml b/message-board/service/.rustfmt.toml new file mode 100644 index 0000000..78fb339 --- /dev/null +++ b/message-board/service/.rustfmt.toml @@ -0,0 +1,64 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Auto" +use_small_heuristics = "Default" +indent_style = "Block" +wrap_comments = false +format_code_in_doc_comments = true +comment_width = 80 +normalize_comments = false +normalize_doc_attributes = false +format_strings = false +format_macro_matchers = false +format_macro_bodies = true +empty_item_single_line = true +struct_lit_single_line = true +fn_single_line = false +where_single_line = false +imports_indent = "Block" +imports_layout = "Mixed" +merge_imports = true +reorder_imports = true +reorder_modules = true +reorder_impl_items = false +type_punctuation_density = "Wide" +space_before_colon = false +space_after_colon = true +spaces_around_ranges = false +binop_separator = "Front" +remove_nested_parens = true +combine_control_expr = true +overflow_delimited_expr = false +struct_field_align_threshold = 0 +enum_discrim_align_threshold = 0 +match_arm_blocks = true +force_multiline_blocks = false +fn_args_layout = "Tall" +brace_style = "SameLineWhere" +control_brace_style = "AlwaysSameLine" +trailing_semicolon = true +trailing_comma = "Vertical" +match_block_trailing_comma = false +blank_lines_upper_bound = 1 +blank_lines_lower_bound = 0 +edition = "2018" +version = "One" +inline_attribute_width = 0 +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true +condense_wildcard_suffixes = false +color = "Auto" +unstable_features = false +disable_all_formatting = false +skip_children = false +hide_parse_errors = false +error_on_line_overflow = false +error_on_unformatted = false +report_todo = "Never" +report_fixme = "Never" +ignore = [] +emit_mode = "Files" +make_backup = false diff --git a/message-board/service/Cargo.toml b/message-board/service/Cargo.toml new file mode 100644 index 0000000..9fa025f --- /dev/null +++ b/message-board/service/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "message-board" +version = "0.1.0" +authors = ["Oasis Labs "] +edition = "2018" +publish = false + +[dependencies] +map_vec = "0.2" +oasis-std = "0.2" +serde = { version = "1.0", features = ["derive"] } + +[dev-dependencies] +oasis-test = "0.2" diff --git a/message-board/service/src/main.rs b/message-board/service/src/main.rs new file mode 100644 index 0000000..b4a7f28 --- /dev/null +++ b/message-board/service/src/main.rs @@ -0,0 +1,350 @@ +#[macro_use] +extern crate serde; // Provides `Serialize` and `Deserialize`. + +use map_vec::{Map, Set}; +use oasis_std::{Address, Context, Event}; + +pub type UserId = Address; +pub type PostId = u32; + +#[derive(oasis_std::Service)] +pub struct MessageBoard { + /// The administrators of this message board. + /// Administrators can add and remove users. + admins: Set, + + /// The maximum number of characters in a post. For instance, 280. + bcast_char_limit: Option, + + /// All of the posts made to this message board. + posts: Vec, + + /// All accounts which have ever participated in this message board. + accounts: Map, +} + +// Types used in the state struct must derive serde `Serialize` and `Deserialize` +// so that they can be persisted and loaded from storage. They must also derive `Clone` +// (and optionally `Copy`) so that service RPC methods can accept borrowed data which +// improves deserialization performance. +// +// Types do not need to be defined in the same module as the service. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Post { + author: UserId, + text: String, + comments: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct Account { + inbox: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct Message { + from: UserId, + text: String, +} + +// An event is a struct that derives `Event`. Calling `Event::emit` on the struct +// will generate a log that can be picked up by anyone watching the blockchain. +// Annotating a field with `#[indexed]` will allow clients to efficiently search for +// events with a particular value of the indexed field. Up to three events may be indexed. +// +// A confidential application will want to encrypt the contents of the event to a +// particular recipent. +// A highly confidential application will probably not want to emit events at all since +// the fact that an event was even emitted can leak information. (It can be done using +// techniques from oblivious transfer, but it requires extreme care to do properly.) +#[derive(Serialize, Deserialize, Event)] +pub struct MessagePosted { + #[indexed] + pub author: UserId, + #[indexed] + pub recipient: Option, +} + +type Result = std::result::Result; + +#[derive(Serialize, Deserialize, Debug)] +pub enum Error { + InvalidUserId, + InvalidPostId, + PermissionDenied, + MessageTooLong, +} + +impl MessageBoard { + /// Constructs a new `MessageBoard`. + pub fn new(ctx: &Context, bcast_char_limit: Option) -> Self { + let mut admins = Set::new(); + admins.insert(ctx.sender()); + + let mut accounts = Map::new(); + accounts.insert(ctx.sender(), Account::default()); + + Self { + admins, + bcast_char_limit, + accounts, + posts: Vec::new(), + } + } + + /// Adds a user to this message board. + /// Can only be used by an admin. + pub fn add_user(&mut self, ctx: &Context, user_id: UserId) -> Result<()> { + if !self.admins.contains(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + self.accounts.entry(user_id).or_default(); + Ok(()) + } + + /// Removes a user from this message board. + /// Can only be used by an admin. + pub fn remove_user(&mut self, ctx: &Context, user_id: UserId) -> Result<()> { + if !self.admins.contains(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + self.accounts.remove(&user_id); + Ok(()) + } +} + +impl MessageBoard { + /// Make a post to this message board. Only registered accounts may post. + /// Returns the id of the post. + pub fn post(&mut self, ctx: &Context, text: String) -> Result { + if !self.accounts.contains_key(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + + if let Some(bcast_char_limit) = self.bcast_char_limit { + if text.len() > bcast_char_limit as usize { + return Err(Error::MessageTooLong); + } + } + + self.posts.push(Post { + author: ctx.sender(), + text, + comments: Vec::new(), + }); + + Event::emit(&MessagePosted { + author: ctx.sender(), + recipient: None, + }); + + Ok(self.posts.len() as u32 - 1) + } + + /// Returns all posts made during a given interval. + pub fn posts( + &self, + ctx: &Context, + range: (Option, Option), + ) -> Result> { + if !self.accounts.contains_key(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + let start = range.0.unwrap_or_default() as usize; + let stop = std::cmp::min( + range + .1 + .map(|s| s as usize) + .unwrap_or_else(|| self.posts.len()), + self.posts.len(), + ); + Ok(self + .posts + .get(start..stop) + .map(<[Post]>::to_vec) + .unwrap_or_default()) + } + + /// Add a comment to a post. + pub fn comment(&mut self, ctx: &Context, post_id: PostId, text: String) -> Result<()> { + if !self.accounts.contains_key(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + if post_id as usize >= self.posts.len() { + return Err(Error::InvalidPostId); + } + self.posts[post_id as usize].comments.push(Message { + from: ctx.sender(), + text, + }); + Ok(()) + } +} + +impl MessageBoard { + pub fn send_dm(&mut self, ctx: &Context, recipient: UserId, text: String) -> Result<()> { + if !self.accounts.contains_key(&ctx.sender()) { + return Err(Error::PermissionDenied); + } + match self.accounts.get_mut(&recipient) { + Some(recip) => { + recip.inbox.push(Message { + from: ctx.sender(), + text, + }); + Event::emit(&MessagePosted { + author: ctx.sender(), + recipient: None, + }); + Ok(()) + } + None => Err(Error::InvalidUserId), + } + } + + /// Retrieves all messages from the sender's inbox. The inbox is emptied by this operation. + pub fn fetch_inbox(&mut self, ctx: &Context) -> Vec { + self.accounts + .get_mut(&ctx.sender()) + .map(|acct| std::mem::replace(&mut acct.inbox, Vec::new())) + .unwrap_or_default() + } +} + +fn main() { + oasis_std::service!(MessageBoard); +} + +#[cfg(test)] +mod tests { + extern crate oasis_test; + + use super::*; + + /// Creates a new account and a `Context` with the new account as the sender. + fn create_account() -> (Address, Context) { + let addr = oasis_test::create_account(0 /* initial balance */); + let ctx = Context::default().with_sender(addr).with_gas(100_000); + (addr, ctx) + } + + #[test] + fn post_nolimit() { + let (_admin, actx) = create_account(); + let mut mb = MessageBoard::new(&actx, None); + mb.post(&actx, "👏".repeat(9999)).unwrap(); + } + + #[test] + fn post_limit() { + let (_admin, actx) = create_account(); + let mut mb = MessageBoard::new(&actx, Some(1)); + mb.post(&actx, "?!".to_string()).unwrap_err(); + mb.post(&actx, "!".to_string()).unwrap(); + } + + #[test] + fn posts() { + let (admin, actx) = create_account(); + let (user, uctx) = create_account(); + + let mut mb = MessageBoard::new(&actx, Some(140)); + + let first_post_text = "f1r5t p0st!!1!".to_string(); + mb.post(&actx, first_post_text.clone()).unwrap(); + + // no permission + mb.post(&uctx, "Add me plz".to_string()).unwrap_err(); + mb.posts(&uctx, (None, None)).unwrap_err(); + mb.add_user(&uctx, user).unwrap_err(); + + assert_eq!( + mb.posts(&actx, (Some(0), Some(9))).unwrap(), + vec![Post { + author: admin, + text: first_post_text, + comments: Vec::new() + }] + ); + + // user can now post + mb.add_user(&actx, user).unwrap(); + let second_post_text = "All your base are belong to me".to_string(); + mb.post(&uctx, second_post_text.clone()).unwrap(); + + assert_eq!( + mb.posts(&uctx, (Some(1), None)).unwrap(), + vec![Post { + author: user, + text: second_post_text, + comments: Vec::new() + }] + ); + + let comment_text = "gtfo".to_string(); + mb.comment(&actx, 0 /* post_id */, comment_text.clone()) + .unwrap(); + + assert_eq!( + mb.posts(&actx, (Some(0), Some(1))).unwrap()[0].comments, + vec![Message { + from: admin, + text: comment_text + }] + .as_slice() + ); + + mb.remove_user(&uctx, user).unwrap_err(); + mb.remove_user(&actx, user).unwrap(); + mb.post(&uctx, "Might I ask where you keep the spoons?".to_string()) + .unwrap_err(); + mb.posts(&uctx, (None, None)).unwrap_err(); + } + + #[test] + fn dm() { + let (kiltavi, kctx) = create_account(); + let (joe, jctx) = create_account(); + + let mut mb = MessageBoard::new(&kctx, Some(140)); + + mb.send_dm(&jctx, kiltavi, "hello".to_string()).unwrap_err(); + mb.add_user(&kctx, joe).unwrap(); + mb.send_dm(&jctx, kiltavi, "hello".to_string()).unwrap(); + mb.send_dm(&jctx, kiltavi, "can I have some eth?".to_string()) + .unwrap(); + + assert_eq!( + mb.fetch_inbox(&kctx), + vec![ + Message { + from: joe, + text: "hello".to_string() + }, + Message { + from: joe, + text: "can I have some eth?".to_string() + } + ] + ); + assert_eq!(mb.fetch_inbox(&kctx), Vec::new()); + + mb.send_dm(&kctx, joe, "No.".to_string()).unwrap(); + assert_eq!( + mb.fetch_inbox(&jctx), + vec![Message { + from: kiltavi, + text: "No.".to_string() + },] + ); + mb.send_dm(&kctx, joe, "I am a non-giver of eth.".to_string()) + .unwrap(); + assert_eq!( + mb.fetch_inbox(&jctx), + vec![Message { + from: kiltavi, + text: "I am a non-giver of eth.".to_string() + },] + ); + } +} From 1c89d1b751289b1a0cc8c3c8734c895683e94785 Mon Sep 17 00:00:00 2001 From: sharmaster96 Date: Thu, 29 Aug 2019 14:42:30 -0700 Subject: [PATCH 2/3] modify ci --- .circleci/config.yml | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0dc073..30e2ffb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,6 +18,20 @@ commands: apt-get -qq update apt-get -qq install python curl git gcc curl --proto '=https' --tlsv1.2 -sSL https://get.oasis.dev | python + build_and_test_service: + description: "Builds and tests a tutorial Rust service" + parameters: + tutorial: + type: string + steps: + - run: + name: Build << parameters.tutorial >> + working_directory: << parameters.tutorial >>/service + command: oasis build + - run: + name: Test << parameters.tutorial >> + working_directory: << parameters.tutorial >>/service + command: oasis test jobs: build_test: @@ -25,30 +39,12 @@ jobs: steps: - checkout - install_oasis - - run: - name: Build Hello World - working_directory: hello-world/service - command: oasis build - - run: - name: Test Hello World - working_directory: hello-world/service - command: oasis test - - run: - name: Build Secret Ballot - working_directory: ballot/service - command: oasis build - - run: - name: Test Secret Ballot - working_directory: ballot/service - command: oasis test - - run: - name: Build MessageBoard - working_directory: message-board/service - command: oasis build - - run: - name: Test MessageBoard - working_directory: message-board/service - command: oasis test + - build_and_test_service: + tutorial: hello-world + - build_and_test_service: + tutorial: ballot + - build_and_test_service: + tutorial: message-board build_ballot: executor: rust steps: From a383333cebb555b1dcccda15df466983a2535559 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Wed, 4 Sep 2019 18:38:30 +0000 Subject: [PATCH 3/3] Bump oasis client version --- ballot/app/package.json | 2 +- hello-world/app/package.json | 2 +- message-board/app/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ballot/app/package.json b/ballot/app/package.json index 83001a4..da88f88 100644 --- a/ballot/app/package.json +++ b/ballot/app/package.json @@ -8,7 +8,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { - "@oasislabs/client": "^1.0.0-rc.13", + "@oasislabs/client": "^1.0.0-rc.15", "apexcharts": "^3.8.2", "core-js": "^3.2.1", "oasis-style": "^1.0.0-rc1", diff --git a/hello-world/app/package.json b/hello-world/app/package.json index 1e2c148..42854e4 100644 --- a/hello-world/app/package.json +++ b/hello-world/app/package.json @@ -30,7 +30,7 @@ "typescript": "^3.5.3" }, "dependencies": { - "@oasislabs/client": "^1.0.0-rc.13" + "@oasislabs/client": "^1.0.0-rc.15" }, "jest": { "preset": "ts-jest", diff --git a/message-board/app/package.json b/message-board/app/package.json index 87dc8d7..4dd44ed 100644 --- a/message-board/app/package.json +++ b/message-board/app/package.json @@ -30,7 +30,7 @@ "typescript": "^3.5.3" }, "dependencies": { - "@oasislabs/client": "^1.0.0-rc.13" + "@oasislabs/client": "^1.0.0-rc.15" }, "jest": { "preset": "ts-jest",