Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements of Challenge/hardworking toptal dev #68

Draft
wants to merge 6 commits into
base: first-hardworking-submission
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 143 additions & 1 deletion soroban-react-dapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,148 @@ The contracts workflow happens in the `contracts/` folder. Here you can see that

Every new contract should be in its own folder, and the folder should be named the same name as the name of the contract in its `cargo.toml` file. You can check how the `tweaked_greeting` contract is changed from the `greeting` contract and you can also start from this to build your own contract.

**Challenge contract**

> _This challenge contract is to add authorization controls to the contract so only specific addresses can modify a title. Implement address management and `required_auth` to verify authorized changes._

- `init` instruction

add init instruction to set admin for contract initial.

```rust
// initialize the contract and set the admin
pub fn init(env: Env, admin: Address) -> Result<(), Error> {
let storage = env.storage().instance();
admin.require_auth();
if storage.has(&Assets::Admin) {
return Err(Error::AlreadyInitialized);
}
storage.set(&Assets::Admin, &admin);
Ok(())
}
```

- `add_editor` instruction

admin can add editors who can modify the title

```rust
pub fn add_editor(env: Env, new_editor: Address) -> Result<(), Error>{
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap();
admin.require_auth();

let mut editors: Vec<Address> = storage.get(&Assets::Editors).unwrap_or(Vec::new(&env));
if !editors.contains(&new_editor) {
editors.push_front(new_editor);
env.storage().instance().set(&Assets::Editors, &editors);
Ok(())
} else {
Err(Error::AlreadyExist)
}
}
```

- `remove_editor` instruction

admin can remove editor from their list.

```rust
// remove wallets from editors
pub fn remove_editor(env: Env, editor_to_remove: Address) {
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap();
admin.require_auth();

let mut editors: Vec<Address> = storage.get(&Assets::Editors).unwrap_or(Vec::new(&env));
editors
.first_index_of(&editor_to_remove)
.map(|index| editors.remove(index));
env.storage().instance().set(&Assets::Editors, &editors);
}
```

- `set_title` instruction

editors can modify the tile, if not editor, it cause UnAthorized Error.

```rust
// set the title only available editors
pub fn set_title(env: Env, user: Address, title: String) -> Result<(), Error> {
user.require_auth();
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap_or_else(|| {
// You can log or handle the error here
// In this case, we'll return an error
return Err(Error::NotInitialized);
})?;

let editors: Vec<Address> = storage.get(&Assets::Editors).unwrap_or(Vec::new(&env));
if editors.contains(&user) || user.eq(&admin) {
env.storage().instance().set(&Assets::Title, &title);
Ok(())
} else {
Err(Error::Unauthorized)
}
}
```

**Test Contract**

- `require_auth` test

```rust
// mofify the title with editors
client.set_title(&new_editor, &String::from_str(&env, "Hello, Stellar"));

// test for require_auth
assert_eq!(
env.auths(),
std::vec![(
// Address for which authorization check is performed
new_editor.clone(),
// Invocation tree that needs to be authorized
AuthorizedInvocation {
// Function that is authorized. Can be a contract function or
// a host function that requires authorization.
function: AuthorizedFunction::Contract((
// Address of the called contract
contract_id.clone(),
// Name of the called function
symbol_short!("set_title"),
// Arguments used to call `set_title`
vec![&env, new_editor.to_val(), String::from_str(&env, "Hello, Stellar").into()]
)),
// The contract doesn't call any other contracts that require
// authorization,
sub_invocations: std::vec![]
}
)]
);
```

- `set_title` and `add_editor` test

```rust
// test either everyone access to modify title or not
let _ = client.try_set_title(&new_editor, &String::from_str(&env, "Hello, Stellar"));
let client_title = client.read_title();
assert_eq!(client_title, String::from_str(&env, "Default Title"));

// test with new title
let client_new_title = client.read_title();
assert_eq!(client_new_title, String::from_str(&env, "Hello, Stellar"));

// remove editors by admin
let _ = client.remove_editor(&new_editor);
let admins = client.fetch_editors();
assert_eq!(admins.len(), 1);
```





To build the contracts you can simply invoke the `make` command which will recursively build all contracts by propagating the `make` command to subfolders. Each contract needs to have its own `Makefile` for this to work. The `Makefile` from the greeting contract is a generic one and can be copied and paste to use with any of your new contract.

If you are not familiar or comfortable with Makefiles you can simply go in the directory of the contract you want to compile and run
Expand Down Expand Up @@ -96,4 +238,4 @@ You then need to adapt the `contractInvoke()` calls in these functions to match

Finally feel, of course, free to change the front-end how you wish, to match your desired functionalities.

*Good luck building!*
*Good luck building!*
2 changes: 1 addition & 1 deletion soroban-react-dapp/contracts/deployments.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
{
"contractId": "greeting",
"networkPassphrase": "Test SDF Network ; September 2015",
"contractAddress": "CBFLKMLYVCBP3MM6FTJZV57GZDHZ5G7CPNPTPQEVLEY2EZS6CRH54CAF"
"contractAddress": "CC7LM32RVA7FSWIDQS7DSYAZWKA2NBIS2PSIDX2NZOCKFVZWBMDP3ZV3"
}
]
18 changes: 14 additions & 4 deletions soroban-react-dapp/contracts/greeting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct TitleContract;
pub enum Error {
Unauthorized = 1,
AlreadyInitialized = 2,
NotInitialized = 3,
AlreadyExist = 4, // when editor already exist, add_editor invoke this error
}

#[contractimpl]
Expand All @@ -29,7 +31,12 @@ impl TitleContract {
pub fn set_title(env: Env, user: Address, title: String) -> Result<(), Error> {
user.require_auth();
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap();
let admin: Address = storage.get(&Assets::Admin).unwrap_or_else(|| {
// You can log or handle the error here
// In this case, we'll return an error
return Err(Error::NotInitialized);
})?;

let editors: Vec<Address> = storage.get(&Assets::Editors).unwrap_or(Vec::new(&env));
if editors.contains(&user) || user.eq(&admin) {
env.storage().instance().set(&Assets::Title, &title);
Expand All @@ -50,7 +57,7 @@ impl TitleContract {
/// ***** Address Management ***** ///

// add wallet address for editors
pub fn add_editor(env: Env, new_editor: Address) {
pub fn add_editor(env: Env, new_editor: Address) -> Result<(), Error>{
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap();
admin.require_auth();
Expand All @@ -59,18 +66,21 @@ impl TitleContract {
if !editors.contains(&new_editor) {
editors.push_front(new_editor);
env.storage().instance().set(&Assets::Editors, &editors);
Ok(())
} else {
Err(Error::AlreadyExist)
}
}

// remove wallets from editors
pub fn remove_editor(env: Env, remover: Address) {
pub fn remove_editor(env: Env, editor_to_remove: Address) {
let storage = env.storage().instance();
let admin: Address = storage.get(&Assets::Admin).unwrap();
admin.require_auth();

let mut editors: Vec<Address> = storage.get(&Assets::Editors).unwrap_or(Vec::new(&env));
editors
.first_index_of(&remover)
.first_index_of(&editor_to_remove)
.map(|index| editors.remove(index));
env.storage().instance().set(&Assets::Editors, &editors);
}
Expand Down
31 changes: 30 additions & 1 deletion soroban-react-dapp/contracts/greeting/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#![cfg(test)]

extern crate std;

use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env, String};
use soroban_sdk::{symbol_short, testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, vec, Address, Env, String};

#[test]
fn test() {
Expand Down Expand Up @@ -34,6 +36,33 @@ fn test() {

// mofify the title with editors
client.set_title(&new_editor, &String::from_str(&env, "Hello, Stellar"));

// test for require_auth
assert_eq!(
env.auths(),
std::vec![(
// Address for which authorization check is performed
new_editor.clone(),
// Invocation tree that needs to be authorized
AuthorizedInvocation {
// Function that is authorized. Can be a contract function or
// a host function that requires authorization.
function: AuthorizedFunction::Contract((
// Address of the called contract
contract_id.clone(),
// Name of the called function
symbol_short!("set_title"),
// Arguments used to call `set_title`
vec![&env, new_editor.to_val(), String::from_str(&env, "Hello, Stellar").into()]
)),
// The contract doesn't call any other contracts that require
// authorization,
sub_invocations: std::vec![]
}
)]
);

// test with new title
let client_new_title = client.read_title();
assert_eq!(client_new_title, String::from_str(&env, "Hello, Stellar"));

Expand Down
Loading