diff --git a/account.move b/account.move new file mode 100644 index 0000000..6aa7524 --- /dev/null +++ b/account.move @@ -0,0 +1,271 @@ +/// This module manages user accounts in the Typus ecosystem. +/// It provides functionalities for creating, accessing, and transferring accounts. +module typus::account { + use std::bcs; + + use sui::dynamic_object_field; + use sui::vec_map; + + use typus::ecosystem::Version; + use typus::error::{ + account_not_found, + account_already_exists, + }; + use typus::event; + use typus::keyed_big_vector::{Self, KeyedBigVector}; + + const KAccountRegistry: vector = b"account_registry"; + + /// A registry for all user accounts in the Typus system. + /// This struct is a dynamic object field of the `Version` object. + public struct AccountRegistry has key, store { + /// The unique identifier of the AccountRegistry object. + id: UID, + /// A keyed big vector mapping account addresses to `Account` objects. + accounts: KeyedBigVector, // + /// A keyed big vector mapping user addresses to their corresponding account addresses. + user_account: KeyedBigVector, // + } + + /// Represents a user account. + public struct Account has key, store { + /// The unique identifier of the Account object. + id: UID, + /// An optional capability object that grants access to this account. + account_cap: Option, + /// The address of the user who created this account. + creator: address, + } + + /// A capability object that grants access to an account. + /// This can be used to authorize actions on behalf of the account owner. + public struct AccountCap has key, store { + /// The unique identifier of the AccountCap object. + id: UID, + /// The address of the account this capability is for. + `for`: address, + } + + /// Initializes the `AccountRegistry` as a dynamic object field of the `Version` object. + /// This function is called only once during the deployment of the contract. + entry fun init_account_registry(version: &mut Version, ctx: &mut TxContext) { + dynamic_object_field::add( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + AccountRegistry { + id: object::new(ctx), + accounts: keyed_big_vector::new(1000, ctx), + user_account: keyed_big_vector::new(1000, ctx), + } + ); + } + + /// Retrieves the account address of the transaction sender. + /// It asserts that the user has an account. + public fun get_user_account_address( + version: &Version, + ctx: &TxContext, + ): address { + // safety check + version.version_check(); + + // main logic + let account_registry: &AccountRegistry = dynamic_object_field::borrow( + version.borrow_uid(), + KAccountRegistry.to_string(), + ); + assert!(account_registry.user_account.contains(ctx.sender()), account_not_found(0)); + + + // return value + *account_registry.user_account.borrow_by_key(ctx.sender()) + } + + /// Retrieves the account address associated with a given `AccountCap`. + public fun get_user_account_address_with_account_cap( + version: &Version, + account_cap: &AccountCap, + ): address { + // safety check + version.version_check(); + + // return value + account_cap.`for` + } + + /// Borrows a mutable reference to the user's `Account` object. + /// The user is identified by the transaction sender's address. + /// Safe with ctx.sender as verification + public fun borrow_user_account( + version: &mut Version, + ctx: &TxContext, + ): &mut Account { + // safety check + version.version_check(); + + // main logic + let account_registry: &mut AccountRegistry = dynamic_object_field::borrow_mut( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + ); + assert!(account_registry.user_account.contains(ctx.sender()), account_not_found(0)); + + + // return value + account_registry.accounts.borrow_by_key_mut( + *account_registry.user_account.borrow_by_key(ctx.sender()) + ) + } + + /// Borrows a mutable reference to an `Account` object using an `AccountCap`. + /// This allows authorized users to access and modify an account. + /// Safe with `AccountCap` as verification + public fun borrow_user_account_with_account_cap( + version: &mut Version, + account_cap: &AccountCap, + ): &mut Account { + // safety check + version.version_check(); + + // main logic + let account_registry: &mut AccountRegistry = dynamic_object_field::borrow_mut( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + ); + + // return value + account_registry.accounts.borrow_by_key_mut(account_cap.`for`) + } + + /// Creates a new `Account` and returns an `AccountCap` for it. + /// This function can be used to create accounts that are not directly tied to a user's address. + /// The `AccountCap` can be transferred to other users to grant them access to the account. + public fun new_account( + version: &mut Version, + ctx: &mut TxContext, + ): AccountCap { + // safety check + version.version_check(); + + // main logic + let account_registry: &mut AccountRegistry = dynamic_object_field::borrow_mut( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + ); + let creator = ctx.sender(); + let account = Account { + id: object::new(ctx), + account_cap: option::none(), + creator, + }; + let account_address = object::id_address(&account); + account_registry.accounts.push_back(account_address, account); + let account_cap = AccountCap { + id: object::new(ctx), + `for`: account_address, + }; + + // emit event + event::emit_typus_event( + b"new_account".to_string(), + vec_map::empty(), + vec_map::from_keys_values( + vector[ + b"account".to_string(), + ], + vector[ + bcs::to_bytes(&account_address), + ], + ), + ); + + // return value + account_cap + } + + /// Creates a new `Account` for the transaction sender and associates it with their address. + /// If the user already has an account, this function does nothing. + public fun create_account( + version: &mut Version, + ctx: &mut TxContext, + ) { + // safety check + version.version_check(); + let account_registry: &mut AccountRegistry = dynamic_object_field::borrow_mut( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + ); + if (account_registry.user_account.contains(ctx.sender())) { return }; + + // main logic + let creator = ctx.sender(); + let mut account = Account { + id: object::new(ctx), + account_cap: option::none(), + creator, + }; + let account_address = object::id_address(&account); + let account_cap = AccountCap { + id: object::new(ctx), + `for`: account_address, + }; + let account_cap_address = object::id_address(&account_cap); + option::fill(&mut account.account_cap, account_cap); + account_registry.accounts.push_back(account_address, account); + account_registry.user_account.push_back(ctx.sender(), account_address); + + // emit event + event::emit_typus_event( + b"create_account".to_string(), + vec_map::empty(), + vec_map::from_keys_values( + vector[ + b"account".to_string(), + b"account_cap".to_string(), + ], + vector[ + bcs::to_bytes(&account_address), + bcs::to_bytes(&account_cap_address), + ], + ), + ); + } + + /// Transfers the sender's account to a new recipient address. + /// It asserts that the sender has an account and the recipient does not have an account yet. + /// Safe with ctx.sender as verification + public fun transfer_account( + version: &mut Version, + recipient: address, + ctx: &TxContext, + ) { + // safety check + version.version_check(); + let account_registry: &mut AccountRegistry = dynamic_object_field::borrow_mut( + version.borrow_uid_mut(), + KAccountRegistry.to_string(), + ); + assert!(account_registry.user_account.contains(ctx.sender()), account_not_found(0)); + assert!(!account_registry.user_account.contains(recipient), account_already_exists(0)); + + // main logic + let account_address: address = account_registry.user_account.swap_remove_by_key(ctx.sender()); + account_registry.user_account.push_back(recipient, account_address); + + // emit event + event::emit_typus_event( + b"transfer_account".to_string(), + vec_map::empty(), + vec_map::from_keys_values( + vector[ + b"account".to_string(), + b"recipient".to_string(), + ], + vector[ + bcs::to_bytes(&account_address), + bcs::to_bytes(&recipient), + ], + ), + ); + } +} \ No newline at end of file diff --git a/airdrop.move b/airdrop.move new file mode 100644 index 0000000..cc28b93 --- /dev/null +++ b/airdrop.move @@ -0,0 +1,391 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements an airdrop mechanism for distributing tokens to a list of users. +/// It allows for setting up airdrops, claiming them, and removing them. +module typus::airdrop { + use std::ascii::String; + use std::type_name::{Self, TypeName}; + + use sui::balance::{Self, Balance}; + use sui::coin::Coin; + use sui::dynamic_field; + use sui::event::emit; + use sui::table::{Self, Table}; + + use typus::big_vector::{Self, BigVector}; + use typus::ecosystem::Version; + use typus::utility; + + // ======== Error Code ======== + + /// Error when the balance of the airdrop is insufficient. + const EInsufficientBalance: u64 = 0; + /// Error for invalid input parameters. + const EInvalidInput: u64 = 1; + + const TotalValue: vector = b"total_value"; + const ClaimedTable: vector = b"claimed_table"; + + // ======== Typus Airdrop ======== + + /// A registry for all airdrops. This is a shared object that holds all `AirdropInfo` objects as dynamic fields. + public struct TypusAirdropRegistry has key { + id: UID, + } + + /// Stores the information for a specific airdrop. + /// The `TOKEN` type parameter indicates the type of token being airdropped. + public struct AirdropInfo has key, store { + /// The unique identifier of the AirdropInfo object. + id: UID, + /// The balance of tokens available for this airdrop. + balance: Balance, + /// A big vector containing the list of `Airdrop` structs for each user. + airdrops: BigVector, + // df: + // total_value: u64, + // claimed_table: Table, + } + + /// Represents a single airdrop for a user. + public struct Airdrop has store, drop { // 40 + /// The address of the user who is eligible for the airdrop. + user: address, // 32 + /// The amount of tokens the user will receive. + value: u64, // 8 + } + + /// Initializes the `TypusAirdropRegistry` and shares it. + fun init(ctx: &mut TxContext) { + transfer::share_object(TypusAirdropRegistry { + id: object::new(ctx), + }); + } + + /// Event emitted when an airdrop is set or updated. + public struct SetAirdropEvent has copy, drop { + /// The type name of the token being airdropped. + token: TypeName, + /// The key identifying the airdrop. + key: String, + /// Log data: [total_value, spent_value] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + /// Sets up or updates an airdrop. + /// This function is authorized and can only be called by the admin. + /// It takes a list of users and corresponding values to be airdropped. + /// It also takes a vector of coins to fund the airdrop. + public fun set_airdrop( + version: &Version, + typus_airdrop_registry: &mut TypusAirdropRegistry, + key: String, + mut coins: vector>, + mut users: vector
, + mut values: vector, + ctx: &mut TxContext, + ) { + // This is an authorized function + version.verify(ctx); + assert!(users.length() == values.length(), EInvalidInput); + + let token = type_name::with_defining_ids(); + let mut airdrop_info = if (dynamic_field::exists(&typus_airdrop_registry.id, key)) { + dynamic_field::remove(&mut typus_airdrop_registry.id, key) + } else { + AirdropInfo { + id: object::new(ctx), + balance: balance::zero(), + airdrops: big_vector::new(2500, ctx), + } + }; + let mut total_value = airdrop_info.balance.value(); + + while (!users.is_empty()) { + let user = users.pop_back(); + let value = values.pop_back(); + total_value = total_value + value; + airdrop_info.airdrops.push_back( + Airdrop { + user, + value, + }, + ); + }; + + if (dynamic_field::exists(& airdrop_info.id, TotalValue.to_string())) { + let v: &mut u64 = dynamic_field::borrow_mut(&mut airdrop_info.id, TotalValue.to_string()); + *v = total_value; + } else { + dynamic_field::add(&mut airdrop_info.id, TotalValue.to_string(), total_value); + }; + + if (!dynamic_field::exists(& airdrop_info.id, ClaimedTable.to_string())) { + let claimed_table = table::new(ctx); + dynamic_field::add(&mut airdrop_info.id, ClaimedTable.to_string(), claimed_table); + }; + + // add insufficient balance from coins to airdrop_info.balance + let airdrop_value = airdrop_info.balance.value(); + let mut spent_value = 0; + if (airdrop_value < total_value) { + let mut insufficient_airdrop_value = total_value - airdrop_value; + spent_value = insufficient_airdrop_value; + while (!coins.is_empty()) { + if (insufficient_airdrop_value > 0) { + let mut coin = coins.pop_back(); + if (coin.value() > insufficient_airdrop_value) { + airdrop_info.balance.join(coin.balance_mut().split(insufficient_airdrop_value)); + coins.push_back(coin); + insufficient_airdrop_value = 0; + break + } + else { + insufficient_airdrop_value = insufficient_airdrop_value - coin.value(); + airdrop_info.balance.join(coin.into_balance()); + }; + } + else { + break + } + }; + assert!(insufficient_airdrop_value == 0, EInsufficientBalance); + }; + utility::transfer_coins(coins, ctx.sender()); + + dynamic_field::add(&mut typus_airdrop_registry.id, key, airdrop_info); + + + emit(SetAirdropEvent { + token, + key, + log : vector[total_value, spent_value], + bcs_padding: vector[], + }); + } + + /// Event emitted when an airdrop is removed. + public struct RemoveAirdropEvent has copy, drop { + /// The type name of the token being airdropped. + token: TypeName, + /// The key identifying the airdrop. + key: String, + /// Log data: [balance_value] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + /// Removes an airdrop and returns the remaining balance to the admin. + /// This function is authorized and can only be called by the admin. + public fun remove_airdrop( + version: &Version, + typus_airdrop_registry: &mut TypusAirdropRegistry, + key: String, + ctx: &mut TxContext, + ): Balance { + // This is an authorized function + version.verify(ctx); + + let AirdropInfo { + mut id, + balance, + airdrops, + } = dynamic_field::remove(&mut typus_airdrop_registry.id, key); + + if (dynamic_field::exists(& id, TotalValue.to_string())) { + let _: u64 = dynamic_field::remove(&mut id, TotalValue.to_string()); + }; + + if (dynamic_field::exists(& id, ClaimedTable.to_string())) { + let t: Table = dynamic_field::remove(&mut id, ClaimedTable.to_string()); + t.drop(); + }; + + object::delete(id); + big_vector::drop(airdrops); + + emit(RemoveAirdropEvent { + token: type_name::with_defining_ids(), + key, + log: vector[balance.value()], + bcs_padding: vector[], + }); + + balance + } + + /// Event emitted when a user claims an airdrop. + public struct ClaimAirdropEvent has copy, drop { + /// The type name of the token being airdropped. + token: TypeName, + /// The key identifying the airdrop. + key: String, + /// The address of the user claiming the airdrop. + user: address, + /// Log data: [claimed_value] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + /// Allows a user to claim their airdrop. + /// It iterates through the airdrop list to find the user's entry and sends them the tokens. + /// If the user has already claimed, the value will be 0, and they won't receive anything. + /// Safe with ctx.sender as verification + public fun claim_airdrop( + version: &Version, + typus_airdrop_registry: &mut TypusAirdropRegistry, + key: String, + ctx: &TxContext, + ): Option> { + version.version_check(); + + if (!dynamic_field::exists_with_type>(&typus_airdrop_registry.id, key)) { + abort EInvalidInput + }; + let airdrop_info = dynamic_field::borrow_mut>(&mut typus_airdrop_registry.id, key); + let user = ctx.sender(); + let length = airdrop_info.airdrops.length(); + let slice_size = (airdrop_info.airdrops.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = airdrop_info.airdrops.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let airdrop: &mut Airdrop = &mut slice[i % slice_size]; + if (airdrop.user == user) { + let balance = airdrop_info.balance.split(airdrop.value); + emit(ClaimAirdropEvent { + token: type_name::with_defining_ids(), + key, + user, + log: vector[airdrop.value], + bcs_padding: vector[], + }); + + // update claimed_table + if (dynamic_field::exists(& airdrop_info.id, ClaimedTable.to_string())) { + let claimed_table: &mut Table = dynamic_field::borrow_mut(&mut airdrop_info.id, ClaimedTable.to_string()); + if (claimed_table.contains(user)) { + let claimed = claimed_table.borrow_mut(user); + *claimed = *claimed + airdrop.value; + } else { + claimed_table.add(user, airdrop.value); + }; + }; + + airdrop.value = 0; + return option::some(balance) + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = airdrop_info.airdrops.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + option::none() + } + + /// Allows a user to claim their airdrop by providing the index of their airdrop entry. + /// This is more efficient than `claim_airdrop` if the user knows their index. + /// Safe with ctx.sender as verification + public fun claim_airdrop_by_index( + version: &Version, + typus_airdrop_registry: &mut TypusAirdropRegistry, + key: String, + i: u64, + ctx: &TxContext, + ): Option> { + version.version_check(); + + if (!dynamic_field::exists_with_type>(&typus_airdrop_registry.id, key)) { + abort EInvalidInput + }; + let airdrop_info = dynamic_field::borrow_mut>(&mut typus_airdrop_registry.id, key); + let user = ctx.sender(); + let airdrop: &mut Airdrop = &mut airdrop_info.airdrops[i]; + if (airdrop.user == user) { + let balance = airdrop_info.balance.split(airdrop.value); + emit(ClaimAirdropEvent { + token: type_name::with_defining_ids(), + key, + user, + log: vector[airdrop.value], + bcs_padding: vector[], + }); + + // update claimed_table + if (dynamic_field::exists(& airdrop_info.id, ClaimedTable.to_string())) { + let claimed_table: &mut Table = dynamic_field::borrow_mut(&mut airdrop_info.id, ClaimedTable.to_string()); + if (claimed_table.contains(user)) { + let claimed = claimed_table.borrow_mut(user); + *claimed = *claimed + airdrop.value; + } else { + claimed_table.add(user, airdrop.value); + }; + }; + + airdrop.value = 0; + return option::some(balance) + }; + + option::none() + } + + /// Retrieves the airdrop information for a specific user. + /// Returns a vector containing the index and value of the airdrop. + /// If the user is not found, it returns `[0, 0]`. + public(package) fun get_airdrop( + version: &Version, + typus_airdrop_registry: &TypusAirdropRegistry, + key: String, + user: address, + ): vector { + version.version_check(); + + if (!dynamic_field::exists_with_type>(&typus_airdrop_registry.id, key)) { + abort EInvalidInput + }; + let airdrop_info = dynamic_field::borrow>(&typus_airdrop_registry.id, key); + + let mut total_value = 0; + if (dynamic_field::exists(& airdrop_info.id, TotalValue.to_string())) { + total_value = *dynamic_field::borrow(& airdrop_info.id, TotalValue.to_string()); + }; + + let length = airdrop_info.airdrops.length(); + let slice_size = (airdrop_info.airdrops.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = airdrop_info.airdrops.borrow_slice(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let airdrop: &Airdrop = &slice[i % slice_size]; + if (airdrop.user == user) { + // get claimed_table + let mut claimed = 0; + if (dynamic_field::exists(& airdrop_info.id, ClaimedTable.to_string())) { + let claimed_table: & Table = dynamic_field::borrow(&airdrop_info.id, ClaimedTable.to_string()); + if (claimed_table.contains(user)) { + claimed = *claimed_table.borrow(user); + }; + }; + + return vector[i, airdrop.value, claimed, total_value] + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = airdrop_info.airdrops.borrow_slice(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + vector[0, 0, 0, total_value] + } +} \ No newline at end of file diff --git a/big_vector.move b/big_vector.move new file mode 100644 index 0000000..2fc1cc3 --- /dev/null +++ b/big_vector.move @@ -0,0 +1,275 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a `BigVector`, a vector-like data structure that can store a large number of elements +/// by splitting them into smaller `Slice` objects. This allows it to overcome the object size limit in Sui. +/// Each `Slice` is a dynamic field of the `BigVector` object. +module typus::big_vector { + use std::type_name::{Self, TypeName}; + + use sui::dynamic_field; + + // ======== Constants ======== + + /// The maximum size of a slice. + const CMaxSliceSize: u32 = 262144; + + // ======== Errors ======== + + /// Error for invalid slice size. + const EInvalidSliceSize: u64 = 0; + /// Error when trying to destroy a non-empty BigVector. + const ENotEmpty: u64 = 1; + /// Error when trying to pop from an empty BigVector. + const EIsEmpty: u64 = 2; + /// Error for out-of-bounds access. + const EIndexOutOfBounds: u64 = 3; + + // ======== Structs ======== + + /// A vector-like data structure that can store a large number of elements. + public struct BigVector has key, store { + /// The unique identifier of the BigVector object. + id: UID, + /// The type name of the elements stored in the BigVector. + element_type: TypeName, + /// The index of the latest slice in the BigVector. + slice_idx: u64, + /// The maximum size of each slice in the BigVector. + slice_size: u32, + /// The total number of elements in the BigVector. + length: u64, + } + + /// A slice of the BigVector, containing a vector of elements. + public struct Slice has store, drop { + /// The index of the slice. + idx: u64, + /// The vector that stores the elements. + vector: vector, + } + + // ======== Functions ======== + + /// Creates a new `BigVector`. + /// The `slice_size` determines the maximum number of elements in each slice. + /// `slice_size * sizeof(Element)` should be below the object size limit of 256000 bytes. + public fun new(slice_size: u32, ctx: &mut TxContext): BigVector { + assert!(slice_size > 0 && slice_size <= CMaxSliceSize, EInvalidSliceSize); + + BigVector { + id: object::new(ctx), + element_type: type_name::with_defining_ids(), + slice_idx: 0, + slice_size, + length: 0, + } + } + + /// Returns the index of the latest slice in the BigVector. + public fun slice_idx(bv: &BigVector): u64 { + bv.slice_idx + } + + /// Returns the maximum size of each slice in the BigVector. + public fun slice_size(bv: &BigVector): u32 { + bv.slice_size + } + + /// Returns the total number of elements in the BigVector. + public fun length(bv: &BigVector): u64 { + bv.length + } + + /// Returns `true` if the BigVector is empty. + public fun is_empty(bv: &BigVector): bool { + bv.length == 0 + } + + /// Returns the index of the slice. + public fun get_slice_idx(slice: &Slice): u64 { + slice.idx + } + + /// Returns the number of elements in the slice. + public fun get_slice_length(slice: &Slice): u64 { + slice.vector.length() + } + + /// Pushes a new element to the end of the BigVector. + /// If the current slice is full, it creates a new slice. + public fun push_back(bv: &mut BigVector, element: Element) { + if (bv.is_empty() || bv.length() % (bv.slice_size as u64) == 0) { + bv.slice_idx = bv.length() / (bv.slice_size as u64); + let new_slice = Slice { + idx: bv.slice_idx, + vector: vector[element] + }; + dynamic_field::add(&mut bv.id, bv.slice_idx, new_slice); + } + else { + let slice = borrow_slice_mut_(&mut bv.id, bv.slice_idx); + slice.vector.push_back(element); + }; + bv.length = bv.length + 1; + } + + /// Pops an element from the end of the BigVector. + /// Aborts if the BigVector is empty. + public fun pop_back(bv: &mut BigVector): Element { + assert!(!bv.is_empty(), EIsEmpty); + + let slice = borrow_slice_mut_(&mut bv.id, bv.slice_idx); + let element = slice.vector.pop_back(); + bv.trim_slice(); + bv.length = bv.length - 1; + + element + } + + /// Borrows an element at index `i` from the BigVector. + /// Aborts if the index is out of bounds. + #[syntax(index)] + public fun borrow(bv: &BigVector, i: u64): &Element { + assert!(i < bv.length, EIndexOutOfBounds); + + let slice = borrow_slice_(&bv.id, i / (bv.slice_size as u64)); + &slice.vector[i % (bv.slice_size as u64)] + } + + /// Borrows a mutable element at index `i` from the BigVector. + /// Aborts if the index is out of bounds. + #[syntax(index)] + public fun borrow_mut(bv: &mut BigVector, i: u64): &mut Element { + assert!(i < bv.length, EIndexOutOfBounds); + + let slice = borrow_slice_mut_(&mut bv.id, i / (bv.slice_size as u64)); + &mut slice.vector[i % (bv.slice_size as u64)] + } + + /// Borrows a slice from the BigVector at `slice_idx`. + /// Aborts if the `slice_idx` is out of bounds. + public fun borrow_slice(bv: &BigVector, slice_idx: u64): &Slice { + assert!(slice_idx <= bv.slice_idx, EIndexOutOfBounds); + assert!(!bv.is_empty(), EIsEmpty); + + borrow_slice_(&bv.id, slice_idx) + } + fun borrow_slice_(id: &UID, slice_idx: u64): &Slice { + dynamic_field::borrow(id, slice_idx) + } + + /// Borrows a mutable slice from the BigVector at `slice_idx`. + /// Aborts if the `slice_idx` is out of bounds. + public fun borrow_slice_mut(bv: &mut BigVector, slice_idx: u64): &mut Slice { + assert!(slice_idx <= bv.slice_idx, EIndexOutOfBounds); + assert!(!bv.is_empty(), EIsEmpty); + + borrow_slice_mut_(&mut bv.id, slice_idx) + } + fun borrow_slice_mut_(id: &mut UID, slice_idx: u64): &mut Slice { + dynamic_field::borrow_mut(id, slice_idx) + } + + /// Borrows an element at index `i` from a slice. + /// Aborts if the index is out of bounds. + #[syntax(index)] + public fun borrow_from_slice(slice: &Slice, i: u64): &Element { + assert!(i < slice.vector.length(), EIndexOutOfBounds); + + &slice.vector[i] + } + + /// Borrows a mutable element at index `i` from a slice. + /// Aborts if the index is out of bounds. + #[syntax(index)] + public fun borrow_from_slice_mut(slice: &mut Slice, i: u64): &mut Element { + assert!(i < slice.vector.length(), EIndexOutOfBounds); + + &mut slice.vector[i] + } + + /// Swaps the element at index `i` with the last element and removes it. + /// This is more efficient than `remove` as it does not require shifting elements. + public fun swap_remove(bv: &mut BigVector, i: u64): Element { + assert!(i < bv.length, EIndexOutOfBounds); + let result = pop_back(bv); + if (i == bv.length()) { + result + } else { + let slice = borrow_slice_mut_(&mut bv.id, i / (bv.slice_size as u64)); + slice.vector.push_back(result); + slice.vector.swap_remove(i % (bv.slice_size as u64)) + } + } + + /// Removes the element at index `i` and shifts the rest of the elements to the left. + /// This is a costly function, especially for large BigVectors. Use with caution. + /// Aborts when referencing more than 1000 slices. + public fun remove(bv: &mut BigVector, i: u64): Element { + assert!(i < bv.length(), EIndexOutOfBounds); + + let slice = borrow_slice_mut_(&mut bv.id, (i / (bv.slice_size as u64))); + let result = slice.vector.remove(i % (bv.slice_size as u64)); + let mut slice_idx = bv.slice_idx; + while (slice_idx > i / (bv.slice_size as u64) && slice_idx > 0) { + let slice = borrow_slice_mut_(&mut bv.id, slice_idx); + let tmp: Element = slice.vector.remove(0); + let prev_slice = borrow_slice_mut_(&mut bv.id, slice_idx - 1); + prev_slice.vector.push_back(tmp); + slice_idx = slice_idx - 1; + }; + bv.trim_slice(); + bv.length = bv.length - 1; + + result + } + + /// Destroys an empty BigVector. + /// Aborts if the BigVector is not empty. + public fun destroy_empty(bv: BigVector) { + let BigVector { + id, + element_type: _, + slice_idx: _, + slice_size: _, + length, + } = bv; + assert!(length == 0, ENotEmpty); + id.delete(); + } + + /// Destroys a BigVector and its elements. + /// The element type must have the `drop` ability. + /// Aborts when the BigVector contains more than 1000 slices. + public fun drop(bv: BigVector) { + let BigVector { + mut id, + element_type: _, + mut slice_idx, + slice_size: _, + length: _, + } = bv; + while (slice_idx > 0) { + dynamic_field::remove>(&mut id, slice_idx); + slice_idx = slice_idx - 1; + }; + dynamic_field::remove>(&mut id, slice_idx); + id.delete(); + } + + /// Removes an empty slice after an element has been removed from it. + fun trim_slice(bv: &mut BigVector) { + let slice = borrow_slice_(&bv.id, bv.slice_idx); + if (slice.vector.is_empty()) { + let Slice { + idx: _, + vector: v, + } = dynamic_field::remove(&mut bv.id, bv.slice_idx); + v.destroy_empty(); + if (bv.slice_idx > 0) { + bv.slice_idx = bv.slice_idx - 1; + }; + }; + } +} \ No newline at end of file diff --git a/critbit.move b/critbit.move new file mode 100644 index 0000000..4e798db --- /dev/null +++ b/critbit.move @@ -0,0 +1,508 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a Crit-bit tree, a binary trie data structure that is highly efficient +/// for searching and storing keys. This implementation uses `u64` keys and generic values. +/// The tree is composed of internal nodes and leaf nodes, stored in tables. +module typus::critbit { + use sui::table::{Self, Table}; + + // ======== Error Code ======== + + /// Error when the tree's capacity is exceeded. + const EExceedCapacity: u64 = 0; + /// Error when trying to destroy a non-empty tree. + const ETreeNotEmpty: u64 = 1; + /// Error when a key to be inserted already exists in the tree. + const EKeyAlreadyExist: u64 = 2; + /// Error when a leaf does not exist. + const ELeafNotExist: u64 = 3; + /// Error for out-of-bounds access. + const EIndexOutOfRange: u64 = 4; + /// Error when a parent node is null. + const ENullParent: u64 = 5; + + // === Constants === + + /// A special value used to distinguish between internal nodes and leaves. + const PARTITION_INDEX: u64 = 0x8000000000000000; // 9223372036854775808 + /// The maximum value of a u64 integer. + const MAX_U64: u64 = 0xFFFFFFFFFFFFFFFF; // 18446744073709551615 + /// The maximum capacity of the tree. + const MAX_CAPACITY: u64 = 0x7fffffffffffffff; + + // === Structs === + + /// Represents a leaf node in the Crit-bit tree, storing a key-value pair. + public struct Leaf has store, drop { + /// The key of the leaf. + key: u64, + /// The value of the leaf. + value: V, + /// The index of the parent node. + parent: u64, + } + + /// Represents an internal node in the Crit-bit tree. + public struct InternalNode has store, drop { + /// The mask used to determine the branching direction. + mask: u64, + /// The index of the left child. + left_child: u64, + /// The index of the right child. + right_child: u64, + /// The index of the parent node. + parent: u64, + } + + /// The main Crit-bit tree structure. + public struct CritbitTree has store { + /// The index of the root node. + root: u64, + /// A table storing the internal nodes of the tree. + internal_nodes: Table, + /// A table storing the leaves of the tree. + leaves: Table>, + /// The index of the leaf with the minimum key. + min_leaf_index: u64, + /// The index of the leaf with the maximum key. + max_leaf_index: u64, + /// The index to be used for the next internal node. + next_internal_node_index: u64, + /// The index to be used for the next leaf. + next_leaf_index: u64 + } + + // ======== Public Functions ======== + + /// Creates a new, empty Crit-bit tree. + public fun new(ctx: &mut TxContext): CritbitTree { + CritbitTree{ + root: PARTITION_INDEX, + internal_nodes: table::new(ctx), + leaves: table::new(ctx), + min_leaf_index: PARTITION_INDEX, + max_leaf_index: PARTITION_INDEX, + next_internal_node_index: 0, + next_leaf_index: 0 + } + } + + /// Returns the number of leaves in the tree. + public fun size(tree: &CritbitTree): u64 { + tree.leaves.length() + } + + /// Returns `true` if the tree is empty. + public fun is_empty(tree: &CritbitTree): bool { + tree.leaves.is_empty() + } + + /// Returns `true` if a leaf with the given key exists in the tree. + public fun has_leaf(tree: &CritbitTree, key: u64): bool { + let (has_leaf, _) = tree.find_leaf(key); + has_leaf + } + + /// Returns `true` if a leaf with the given index exists in the tree. + public fun has_index(tree: &CritbitTree, index: u64): bool { + tree.leaves.contains(index) + } + + /// Returns the key and index of the leaf with the minimum key in the tree. + /// Aborts if the tree is empty. + public fun min_leaf(tree: &CritbitTree): (u64, u64) { + assert!(!tree.is_empty(), ELeafNotExist); + let min_leaf = tree.leaves.borrow(tree.min_leaf_index); + (min_leaf.key, tree.min_leaf_index) + } + + /// Returns the key and index of the leaf with the maximum key in the tree. + /// Aborts if the tree is empty. + public fun max_leaf(tree: &CritbitTree): (u64, u64) { + assert!(!tree.is_empty(), ELeafNotExist); + let max_leaf = tree.leaves.borrow(tree.max_leaf_index); + (max_leaf.key, tree.max_leaf_index) + } + + /// Returns the key and index of the leaf that comes before the given key in sorted order. + /// Returns `(0, PARTITION_INDEX)` if there is no previous leaf. + public fun previous_leaf(tree: &CritbitTree, key: u64): (u64, u64) { + let (has_leaf, mut index) = tree.find_leaf(key); + assert!(has_leaf, ELeafNotExist); + let mut ptr = MAX_U64 - index; + let mut parent = tree.leaves.borrow(index).parent; + while (parent != PARTITION_INDEX && tree.is_left_child(parent, ptr)) { + ptr = parent; + parent = tree.internal_nodes.borrow(ptr).parent; + }; + if(parent == PARTITION_INDEX) { + return (0, PARTITION_INDEX) + }; + index = MAX_U64 - tree.right_most_leaf(tree.internal_nodes.borrow(parent).left_child); + (tree.leaves.borrow(index).key, index) + } + + /// Returns the key and index of the leaf that comes after the given key in sorted order. + /// Returns `(0, PARTITION_INDEX)` if there is no next leaf. + public fun next_leaf(tree: &CritbitTree, key: u64): (u64, u64) { + let (has_leaf, mut index) = tree.find_leaf(key); + assert!(has_leaf, ELeafNotExist); + let mut ptr = MAX_U64 - index; + let mut parent = tree.leaves.borrow(index).parent; + while (parent != PARTITION_INDEX && !tree.is_left_child(parent, ptr)) { + ptr = parent; + parent = tree.internal_nodes.borrow(ptr).parent; + }; + if(parent == PARTITION_INDEX) { + return (0, PARTITION_INDEX) + }; + index = MAX_U64 - tree.left_most_leaf(tree.internal_nodes.borrow(parent).right_child); + (tree.leaves.borrow(index).key, index) + } + + /// Inserts a new leaf with the given key and value into the tree. + /// Returns the index of the new leaf. + /// Aborts if the key already exists or if the tree exceeds its capacity. + public fun insert_leaf(tree: &mut CritbitTree, key: u64, value: V): u64 { + let new_leaf = Leaf{ + key, + value, + parent: PARTITION_INDEX, + }; + let new_leaf_index = tree.next_leaf_index; + tree.next_leaf_index = tree.next_leaf_index + 1; + assert!(new_leaf_index < MAX_CAPACITY - 1, EExceedCapacity); + tree.leaves.add(new_leaf_index, new_leaf); + + let closest_leaf_index = tree.get_closest_leaf_index_by_key(key); + + // handle the first insertion + if(closest_leaf_index == PARTITION_INDEX) { + assert!(new_leaf_index == 0, ETreeNotEmpty); + tree.root = MAX_U64 - new_leaf_index; + tree.min_leaf_index = new_leaf_index; + tree.max_leaf_index = new_leaf_index; + return 0 + }; + + let closest_key = tree.leaves.borrow(closest_leaf_index).key; + assert!(closest_key != key, EKeyAlreadyExist); + + // note that we reserve count_leading_zeros of form u128 for future usage + let critbit = 64 - (count_leading_zeros(((closest_key ^ key) as u128) ) - 64); + let new_mask = 1u64 << (critbit - 1); + + let new_internal_node = InternalNode{ + mask: new_mask, + left_child: PARTITION_INDEX, + right_child: PARTITION_INDEX, + parent: PARTITION_INDEX, + }; + let new_internal_node_index = tree.next_internal_node_index; + tree.next_internal_node_index = tree.next_internal_node_index + 1; + tree.internal_nodes.add(new_internal_node_index, new_internal_node); + + let mut ptr = tree.root; + let mut new_internal_node_parent_index = PARTITION_INDEX; + // search position of the new internal node + while (ptr < PARTITION_INDEX) { + let internal_node = tree.internal_nodes.borrow(ptr); + if (new_mask > internal_node.mask) { + break + }; + new_internal_node_parent_index = ptr; + if (key & internal_node.mask == 0) { + ptr = internal_node.left_child; + }else { + ptr = internal_node.right_child; + }; + }; + + // we update the child info of new internal node's parent + if (new_internal_node_parent_index == PARTITION_INDEX) { + // if the new internal node is root + tree.root = new_internal_node_index; + } else{ + // In another case, we update the child field of the new internal node's parent + // and the parent field of the new internal node + let is_left_child = tree.is_left_child(new_internal_node_parent_index, ptr); + tree.update_child(new_internal_node_parent_index, new_internal_node_index, is_left_child); + }; + + // finally, we update the child filed of the new internal node + let is_left_child = new_mask & key == 0; + tree.update_child(new_internal_node_index, MAX_U64 - new_leaf_index, is_left_child); + tree.update_child(new_internal_node_index, ptr, !is_left_child); + + if (tree.leaves.borrow(tree.min_leaf_index).key > key) { + tree.min_leaf_index = new_leaf_index; + }; + if (tree.leaves.borrow(tree.max_leaf_index).key < key) { + tree.max_leaf_index = new_leaf_index; + }; + new_leaf_index + } + + /// Finds a leaf with the given key and returns a boolean indicating if it was found, + /// along with the index of the leaf if found. + public fun find_leaf(tree: & CritbitTree, key: u64): (bool, u64) { + if (tree.is_empty()) { + return (false, PARTITION_INDEX) + }; + let closest_leaf_index = tree.get_closest_leaf_index_by_key(key); + let closeset_leaf = tree.leaves.borrow(closest_leaf_index); + if (closeset_leaf.key != key) { + (false, PARTITION_INDEX) + } else { + (true, closest_leaf_index) + } + } + + /// Finds the key of the leaf that is closest to the given key. + /// Returns 0 if the tree is empty. + public fun find_closest_key(tree: & CritbitTree, key: u64): u64 { + if (tree.is_empty()) { + return 0 + }; + let closest_leaf_index = tree.get_closest_leaf_index_by_key(key); + let closeset_leaf = tree.leaves.borrow(closest_leaf_index); + closeset_leaf.key + } + + /// Removes the leaf with the minimum key from the tree and returns its value. + public fun remove_min_leaf(tree: &mut CritbitTree): V { + let index = tree.min_leaf_index; + tree.remove_leaf_by_index(index) + } + + /// Removes the leaf with the maximum key from the tree and returns its value. + public fun remove_max_leaf(tree: &mut CritbitTree): V { + let index = tree.max_leaf_index; + tree.remove_leaf_by_index(index) + } + + /// Removes a leaf from the tree by its index and returns its value. + public fun remove_leaf_by_index(tree: &mut CritbitTree, index: u64): V { + let key = tree.leaves.borrow(index).key; + if(tree.min_leaf_index == index) { + let (_, next_index) = tree.next_leaf(key); + tree.min_leaf_index = next_index; + }; + if(tree.max_leaf_index == index) { + let (_, previous_index) = tree.previous_leaf(key); + tree.max_leaf_index = previous_index; + }; + + let mut is_left_child_; + let Leaf {key: _, value, parent: removed_leaf_parent_index} = tree.leaves.remove(index); + if (size(tree) == 0) { + tree.root = PARTITION_INDEX; + tree.min_leaf_index = PARTITION_INDEX; + tree.max_leaf_index = PARTITION_INDEX; + tree.next_internal_node_index = 0; + tree.next_leaf_index = 0; + } else{ + assert!(removed_leaf_parent_index != PARTITION_INDEX, EIndexOutOfRange); + let removed_leaf_parent = tree.internal_nodes.borrow(removed_leaf_parent_index); + let removed_leaf_grand_parent_index = removed_leaf_parent.parent; + + // note that sibling of the removed leaf can be a leaf or a internal node + is_left_child_ = tree.is_left_child(removed_leaf_parent_index, MAX_U64 - index); + let sibling_index = if (is_left_child_) { removed_leaf_parent.right_child } + else { removed_leaf_parent.left_child }; + + if (removed_leaf_grand_parent_index == PARTITION_INDEX) { + // parent of the removed leaf is the tree root + // update the parent of the sibling node and and set sibling as the tree root + if (sibling_index < PARTITION_INDEX) { + // sibling is a internal node + tree.internal_nodes.borrow_mut(sibling_index).parent = PARTITION_INDEX; + } else { + // sibling is a leaf + tree.leaves.borrow_mut(MAX_U64 - sibling_index).parent = PARTITION_INDEX; + }; + tree.root = sibling_index; + } else { + // grand parent of the removed leaf is a internal node + // set sibling as the child of the grand parent of the removed leaf + is_left_child_ = tree.is_left_child(removed_leaf_grand_parent_index, removed_leaf_parent_index); + tree.update_child(removed_leaf_grand_parent_index, sibling_index, is_left_child_); + }; + tree.internal_nodes.remove(removed_leaf_parent_index); + }; + value + } + + /// Removes a leaf from the tree by its key and returns its value. + /// Aborts if the key does not exist. + public fun remove_leaf_by_key(tree: &mut CritbitTree, key: u64): V { + let (is_exist, index) = tree.find_leaf(key); + assert!(is_exist, ELeafNotExist); + tree.remove_leaf_by_index(index) + } + + /// Borrows a mutable reference to the value of a leaf by its index. + public fun borrow_mut_leaf_by_index(tree: &mut CritbitTree, index: u64): &mut V { + let entry = tree.leaves.borrow_mut(index); + &mut entry.value + } + + /// Borrows a mutable reference to the value of a leaf by its key. + /// Aborts if the key does not exist. + public fun borrow_mut_leaf_by_key(tree: &mut CritbitTree, key: u64): &mut V { + let (is_exist, index) = tree.find_leaf(key); + assert!(is_exist, ELeafNotExist); + tree.borrow_mut_leaf_by_index(index) + } + + /// Borrows an immutable reference to the value of a leaf by its index. + public fun borrow_leaf_by_index(tree: & CritbitTree, index: u64): &V { + let entry = tree.leaves.borrow(index); + &entry.value + } + + /// Borrows an immutable reference to the value of a leaf by its key. + /// Aborts if the key does not exist. + public fun borrow_leaf_by_key(tree: & CritbitTree, key: u64): &V { + let (is_exist, index) = tree.find_leaf(key); + assert!(is_exist, ELeafNotExist); + tree.borrow_leaf_by_index(index) + } + + /// Destroys the tree, dropping all the entries within. + /// The value type must have the `drop` ability. + public fun drop(tree: CritbitTree) { + let CritbitTree { + root: _, + internal_nodes, + leaves, + min_leaf_index: _, + max_leaf_index: _, + next_internal_node_index: _, + next_leaf_index: _, + + } = tree; + internal_nodes.drop(); + leaves.drop(); + } + + /// Destroys an empty tree. + /// Aborts if the tree is not empty. + public fun destroy_empty(tree: CritbitTree) { + assert!(tree.leaves.length() == 0, ETreeNotEmpty); + + let CritbitTree { + root: _, + leaves, + internal_nodes, + min_leaf_index: _, + max_leaf_index: _, + next_internal_node_index: _, + next_leaf_index: _, + } = tree; + leaves.destroy_empty(); + internal_nodes.destroy_empty(); + } + + // === Helper functions === + + /// Finds the leftmost leaf starting from a given root. + fun left_most_leaf(tree: &CritbitTree, root: u64): u64 { + let mut ptr = root; + while (ptr < PARTITION_INDEX) { + ptr = tree.internal_nodes.borrow(ptr).left_child; + }; + ptr + } + + /// Finds the rightmost leaf starting from a given root. + fun right_most_leaf(tree: &CritbitTree, root: u64): u64 { + let mut ptr = root; + while (ptr < PARTITION_INDEX) { + ptr = tree.internal_nodes.borrow(ptr).right_child; + }; + ptr + } + + /// Finds the index of the leaf that is closest to the given key. + fun get_closest_leaf_index_by_key(tree: &CritbitTree, key: u64): u64 { + let mut ptr = tree.root; + // if tree is empty, return the patrition index + if(ptr == PARTITION_INDEX) return PARTITION_INDEX; + while (ptr < PARTITION_INDEX) { + let node = tree.internal_nodes.borrow(ptr); + if (key & node.mask == 0) { + ptr = node.left_child; + } else { + ptr = node.right_child; + } + }; + MAX_U64 - ptr + } + + /// Updates the child of a parent node. + fun update_child(tree: &mut CritbitTree, parent_index: u64, new_child: u64, is_left_child: bool) { + assert!(parent_index != PARTITION_INDEX, ENullParent); + if (is_left_child) { + tree.internal_nodes.borrow_mut(parent_index).left_child = new_child; + } else{ + tree.internal_nodes.borrow_mut(parent_index).right_child = new_child; + }; + if (new_child != PARTITION_INDEX) { + if (new_child > PARTITION_INDEX) { + tree.leaves.borrow_mut(MAX_U64 - new_child).parent = parent_index; + }else{ + tree.internal_nodes.borrow_mut(new_child).parent = parent_index; + } + }; + } + + /// Returns `true` if the node at `index` is the left child of the node at `parent_index`. + fun is_left_child(tree: &CritbitTree, parent_index: u64, index: u64): bool { + tree.internal_nodes.borrow(parent_index).left_child == index + } + + /// Counts the number of leading zeros in a u128 integer. + fun count_leading_zeros(mut x: u128): u8 { + if (x == 0) { + 128 + } else { + let mut n: u8 = 0; + if (x & 0xFFFFFFFFFFFFFFFF0000000000000000 == 0) { + // x's higher 64 is all zero, shift the lower part over + x = x << 64; + n = n + 64; + }; + if (x & 0xFFFFFFFF000000000000000000000000 == 0) { + // x's higher 32 is all zero, shift the lower part over + x = x << 32; + n = n + 32; + }; + if (x & 0xFFFF0000000000000000000000000000 == 0) { + // x's higher 16 is all zero, shift the lower part over + x = x << 16; + n = n + 16; + }; + if (x & 0xFF000000000000000000000000000000 == 0) { + // x's higher 8 is all zero, shift the lower part over + x = x << 8; + n = n + 8; + }; + if (x & 0xF0000000000000000000000000000000 == 0) { + // x's higher 4 is all zero, shift the lower part over + x = x << 4; + n = n + 4; + }; + if (x & 0xC0000000000000000000000000000000 == 0) { + // x's higher 2 is all zero, shift the lower part over + x = x << 2; + n = n + 2; + }; + if (x & 0x80000000000000000000000000000000 == 0) { + n = n + 1; + }; + n + } + } +} \ No newline at end of file diff --git a/ecosystem.move b/ecosystem.move new file mode 100644 index 0000000..c5b6995 --- /dev/null +++ b/ecosystem.move @@ -0,0 +1,249 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module defines the core components of the Typus ecosystem, including version management, +/// authority control, and a fee collection mechanism. It serves as the central point of control +/// for the entire system. +module typus::ecosystem { + use std::type_name::{Self, TypeName}; + + use sui::balance::{Self, Balance}; + use sui::coin; + use sui::dynamic_field; + use sui::event::emit; + use sui::vec_set::{Self, VecSet}; + + // ======== Constants ======== + + /// The current version of the ecosystem. + const CVersion: u64 = 31; + + // ======== Error Code ======== + + /// Error when an authority to be added already exists. + const EAuthorityAlreadyExists: u64 = 0; + /// Error when an authority to be removed does not exist. + const EAuthorityDoesNotExist: u64 = 1; + /// Error when there are no authorities left. + const EAuthorityEmpty: u64 = 2; + /// Error for an invalid version. + const EInvalidVersion: u64 = 3; + /// Error for an unauthorized action. + const EUnauthorized: u64 = 4; + + // ======== Manager Cap ======== + + /// A capability object that grants manager-level privileges for the Typus ecosystem. + public struct ManagerCap has store { } + + /// Issues a `ManagerCap` to the transaction sender. + /// This is an authorized function and can only be called by an existing authority. + public fun issue_manager_cap( + version: &Version, + ctx: &TxContext, + ): ManagerCap { + version.verify(ctx); + + ManagerCap { } + } + + /// Burns a `ManagerCap`. + /// This is an authorized function and can only be called by an existing authority. + public fun burn_manager_cap( + version: &Version, + manager_cap: ManagerCap, + ctx: &TxContext, + ) { + version.verify(ctx); + let ManagerCap { } = manager_cap; + } + + // ======== Version ======== + + /// A shared object that represents the current version of the Typus ecosystem. + /// It holds the authority list and the fee pool. + public struct Version has key { + /// The unique identifier of the Version object. + id: UID, + /// The current version number. + value: u64, + /// The fee pool for collecting fees. + fee_pool: FeePool, + /// A set of addresses of the authorized users. + authority: VecSet
, + /// Padding for future use. + u64_padding: vector, + } + + /// Checks if the current version is valid. + /// Aborts if the version is older than the current version. + public(package) fun version_check(version: &Version) { + assert!(CVersion >= version.value, EInvalidVersion); + } + + /// Borrows a mutable reference to the UID of the `Version` object. + public(package) fun borrow_uid_mut(version: &mut Version): &mut UID { + &mut version.id + } + + /// Borrows an immutable reference to the UID of the `Version` object. + public(package) fun borrow_uid(version: &Version): &UID { + &version.id + } + + /// Upgrades the version of the ecosystem to the latest version. + entry fun upgrade(version: &mut Version) { + version.version_check(); + version.value = CVersion; + } + + // ======== Init ======== + + /// Initializes the `Version` object and shares it. + /// The initial authority is the sender of the transaction. + fun init(ctx: &mut TxContext) { + transfer::share_object(Version { + id: object::new(ctx), + value: CVersion, + fee_pool: FeePool { + id: object::new(ctx), + fee_infos: vector[], + }, + authority: vec_set::singleton(ctx.sender()), + u64_padding: vector[], + }); + } + + // ======== Authority ======== + + /// Verifies if the sender of the transaction is an authorized user. + /// Aborts if the sender is not in the authority list. + public(package) fun verify( + version: &Version, + ctx: &TxContext, + ) { + version.version_check(); + + assert!( + version.authority.contains(&ctx.sender()), + EUnauthorized + ); + } + + /// Adds a new authorized user to the authority list. + /// This is an authorized function and can only be called by an existing authority. + entry fun add_authorized_user( + version: &mut Version, + user_address: address, + ctx: &TxContext, + ) { + version.verify(ctx); + + assert!(!version.authority.contains(&user_address), EAuthorityAlreadyExists); + version.authority.insert(user_address); + } + + /// Removes an authorized user from the authority list. + /// This is an authorized function and can only be called by an existing authority. + /// Aborts if the user to be removed does not exist or if there are no authorities left. + entry fun remove_authorized_user( + version: &mut Version, + user_address: address, + ctx: &TxContext, + ) { + version.verify(ctx); + + assert!(version.authority.contains(&user_address), EAuthorityDoesNotExist); + version.authority.remove(&user_address); + assert!(version.authority.length() > 0, EAuthorityEmpty); + } + + // ======== Fee Pool ======== + + /// Manages the collection of fees in the ecosystem. + public struct FeePool has key, store { + /// The unique identifier of the FeePool object. + id: UID, + /// A vector of `FeeInfo` structs, one for each token type. + fee_infos: vector, + } + + /// Stores the fee information for a specific token. + public struct FeeInfo has copy, drop, store { + /// The type name of the token. + token: TypeName, + /// The total amount of fees collected for this token. + value: u64, + } + + /// Event emitted when fees are sent from the fee pool. + public struct SendFeeEvent has copy, drop { + /// The type name of the token. + token: TypeName, + /// Log data: [sent_fee_value] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + /// Sends the collected fees for a specific token to a designated fee address. + entry fun send_fee( + version: &mut Version, + ctx: &mut TxContext, + ) { + version.version_check(); + + let mut i = 0; + while (i < version.fee_pool.fee_infos.length()) { + let fee_info = version.fee_pool.fee_infos.borrow_mut(i); + if (fee_info.token == type_name::with_defining_ids()) { + transfer::public_transfer( + coin::from_balance( + balance::withdraw_all(dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::with_defining_ids())), + ctx, + ), + @fee_address, + ); + emit(SendFeeEvent { + token: type_name::with_defining_ids(), + log: vector[fee_info.value], + bcs_padding: vector[], + }); + fee_info.value = 0; + }; + i = i + 1; + }; + } + + /// Charges a fee for a specific token and adds it to the fee pool. + /// If the token is not yet in the fee pool, it adds a new `FeeInfo` entry. + public fun charge_fee( + version: &mut Version, + balance: Balance, + ) { + let mut i = 0; + while (i < version.fee_pool.fee_infos.length()) { + let fee_info = &mut version.fee_pool.fee_infos[i]; + if (fee_info.token == type_name::with_defining_ids()) { + fee_info.value = fee_info.value + balance::value(&balance); + balance::join( + dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::with_defining_ids()), + balance, + ); + return + }; + i = i + 1; + }; + version.fee_pool.fee_infos.push_back( + FeeInfo { + token: type_name::with_defining_ids(), + value: balance::value(&balance), + }, + ); + dynamic_field::add(&mut version.fee_pool.id, type_name::with_defining_ids(), balance); + } + + #[test_only] + public fun test_init(ctx: &mut TxContext) { + init(ctx); + } +} \ No newline at end of file diff --git a/error.move b/error.move new file mode 100644 index 0000000..4d00d85 --- /dev/null +++ b/error.move @@ -0,0 +1,9 @@ +/// This module defines custom error codes for the Typus ecosystem. +/// These functions are used to abort transactions with specific error codes, +/// providing more context about the reason for the failure. +module typus::error { + /// Aborts the transaction with an error code indicating that an account was not found. + public fun account_not_found(error_code: u64): u64 { abort error_code } + /// Aborts the transaction with an error code indicating that an account already exists. + public fun account_already_exists(error_code: u64): u64 { abort error_code } +} \ No newline at end of file diff --git a/event.move b/event.move new file mode 100644 index 0000000..5852141 --- /dev/null +++ b/event.move @@ -0,0 +1,45 @@ +/// This module provides a standardized way to emit events in the Typus ecosystem. +/// It defines a generic `Event` struct and a helper function to emit events with a consistent format. +module typus::event { + use std::string::String; + + use sui::event::emit; + use sui::vec_map::VecMap; + + /// A generic event structure for logging actions and data in the Typus ecosystem. + public struct TypusEvent has copy, drop { + /// A string that describes the action being performed. + action: String, + /// A map for logging key-value pairs of `u64` data. + log: VecMap, + /// A map for logging key-value pairs of BCS-encoded data. + bcs_padding: VecMap>, + } + + /// Emits a generic `Event`. + /// This function is used throughout the Typus ecosystem to log events in a standardized format. + public(package) fun emit_typus_event( + action: String, + log: VecMap, + bcs_padding: VecMap>, + ) { + emit(TypusEvent { + action, + log, + bcs_padding, + }); + } + + #[deprecated] + public struct Event has copy, drop { + action: String, + log: VecMap, + bcs_padding: VecMap>, + } + #[deprecated, allow(unused)] + public fun emit_event( + action: String, + log: VecMap, + bcs_padding: VecMap>, + ) { abort 0 } +} \ No newline at end of file diff --git a/keyed_big_vector.move b/keyed_big_vector.move new file mode 100644 index 0000000..7e934ed --- /dev/null +++ b/keyed_big_vector.move @@ -0,0 +1,398 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a `KeyedBigVector`, a data structure that combines the features of a `BigVector` +/// and a `Table`. It allows for both indexed and keyed access to a large number of elements by storing +/// them in slices, while maintaining a mapping from keys to indices in a `Table`. +module typus::keyed_big_vector { + use std::type_name::{Self, TypeName}; + + use sui::dynamic_field; + use sui::table; + + // ======== Constants ======== + + /// The maximum number of slices allowed in a KeyedBigVector. + const CMaxSliceAmount: u16 = 1000; + /// The maximum size of a slice. + const CMaxSliceSize: u32 = 262144; + /// The key for the dynamic field that stores the key-to-index table. + const SKeyIndexTable: vector = b"key_index_table"; + + // ======== Errors ======== + + /// Error for a duplicate key. + fun duplicate_key(): u64 { abort 0 } + /// Error for an out-of-bounds index. + fun index_out_of_bounds(): u64 { abort 1 } + /// Error for an invalid slice size. + fun invalid_slice_size(): u64 { abort 2 } + /// Error when a key is not found. + fun key_not_found(): u64 { abort 3 } + /// Error when the maximum number of slices is reached. + fun max_slice_amount_reached(): u64 { abort 4 } + /// Error when trying to destroy a non-empty KeyedBigVector. + fun not_empty(): u64 { abort 5 } + + // ======== Structs ======== + + /// A data structure that allows for both indexed and keyed access to a large number of elements. + public struct KeyedBigVector has key, store { + /// The unique identifier of the KeyedBigVector object. + id: UID, + /// The type name of the keys. + key_type: TypeName, + /// The type name of the values. + value_type: TypeName, + /// The index of the latest slice. + slice_idx: u16, + /// The maximum size of each slice. + slice_size: u32, + /// The total number of elements in the KeyedBigVector. + length: u64, + } + + /// A slice of the KeyedBigVector, containing a vector of elements. + public struct Slice has store, drop { + /// The index of the slice. + idx: u16, + /// The vector that stores the elements. + vector: vector>, + } + + /// An element in the KeyedBigVector, containing a key-value pair. + public struct Element has store, drop { + /// The key of the element. + key: K, + /// The value of the element. + value: V, + } + + // ======== Functions ======== + + /// Creates a new `KeyedBigVector`. + /// The `slice_size` determines the maximum number of elements in each slice. + public fun new(slice_size: u32, ctx: &mut TxContext): KeyedBigVector { + assert!(slice_size > 0 && slice_size <= CMaxSliceSize, invalid_slice_size()); + let mut id = object::new(ctx); + dynamic_field::add(&mut id, SKeyIndexTable.to_string(), table::new(ctx)); + + KeyedBigVector { + id, + key_type: type_name::with_defining_ids(), + value_type: type_name::with_defining_ids(), + slice_idx: 0, + slice_size, + length: 0, + } + } + + /// Returns the index of the latest slice in the KeyedBigVector. + public fun slice_idx(kbv: &KeyedBigVector): u16 { + kbv.slice_idx + } + + /// Returns the maximum size of each slice in the KeyedBigVector. + public fun slice_size(kbv: &KeyedBigVector): u32 { + kbv.slice_size + } + + /// Returns the total number of elements in the KeyedBigVector. + public fun length(kbv: &KeyedBigVector): u64 { + kbv.length + } + + /// Returns `true` if the KeyedBigVector is empty. + public fun is_empty(kbv: &KeyedBigVector): bool { + kbv.length == 0 + } + + /// Returns `true` if there is a value associated with the key `key` in the KeyedBigVector. + public fun contains(kbv: &KeyedBigVector, key: K): bool { + table::contains(dynamic_field::borrow(&kbv.id, SKeyIndexTable.to_string()), key) + } + + /// Returns the index of the slice. + public fun get_slice_idx(slice: &Slice): u16 { + slice.idx + } + + /// Returns the number of elements in the slice. + public fun get_slice_length(slice: &Slice): u64 { + slice.vector.length() + } + + /// Pushes a new element to the end of the KeyedBigVector. + /// Aborts if the key already exists or if the maximum number of slices is reached. + public fun push_back(kbv: &mut KeyedBigVector, key: K, value: V) { + assert!(!kbv.contains(key), duplicate_key()); + let element = Element { key, value }; + if (kbv.is_empty() || kbv.length() % (kbv.slice_size as u64) == 0) { + kbv.slice_idx = (kbv.length() / (kbv.slice_size as u64) as u16); + assert!(kbv.slice_idx < CMaxSliceAmount, max_slice_amount_reached()); + let new_slice = Slice { + idx: kbv.slice_idx, + vector: vector[element] + }; + dynamic_field::add(&mut kbv.id, kbv.slice_idx, new_slice); + } + else { + let slice = borrow_slice_mut_(&mut kbv.id, kbv.slice_idx); + slice.vector.push_back(element); + }; + table::add(dynamic_field::borrow_mut(&mut kbv.id, SKeyIndexTable.to_string()), key, kbv.length); + kbv.length = kbv.length + 1; + } + + /// Pops an element from the end of the KeyedBigVector and returns its key and value. + /// Aborts if the KeyedBigVector is empty. + public fun pop_back(kbv: &mut KeyedBigVector): (K, V) { + assert!(!kbv.is_empty(), index_out_of_bounds()); + + let slice = borrow_slice_mut_(&mut kbv.id, kbv.slice_idx); + let Element { key, value } = slice.vector.pop_back(); + kbv.trim_slice(); + table::remove(dynamic_field::borrow_mut(&mut kbv.id, SKeyIndexTable.to_string()), key); + kbv.length = kbv.length - 1; + + (key, value) + } + + /// Borrows a slice from the KeyedBigVector at `slice_idx`. + public fun borrow_slice(kbv: &KeyedBigVector, slice_idx: u16): &Slice { + assert!(slice_idx <= kbv.slice_idx, index_out_of_bounds()); + + borrow_slice_(&kbv.id, slice_idx) + } + fun borrow_slice_(id: &UID, slice_idx: u16): &Slice { + dynamic_field::borrow(id, slice_idx) + } + + /// Borrows a mutable slice from the KeyedBigVector at `slice_idx`. + public fun borrow_slice_mut(kbv: &mut KeyedBigVector, slice_idx: u16): &mut Slice { + assert!(slice_idx <= kbv.slice_idx, index_out_of_bounds()); + + borrow_slice_mut_(&mut kbv.id, slice_idx) + } + fun borrow_slice_mut_(id: &mut UID, slice_idx: u16): &mut Slice { + dynamic_field::borrow_mut(id, slice_idx) + } + + /// Borrows an element at index `i` from the KeyedBigVector. + public fun borrow(kbv: &KeyedBigVector, i: u64): (K, &V) { + assert!(i < kbv.length, index_out_of_bounds()); + + borrow_(kbv, i) + } + fun borrow_(kbv: &KeyedBigVector, i: u64): (K, &V) { + let slice = borrow_slice_(&kbv.id, (i / (kbv.slice_size as u64) as u16)); + let element = &slice.vector[i % (kbv.slice_size as u64)]; + + (element.key, &element.value) + } + + /// Borrows a mutable element at index `i` from the KeyedBigVector. + public fun borrow_mut(kbv: &mut KeyedBigVector, i: u64): (K, &mut V) { + assert!(i < kbv.length, index_out_of_bounds()); + + borrow_mut_(kbv, i) + } + fun borrow_mut_(kbv: &mut KeyedBigVector, i: u64): (K, &mut V) { + let slice = borrow_slice_mut_(&mut kbv.id, (i / (kbv.slice_size as u64) as u16)); + let element = &mut slice.vector[i % (kbv.slice_size as u64)]; + + (element.key, &mut element.value) + } + + /// Borrows an element by its key from the KeyedBigVector. + #[syntax(index)] + public fun borrow_by_key(kbv: &KeyedBigVector, key: K): &V { + assert!(kbv.contains(key), key_not_found()); + + let i = *table::borrow(dynamic_field::borrow(&kbv.id, SKeyIndexTable.to_string()), key); + let (_, v) = borrow_(kbv, i); + + v + } + + /// Borrows a mutable element by its key from the KeyedBigVector. + #[syntax(index)] + public fun borrow_by_key_mut(kbv: &mut KeyedBigVector, key: K): &mut V { + assert!(kbv.contains(key), key_not_found()); + + let i = *table::borrow(dynamic_field::borrow(&kbv.id, SKeyIndexTable.to_string()), key); + let (_, v) = borrow_mut_(kbv, i); + + v + } + + /// Borrows an element at index `i` from a slice. + public fun borrow_from_slice(slice: &Slice, i: u64): (K, &V) { + assert!(i < slice.vector.length(), index_out_of_bounds()); + + let element = &slice.vector[i]; + + (element.key, &element.value) + } + + /// Borrows a mutable element at index `i` from a slice. + public fun borrow_from_slice_mut(slice: &mut Slice, i: u64): (K, &mut V) { + assert!(i < slice.vector.length(), index_out_of_bounds()); + + let element = &mut slice.vector[i]; + + (element.key, &mut element.value) + } + + /// Swaps the element at index `i` with the last element and removes it. + public fun swap_remove(kbv: &mut KeyedBigVector, i: u64): (K, V) { + assert!(i < kbv.length, index_out_of_bounds()); + + swap_remove_(kbv, i) + } + fun swap_remove_(kbv: &mut KeyedBigVector, i: u64): (K, V) { + let (key, value) = pop_back(kbv); + if (i == kbv.length()) { + (key, value) + } else { + table::add(dynamic_field::borrow_mut(&mut kbv.id, SKeyIndexTable.to_string()), key, i); + let slice = borrow_slice_mut_(&mut kbv.id, (i / (kbv.slice_size as u64) as u16)); + slice.vector.push_back(Element { key, value }); + let Element { key, value } = slice.vector.swap_remove(i % (kbv.slice_size as u64)); + table::remove(dynamic_field::borrow_mut(&mut kbv.id, SKeyIndexTable.to_string()), key); + (key, value) + } + } + + /// Swaps the element with the given key with the last element and removes it. + public fun swap_remove_by_key(kbv: &mut KeyedBigVector, key: K): V { + assert!(kbv.contains(key), key_not_found()); + + let i = *table::borrow(dynamic_field::borrow(&kbv.id, SKeyIndexTable.to_string()), key); + let (_, v) = swap_remove_(kbv, i); + + v + } + + /// Destroys an empty KeyedBigVector. + /// Aborts if the KeyedBigVector is not empty. + public fun destroy_empty(kbv: KeyedBigVector) { + let KeyedBigVector { + id, + key_type: _, + value_type: _, + slice_idx: _, + slice_size: _, + length, + } = kbv; + assert!(length == 0, not_empty()); + id.delete(); + } + + /// Destroys a KeyedBigVector. + public fun drop(kbv: KeyedBigVector) { + let KeyedBigVector { + id, + key_type: _, + value_type: _, + slice_idx: _, + slice_size: _, + length: _, + } = kbv; + id.delete(); + } + + /// Destroys a KeyedBigVector and its elements completely. + public fun completely_drop(kbv: KeyedBigVector) { + let KeyedBigVector { + mut id, + key_type: _, + value_type: _, + slice_idx, + slice_size: _, + length, + } = kbv; + if (length > 0) { + (slice_idx + 1).do!(|i| { + dynamic_field::remove>(&mut id, slice_idx - i); + }); + }; + id.delete(); + } + + /// Removes an empty slice after an element has been removed from it. + fun trim_slice(kbv: &mut KeyedBigVector) { + let slice = borrow_slice_(&kbv.id, kbv.slice_idx); + if (slice.vector.is_empty>()) { + let Slice { + idx: _, + vector: v, + } = dynamic_field::remove(&mut kbv.id, kbv.slice_idx); + v.destroy_empty>(); + if (kbv.slice_idx > 0) { + kbv.slice_idx = kbv.slice_idx - 1; + }; + }; + } + + /// A macro for iterating over the elements of a KeyedBigVector with immutable references. + public macro fun do_ref<$K, $V>($kbv: &KeyedBigVector, $f: |$K, &$V|) { + let kbv = $kbv; + let length = kbv.length(); + if (length > 0) { + let slice_size = (kbv.slice_size() as u64); + let mut slice = kbv.borrow_slice(0); + length.do!(|i| { + let (key, value) = slice.borrow_from_slice(i % slice_size); + $f(key, value); + // jump to next slice + if (i + 1 < length && (i + 1) % slice_size == 0) { + slice = kbv.borrow_slice(((i + 1) / slice_size) as u16); + }; + }); + }; + } + + /// A macro for iterating over the elements of a KeyedBigVector with mutable references. + public macro fun do_mut<$K, $V>($kbv: &mut KeyedBigVector, $f: |$K, &mut $V|) { + let kbv = $kbv; + let length = kbv.length(); + if (length > 0) { + let slice_size = (kbv.slice_size() as u64); + let mut slice = kbv.borrow_slice_mut(0); + length.do!(|i| { + let (key, value) = slice.borrow_from_slice_mut(i % slice_size); + $f(key, value); + // jump to next slice + if (i + 1 < length && (i + 1) % slice_size == 0) { + slice = kbv.borrow_slice_mut(((i + 1) / slice_size) as u16); + }; + }); + }; + } + + #[test, expected_failure] + fun test_duplicate_key() { + duplicate_key(); + } + #[test, expected_failure] + fun test_index_out_of_bounds() { + index_out_of_bounds(); + } + #[test, expected_failure] + fun test_invalid_slice_size() { + invalid_slice_size(); + } + #[test, expected_failure] + fun test_key_not_found() { + key_not_found(); + } + #[test, expected_failure] + fun test_max_slice_amount_reached() { + max_slice_amount_reached(); + } + #[test, expected_failure] + fun test_not_empty() { + not_empty(); + } +} \ No newline at end of file diff --git a/leaderboard.move b/leaderboard.move new file mode 100644 index 0000000..3446b9a --- /dev/null +++ b/leaderboard.move @@ -0,0 +1,525 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a leaderboard system for tracking user scores and rankings. +/// It supports multiple leaderboards, each with its own start and end time. +/// Leaderboards can be activated, extended, deactivated, and removed. +/// User scores can be updated, and rankings can be retrieved. +module typus::leaderboard { + use std::ascii::String; + + use sui::bcs; + use sui::clock::Clock; + use sui::dynamic_field; + use sui::event::emit; + use sui::table::{Self, Table}; + + use typus::critbit::{Self, CritbitTree}; + use typus::ecosystem::{ManagerCap, Version}; + use typus::linked_object_table::{Self, LinkedObjectTable}; + use typus::linked_set::{Self, LinkedSet}; + + // ======== Error Code ======== + + #[error] + const EInvalidTimePeriod: vector = b"invalid_time_period"; + + // ======== Typus Leaderboard ======== + + /// A registry for all leaderboards, separating them into active and inactive categories. + public struct TypusLeaderboardRegistry has key { + id: UID, + /// A UID for the dynamic field that stores the active leaderboards. + active_leaderboard_registry: UID, + /// A UID for the dynamic field that stores the inactive leaderboards. + inactive_leaderboard_registry: UID, + } + + /// Represents a single leaderboard. + public struct Leaderboard has key, store { + /// The unique identifier of the Leaderboard object. + id: UID, + /// The start timestamp of the leaderboard in milliseconds. + start_ts_ms: u64, + /// The end timestamp of the leaderboard in milliseconds. + end_ts_ms: u64, + /// A table mapping user addresses to their scores. + score: Table, + /// A Crit-bit tree for ranking users by score. The value is a linked set of user addresses with the same score. + ranking: CritbitTree>, + } + + /// Initializes the `TypusLeaderboardRegistry` and shares it. + fun init(ctx: &mut TxContext) { + transfer::share_object(TypusLeaderboardRegistry { + id: object::new(ctx), + active_leaderboard_registry: object::new(ctx), + inactive_leaderboard_registry: object::new(ctx), + }); + } + + /// Event emitted when a leaderboard is activated. + public struct ActivateLeaderboardEvent has copy, drop { + key: String, + id: address, + log: vector, + bcs_padding: vector>, + } + /// Activates a new leaderboard. + /// This is an authorized function. + public fun activate_leaderboard( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + start_ts_ms: u64, + end_ts_ms: u64, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + assert!(end_ts_ms > start_ts_ms, EInvalidTimePeriod); + if (!dynamic_field::exists(®istry.active_leaderboard_registry, key)) { + dynamic_field::add( + &mut registry.active_leaderboard_registry, + key, + linked_object_table::new(ctx), + ); + }; + let leaderboards: &mut LinkedObjectTable = + dynamic_field::borrow_mut(&mut registry.active_leaderboard_registry, key); + let leaderboard = Leaderboard { + id: object::new(ctx), + start_ts_ms, + end_ts_ms, + score: table::new(ctx), + ranking: critbit::new(ctx), + }; + emit(ActivateLeaderboardEvent { + key, + id: object::id_address(&leaderboard), + log: vector[start_ts_ms, end_ts_ms], + bcs_padding: vector[], + }); + leaderboards.push_back( + object::id_address(&leaderboard), + leaderboard, + ); + } + + /// Event emitted when a leaderboard's end time is extended. + public struct ExtendLeaderboardEvent has copy, drop { + key: String, + id: address, + log: vector, + bcs_padding: vector>, + } + /// Extends the end time of an active leaderboard. + /// This is an authorized function. + public fun extend_leaderboard( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + id: address, + end_ts_ms: u64, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + let leaderboards: &mut LinkedObjectTable = + dynamic_field::borrow_mut(&mut registry.active_leaderboard_registry, key); + assert!(end_ts_ms > leaderboards[id].start_ts_ms, EInvalidTimePeriod); + *&mut leaderboards[id].end_ts_ms = end_ts_ms; + emit(ExtendLeaderboardEvent { + key, + id, + log: vector[end_ts_ms], + bcs_padding: vector[], + }); + } + + /// Event emitted when a leaderboard is deactivated. + public struct DeactivateLeaderboardEvent has copy, drop { + key: String, + id: address, + log: vector, + bcs_padding: vector>, + } + /// Deactivates a leaderboard, moving it from the active to the inactive registry. + /// This is an authorized function. + public fun deactivate_leaderboard( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + id: address, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + if (!dynamic_field::exists(®istry.inactive_leaderboard_registry, key)) { + dynamic_field::add( + &mut registry.inactive_leaderboard_registry, + key, + linked_object_table::new(ctx), + ); + }; + let leaderboards: &mut LinkedObjectTable = + dynamic_field::borrow_mut(&mut registry.active_leaderboard_registry, key); + let leaderboard: Leaderboard = leaderboards.remove(id); + emit(DeactivateLeaderboardEvent { + key, + id: object::id_address(&leaderboard), + log: vector[], + bcs_padding: vector[], + }); + let leaderboards: &mut LinkedObjectTable = + dynamic_field::borrow_mut(&mut registry.inactive_leaderboard_registry, key); + leaderboards.push_back( + object::id_address(&leaderboard), + leaderboard, + ); + } + + /// Event emitted when a leaderboard is removed. + public struct RemoveLeaderboardEvent has copy, drop { + key: String, + id: address, + log: vector, + bcs_padding: vector>, + } + /// Removes a leaderboard from the inactive registry. + /// This is an authorized function. + public fun remove_leaderboard( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + id: address, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + let leaderboards: &mut LinkedObjectTable = + dynamic_field::borrow_mut(&mut registry.inactive_leaderboard_registry, key); + let Leaderboard { + id, + start_ts_ms: _, + end_ts_ms: _, + score, + mut ranking, + } = leaderboards.remove(id); + emit(RemoveLeaderboardEvent { + key, + id: object::uid_to_address(&id), + log: vector[], + bcs_padding: vector[], + }); + id.delete(); + score.drop(); + while (ranking.size() > 0) { + ranking.remove_min_leaf().drop(); + }; + ranking.destroy_empty(); + } + + /// Event emitted when a user's score is updated. + public struct ScoreEvent has copy, drop { + key: String, + id: address, + user: address, + log: vector, + bcs_padding: vector>, + } + /// A wrapper function that delegates the call to the `score` function. + /// It requires a `ManagerCap` for authorization. + public fun delegate_score( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + user: address, + score: u64, + clock: &Clock, + ctx: &mut TxContext, + ): vector { + let manager_cap = version.issue_manager_cap(ctx); + let log = score( + &manager_cap, + version, + registry, + key, + user, + score, + clock, + ctx, + ); + version.burn_manager_cap(manager_cap, ctx); + + log + } + /// Updates a user's score on all active leaderboards. + /// This function is authorized by requiring a `ManagerCap`. + public fun score( + _manager_cap: &ManagerCap, + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + user: address, + score: u64, + clock: &Clock, + ctx: &mut TxContext, + ): vector { + version.version_check(); + + if (!dynamic_field::exists(®istry.active_leaderboard_registry, key) || score == 0) { + return vector[0] + }; + let leaderboards: &mut LinkedObjectTable = dynamic_field::borrow_mut(&mut registry.active_leaderboard_registry, key); + let ts_ms = clock.timestamp_ms(); + let mut first = *leaderboards.front(); + while (option::is_some(&first)) { + let id = option::destroy_some(first); + let leaderboard = leaderboards.borrow_mut(id); + if (ts_ms >= leaderboard.start_ts_ms && ts_ms < leaderboard.end_ts_ms) { + if (!leaderboard.score.contains(user)) { + leaderboard.score.add(user, 0); + }; + let user_score = leaderboard.score.borrow_mut(user); + let (has_leaf, index) = leaderboard.ranking.find_leaf(*user_score); + if (has_leaf) { + if (critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index).length() == 1) { + critbit::remove_leaf_by_index(&mut leaderboard.ranking, index).drop(); + } else { + linked_set::remove( + critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index), + user, + ); + }; + }; + *user_score = *user_score + score; + let (has_leaf, mut index) = leaderboard.ranking.find_leaf(*user_score); + if (!has_leaf) { + index = critbit::insert_leaf( + &mut leaderboard.ranking, + *user_score, + linked_set::new(ctx), + ); + }; + linked_set::push_back( + critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index), + user, + ); + emit(ScoreEvent { + key, + id: object::id_address(leaderboard), + user, + log: vector[score], + bcs_padding: vector[], + }); + return vector[score] + }; + first = *leaderboards.next(id); + }; + + vector[0] + } + + /// Event emitted when a user's score is deducted. + public struct DeductEvent has copy, drop { + key: String, + id: address, + user: address, + log: vector, + bcs_padding: vector>, + } + /// A wrapper function that delegates the call to the `deduct` function. + /// It requires a `ManagerCap` for authorization. + public fun delegate_deduct( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + user: address, + score: u64, + clock: &Clock, + ctx: &mut TxContext, + ): vector { + let manager_cap = version.issue_manager_cap(ctx); + let log = deduct( + &manager_cap, + version, + registry, + key, + user, + score, + clock, + ctx, + ); + version.burn_manager_cap(manager_cap, ctx); + + log + } + /// Deducts a user's score on all active leaderboards. + /// This function is authorized by requiring a `ManagerCap`. + public fun deduct( + _manager_cap: &ManagerCap, + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + user: address, + score: u64, + clock: &Clock, + ctx: &mut TxContext, + ): vector { + version.version_check(); + + if (!dynamic_field::exists(®istry.active_leaderboard_registry, key) || score == 0) { + return vector[0] + }; + let leaderboards: &mut LinkedObjectTable = dynamic_field::borrow_mut(&mut registry.active_leaderboard_registry, key); + let ts_ms = clock.timestamp_ms(); + let mut first = *leaderboards.front(); + while (option::is_some(&first)) { + let id = option::destroy_some(first); + let leaderboard = leaderboards.borrow_mut(id); + if (ts_ms >= leaderboard.start_ts_ms && ts_ms < leaderboard.end_ts_ms) { + if (!leaderboard.score.contains(user)) { + return vector[0] + }; + let user_score = leaderboard.score.borrow_mut(user); + let (has_leaf, index) = leaderboard.ranking.find_leaf(*user_score); + if (has_leaf) { + if (critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index).length() == 1) { + critbit::remove_leaf_by_index(&mut leaderboard.ranking, index).drop(); + } else { + linked_set::remove( + critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index), + user, + ); + }; + }; + *user_score = *user_score - score; + if (user_score == 0) { + return vector[score] + }; + let (has_leaf, mut index) = leaderboard.ranking.find_leaf(*user_score); + if (!has_leaf) { + index = critbit::insert_leaf( + &mut leaderboard.ranking, + *user_score, + linked_set::new(ctx), + ); + }; + linked_set::push_back( + critbit::borrow_mut_leaf_by_index(&mut leaderboard.ranking, index), + user, + ); + emit(DeductEvent { + key, + id: object::id_address(leaderboard), + user, + log: vector[score], + bcs_padding: vector[], + }); + return vector[score] + }; + first = *leaderboards.next(id); + }; + + vector[0] + } + + /// Retrieves the rankings from a leaderboard. + /// It returns the user's score and the top `ranks` users. + public(package) fun get_rankings( + version: &Version, + registry: &TypusLeaderboardRegistry, + key: String, + id: address, + mut ranks: u64, + user: address, + active: bool, + ): vector> { + version.version_check(); + + let uid = if (active) { + ®istry.active_leaderboard_registry + } else { + ®istry.inactive_leaderboard_registry + }; + let leaderboards: &LinkedObjectTable = dynamic_field::borrow(uid, key); + let leaderboard: &Leaderboard = leaderboards.borrow(id); + if (leaderboard.ranking.is_empty()) { + return vector[bcs::to_bytes(&0u64)] + }; + let mut result = if (leaderboard.score.contains(user)) { + vector[bcs::to_bytes(leaderboard.score.borrow(user))] + } else { + vector[bcs::to_bytes(&0u64)] + }; + let (mut max_score, mut max_score_index) = leaderboard.ranking.max_leaf(); + let mut max_leaf_bcs = bcs::to_bytes(&max_score); + let mut max_leaf_users = vector[]; + let mut max_rankings = leaderboard.ranking.borrow_leaf_by_index(max_score_index); + let mut front = *max_rankings.front().borrow(); + while (ranks > 0) { + max_leaf_users.push_back(front); + ranks = ranks - 1; + let next = max_rankings.next(front); + if (next.is_some()) { + front = *next.borrow(); + } else { + max_leaf_bcs.append(bcs::to_bytes(&max_leaf_users)); + result.push_back(max_leaf_bcs); + let (next_max_score, next_max_score_index) = leaderboard.ranking.previous_leaf(max_score); + if (next_max_score == 0) { + break + }; + max_score = next_max_score; + max_score_index = next_max_score_index; + max_leaf_bcs = bcs::to_bytes(&max_score); + max_leaf_users = vector[]; + max_rankings = leaderboard.ranking.borrow_leaf_by_index(max_score_index); + front = *max_rankings.front().borrow(); + }; + }; + + result + } + + /// Trims empty leaves from a leaderboard's ranking tree. + /// This is an authorized function. + entry fun trim_leaderboard( + version: &Version, + registry: &mut TypusLeaderboardRegistry, + key: String, + id: address, + active: bool, + from: u64, + to: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + let uid = if (active) { + &mut registry.active_leaderboard_registry + } else { + &mut registry.inactive_leaderboard_registry + }; + let leaderboards: &mut LinkedObjectTable = dynamic_field::borrow_mut(uid, key); + let leaderboard: &mut Leaderboard = leaderboards.borrow_mut(id); + if (leaderboard.ranking.is_empty()) { + return + }; + let mut index = from; + while (index <= to) { + if (leaderboard.ranking.has_index(index)) { + if (leaderboard.ranking.borrow_leaf_by_index(index).is_empty()) { + leaderboard.ranking.remove_leaf_by_index(index).drop(); + } + }; + index = index + 1; + } + } + + #[test_only] + public fun test_init(ctx: &mut TxContext) { + init(ctx); + } +} \ No newline at end of file diff --git a/linked_object_table.move b/linked_object_table.move new file mode 100644 index 0000000..0019070 --- /dev/null +++ b/linked_object_table.move @@ -0,0 +1,221 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a `LinkedObjectTable`, which is similar to `sui::linked_table` but stores +/// its values as dynamic object fields. This allows the values to be objects themselves, which can be +/// useful for storing complex data structures. The table maintains a doubly-linked list of its entries, +/// allowing for efficient iteration in both forward and reverse order. +module typus::linked_object_table { + use sui::dynamic_field as field; + use sui::dynamic_object_field as ofield; + + // ======== Error Code ======== + + /// Error when trying to destroy a non-empty table. + const ETableNotEmpty: u64 = 0; + /// Error when trying to pop from an empty table. + const ETableIsEmpty: u64 = 1; + + // ======== Structs ======== + + /// A doubly-linked list of key-value pairs where values are stored as dynamic object fields. + public struct LinkedObjectTable has key, store { + /// The UID for storing the nodes of the linked list. + id: UID, + /// The UID for storing the values as dynamic object fields. + vid: UID, + /// The number of key-value pairs in the table. + size: u64, + /// The key of the first entry in the table. + head: Option, + /// The key of the last entry in the table. + tail: Option, + } + + /// A node in the linked list, containing pointers to the previous and next keys. + public struct Node has store { + /// The key of the previous entry. + prev: Option, + /// The key of the next entry. + next: Option, + } + + // ======== Public Functions ======== + + /// Creates a new, empty `LinkedObjectTable`. + public fun new(ctx: &mut TxContext): LinkedObjectTable { + LinkedObjectTable { + id: object::new(ctx), + vid: object::new(ctx), + size: 0, + head: option::none(), + tail: option::none(), + } + } + + /// Returns the key of the first element in the table, or `None` if the table is empty. + public fun front(table: &LinkedObjectTable): &Option { + &table.head + } + + /// Returns the key of the last element in the table, or `None` if the table is empty. + public fun back(table: &LinkedObjectTable): &Option { + &table.tail + } + + /// Inserts a key-value pair at the front of the table. + /// Aborts if the key already exists. + public fun push_front( + table: &mut LinkedObjectTable, + k: K, + v: V, + ) { + let old_head = option::swap_or_fill(&mut table.head, k); + if (option::is_none(&table.tail)) option::fill(&mut table.tail, k); + let prev = option::none(); + let next = if (option::is_some(&old_head)) { + let old_head_k = option::destroy_some(old_head); + field::borrow_mut>(&mut table.id, old_head_k).prev = option::some(k); + option::some(old_head_k) + } else { + option::none() + }; + field::add(&mut table.id, k, Node { prev, next }); + ofield::add(&mut table.vid, k, v); + table.size = table.size + 1; + } + + /// Inserts a key-value pair at the back of the table. + /// Aborts if the key already exists. + public fun push_back( + table: &mut LinkedObjectTable, + k: K, + v: V, + ) { + if (option::is_none(&table.head)) option::fill(&mut table.head, k); + let old_tail = option::swap_or_fill(&mut table.tail, k); + let prev = if (option::is_some(&old_tail)) { + let old_tail_k = option::destroy_some(old_tail); + field::borrow_mut>(&mut table.id, old_tail_k).next = option::some(k); + option::some(old_tail_k) + } else { + option::none() + }; + let next = option::none(); + field::add(&mut table.id, k, Node { prev, next }); + ofield::add(&mut table.vid, k, v); + table.size = table.size + 1; + } + + /// Borrows an immutable reference to the value associated with the given key. + /// Aborts if the key does not exist. + #[syntax(index)] + public fun borrow(table: &LinkedObjectTable, k: K): &V { + ofield::borrow(&table.vid, k) + } + + /// Borrows a mutable reference to the value associated with the given key. + /// Aborts if the key does not exist. + #[syntax(index)] + public fun borrow_mut( + table: &mut LinkedObjectTable, + k: K, + ): &mut V { + ofield::borrow_mut(&mut table.vid, k) + } + + /// Returns the key of the previous entry for the specified key. + /// Returns `None` if there is no previous entry. + /// Aborts if the key does not exist. + public fun prev(table: &LinkedObjectTable, k: K): &Option { + &field::borrow>(&table.id, k).prev + } + + /// Returns the key of the next entry for the specified key. + /// Returns `None` if there is no next entry. + /// Aborts if the key does not exist. + public fun next(table: &LinkedObjectTable, k: K): &Option { + &field::borrow>(&table.id, k).next + } + + /// Removes the key-value pair with the given key from the table and returns the value. + /// Aborts if the key does not exist. + public fun remove(table: &mut LinkedObjectTable, k: K): V { + let Node { prev, next } = field::remove(&mut table.id, k); + let v = ofield::remove(&mut table.vid, k); + table.size = table.size - 1; + if (option::is_some(&prev)) { + field::borrow_mut>(&mut table.id, *option::borrow(&prev)).next = next + }; + if (option::is_some(&next)) { + field::borrow_mut>(&mut table.id, *option::borrow(&next)).prev = prev + }; + if (option::borrow(&table.head) == &k) table.head = next; + if (option::borrow(&table.tail) == &k) table.tail = prev; + v + } + + /// Removes the first entry from the table and returns its key and value. + /// Aborts if the table is empty. + public fun pop_front(table: &mut LinkedObjectTable): (K, V) { + assert!(option::is_some(&table.head), ETableIsEmpty); + let head = *option::borrow(&table.head); + (head, remove(table, head)) + } + + /// Removes the last entry from the table and returns its key and value. + /// Aborts if the table is empty. + public fun pop_back(table: &mut LinkedObjectTable): (K, V) { + assert!(option::is_some(&table.tail), ETableIsEmpty); + let tail = *option::borrow(&table.tail); + (tail, remove(table, tail)) + } + + /// Returns `true` if the table contains an entry with the given key. + public fun contains(table: &LinkedObjectTable, k: K): bool { + field::exists_with_type>(&table.id, k) + } + + /// Returns the number of key-value pairs in the table. + public fun length(table: &LinkedObjectTable): u64 { + table.size + } + + /// Returns `true` if the table is empty. + public fun is_empty(table: &LinkedObjectTable): bool { + table.size == 0 + } + + /// Destroys an empty table. + /// Aborts if the table is not empty. + public fun destroy_empty(table: LinkedObjectTable) { + let LinkedObjectTable { id, vid, size, head: _, tail: _ } = table; + assert!(size == 0, ETableNotEmpty); + object::delete(id); + object::delete(vid); + } + + /// A macro for iterating over the elements of a `LinkedObjectTable` with immutable references. + public macro fun do_ref<$K, $V>($lot: &LinkedObjectTable<$K, $V>, $f: |$K, &$V|) { + let lot = $lot; + let mut front = lot.front(); + while (front.is_some()) { + let key = *front.borrow(); + let value = lot.borrow(key); + $f(key, value); + front = lot.next(key); + }; + } + + /// A macro for iterating over the elements of a `LinkedObjectTable` with mutable references. + public macro fun do_mut<$K, $V>($lot: &mut LinkedObjectTable<$K, $V>, $f: |$K, &mut $V|) { + let lot = $lot; + let mut front = lot.front(); + while (front.is_some()) { + let key = *front.borrow(); + let value = lot.borrow_mut(key); + $f(key, value); + front = lot.next(key); + }; + } +} diff --git a/linked_set.move b/linked_set.move new file mode 100644 index 0000000..f4d1d66 --- /dev/null +++ b/linked_set.move @@ -0,0 +1,177 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements a `LinkedSet`, a data structure that stores a set of keys in a doubly-linked list. +/// It is similar to `sui::linked_set` but only stores keys, not values. This is useful when you need to +/// maintain an ordered set of unique elements. +module typus::linked_set { + use sui::dynamic_field as field; + + + // ======== Error Code ======== + + /// Error when trying to destroy a non-empty set. + const ESetNotEmpty: u64 = 0; + /// Error when trying to pop from an empty set. + const ESetIsEmpty: u64 = 1; + + // ======== Structs ======== + + /// A doubly-linked list of unique keys. + public struct LinkedSet has key, store { + /// The UID for storing the nodes of the linked list. + id: UID, + /// The number of keys in the set. + size: u64, + /// The first key in the set. + head: Option, + /// The last key in the set. + tail: Option, + } + + /// A node in the linked list, containing pointers to the previous and next keys. + public struct Node has store { + /// The previous key in the list. + prev: Option, + /// The next key in the list. + next: Option, + } + + // ======== Public Functions ======== + + /// Creates a new, empty `LinkedSet`. + public fun new(ctx: &mut TxContext): LinkedSet { + LinkedSet { + id: object::new(ctx), + size: 0, + head: option::none(), + tail: option::none(), + } + } + + /// Returns the first key in the set, or `None` if the set is empty. + public fun front(set: &LinkedSet): &Option { + &set.head + } + + /// Returns the last key in the set, or `None` if the set is empty. + public fun back(set: &LinkedSet): &Option { + &set.tail + } + + /// Inserts a key at the front of the set. + /// Aborts if the key already exists. + public fun push_front( + set: &mut LinkedSet, + k: K, + ) { + let old_head = option::swap_or_fill(&mut set.head, k); + if (option::is_none(&set.tail)) option::fill(&mut set.tail, k); + let prev = option::none(); + let next = if (option::is_some(&old_head)) { + let old_head_k = option::destroy_some(old_head); + field::borrow_mut>(&mut set.id, old_head_k).prev = option::some(k); + option::some(old_head_k) + } else { + option::none() + }; + field::add(&mut set.id, k, Node { prev, next }); + set.size = set.size + 1; + } + + /// Inserts a key at the back of the set. + /// Aborts if the key already exists. + public fun push_back( + set: &mut LinkedSet, + k: K, + ) { + if (option::is_none(&set.head)) option::fill(&mut set.head, k); + let old_tail = option::swap_or_fill(&mut set.tail, k); + let prev = if (option::is_some(&old_tail)) { + let old_tail_k = option::destroy_some(old_tail); + field::borrow_mut>(&mut set.id, old_tail_k).next = option::some(k); + option::some(old_tail_k) + } else { + option::none() + }; + let next = option::none(); + field::add(&mut set.id, k, Node { prev, next }); + set.size = set.size + 1; + } + + /// Returns the previous key for the specified key. + /// Returns `None` if there is no previous key. + /// Aborts if the key does not exist. + public fun prev(set: &LinkedSet, k: K): &Option { + &field::borrow>(&set.id, k).prev + } + + /// Returns the next key for the specified key. + /// Returns `None` if there is no next key. + /// Aborts if the key does not exist. + public fun next(set: &LinkedSet, k: K): &Option { + &field::borrow>(&set.id, k).next + } + + /// Removes the key from the set. + /// Aborts if the key does not exist. + public fun remove(set: &mut LinkedSet, k: K) { + let Node { prev, next } = field::remove(&mut set.id, k); + set.size = set.size - 1; + if (option::is_some(&prev)) { + field::borrow_mut>(&mut set.id, *option::borrow(&prev)).next = next + }; + if (option::is_some(&next)) { + field::borrow_mut>(&mut set.id, *option::borrow(&next)).prev = prev + }; + if (option::borrow(&set.head) == &k) set.head = next; + if (option::borrow(&set.tail) == &k) set.tail = prev; + } + + /// Removes the first key from the set and returns it. + /// Aborts if the set is empty. + public fun pop_front(set: &mut LinkedSet): K { + assert!(option::is_some(&set.head), ESetIsEmpty); + let head = *option::borrow(&set.head); + remove(set, head); + head + } + + /// Removes the last key from the set and returns it. + /// Aborts if the set is empty. + public fun pop_back(set: &mut LinkedSet): K { + assert!(option::is_some(&set.tail), ESetIsEmpty); + let tail = *option::borrow(&set.tail); + remove(set, tail); + tail + } + + /// Returns `true` if the set contains the given key. + public fun contains(set: &LinkedSet, k: K): bool { + field::exists_with_type>(&set.id, k) + } + + /// Returns the number of keys in the set. + public fun length(set: &LinkedSet): u64 { + set.size + } + + /// Returns `true` if the set is empty. + public fun is_empty(set: &LinkedSet): bool { + set.size == 0 + } + + /// Destroys an empty set. + /// Aborts if the set is not empty. + public fun destroy_empty(set: LinkedSet) { + let LinkedSet { id, size, head: _, tail: _ } = set; + assert!(size == 0, ESetNotEmpty); + object::delete(id); + } + + /// Destroys a set, regardless of whether it is empty or not. + public fun drop(set: LinkedSet) { + let LinkedSet { id, size: _, head: _, tail: _ } = set; + object::delete(id) + } +} diff --git a/tails_staking.move b/tails_staking.move new file mode 100644 index 0000000..f1e9aab --- /dev/null +++ b/tails_staking.move @@ -0,0 +1,1452 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module implements the staking functionality for Typus Tails NFTs. +/// It allows users to stake their Tails NFTs to earn rewards, participate in profit sharing, +/// and level up their NFTs by gaining experience points (EXP). +module typus::tails_staking { + use std::bcs; + use std::string; + use std::type_name::{Self, TypeName}; + + use sui::bag::{Self, Bag}; + use sui::balance::{Self, Balance}; + use sui::clock::Clock; + use sui::coin::{Self, Coin}; + use sui::dynamic_field; + use sui::event::emit; + use sui::kiosk::{Self, Kiosk, KioskOwnerCap}; + use sui::object_table::{Self, ObjectTable}; + use sui::sui::SUI; + use sui::table::{Self, Table}; + use sui::transfer_policy::{Self, TransferPolicy}; + + use typus::big_vector::{Self, BigVector}; + use typus::ecosystem::{ManagerCap, Version}; + use typus::user::{Self, TypusUserRegistry}; + use typus::utility; + + use typus_nft::typus_nft::{Self, Tails, ManagerCap as TailsManagerCap}; + + /// Constant for the number of milliseconds in a day. + const CMillisecondsADay: u64 = 24 * 60 * 60 * 1000; + + // ======== TailsStakingRegistry config Index ======== + + /// Index for the maximum number of Tails a user can stake. + const IMaxStakeAmount: u64 = 0; + /// Index for the fee required to stake a Tails NFT (in SUI). + const IStakeTailsFee: u64 = 1; + /// Index for the fee required to transfer a Tails NFT (in SUI). + const ITransferTailsFee: u64 = 2; + /// Index for the amount of EXP gained from a daily sign-up. + const IDailySignUpExp: u64 = 3; + /// Index for the fee required for a daily sign-up (in SUI). + const IDailySignUpFee: u64 = 4; + /// Index for the fee required to convert EXP back to the user's balance (in SUI). + const IExpDownFee: u64 = 5; + + // ======== StakingInfo u64_padding Index ======== + + /// Index for the timestamp of the last daily sign-up. + const ILastSignUpTsMs: u64 = 0; + + // ======== Tails Metadata Key ======== + + /// Key for the vector of Tails NFT IDs. + const KTailsIds: vector = b"tails_ids"; // vector
+ /// Key for the vector of Tails NFT levels. + const KTailsLevels: vector = b"tails_levels"; // vector + /// Key for the table of Tails IPFS URLs. + const KTailsIpfsUrls: vector = b"tails_ipfs_urls"; // Table)> + /// Key for the table of Tails WEBP images. + const KTailsWebpImages: vector = b"tails_webp_images"; // Table> + + // ======== Error Code ======== + + /// Error when a user has already signed up for the day. + #[error] + const EAlreadySignedUp: vector = b"already_signed_up"; + /// Error for insufficient balance. + #[error] + const EInsufficientExp: vector = b"insufficient_exp"; + /// Error for an invalid fee amount. + #[error] + const EInvalidFee: vector = b"invalid_fee"; + /// Error for invalid input. + #[error] + const EInvalidInput: vector = b"invalid_input"; + /// Error for an invalid token type. + #[error] + const EInvalidToken: vector = b"invalid_token"; + /// Error when the maximum stake amount is reached. + #[error] + const EMaxStakeAmountReached: vector = b"max_stake_amount_reached"; + /// Error when staking information for a user is not found. + #[error] + const EStakingInfoNotFound: vector = b"staking_info_not_found"; + + // ======== Tails Staking ======== + + /// The main registry for the Tails NFT staking system. + public struct TailsStakingRegistry has key { + id: UID, + /// A vector of configuration values for the staking system. + config: vector, + /// The manager capability for the Tails NFT contract. + tails_manager_cap: TailsManagerCap, + /// A table storing the staked Tails NFTs. + tails: ObjectTable, + /// A bag for storing various metadata related to Tails NFTs. + tails_metadata: Bag, + /// A big vector of `StakingInfo` structs for all users. + staking_infos: BigVector, + /// A vector of token types that are used for profit sharing. + profit_assets: vector, + /// The transfer policy for Tails NFTs. + transfer_policy: TransferPolicy, + } + + /// Stores staking information for a single user. + public struct StakingInfo has store, drop { + /// The address of the user. + user: address, + /// A vector of the numbers of the Tails NFTs staked by the user. + tails: vector, + /// A vector of the profits earned by the user from profit sharing. + profits: vector, + /// Padding for future use. + u64_padding: vector, + } + + /// Initializes the `TailsStakingRegistry`. + /// This is an authorized function. + entry fun init_tails_staking_registry( + version: &Version, + tails_manager_cap: TailsManagerCap, + transfer_policy: TransferPolicy, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + let mut tails_metadata = bag::new(ctx); + let mut tails_ipfs_urls = table::new(ctx); + let mut i = 1u64; + while (i <= 7) { + table::add(&mut tails_ipfs_urls, i, big_vector::new>(1111, ctx)); + i = i + 1; + }; + bag::add(&mut tails_metadata, KTailsIpfsUrls, tails_ipfs_urls); + bag::add(&mut tails_metadata, KTailsIds, vector
[]); + bag::add(&mut tails_metadata, KTailsLevels, vector[]); + bag::add(&mut tails_metadata, KTailsWebpImages, table::new>(ctx)); + transfer::share_object(TailsStakingRegistry { + id: object::new(ctx), + config: vector[ + 5, // IMaxStakeAmount, no greater than 10 + 0_050000000, // IStakeTailsFee, SUI + 0_010000000, // ITransferTailsFee, SUI + 10, // IDailySignUpExp + 0_050000000, // IDailySignUpFee, SUI + 10_000000000, // IExpDownFee, SUI + ], + tails_manager_cap, + tails: object_table::new(ctx), + tails_metadata, + staking_infos: big_vector::new(1000, ctx), + profit_assets: vector[], + transfer_policy, + }); + } + + /// Uploads a vector of placeholder IDs for Tails NFTs. + /// This is an authorized function used for initialization. + entry fun upload_ids( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + // mut count: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + let mut count = 6666u64; + let tails_ids: &mut vector
= &mut tails_staking_registry.tails_metadata[KTailsIds]; + while (count > 0) { + tails_ids.push_back(@0x0); + count = count - 1; + } + } + + // entry fun remove_ids( + // version: &Version, + // tails_staking_registry: &mut TailsStakingRegistry, + // mut count: u64, + // ctx: &TxContext, + // ) { + // version.verify(ctx); + + // let tails_ids: &mut vector
= &mut tails_staking_registry.tails_metadata[KTailsIds]; + // while (count > 0) { + // tails_ids.pop_back(); + // count = count - 1; + // } + // } + + /// Uploads a vector of placeholder levels for Tails NFTs. + /// This is an authorized function used for initialization. + entry fun upload_levels( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + // mut count: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + let mut count = 6666u64; + let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + while (count > 0) { + tails_levels.push_back(0); + count = count - 1; + } + } + + // entry fun remove_levels( + // version: &Version, + // tails_staking_registry: &mut TailsStakingRegistry, + // mut count: u64, + // ctx: &TxContext, + // ) { + // version.verify(ctx); + + // let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + // while (count > 0) { + // tails_levels.pop_back(); + // count = count - 1; + // } + // } + + /// Uploads IPFS URLs for a specific level of Tails NFTs. + /// This is an authorized function. + entry fun upload_ipfs_urls( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + level: u64, + mut urls: vector>, // reverse + ctx: &TxContext, + ) { + version.verify(ctx); + + let tails_ipfs_urls: &mut Table = &mut tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + let v = &mut tails_ipfs_urls[level]; + while (!urls.is_empty()) { + v.push_back(urls.pop_back()); + } + } + + /// Removes all IPFS URLs for a specific level. + /// This is an authorized function. + entry fun remove_ipfs_urls( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + level: u64, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + let tails_ipfs_urls: &mut Table = &mut tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + tails_ipfs_urls.remove(level).drop>(); + tails_ipfs_urls.add(level, big_vector::new>(1111, ctx)); + } + + /// Uploads the WEBP image bytes for a specific Tails NFT. + /// This is an authorized function. + entry fun upload_webp_bytes( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + number: u64, + level: u64, + mut bytes: vector, // reverse when extend + ctx: &TxContext, + ) { + version.verify(ctx); + + let tails_webp_images: &mut Table> = &mut tails_staking_registry.tails_metadata[KTailsWebpImages]; + if (!tails_webp_images.contains(level * 10000 + number)) { + tails_webp_images.add(level * 10000 + number, bytes); + } else { + let v: &mut vector = &mut tails_webp_images[level * 10000 + number]; + while (!bytes.is_empty()) { + v.push_back(bytes.pop_back()); + } + }; + } + + /// Removes the WEBP image bytes for a specific Tails NFT. + /// This is an authorized function. + entry fun remove_webp_bytes( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + number: u64, + level: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + let tails_webp_images: &mut Table> = &mut tails_staking_registry.tails_metadata[KTailsWebpImages]; + tails_webp_images.remove(level * 10000 + number); + } + + /// Event emitted when the staking registry config is updated. + public struct UpdateTailsStakingRegistryConfigEvent has copy, drop { + index: u64, + log: vector, + bcs_padding: vector>, + } + /// Updates a configuration value in the `TailsStakingRegistry`. + /// This is an authorized function. + entry fun update_tails_staking_registry_config( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + index: u64, + value: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + while (tails_staking_registry.config.length() < index + 1) { + tails_staking_registry.config.push_back(0); + }; + emit(UpdateTailsStakingRegistryConfigEvent { + index, + log: vector[tails_staking_registry.config[index], value], + bcs_padding: vector[], + }); + *&mut tails_staking_registry.config[index] = value; + } + + /// Event emitted when profit sharing is set. + public struct SetProfitSharingEvent has copy, drop { + token: TypeName, + level_profits: vector, + level_counts: vector, + log: vector, + bcs_padding: vector>, + } + /// Sets the profit sharing for a specific token. + /// This is an authorized function. + entry fun set_profit_sharing( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + level_profits: vector, + profit: Coin, + amount: u64, + ts_ms: u64, + ctx: &TxContext, + ) { + version.verify(ctx); + + let mut level_counts = vector[0, 0, 0, 0, 0, 0, 0]; + let mut total_profit = 0; + let profit_asset = type_name::with_defining_ids(); + let (profit_asset_exists, profit_asset_index) = tails_staking_registry.profit_assets.index_of(&profit_asset); + let tails_levels: &vector = &tails_staking_registry.tails_metadata[KTailsLevels]; + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &mut StakingInfo = &mut slice[i % slice_size]; + let mut profit = 0; + let mut j = 0; + let length = staking_info.tails.length(); + while (j < length) { + let tails_number = staking_info.tails[j]; + let tails_level = tails_levels[tails_number - 1]; + profit = profit + level_profits[tails_level - 1]; + *&mut level_counts[tails_level - 1] = level_counts[tails_level - 1] + 1; + j = j + 1; + }; + // update user profit + staking_info.profits.push_back(profit); + if (profit_asset_exists) { + staking_info.profits.swap_remove(profit_asset_index); + }; + total_profit = total_profit + profit; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + if (!profit_asset_exists) { + tails_staking_registry.profit_assets.push_back(profit_asset); + if (!dynamic_field::exists(&tails_staking_registry.id, profit_asset)) { + dynamic_field::add(&mut tails_staking_registry.id, profit_asset, balance::zero()); + } + }; + let shared_profit = dynamic_field::borrow_mut>(&mut tails_staking_registry.id, profit_asset); + let spent_profit = profit.value(); + balance::join(shared_profit, coin::into_balance(profit)); + assert!(shared_profit.value() == total_profit, EInvalidInput); + + emit(SetProfitSharingEvent { + token: profit_asset, + level_profits, + level_counts, + log: vector[total_profit, spent_profit, amount, ts_ms], + bcs_padding: vector[bcs::to_bytes(&type_name::with_defining_ids())], + }); + } + + /// Event emitted when profit sharing is removed. + public struct RemoveProfitSharingEvent has copy, drop { + token: TypeName, + log: vector, + bcs_padding: vector>, + } + /// Removes a profit sharing token from the registry. + /// This is an authorized function. + entry fun remove_profit_sharing( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + recipient: address, + ctx: &mut TxContext, + ) { + version.verify(ctx); + + let profit_asset = type_name::with_defining_ids(); + let (profit_asset_exists, profit_asset_index) = tails_staking_registry.profit_assets.index_of(&profit_asset); + if (!profit_asset_exists) { + abort EInvalidToken + }; + tails_staking_registry.profit_assets.swap_remove(profit_asset_index); + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &mut StakingInfo = &mut slice[i % slice_size]; + staking_info.profits.swap_remove(profit_asset_index); + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + let shared_profit: Balance = dynamic_field::remove(&mut tails_staking_registry.id, profit_asset); + let balance = shared_profit.value(); + transfer::public_transfer(coin::from_balance(shared_profit, ctx), recipient); + + emit(RemoveProfitSharingEvent { + token: profit_asset, + log: vector[balance], + bcs_padding: vector[], + }); + } + + #[deprecated, allow(unused)] + public fun import_tails( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + mut tailses: vector, + mut users: vector
, + ctx: &TxContext, + ) { abort 0 } + + /// Event emitted when a user claims their profit sharing. + public struct ClaimProfitSharingEvent has copy, drop { + tails: vector, + profit_asset: TypeName, + log: vector, + bcs_padding: vector>, + } + /// Allows a user to claim their profit sharing for a specific token. + /// Safe with ctx.sender as verification + public fun claim_profit_sharing( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + ctx: &mut TxContext, + ): Balance { + version.version_check(); + + let profit_asset = type_name::with_defining_ids(); + let (profit_asset_exists, profit_asset_index) = tails_staking_registry.profit_assets.index_of(&profit_asset); + if (!profit_asset_exists) { + abort EInvalidToken + }; + let user = ctx.sender(); + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &mut StakingInfo = &mut slice[i % slice_size]; + if (staking_info.user == user) { + let profit_balance = dynamic_field::borrow_mut(&mut tails_staking_registry.id, profit_asset); + emit(ClaimProfitSharingEvent { + tails: staking_info.tails, + profit_asset, + log: vector[staking_info.profits[profit_asset_index]], + bcs_padding: vector[], + }); + let balance = balance::split(profit_balance, staking_info.profits[profit_asset_index]); + *&mut staking_info.profits[profit_asset_index] = 0; + return balance + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + abort EStakingInfoNotFound + } + + /// Event emitted when a Tails NFT is staked. + public struct StakeTailsEvent has copy, drop { + tails: address, + log: vector, + bcs_padding: vector>, + } + /// Stakes a Tails NFT. + public fun stake_tails( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + coin: Coin, + ctx: &mut TxContext, + ) { + version.version_check(); + + assert!(coin.value() == tails_staking_registry.config[IStakeTailsFee], EInvalidFee); + version.charge_fee(coin.into_balance()); + kiosk::list(kiosk, kiosk_owner_cap, object::id_from_address(tails), 0); + let (tails, request) = kiosk::purchase(kiosk, object::id_from_address(tails), coin::zero(ctx)); + transfer_policy::confirm_request(&tails_staking_registry.transfer_policy, request); + let tails_address = object::id_address(&tails); + let tails_number = typus_nft::tails_number(&tails); + let tails_level = typus_nft::tails_level(&tails); + stake_tails_( + tails_staking_registry, + tails, + ctx.sender(), + ); + + emit(StakeTailsEvent { + tails: tails_address, + log: vector[ + tails_number, + tails_level, + ], + bcs_padding: vector[], + }); + } + + /// Event emitted when a Tails NFT is unstaked. + public struct UnstakeTailsEvent has copy, drop { + tails: address, + log: vector, + bcs_padding: vector>, + } + /// Unstakes a Tails NFT. + /// Safe with ctx.sender as verification + public fun unstake_tails( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + ctx: &TxContext, + ) { + version.version_check(); + + let tails = unstake_tails_(tails_staking_registry, tails, ctx.sender()); + let tails_address = object::id_address(&tails); + let tails_number = typus_nft::tails_number(&tails); + let tails_level = typus_nft::tails_level(&tails); + kiosk::lock(kiosk, kiosk_owner_cap, &tails_staking_registry.transfer_policy, tails); + + emit(UnstakeTailsEvent { + tails: tails_address, + log: vector[ + tails_number, + tails_level, + ], + bcs_padding: vector[], + }); + } + + /// Event emitted when a Tails NFT is transferred. + public struct TransferTailsEvent has copy, drop { + tails: address, + recipient: address, + log: vector, + bcs_padding: vector>, + } + /// Transfers a Tails NFT to another user. + #[lint_allow(share_owned)] + public fun transfer_tails( + version: &mut Version, + tails_staking_registry: &TailsStakingRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + coin: Coin, + recipient: address, + ctx: &mut TxContext, + ) { + version.version_check(); + + assert!(coin.value() == tails_staking_registry.config[ITransferTailsFee], EInvalidFee); + version.charge_fee(coin.into_balance()); + kiosk::list(kiosk, kiosk_owner_cap, object::id_from_address(tails), 0); + let (tails, request) = kiosk::purchase(kiosk, object::id_from_address(tails), coin::zero(ctx)); + transfer_policy::confirm_request(&tails_staking_registry.transfer_policy, request); + let tails_address = object::id_address(&tails); + let tails_number = typus_nft::tails_number(&tails); + let tails_level = typus_nft::tails_level(&tails); + let (mut recipient_kiosk, recipient_kiosk_owner_cap) = kiosk::new(ctx); + kiosk::lock(&mut recipient_kiosk, &recipient_kiosk_owner_cap, &tails_staking_registry.transfer_policy, tails); + transfer::public_share_object(recipient_kiosk); + transfer::public_transfer(recipient_kiosk_owner_cap, recipient); + + emit(TransferTailsEvent { + tails: tails_address, + recipient, + log: vector[ + tails_number, + tails_level, + ], + bcs_padding: vector[], + }); + } + + /// Event emitted when a user performs a daily sign-up. + public struct DailySignUpEvent has copy, drop { + tails: vector, + log: vector, + bcs_padding: vector>, + } + /// Allows a user to perform a daily sign-up to earn EXP for their staked Tails NFTs. + /// Safe with ctx.sender as verification + entry fun daily_sign_up( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + coin: Coin, + clock: &Clock, + ctx: &TxContext, + ) { + version.version_check(); + + assert!(coin.value() == tails_staking_registry.config[IDailySignUpFee], EInvalidFee); + version.charge_fee(coin.into_balance()); + let user = ctx.sender(); + let tails_ids: &vector
= &tails_staking_registry.tails_metadata[KTailsIds]; + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &mut StakingInfo = &mut slice[i % slice_size]; + if (staking_info.user == user) { + let ts_ms = clock.timestamp_ms(); + if (ts_ms / CMillisecondsADay - staking_info.u64_padding[ILastSignUpTsMs] / CMillisecondsADay == 0) { + abort EAlreadySignedUp + }; + *&mut staking_info.u64_padding[ILastSignUpTsMs] = ts_ms; + let mut i = 0; + let length = staking_info.tails.length(); + let exp = tails_staking_registry.config[IDailySignUpExp]; + while (i < length) { + let tails_number = staking_info.tails[i]; + let tails = &mut tails_staking_registry.tails[tails_ids[tails_number - 1]]; + typus_nft::nft_exp_up( + &tails_staking_registry.tails_manager_cap, + tails, + exp, + ); + i = i + 1; + }; + + emit(DailySignUpEvent { + tails: staking_info.tails, + log: vector[ + exp, + tails_staking_registry.config[IDailySignUpFee], + ], + bcs_padding: vector[], + }); + return + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + abort EStakingInfoNotFound + } + + /// Event emitted when a Tails NFT's EXP is increased. + public struct ExpUpEvent has copy, drop { + tails: address, + log: vector, + bcs_padding: vector>, + } + /// Increases the EXP of a staked Tails NFT. + /// Safe with ctx.sender as verification + public fun exp_up( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + typus_user_registry: &mut TypusUserRegistry, + tails: address, + amount: u64, + ctx: &TxContext, + ) { + let user = tx_context::sender(ctx); + assert!(verify_staking(version, tails_staking_registry, user, tails), EStakingInfoNotFound); + + if (!tails_staking_registry.tails.contains(tails)) { + abort EStakingInfoNotFound + }; + let tails = &mut tails_staking_registry.tails[tails]; + typus_nft::nft_exp_up( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + user::remove_tails_exp_amount_( + version, + typus_user_registry, + user, + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + + emit(ExpUpEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Increases the EXP of a non-staked Tails NFT. + public fun exp_up_without_staking( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + typus_user_registry: &mut TypusUserRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + amount: u64, + ctx: &TxContext, + ) { + version.version_check(); + + let tails = kiosk.borrow_mut(kiosk_owner_cap, object::id_from_address(tails)); + typus_nft::nft_exp_up( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + user::remove_tails_exp_amount_( + version, + typus_user_registry, + tx_context::sender(ctx), + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + + emit(ExpUpEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Publicly increases the EXP of a staked Tails NFT. + /// This is an authorized function that requires a `ManagerCap`. + public fun public_exp_up( + _manager_cap: &ManagerCap, + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + tails: address, + amount: u64, + ) { + version.version_check(); + + if (!tails_staking_registry.tails.contains(tails)) { + abort EStakingInfoNotFound + }; + let tails = &mut tails_staking_registry.tails[tails]; + typus_nft::nft_exp_up( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + + emit(ExpUpEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Publicly increases the EXP of a non-staked Tails NFT. + /// This is an authorized function that requires a `ManagerCap`. + public fun public_exp_up_without_staking( + _manager_cap: &ManagerCap, + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + amount: u64, + ) { + version.version_check(); + + let tails = kiosk.borrow_mut(kiosk_owner_cap, object::id_from_address(tails)); + typus_nft::nft_exp_up( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + + emit(ExpUpEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + + /// Event emitted when a Tails NFT's EXP is decreased. + public struct ExpDownEvent has copy, drop { + tails: address, + log: vector, + bcs_padding: vector>, + } + /// Decreases the EXP of a staked Tails NFT, with a fee. + /// Safe with ctx.sender as verification + public fun exp_down_with_fee( + version: &mut Version, + tails_staking_registry: &mut TailsStakingRegistry, + typus_user_registry: &mut TypusUserRegistry, + tails: address, + amount: u64, + coin: Coin, + ctx: &TxContext, + ) { + let user = tx_context::sender(ctx); + assert!(verify_staking(version, tails_staking_registry, user, tails), EStakingInfoNotFound); + + assert!(coin.value() == tails_staking_registry.config[IExpDownFee], EInvalidFee); + version.charge_fee(coin.into_balance()); + if (!tails_staking_registry.tails.contains(tails)) { + abort EStakingInfoNotFound + }; + let tails = &mut tails_staking_registry.tails[tails]; + typus_nft::nft_exp_down( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + user::add_tails_exp_amount_( + version, + typus_user_registry, + tx_context::sender(ctx), + amount, + ); + let opt_level = typus_nft::level_up(&tails_staking_registry.tails_manager_cap, tails); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + let tails_level = typus_nft::tails_level(tails); + if (opt_level.is_some()) { + let tails_ipfs_urls: &Table = &tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_ipfs_urls[tails_level][tails_number - 1], + ); + }; + let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + *&mut tails_levels[tails_number - 1] = tails_level; + + emit(ExpDownEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Decreases the EXP of a non-staked Tails NFT, with a fee. + public fun exp_down_without_staking_with_fee( + version: &mut Version, + tails_staking_registry: &TailsStakingRegistry, + typus_user_registry: &mut TypusUserRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + amount: u64, + coin: Coin, + ctx: &TxContext, + ) { + version.version_check(); + + assert!(coin.value() == tails_staking_registry.config[IExpDownFee], EInvalidFee); + version.charge_fee(coin.into_balance()); + let tails = kiosk.borrow_mut(kiosk_owner_cap, object::id_from_address(tails)); + typus_nft::nft_exp_down( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + user::add_tails_exp_amount_( + version, + typus_user_registry, + tx_context::sender(ctx), + amount, + ); + let opt_level = typus_nft::level_up(&tails_staking_registry.tails_manager_cap, tails); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + let tails_level = typus_nft::tails_level(tails); + if (opt_level.is_some()) { + let tails_ipfs_urls: &Table = &tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_ipfs_urls[tails_level][tails_number - 1], + ); + }; + + emit(ExpDownEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Publicly decreases the EXP of a staked Tails NFT. + /// This is an authorized function that requires a `ManagerCap`. + public fun public_exp_down( + _manager_cap: &ManagerCap, + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + tails: address, + amount: u64, + ) { + version.version_check(); + + if (!tails_staking_registry.tails.contains(tails)) { + abort EStakingInfoNotFound + }; + let tails = &mut tails_staking_registry.tails[tails]; + typus_nft::nft_exp_down( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + let tails_level = typus_nft::tails_level(tails); + let opt_level = typus_nft::level_up(&tails_staking_registry.tails_manager_cap, tails); + if (opt_level.is_some()) { + let tails_ipfs_urls: &Table = &tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_ipfs_urls[tails_level][tails_number - 1], + ); + }; + let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + *&mut tails_levels[tails_number - 1] = tails_level; + + emit(ExpDownEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + /// Publicly decreases the EXP of a non-staked Tails NFT. + /// This is an authorized function that requires a `ManagerCap`. + public fun public_exp_down_without_staking( + _manager_cap: &ManagerCap, + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + kiosk: &mut Kiosk, + kiosk_owner_cap: &KioskOwnerCap, + tails: address, + amount: u64, + ) { + version.version_check(); + + let tails = kiosk.borrow_mut(kiosk_owner_cap, object::id_from_address(tails)); + typus_nft::nft_exp_down( + &tails_staking_registry.tails_manager_cap, + tails, + amount, + ); + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + let tails_level = typus_nft::tails_level(tails); + let opt_level = typus_nft::level_up(&tails_staking_registry.tails_manager_cap, tails); + if (opt_level.is_some()) { + let tails_ipfs_urls: &Table = &tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_ipfs_urls[tails_level][tails_number - 1], + ); + }; + + emit(ExpDownEvent { + tails: tails_address, + log: vector[tails_number, amount], + bcs_padding: vector[], + }); + } + + /// Event emitted when a Tails NFT levels up. + public struct LevelUpEvent has copy, drop { + tails: address, + log: vector, + bcs_padding: vector>, + } + /// Levels up a staked Tails NFT. + /// WARNING: no owner check + entry fun level_up( + version: &Version, + tails_staking_registry: &mut TailsStakingRegistry, + tails: address, + raw: bool, + ) { + version.version_check(); + + if (!tails_staking_registry.tails.contains(tails)) { + abort EStakingInfoNotFound + }; + let tails = &mut tails_staking_registry.tails[tails]; + let opt_level = typus_nft::level_up(&tails_staking_registry.tails_manager_cap, tails); + if (opt_level.is_none()) { + abort EInsufficientExp + }; + let tails_address = object::id_address(tails); + let tails_number = typus_nft::tails_number(tails); + let tails_level = typus_nft::tails_level(tails); + if (raw) { + let tails_webp_images: &Table> = &tails_staking_registry.tails_metadata[KTailsWebpImages]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_webp_images[tails_level * 10000 + tails_number], + ); + } else { + let tails_ipfs_urls: &Table = &tails_staking_registry.tails_metadata[KTailsIpfsUrls]; + typus_nft::update_image_url( + &tails_staking_registry.tails_manager_cap, + tails, + tails_ipfs_urls[tails_level][tails_number - 1], + ); + }; + let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + *&mut tails_levels[tails_number - 1] = tails_level; + + emit(LevelUpEvent { + tails: tails_address, + log: vector[tails_number, tails_level], + bcs_padding: vector[], + }); + } + + /// Internal function to handle the logic of staking a Tails NFT. + fun stake_tails_( + tails_staking_registry: &mut TailsStakingRegistry, + mut tails: Tails, + user: address, + ) { + let tails_ids: &mut vector
= &mut tails_staking_registry.tails_metadata[KTailsIds]; + *&mut tails_ids[typus_nft::tails_number(&tails) - 1] = object::id_address(&tails); + let tails_levels: &mut vector = &mut tails_staking_registry.tails_metadata[KTailsLevels]; + *&mut tails_levels[typus_nft::tails_number(&tails) - 1] = typus_nft::tails_level(&tails); + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + if (slice[i % slice_size].user == user) { + break + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + if (i == length) { + let mut profits = vector[]; + utility::pad_u64_vector(&mut profits, tails_staking_registry.profit_assets.length() - 1); + tails_staking_registry.staking_infos.push_back( + StakingInfo { + user, + tails: vector[], + profits, + u64_padding: vector[0], + } + ); + }; + let staking_info: &mut StakingInfo = &mut tails_staking_registry.staking_infos[i]; + assert!(staking_info.tails.length() < tails_staking_registry.config[IMaxStakeAmount], EMaxStakeAmountReached); + staking_info.tails.push_back(typus_nft::tails_number(&tails)); + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"updating_url"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"updating_url")); + }; + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"attendance_ms"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"attendance_ms")); + }; + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"snapshot_ms"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"snapshot_ms")); + }; + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"usd_in_deposit"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"usd_in_deposit")); + }; + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"dice_profit"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"dice_profit")); + }; + if (typus_nft::contains_u64_padding(&tails_staking_registry.tails_manager_cap, &tails, string::utf8(b"exp_profit"))) { + typus_nft::remove_u64_padding(&tails_staking_registry.tails_manager_cap, &mut tails, string::utf8(b"exp_profit")); + }; + tails_staking_registry.tails.add(object::id_address(&tails), tails); + } + + /// Internal function to handle the logic of unstaking a Tails NFT. + fun unstake_tails_( + tails_staking_registry: &mut TailsStakingRegistry, + tails: address, + user: address, + ): Tails { + let tails_ids: &vector
= &tails_staking_registry.tails_metadata[KTailsIds]; + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &mut StakingInfo = &mut slice[i % slice_size]; + if (staking_info.user == user) { + let mut j = 0; + let length = staking_info.tails.length(); + while (j < length) { + if (tails_ids[staking_info.tails[j] - 1] == tails) { + staking_info.tails.remove(j); + let tails = tails_staking_registry.tails.remove(tails); + if (staking_info.tails.is_empty()) { + tails_staking_registry.staking_infos.swap_remove(i); + }; + return tails + }; + j = j + 1; + }; + break + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice_mut(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + abort EStakingInfoNotFound + } + + /// Retrieves the staking information for a specific user. + public(package) fun get_staking_info( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + user: address, + ): vector { + version.version_check(); + + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + if (slice[i % slice_size].user == user) { + return bcs::to_bytes(&slice[i % slice_size]) + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + vector[] + } + + /// Retrieves all staking information for a specific user. + public(package) fun get_staking_infos( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + user: address, + ): vector> { + version.version_check(); + + let mut result = vector[]; + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + if (slice[i % slice_size].user == user) { + result.push_back(bcs::to_bytes(&slice[i % slice_size])); + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + result + } + + /// Retrieves the counts of staked Tails NFTs for each level. + public(package) fun get_level_counts( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + ): vector { + version.version_check(); + + let mut level_counts = vector[0, 0, 0, 0, 0, 0, 0]; + let tails_levels: &vector = &tails_staking_registry.tails_metadata[KTailsLevels]; + let length = tails_staking_registry.staking_infos.length(); + let slice_size = (tails_staking_registry.staking_infos.slice_size() as u64); + let mut slice_idx = 0; + let mut slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + let mut slice_length = slice.get_slice_length(); + let mut i = 0; + while (i < length) { + let staking_info: &StakingInfo = &slice[i % slice_size]; + let mut j = 0; + let length = staking_info.tails.length(); + while (j < length) { + let tails_number = staking_info.tails[j]; + let tails_level = tails_levels[tails_number - 1]; + *&mut level_counts[tails_level - 1] = level_counts[tails_level - 1] + 1; + j = j + 1; + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = slice.get_slice_idx() + 1; + slice = tails_staking_registry.staking_infos.borrow_slice(slice_idx); + slice_length = slice.get_slice_length(); + }; + i = i + 1; + }; + + level_counts + } + + /// Verifies if a user has a staked Tails NFT of the certain address. + public fun verify_staking( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + user: address, + tails: address, + ): bool { + version.version_check(); + + let tails_ids: &vector
= &tails_staking_registry.tails_metadata[KTailsIds]; + let length = big_vector::length(&tails_staking_registry.staking_infos); + let slice_size = (big_vector::slice_size(&tails_staking_registry.staking_infos) as u64); + let mut slice_idx = 0; + let mut slice = big_vector::borrow_slice(&tails_staking_registry.staking_infos, slice_idx); + let mut slice_length = big_vector::get_slice_length(slice); + let mut i = 0; + while (i < length) { + let staking_info = big_vector::borrow_from_slice(slice, i % slice_size); + if (staking_info.user == user) { + let mut i = 0; + let length = vector::length(&staking_info.tails); + while (i < length) { + let tails_number = *vector::borrow(&staking_info.tails, i); + if (*vector::borrow(tails_ids, tails_number - 1) == tails) { + return true + }; + i = i + 1; + }; + return false + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = big_vector::get_slice_idx(slice) + 1; + slice = big_vector::borrow_slice( + &tails_staking_registry.staking_infos, + slice_idx, + ); + slice_length = big_vector::get_slice_length(slice); + }; + i = i + 1; + }; + + false + } + + /// Verifies if a user has a staked Tails NFT of a certain level or higher. + public fun verify_staking_identity( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + user: address, + level: u64, + ): bool { + version.version_check(); + + if (level == 0) { + return true + }; + let tails_levels: &vector = bag::borrow(&tails_staking_registry.tails_metadata, KTailsLevels); + let length = big_vector::length(&tails_staking_registry.staking_infos); + let slice_size = (big_vector::slice_size(&tails_staking_registry.staking_infos) as u64); + let mut slice_idx = 0; + let mut slice = big_vector::borrow_slice(&tails_staking_registry.staking_infos, slice_idx); + let mut slice_length = big_vector::get_slice_length(slice); + let mut i = 0; + while (i < length) { + let staking_info = big_vector::borrow_from_slice(slice, i % slice_size); + if (staking_info.user == user) { + let mut i = 0; + let length = vector::length(&staking_info.tails); + while (i < length) { + let tails_number = *vector::borrow(&staking_info.tails, i); + if (*vector::borrow(tails_levels, tails_number - 1) >= level) { + return true + }; + i = i + 1; + }; + return false + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = big_vector::get_slice_idx(slice) + 1; + slice = big_vector::borrow_slice( + &tails_staking_registry.staking_infos, + slice_idx, + ); + slice_length = big_vector::get_slice_length(slice); + }; + i = i + 1; + }; + + false + } + + /// Retrieves the maximum level of a user's staked Tails NFTs. + public fun get_max_staking_level( + version: &Version, + tails_staking_registry: &TailsStakingRegistry, + user: address, + ): u64 { + version.version_check(); + + let mut level = 0; + let tails_levels: &vector = bag::borrow(&tails_staking_registry.tails_metadata, KTailsLevels); + let length = big_vector::length(&tails_staking_registry.staking_infos); + if (length > 0) { + let slice_size = (big_vector::slice_size(&tails_staking_registry.staking_infos) as u64); + let mut slice_idx = 0; + let mut slice = big_vector::borrow_slice(&tails_staking_registry.staking_infos, slice_idx); + let mut slice_length = big_vector::get_slice_length(slice); + let mut i = 0; + while (i < length) { + let staking_info = big_vector::borrow_from_slice(slice, i % slice_size); + if (staking_info.user == user) { + let mut i = 0; + let length = vector::length(&staking_info.tails); + while (i < length) { + let tails_number = *vector::borrow(&staking_info.tails, i); + if (tails_levels[tails_number - 1] > level) { + level = tails_levels[tails_number - 1]; + }; + i = i + 1; + }; + break + }; + // jump to next slice + if (i + 1 < length && i + 1 == slice_idx * slice_size + slice_length) { + slice_idx = big_vector::get_slice_idx(slice) + 1; + slice = big_vector::borrow_slice( + &tails_staking_registry.staking_infos, + slice_idx, + ); + slice_length = big_vector::get_slice_length(slice); + }; + i = i + 1; + }; + }; + + level + } + + #[deprecated(note = b"Use `exp_down_with_fee` instead.")] + public fun exp_down( + _version: &Version, + _tails_staking_registry: &mut TailsStakingRegistry, + _typus_user_registry: &mut TypusUserRegistry, + _tails: address, + _amount: u64, + _ctx: &TxContext, + ) { + abort 0 + } + #[deprecated(note = b"Use `exp_down_without_staking_with_fee` instead.")] + public fun exp_down_without_staking( + _version: &Version, + _tails_staking_registry: &TailsStakingRegistry, + _typus_user_registry: &mut TypusUserRegistry, + _kiosk: &mut Kiosk, + _kiosk_owner_cap: &KioskOwnerCap, + _tails: address, + _amount: u64, + _ctx: &TxContext, + ) { + abort 0 + } +} \ No newline at end of file diff --git a/tgld.move b/tgld.move new file mode 100644 index 0000000..6ec98f6 --- /dev/null +++ b/tgld.move @@ -0,0 +1,123 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module defines the `TGLD` (Typus Gold) token, a fungible token used within the Typus ecosystem. +/// It provides functions for creating, minting, and burning the token. +module typus::tgld { + use std::ascii; + + use sui::coin::{Self, TreasuryCap}; + use sui::event::emit; + use sui::token::{Self, Token, TokenPolicyCap}; + use sui::url; + + use typus::ecosystem::{ManagerCap, Version}; + + // ======== Structs ======== + + /// A struct representing the Typus Gold token type. + public struct TGLD has drop {} + + /// A registry object that holds the `TreasuryCap` and `TokenPolicyCap` for the `TGLD` token. + public struct TgldRegistry has key { + id: UID, + /// The treasury capability for the `TGLD` token, which allows for minting and burning. + treasury_cap: TreasuryCap, + /// The token policy capability, which allows for managing the token's transfer policy. + token_policy_cap: TokenPolicyCap, + } + + /// An event emitted when new `TGLD` tokens are minted. + public struct MintEvent has copy, drop { + /// The address of the recipient of the minted tokens. + recipient: address, + /// Log data: [minted_amount] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + + /// An event emitted when `TGLD` tokens are burned. + public struct BurnEvent has copy, drop { + /// Log data: [burned_amount] + log: vector, + /// Padding for BCS. + bcs_padding: vector>, + } + + // ======== Public Functions ======== + + /// Initializes the `TGLD` token, creating the `TreasuryCap`, `CoinMetadata`, and `TokenPolicy`. + /// It also creates and shares the `TgldRegistry`. This function is called only once during deployment. + #[lint_allow(share_owned), allow(deprecated_usage)] + fun init(witness: TGLD, ctx: &mut TxContext) { + let (treasury_cap, coin_metadata) = coin::create_currency( + witness, + 0, + b"TGLD", + b"Typus Gold", + b"TGLD on Sui maintained by Typus Lab", + option::some(url::new_unsafe(ascii::string(b"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTIiIGhlaWdodD0iNTIiIHZpZXdCb3g9IjAgMCA1MiA1MiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjYiIGN5PSIyNiIgcj0iMjYiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl8xNDU5MV80NjczMTApIi8+CjxwYXRoIGQ9Ik0yNy41ODA4IDguOTkyOTlDMjguNzUxNSA5Ljc3ODc5IDI5LjgyNTUgMTAuOTU3NSAzMC40OTgxIDExLjc1MzZDMzEuNzQwMiAxMy4yMTU2IDMzLjEyMzIgMTQuODIwMyAzMy44MTA1IDE2LjYyMTRDMzQuMzA0NCAxNy45MiAzNC4zNjEyIDE5LjQ0ODIgMzMuNjE5MiAyMC42Mjg5QzMyLjk3ODIgMjEuNjUwNCAzMS44NDk1IDIyLjI2NDYgMzAuNzcxMyAyMi44MzUzQzI5LjA2ODggMjMuNzM2OSAyNy4zNjY0IDI0LjYzODUgMjUuNjU5NyAyNS41NDAxQzI1LjU5MjUgMjUuNTc1MyAyNS41MjEgMjUuNjE2NiAyNS40OTU4IDI1LjY4NDlDMjUuNDY2NCAyNS43NTcyIDI1LjUgMjUuODM1OCAyNS41MjczIDI1LjkwODJDMjUuNzM3NSAyNi40MjUyIDI1Ljk2ODcgMjYuODc1OSAyNi4wOTA2IDI3LjM1MTZDMjYuMjQxOSAyNy45Mzg4IDI2LjE4NTIgMjcuNDY3NCAyNi4yNTg3IDI4LjQ2MkMyNi4yOTAzIDI4Ljg4OCAyNi4xOTk5IDI5LjM3NiAyNS45ODc2IDI5Ljc3NzJDMjUuOTA5OCAyOS45MjE5IDI1LjgzIDMwLjE0OTQgMjUuNjc0NCAzMC4xOTlDMjUuMzczOSAzMC4yOTQxIDI0LjE1MjcgMjguODI4IDIzLjcwNTEgMjguNTQ2OEMyMi43MDQ2IDI3LjkyMDIgMjIuMTA5OCAyNy42MDM4IDIxLjY2NDIgMjcuNzE1NUMyMS42MDk2IDI3LjcyNzkgMjAuNTkwMiAyOC4yOTQ1IDIwLjE1NTEgMjguODU3QzE5Ljg4MTkgMjkuMjEyNiAxOS42MDY2IDI5LjYxMTcgMTkuMzg4IDI5Ljk0NjdDMTguNTc0NiAzMS4xODc1IDE4Ljg3OTMgMzIuNDQ2OCAxOC45ODQ0IDMyLjg3NjlDMTkuMzEwMiAzNC4yMjMxIDIwLjY5OTUgMzQuMjM5NiAyMS44ODQ5IDM0LjIwMDRDMjQuMTU5IDM0LjEyMzggMjYuNTM4MyAzNC4yNTgzIDI4LjUyMjQgMzUuMzU2M0MyOC44ODM5IDM1LjU1NjkgMjkuMjc2OSAzNS45MjA4IDI5LjEzMTkgMzYuMzAxM0MyOS4wNDk5IDM2LjUxMjIgMjguODI5MiAzNi42MzQyIDI4LjYyMTIgMzYuNzI5NEMyNy42ODU5IDM3LjE2MzYgMjYuNjkxNyAzNy40Nzc5IDI1LjY3NDQgMzcuNjU3OEMyNC44ODQyIDM3Ljc5ODUgMjQuMDU4MiAzNy44NjY3IDIzLjM1NDEgMzguMjQ1MUMyMi41ODI3IDM4LjY1ODcgMjIuMDQyNSAzOS40MDczIDIxLjY5NzggNDAuMjAzNEMyMS40MjA0IDQwLjg0NjUgMjAuOTg5NSA0NC4xMzg1IDE5Ljc3NjggNDMuNTUxM0MxOS41NjQ1IDQzLjQ0OTkgMTkuNDE1MyA0My4yNTU2IDE5LjI5MzQgNDMuMDU3MUMxOC43ODI2IDQyLjIyMTYgMTguNjc3NiA0MS4yMzMyIDE4LjI4NjYgNDAuMzUwMkMxNy44NDc0IDM5LjM1NTYgMTcuMDQyNCAzOC44NTcyIDE2LjE3NDMgMzguMjQ3MkMxNC4zOTQxIDM2Ljk5ODIgMTIuODU3NyAzNS4xMTIzIDEyLjMwOTEgMzMuMDA3MkMxMS41NzE0IDMwLjE4MjUgMTEuNzQ3OSAyNi42MzQgMTMuMjUyOCAyNC4wNTc0QzEyLjk1MjMgMjMuNzUxNCAxMi44MDA5IDIzLjcyNjYgMTEuMDk2NCAyMy44NTQ4QzEwLjg0ODQgMjMuODczNCA5LjQ2NTM5IDI0LjIxNDYgOS4zMzkyOCAyMy45OTk1QzkuMjM4NCAyMy44MjU4IDkuMzMyOTggMjMuNDU3OCA5LjQ2MTE5IDIzLjE1NzlDOS41MDMyMyAyMy4wNjI4IDkuNTUzNjYgMjIuOTUzMiA5LjU4NzI5IDIyLjg3NDZDOS43ODY5NiAyMi4zNzAxIDEwLjE4IDIyLjAyMjcgMTAuNjAyNSAyMS42ODM1QzExLjc3NTMgMjAuNzQwNiAxMy4wNDkgMTkuOTUyNyAxNC40ODI0IDE5LjQzOTlDMTQuOTA5IDE5LjI4ODkgMTUuMDk0IDE5LjAyNDIgMTUuMjc0OCAxOC41OTQxQzE2Ljg3NDIgMTQuNzk1NCAyMC40ODkzIDguMzgwOSAyNS4yNjY3IDguMTc4MjVDMjYuMDIzMyA4LjE0MzEgMjYuODI0MSA4LjQ4NjM3IDI3LjU4MDggOC45OTI5OVoiIGZpbGw9IiMxRDI1MkQiLz4KPHBhdGggZD0iTTQyLjcwNyAzMC40Mzk0QzM5LjU0NTggMzIuNTA1MyAzNy43MTMzIDM0LjQ3ODggMzcuNzk4MyAzNS42OTkyQzM3Ljc5NTEgMzUuNjk2IDM3Ljc5NTEgMzUuNjkyNyAzNy43OTUxIDM1LjY4OTVDMzcuNjMxNCAzMi44ODY3IDM2LjAxODEgMzAuNjg3NyAzNC4wNTE0IDMwLjY4NzdDMzMuNTc2OSAzMC42ODc3IDMzLjEyMiAzMC44MTU5IDMyLjcwMzEgMzEuMDQ5NkMzNS43NzI3IDI5LjAzODggMzcuNTc5MSAyNy4xMjA1IDM3LjU4NTYgMjUuODk4NEMzNy43Nzg3IDI4LjY2MjMgMzkuMzgyMiAzMC44MTkxIDQxLjMyOTMgMzAuODE5MUM0MS44MTY5IDMwLjgxOTEgNDIuMjgxNiAzMC42ODQ0IDQyLjcwNyAzMC40Mzk0WiIgZmlsbD0iIzFEMjUyRCIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzE0NTkxXzQ2NzMxMCIgeDE9IjIuNzE0MjkiIHkxPSI1MC4wNzE0IiB4Mj0iNDAuMTQyOSIgeTI9IjcuNzE0MjgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0ZGQkUxNyIvPgo8c3RvcCBvZmZzZXQ9IjAuNjQwNjI1IiBzdG9wLWNvbG9yPSIjRkZFMjk3Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg=="))), + ctx + ); + let (token_policy, token_policy_cap) = token::new_policy(&treasury_cap, ctx); + let registry = TgldRegistry { + id: object::new(ctx), + treasury_cap, + token_policy_cap, + }; + token::share_policy(token_policy); + transfer::public_share_object(coin_metadata); + transfer::share_object(registry); + } + + /// Mints new `TGLD` tokens and transfers them to a recipient. + /// This is an authorized function that requires a `ManagerCap`. + public fun mint( + _manager_cap: &ManagerCap, + version: &Version, + registry: &mut TgldRegistry, + recipient: address, + amount: u64, + ctx: &mut TxContext, + ) { + version.version_check(); + + token::confirm_with_policy_cap( + ®istry.token_policy_cap, + token::transfer( + token::mint(&mut registry.treasury_cap, amount, ctx), + recipient, + ctx, + ), + ctx, + ); + emit(MintEvent { + recipient, + log: vector[amount], + bcs_padding: vector[], + }); + } + + /// Burns `TGLD` tokens. + /// This is an authorized function that requires a `ManagerCap`. + public fun burn( + _manager_cap: &ManagerCap, + version: &Version, + registry: &mut TgldRegistry, + tgld: Token, + ) { + version.version_check(); + + emit(BurnEvent { + log: vector[token::value(&tgld)], + bcs_padding: vector[], + }); + token::burn(&mut registry.treasury_cap, tgld); + } + + #[test_only] + public fun test_init(ctx: &mut TxContext) { + init(TGLD {}, ctx); + } +} \ No newline at end of file diff --git a/typus_stake_pool/sources/admin.move b/typus_stake_pool/sources/admin.move index f806820..2894619 100644 --- a/typus_stake_pool/sources/admin.move +++ b/typus_stake_pool/sources/admin.move @@ -6,7 +6,6 @@ module typus_stake_pool::admin { use sui::balance::{Self, Balance}; use sui::coin; use sui::dynamic_field; - use sui::event::emit; use sui::vec_set::{Self, VecSet}; // ======== Errors ======== diff --git a/typus_stake_pool/sources/stake_pool.move b/typus_stake_pool/sources/stake_pool.move index bcdce3f..800e032 100644 --- a/typus_stake_pool/sources/stake_pool.move +++ b/typus_stake_pool/sources/stake_pool.move @@ -214,22 +214,22 @@ module typus_stake_pool::stake_pool { active: true, new_tlp_price: 10000, depositors_count: 0, - u64_padding: vector::empty() + u64_padding: vector[] }, config: StakePoolConfig { unlock_countdown_ts_ms, usd_per_exp: 200, - u64_padding: vector::empty() + u64_padding: vector[] }, - incentives: vector::empty(), - u64_padding: vector::empty() + incentives: vector[], + u64_padding: vector[] }; emit(NewStakePoolEvent { sender: tx_context::sender(ctx), stake_pool_info: stake_pool.pool_info, stake_pool_config: stake_pool.config, - u64_padding: vector::empty() + u64_padding: vector[] }); dynamic_object_field::add(&mut registry.id, registry.num_pool, stake_pool); @@ -308,7 +308,7 @@ module typus_stake_pool::stake_pool { total_amount: total_incentive_value, compound_users, total_users, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -351,14 +351,14 @@ module typus_stake_pool::stake_pool { config: IncentiveConfig { period_incentive_amount, incentive_interval_ts_ms, - u64_padding: vector::empty(), + u64_padding: vector[], }, info: IncentiveInfo { active: true, last_allocate_ts_ms: clock::timestamp_ms(clock), incentive_price_index: 0, unallocated_amount: 0, - u64_padding: vector::empty(), + u64_padding: vector[], } }; vector::push_back(&mut stake_pool.incentives, incentive); @@ -369,7 +369,7 @@ module typus_stake_pool::stake_pool { incentive_token: incentive.token_type, incentive_info: incentive.info, incentive_config: incentive.config, - u64_padding: vector::empty() + u64_padding: vector[] }); dynamic_field::add(&mut stake_pool.id, incentive_token, balance::zero()); @@ -411,7 +411,7 @@ module typus_stake_pool::stake_pool { emit(DeactivateStakePoolEvent { sender: tx_context::sender(ctx), index, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -438,7 +438,7 @@ module typus_stake_pool::stake_pool { emit(ActivateStakePoolEvent { sender: tx_context::sender(ctx), index, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -472,7 +472,7 @@ module typus_stake_pool::stake_pool { sender: tx_context::sender(ctx), index, incentive_token, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -507,7 +507,7 @@ module typus_stake_pool::stake_pool { sender: tx_context::sender(ctx), index, incentive_token, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -571,7 +571,7 @@ module typus_stake_pool::stake_pool { index, incentive_token, incentive_balance_value: balance::value(&incentive_balance), - u64_padding: vector::empty() + u64_padding: vector[] }); coin::from_balance(incentive_balance, ctx) @@ -608,7 +608,7 @@ module typus_stake_pool::stake_pool { index, previous_unlock_countdown_ts_ms, new_unlock_countdown_ts_ms: unlock_countdown_ts_ms, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -659,7 +659,7 @@ module typus_stake_pool::stake_pool { index, previous_incentive_config, new_incentive_config: incentive.config, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -744,7 +744,7 @@ module typus_stake_pool::stake_pool { index, incentive_token_type: incentive_token, deposit_amount: incentive_amount, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -791,7 +791,7 @@ module typus_stake_pool::stake_pool { index, incentive_token_type: incentive_token, withdrawal_amount, - u64_padding: vector::empty() + u64_padding: vector[] }); coin::from_balance(withdraw_balance, ctx) } @@ -866,7 +866,7 @@ module typus_stake_pool::stake_pool { stake_ts_ms: current_ts_ms, total_shares: balance_value, active_shares: balance_value, - deactivating_shares: vector::empty(), + deactivating_shares: vector[], last_incentive_price_index, snapshot_ts_ms: current_ts_ms, tlp_price: new_tlp_price, @@ -1041,7 +1041,7 @@ module typus_stake_pool::stake_pool { unsubscribed_ts_ms: current_ts_ms, unlocked_ts_ms, unsubscribed_incentive_price_index: last_incentive_price_index, - u64_padding: vector::empty(), + u64_padding: vector[], }; lp_user_share.deactivating_shares.push_back(deactivating_shares); @@ -1054,7 +1054,7 @@ module typus_stake_pool::stake_pool { unsubscribed_shares, unsubscribe_ts_ms: current_ts_ms, unlocked_ts_ms, - u64_padding: vector::empty() + u64_padding: vector[] }); } @@ -1146,7 +1146,7 @@ module typus_stake_pool::stake_pool { user_share_id, unstake_amount: temp_unstaked_shares, unstake_ts_ms: current_ts_ms, - u64_padding: vector::empty() + u64_padding: vector[] }); let b = balance::split(dynamic_field::borrow_mut(&mut stake_pool.id, string::utf8(K_STAKED_TLP)), temp_unstaked_shares); @@ -1213,7 +1213,7 @@ module typus_stake_pool::stake_pool { incentive_token_type: incentive_token, harvest_amount: incentive_value, user_share_id, - u64_padding: vector::empty() + u64_padding: vector[] }); let b = balance::split(dynamic_field::borrow_mut(&mut stake_pool.id, incentive_token), incentive_value); @@ -1311,12 +1311,12 @@ module typus_stake_pool::stake_pool { // check exist if (!all_lp_user_shares.contains(user)) { // early return - return vector::empty() + return vector[] }; let user_share: & LpUserShare = all_lp_user_shares.borrow_by_key(user); let incentive_tokens = get_incentive_tokens(stake_pool); - let mut incentive_values = vector::empty(); + let mut incentive_values = vector[]; incentive_tokens.do_ref!(|incentive_token| { let incentive = get_incentive(stake_pool, incentive_token); let current_incentive_index = incentive.info.incentive_price_index; @@ -1340,12 +1340,12 @@ module typus_stake_pool::stake_pool { let stake_pool = get_stake_pool(®istry.id, index); let all_lp_user_shares = dynamic_field::borrow(&stake_pool.id, string::utf8(K_LP_USER_SHARES)); - let mut result = vector::empty(); + let mut result = vector[]; all_lp_user_shares.do_ref!(|_user, user_share| { if (user_share.user_share_id == user_share_id) { let incentive_tokens = get_incentive_tokens(stake_pool); - let mut incentive_values = vector::empty(); + let mut incentive_values = vector[]; incentive_tokens.do_ref!(|incentive_token| { let incentive = get_incentive(stake_pool, incentive_token); let current_incentive_index = incentive.info.incentive_price_index; @@ -1383,7 +1383,7 @@ module typus_stake_pool::stake_pool { fun get_incentive_tokens(stake_pool: &StakePool): vector { let mut i = 0; let length = vector::length(&stake_pool.incentives); - let mut incentive_tokens = vector::empty(); + let mut incentive_tokens = vector[]; while (i < length) { vector::push_back( &mut incentive_tokens, @@ -1446,7 +1446,7 @@ module typus_stake_pool::stake_pool { fun get_last_incentive_price_index(stake_pool: &StakePool): vector { let mut i = 0; let length = vector::length(&stake_pool.incentives); - let mut last_incentive_price_index = vector::empty(); + let mut last_incentive_price_index = vector[]; while (i < length) { let incentive = vector::borrow(&stake_pool.incentives, i); vector::push_back(&mut last_incentive_price_index, incentive.info.incentive_price_index); diff --git a/user.move b/user.move new file mode 100644 index 0000000..bb895eb --- /dev/null +++ b/user.move @@ -0,0 +1,216 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module manages user data for the Typus ecosystem. It provides a registry for storing +/// user metadata, such as accumulated TGLD and Tails EXP amounts. +module typus::user { + use std::bcs; + + use sui::event::emit; + use sui::linked_table::{Self, LinkedTable}; + + use typus::ecosystem::{ManagerCap, Version}; + use typus::tgld::{Self, TgldRegistry}; + use typus::utility; + + // ======== Metadata content index ======== + + /// Index for the accumulated TGLD amount in the metadata content vector. + const IAccumulatedTgldAmount: u64 = 0; + /// Index for the Tails EXP amount in the metadata content vector. + const ITailsExpAmount: u64 = 1; + + // ======== Typus User ======== + + /// A registry for storing user metadata. + public struct TypusUserRegistry has key { + id: UID, + /// A linked table mapping user addresses to their `Metadata`. + metadata: LinkedTable, + } + + /// Stores user-specific metadata. + public struct Metadata has store, drop { + /// A vector of `u64` values representing user metadata. + content: vector, + } + + /// Initializes the `TypusUserRegistry`. + fun init(ctx: &mut TxContext) { + transfer::share_object(TypusUserRegistry { + id: object::new(ctx), + metadata: linked_table::new(ctx), + }); + } + + /// Event emitted when a user's accumulated TGLD amount is increased. + public struct AddAccumulatedTgldAmount has copy, drop { + user: address, + log: vector, + bcs_padding: vector>, + } + /// Increases a user's accumulated TGLD amount and mints the corresponding amount of `TGLD` tokens. + /// This is an authorized function that requires a `ManagerCap`. + public fun add_accumulated_tgld_amount( + manager_cap: &ManagerCap, + version: &Version, + typus_user_registry: &mut TypusUserRegistry, + tgld_registry: &mut TgldRegistry, + user: address, + amount: u64, + ctx: &mut TxContext, + ): vector { + version.version_check(); + + if (amount == 0) { + return vector[0] + }; + if (!typus_user_registry.metadata.contains(user)) { + typus_user_registry.metadata.push_back( + user, + Metadata { + content: vector[], + }, + ); + }; + let metadata = typus_user_registry.metadata.borrow_mut(user); + utility::increase_u64_vector_value(&mut metadata.content, IAccumulatedTgldAmount, amount); + tgld::mint( + manager_cap, + version, + tgld_registry, + user, + amount, + ctx, + ); + emit(AddAccumulatedTgldAmount { + user, + log: vector[amount], + bcs_padding: vector[], + }); + + vector[amount] + } + + /// Event emitted when a user's Tails EXP amount is increased. + public struct AddTailsExpAmount has copy, drop { + user: address, + log: vector, + bcs_padding: vector>, + } + /// Increases a user's Tails EXP amount. + /// This is an authorized function that requires a `ManagerCap`. + public fun add_tails_exp_amount( + _manager_cap: &ManagerCap, + version: &Version, + typus_user_registry: &mut TypusUserRegistry, + user: address, + amount: u64, + ): vector { + add_tails_exp_amount_( + version, + typus_user_registry, + user, + amount, + ) + } + /// Increases a user's Tails EXP amount. This is a package-private function. + /// WARNING: mut inputs without authority check inside + public(package) fun add_tails_exp_amount_( + version: &Version, + typus_user_registry: &mut TypusUserRegistry, + user: address, + amount: u64, + ): vector { + version.version_check(); + + if (amount == 0) { + return vector[0] + }; + if (!typus_user_registry.metadata.contains(user)) { + typus_user_registry.metadata.push_back( + user, + Metadata { + content: vector[], + }, + ); + }; + let metadata = typus_user_registry.metadata.borrow_mut(user); + utility::increase_u64_vector_value(&mut metadata.content, ITailsExpAmount, amount); + emit(AddTailsExpAmount { + user, + log: vector[amount], + bcs_padding: vector[], + }); + + vector[amount] + } + + + /// Event emitted when a user's Tails EXP amount is decreased. + public struct RemoveTailsExpAmount has copy, drop { + user: address, + log: vector, + bcs_padding: vector>, + } + /// Decreases a user's Tails EXP amount. + /// This is an authorized function that requires a `ManagerCap`. + public fun remove_tails_exp_amount( + _manager_cap: &ManagerCap, + version: &Version, + typus_user_registry: &mut TypusUserRegistry, + user: address, + amount: u64, + ): vector { + remove_tails_exp_amount_( + version, + typus_user_registry, + user, + amount, + ) + } + /// Decreases a user's Tails EXP amount. This is a package-private function. + /// WARNING: mut inputs without authority check inside + public(package) fun remove_tails_exp_amount_( + version: &Version, + typus_user_registry: &mut TypusUserRegistry, + user: address, + amount: u64, + ): vector { + version.version_check(); + + if (amount == 0 || !typus_user_registry.metadata.contains(user)) { + return vector[0] + }; + let metadata = typus_user_registry.metadata.borrow_mut(user); + utility::decrease_u64_vector_value(&mut metadata.content, ITailsExpAmount, amount); + emit(RemoveTailsExpAmount { + user, + log: vector[amount], + bcs_padding: vector[], + }); + + vector[amount] + } + + /// Retrieves the metadata for a specific user. + public fun get_user_metadata( + version: &Version, + typus_user_registry: &TypusUserRegistry, + user: address, + ): vector { + version.version_check(); + + if (!typus_user_registry.metadata.contains(user)) { + bcs::to_bytes(&Metadata { content: vector[] }) + } else { + bcs::to_bytes(typus_user_registry.metadata.borrow(user)) + } + + } + + #[test_only] + public fun test_init(ctx: &mut TxContext) { + init(ctx); + } +} \ No newline at end of file diff --git a/utility.move b/utility.move new file mode 100644 index 0000000..7a4d5ad --- /dev/null +++ b/utility.move @@ -0,0 +1,92 @@ +// Copyright (c) Typus Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// This module provides a collection of utility functions that are used throughout the Typus ecosystem. +/// These functions include helpers for transferring coins and balances, calculating basis points, +/// and manipulating vectors of `u64`. +module typus::utility { + use sui::balance::Balance; + use sui::coin::{Self, Coin}; + + /// Transfers a vector of `Coin`s to a specified user. + public fun transfer_coins(mut coins: vector>, user: address) { + while (!vector::is_empty(&coins)) { + transfer::public_transfer(coins.pop_back(), user); + }; + coins.destroy_empty(); + } + + /// Transfers a `Balance` to a specified user. + /// If the balance is zero, it is destroyed. + public fun transfer_balance(balance: Balance, user: address, ctx: &mut TxContext) { + if (balance.value() == 0) { + balance.destroy_zero(); + } else { + transfer::public_transfer(coin::from_balance(balance, ctx), user); + } + } + + /// Transfers an `Option` to a specified user. + /// If the option is `None`, it does nothing. + public fun transfer_balance_opt(balance_opt: Option>, user: address, ctx: &mut TxContext) { + if(balance_opt.is_some()) { + transfer_balance(balance_opt.destroy_some(), user, ctx); + } else { + balance_opt.destroy_none(); + } + } + + /// Calculates a value based on basis points (1/10000). + public fun basis_point_value(value: u64, bp: u64): u64 { + ((value as u128) * (bp as u128) / (10000u64 as u128) as u64) + } + + /// Sets a value in a `vector` at a specific index. + /// If the index is out of bounds, it pads the vector with zeros. + public fun set_u64_vector_value(data: &mut vector, i: u64, value: u64) { + pad_u64_vector(data, i); + *&mut data[i] = value; + } + + /// Increases a value in a `vector` at a specific index. + /// If the index is out of bounds, it pads the vector with zeros before increasing the value. + public fun increase_u64_vector_value(data: &mut vector, i: u64, value: u64) { + pad_u64_vector(data, i); + *&mut data[i] = data[i] + value; + } + + /// Decreases a value in a `vector` at a specific index. + /// If the index is out of bounds, it pads the vector with zeros before decreasing the value. + public fun decrease_u64_vector_value(data: &mut vector, i: u64, value: u64) { + pad_u64_vector(data, i); + *&mut data[i] = data[i] - value; + } + + /// Pads a `vector` with zeros until it reaches a specified length. + public fun pad_u64_vector(data: &mut vector, i: u64) { + while (data.length() < i + 1) { + data.push_back(0); + }; + } + + /// Gets a value from a `vector` at a specific index. + /// Returns 0 if the index is out of bounds. + public fun get_u64_vector_value(data: &vector, i: u64): u64 { + if (data.length() > i) { + return *data.borrow(i) + }; + + 0 + } + + /// Calculates a multiplier based on a number of decimals (10^decimal). + public fun multiplier(decimal: u64): u64 { + let mut i = 0; + let mut multiplier = 1; + while (i < decimal) { + multiplier = multiplier * 10; + i = i + 1; + }; + multiplier + } +} \ No newline at end of file diff --git a/version/sources/version.move b/version/sources/version.move index b8252f3..7e04079 100644 --- a/version/sources/version.move +++ b/version/sources/version.move @@ -40,7 +40,7 @@ module version::version { fee_infos: vector[], }, authority: vec_set::singleton(ctx.sender()), - witness: type_name::get(), + witness: type_name::with_defining_ids(), u64_padding: vector[], }); } @@ -58,7 +58,7 @@ module version::version { } public fun verify_witness(version: &Version, _: W) { - assert!(type_name::get() == version.witness, EInvalidWitness); + assert!(type_name::with_defining_ids() == version.witness, EInvalidWitness); } public fun verify_authority( @@ -88,7 +88,7 @@ module version::version { assert!(version.authority.contains(&user_address), EAuthorityDoesNotExist); version.authority.remove(&user_address); - assert!(version.authority.size() > 0, EAuthorityEmpty); + assert!(version.authority.length() > 0, EAuthorityEmpty); } // ======== Fee Pool ======== @@ -113,10 +113,10 @@ module version::version { let mut i = 0; while (i < version.fee_pool.fee_infos.length()) { let fee_info = version.fee_pool.fee_infos.borrow_mut(i); - if (fee_info.token == type_name::get()) { + if (fee_info.token == type_name::with_defining_ids()) { transfer::public_transfer( coin::from_balance( - balance::withdraw_all(dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::get())), + balance::withdraw_all(dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::with_defining_ids())), ctx, ), recipient, @@ -135,10 +135,10 @@ module version::version { let mut i = 0; while (i < version.fee_pool.fee_infos.length()) { let fee_info = &mut version.fee_pool.fee_infos[i]; - if (fee_info.token == type_name::get()) { + if (fee_info.token == type_name::with_defining_ids()) { fee_info.value = fee_info.value + balance::value(&balance); balance::join( - dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::get()), + dynamic_field::borrow_mut(&mut version.fee_pool.id, type_name::with_defining_ids()), balance, ); return @@ -147,10 +147,10 @@ module version::version { }; version.fee_pool.fee_infos.push_back( FeeInfo { - token: type_name::get(), + token: type_name::with_defining_ids(), value: balance::value(&balance), }, ); - dynamic_field::add(&mut version.fee_pool.id, type_name::get(), balance); + dynamic_field::add(&mut version.fee_pool.id, type_name::with_defining_ids(), balance); } } \ No newline at end of file diff --git a/witness_lock.move b/witness_lock.move new file mode 100644 index 0000000..4a8b5ed --- /dev/null +++ b/witness_lock.move @@ -0,0 +1,54 @@ +/// This module implements a "witness lock" pattern, which is a way to create restricted functions +/// that can only be called if a specific witness type is provided. This is a common pattern in Sui Move +/// for creating authorization mechanisms that are not tied to a specific authority address. +module typus::witness_lock { + use std::type_name; + use std::string::String; + use typus::ecosystem::Version; + + public struct HotPotato { + obj: T, + witness: String + } + + /// Wraps an object in a `HotPotato`, effectively locking it with a witness. + /// The witness is the type name of a specific type that will be required to unlock the object. + public fun wrap( + version: &Version, + obj: T, + witness: String, + ): HotPotato { + version.version_check(); + + let hot_potato = HotPotato { + obj, + witness, + }; + hot_potato + } + + /// Unwraps a `HotPotato`, returning the original object. + /// This function requires a witness of type `W` to be passed in. It checks that the type name + /// of the witness matches the witness string stored in the `HotPotato`. + /// Aborts if the witness is invalid. + public fun unwrap( + version: &Version, + hot_potato: HotPotato, + _witness: W, + ): T { + version.version_check(); + + let HotPotato { obj, witness } = hot_potato; + // check witness + assert!(type_name::with_defining_ids().into_string().to_string() == witness, invalid_witness()); + obj + } + + /// Aborts with an error code indicating an invalid witness. + fun invalid_witness(): u64 { abort 0 } + + #[test_only] + public fun update_witness_for_testing(hot_potato: &mut HotPotato, witness: String) { + hot_potato.witness = witness; + } +} \ No newline at end of file