diff --git a/candid/address_book.did b/candid/address_book.did index 1eea533..2d6ca61 100644 --- a/candid/address_book.did +++ b/candid/address_book.did @@ -14,7 +14,7 @@ type value_type = variant { type operation_error = variant { NotAuthorized; NonExistentItem; - BadParameters; + BadParameters: text; Unknown : text; }; diff --git a/candid/nft.did b/candid/nft.did index 4ebd9f4..2f31279 100644 --- a/candid/nft.did +++ b/candid/nft.did @@ -19,10 +19,15 @@ type nft_canister = record { details : vec record { text; detail_value }; }; +type paginated_nft_canisters = record { + nfts: vec nft_canister; + amount: nat64; +}; + type operation_error = variant { NotAuthorized; NonExistentItem; - BadParameters; + BadParameters: text; Unknown : text; }; @@ -40,5 +45,7 @@ service : { // Canister ethods "get_all" : () -> (vec nft_canister) query; + "get_all_paginated" : (offset: opt nat64, limit: opt nat64) -> (variant { Ok: paginated_nft_canisters; Err: operation_error }); + "add_admin" : (principal) -> (operation_response); } diff --git a/candid/registry.did b/candid/registry.did index b3c291a..ab9fea4 100644 --- a/candid/registry.did +++ b/candid/registry.did @@ -19,9 +19,14 @@ type canister_metadata = record { details : vec record { text; detail_value }; }; +type paginated_canister_metadata = record { + canisters: vec canister_metadata; + amount: nat64; +}; + type operation_error = variant { NotAuthorized; - BadParameters; + BadParameters: text; NonExistentItem; Unknown: text; }; @@ -38,6 +43,7 @@ service : { "add" : (canister_metadata) -> (operation_response); "remove" : (principal) -> (operation_response); "get_all" : () -> (vec canister_metadata) query; + "get_all_paginated" : (offset: opt nat64, limit: opt nat64) -> (variant { Ok: paginated_canister_metadata; Err: operation_error }); "add_admin" : (principal) -> (operation_response); } diff --git a/candid/tokens.did b/candid/tokens.did index 068da5f..b40d221 100644 --- a/candid/tokens.did +++ b/candid/tokens.did @@ -19,10 +19,15 @@ type token = record { details : vec record { text; detail_value } }; +type paginated_tokens = record { + tokens: vec token; + amount: nat64; +}; + type operation_error = variant { NotAuthorized; NonExistentItem; - BadParameters; + BadParameters: text; Unknown : text; }; @@ -40,5 +45,6 @@ service : { // Canister methods "get_all" : () -> (vec token) query; + "get_all_paginated" : (offset: opt nat64, limit: opt nat64) -> (variant { Ok: paginated_tokens; Err: operation_error }); "add_admin" : (principal) -> (operation_response); } diff --git a/registries/address_book/src/address_book.rs b/registries/address_book/src/address_book.rs index 304d227..4fa8dfe 100644 --- a/registries/address_book/src/address_book.rs +++ b/registries/address_book/src/address_book.rs @@ -57,33 +57,46 @@ impl AddressBook { return result.0.is_some(); } - pub async fn validate_address_type(&mut self, address: AddressType) -> Result<(), Failure> { + pub async fn validate_address_type( + &mut self, + address: AddressType, + ) -> Result<(), OperationError> { match address { AddressType::Icns(s) => match self.validate_icns(s).await { true => return Ok(()), - false => return Err(Failure::BadParameters), + false => return Err(OperationError::BadParameters(String::from("Invalid ICNS."))), }, AddressType::AccountId(s) => match self.validate_account_id(s) { true => return Ok(()), - false => return Err(Failure::BadParameters), + false => { + return Err(OperationError::BadParameters(String::from( + "Invalid Account.", + ))) + } }, AddressType::PrincipalId(_s) => Ok(()), - _ => Err(Failure::BadParameters), + _ => Err(OperationError::BadParameters(String::from( + "Invalid Principal.", + ))), } } - pub fn add(&mut self, account: Principal, address: Address) -> Result<(), Failure> { + pub fn add(&mut self, account: Principal, address: Address) -> Result<(), OperationError> { let pointer: Key = (account, address.name.clone()); self.0.insert(pointer.clone(), address); return Ok(()); } - pub fn remove(&mut self, account: Principal, canister_name: String) -> Result<(), Failure> { + pub fn remove( + &mut self, + account: Principal, + canister_name: String, + ) -> Result<(), OperationError> { let pointer: Key = (account, canister_name); if !self.0.contains_key(&pointer) { - return Err(Failure::NonExistentItem); + return Err(OperationError::NonExistentItem); } self.0.remove(&pointer); @@ -103,13 +116,9 @@ impl AddressBook { account: Principal, offset: usize, _limit: usize, - ) -> Result, Failure> { + ) -> Result, OperationError> { let mut limit = _limit; - if offset >= limit { - return Err(Failure::BadParameters); - } - let start: Key = (account.clone(), String::new()); let end: Key = (account.clone(), unsafe { String::from(std::char::from_u32_unchecked(u32::MAX)) @@ -117,6 +126,12 @@ impl AddressBook { let addresses: Vec<(&(ic_kit::Principal, std::string::String), &Address)> = self.0.range((Included(start), Included(end))).collect(); + if offset >= addresses.len() { + return Err(OperationError::BadParameters(String::from( + "Offset out of bound.", + ))); + } + if offset + limit > addresses.len() { limit = addresses.len() - offset; } @@ -131,20 +146,32 @@ fn name() -> String { } #[update] -pub async fn add(address: Address) -> Result<(), Failure> { - if &address.name.len() > &NAME_LIMIT { - return Err(Failure::BadParameters); - } else if address.description.is_some() { +pub async fn add(address: Address) -> Result<(), OperationError> { + if address.name.len() > NAME_LIMIT { + return Err(OperationError::BadParameters(format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + ))); + } + + if address.description.is_some() { let description = address.clone().description.unwrap(); - if &description.len() > &DESCRIPTION_LIMIT { - return Err(Failure::BadParameters); + if description.len() > DESCRIPTION_LIMIT { + return Err(OperationError::BadParameters(format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + ))); } - } else if address.emoji.is_some() { + } + + if address.emoji.is_some() { let emojis: Vec = address.clone().emoji.unwrap().chars().take(1).collect(); if !is_emoji(emojis[0]) { - return Err(Failure::BadParameters); + return Err(OperationError::BadParameters(String::from( + "Invalid emoji field.", + ))); } } @@ -159,7 +186,7 @@ pub async fn add(address: Address) -> Result<(), Failure> { } #[update] -pub fn remove(address_name: String) -> Result<(), Failure> { +pub fn remove(address_name: String) -> Result<(), OperationError> { let address_book = ic::get_mut::(); return address_book.remove(ic::caller(), address_name); } @@ -178,7 +205,7 @@ pub fn get_all() -> Vec<&'static Address> { pub fn get_all_paginated( offset: Option, limit: Option, -) -> Result, Failure> { +) -> Result, OperationError> { let address_book = ic::get_mut::(); let addresses = address_book .get_all_paginated( diff --git a/registries/address_book/src/common_types.rs b/registries/address_book/src/common_types.rs index c2a4bab..4e10e4d 100644 --- a/registries/address_book/src/common_types.rs +++ b/registries/address_book/src/common_types.rs @@ -36,9 +36,9 @@ pub const ICNS_REGISTRY_PRINCIPAL_ID: &str = "e5kvl-zyaaa-aaaan-qabaq-cai"; pub const DEFAULT_LIMIT: usize = 20; #[derive(CandidType, Debug, PartialEq)] -pub enum Failure { +pub enum OperationError { NotAuthorized, - BadParameters, + BadParameters(String), NonExistentItem, Unknown(String), } diff --git a/registries/address_book/src/tests.rs b/registries/address_book/src/tests.rs index 82cc87c..d0fd3a2 100644 --- a/registries/address_book/src/tests.rs +++ b/registries/address_book/src/tests.rs @@ -69,7 +69,13 @@ mod tests { let addition_result = add(address_info.clone()).await; assert!(addition_result.is_err()); - assert_eq!(addition_result.unwrap_err(), Failure::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + )) + ); } #[tokio::test] @@ -87,26 +93,35 @@ mod tests { let addition_result = add(address_info.clone()).await; assert!(addition_result.is_err()); - assert_eq!(addition_result.unwrap_err(), Failure::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + )) + ); } - // #[tokio::test] - // async fn test_add_address_fails_because_of_bad_emoji_param() { - // MockContext::new() - // .with_caller(mock_principals::alice()) - // .inject(); - - // let address_info = Address { - // name: String::from("Bob"), - // description: Some(String::from("description")), - // emoji: Some(String::from("a")), - // value: AddressType::PrincipalId(mock_principals::bob()), - // }; - - // let addition_result = add(address_info.clone()).await; - // assert!(addition_result.is_err()); - // assert_eq!(addition_result.unwrap_err(), Failure::BadParameters); - // } + #[tokio::test] + async fn test_add_address_fails_because_of_bad_emoji_param() { + MockContext::new() + .with_caller(mock_principals::alice()) + .inject(); + + let address_info = Address { + name: String::from("Bob"), + description: Some(String::from("description")), + emoji: Some(String::from("a")), + value: AddressType::PrincipalId(mock_principals::bob()), + }; + + let addition_result = add(address_info.clone()).await; + assert!(addition_result.is_err()); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Invalid emoji field.",)) + ); + } #[tokio::test] async fn test_remove_address_successfully() { diff --git a/registries/canister_registry/Cargo.toml b/registries/canister_registry/Cargo.toml index b3569c0..d93af30 100644 --- a/registries/canister_registry/Cargo.toml +++ b/registries/canister_registry/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -ic-cdk = "0.3" +ic-cdk = "0.5" ic-cdk-macros = "0.3" ic-types = "0.1.3" ic-kit = "0.4.2" diff --git a/registries/canister_registry/src/common_types.rs b/registries/canister_registry/src/common_types.rs index 01dae48..d498921 100644 --- a/registries/canister_registry/src/common_types.rs +++ b/registries/canister_registry/src/common_types.rs @@ -2,9 +2,9 @@ use ic_kit::{candid::CandidType, Principal}; use serde::{Deserialize, Serialize}; #[derive(CandidType, Debug, PartialEq)] -pub enum Failure { +pub enum OperationError { NotAuthorized, - BadParameters, + BadParameters(String), NonExistentItem, Unknown(String), } @@ -19,6 +19,12 @@ pub struct CanisterMetadata { pub details: Vec<(String, DetailValue)>, } +#[derive(CandidType, Clone, Debug, PartialEq)] +pub struct GetAllPaginatedResponse { + pub amount: usize, + pub canisters: Vec<&'static CanisterMetadata>, +} + #[derive(CandidType, Serialize, Deserialize, Clone, PartialEq, Debug)] pub enum DetailValue { True, @@ -35,3 +41,4 @@ pub enum DetailValue { pub const DESCRIPTION_LIMIT: usize = 1200; pub const NAME_LIMIT: usize = 24; +pub const DEFAULT_LIMIT: usize = 20; diff --git a/registries/canister_registry/src/management.rs b/registries/canister_registry/src/management.rs index 633758e..8c042ff 100644 --- a/registries/canister_registry/src/management.rs +++ b/registries/canister_registry/src/management.rs @@ -2,7 +2,7 @@ use ic_kit::ic; use ic_kit::macros::*; use ic_kit::Principal; -use crate::common_types::Failure; +use crate::common_types::OperationError; pub struct Admins(pub Vec); @@ -17,19 +17,19 @@ pub fn is_admin(account: &Principal) -> bool { } #[update] -pub fn add_admin(new_admin: Principal) -> Result<(), Failure> { +pub fn add_admin(new_admin: Principal) -> Result<(), OperationError> { if is_admin(&ic::caller()) { ic::get_mut::().0.push(new_admin); return Ok(()); } - Err(Failure::NotAuthorized) + Err(OperationError::NotAuthorized) } #[update] -pub fn remove_admin(admin: Principal) -> Result<(), Failure> { +pub fn remove_admin(admin: Principal) -> Result<(), OperationError> { if is_admin(&ic::caller()) { ic::get_mut::().0.retain(|x| *x != admin); return Ok(()); } - Err(Failure::NotAuthorized) + Err(OperationError::NotAuthorized) } diff --git a/registries/canister_registry/src/registry.rs b/registries/canister_registry/src/registry.rs index fde7a3b..38d6cfe 100644 --- a/registries/canister_registry/src/registry.rs +++ b/registries/canister_registry/src/registry.rs @@ -24,20 +24,20 @@ impl CanisterDB { self.0.get(&canister) } - pub fn add_canister(&mut self, metadata: CanisterMetadata) -> Result<(), Failure> { + pub fn add_canister(&mut self, metadata: CanisterMetadata) -> Result<(), OperationError> { let id: Principal = metadata.principal_id; self.0.insert(metadata.principal_id, metadata); if !self.0.contains_key(&id) { - return Err(Failure::Unknown(String::from( + return Err(OperationError::Unknown(String::from( "Something unexpected happend. Try again.", ))); } Ok(()) } - pub fn remove_canister(&mut self, canister: &Principal) -> Result<(), Failure> { + pub fn remove_canister(&mut self, canister: &Principal) -> Result<(), OperationError> { if !self.0.contains_key(canister) { - return Err(Failure::NonExistentItem); + return Err(OperationError::NonExistentItem); } self.0.remove(canister); Ok(()) @@ -46,6 +46,32 @@ impl CanisterDB { pub fn get_all(&self) -> Vec<&CanisterMetadata> { self.0.values().collect() } + + pub fn get_all_paginated( + &self, + offset: usize, + _limit: usize, + ) -> Result, OperationError> { + let canisters: Vec<&CanisterMetadata> = self.0.values().collect(); + + if offset > canisters.len() { + return Err(OperationError::BadParameters(String::from( + "Offset out of bound.", + ))); + } + + let mut limit = _limit; + + if offset + _limit > canisters.len() { + limit = canisters.len() - offset; + } + + return Ok(canisters[offset..(offset + limit)].to_vec()); + } + + pub fn get_amount(&self) -> usize { + return self.0.values().len(); + } } #[init] @@ -65,15 +91,35 @@ pub fn get(canister: Principal) -> Option<&'static CanisterMetadata> { } #[update] -pub fn add(metadata: CanisterMetadata) -> Result<(), Failure> { +pub fn add(metadata: CanisterMetadata) -> Result<(), OperationError> { if !is_admin(&ic::caller()) { - return Err(Failure::NotAuthorized); - } else if &metadata.name.len() > &NAME_LIMIT - || &metadata.description.len() > &DESCRIPTION_LIMIT - || !validate_url(&metadata.thumbnail) - || !metadata.clone().frontend.map(validate_url).unwrap_or(true) - { - return Err(Failure::BadParameters); + return Err(OperationError::NotAuthorized); + } + + if metadata.name.len() > NAME_LIMIT { + return Err(OperationError::BadParameters(format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + ))); + } + + if metadata.description.len() > DESCRIPTION_LIMIT { + return Err(OperationError::BadParameters(format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + ))); + } + + if !validate_url(&metadata.thumbnail) { + return Err(OperationError::BadParameters(String::from( + "Thumbnail field has to be a url.", + ))); + } + + if metadata.frontend.is_some() && !validate_url(metadata.clone().frontend.unwrap()) { + return Err(OperationError::BadParameters(String::from( + "Frontend field has to be a url.", + ))); } let canister_db = ic::get_mut::(); @@ -81,9 +127,9 @@ pub fn add(metadata: CanisterMetadata) -> Result<(), Failure> { } #[update] -pub fn remove(canister: Principal) -> Result<(), Failure> { +pub fn remove(canister: Principal) -> Result<(), OperationError> { if !is_admin(&ic::caller()) { - return Err(Failure::NotAuthorized); + return Err(OperationError::NotAuthorized); } let canister_db = ic::get_mut::(); canister_db.remove_canister(&canister) @@ -94,3 +140,15 @@ pub fn get_all() -> Vec<&'static CanisterMetadata> { let canister_db = ic::get_mut::(); canister_db.get_all() } + +#[query] +pub fn get_all_paginated( + offset: Option, + limit: Option, +) -> Result { + let db = ic::get_mut::(); + let canisters = db.get_all_paginated(offset.unwrap_or(0), limit.unwrap_or(DEFAULT_LIMIT))?; + let amount = db.get_amount(); + + return Ok(GetAllPaginatedResponse { canisters, amount }); +} diff --git a/registries/canister_registry/src/tests.rs b/registries/canister_registry/src/tests.rs index c95f15c..2d97068 100644 --- a/registries/canister_registry/src/tests.rs +++ b/registries/canister_registry/src/tests.rs @@ -128,7 +128,10 @@ mod tests { let addition_result = add(canister_metadata.clone()); assert!(addition_result.is_err()); - assert_eq!(addition_result.unwrap_err(), Failure::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Thumbnail field has to be a url.")) + ); let added_canister = get(mock_principals::xtc()); assert!(added_canister.is_none()); @@ -156,7 +159,10 @@ mod tests { let addition_result = add(canister_metadata.clone()); assert!(addition_result.is_err()); - assert_eq!(addition_result.unwrap_err(), Failure::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Frontend field has to be a url.",)) + ); let added_canister = get(mock_principals::xtc()); assert!(added_canister.is_none()); @@ -186,7 +192,7 @@ mod tests { let addition_result = add(canister_metadata.clone()); assert!(addition_result.is_err()); - assert_eq!(addition_result.unwrap_err(), Failure::NotAuthorized); + assert_eq!(addition_result.unwrap_err(), OperationError::NotAuthorized); let added_canister = get(mock_principals::xtc()); assert!(added_canister.is_none()); @@ -288,7 +294,7 @@ mod tests { let remove_result = remove(mock_principals::xtc()); assert!(remove_result.is_err()); - assert_eq!(remove_result.unwrap_err(), Failure::NonExistentItem); + assert_eq!(remove_result.unwrap_err(), OperationError::NonExistentItem); } #[test] @@ -318,7 +324,7 @@ mod tests { let remove_result = remove(mock_principals::xtc()); assert!(remove_result.is_err()); - assert_eq!(remove_result.unwrap_err(), Failure::NotAuthorized); + assert_eq!(remove_result.unwrap_err(), OperationError::NotAuthorized); } #[test] @@ -486,11 +492,17 @@ mod tests { // the canister should return an error if we try to remove a non-existent canister let remove_operation = remove(mock_principals::xtc()); - assert_eq!(remove_operation.err().unwrap(), Failure::NonExistentItem); + assert_eq!( + remove_operation.err().unwrap(), + OperationError::NonExistentItem + ); // Bob should not be able to remove a canister because he is not an admin ctx.update_caller(mock_principals::bob()); let remove_operation = remove(mock_principals::xtc()); - assert_eq!(remove_operation.err().unwrap(), Failure::NotAuthorized); + assert_eq!( + remove_operation.err().unwrap(), + OperationError::NotAuthorized + ); } } diff --git a/registries/nft/Cargo.toml b/registries/nft/Cargo.toml index 898233a..0751c9f 100644 --- a/registries/nft/Cargo.toml +++ b/registries/nft/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -ic-cdk = "0.3" +ic-cdk = "0.5" ic-cdk-macros = "0.3" ic-types = "0.1.3" serde = "1.0.116" diff --git a/registries/nft/src/common_types.rs b/registries/nft/src/common_types.rs index 51bc09b..b272fc5 100644 --- a/registries/nft/src/common_types.rs +++ b/registries/nft/src/common_types.rs @@ -28,11 +28,17 @@ pub struct NftCanister { pub details: Vec<(String, DetailValue)>, } +#[derive(CandidType, Clone, Debug, PartialEq)] +pub struct GetAllPaginatedResponse { + pub amount: usize, + pub nfts: Vec<&'static NftCanister>, +} + #[derive(CandidType, Debug, PartialEq, Deserialize, Clone)] pub enum OperationError { NotAuthorized, NonExistentItem, - BadParameters, + BadParameters(String), Unknown(String), } @@ -43,3 +49,4 @@ pub enum RegistryResponse { } pub const CANISTER_REGISTRY_ID: &'static str = "curr3-vaaaa-aaaah-abbdq-cai"; +pub const DEFAULT_LIMIT: usize = 20; diff --git a/registries/nft/src/nft.rs b/registries/nft/src/nft.rs index 6dfdb69..f6c4c22 100644 --- a/registries/nft/src/nft.rs +++ b/registries/nft/src/nft.rs @@ -46,6 +46,32 @@ impl Registry { pub fn get_all(&self) -> Vec<&NftCanister> { self.0.values().collect() } + + pub fn get_all_paginated( + &self, + offset: usize, + _limit: usize, + ) -> Result, OperationError> { + let nfts: Vec<&NftCanister> = self.0.values().collect(); + + if offset > nfts.len() { + return Err(OperationError::BadParameters(String::from( + "Offset out of bound.", + ))); + } + + let mut limit = _limit; + + if offset + _limit > nfts.len() { + limit = nfts.len() - offset; + } + + return Ok(nfts[offset..(offset + limit)].to_vec()); + } + + pub fn get_amount(&self) -> usize { + return self.0.values().len(); + } } #[query] @@ -57,42 +83,71 @@ fn name() -> String { pub async fn add(canister_info: NftCanister) -> Result<(), OperationError> { if !is_admin(&ic::caller()) { return Err(OperationError::NotAuthorized); - } else if !validate_url(&canister_info.thumbnail) { - return Err(OperationError::BadParameters); - } else if canister_info.frontend.is_some() - && !validate_url(&canister_info.frontend.clone().unwrap()) + } + + if canister_info.name.len() > NAME_LIMIT { + return Err(OperationError::BadParameters( + format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + ) + .to_string(), + )); + } + + if canister_info.description.len() > DESCRIPTION_LIMIT { + return Err(OperationError::BadParameters( + format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + ) + .to_string(), + )); + } + + if !validate_url(canister_info.clone().thumbnail) { + return Err(OperationError::BadParameters(String::from( + "Thumbnail field has to be a url.", + ))); + } + + if canister_info.frontend.is_some() && !validate_url(canister_info.clone().frontend.unwrap()) { + return Err(OperationError::BadParameters(String::from( + "Frontend field has to be a url.", + ))); + } + + if canister_info.details.len() < 1 { + return Err(OperationError::BadParameters(String::from( + "Details has to have standard field.", + ))); + } + + if canister_info.details[0].0 != String::from("standard") { + return Err(OperationError::BadParameters(String::from( + "First detail field has to be standard.", + ))); + } + + // Add the collection to the canister registry + let mut call_arg: NftCanister = canister_info.clone(); + call_arg.details = vec![("category".to_string(), DetailValue::Text("NFT".to_string()))]; + + let _registry_add_response: RegistryResponse = match ic::call( + Principal::from_str(CANISTER_REGISTRY_ID).unwrap(), + "add", + (call_arg,), + ) + .await { - return Err(OperationError::BadParameters); - } else if canister_info.details[0].0 != String::from("standard") { - return Err(OperationError::BadParameters); - } else if canister_info.details.len() != 1 { - return Err(OperationError::BadParameters); - } - - let name = canister_info.name.clone(); - if name.len() <= NAME_LIMIT && &canister_info.description.len() <= &DESCRIPTION_LIMIT { - // Add the collection to the canister registry - let mut call_arg: NftCanister = canister_info.clone(); - call_arg.details = vec![("category".to_string(), DetailValue::Text("NFT".to_string()))]; - - let _registry_add_response: RegistryResponse = match ic::call( - Principal::from_str(CANISTER_REGISTRY_ID).unwrap(), - "add", - (call_arg,), - ) - .await - { - Ok((x,)) => x, - Err((_code, msg)) => { - return Err(OperationError::Unknown(msg)); - } - }; - - let db = ic::get_mut::(); - return db.add(canister_info); - } - - Err(OperationError::BadParameters) + Ok((x,)) => x, + Err((_code, msg)) => { + return Err(OperationError::Unknown(msg)); + } + }; + + let db = ic::get_mut::(); + return db.add(canister_info); } #[update] @@ -116,3 +171,15 @@ pub fn get_all() -> Vec<&'static NftCanister> { let db = ic::get_mut::(); db.get_all() } + +#[query] +pub fn get_all_paginated( + offset: Option, + limit: Option, +) -> Result { + let db = ic::get_mut::(); + let nfts = db.get_all_paginated(offset.unwrap_or(0), limit.unwrap_or(DEFAULT_LIMIT))?; + let amount = db.get_amount(); + + return Ok(GetAllPaginatedResponse { nfts, amount }); +} diff --git a/registries/nft/src/tests.rs b/registries/nft/src/tests.rs index 408874b..6f07b90 100644 --- a/registries/nft/src/tests.rs +++ b/registries/nft/src/tests.rs @@ -113,7 +113,16 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters( + format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + ) + .to_string() + ) + ); } #[tokio::test] @@ -140,7 +149,16 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters( + format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + ) + .to_string() + ) + ); } #[tokio::test] @@ -165,7 +183,10 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Thumbnail field has to be a url.")) + ); } #[tokio::test] @@ -190,7 +211,10 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Frontend field has to be a url.")) + ); } #[tokio::test] @@ -215,7 +239,10 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("First detail field has to be standard.",)) + ); } #[tokio::test] @@ -246,7 +273,10 @@ mod tests { let addition_result = add(canister_info).await; assert!(addition_result.clone().is_err()); - assert_eq!(addition_result.unwrap_err(), OperationError::BadParameters); + assert_eq!( + addition_result.unwrap_err(), + OperationError::BadParameters(String::from("Details has to have standard field.")) + ); } #[tokio::test] diff --git a/registries/tokens/Cargo.toml b/registries/tokens/Cargo.toml index 7109980..30d4a8d 100644 --- a/registries/tokens/Cargo.toml +++ b/registries/tokens/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" crate-type = ["cdylib"] [dependencies] -ic-cdk = "0.3" +ic-cdk = "0.5" ic-cdk-macros = "0.3" ic-types = "0.1.3" serde = "1.0.116" diff --git a/registries/tokens/src/common_types.rs b/registries/tokens/src/common_types.rs index 32cab02..b9aff37 100644 --- a/registries/tokens/src/common_types.rs +++ b/registries/tokens/src/common_types.rs @@ -15,6 +15,9 @@ pub enum DetailValue { Vec(Vec), } +pub const DESCRIPTION_LIMIT: usize = 1200; +pub const NAME_LIMIT: usize = 120; + #[derive(CandidType, Deserialize, Clone, Debug, PartialEq)] pub struct Token { pub name: String, @@ -25,11 +28,17 @@ pub struct Token { pub details: Vec<(String, DetailValue)>, } +#[derive(CandidType, Clone, Debug, PartialEq)] +pub struct GetAllPaginatedResponse { + pub amount: usize, + pub tokens: Vec<&'static Token>, +} + #[derive(CandidType, Debug, Deserialize)] pub enum OperationError { NotAuthorized, NonExistentItem, - BadParameters, + BadParameters(String), Unknown(String), } @@ -40,3 +49,4 @@ pub enum RegistryResponse { } pub const CANISTER_REGISTRY_ID: &'static str = "curr3-vaaaa-aaaah-abbdq-cai"; +pub const DEFAULT_LIMIT: usize = 20; diff --git a/registries/tokens/src/tokens.rs b/registries/tokens/src/tokens.rs index 2c1ee6b..175ff3d 100644 --- a/registries/tokens/src/tokens.rs +++ b/registries/tokens/src/tokens.rs @@ -48,6 +48,32 @@ impl TokenRegistry { pub fn get_all(&self) -> Vec<&Token> { self.0.values().collect() } + + pub fn get_all_paginated( + &self, + offset: usize, + _limit: usize, + ) -> Result, OperationError> { + let tokens: Vec<&Token> = self.0.values().collect(); + + if offset > tokens.len() { + return Err(OperationError::BadParameters(String::from( + "Offset out of bound.", + ))); + } + + let mut limit = _limit; + + if offset + _limit > tokens.len() { + limit = tokens.len() - offset; + } + + return Ok(tokens[offset..(offset + limit)].to_vec()); + } + + pub fn get_amount(&self) -> usize { + return self.0.values().len(); + } } #[init] @@ -67,27 +93,63 @@ pub async fn add(token: Token) -> Result<(), OperationError> { return Err(OperationError::NotAuthorized); } - // Check URLs - if !validate_url(&token.thumbnail) || !token.clone().frontend.map(validate_url).unwrap_or(true) - { - return Err(OperationError::BadParameters); + if token.name.len() > NAME_LIMIT { + return Err(OperationError::BadParameters(format!( + "Name field has to be less than {} characters long.", + NAME_LIMIT + ))); + } + + if token.description.len() > DESCRIPTION_LIMIT { + return Err(OperationError::BadParameters(format!( + "Description field has to be less than {} characters long.", + DESCRIPTION_LIMIT + ))); + } + + if !validate_url(&token.thumbnail) { + return Err(OperationError::BadParameters(String::from( + "Thumbnail field has to be a url.", + ))); } - // Check Character Limits - let name = token.name.clone(); - if name.len() > 120 && &token.description.len() > &1200 { - return Err(OperationError::BadParameters); + if token.frontend.is_some() && !validate_url(token.clone().frontend.unwrap()) { + return Err(OperationError::BadParameters(String::from( + "Frontend field has to be a url.", + ))); } - // Check details - if token.details.len() != 4 - || token.details[0].0 != String::from("symbol") - || token.details[1].0 != String::from("standard") - || token.details[2].0 != String::from("total_supply") - || token.details[3].0 != String::from("verified") - || (token.details[3].1 != DetailValue::True && token.details[3].1 != DetailValue::False) + if token.details.len() < 4 { + return Err(OperationError::BadParameters(String::from( + "Details field has to specifiy: symbol, standard, total_supply and verified fields.", + ))); + } + + if token.details[0].0 != String::from("symbol") { + return Err(OperationError::BadParameters(String::from( + "First detail field has to be symbol.", + ))); + } + + if token.details[1].0 != String::from("standard") { + return Err(OperationError::BadParameters(String::from( + "Second detail field has to be standard.", + ))); + } + + if token.details[2].0 != String::from("total_supply") { + return Err(OperationError::BadParameters(String::from( + "Third detail field has to be total_supply.", + ))); + } + + if token.details[0].0 != String::from("verified") + && token.details[3].1 != DetailValue::True + && token.details[3].1 != DetailValue::False { - return Err(OperationError::BadParameters); + return Err(OperationError::BadParameters(String::from( + "Fourth detail field has to be verified (boolean).", + ))); } // Add the collection to the canister registry @@ -135,3 +197,15 @@ pub fn get_all() -> Vec<&'static Token> { let db = ic::get_mut::(); db.get_all() } + +#[query] +pub fn get_all_paginated( + offset: Option, + limit: Option, +) -> Result { + let db = ic::get_mut::(); + let tokens = db.get_all_paginated(offset.unwrap_or(0), limit.unwrap_or(DEFAULT_LIMIT))?; + let amount = db.get_amount(); + + return Ok(GetAllPaginatedResponse { tokens, amount }); +}