diff --git a/move/operators/Move.lock b/move/operators/Move.lock new file mode 100644 index 00000000..18983bb6 --- /dev/null +++ b/move/operators/Move.lock @@ -0,0 +1,26 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 2 +manifest_digest = "F3843E711A5044D1CF9708AF5AA3CEC7EDA8C17E39065DE7ACBC14C91B9C3B24" +deps_digest = "F8BBB0CCB2491CA29A3DF03D6F92277A4F3574266507ACD77214D37ECA3F3082" +dependencies = [ + { name = "Sui" }, +] + +[[move.package]] +name = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "mainnet-v1.25.3", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +name = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "mainnet-v1.25.3", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { name = "MoveStdlib" }, +] + +[move.toolchain-version] +compiler-version = "1.26.1" +edition = "2024.beta" +flavor = "sui" diff --git a/move/operators/Move.toml b/move/operators/Move.toml new file mode 100644 index 00000000..6e2e2051 --- /dev/null +++ b/move/operators/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "Operators" +version = "0.1.0" +edition = "2024.beta" + +[dependencies] +Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet-v1.25.3" } + +[addresses] +operators = "0x107" diff --git a/move/operators/info.json b/move/operators/info.json new file mode 100644 index 00000000..171fda61 --- /dev/null +++ b/move/operators/info.json @@ -0,0 +1,4 @@ +{ + "singletons": [ + ] +} \ No newline at end of file diff --git a/move/operators/sources/operators.move b/move/operators/sources/operators.move new file mode 100644 index 00000000..a68e262d --- /dev/null +++ b/move/operators/sources/operators.move @@ -0,0 +1,381 @@ +module operators::operators { + use sui::bag::{Self, Bag}; + use sui::vec_set::{Self, VecSet}; + use sui::event; + use std::ascii::String; + use std::type_name; + + // ----- + // Types + // ----- + + /// The `OwnerCap` capability representing the owner of the contract. + public struct OwnerCap has key, store { + id: UID, + } + + /// The `OperatorCap` capability representing an approved operator. + public struct OperatorCap has key, store { + id: UID, + } + + /// The main `Operators` struct storing the capabilities and operator IDs. + public struct Operators has key { + id: UID, + // The number of operators are small in practice, and under the Sui object size limit, so a dynamic collection doesn't need to be used + operators: VecSet<address>, + // map-like collection of capabilities stored as Sui objects + caps: Bag, + } + + // ------ + // Errors + // ------ + + /// When the operator is not found in the set of approved operators. + const EOperatorNotFound: u64 = 0; + + /// When the capability is not found. + const ECapNotFound: u64 = 1; + + // ------ + // Events + // ------ + + /// Event emitted when a new operator is added. + public struct OperatorAdded has copy, drop { + operator: address, + } + + /// Event emitted when an operator is removed. + public struct OperatorRemoved has copy, drop { + operator: address, + } + + /// Event emitted when a capability is stored. + public struct CapabilityStored has copy, drop { + cap_id: ID, + cap_name: String, + } + + /// Event emitted when a capability is removed. + public struct CapabilityRemoved has copy, drop { + cap_id: ID, + cap_name: String, + } + + // ----- + // Setup + // ----- + + /// Initializes the contract and transfers the `OwnerCap` to the sender. + fun init(ctx: &mut TxContext) { + transfer::share_object(Operators { + id: object::new(ctx), + operators: vec_set::empty(), + caps: bag::new(ctx), + }); + + let cap = OwnerCap { + id: object::new(ctx), + }; + + transfer::transfer(cap, ctx.sender()); + } + + // ---------------- + // Public Functions + // ---------------- + + /// Adds a new operator by issuing an `OperatorCap` and storing its ID. + public fun add_operator(self: &mut Operators, _: &OwnerCap, new_operator: address, ctx: &mut TxContext) { + let operator_cap = OperatorCap { + id: object::new(ctx), + }; + + transfer::transfer(operator_cap, new_operator); + self.operators.insert(new_operator); + + event::emit(OperatorAdded { + operator: new_operator, + }); + } + + /// Removes an operator by ID, revoking their `OperatorCap`. + public fun remove_operator(self: &mut Operators, _: &OwnerCap, operator: address) { + self.operators.remove(&operator); + + event::emit(OperatorRemoved { + operator, + }); + } + + /// Stores a capability in the `Operators` struct. + public fun store_cap<T: key + store>(self: &mut Operators, _: &OwnerCap, cap: T) { + let cap_id = object::id(&cap); + self.caps.add(cap_id, cap); + + event::emit(CapabilityStored { + cap_id, + cap_name: type_name::get<T>().into_string(), + }); + } + + /// Allows an approved operator to borrow a capability by its ID. + public fun borrow_cap<T: key + store>( + self: &Operators, + _operator_cap: &OperatorCap, + cap_id: ID, + ctx: &mut TxContext + ): &T { + assert!(self.operators.contains(&ctx.sender()), EOperatorNotFound); + assert!(self.caps.contains(cap_id), ECapNotFound); + + &self.caps[cap_id] + } + + /// Allows an approved operator to borrow a capability by its ID. + public fun borrow_cap_mut<T: key + store>( + self: &mut Operators, + _operator_cap: &OperatorCap, + cap_id: ID, + ctx: &mut TxContext + ): &mut T { + assert!(self.operators.contains(&ctx.sender()), EOperatorNotFound); + assert!(self.caps.contains(cap_id), ECapNotFound); + + &mut self.caps[cap_id] + } + + /// Removes a capability from the `Operators` struct. + public fun remove_cap<T: key + store>(self: &mut Operators, _: &OwnerCap, cap_id: ID): T { + event::emit(CapabilityRemoved { + cap_id, + cap_name: type_name::get<T>().into_string(), + }); + + self.caps.remove<ID, T>(cap_id) + } + + // ----- + // Tests + // ----- + + #[test_only] + fun new_operators(ctx: &mut TxContext): Operators { + Operators { + id: object::new(ctx), + operators: vec_set::empty(), + caps: bag::new(ctx), + } + } + + #[test_only] + fun destroy_operators(operators: Operators) { + let Operators { id, operators, caps } = operators; + + id.delete(); + caps.destroy_empty(); + + let mut keys = operators.into_keys(); + + while (!keys.is_empty()) { + keys.pop_back(); + }; + + keys.destroy_empty(); + } + + #[test_only] + fun new_owner_cap(ctx: &mut TxContext): OwnerCap { + OwnerCap { + id: object::new(ctx), + } + } + + #[test_only] + fun destroy_owner_cap(owner_cap: OwnerCap) { + let OwnerCap { id } = owner_cap; + object::delete(id); + } + + #[test_only] + fun new_operator_cap(self: &mut Operators, ctx: &mut TxContext): OperatorCap { + let operator_cap = OperatorCap { + id: object::new(ctx), + }; + + self.operators.insert(ctx.sender()); + operator_cap + } + + #[test_only] + fun destroy_operator_cap(operator_cap: OperatorCap) { + let OperatorCap { id } = operator_cap; + object::delete(id); + } + + #[test] + fun test_init() { + let ctx = &mut tx_context::dummy(); + init(ctx); + + let owner_cap = new_owner_cap(ctx); + destroy_owner_cap(owner_cap); + } + + #[test] + fun test_add_and_remove_operator() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + + let new_operator = @0x1; + add_operator(&mut operators, &owner_cap, new_operator, ctx); + assert!(operators.operators.size() == 1, 0); + + let operator_id = operators.operators.keys()[0]; + remove_operator(&mut operators, &owner_cap, operator_id); + assert!(operators.operators.is_empty(), 1); + + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + fun test_store_and_remove_cap() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + let external_cap = new_owner_cap(ctx); + + let external_id = object::id(&external_cap); + + store_cap(&mut operators, &owner_cap, external_cap); + assert!(operators.caps.contains(external_id), 0); + + let borrowed_cap = borrow_cap<OwnerCap>(&operators, &operator_cap, external_id, ctx); + assert!(object::id(borrowed_cap) == external_id, 1); + + let borrowed_mut_cap = borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, external_id, ctx); + assert!(object::id(borrowed_mut_cap) == external_id, 1); + + let removed_cap = remove_cap<OwnerCap>(&mut operators, &owner_cap, external_id); + assert!(!operators.caps.contains(external_id), 2); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_owner_cap(removed_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = vec_set::EKeyDoesNotExist)] + fun test_remove_operator_fail() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + + remove_operator(&mut operators, &owner_cap, ctx.sender()); + + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = EOperatorNotFound)] + fun test_borrow_cap_not_operator() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + let external_cap = new_owner_cap(ctx); + + let external_id = object::id(&external_cap); + + store_cap(&mut operators, &owner_cap, external_cap); + remove_operator(&mut operators, &owner_cap, ctx.sender()); + + borrow_cap<OwnerCap>(&operators, &operator_cap, external_id, ctx); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = ECapNotFound)] + fun test_borrow_cap_no_such_cap() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + + let operator_id = object::id(&operator_cap); + + borrow_cap<OwnerCap>(&operators, &operator_cap, operator_id, ctx); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = EOperatorNotFound)] + fun test_borrow_cap_mut_not_operator() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + let external_cap = new_owner_cap(ctx); + + let external_id = object::id(&external_cap); + + store_cap(&mut operators, &owner_cap, external_cap); + remove_operator(&mut operators, &owner_cap, ctx.sender()); + + borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, external_id, ctx); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = ECapNotFound)] + fun test_borrow_cap_mut_no_such_cap() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + + let operator_id = object::id(&operator_cap); + + borrow_cap_mut<OwnerCap>(&mut operators, &operator_cap, operator_id, ctx); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_operators(operators); + } + + #[test] + #[expected_failure(abort_code = sui::dynamic_field::EFieldDoesNotExist)] + fun test_remove_cap_fail() { + let ctx = &mut tx_context::dummy(); + let mut operators = new_operators(ctx); + let owner_cap = new_owner_cap(ctx); + let operator_cap = new_operator_cap(&mut operators, ctx); + let external_cap = new_owner_cap(ctx); + + let external_id = object::id(&external_cap); + + let removed_cap = remove_cap<OwnerCap>(&mut operators, &owner_cap, external_id); + + destroy_operator_cap(operator_cap); + destroy_owner_cap(owner_cap); + destroy_owner_cap(external_cap); + destroy_owner_cap(removed_cap); + destroy_operators(operators); + } +}