diff --git a/aptos-move/move-examples/token_objects/ambassador/sources/ambassador.move b/aptos-move/move-examples/token_objects/ambassador/sources/ambassador.move index 5c86020ebddf1..d60102db45134 100644 --- a/aptos-move/move-examples/token_objects/ambassador/sources/ambassador.move +++ b/aptos-move/move-examples/token_objects/ambassador/sources/ambassador.move @@ -47,6 +47,7 @@ module token_objects::ambassador { const RANK_SILVER: vector = b"Silver"; const RANK_BRONZE: vector = b"Bronze"; + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] /// The ambassador token struct AmbassadorToken has key { /// Used to mutate the token uri @@ -61,6 +62,7 @@ module token_objects::ambassador { base_uri: String, } + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] /// The ambassador level struct AmbassadorLevel has key { ambassador_level: u64, diff --git a/aptos-move/move-examples/token_objects/knight/Move.toml b/aptos-move/move-examples/token_objects/knight/Move.toml new file mode 100644 index 0000000000000..04e829911b84a --- /dev/null +++ b/aptos-move/move-examples/token_objects/knight/Move.toml @@ -0,0 +1,10 @@ +[package] +name = 'knight' +version = '1.0.0' + +[addresses] +token_objects = "_" + +[dependencies] +AptosFramework = { local = "../../../framework/aptos-framework" } +AptosTokenObjects = { local = "../../../framework/aptos-token-objects" } diff --git a/aptos-move/move-examples/token_objects/knight/sources/food.move b/aptos-move/move-examples/token_objects/knight/sources/food.move new file mode 100644 index 0000000000000..94060fff516dd --- /dev/null +++ b/aptos-move/move-examples/token_objects/knight/sources/food.move @@ -0,0 +1,327 @@ +/// This module implements the the food tokens (fungible token). When the module initializes, +/// it creates the collection and two fungible tokens such as Corn and Meat. +module token_objects::food { + use std::error; + use std::option; + use std::signer; + use std::string::{Self, String}; + use aptos_framework::fungible_asset::{Self, Metadata}; + use aptos_framework::object::{Self, Object}; + use aptos_framework::primary_fungible_store; + use aptos_token_objects::collection; + use aptos_token_objects::property_map; + use aptos_token_objects::token; + + /// The token does not exist + const ETOKEN_DOES_NOT_EXIST: u64 = 1; + /// The provided signer is not the creator + const ENOT_CREATOR: u64 = 2; + /// Attempted to mutate an immutable field + const EFIELD_NOT_MUTABLE: u64 = 3; + /// Attempted to burn a non-burnable token + const ETOKEN_NOT_BURNABLE: u64 = 4; + /// Attempted to mutate a property map that is not mutable + const EPROPERTIES_NOT_MUTABLE: u64 = 5; + // The collection does not exist + const ECOLLECTION_DOES_NOT_EXIST: u64 = 6; + + /// The food collection name + const FOOD_COLLECTION_NAME: vector = b"Food Collection Name"; + /// The food collection description + const FOOD_COLLECTION_DESCRIPTION: vector = b"Food Collection Description"; + /// The food collection URI + const FOOD_COLLECTION_URI: vector = b"https://food.collection.uri"; + + /// The knight token collection name + const KNIGHT_COLLECTION_NAME: vector = b"Knight Collection Name"; + /// The knight collection description + const KNIGHT_COLLECTION_DESCRIPTION: vector = b"Knight Collection Description"; + /// The knight collection URI + const KNIGHT_COLLECTION_URI: vector = b"https://knight.collection.uri"; + + /// The corn token name + const CORN_TOKEN_NAME: vector = b"Corn Token"; + /// The meat token name + const MEAT_TOKEN_NAME: vector = b"Meat Token"; + + /// Property names + const CONDITION_PROPERTY_NAME: vector = b"Condition"; + const RESTORATION_VALUE_PROPERTY_NAME: vector = b"Restoration Value"; + const HEALTH_POINT_PROPERTY_NAME: vector = b"Health Point"; + + /// The condition of a knight + const CONDITION_HUNGRY: vector = b"Hungry"; + const CONDITION_GOOD: vector = b"Good"; + + friend token_objects::knight; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + // Food Token + struct FoodToken has key { + /// Used to mutate properties + property_mutator_ref: property_map::MutatorRef, + /// Used to mint fungible assets. + fungible_asset_mint_ref: fungible_asset::MintRef, + /// Used to burn fungible assets. + fungible_asset_burn_ref: fungible_asset::BurnRef, + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// Restoration value of the food. An attribute of a food token. + struct RestorationValue has key { + value: u64, + } + + /// Initializes the module, creating the food collection and creating two fungible tokens such as Corn, and Meat. + fun init_module(sender: &signer) { + // Create a collection for food tokens. + create_food_collection(sender); + // Create two food token (i.e., Corn and Meat) as fungible tokens, meaning that there can be multiple units of them. + create_food_token_as_fungible_token( + sender, + string::utf8(b"Corn Token Description"), + string::utf8(CORN_TOKEN_NAME), + string::utf8(b"https://raw.githubusercontent.com/junkil-park/metadata/main/knight/Corn"), + string::utf8(b"Corn"), + string::utf8(b"CORN"), + string::utf8(b"https://raw.githubusercontent.com/junkil-park/metadata/main/knight/Corn.png"), + string::utf8(b"https://www.aptoslabs.com"), + 5, + ); + create_food_token_as_fungible_token( + sender, + string::utf8(b"Meat Token Description"), + string::utf8(MEAT_TOKEN_NAME), + string::utf8(b"https://raw.githubusercontent.com/junkil-park/metadata/main/knight/Meat"), + string::utf8(b"Meat"), + string::utf8(b"MEAT"), + string::utf8(b"https://raw.githubusercontent.com/junkil-park/metadata/main/knight/Meat.png"), + string::utf8(b"https://www.aptoslabs.com"), + 20, + ); + } + + #[view] + /// Returns the restoration value of the food token + public fun restoration_value(token: Object): u64 acquires RestorationValue { + let restoration_value_in_food = borrow_global(object::object_address(&token)); + restoration_value_in_food.value + } + + #[view] + /// Returns the balance of the food token of the owner + public fun food_balance(owner_addr: address, food: Object): u64 { + let metadata = object::convert(food); + let store = primary_fungible_store::ensure_primary_store_exists(owner_addr, metadata); + fungible_asset::balance(store) + } + + #[view] + /// Returns the corn token address + public fun corn_token_address(): address { + food_token_address(string::utf8(CORN_TOKEN_NAME)) + } + + #[view] + /// Returns the meat token address + public fun meat_token_address(): address { + food_token_address(string::utf8(MEAT_TOKEN_NAME)) + } + + #[view] + /// Returns the food token address by name + public fun food_token_address(food_token_name: String): address { + token::create_token_address(&@token_objects, &string::utf8(FOOD_COLLECTION_NAME), &food_token_name) + } + + /// Creates the food collection. + fun create_food_collection(creator: &signer) { + // Constructs the strings from the bytes. + let description = string::utf8(FOOD_COLLECTION_DESCRIPTION); + let name = string::utf8(FOOD_COLLECTION_NAME); + let uri = string::utf8(FOOD_COLLECTION_URI); + + // Creates the collection with unlimited supply and without establishing any royalty configuration. + collection::create_unlimited_collection( + creator, + description, + name, + option::none(), + uri, + ); + } + + /// Creates the food token as fungible token. + fun create_food_token_as_fungible_token( + creator: &signer, + description: String, + name: String, + uri: String, + fungible_asset_name: String, + fungible_asset_symbol: String, + icon_uri: String, + project_uri: String, + restoration_value: u64, + ) { + // The collection name is used to locate the collection object and to create a new token object. + let collection = string::utf8(FOOD_COLLECTION_NAME); + // Creates the food token, and get the constructor ref of the token. The constructor ref + // is used to generate the refs of the token. + let constructor_ref = token::create_named_token( + creator, + collection, + description, + name, + option::none(), + uri, + ); + + // Generates the object signer and the refs. The object signer is used to publish a resource + // (e.g., RestorationValue) under the token object address. The refs are used to manage the token. + let object_signer = object::generate_signer(&constructor_ref); + let property_mutator_ref = property_map::generate_mutator_ref(&constructor_ref); + + // Initializes the value with the given value in food. + move_to(&object_signer, RestorationValue { value: restoration_value }); + + // Initialize the property map. + let properties = property_map::prepare_input(vector[], vector[], vector[]); + property_map::init(&constructor_ref, properties); + property_map::add_typed( + &property_mutator_ref, + string::utf8(RESTORATION_VALUE_PROPERTY_NAME), + restoration_value + ); + + // Creates the fungible asset. + primary_fungible_store::create_primary_store_enabled_fungible_asset( + &constructor_ref, + option::none(), + fungible_asset_name, + fungible_asset_symbol, + 0, + icon_uri, + project_uri, + ); + let fungible_asset_mint_ref = fungible_asset::generate_mint_ref(&constructor_ref); + let fungible_asset_burn_ref = fungible_asset::generate_burn_ref(&constructor_ref); + + // Publishes the FoodToken resource with the refs. + let food_token = FoodToken { + property_mutator_ref, + fungible_asset_mint_ref, + fungible_asset_burn_ref, + }; + move_to(&object_signer, food_token); + } + + /// Mints the given amount of the corn token to the given receiver. + public entry fun mint_corn(creator: &signer, receiver: address, amount: u64) acquires FoodToken { + let corn_token = object::address_to_object(corn_token_address()); + mint_internal(creator, corn_token, receiver, amount); + } + + /// Mints the given amount of the meat token to the given receiver. + public entry fun mint_meat(creator: &signer, receiver: address, amount: u64) acquires FoodToken { + let meat_token = object::address_to_object(meat_token_address()); + mint_internal(creator, meat_token, receiver, amount); + } + + /// The internal mint function. + fun mint_internal(creator: &signer, token: Object, receiver: address, amount: u64) acquires FoodToken { + let food_token = authorized_borrow(creator, &token); + let fungible_asset_mint_ref = &food_token.fungible_asset_mint_ref; + let fa = fungible_asset::mint(fungible_asset_mint_ref, amount); + primary_fungible_store::deposit(receiver, fa); + } + + /// Transfers the given amount of the corn token from the given sender to the given receiver. + public entry fun transfer_corn(from: &signer, to: address, amount: u64) { + transfer_food(from, object::address_to_object(corn_token_address()), to, amount); + } + + /// Transfers the given amount of the meat token from the given sender to the given receiver. + public entry fun transfer_meat(from: &signer, to: address, amount: u64) { + transfer_food(from, object::address_to_object(meat_token_address()), to, amount); + } + + public entry fun transfer_food(from: &signer, food: Object, to: address, amount: u64) { + let metadata = object::convert(food); + primary_fungible_store::transfer(from, metadata, to, amount); + } + + public(friend) fun burn_food(from: &signer, food: Object, amount: u64) acquires FoodToken { + let metadata = object::convert(food); + let food_addr = object::object_address(&food); + let food_token = borrow_global(food_addr); + let from_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(from), metadata); + fungible_asset::burn_from(&food_token.fungible_asset_burn_ref, from_store, amount); + } + + inline fun authorized_borrow(creator: &signer, token: &Object): &FoodToken { + let token_address = object::object_address(token); + assert!( + exists(token_address), + error::not_found(ETOKEN_DOES_NOT_EXIST), + ); + + assert!( + token::creator(*token) == signer::address_of(creator), + error::permission_denied(ENOT_CREATOR), + ); + borrow_global(token_address) + } + + #[test_only] + public fun init_module_for_test(creator: &signer) { + init_module(creator); + } + + #[test(creator = @token_objects, user1 = @0x456, user2 = @0x789)] + public fun test_food(creator: &signer, user1: &signer, user2: &signer) acquires FoodToken { + // This test assumes that the creator's address is equal to @token_objects. + assert!(signer::address_of(creator) == @token_objects, 0); + + // --------------------------------------------------------------------- + // Creator creates the collection, and mints corn and meat tokens in it. + // --------------------------------------------------------------------- + init_module(creator); + + // ------------------------------------------- + // Creator mints and sends 100 corns to User1. + // ------------------------------------------- + let user1_addr = signer::address_of(user1); + mint_corn(creator, user1_addr, 100); + + let corn_token = object::address_to_object(corn_token_address()); + // Assert that the user1 has 100 corns. + assert!(food_balance(user1_addr, corn_token) == 100, 0); + + // ------------------------------------------- + // Creator mints and sends 200 meats to User2. + // ------------------------------------------- + let user2_addr = signer::address_of(user2); + mint_meat(creator, user2_addr, 200); + let meat_token = object::address_to_object(meat_token_address()); + // Assert that the user2 has 200 meats. + assert!(food_balance(user2_addr, meat_token) == 200, 0); + + // ------------------------------ + // User1 sends 10 corns to User2. + // ------------------------------ + transfer_corn(user1, user2_addr, 10); + // Assert that the user1 has 90 corns. + assert!(food_balance(user1_addr, corn_token) == 90, 0); + // Assert that the user2 has 10 corns. + assert!(food_balance(user2_addr, corn_token) == 10, 0); + + // ------------------------------ + // User2 sends 20 meats to User1. + // ------------------------------ + transfer_meat(user2, user1_addr, 20); + // Assert that the user1 has 20 meats. + assert!(food_balance(user1_addr, meat_token) == 20, 0); + // Assert that the user2 has 180 meats. + assert!(food_balance(user2_addr, meat_token) == 180, 0); + } +} diff --git a/aptos-move/move-examples/token_objects/knight/sources/knight.move b/aptos-move/move-examples/token_objects/knight/sources/knight.move new file mode 100644 index 0000000000000..9501b4572457d --- /dev/null +++ b/aptos-move/move-examples/token_objects/knight/sources/knight.move @@ -0,0 +1,278 @@ +/// This module implements the knight token (non-fungible token) including the +/// functions create the collection and the knight tokens, and the function to feed a +/// knight token with food tokens to increase the knight's health point. +module token_objects::knight { + use std::option; + use std::string::{Self, String}; + use aptos_framework::event; + use aptos_framework::object::{Self, Object}; + use aptos_token_objects::collection; + use aptos_token_objects::property_map; + use aptos_token_objects::token; + use token_objects::food::{Self, FoodToken}; + + /// The token does not exist + const ETOKEN_DOES_NOT_EXIST: u64 = 1; + /// The provided signer is not the creator + const ENOT_CREATOR: u64 = 2; + /// Attempted to mutate an immutable field + const EFIELD_NOT_MUTABLE: u64 = 3; + /// Attempted to burn a non-burnable token + const ETOKEN_NOT_BURNABLE: u64 = 4; + /// Attempted to mutate a property map that is not mutable + const EPROPERTIES_NOT_MUTABLE: u64 = 5; + // The collection does not exist + const ECOLLECTION_DOES_NOT_EXIST: u64 = 6; + + /// The knight token collection name + const KNIGHT_COLLECTION_NAME: vector = b"Knight Collection Name"; + /// The knight collection description + const KNIGHT_COLLECTION_DESCRIPTION: vector = b"Knight Collection Description"; + /// The knight collection URI + const KNIGHT_COLLECTION_URI: vector = b"https://knight.collection.uri"; + + /// Property names + const CONDITION_PROPERTY_NAME: vector = b"Condition"; + const HEALTH_POINT_PROPERTY_NAME: vector = b"Health Point"; + + /// The condition of a knight + const CONDITION_HUNGRY: vector = b"Hungry"; + const CONDITION_GOOD: vector = b"Good"; + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// Knight token + struct KnightToken has key { + /// Used to mutate the token uri + mutator_ref: token::MutatorRef, + /// Used to mutate properties + property_mutator_ref: property_map::MutatorRef, + /// Used to emit HealthUpdateEvent + health_update_events: event::EventHandle, + /// the base URI of the token + base_uri: String, + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// The knight's health point + struct HealthPoint has key { + value: u64, + } + + /// The health update event + struct HealthUpdateEvent has drop, store { + old_health: u64, + new_health: u64, + } + + /// Initializes the module, creating the knight token collection. + fun init_module(sender: &signer) { + // Create a collection for knight tokens. + create_knight_collection(sender); + } + + #[view] + /// Returns the knight health point of the token + public fun health_point(token: Object): u64 acquires HealthPoint { + let health = borrow_global(object::object_address(&token)); + health.value + } + + #[view] + /// Returns the knight token address by name + public fun knight_token_address(knight_token_name: String): address { + token::create_token_address(&@token_objects, &string::utf8(KNIGHT_COLLECTION_NAME), &knight_token_name) + } + + /// Creates the knight collection. This function creates a collection with unlimited supply using + /// the module constants for description, name, and URI, defined above. The royalty configuration + /// is skipped in this collection for simplicity. + fun create_knight_collection(creator: &signer) { + // Constructs the strings from the bytes. + let description = string::utf8(KNIGHT_COLLECTION_DESCRIPTION); + let name = string::utf8(KNIGHT_COLLECTION_NAME); + let uri = string::utf8(KNIGHT_COLLECTION_URI); + + // Creates the collection with unlimited supply and without establishing any royalty configuration. + collection::create_unlimited_collection( + creator, + description, + name, + option::none(), + uri, + ); + } + + /// Mints an knight token. This function mints a new knight token and transfers it to the + /// `soul_bound_to` address. The token is minted with health point 0 and condition Hungry. + public entry fun mint_knight( + creator: &signer, + description: String, + name: String, + base_uri: String, + receiver: address, + ) { + // The collection name is used to locate the collection object and to create a new token object. + let collection = string::utf8(KNIGHT_COLLECTION_NAME); + // Creates the knight token, and get the constructor ref of the token. The constructor ref + // is used to generate the refs of the token. + let uri = base_uri; + string::append(&mut uri, string::utf8(CONDITION_HUNGRY)); + let constructor_ref = token::create_named_token( + creator, + collection, + description, + name, + option::none(), + uri, + ); + + // Generates the object signer and the refs. The object signer is used to publish a resource + // (e.g., HealthPoint) under the token object address. The refs are used to manage the token. + let object_signer = object::generate_signer(&constructor_ref); + let transfer_ref = object::generate_transfer_ref(&constructor_ref); + let mutator_ref = token::generate_mutator_ref(&constructor_ref); + let property_mutator_ref = property_map::generate_mutator_ref(&constructor_ref); + + // Transfers the token to the `soul_bound_to` address + let linear_transfer_ref = object::generate_linear_transfer_ref(&transfer_ref); + object::transfer_with_ref(linear_transfer_ref, receiver); + + // Initializes the knight health point as 0 + move_to(&object_signer, HealthPoint { value: 1 }); + + // Initialize the property map and the knight condition as Hungry + let properties = property_map::prepare_input(vector[], vector[], vector[]); + property_map::init(&constructor_ref, properties); + property_map::add_typed( + &property_mutator_ref, + string::utf8(CONDITION_PROPERTY_NAME), + string::utf8(CONDITION_HUNGRY), + ); + // Although the health point is stored in the HealthPoint resource, it is also duplicated + // and stored in the property map to be recognized as a property by the wallet. + property_map::add_typed( + &property_mutator_ref, + string::utf8(HEALTH_POINT_PROPERTY_NAME), + 1, + ); + + // Publishes the KnightToken resource with the refs and the event handle for `HealthUpdateEvent`. + let knight_token = KnightToken { + mutator_ref, + property_mutator_ref, + health_update_events: object::new_event_handle(&object_signer), + base_uri + }; + move_to(&object_signer, knight_token); + } + + public entry fun feed_corn(from: &signer, to: Object, amount: u64) acquires HealthPoint, KnightToken { + let corn_token = object::address_to_object(food::corn_token_address()); + feed_food(from, corn_token, to, amount); + } + + public entry fun feed_meat(from: &signer, to: Object, amount: u64) acquires HealthPoint, KnightToken { + let meat_token = object::address_to_object(food::meat_token_address()); + feed_food(from, meat_token, to, amount); + } + + public entry fun feed_food(from: &signer, food_token: Object, to: Object, amount: u64) acquires HealthPoint, KnightToken { + let knight_token_address = object::object_address(&to); + food::burn_food(from, food_token, amount); + + let restoration_amount = food::restoration_value(food_token) * amount; + let health_point = borrow_global_mut(object::object_address(&to)); + let old_health_point = health_point.value; + let new_health_point = old_health_point + restoration_amount; + health_point.value = new_health_point; + + let knight = borrow_global_mut(knight_token_address); + // Gets `property_mutator_ref` to update the health point and condition in the property map. + let property_mutator_ref = &knight.property_mutator_ref; + // Updates the health point in the property map. + property_map::update_typed(property_mutator_ref, &string::utf8(HEALTH_POINT_PROPERTY_NAME), new_health_point); + + event::emit_event( + &mut knight.health_update_events, + HealthUpdateEvent { + old_health: old_health_point, + new_health: new_health_point, + } + ); + + // `new_condition` is determined based on the new health point. + let new_condition = if (new_health_point <= 20) { + CONDITION_HUNGRY + } else { + CONDITION_GOOD + }; + // Updates the condition in the property map. + property_map::update_typed(property_mutator_ref, &string::utf8(CONDITION_PROPERTY_NAME), string::utf8(new_condition)); + + // Updates the token URI based on the new condition. + let uri = knight.base_uri; + string::append(&mut uri, string::utf8(new_condition)); + token::set_uri(&knight.mutator_ref, uri); + } + + #[test_only] + use std::signer; + + #[test(creator = @token_objects, user1 = @0x456)] + public fun test_knight(creator: &signer, user1: &signer) acquires HealthPoint, KnightToken { + // This test assumes that the creator's address is equal to @token_objects. + assert!(signer::address_of(creator) == @token_objects, 0); + + // --------------------------------------------------------------------- + // Creator creates the collection, and mints corn and meat tokens in it. + // --------------------------------------------------------------------- + food::init_module_for_test(creator); + init_module(creator); + + // ------------------------------------------------------- + // Creator mints and sends 90 corns and 20 meats to User1. + // ------------------------------------------------------- + let user1_addr = signer::address_of(user1); + food::mint_corn(creator, user1_addr, 90); + food::mint_meat(creator, user1_addr, 20); + + // --------------------------------------- + // Creator mints a knight token for User1. + // --------------------------------------- + let token_name = string::utf8(b"Knight Token #1"); + let token_description = string::utf8(b"Knight Token #1 Description"); + let token_uri = string::utf8(b"Knight Token #1 URI"); + let user1_addr = signer::address_of(user1); + // Creates the knight token for User1. + mint_knight( + creator, + token_description, + token_name, + token_uri, + user1_addr, + ); + let token_address = knight_token_address(token_name); + let knight_token = object::address_to_object(token_address); + + // Asserts that the owner of the token is User1. + assert!(object::owner(knight_token) == user1_addr, 1); + // Asserts that the health point of the token is 1. + assert!(health_point(knight_token) == 1, 2); + + let corn_token = object::address_to_object(food::corn_token_address()); + let old_corn_balance = food::food_balance(user1_addr, corn_token); + feed_food(user1, corn_token, knight_token, 3); + // Asserts that the corn balance decreases by 3. + assert!(food::food_balance(user1_addr, corn_token) == old_corn_balance - 3, 0); + // Asserts that the health point increases by 15 (= amount * restoration_value = 3 * 5). + assert!(health_point(knight_token) == 16, 2); + + let meat_token = object::address_to_object(food::meat_token_address()); + let old_meat_balance = food::food_balance(user1_addr, meat_token); + feed_food(user1, meat_token, knight_token, 2); + // Asserts that the corn balance decreases by 3. + assert!(food::food_balance(user1_addr, meat_token) == old_meat_balance - 2, 0); + // Asserts that the health point increases by 40 (= amount * restoration_value = 2 * 20). + assert!(health_point(knight_token) == 56, 3); + } +}