From 5d5b6715f58acd2fedb8bb55f8bb11908633aa96 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Tue, 27 Aug 2024 21:33:25 +0800 Subject: [PATCH] feat: add `ic_message_channel` --- Cargo.lock | 276 +++++++-- Cargo.toml | 4 + Makefile | 11 +- src/ic_message/Cargo.toml | 18 + src/ic_message/ic_message.did | 4 + src/ic_message/src/api_init.rs | 70 +++ src/ic_message/src/lib.rs | 42 ++ src/ic_message/src/store.rs | 129 ++++ src/ic_message_channel/Cargo.toml | 19 + src/ic_message_channel/ic_message_channel.did | 142 +++++ src/ic_message_channel/src/api_admin.rs | 35 ++ src/ic_message_channel/src/api_init.rs | 70 +++ src/ic_message_channel/src/api_query.rs | 65 ++ src/ic_message_channel/src/api_update.rs | 180 ++++++ src/ic_message_channel/src/lib.rs | 49 ++ src/ic_message_channel/src/store.rs | 584 ++++++++++++++++++ src/ic_message_channel/src/types.rs | 205 ++++++ src/ic_message_profile/Cargo.toml | 19 + src/ic_message_profile/ic_message_profile.did | 4 + src/ic_message_profile/src/api_init.rs | 70 +++ src/ic_message_profile/src/lib.rs | 42 ++ src/ic_message_profile/src/store.rs | 127 ++++ src/ic_panda_infra/src/lib.rs | 4 + 23 files changed, 2105 insertions(+), 64 deletions(-) create mode 100644 src/ic_message/Cargo.toml create mode 100644 src/ic_message/ic_message.did create mode 100644 src/ic_message/src/api_init.rs create mode 100644 src/ic_message/src/lib.rs create mode 100644 src/ic_message/src/store.rs create mode 100644 src/ic_message_channel/Cargo.toml create mode 100644 src/ic_message_channel/ic_message_channel.did create mode 100644 src/ic_message_channel/src/api_admin.rs create mode 100644 src/ic_message_channel/src/api_init.rs create mode 100644 src/ic_message_channel/src/api_query.rs create mode 100644 src/ic_message_channel/src/api_update.rs create mode 100644 src/ic_message_channel/src/lib.rs create mode 100644 src/ic_message_channel/src/store.rs create mode 100644 src/ic_message_channel/src/types.rs create mode 100644 src/ic_message_profile/Cargo.toml create mode 100644 src/ic_message_profile/ic_message_profile.did create mode 100644 src/ic_message_profile/src/api_init.rs create mode 100644 src/ic_message_profile/src/lib.rs create mode 100644 src/ic_message_profile/src/store.rs diff --git a/Cargo.lock b/Cargo.lock index dcf5228..8c864f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -458,9 +493,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "shlex", ] @@ -513,6 +548,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.16" @@ -582,6 +627,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "coset" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpufeatures" version = "0.2.13" @@ -650,9 +705,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1234,6 +1299,16 @@ dependencies = [ "wasi", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.29.0" @@ -1642,6 +1717,69 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de254dd67bbd58073e23dc1c8553ba12fa1dc610a19de94ad2bbcd0460c067f" +[[package]] +name = "ic_cose_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d96bb6153739535ad8d417a49349e4ae8cb3505be2df71a923fb6782345c5b" +dependencies = [ + "aes-gcm", + "candid", + "ciborium", + "coset", + "ed25519-dalek", + "hmac", + "icrc-ledger-types", + "k256", + "num-traits", + "serde", + "serde_bytes", + "sha2", + "sha3", + "x25519-dalek", +] + +[[package]] +name = "ic_message" +version = "0.1.0" +dependencies = [ + "candid", + "ciborium", + "getrandom", + "ic-cdk 0.15.1", + "ic-stable-structures", + "ic_cose_types", + "serde", +] + +[[package]] +name = "ic_message_channel" +version = "0.1.0" +dependencies = [ + "candid", + "ciborium", + "getrandom", + "ic-cdk 0.15.1", + "ic-stable-structures", + "ic_cose_types", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic_message_profile" +version = "0.1.0" +dependencies = [ + "candid", + "ciborium", + "getrandom", + "ic-cdk 0.15.1", + "ic-stable-structures", + "ic_cose_types", + "serde", + "serde_bytes", +] + [[package]] name = "ic_panda_ai" version = "0.1.0" @@ -1757,6 +1895,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2105,6 +2252,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2182,6 +2335,18 @@ dependencies = [ "spki", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.7.0" @@ -2216,9 +2381,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -2884,15 +3049,15 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a5daa25ea337c85ed954c0496e3bdd2c7308cc3b24cf7b50d04876654c579f" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.36.1", + "windows-sys 0.59.0", ] [[package]] @@ -3239,9 +3404,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "toml_datetime", @@ -3417,6 +3582,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3676,19 +3851,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -3714,13 +3876,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.52.6", + "windows_x86_64_msvc", ] [[package]] @@ -3729,24 +3891,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3759,24 +3909,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3789,12 +3927,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3803,9 +3935,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -3838,6 +3970,18 @@ dependencies = [ "url", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "yoke" version = "0.7.4" @@ -3909,6 +4053,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] [[package]] name = "zip" diff --git a/Cargo.toml b/Cargo.toml index be3ee98..9b9c424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] members = [ "src/cli_cryptogram", + "src/ic_message", + "src/ic_message_channel", + "src/ic_message_profile", "src/ic_panda_ai", "src/ic_panda_infra", "src/ic_panda_luckypool", @@ -30,5 +33,6 @@ num-traits = "0.2" ic-cdk = "0.15" ic-cdk-timers = "0.8" ic-stable-structures = "0.6" +ic_cose_types = "0.1" getrandom = { version = "0.2", features = ["custom"] } rand = { version = "0.8", features = ["getrandom"] } diff --git a/Makefile b/Makefile index 87f75f8..59b460b 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,14 @@ twiggy: # cargo install ic-wasm build-wasm: - cargo build --release --target wasm32-unknown-unknown --package ic_panda_ai + cargo build --release --target wasm32-unknown-unknown --package ic_message + cargo build --release --target wasm32-unknown-unknown --package ic_message_channel + cargo build --release --target wasm32-unknown-unknown --package ic_message_profile cargo build --release --target wasm32-unknown-unknown --package ic_panda_luckypool -shrink-wasm: - ic-wasm -o target/wasm32-unknown-unknown/release/ic_panda_ai_optimized.wasm target/wasm32-unknown-unknown/release/ic_panda_ai.wasm shrink - # cargo install candid-extractor build-did: - candid-extractor target/wasm32-unknown-unknown/release/ic_panda_ai.wasm > src/ic_panda_ai/ic_panda_ai.did + candid-extractor target/wasm32-unknown-unknown/release/ic_message.wasm > src/ic_message/ic_message.did + candid-extractor target/wasm32-unknown-unknown/release/ic_message_channel.wasm > src/ic_message_channel/ic_message_channel.did + candid-extractor target/wasm32-unknown-unknown/release/ic_message_profile.wasm > src/ic_message_profile/ic_message_profile.did candid-extractor target/wasm32-unknown-unknown/release/ic_panda_luckypool.wasm > src/ic_panda_luckypool/ic_panda_luckypool.did diff --git a/src/ic_message/Cargo.toml b/src/ic_message/Cargo.toml new file mode 100644 index 0000000..05ea905 --- /dev/null +++ b/src/ic_message/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ic_message" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +ciborium = { workspace = true } +serde = { workspace = true } +getrandom = { workspace = true } +ic-cdk = { workspace = true } +ic-stable-structures = { workspace = true } +ic_cose_types = { workspace = true } diff --git a/src/ic_message/ic_message.did b/src/ic_message/ic_message.did new file mode 100644 index 0000000..c82b88c --- /dev/null +++ b/src/ic_message/ic_message.did @@ -0,0 +1,4 @@ +type ChainArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs }; +type InitArgs = record { managers : vec principal; name : text }; +type UpgradeArgs = record { managers : opt vec principal; name : opt text }; +service : (opt ChainArgs) -> {} diff --git a/src/ic_message/src/api_init.rs b/src/ic_message/src/api_init.rs new file mode 100644 index 0000000..a9154ee --- /dev/null +++ b/src/ic_message/src/api_init.rs @@ -0,0 +1,70 @@ +use candid::{CandidType, Principal}; +use serde::Deserialize; +use std::collections::BTreeSet; + +use crate::store; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ChainArgs { + Init(InitArgs), + Upgrade(UpgradeArgs), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct InitArgs { + name: String, + managers: BTreeSet, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct UpgradeArgs { + name: Option, + managers: Option>, +} + +#[ic_cdk::init] +fn init(args: Option) { + match args { + None => {} + Some(ChainArgs::Init(args)) => { + store::state::with_mut(|s| { + s.name = args.name; + s.managers = args.managers; + }); + } + Some(ChainArgs::Upgrade(_)) => { + ic_cdk::trap( + "cannot initialize the canister with an Upgrade args. Please provide an Init args.", + ); + } + } +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + store::state::save(); +} + +#[ic_cdk::post_upgrade] +fn post_upgrade(args: Option) { + store::state::load(); + + match args { + Some(ChainArgs::Upgrade(args)) => { + store::state::with_mut(|s| { + if let Some(name) = args.name { + s.name = name; + } + if let Some(managers) = args.managers { + s.managers = managers; + } + }); + } + Some(ChainArgs::Init(_)) => { + ic_cdk::trap( + "cannot upgrade the canister with an Init args. Please provide an Upgrade args.", + ); + } + _ => {} + } +} diff --git a/src/ic_message/src/lib.rs b/src/ic_message/src/lib.rs new file mode 100644 index 0000000..c8e6eb6 --- /dev/null +++ b/src/ic_message/src/lib.rs @@ -0,0 +1,42 @@ +use ic_cose_types::ANONYMOUS; + +mod api_init; +mod store; + +use crate::api_init::ChainArgs; + +fn is_controller() -> Result<(), String> { + let caller = ic_cdk::caller(); + if ic_cdk::api::is_controller(&caller) { + Ok(()) + } else { + Err("user is not a controller".to_string()) + } +} + +fn is_authenticated() -> Result<(), String> { + if ic_cdk::caller() == ANONYMOUS { + Err("anonymous user is not allowed".to_string()) + } else { + Ok(()) + } +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +/// A getrandom implementation that always fails +pub fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +getrandom::register_custom_getrandom!(always_fail); + +ic_cdk::export_candid!(); diff --git a/src/ic_message/src/store.rs b/src/ic_message/src/store.rs new file mode 100644 index 0000000..3d21352 --- /dev/null +++ b/src/ic_message/src/store.rs @@ -0,0 +1,129 @@ +use candid::Principal; +use ciborium::{from_reader, from_reader_with_buffer, into_writer}; +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + storable::Bound, + DefaultMemoryImpl, StableBTreeMap, StableCell, Storable, +}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, cell::RefCell, collections::BTreeSet}; + +type Memory = VirtualMemory; + +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct State { + pub name: String, + pub managers: BTreeSet, +} + +impl Storable for State { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode State data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode State data") + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct User { + #[serde(rename = "n")] + pub name: String, + #[serde(rename = "i")] + pub image: String, + #[serde(rename = "p")] + pub profile_canister: Principal, // profile canister + #[serde(rename = "cn")] + pub cose_namespace: Option<(Principal, String)>, // namespace in shared COSE service +} + +impl Storable for User { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode User data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode User data") + } +} + +const STATE_MEMORY_ID: MemoryId = MemoryId::new(0); +const NAME_MEMORY_ID: MemoryId = MemoryId::new(1); +const USER_MEMORY_ID: MemoryId = MemoryId::new(2); + +thread_local! { + static STATE: RefCell = RefCell::new(State::default()); + + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static STATE_STORE: RefCell, Memory>> = RefCell::new( + StableCell::init( + MEMORY_MANAGER.with_borrow(|m| m.get(STATE_MEMORY_ID)), + Vec::new() + ).expect("failed to init STATE store") + ); + + static NAME_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(NAME_MEMORY_ID)), + ) + ); + + static USER_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(USER_MEMORY_ID)), + ) + ); +} + +pub mod state { + use super::*; + + pub fn with(f: impl FnOnce(&State) -> R) -> R { + STATE.with(|r| f(&r.borrow())) + } + + pub fn with_mut(f: impl FnOnce(&mut State) -> R) -> R { + STATE.with(|r| f(&mut r.borrow_mut())) + } + + pub fn is_manager(caller: &Principal) -> Result<(), String> { + STATE.with(|r| match r.borrow().managers.contains(caller) { + true => Ok(()), + false => Err("caller is not a manager".to_string()), + }) + } + + pub fn load() { + let mut scratch = [0; 4096]; + STATE_STORE.with(|r| { + STATE.with(|h| { + let v: State = from_reader_with_buffer(&r.borrow().get()[..], &mut scratch) + .expect("failed to decode STATE_STORE data"); + *h.borrow_mut() = v; + }); + }); + } + + pub fn save() { + STATE.with(|h| { + STATE_STORE.with(|r| { + let mut buf = vec![]; + into_writer(&(*h.borrow()), &mut buf).expect("failed to encode STATE_STORE data"); + r.borrow_mut() + .set(buf) + .expect("failed to set STATE_STORE data"); + }); + }); + } +} diff --git a/src/ic_message_channel/Cargo.toml b/src/ic_message_channel/Cargo.toml new file mode 100644 index 0000000..1287243 --- /dev/null +++ b/src/ic_message_channel/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ic_message_channel" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +ciborium = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +getrandom = { workspace = true } +ic-cdk = { workspace = true } +ic-stable-structures = { workspace = true } +ic_cose_types = { workspace = true } diff --git a/src/ic_message_channel/ic_message_channel.did b/src/ic_message_channel/ic_message_channel.did new file mode 100644 index 0000000..5cd1daa --- /dev/null +++ b/src/ic_message_channel/ic_message_channel.did @@ -0,0 +1,142 @@ +type AddMessageInput = record { + reply_to : opt nat32; + channel : nat32; + payload : blob; +}; +type AddMessageOutput = record { + id : nat32; + kind : nat8; + created_at : nat64; + channel : nat32; +}; +type CanisterStatusResponse = record { + status : CanisterStatusType; + memory_size : nat; + cycles : nat; + settings : DefiniteCanisterSettings; + query_stats : QueryStats; + idle_cycles_burned_per_day : nat; + module_hash : opt blob; + reserved_cycles : nat; +}; +type CanisterStatusType = variant { stopped; stopping; running }; +type ChainArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs }; +type ChannelBasicInfo = record { + id : nat32; + updated_at : nat64; + name : text; + last_message_at : nat32; + last_message_by : principal; + my_setting : ChannelSetting; +}; +type ChannelCreateInput = record { + dek : blob; + managers : vec record { principal; ChannelECDHInput }; + name : text; + description : text; + created_by : principal; +}; +type ChannelECDHInput = record { + ecdh_remote : opt record { blob; blob }; + ecdh_pub : opt blob; +}; +type ChannelInfo = record { + id : nat32; + dek : blob; + updated_at : nat64; + members : vec principal; + managers : vec principal; + name : text; + description : text; + last_message_at : nat32; + last_message_by : principal; + created_at : nat64; + created_by : principal; + my_setting : ChannelSetting; +}; +type ChannelSetting = record { + mute : bool; + ecdh_remote : opt record { blob; blob }; + unread : nat32; + last_read : nat32; + ecdh_pub : opt blob; +}; +type ChannelUpdateInput = record { + id : nat32; + name : opt text; + description : opt text; +}; +type ChannelUpdateMemberInput = record { + id : nat32; + member : principal; + ecdh : ChannelECDHInput; +}; +type ChannelUpdateMySettingInput = record { + id : nat32; + ecdh : opt ChannelECDHInput; + mute : opt bool; + last_read : opt nat32; +}; +type DefiniteCanisterSettings = record { + freezing_threshold : nat; + controllers : vec principal; + reserved_cycles_limit : nat; + log_visibility : LogVisibility; + wasm_memory_limit : nat; + memory_allocation : nat; + compute_allocation : nat; +}; +type InitArgs = record { managers : vec principal; name : text }; +type LogVisibility = variant { controllers; public }; +type Message = record { + id : nat32; + reply_to : nat32; + kind : nat8; + created_at : nat64; + created_by : principal; + channel : nat32; + payload : blob; +}; +type QueryStats = record { + response_payload_bytes_total : nat; + num_instructions_total : nat; + num_calls_total : nat; + request_payload_bytes_total : nat; +}; +type Result = variant { Ok : AddMessageOutput; Err : text }; +type Result_1 = variant { Ok; Err : text }; +type Result_2 = variant { Ok : vec ChannelBasicInfo; Err : text }; +type Result_3 = variant { Ok : ChannelInfo; Err : text }; +type Result_4 = variant { Ok : CanisterStatusResponse; Err : text }; +type Result_5 = variant { Ok : opt ChannelInfo; Err : text }; +type Result_6 = variant { Ok : Message; Err : text }; +type Result_7 = variant { Ok : StateInfo; Err : text }; +type Result_8 = variant { Ok : vec Message; Err : text }; +type Result_9 = variant { Ok : nat64; Err : text }; +type StateInfo = record { + channel_id : nat32; + managers : vec principal; + name : text; + channels_total : nat64; + messages_total : nat64; +}; +type UpgradeArgs = record { managers : opt vec principal; name : opt text }; +service : (opt ChainArgs) -> { + add_message : (AddMessageInput) -> (Result); + admin_add_managers : (vec principal) -> (Result_1); + admin_remove_managers : (vec principal) -> (Result_1); + batch_get_channels : (vec nat32) -> (Result_2) query; + create_channel : (ChannelCreateInput) -> (Result_3); + get_canister_status : () -> (Result_4) query; + get_channel_if_update : (nat32, nat64) -> (Result_5) query; + get_message : (nat32, nat32) -> (Result_6) query; + get_state : () -> (Result_7) query; + list_messages : (nat32, opt nat32, opt nat32, opt nat32) -> (Result_8) query; + quit_channel : (ChannelUpdateMySettingInput) -> (Result_1); + update_channel : (ChannelUpdateInput) -> (Result_9); + update_manager : (ChannelUpdateMemberInput) -> (Result_9); + update_member : (ChannelUpdateMemberInput) -> (Result_9); + update_my_setting : (ChannelUpdateMySettingInput) -> (Result_1); + validate_admin_add_managers : (vec principal) -> (Result_1); + validate_admin_remove_managers : (vec principal) -> (Result_1); +} diff --git a/src/ic_message_channel/src/api_admin.rs b/src/ic_message_channel/src/api_admin.rs new file mode 100644 index 0000000..bb6bdf9 --- /dev/null +++ b/src/ic_message_channel/src/api_admin.rs @@ -0,0 +1,35 @@ +use candid::Principal; +use ic_cose_types::validate_principals; +use std::collections::BTreeSet; + +use crate::{is_controller, store}; + +#[ic_cdk::update(guard = "is_controller")] +fn admin_add_managers(mut args: BTreeSet) -> Result<(), String> { + validate_principals(&args)?; + store::state::with_mut(|r| { + r.managers.append(&mut args); + Ok(()) + }) +} + +#[ic_cdk::update(guard = "is_controller")] +fn admin_remove_managers(args: BTreeSet) -> Result<(), String> { + validate_principals(&args)?; + store::state::with_mut(|r| { + r.managers.retain(|p| !args.contains(p)); + Ok(()) + }) +} + +#[ic_cdk::update] +fn validate_admin_add_managers(args: BTreeSet) -> Result<(), String> { + validate_principals(&args)?; + Ok(()) +} + +#[ic_cdk::update] +fn validate_admin_remove_managers(args: BTreeSet) -> Result<(), String> { + validate_principals(&args)?; + Ok(()) +} diff --git a/src/ic_message_channel/src/api_init.rs b/src/ic_message_channel/src/api_init.rs new file mode 100644 index 0000000..a9154ee --- /dev/null +++ b/src/ic_message_channel/src/api_init.rs @@ -0,0 +1,70 @@ +use candid::{CandidType, Principal}; +use serde::Deserialize; +use std::collections::BTreeSet; + +use crate::store; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ChainArgs { + Init(InitArgs), + Upgrade(UpgradeArgs), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct InitArgs { + name: String, + managers: BTreeSet, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct UpgradeArgs { + name: Option, + managers: Option>, +} + +#[ic_cdk::init] +fn init(args: Option) { + match args { + None => {} + Some(ChainArgs::Init(args)) => { + store::state::with_mut(|s| { + s.name = args.name; + s.managers = args.managers; + }); + } + Some(ChainArgs::Upgrade(_)) => { + ic_cdk::trap( + "cannot initialize the canister with an Upgrade args. Please provide an Init args.", + ); + } + } +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + store::state::save(); +} + +#[ic_cdk::post_upgrade] +fn post_upgrade(args: Option) { + store::state::load(); + + match args { + Some(ChainArgs::Upgrade(args)) => { + store::state::with_mut(|s| { + if let Some(name) = args.name { + s.name = name; + } + if let Some(managers) = args.managers { + s.managers = managers; + } + }); + } + Some(ChainArgs::Init(_)) => { + ic_cdk::trap( + "cannot upgrade the canister with an Init args. Please provide an Upgrade args.", + ); + } + _ => {} + } +} diff --git a/src/ic_message_channel/src/api_query.rs b/src/ic_message_channel/src/api_query.rs new file mode 100644 index 0000000..057e3af --- /dev/null +++ b/src/ic_message_channel/src/api_query.rs @@ -0,0 +1,65 @@ +use ic_cdk::api::management_canister::main::{ + canister_status, CanisterIdRecord, CanisterStatusResponse, +}; +use ic_cose_types::format_error; +use std::collections::BTreeSet; + +use crate::{is_authenticated, store, types}; + +#[ic_cdk::query] +fn get_state() -> Result { + Ok(store::state::with(|s| types::StateInfo { + name: s.name.clone(), + managers: s.managers.clone(), + channel_id: s.channel_id, + channels_total: store::channel::channels_total(), + messages_total: store::channel::messages_total(), + })) +} + +#[ic_cdk::query] +async fn get_canister_status() -> Result { + store::state::is_manager(&ic_cdk::caller())?; + + let (res,) = canister_status(CanisterIdRecord { + canister_id: ic_cdk::id(), + }) + .await + .map_err(format_error)?; + Ok(res) +} + +#[ic_cdk::query(guard = "is_authenticated")] +async fn get_channel_if_update( + id: u32, + updated_at: u64, +) -> Result, String> { + store::channel::get_if_update(ic_cdk::caller(), id, updated_at) +} + +#[ic_cdk::query(guard = "is_authenticated")] +async fn batch_get_channels(ids: BTreeSet) -> Result, String> { + if ids.len() > 100 { + return Err("too many channels".to_string()); + } + + Ok(store::channel::batch_get(ic_cdk::caller(), ids)) +} + +#[ic_cdk::query(guard = "is_authenticated")] +fn get_message(channel: u32, id: u32) -> Result { + let caller = ic_cdk::caller(); + store::channel::get_message(caller, channel, id) +} + +#[ic_cdk::query(guard = "is_authenticated")] +fn list_messages( + channel: u32, + prev: Option, + take: Option, + util: Option, +) -> Result, String> { + let caller = ic_cdk::caller(); + let take = take.unwrap_or(10).min(100); + store::channel::list_messages(caller, channel, prev.unwrap_or(0), take, util.unwrap_or(0)) +} diff --git a/src/ic_message_channel/src/api_update.rs b/src/ic_message_channel/src/api_update.rs new file mode 100644 index 0000000..f48aa7d --- /dev/null +++ b/src/ic_message_channel/src/api_update.rs @@ -0,0 +1,180 @@ +use ic_cose_types::MILLISECONDS; +use std::collections::hash_map::Entry; + +use crate::{is_authenticated, store, types}; + +#[ic_cdk::update(guard = "is_authenticated")] +fn create_channel(input: types::ChannelCreateInput) -> Result { + input.validate()?; + + let caller = ic_cdk::caller(); + let now_ms = ic_cdk::api::time() / MILLISECONDS; + store::state::is_manager(&caller)?; + store::channel::create(caller, input, now_ms) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn update_channel(input: types::ChannelUpdateInput) -> Result { + input.validate()?; + + let caller = ic_cdk::caller(); + let now_ms = ic_cdk::api::time() / MILLISECONDS; + store::channel::manager_with_mut(caller, input.id, |c| { + if let Some(name) = input.name { + c.name = name; + } + if let Some(description) = input.description { + c.description = description; + } + c.updated_at = now_ms; + c.last_message_at += 1; + c.last_message_by = caller; + + store::channel::add_sys_message( + caller, + now_ms, + store::MessageId(input.id, c.last_message_at), + types::SYS_MSG_CHANNEL_UPDATE_INFO.to_string(), + ); + Ok(c.updated_at) + }) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn update_manager(input: types::ChannelUpdateMemberInput) -> Result { + input.validate()?; + + let caller = ic_cdk::caller(); + let now_ms = ic_cdk::api::time() / MILLISECONDS; + store::channel::manager_with_mut(caller, input.id, |c| { + let is_new = match c.managers.entry(input.member) { + Entry::Occupied(mut e) => { + let s = e.get_mut(); + s.ecdh_pub = input.ecdh.ecdh_pub; + s.ecdh_remote = input.ecdh.ecdh_remote; + false + } + Entry::Vacant(e) => match c.members.remove(&input.member) { + Some(mut s) => { + s.ecdh_pub = input.ecdh.ecdh_pub; + s.ecdh_remote = input.ecdh.ecdh_remote; + e.insert(s); + true + } + None => { + e.insert(input.ecdh.into()); + true + } + }, + }; + + if c.managers.len() > types::MAX_CHANNEL_MANAGERS { + Err("too many managers".to_string())?; + } + + c.updated_at = now_ms; + if is_new { + c.last_message_at += 1; + c.last_message_by = caller; + + store::channel::add_sys_message( + caller, + now_ms, + store::MessageId(input.id, c.last_message_at), + format!( + "{}: {}", + types::SYS_MSG_CHANNEL_ADD_MANAGER, + input.member.to_text() + ), + ); + } + Ok(c.updated_at) + }) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn update_member(input: types::ChannelUpdateMemberInput) -> Result { + input.validate()?; + + let caller = ic_cdk::caller(); + let now_ms = ic_cdk::api::time() / MILLISECONDS; + store::channel::manager_with_mut(caller, input.id, |c| { + if c.managers.contains_key(&input.member) { + Err("member is a manager".to_string())?; + } + + let mut is_new = true; + c.members + .entry(input.member) + .and_modify(|s| { + s.ecdh_pub = input.ecdh.ecdh_pub; + s.ecdh_remote = input.ecdh.ecdh_remote.clone(); + is_new = false; + }) + .or_insert(input.ecdh.into()); + + if c.members.len() > types::MAX_CHANNEL_MEMBERS { + Err("too many members".to_string())?; + } + + c.updated_at = now_ms; + if is_new { + c.last_message_at += 1; + c.last_message_by = caller; + + store::channel::add_sys_message( + caller, + now_ms, + store::MessageId(input.id, c.last_message_at), + format!( + "{}: {}", + types::SYS_MSG_CHANNEL_ADD_MEMBER, + input.member.to_text() + ), + ); + } + Ok(c.updated_at) + }) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn update_my_setting(input: types::ChannelUpdateMySettingInput) -> Result<(), String> { + input.validate()?; + + let caller = ic_cdk::caller(); + store::channel::update_my_setting(caller, input)?; + Ok(()) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn quit_channel(input: types::ChannelUpdateMySettingInput) -> Result<(), String> { + input.validate()?; + + let caller = ic_cdk::caller(); + store::channel::quit(caller, input.id)?; + Ok(()) +} + +#[ic_cdk::update(guard = "is_authenticated")] +fn add_message(input: types::AddMessageInput) -> Result { + input.validate()?; + + let now_ms = ic_cdk::api::time() / MILLISECONDS; + let id = store::channel::add_message( + input.channel, + store::Message { + kind: 0, + reply_to: input.reply_to.unwrap_or_default(), + created_by: ic_cdk::caller(), + created_at: now_ms, + payload: input.payload, + }, + )?; + + Ok(types::AddMessageOutput { + id, + channel: input.channel, + kind: 0, + created_at: now_ms, + }) +} diff --git a/src/ic_message_channel/src/lib.rs b/src/ic_message_channel/src/lib.rs new file mode 100644 index 0000000..2f1bead --- /dev/null +++ b/src/ic_message_channel/src/lib.rs @@ -0,0 +1,49 @@ +use candid::Principal; +use ic_cdk::api::management_canister::main::CanisterStatusResponse; +use ic_cose_types::ANONYMOUS; +use std::collections::BTreeSet; + +mod api_admin; +mod api_init; +mod api_query; +mod api_update; +mod store; +mod types; + +use crate::api_init::ChainArgs; + +fn is_controller() -> Result<(), String> { + let caller = ic_cdk::caller(); + if ic_cdk::api::is_controller(&caller) { + Ok(()) + } else { + Err("user is not a controller".to_string()) + } +} + +fn is_authenticated() -> Result<(), String> { + if ic_cdk::caller() == ANONYMOUS { + Err("anonymous user is not allowed".to_string()) + } else { + Ok(()) + } +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +/// A getrandom implementation that always fails +pub fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +getrandom::register_custom_getrandom!(always_fail); + +ic_cdk::export_candid!(); diff --git a/src/ic_message_channel/src/store.rs b/src/ic_message_channel/src/store.rs new file mode 100644 index 0000000..acfb62a --- /dev/null +++ b/src/ic_message_channel/src/store.rs @@ -0,0 +1,584 @@ +use candid::Principal; +use ciborium::{from_reader, from_reader_with_buffer, into_writer}; +use ic_cose_types::to_cbor_bytes; +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + storable::Bound, + DefaultMemoryImpl, StableBTreeMap, StableCell, Storable, +}; +use serde::{Deserialize, Serialize}; +use serde_bytes::{ByteArray, ByteBuf}; +use std::{ + borrow::Cow, + cell::RefCell, + collections::{BTreeSet, HashMap}, +}; + +use crate::types; + +type Memory = VirtualMemory; + +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct State { + pub name: String, + pub managers: BTreeSet, + pub channel_id: u32, +} + +impl Storable for State { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode State data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode State data") + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct Channel { + #[serde(rename = "n")] + pub name: String, + #[serde(rename = "d")] + pub description: String, + #[serde(rename = "m")] + pub managers: HashMap, + #[serde(rename = "p")] + pub members: HashMap, + #[serde(rename = "k")] + pub dek: ByteBuf, + #[serde(rename = "ca")] + pub created_at: u64, + #[serde(rename = "cb")] + pub created_by: Principal, + #[serde(rename = "ua")] + pub updated_at: u64, + #[serde(rename = "la")] + pub last_message_at: u32, + #[serde(rename = "lb")] + pub last_message_by: Principal, +} + +impl Channel { + pub fn into_info(self, caller: Principal, id: u32) -> types::ChannelInfo { + let my_setting = if let Some(s) = self.managers.get(&caller) { + s.to_owned().into() + } else if let Some(s) = self.members.get(&caller) { + s.to_owned().into() + } else { + types::ChannelSetting { + last_read: 0, + unread: 0, + mute: false, + ecdh_pub: None, + ecdh_remote: None, + } + }; + + types::ChannelInfo { + id, + name: self.name, + description: self.description, + managers: self.managers.into_keys().collect(), + members: self.members.into_keys().collect(), + dek: self.dek, + created_at: self.created_at, + created_by: self.created_by, + last_message_at: self.last_message_at, + last_message_by: self.last_message_by, + updated_at: self.updated_at, + my_setting, + } + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct ChannelSetting { + #[serde(rename = "l")] + pub last_read: u32, // message id + #[serde(rename = "u")] + pub unread: u32, // unread message count + #[serde(rename = "m")] + pub mute: bool, + #[serde(rename = "ep")] + pub ecdh_pub: Option>, + #[serde(rename = "er")] + pub ecdh_remote: Option<(ByteArray<32>, ByteBuf)>, +} + +impl From for types::ChannelSetting { + fn from(s: ChannelSetting) -> Self { + types::ChannelSetting { + last_read: s.last_read, + unread: s.unread, + mute: s.mute, + ecdh_pub: s.ecdh_pub, + ecdh_remote: s.ecdh_remote, + } + } +} + +impl From for ChannelSetting { + fn from(s: types::ChannelECDHInput) -> Self { + ChannelSetting { + last_read: 0, + unread: 0, + mute: false, + ecdh_pub: s.ecdh_pub, + ecdh_remote: s.ecdh_remote, + } + } +} + +impl Storable for Channel { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode Channel data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode Channel data") + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct Message { + #[serde(rename = "k")] + pub kind: u8, // 0: created by user, encrypted, 1: created by system, not encrypted + #[serde(rename = "r")] + pub reply_to: u32, // 0 means not a reply + #[serde(rename = "ca")] + pub created_at: u64, + #[serde(rename = "cb")] + pub created_by: Principal, + #[serde(rename = "p")] + pub payload: ByteBuf, +} + +impl Message { + pub fn into_info(self, channel: u32, id: u32) -> types::Message { + types::Message { + id, + channel, + kind: self.kind, + reply_to: self.reply_to, + created_at: self.created_at, + created_by: self.created_by, + payload: self.payload, + } + } +} + +impl Storable for Message { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode Message data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode Message data") + } +} + +// MessageId: (channel id, message id) +#[derive(Clone, Default, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)] +pub struct MessageId(pub u32, pub u32); +impl Storable for MessageId { + const BOUND: Bound = Bound::Bounded { + max_size: 11, + is_fixed_size: false, + }; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode MessageId data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode MessageId data") + } +} + +const STATE_MEMORY_ID: MemoryId = MemoryId::new(0); +const CHANNEL_MEMORY_ID: MemoryId = MemoryId::new(1); +const MESSAGE_MEMORY_ID: MemoryId = MemoryId::new(2); + +thread_local! { + static STATE: RefCell = RefCell::new(State::default()); + + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static STATE_STORE: RefCell, Memory>> = RefCell::new( + StableCell::init( + MEMORY_MANAGER.with_borrow(|m| m.get(STATE_MEMORY_ID)), + Vec::new() + ).expect("failed to init STATE store") + ); + + static CHANNEL_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(CHANNEL_MEMORY_ID)), + ) + ); + + static MESSAGE_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(MESSAGE_MEMORY_ID)), + ) + ); +} + +pub mod state { + use super::*; + + pub fn with(f: impl FnOnce(&State) -> R) -> R { + STATE.with(|r| f(&r.borrow())) + } + + pub fn with_mut(f: impl FnOnce(&mut State) -> R) -> R { + STATE.with(|r| f(&mut r.borrow_mut())) + } + + pub fn is_manager(caller: &Principal) -> Result<(), String> { + STATE.with(|r| match r.borrow().managers.contains(caller) { + true => Ok(()), + false => Err("caller is not a manager".to_string()), + }) + } + + pub fn load() { + let mut scratch = [0; 4096]; + STATE_STORE.with(|r| { + STATE.with(|h| { + let v: State = from_reader_with_buffer(&r.borrow().get()[..], &mut scratch) + .expect("failed to decode STATE_STORE data"); + *h.borrow_mut() = v; + }); + }); + } + + pub fn save() { + STATE.with(|h| { + STATE_STORE.with(|r| { + let mut buf = vec![]; + into_writer(&(*h.borrow()), &mut buf).expect("failed to encode STATE_STORE data"); + r.borrow_mut() + .set(buf) + .expect("failed to set STATE_STORE data"); + }); + }); + } +} + +pub mod channel { + use super::*; + + pub fn channels_total() -> u64 { + CHANNEL_STORE.with(|r| r.borrow().len()) + } + + pub fn messages_total() -> u64 { + MESSAGE_STORE.with(|r| r.borrow().len()) + } + + pub fn manager_with_mut( + caller: Principal, + id: u32, + f: impl FnOnce(&mut Channel) -> Result, + ) -> Result { + CHANNEL_STORE.with(|r| { + let mut m = r.borrow_mut(); + match m.get(&id) { + None => Err("channel not found".to_string()), + Some(mut v) => { + if !v.managers.contains_key(&caller) { + Err("caller is not a manager".to_string())?; + } + match f(&mut v) { + Err(err) => Err(err), + Ok(res) => { + m.insert(id, v); + Ok(res) + } + } + } + } + }) + } + + pub fn add_sys_message(caller: Principal, now_ms: u64, mid: MessageId, message: String) { + MESSAGE_STORE.with(|r| { + r.borrow_mut().insert( + mid, + Message { + kind: 1, + reply_to: 0, + created_at: now_ms, + created_by: caller, + payload: to_cbor_bytes(&message).into(), + }, + ); + }); + } + + pub fn create( + caller: Principal, + input: types::ChannelCreateInput, + now_ms: u64, + ) -> Result { + let id = state::with_mut(|s| { + s.channel_id = s.channel_id.saturating_add(1); + s.channel_id + }); + + add_sys_message( + caller, + now_ms, + MessageId(id, 1), + format!( + "{}: Channel {} created by {}", + types::SYS_MSG_CHANNEL_CREATE, + input.name, + input.created_by.to_text() + ), + ); + + CHANNEL_STORE.with(|r| { + let channel = Channel { + name: input.name, + description: input.description, + managers: input + .managers + .into_iter() + .map(|p| (p.0, p.1.into())) + .collect(), + members: HashMap::new(), + dek: input.dek, + created_at: now_ms, + created_by: input.created_by, + last_message_at: 1, + last_message_by: caller, + updated_at: now_ms, + }; + + let output = channel.clone().into_info(input.created_by, id); + r.borrow_mut().insert(id, channel); + Ok(output) + }) + } + + pub fn update_my_setting( + caller: Principal, + input: types::ChannelUpdateMySettingInput, + ) -> Result<(), String> { + CHANNEL_STORE.with(|r| { + let mut m = r.borrow_mut(); + match m.get(&input.id) { + None => Err("channel not found".to_string()), + Some(mut v) => { + let setting = match v.members.get_mut(&caller) { + Some(s) => s, + None => match v.managers.get_mut(&caller) { + Some(s) => s, + None => Err("caller is not a manager or member".to_string())?, + }, + }; + + if let Some(last_read) = input.last_read { + if last_read > setting.last_read { + setting.last_read = last_read; + setting.unread = 0; + } + } + if let Some(mute) = input.mute { + setting.mute = mute; + } + if let Some(ecdh) = input.ecdh { + setting.ecdh_pub = ecdh.ecdh_pub; + setting.ecdh_remote = ecdh.ecdh_remote; + } + m.insert(input.id, v); + Ok(()) + } + } + }) + } + + pub fn quit(caller: Principal, id: u32) -> Result<(), String> { + CHANNEL_STORE.with(|r| { + let mut m = r.borrow_mut(); + match m.get(&id) { + None => Err("channel not found".to_string()), + Some(mut v) => { + v.members.remove(&caller); + v.managers.remove(&caller); + + // remove channel if no managers + if v.managers.is_empty() { + m.remove(&id); + MESSAGE_STORE.with(|r| { + let mut messages = r.borrow_mut(); + for i in 1..v.last_message_at + 1 { + messages.remove(&MessageId(id, i)); + } + }); + } else { + m.insert(id, v); + } + Ok(()) + } + } + }) + } + + pub fn add_message(id: u32, msg: Message) -> Result { + CHANNEL_STORE.with(|r| { + let mut m = r.borrow_mut(); + match m.get(&id) { + None => Err("channel not found".to_string()), + Some(mut v) => { + if !v.managers.contains_key(&msg.created_by) + && !v.members.contains_key(&msg.created_by) + { + Err("caller is not a manager or member".to_string())?; + } + + if v.last_message_at >= types::MAX_CHANNEL_MESSAGES { + Err("too many messages".to_string())?; + } + + v.last_message_at += 1; + v.last_message_by = msg.created_by; + for (p, s) in v.managers.iter_mut() { + if p != &msg.created_by { + s.unread += 1; + } + } + for (p, s) in v.members.iter_mut() { + if p != &msg.created_by { + s.unread += 1; + } + } + let mid = v.last_message_at; + m.insert(id, v); + MESSAGE_STORE.with(|r| r.borrow_mut().insert(MessageId(id, mid), msg)); + Ok(mid) + } + } + }) + } + + pub fn get_if_update( + caller: Principal, + id: u32, + updated_at: u64, + ) -> Result, String> { + CHANNEL_STORE.with(|r| match r.borrow().get(&id) { + None => Err("channel not found".to_string()), + Some(v) => { + if !v.managers.contains_key(&caller) && !v.members.contains_key(&caller) { + Err("caller is not a manager or member".to_string())?; + } + + if v.updated_at > updated_at { + Ok(Some(v.into_info(caller, id))) + } else { + Ok(None) + } + } + }) + } + + pub fn batch_get(caller: Principal, ids: BTreeSet) -> Vec { + CHANNEL_STORE.with(|r| { + let m = r.borrow(); + let mut output = Vec::with_capacity(ids.len()); + for id in ids { + if let Some(v) = m.get(&id) { + let my_setting = if let Some(s) = v.managers.get(&caller) { + Some(s) + } else { v.members.get(&caller) }; + + if let Some(my_setting) = my_setting { + output.push(types::ChannelBasicInfo { + id, + name: v.name.clone(), + updated_at: v.updated_at, + last_message_at: v.last_message_at, + last_message_by: v.last_message_by, + my_setting: my_setting.to_owned().into(), + }); + } + } + } + output + }) + } + + pub fn get_message(caller: Principal, channel: u32, id: u32) -> Result { + CHANNEL_STORE.with(|r| match r.borrow().get(&channel) { + None => Err("channel not found".to_string()), + Some(v) => { + if !v.managers.contains_key(&caller) && !v.members.contains_key(&caller) { + Err("caller is not a manager or member".to_string())?; + } + + MESSAGE_STORE.with(|r| { + r.borrow() + .get(&MessageId(channel, id)) + .map(|msg| msg.into_info(channel, id)) + .ok_or("message not found".to_string()) + }) + } + }) + } + + pub fn list_messages( + caller: Principal, + channel: u32, + prev: u32, + take: u32, + util: u32, + ) -> Result, String> { + CHANNEL_STORE.with(|r| match r.borrow().get(&channel) { + None => Err("channel not found".to_string()), + Some(v) => { + if !v.managers.contains_key(&caller) && !v.members.contains_key(&caller) { + Err("caller is not a manager or member".to_string())?; + } + + let end = if prev == 0 || prev > v.last_message_at { + v.last_message_at + 1 + } else { + prev + }; + let start = if util == 0 || util >= prev { 1 } else { util }; + MESSAGE_STORE.with(|r| { + let m = r.borrow(); + let mut output = Vec::with_capacity(take as usize); + for i in (start..end).rev() { + if output.len() >= take as usize { + break; + } + if let Some(msg) = m.get(&MessageId(channel, i)) { + output.push(msg.into_info(channel, i)); + } + } + Ok(output) + }) + } + }) + } +} diff --git a/src/ic_message_channel/src/types.rs b/src/ic_message_channel/src/types.rs new file mode 100644 index 0000000..374657f --- /dev/null +++ b/src/ic_message_channel/src/types.rs @@ -0,0 +1,205 @@ +use candid::{CandidType, Principal}; +use ic_cose_types::cose::encrypt0::try_decode_encrypt0; +use serde::{Deserialize, Serialize}; +use serde_bytes::{ByteArray, ByteBuf}; +use std::collections::{BTreeSet, HashMap}; + +pub const MAX_CHANNEL_MANAGERS: usize = 5; +pub const MAX_CHANNEL_MEMBERS: usize = 100; +pub const MAX_CHANNEL_MESSAGES: u32 = 256 * 256; +pub const MAX_MESSAGE_SIZE: usize = 1024 * 32; // 32KB + +pub static SYS_MSG_CHANNEL_CREATE: &str = "Channel.Create"; +pub static SYS_MSG_CHANNEL_UPDATE_INFO: &str = "Channel.Update.Info"; +pub static SYS_MSG_CHANNEL_ADD_MANAGER: &str = "Channel.Add.Manager"; +pub static SYS_MSG_CHANNEL_ADD_MEMBER: &str = "Channel.Add.Member"; + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct StateInfo { + pub name: String, + pub managers: BTreeSet, + pub channel_id: u32, + pub channels_total: u64, + pub messages_total: u64, +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelInfo { + pub id: u32, + pub name: String, + pub description: String, + pub managers: BTreeSet, + pub members: BTreeSet, + pub dek: ByteBuf, + pub created_at: u64, + pub created_by: Principal, + pub updated_at: u64, + pub last_message_at: u32, + pub last_message_by: Principal, + pub my_setting: ChannelSetting, +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelBasicInfo { + pub id: u32, + pub name: String, + pub updated_at: u64, + pub last_message_at: u32, + pub last_message_by: Principal, + pub my_setting: ChannelSetting, +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelSetting { + pub last_read: u32, // message id + pub unread: u32, // unread message count + pub mute: bool, + pub ecdh_pub: Option>, + pub ecdh_remote: Option<(ByteArray<32>, ByteBuf)>, +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct Message { + pub id: u32, + pub channel: u32, + pub kind: u8, // 0: created by user, 1: created by system + pub reply_to: u32, // 0 means not a reply + pub created_at: u64, + pub created_by: Principal, + pub payload: ByteBuf, +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelCreateInput { + pub name: String, + pub description: String, + pub managers: HashMap, + pub dek: ByteBuf, + pub created_by: Principal, +} + +impl ChannelCreateInput { + pub fn validate(&self) -> Result<(), String> { + if self.name.is_empty() { + Err("name is empty".to_string())?; + } + if self.name.len() > 64 { + Err("name is too long".to_string())?; + } + if self.description.len() > 256 { + Err("description is too long".to_string())?; + } + + if self.managers.is_empty() { + Err("managers is empty".to_string())?; + } + if !self.managers.contains_key(&self.created_by) { + Err("created_by is not in managers".to_string())?; + } + for (_, v) in &self.managers { + v.validate()?; + } + + try_decode_encrypt0(&self.dek)?; + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelUpdateInput { + pub id: u32, + pub name: Option, + pub description: Option, +} + +impl ChannelUpdateInput { + pub fn validate(&self) -> Result<(), String> { + if let Some(ref name) = self.name { + if name.is_empty() { + Err("name is empty".to_string())?; + } + if name.len() > 64 { + Err("name is too long".to_string())?; + } + } + + if let Some(ref description) = self.description { + if description.len() > 256 { + Err("description is too long".to_string())?; + } + } + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelUpdateMemberInput { + pub id: u32, + pub member: Principal, + pub ecdh: ChannelECDHInput, +} + +impl ChannelUpdateMemberInput { + pub fn validate(&self) -> Result<(), String> { + self.ecdh.validate()?; + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelECDHInput { + pub ecdh_pub: Option>, + pub ecdh_remote: Option<(ByteArray<32>, ByteBuf)>, +} + +impl ChannelECDHInput { + pub fn validate(&self) -> Result<(), String> { + if let Some((_, ref kek)) = self.ecdh_remote { + try_decode_encrypt0(kek)?; + } + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct ChannelUpdateMySettingInput { + pub id: u32, + pub last_read: Option, // message id + pub mute: Option, + pub ecdh: Option, +} + +impl ChannelUpdateMySettingInput { + pub fn validate(&self) -> Result<(), String> { + if let Some(ref ecdh) = self.ecdh { + ecdh.validate()?; + } + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct AddMessageInput { + pub channel: u32, + pub payload: ByteBuf, + pub reply_to: Option, +} + +impl AddMessageInput { + pub fn validate(&self) -> Result<(), String> { + if self.payload.len() > MAX_MESSAGE_SIZE { + Err("payload is too large".to_string())?; + } + + try_decode_encrypt0(&self.payload)?; + Ok(()) + } +} + +#[derive(CandidType, Clone, Debug, Deserialize, Serialize)] +pub struct AddMessageOutput { + pub id: u32, + pub channel: u32, + pub kind: u8, + pub created_at: u64, +} diff --git a/src/ic_message_profile/Cargo.toml b/src/ic_message_profile/Cargo.toml new file mode 100644 index 0000000..65357ea --- /dev/null +++ b/src/ic_message_profile/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ic_message_profile" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +ciborium = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +getrandom = { workspace = true } +ic-cdk = { workspace = true } +ic-stable-structures = { workspace = true } +ic_cose_types = { workspace = true } diff --git a/src/ic_message_profile/ic_message_profile.did b/src/ic_message_profile/ic_message_profile.did new file mode 100644 index 0000000..c82b88c --- /dev/null +++ b/src/ic_message_profile/ic_message_profile.did @@ -0,0 +1,4 @@ +type ChainArgs = variant { Upgrade : UpgradeArgs; Init : InitArgs }; +type InitArgs = record { managers : vec principal; name : text }; +type UpgradeArgs = record { managers : opt vec principal; name : opt text }; +service : (opt ChainArgs) -> {} diff --git a/src/ic_message_profile/src/api_init.rs b/src/ic_message_profile/src/api_init.rs new file mode 100644 index 0000000..a9154ee --- /dev/null +++ b/src/ic_message_profile/src/api_init.rs @@ -0,0 +1,70 @@ +use candid::{CandidType, Principal}; +use serde::Deserialize; +use std::collections::BTreeSet; + +use crate::store; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ChainArgs { + Init(InitArgs), + Upgrade(UpgradeArgs), +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct InitArgs { + name: String, + managers: BTreeSet, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct UpgradeArgs { + name: Option, + managers: Option>, +} + +#[ic_cdk::init] +fn init(args: Option) { + match args { + None => {} + Some(ChainArgs::Init(args)) => { + store::state::with_mut(|s| { + s.name = args.name; + s.managers = args.managers; + }); + } + Some(ChainArgs::Upgrade(_)) => { + ic_cdk::trap( + "cannot initialize the canister with an Upgrade args. Please provide an Init args.", + ); + } + } +} + +#[ic_cdk::pre_upgrade] +fn pre_upgrade() { + store::state::save(); +} + +#[ic_cdk::post_upgrade] +fn post_upgrade(args: Option) { + store::state::load(); + + match args { + Some(ChainArgs::Upgrade(args)) => { + store::state::with_mut(|s| { + if let Some(name) = args.name { + s.name = name; + } + if let Some(managers) = args.managers { + s.managers = managers; + } + }); + } + Some(ChainArgs::Init(_)) => { + ic_cdk::trap( + "cannot upgrade the canister with an Init args. Please provide an Upgrade args.", + ); + } + _ => {} + } +} diff --git a/src/ic_message_profile/src/lib.rs b/src/ic_message_profile/src/lib.rs new file mode 100644 index 0000000..c8e6eb6 --- /dev/null +++ b/src/ic_message_profile/src/lib.rs @@ -0,0 +1,42 @@ +use ic_cose_types::ANONYMOUS; + +mod api_init; +mod store; + +use crate::api_init::ChainArgs; + +fn is_controller() -> Result<(), String> { + let caller = ic_cdk::caller(); + if ic_cdk::api::is_controller(&caller) { + Ok(()) + } else { + Err("user is not a controller".to_string()) + } +} + +fn is_authenticated() -> Result<(), String> { + if ic_cdk::caller() == ANONYMOUS { + Err("anonymous user is not allowed".to_string()) + } else { + Ok(()) + } +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +/// A getrandom implementation that always fails +pub fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +getrandom::register_custom_getrandom!(always_fail); + +ic_cdk::export_candid!(); diff --git a/src/ic_message_profile/src/store.rs b/src/ic_message_profile/src/store.rs new file mode 100644 index 0000000..e2a904e --- /dev/null +++ b/src/ic_message_profile/src/store.rs @@ -0,0 +1,127 @@ +use candid::Principal; +use ciborium::{from_reader, from_reader_with_buffer, into_writer}; +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + storable::Bound, + DefaultMemoryImpl, StableBTreeMap, StableCell, Storable, +}; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Cow, + cell::RefCell, + collections::{BTreeSet, HashMap}, +}; + +type Memory = VirtualMemory; + +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct State { + pub name: String, + pub managers: BTreeSet, +} + +impl Storable for State { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode State data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode State data") + } +} + +#[derive(Clone, Default, Deserialize, Serialize)] +pub struct Profile { + #[serde(rename = "ca")] + pub created_at: u64, + #[serde(rename = "aa")] + pub active_at: Vec, + pub followers: u64, + pub following: BTreeSet, + pub channels: HashMap<(Principal, u64), u64>, +} + +impl Storable for Profile { + const BOUND: Bound = Bound::Unbounded; + + fn to_bytes(&self) -> Cow<[u8]> { + let mut buf = vec![]; + into_writer(self, &mut buf).expect("failed to encode Profile data"); + Cow::Owned(buf) + } + + fn from_bytes(bytes: Cow<'_, [u8]>) -> Self { + from_reader(&bytes[..]).expect("failed to decode Profile data") + } +} + +const STATE_MEMORY_ID: MemoryId = MemoryId::new(0); +const PROFILE_MEMORY_ID: MemoryId = MemoryId::new(1); + +thread_local! { + static STATE: RefCell = RefCell::new(State::default()); + + + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static STATE_STORE: RefCell, Memory>> = RefCell::new( + StableCell::init( + MEMORY_MANAGER.with_borrow(|m| m.get(STATE_MEMORY_ID)), + Vec::new() + ).expect("failed to init STATE store") + ); + + static PROFILE_STORE: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(PROFILE_MEMORY_ID)), + ) + ); + +} + +pub mod state { + use super::*; + + pub fn with(f: impl FnOnce(&State) -> R) -> R { + STATE.with(|r| f(&r.borrow())) + } + + pub fn with_mut(f: impl FnOnce(&mut State) -> R) -> R { + STATE.with(|r| f(&mut r.borrow_mut())) + } + + pub fn is_manager(caller: &Principal) -> Result<(), String> { + STATE.with(|r| match r.borrow().managers.contains(caller) { + true => Ok(()), + false => Err("caller is not a manager".to_string()), + }) + } + + pub fn load() { + let mut scratch = [0; 4096]; + STATE_STORE.with(|r| { + STATE.with(|h| { + let v: State = from_reader_with_buffer(&r.borrow().get()[..], &mut scratch) + .expect("failed to decode STATE_STORE data"); + *h.borrow_mut() = v; + }); + }); + } + + pub fn save() { + STATE.with(|h| { + STATE_STORE.with(|r| { + let mut buf = vec![]; + into_writer(&(*h.borrow()), &mut buf).expect("failed to encode STATE_STORE data"); + r.borrow_mut() + .set(buf) + .expect("failed to set STATE_STORE data"); + }); + }); + } +} diff --git a/src/ic_panda_infra/src/lib.rs b/src/ic_panda_infra/src/lib.rs index 3c1a01b..d11aa6e 100644 --- a/src/ic_panda_infra/src/lib.rs +++ b/src/ic_panda_infra/src/lib.rs @@ -1,5 +1,9 @@ +use candid::Principal; + #[ic_cdk::query] fn api_version() -> u16 { + let id: Principal = ic_cdk::caller(); + ic_cdk::println!("{}", id); 1 }