From 966923e9008482ad43735279c235bd75c1f4df3f Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 02:02:18 +0900 Subject: [PATCH 01/14] feat: build own contract --- soroban-react-dapp/contracts/deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index da99780..abd02d9 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CDWGVPSUXXSGABQ663FVV4TZJH4Q2R3HVAKTKWFFFMWPF23O7KMNS4KU" + "contractAddress": "CDLD5YFQ3PSNUWQTY2UCD3UB7XKJE27ZEXPHR6XKU3UOQLRH66J6RLF3" } ] \ No newline at end of file From 658bdb6922e71b0ef783db4da9e0842a261c06eb Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 02:33:31 +0900 Subject: [PATCH 02/14] refactor: update smart contract with challenge's requirement --- .../contracts/greeting/src/lib.rs | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index b6c9990..e2815b0 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -1,24 +1,89 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, Env, Symbol, symbol_short, String}; +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Symbol, Vec}; const TITLE: Symbol = symbol_short!("TITLE"); - #[contract] pub struct TitleContract; +#[contracterror] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + Unauthorized = 1, + AlreadyInitialized = 2, +} + #[contractimpl] impl TitleContract { + // 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(()) + } - pub fn set_title(env: Env, title: String) { - env.storage().instance().set(&TITLE, &title) + // set the title only available editors + pub fn set_title(env: Env, user: Address, title: String) { + user.require_auth(); + let storage = env.storage().instance(); + let admin: Address = storage.get(&Assets::Admin).unwrap(); + let editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + if editors.contains(&user) || caller.eq(&admin) { + env.storage().instance().set(&Assets::Title, &title); + Ok(()) + } else { + Err(Error::Unauthorized) + } } + // read the title pub fn read_title(env: Env) -> String { - env.storage().instance().get(&TITLE) + env.storage() + .instance() + .get(&Assets::Title) .unwrap_or(String::from_str(&env, "Default Title")) } - -} + + /// ***** Address Management ***** /// + + // add wallet address for editors + pub fn add_editor(env: Env, new_editor: Address) { + let storage = env.storage().instance(); + let admin: Address = storage.get(&Assets::Admin).unwrap(); + admin.require_auth(); + + let mut editors: Vec
= 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); + } + } + + // remove wallets from editors + pub fn remove_admin(env: Env, remover: Address) { + let storage = env.storage().instance(); + let admin: Address = storage.get(&Assets::Admin).unwrap(); + admin.require_auth(); + + let mut admins: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + admins + .first_index_of(&remover) + .map(|index| admins.remove(index)); + env.storage().instance().set(&Assets::Editors, &admins); + } +} + +#[derive(Clone)] +#[contracttype] +pub enum Assets { + Admin, + Editors, + Title, +} mod test; From ca3391f60e58c39f0e200fead2d64f48ae17cfdc Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 02:40:21 +0900 Subject: [PATCH 03/14] chore: add some types --- soroban-react-dapp/contracts/greeting/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index e2815b0..afe9e03 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -1,5 +1,8 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Symbol, Vec}; +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, + Symbol, Vec, +}; const TITLE: Symbol = symbol_short!("TITLE"); @@ -28,12 +31,12 @@ impl TitleContract { } // set the title only available editors - pub fn set_title(env: Env, user: Address, title: String) { + 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 editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); - if editors.contains(&user) || caller.eq(&admin) { + if editors.contains(&user) || user.eq(&admin) { env.storage().instance().set(&Assets::Title, &title); Ok(()) } else { From 364966831c30717f5a1b02f28fd69ddf6627a4e9 Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 02:42:16 +0900 Subject: [PATCH 04/14] feat: finish and deploy smart contract --- soroban-react-dapp/contracts/deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index abd02d9..455d673 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CDLD5YFQ3PSNUWQTY2UCD3UB7XKJE27ZEXPHR6XKU3UOQLRH66J6RLF3" + "contractAddress": "CC7S2ADZBM7WUHD6JN6UPDQ7DY4QOCER7QQHXMGKUTJWXVTPXJUG7C3E" } ] \ No newline at end of file From 7cca58fac35bfdceb3d9d0248443051b2dcaf551 Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 10:19:38 +0900 Subject: [PATCH 05/14] feat: add test script --- soroban-react-dapp/contracts/deployments.json | 2 +- .../contracts/greeting/src/lib.rs | 2 +- .../contracts/greeting/src/test.rs | 37 +- .../greeting/test_snapshots/test/test.1.json | 788 ++++++++++++++++++ 4 files changed, 820 insertions(+), 9 deletions(-) create mode 100644 soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index 455d673..d6c40b2 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CC7S2ADZBM7WUHD6JN6UPDQ7DY4QOCER7QQHXMGKUTJWXVTPXJUG7C3E" + "contractAddress": "CAMHGS47DPLS7NXH5MP4AS74NOUBMCMUKG27K3PZJPKHWJNWUWIETDZV" } ] \ No newline at end of file diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index afe9e03..ff9a49b 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -68,7 +68,7 @@ impl TitleContract { } // remove wallets from editors - pub fn remove_admin(env: Env, remover: Address) { + pub fn remove_editor(env: Env, remover: Address) { let storage = env.storage().instance(); let admin: Address = storage.get(&Assets::Admin).unwrap(); admin.require_auth(); diff --git a/soroban-react-dapp/contracts/greeting/src/test.rs b/soroban-react-dapp/contracts/greeting/src/test.rs index b95c919..bfcde2c 100755 --- a/soroban-react-dapp/contracts/greeting/src/test.rs +++ b/soroban-react-dapp/contracts/greeting/src/test.rs @@ -1,21 +1,44 @@ #![cfg(test)] use super::*; -use soroban_sdk::{Env, String}; - +use soroban_sdk::{testutils::Address as _, Address, Env, String}; #[test] fn test() { + // test to mock the all auths let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, TitleContract); let client = TitleContractClient::new(&env, &contract_id); - let client_default_title = client.read_title(); - assert_eq!(client_default_title, String::from_slice(&env, "Default Title")); + let admin = Address::generate(&env); + let new_editor = Address::generate(&env); + + // init by admin + client.init(&admin); + + let client_default_title = client.read_title(); + assert_eq!( + client_default_title, + String::from_str(&env, "Default Title") + ); + + // 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")); - client.set_title(&String::from_slice(&env, "My New Title")); - let client_new_title = client.read_title(); + // give edit access + client.add_editor(&new_editor); - assert_eq!(client_new_title, String::from_slice(&env, "My New Title")); + // mofify the title with editors + client.set_title(&new_editor, &String::from_str(&env, "Hello, Stellar")); + 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.read_admins(); + // assert_eq!(admins.len(), 1); } diff --git a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json new file mode 100644 index 0000000..c7184d9 --- /dev/null +++ b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json @@ -0,0 +1,788 @@ +{ + "generators": { + "address": 3, + "nonce": 0 + }, + "auth": [ + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "init", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "add_editor", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "set_title", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "string": "Hello, Stellar" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "remove_editor", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Editors" + } + ] + }, + "val": { + "vec": [] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Title" + } + ] + }, + "val": { + "string": "Hello, Stellar" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "ledger_key_nonce": { + "nonce": 2032731177588607455 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "init" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "read_title" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "read_title" + } + ], + "data": { + "string": "Default Title" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "set_title" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "string": "Hello, Stellar" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_title" + } + ], + "data": { + "error": { + "contract": 1 + } + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 1 + } + } + ], + "data": { + "string": "escalating Ok(ScErrorType::Contract) frame-exit to Err" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "contract": 1 + } + } + ], + "data": { + "vec": [ + { + "string": "contract try_call failed" + }, + { + "symbol": "set_title" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "string": "Hello, Stellar" + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "read_title" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "read_title" + } + ], + "data": { + "string": "Default Title" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "add_editor" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "add_editor" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "set_title" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "string": "Hello, Stellar" + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "set_title" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "read_title" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "read_title" + } + ], + "data": { + "string": "Hello, Stellar" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "remove_editor" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "remove_editor" + } + ], + "data": "void" + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file From cf74475b149c6003787f88f84fb389180dc7b22a Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 10:26:58 +0900 Subject: [PATCH 06/14] chore: remove unnecessary values --- soroban-react-dapp/contracts/greeting/src/lib.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index ff9a49b..428c483 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -1,10 +1,5 @@ #![no_std] -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, - Symbol, Vec, -}; - -const TITLE: Symbol = symbol_short!("TITLE"); +use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String, Vec}; #[contract] pub struct TitleContract; From 7722c6def504f7549aed17d4ab210ac48768df2e Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 12:01:09 +0900 Subject: [PATCH 07/14] feat: add admin section in GreeterContractInteraction --- soroban-react-dapp/contracts/deployments.json | 2 +- .../contracts/greeting/src/lib.rs | 7 + .../contracts/greeting/src/test.rs | 4 +- .../greeting/test_snapshots/test/test.1.json | 50 +++- .../web3/GreeterContractInteractions.tsx | 243 +++++++++++++++--- 5 files changed, 268 insertions(+), 38 deletions(-) diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index d6c40b2..0abc5fd 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CAMHGS47DPLS7NXH5MP4AS74NOUBMCMUKG27K3PZJPKHWJNWUWIETDZV" + "contractAddress": "CAAIO5RZEHFLUKN2F7LSGQOPSAJ6IJNJSVNLHTLX3M2VOGJV6ZBAI6GJ" } ] \ No newline at end of file diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index 428c483..7d01cd7 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -74,6 +74,13 @@ impl TitleContract { .map(|index| admins.remove(index)); env.storage().instance().set(&Assets::Editors, &admins); } + + // fetch the editor lists + pub fn fetch_editors(env: Env) -> Vec
{ + let storage = env.storage().instance(); + let editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + editors + } } #[derive(Clone)] diff --git a/soroban-react-dapp/contracts/greeting/src/test.rs b/soroban-react-dapp/contracts/greeting/src/test.rs index bfcde2c..6fab238 100755 --- a/soroban-react-dapp/contracts/greeting/src/test.rs +++ b/soroban-react-dapp/contracts/greeting/src/test.rs @@ -39,6 +39,6 @@ fn test() { // remove editors by admin let _ = client.remove_editor(&new_editor); - // let admins = client.read_admins(); - // assert_eq!(admins.len(), 1); + let admins = client.fetch_editors(); + assert_eq!(admins.len(), 0); } diff --git a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json index c7184d9..7b4f00d 100644 --- a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json +++ b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json @@ -86,7 +86,8 @@ "sub_invocations": [] } ] - ] + ], + [] ], "ledger": { "protocol_version": 21, @@ -783,6 +784,53 @@ } }, "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "fetch_editors" + } + ], + "data": "void" + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_return" + }, + { + "symbol": "fetch_editors" + } + ], + "data": { + "vec": [] + } + } + } + }, + "failed_call": false } ] } \ No newline at end of file diff --git a/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx b/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx index e22e0a8..8a0dea5 100644 --- a/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx +++ b/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx @@ -11,9 +11,12 @@ import React from 'react' import Link from 'next/link' import { contractInvoke, useRegisteredContract } from '@soroban-react/contracts' -import { nativeToScVal, xdr } from '@stellar/stellar-sdk' +import { Address, nativeToScVal, xdr } from '@stellar/stellar-sdk' +import { Adamina } from 'next/font/google' type UpdateGreetingValues = { newMessage: string } +type AddEditorValues = { newEditor: string } +type RemoveEditorValues = { remover: string } export const GreeterContractInteractions: FC = () => { const sorobanContext = useSorobanReact() @@ -21,22 +24,27 @@ export const GreeterContractInteractions: FC = () => { const [, setFetchIsLoading] = useState(false) const [updateIsLoading, setUpdateIsLoading] = useState(false) - const { register, handleSubmit } = useForm() - + const { register: registerGreeting, handleSubmit: handleSubmitGreeting } = useForm() + const { register: registerEditor, handleSubmit: handleSubmitAdmin } = useForm() + // Two options are existing for fetching data from the blockchain // The first consists on using the useContractValue hook demonstrated in the useGreeting.tsx file // This hook simulate the transation to happen on the bockchain and allow to read the value from it // Its main advantage is to allow for updating the value display on the frontend without any additional action // const {isWrongConnection, fetchedGreeting} = useGreeting({ sorobanContext }) - + // The other option, maybe simpler to understand and implement is the one implemented here // Where we fetch the value manually with each change of the state. // We trigger the fetch with flipping the value of updateFrontend or refreshing the page - + const [fetchedGreeting, setGreeterMessage] = useState() const [updateFrontend, toggleUpdate] = useState(true) const [contractAddressStored, setContractAddressStored] = useState() + const [isAdmin, setIsAdmin] = useState(false) + const [isEditor, setIsEditor] = useState(false) + const [editors, setEditors] = useState() + // Retrieve the deployed contract object from contract Registry const contract = useRegisteredContract("greeting") @@ -74,14 +82,42 @@ export const GreeterContractInteractions: FC = () => { setFetchIsLoading(false) } } - },[sorobanContext,contract]) + }, [sorobanContext, contract]) - useEffect(() => {void fetchGreeting()}, [updateFrontend,fetchGreeting]) + // fetch the editor lists + const fetchEditors = useCallback(async () => { + const contractAddress = contract?.deploymentInfo.contractAddress + setContractAddressStored(contractAddress) + setFetchIsLoading(true) + try { + const result = await contract?.invoke({ + method: 'fetch_editors', + args: [] + }) + + if (!result) return + + const result_arr = StellarSdk.scValToNative(result as StellarSdk.xdr.ScVal) as string[] + setIsAdmin(result_arr.indexOf(sorobanContext.address!!) == 0) + setIsEditor(result_arr.indexOf(sorobanContext.address!!) >= 0) + setEditors(result_arr) + } catch (e) { + console.error(e) + toast.error('Error while fetching editors. Try again…') + } finally { + setFetchIsLoading(false) + } + }, [sorobanContext, contract]) + + useEffect(() => { + fetchGreeting() + fetchEditors() + }, [updateFrontend, fetchGreeting, fetchEditors]) const { activeChain, server, address } = sorobanContext - const updateGreeting = async ({ newMessage }: UpdateGreetingValues ) => { + const updateGreeting = async ({ newMessage }: UpdateGreetingValues) => { if (!address) { console.log("Address is not defined") toast.error('Wallet is not connected. Try again...') @@ -106,17 +142,17 @@ export const GreeterContractInteractions: FC = () => { try { const result = await contract?.invoke({ method: 'set_title', - args: [nativeToScVal(newMessage, {type: "string"})], + args: [nativeToScVal(Address.fromString(address)), nativeToScVal(newMessage, { type: "string" })], signAndSend: true }) console.log('🚀 « result:', result); - + if (true) { toast.success("New greeting successfully published!") } else { toast.error("Greeting unsuccessful...") - + } } catch (e) { console.error(e) @@ -124,15 +160,150 @@ export const GreeterContractInteractions: FC = () => { } finally { setUpdateIsLoading(false) toggleUpdate(!updateFrontend) - } + } // await sorobanContext.connect(); } } } + const addEditor = async ({ newEditor }: AddEditorValues) => { + if (!address) { + console.log("Address is not defined") + toast.error('Wallet is not connected. Try again...') + return + } + else if (!server) { + console.log("Server is not setup") + toast.error('Server is not defined. Unabled to connect to the blockchain') + return + } + else { + const currentChain = activeChain?.name?.toLocaleLowerCase() + if (!currentChain) { + console.log("No active chain") + toast.error('Wallet not connected. Try again…') + return + } + else { + + setUpdateIsLoading(true) - if(!contract){ + try { + const result = await contract?.invoke({ + method: 'add_editor', + args: [nativeToScVal(Address.fromString(newEditor))], + signAndSend: true + }) + console.log('🚀 « result:', result); + + if (true) { + toast.success("New Editor successfully added!") + } + else { + toast.error("adding unsuccessful...") + + } + } catch (e) { + console.error(e) + toast.error('Error while sending tx. Try again…') + } finally { + setUpdateIsLoading(false) + toggleUpdate(!updateFrontend) + } + + // await sorobanContext.connect(); + } + } + } + + const removeEditor = async ({ remover }: RemoveEditorValues) => { + if (!address) { + console.log("Address is not defined") + toast.error('Wallet is not connected. Try again...') + return + } + else if (!server) { + console.log("Server is not setup") + toast.error('Server is not defined. Unabled to connect to the blockchain') + return + } + else { + const currentChain = activeChain?.name?.toLocaleLowerCase() + if (!currentChain) { + console.log("No active chain") + toast.error('Wallet not connected. Try again…') + return + } + else { + + setUpdateIsLoading(true) + + try { + const result = await contract?.invoke({ + method: 'remove_editor', + args: [nativeToScVal(Address.fromString(remover))], + signAndSend: true + }) + console.log('🚀 « result:', result); + + if (true) { + toast.success("Editor successfully removed!") + } + else { + toast.error("removing unsuccessful...") + + } + } catch (e) { + console.error(e) + toast.error('Error while sending tx. Try again…') + } finally { + setUpdateIsLoading(false) + toggleUpdate(!updateFrontend) + } + + // await sorobanContext.connect(); + } + } + } + + const adminManagement = ( +
+

Contract Admins

+ + {editors?.map((editor, i) => ( +
+

- {editor}

+ {i !== 0 && } +
+ ))} + {/* Input for adding new admins */} +
+
+
+
+ + + Add Admin + + + + +
+
+
+ ) + if (!contract) { return (

Greeter Smart Contract

@@ -142,7 +313,7 @@ export const GreeterContractInteractions: FC = () => { { sorobanContext?.deployments?.map((d, i) => (

- {d.networkPassphrase}

- )) + )) }
@@ -153,6 +324,8 @@ export const GreeterContractInteractions: FC = () => { <>

Greeter Smart Contract

+ {/* Admin section */} + {isAdmin && adminManagement} {/* Fetched Greeting */} @@ -166,29 +339,31 @@ export const GreeterContractInteractions: FC = () => { {/* Update Greeting */} - -
- - - Update Greeting - - - - -
-
+ {isEditor && + +
+ + + Update Greeting + + + + +
+
+ } {/* Contract Address */}

- + {contractAddressStored ? {contractAddressStored} : "Loading address.."}

From 5fecef8f8e036e4897af6688e07af7368406ec96 Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:22:46 +0900 Subject: [PATCH 08/14] feat: finish challenge --- soroban-react-dapp/contracts/deployments.json | 2 +- .../contracts/greeting/src/lib.rs | 12 +++--- .../contracts/greeting/src/test.rs | 2 +- .../greeting/test_snapshots/test/test.1.json | 6 ++- .../contracts/scripts/deploy.ts | 14 +++++-- .../web3/GreeterContractInteractions.tsx | 37 +++++++++++-------- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index 0abc5fd..4b7ef89 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CAAIO5RZEHFLUKN2F7LSGQOPSAJ6IJNJSVNLHTLX3M2VOGJV6ZBAI6GJ" + "contractAddress": "CBFLKMLYVCBP3MM6FTJZV57GZDHZ5G7CPNPTPQEVLEY2EZS6CRH54CAF" } ] \ No newline at end of file diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index 7d01cd7..3a01dde 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -68,17 +68,19 @@ impl TitleContract { let admin: Address = storage.get(&Assets::Admin).unwrap(); admin.require_auth(); - let mut admins: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); - admins + let mut editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + editors .first_index_of(&remover) - .map(|index| admins.remove(index)); - env.storage().instance().set(&Assets::Editors, &admins); + .map(|index| editors.remove(index)); + env.storage().instance().set(&Assets::Editors, &editors); } // fetch the editor lists pub fn fetch_editors(env: Env) -> Vec
{ let storage = env.storage().instance(); - let editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + let admin: Address = storage.get(&Assets::Admin).unwrap(); + let mut editors: Vec
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); + editors.push_front(admin); editors } } diff --git a/soroban-react-dapp/contracts/greeting/src/test.rs b/soroban-react-dapp/contracts/greeting/src/test.rs index 6fab238..2379db5 100755 --- a/soroban-react-dapp/contracts/greeting/src/test.rs +++ b/soroban-react-dapp/contracts/greeting/src/test.rs @@ -40,5 +40,5 @@ fn test() { // remove editors by admin let _ = client.remove_editor(&new_editor); let admins = client.fetch_editors(); - assert_eq!(admins.len(), 0); + assert_eq!(admins.len(), 1); } diff --git a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json index 7b4f00d..d48b4d1 100644 --- a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json +++ b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test.1.json @@ -825,7 +825,11 @@ } ], "data": { - "vec": [] + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] } } } diff --git a/soroban-react-dapp/contracts/scripts/deploy.ts b/soroban-react-dapp/contracts/scripts/deploy.ts index 8cc5614..19622f4 100644 --- a/soroban-react-dapp/contracts/scripts/deploy.ts +++ b/soroban-react-dapp/contracts/scripts/deploy.ts @@ -1,6 +1,6 @@ -import { Horizon } from '@stellar/stellar-sdk'; +import { Address, Horizon, nativeToScVal, xdr } from '@stellar/stellar-sdk'; import { AddressBook } from '../utils/address_book.js'; -import { airdropAccount, deployContract, installContract} from '../utils/contract.js'; +import { airdropAccount, deployContract, installContract, invokeContract} from '../utils/contract.js'; import { config } from '../utils/env_config.js'; export async function deployContracts(addressBook: AddressBook, contracts_to_deploy: Array) { @@ -23,7 +23,15 @@ export async function deployContracts(addressBook: AddressBook, contracts_to_dep console.log(`Contract ID of ${contract_name} is ${contractId}\n\n`) } - + // init contract with the admin account + console.log('-------------------------------------------------------'); + console.log('Initializing Contract'); + console.log('-------------------------------------------------------'); + let accountAddress = Address.fromString(loadedConfig.admin.publicKey()); + const params: xdr.ScVal[] = [ + nativeToScVal(accountAddress) + ]; + await invokeContract('greeting', addressBook, 'init', params, loadedConfig.admin); } const network = process.argv[2]; diff --git a/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx b/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx index 8a0dea5..95386b8 100644 --- a/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx +++ b/soroban-react-dapp/src/components/web3/GreeterContractInteractions.tsx @@ -23,7 +23,8 @@ export const GreeterContractInteractions: FC = () => { const [, setFetchIsLoading] = useState(false) - const [updateIsLoading, setUpdateIsLoading] = useState(false) + const [updateGreetingIsLoading, setUpdateGreetingIsLoading] = useState(false) + const [updateEditorIsLoading, setUpdateEditorIsLoading] = useState(false) const { register: registerGreeting, handleSubmit: handleSubmitGreeting } = useForm() const { register: registerEditor, handleSubmit: handleSubmitAdmin } = useForm() @@ -137,7 +138,7 @@ export const GreeterContractInteractions: FC = () => { } else { - setUpdateIsLoading(true) + setUpdateGreetingIsLoading(true) try { const result = await contract?.invoke({ @@ -158,7 +159,7 @@ export const GreeterContractInteractions: FC = () => { console.error(e) toast.error('Error while sending tx. Try again…') } finally { - setUpdateIsLoading(false) + setUpdateGreetingIsLoading(false) toggleUpdate(!updateFrontend) } @@ -187,7 +188,7 @@ export const GreeterContractInteractions: FC = () => { } else { - setUpdateIsLoading(true) + setUpdateEditorIsLoading(true) try { const result = await contract?.invoke({ @@ -208,7 +209,7 @@ export const GreeterContractInteractions: FC = () => { console.error(e) toast.error('Error while sending tx. Try again…') } finally { - setUpdateIsLoading(false) + setUpdateEditorIsLoading(false) toggleUpdate(!updateFrontend) } @@ -237,7 +238,7 @@ export const GreeterContractInteractions: FC = () => { } else { - setUpdateIsLoading(true) + setUpdateEditorIsLoading(true) try { const result = await contract?.invoke({ @@ -258,7 +259,7 @@ export const GreeterContractInteractions: FC = () => { console.error(e) toast.error('Error while sending tx. Try again…') } finally { - setUpdateIsLoading(false) + setUpdateEditorIsLoading(false) toggleUpdate(!updateFrontend) } @@ -267,13 +268,17 @@ export const GreeterContractInteractions: FC = () => { } } + const trimAddress = (string: string, sideLength: number = 16) => { + return string.length > sideLength * 2 ? string.substring(0, sideLength) + '…' + + string.substring(string.length - sideLength) : string + } + const adminManagement = (
-

Contract Admins

{editors?.map((editor, i) => (
-

- {editor}

+

- {trimAddress(editor)}

{i !== 0 && } @@ -286,15 +291,15 @@ export const GreeterContractInteractions: FC = () => {
- Add Admin - + Add Editor + @@ -345,14 +350,14 @@ export const GreeterContractInteractions: FC = () => { Update Greeting - + From 4d62f2511c4237f3306da15c720d54910b137172 Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:51:09 -0400 Subject: [PATCH 09/14] fix: add not initialize error handling --- soroban-react-dapp/contracts/greeting/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index 3a01dde..dfb93d5 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -10,6 +10,7 @@ pub struct TitleContract; pub enum Error { Unauthorized = 1, AlreadyInitialized = 2, + NotInitialized = 3, } #[contractimpl] @@ -29,7 +30,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
= storage.get(&Assets::Editors).unwrap_or(Vec::new(&env)); if editors.contains(&user) || user.eq(&admin) { env.storage().instance().set(&Assets::Title, &title); From 153f799fbd5b653c77d8661e226a9fcd2786fbef Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:00:26 -0400 Subject: [PATCH 10/14] fix: add editor alreadyExist error handling --- soroban-react-dapp/contracts/greeting/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index dfb93d5..8dff005 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -11,6 +11,7 @@ pub enum Error { Unauthorized = 1, AlreadyInitialized = 2, NotInitialized = 3, + AlreadyExist = 4, // when editor already exist, add_editor invoke this error } #[contractimpl] @@ -56,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(); @@ -65,6 +66,9 @@ impl TitleContract { if !editors.contains(&new_editor) { editors.push_front(new_editor); env.storage().instance().set(&Assets::Editors, &editors); + Ok(()) + } else { + Err(Error::AlreadyExist) } } From 79ca7062728c1a362cf0d9e17e7e6e15085e6fca Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:01:28 -0400 Subject: [PATCH 11/14] chore: change parameter name --- soroban-react-dapp/contracts/greeting/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soroban-react-dapp/contracts/greeting/src/lib.rs b/soroban-react-dapp/contracts/greeting/src/lib.rs index 8dff005..0f91f60 100755 --- a/soroban-react-dapp/contracts/greeting/src/lib.rs +++ b/soroban-react-dapp/contracts/greeting/src/lib.rs @@ -73,14 +73,14 @@ impl TitleContract { } // 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
= 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); } From 06a6966e6afeffe64ede03c60ba774a32384edaa Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:08:15 -0400 Subject: [PATCH 12/14] feat: add test for require_auth --- .../contracts/greeting/src/test.rs | 31 ++- .../test/test_unauthorized_set_greet.1.json | 248 ++++++++++++++++++ 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 soroban-react-dapp/contracts/greeting/test_snapshots/test/test_unauthorized_set_greet.1.json diff --git a/soroban-react-dapp/contracts/greeting/src/test.rs b/soroban-react-dapp/contracts/greeting/src/test.rs index 2379db5..e0851cf 100755 --- a/soroban-react-dapp/contracts/greeting/src/test.rs +++ b/soroban-react-dapp/contracts/greeting/src/test.rs @@ -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() { @@ -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")); diff --git a/soroban-react-dapp/contracts/greeting/test_snapshots/test/test_unauthorized_set_greet.1.json b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test_unauthorized_set_greet.1.json new file mode 100644 index 0000000..cf80df5 --- /dev/null +++ b/soroban-react-dapp/contracts/greeting/test_snapshots/test/test_unauthorized_set_greet.1.json @@ -0,0 +1,248 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [] + ], + "ledger": { + "protocol_version": 21, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "fn_call" + }, + { + "bytes": "0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "symbol": "init" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "auth": "invalid_action" + } + } + ], + "data": { + "vec": [ + { + "string": "Unauthorized function call for address" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "auth": "invalid_action" + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "auth": "invalid_action" + } + } + ], + "data": { + "string": "caught error from function" + } + } + } + }, + "failed_call": true + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "auth": "invalid_action" + } + } + ], + "data": { + "vec": [ + { + "string": "contract call failed" + }, + { + "symbol": "init" + }, + { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + } + ] + } + } + } + }, + "failed_call": false + }, + { + "event": { + "ext": "v0", + "contract_id": null, + "type_": "diagnostic", + "body": { + "v0": { + "topics": [ + { + "symbol": "error" + }, + { + "error": { + "auth": "invalid_action" + } + } + ], + "data": { + "string": "escalating error to panic" + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file From 62be925e1ace7b81ab203e8dbca16ecfeff563a7 Mon Sep 17 00:00:00 2001 From: hardworking-toptal-dev <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:34:31 -0400 Subject: [PATCH 13/14] feat: add deploy address --- soroban-react-dapp/contracts/deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soroban-react-dapp/contracts/deployments.json b/soroban-react-dapp/contracts/deployments.json index 4b7ef89..2c87ca0 100644 --- a/soroban-react-dapp/contracts/deployments.json +++ b/soroban-react-dapp/contracts/deployments.json @@ -7,6 +7,6 @@ { "contractId": "greeting", "networkPassphrase": "Test SDF Network ; September 2015", - "contractAddress": "CBFLKMLYVCBP3MM6FTJZV57GZDHZ5G7CPNPTPQEVLEY2EZS6CRH54CAF" + "contractAddress": "CC7LM32RVA7FSWIDQS7DSYAZWKA2NBIS2PSIDX2NZOCKFVZWBMDP3ZV3" } ] \ No newline at end of file From e09613846dcf7b7caedd37851c9d54a52616fd46 Mon Sep 17 00:00:00 2001 From: hardworking <161673729+hardworking-toptal-dev@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:49:11 -0700 Subject: [PATCH 14/14] feat: update README.md --- soroban-react-dapp/README.md | 144 ++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/soroban-react-dapp/README.md b/soroban-react-dapp/README.md index 766edd8..3ed1492 100644 --- a/soroban-react-dapp/README.md +++ b/soroban-react-dapp/README.md @@ -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
= 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
= 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
= 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 @@ -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!* \ No newline at end of file +*Good luck building!*