Smart Contract
Build a ToDo List on Web3.
Author: Jianyi

Build a ToDo list on Web3.


module advanced_todo_list_addr::advanced_todo_list {
use std::bcs;
use std::signer;
use std::vector;
use std::string::String;
use aptos_std::string_utils;
use aptos_framework::object;
/// Todo list does not exist
const E_TODO_LIST_DOSE_NOT_EXIST: u64 = 1;
/// Todo does not exist
const E_TODO_DOSE_NOT_EXIST: u64 = 2;
/// Todo is already completed
const E_TODO_ALREADY_COMPLETED: u64 = 3;
struct UserTodoListCounter has key {
counter: u64,
}
struct TodoList has key {
owner: address,
todos: vector<Todo>,
}
struct Todo has store, drop, copy {
content: String,
completed: bool,
}
// This function is only called once when the module is published for the first time.
// init_module is optional, you can also have an entry function as the initializer.
fun init_module(_module_publisher: &signer) {
// nothing to do here
}
// ======================== Write functions ========================
public entry fun create_todo_list(sender: &signer) acquires UserTodoListCounter {
let sender_address = signer::address_of(sender);
let counter = if (exists<UserTodoListCounter>(sender_address)) {
let counter = borrow_global<UserTodoListCounter>(sender_address);
counter.counter
} else {
let counter = UserTodoListCounter { counter: 0 };
// store the UserTodoListCounter resource directly under the sender
move_to(sender, counter);
0
};
// create a new object to hold the todo list, use the contract_addr_counter as seed
let obj_holds_todo_list = object::create_named_object(
sender,
construct_todo_list_object_seed(counter),
);
let obj_signer = object::generate_signer(&obj_holds_todo_list);
let todo_list = TodoList {
owner: sender_address,
todos: vector::empty(),
};
// store the TodoList resource under the newly created object
move_to(&obj_signer, todo_list);
// increment the counter
let counter = borrow_global_mut<UserTodoListCounter>(sender_address);
counter.counter = counter.counter + 1;
}
public entry fun create_todo(sender: &signer, todo_list_idx: u64, content: String) acquires TodoList {
let sender_address = signer::address_of(sender);
let todo_list_obj_addr = object::create_object_address(
&sender_address,
construct_todo_list_object_seed(todo_list_idx)
);
assert_user_has_todo_list(todo_list_obj_addr);
let todo_list = borrow_global_mut<TodoList>(todo_list_obj_addr);
let new_todo = Todo {
content,
completed: false
};
vector::push_back(&mut todo_list.todos, new_todo);
}
public entry fun complete_todo(sender: &signer, todo_list_idx: u64, todo_idx: u64) acquires TodoList {
let sender_address = signer::address_of(sender);
let todo_list_obj_addr = object::create_object_address(
&sender_address,
construct_todo_list_object_seed(todo_list_idx)
);
assert_user_has_todo_list(todo_list_obj_addr);
let todo_list = borrow_global_mut<TodoList>(todo_list_obj_addr);
assert_user_has_given_todo(todo_list, todo_idx);
let todo_record = vector::borrow_mut(&mut todo_list.todos, todo_idx);
assert!(todo_record.completed == false, E_TODO_ALREADY_COMPLETED);
todo_record.completed = true;
}
// ======================== Read Functions ========================
// Get how many todo lists the sender has, return 0 if the sender has none.
#[view]
public fun get_todo_list_counter(sender: address): u64 acquires UserTodoListCounter {
if (exists<UserTodoListCounter>(sender)) {
let counter = borrow_global<UserTodoListCounter>(sender);
counter.counter
} else {
0
}
}
#[view]
public fun get_todo_list_obj_addr(sender: address, todo_list_idx: u64): address {
object::create_object_address(&sender, construct_todo_list_object_seed(todo_list_idx))
}
#[view]
public fun has_todo_list(sender: address, todo_list_idx: u64): bool {
let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
exists<TodoList>(todo_list_obj_addr)
}
#[view]
public fun get_todo_list(sender: address, todo_list_idx: u64): (address, u64) acquires TodoList {
let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
assert_user_has_todo_list(todo_list_obj_addr);
let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
(todo_list.owner, vector::length(&todo_list.todos))
}
#[view]
public fun get_todo_list_by_todo_list_obj_addr(todo_list_obj_addr: address): (address, u64) acquires TodoList {
let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
(todo_list.owner, vector::length(&todo_list.todos))
}
#[view]
public fun get_todo(sender: address, todo_list_idx: u64, todo_idx: u64): (String, bool) acquires TodoList {
let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
assert_user_has_todo_list(todo_list_obj_addr);
let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
assert!(todo_idx < vector::length(&todo_list.todos), E_TODO_DOSE_NOT_EXIST);
let todo_record = vector::borrow(&todo_list.todos, todo_idx);
(todo_record.content, todo_record.completed)
}
// ======================== Helper Functions ========================
fun assert_user_has_todo_list(user_addr: address) {
assert!(
exists<TodoList>(user_addr),
E_TODO_LIST_DOSE_NOT_EXIST
);
}
fun assert_user_has_given_todo(todo_list: &TodoList, todo_id: u64) {
assert!(
todo_id < vector::length(&todo_list.todos),
E_TODO_DOSE_NOT_EXIST
);
}
fun get_todo_list_obj(sender: address, todo_list_idx: u64): object::Object<TodoList> {
let addr = get_todo_list_obj_addr(sender, todo_list_idx);
object::address_to_object(addr)
}
fun construct_todo_list_object_seed(counter: u64): vector<u8> {
// The seed must be unique per todo list creator
//Wwe add contract address as part of the seed so seed from 2 todo list contract for same user would be different
bcs::to_bytes(&string_utils::format2(&b"{}_{}", @advanced_todo_list_addr, counter))
}
// ======================== Unit Tests ========================
#[test_only]
use std::string;
#[test_only]
use aptos_framework::account;
#[test_only]
use aptos_std::debug;
#[test(admin = @0x100)]
public entry fun test_end_to_end(admin: signer) acquires TodoList, UserTodoListCounter {
let admin_addr = signer::address_of(&admin);
let todo_list_idx = get_todo_list_counter(admin_addr);
assert!(todo_list_idx == 0, 1);
account::create_account_for_test(admin_addr);
assert!(!has_todo_list(admin_addr, todo_list_idx), 2);
create_todo_list(&admin);
assert!(get_todo_list_counter(admin_addr) == 1, 3);
assert!(has_todo_list(admin_addr, todo_list_idx), 4);
create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_idx);
debug::print(&string_utils::format1(&b"todo_list_owner: {}", todo_list_owner));
debug::print(&string_utils::format1(&b"todo_list_length: {}", todo_list_length));
assert!(todo_list_owner == admin_addr, 5);
assert!(todo_list_length == 1, 6);
let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_idx, 0);
debug::print(&string_utils::format1(&b"todo_content: {}", todo_content));
debug::print(&string_utils::format1(&b"todo_completed: {}", todo_completed));
assert!(!todo_completed, 7);
assert!(todo_content == string::utf8(b"New Todo"), 8);
complete_todo(&admin, todo_list_idx, 0);
let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_idx, 0);
debug::print(&string_utils::format1(&b"todo_completed: {}", todo_completed));
assert!(todo_completed, 9);
}
#[test(admin = @0x100)]
public entry fun test_end_to_end_2_todo_lists(admin: signer) acquires TodoList, UserTodoListCounter {
let admin_addr = signer::address_of(&admin);
create_todo_list(&admin);
let todo_list_1_idx = get_todo_list_counter(admin_addr) - 1;
create_todo_list(&admin);
let todo_list_2_idx = get_todo_list_counter(admin_addr) - 1;
create_todo(&admin, todo_list_1_idx, string::utf8(b"New Todo"));
let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_1_idx);
assert!(todo_list_owner == admin_addr, 1);
assert!(todo_list_length == 1, 2);
let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_1_idx, 0);
assert!(!todo_completed, 3);
assert!(todo_content == string::utf8(b"New Todo"), 4);
complete_todo(&admin, todo_list_1_idx, 0);
let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_1_idx, 0);
assert!(todo_completed, 5);
create_todo(&admin, todo_list_2_idx, string::utf8(b"New Todo"));
let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_2_idx);
assert!(todo_list_owner == admin_addr, 6);
assert!(todo_list_length == 1, 7);
let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_2_idx, 0);
assert!(!todo_completed, 8);
assert!(todo_content == string::utf8(b"New Todo"), 9);
complete_todo(&admin, todo_list_2_idx, 0);
let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_2_idx, 0);
assert!(todo_completed, 10);
}
#[test(admin = @0x100)]
#[expected_failure(abort_code = E_TODO_LIST_DOSE_NOT_EXIST, location = Self)]
public entry fun test_todo_list_does_not_exist(admin: signer) acquires TodoList, UserTodoListCounter {
let admin_addr = signer::address_of(&admin);
account::create_account_for_test(admin_addr);
let todo_list_idx = get_todo_list_counter(admin_addr);
// account cannot create todo on a todo list (that does not exist
create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
}
#[test(admin = @0x100)]
#[expected_failure(abort_code = E_TODO_DOSE_NOT_EXIST, location = Self)]
public entry fun test_todo_does_not_exist(admin: signer) acquires TodoList, UserTodoListCounter {
let admin_addr = signer::address_of(&admin);
account::create_account_for_test(admin_addr);
let todo_list_idx = get_todo_list_counter(admin_addr);
create_todo_list(&admin);
// can not complete todo that does not exist
complete_todo(&admin, todo_list_idx, 1);
}
#[test(admin = @0x100)]
#[expected_failure(abort_code = E_TODO_ALREADY_COMPLETED, location = Self)]
public entry fun test_todo_already_completed(admin: signer) acquires TodoList, UserTodoListCounter {
let admin_addr = signer::address_of(&admin);
account::create_account_for_test(admin_addr);
let todo_list_idx = get_todo_list_counter(admin_addr);
create_todo_list(&admin);
create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
complete_todo(&admin, todo_list_idx, 0);
// can not complete todo that is already completed
complete_todo(&admin, todo_list_idx, 0);
}
}