diff --git a/aptos-move/framework/aptos-token-objects/doc/property_map.md b/aptos-move/framework/aptos-token-objects/doc/property_map.md index 048b4de8b9b07..5983af879154c 100644 --- a/aptos-move/framework/aptos-token-objects/doc/property_map.md +++ b/aptos-move/framework/aptos-token-objects/doc/property_map.md @@ -44,6 +44,8 @@ represent types and storing values in bcs format. - [Function `update_typed`](#0x4_property_map_update_typed) - [Function `update_internal`](#0x4_property_map_update_internal) - [Function `remove`](#0x4_property_map_remove) +- [Function `exists_at`](#0x4_property_map_exists_at) +- [Function `delete`](#0x4_property_map_delete) - [Function `assert_end_to_end_input`](#0x4_property_map_assert_end_to_end_input) @@ -1262,6 +1264,54 @@ Removes a property from the map, ensuring that it does in fact exist + + + + +## Function `exists_at` + + + +
public(friend) fun exists_at(addr: address): bool
+
+ + + +
+Implementation + + +
public(friend) fun exists_at(addr: address): bool {
+    exists<PropertyMap>(addr)
+}
+
+ + + +
+ + + +## Function `delete` + + + +
public(friend) fun delete(addr: address): property_map::PropertyMap
+
+ + + +
+Implementation + + +
public(friend) fun delete(addr: address): PropertyMap acquires PropertyMap {
+    move_from<PropertyMap>(addr)
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-token-objects/doc/royalty.md b/aptos-move/framework/aptos-token-objects/doc/royalty.md index 84cabeecbfaef..4ca4911c23bd3 100644 --- a/aptos-move/framework/aptos-token-objects/doc/royalty.md +++ b/aptos-move/framework/aptos-token-objects/doc/royalty.md @@ -41,7 +41,7 @@ by (numerator / denominator) * 100%
#[resource_group_member(#[group = 0x1::object::ObjectGroup])]
-struct Royalty has copy, drop, key
+struct Royalty has copy, drop, store, key
 
@@ -277,7 +277,7 @@ Creates a new royalty, verifying that it is a valid percentage -
public(friend) fun delete(addr: address)
+
public(friend) fun delete(addr: address): royalty::Royalty
 
@@ -286,9 +286,9 @@ Creates a new royalty, verifying that it is a valid percentage Implementation -
public(friend) fun delete(addr: address) acquires Royalty {
+
public(friend) fun delete(addr: address): Royalty acquires Royalty {
     assert!(exists<Royalty>(addr), error::not_found(EROYALTY_DOES_NOT_EXIST));
-    move_from<Royalty>(addr);
+    move_from<Royalty>(addr)
 }
 
diff --git a/aptos-move/framework/aptos-token-objects/doc/token.md b/aptos-move/framework/aptos-token-objects/doc/token.md index f850767b9a17b..f54896d00b86f 100644 --- a/aptos-move/framework/aptos-token-objects/doc/token.md +++ b/aptos-move/framework/aptos-token-objects/doc/token.md @@ -15,8 +15,11 @@ token are: - [Resource `ConcurrentTokenIdentifiers`](#0x4_token_ConcurrentTokenIdentifiers) - [Struct `BurnRef`](#0x4_token_BurnRef) - [Struct `MutatorRef`](#0x4_token_MutatorRef) +- [Struct `CompressionRef`](#0x4_token_CompressionRef) - [Struct `MutationEvent`](#0x4_token_MutationEvent) - [Struct `Mutation`](#0x4_token_Mutation) +- [Struct `CompressedToken`](#0x4_token_CompressedToken) +- [Resource `CompressedCollection`](#0x4_token_CompressedCollection) - [Constants](#@Constants_0) - [Function `create_common`](#0x4_token_create_common) - [Function `create_common_with_collection`](#0x4_token_create_common_with_collection) @@ -41,6 +44,8 @@ token are: - [Function `generate_mutator_ref`](#0x4_token_generate_mutator_ref) - [Function `generate_burn_ref`](#0x4_token_generate_burn_ref) - [Function `address_from_burn_ref`](#0x4_token_address_from_burn_ref) +- [Function `generate_compression_ref`](#0x4_token_generate_compression_ref) +- [Function `address_from_compression_ref`](#0x4_token_address_from_compression_ref) - [Function `borrow`](#0x4_token_borrow) - [Function `creator`](#0x4_token_creator) - [Function `collection_name`](#0x4_token_collection_name) @@ -55,11 +60,16 @@ token are: - [Function `set_description`](#0x4_token_set_description) - [Function `set_name`](#0x4_token_set_name) - [Function `set_uri`](#0x4_token_set_uri) +- [Function `collection_enable_compressed_tokens`](#0x4_token_collection_enable_compressed_tokens) +- [Function `compress_token`](#0x4_token_compress_token) +- [Function `decompress_token`](#0x4_token_decompress_token)
use 0x1::aggregator_v2;
+use 0x1::any_map;
 use 0x1::error;
 use 0x1::event;
+use 0x1::external_object;
 use 0x1::features;
 use 0x1::object;
 use 0x1::option;
@@ -67,6 +77,7 @@ token are:
 use 0x1::string;
 use 0x1::vector;
 use 0x4::collection;
+use 0x4::property_map;
 use 0x4::royalty;
 
@@ -104,6 +115,7 @@ Represents the common fields to all tokens. Was populated until concurrent_token_v2_enabled feature flag was enabled. Unique identifier within the collection, optional, 0 means unassigned + DEPRECATED
description: string::String @@ -120,6 +132,7 @@ Represents the common fields to all tokens. The name of the token, which should be unique within the collection; the length of name should be smaller than 128, characters, eg: "Aptos Animal #1234" + DEPRECATED
uri: string::String @@ -273,6 +286,33 @@ This enables mutating description and URI by higher level services. + + + + +## Struct `CompressionRef` + + + +
struct CompressionRef has drop, store
+
+ + + +
+Fields + + +
+
+inner: object::DeleteAndRecreateRef +
+
+ +
+
+ +
@@ -360,6 +400,88 @@ directly understand the behavior in a writeset. + + + + +## Struct `CompressedToken` + + + +
struct CompressedToken has drop, store
+
+ + + +
+Fields + + +
+
+collection: object::Object<collection::Collection> +
+
+ The collection from which this token resides. +
+
+index: u64 +
+
+ Unique identifier within the collection, optional, 0 means unassigned +
+
+description: string::String +
+
+ A brief description of the token. +
+
+name: string::String +
+
+ The name of the token, which should be unique within the collection; the length of name + should be smaller than 128, characters, eg: "Aptos Animal #1234" +
+
+uri: string::String +
+
+ The Uniform Resource Identifier (uri) pointing to the JSON file stored in off-chain + storage; the URL length will likely need a maximum any suggestions? +
+
+ + +
+ + + +## Resource `CompressedCollection` + +Represents the common fields for a collection. + + +
#[resource_group_member(#[group = 0x1::object::ObjectGroup])]
+struct CompressedCollection has key
+
+ + + +
+Fields + + +
+
+dummy_field: bool +
+
+ +
+
+ +
@@ -425,6 +547,24 @@ The description is over the maximum length + + + + +
const ECOLLECTION_ALREADY_COMPRESSED: u64 = 30;
+
+ + + + + + + +
const ECOLLECTION_NOT_COMPRESSED: u64 = 31;
+
+ + + The field being changed is not mutable @@ -1456,6 +1596,56 @@ Extracts the tokens address from a BurnRef. + + + + +## Function `generate_compression_ref` + + + +
public fun generate_compression_ref(ref: &object::ConstructorRef): token::CompressionRef
+
+ + + +
+Implementation + + +
public fun generate_compression_ref(ref: &ConstructorRef): CompressionRef {
+    CompressionRef {
+        inner: object::generate_delete_and_recreate_ref(ref),
+    }
+}
+
+ + + +
+ + + +## Function `address_from_compression_ref` + + + +
public fun address_from_compression_ref(ref: &token::CompressionRef): address
+
+ + + +
+Implementation + + +
public fun address_from_compression_ref(ref: &CompressionRef): address {
+    ref.inner.address_from_delete_and_recreate_ref()
+}
+
+ + +
@@ -1768,7 +1958,7 @@ as that would prohibit transactions to be executed in parallel. }; if (royalty::exists_at(addr)) { - royalty::delete(addr) + royalty::delete(addr); }; let Token { @@ -1938,6 +2128,181 @@ as that would prohibit transactions to be executed in parallel. + + + + +## Function `collection_enable_compressed_tokens` + + + +
public fun collection_enable_compressed_tokens(constructor_ref: object::ConstructorRef)
+
+ + + +
+Implementation + + +
public fun collection_enable_compressed_tokens(
+    constructor_ref: ConstructorRef, // TODO - should we support with ExtendRef with existing collections?
+) {
+    let object_signer = object::generate_signer(&constructor_ref);
+
+    let compressed_collection = CompressedCollection {
+    };
+    assert!(!exists<CompressedCollection>(signer::address_of(&object_signer)), error::invalid_argument(ECOLLECTION_ALREADY_COMPRESSED));
+    move_to(&object_signer, compressed_collection);
+}
+
+ + + +
+ + + +## Function `compress_token` + + + +
public fun compress_token<P: drop, store>(compression_ref: token::CompressionRef, resources: any_map::AnyMap, mut_permission: P)
+
+ + + +
+Implementation + + +
public fun compress_token<P: drop + store>(
+    // TODO: should we gate it to collection creator? do a permission/ref? anyone? owner of the token only?
+    // creator: &signer,
+    compression_ref: CompressionRef,
+    resources: AnyMap,
+    mut_permission: P,
+) acquires Token, TokenIdentifiers {
+    let object_addr = object::address_from_delete_and_recreate_ref(&compression_ref.inner);
+
+    let Token {
+        collection,
+        index: deprecated_index,
+        description,
+        name: deprecated_name,
+        uri,
+        mutation_events,
+    } = move_from<Token>(object_addr);
+
+    // assert!(object::owner(collection) == signer::address_of(creator), error::unauthenticated(ENOT_OWNER));
+    assert!(exists<CompressedCollection>(object::object_address(&collection)), error::invalid_argument(ECOLLECTION_NOT_COMPRESSED));
+
+    event::destroy_handle(mutation_events);
+
+    let (index, name) = if (exists<TokenIdentifiers>(object_addr)) {
+        let TokenIdentifiers {
+            index,
+            name,
+        } = move_from<TokenIdentifiers>(object_addr);
+        (aggregator_v2::read_snapshot(&index), aggregator_v2::read_derived_string(&name))
+    } else {
+        (deprecated_index, deprecated_name)
+    };
+
+    let compressed_token = CompressedToken {
+        collection,
+        index,
+        description,
+        name,
+        uri,
+    };
+
+    resources.add(compressed_token);
+
+    if (royalty::exists_at(object_addr)) {
+        resources.add(royalty::delete(object_addr));
+    };
+
+    if (property_map::exists_at(object_addr)) {
+        resources.add(property_map::delete(object_addr));
+    };
+
+    let CompressionRef { inner } = compression_ref;
+    external_object::move_existing_object_to_external_storage(inner, resources, mut_permission)
+}
+
+ + + +
+ + + +## Function `decompress_token` + + + +
public fun decompress_token<P: drop, store>(external_bytes: vector<u8>, mut_permission: P): (object::ConstructorRef, external_object::MovingToStateObject)
+
+ + + +
+Implementation + + +
public fun decompress_token<P: drop + store>(
+    // TODO: should we gate it to collection creator? do a permission/ref? anyone? owner of the token only?
+    // creator: &signer
+    external_bytes: vector<u8>,
+    mut_permission: P,
+): (ConstructorRef, MovingToStateObject) {
+    let (constructor_ref, resources_to_move) = external_object::move_external_object_to_state(external_bytes, mut_permission);
+
+    let object_signer = object::generate_signer(&constructor_ref);
+
+    let CompressedToken {
+        collection,
+        index,
+        description,
+        name,
+        uri,
+    } = resources_to_move.get_resources_mut().remove();
+
+    let deprecated_index = 0;
+    let deprecated_name = string::utf8(b"");
+
+    let token_concurrent = TokenIdentifiers {
+        index: aggregator_v2::create_snapshot(index),
+        name: aggregator_v2::create_derived_string(name),
+    };
+    move_to(&object_signer, token_concurrent);
+
+    let token = Token {
+        collection,
+        index: deprecated_index,
+        description,
+        name: deprecated_name,
+        uri,
+        mutation_events: object::new_event_handle(&object_signer),
+    };
+    move_to(&object_signer, token);
+
+    let royalty = any_map::remove_if_present<Royalty>(resources_to_move.get_resources_mut());
+    if (option::is_some(&royalty)) {
+        royalty::init(&constructor_ref, option::extract(&mut royalty))
+    };
+    let property_map = any_map::remove_if_present<PropertyMap>(resources_to_move.get_resources_mut());
+    if (option::is_some(&property_map)) {
+        property_map::init(&constructor_ref, option::extract(&mut property_map))
+    };
+
+    (constructor_ref, resources_to_move)
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-token-objects/sources/property_map.move b/aptos-move/framework/aptos-token-objects/sources/property_map.move index e71c120fa2456..2727f1a1f70b6 100644 --- a/aptos-move/framework/aptos-token-objects/sources/property_map.move +++ b/aptos-move/framework/aptos-token-objects/sources/property_map.move @@ -11,6 +11,8 @@ module aptos_token_objects::property_map { use aptos_std::type_info; use aptos_framework::object::{Self, ConstructorRef, Object, ExtendRef, ObjectCore}; + friend aptos_token_objects::token; + // Errors /// The property map does not exist const EPROPERTY_MAP_DOES_NOT_EXIST: u64 = 1; @@ -347,6 +349,14 @@ module aptos_token_objects::property_map { simple_map::remove(&mut property_map.inner, key); } + public(friend) fun exists_at(addr: address): bool { + exists(addr) + } + + public(friend) fun delete(addr: address): PropertyMap acquires PropertyMap { + move_from(addr) + } + // Tests #[test(creator = @0x123)] fun test_end_to_end(creator: &signer) acquires PropertyMap { diff --git a/aptos-move/framework/aptos-token-objects/sources/royalty.move b/aptos-move/framework/aptos-token-objects/sources/royalty.move index e3068b68ad4ff..6bdcde96d6537 100644 --- a/aptos-move/framework/aptos-token-objects/sources/royalty.move +++ b/aptos-move/framework/aptos-token-objects/sources/royalty.move @@ -20,7 +20,7 @@ module aptos_token_objects::royalty { /// /// Royalties are optional for a collection. Royalty percentage is calculated /// by (numerator / denominator) * 100% - struct Royalty has copy, drop, key { + struct Royalty has copy, drop, key, store { numerator: u64, denominator: u64, /// The recipient of royalty payments. See the `shared_account` for how to handle multiple @@ -66,9 +66,9 @@ module aptos_token_objects::royalty { exists(addr) } - public(friend) fun delete(addr: address) acquires Royalty { + public(friend) fun delete(addr: address): Royalty acquires Royalty { assert!(exists(addr), error::not_found(EROYALTY_DOES_NOT_EXIST)); - move_from(addr); + move_from(addr) } // Accessors diff --git a/aptos-move/framework/aptos-token-objects/sources/token.move b/aptos-move/framework/aptos-token-objects/sources/token.move index 1128114036aa5..193ece2d52839 100644 --- a/aptos-move/framework/aptos-token-objects/sources/token.move +++ b/aptos-move/framework/aptos-token-objects/sources/token.move @@ -11,11 +11,14 @@ module aptos_token_objects::token { use std::string::{Self, String}; use std::signer; use std::vector; + use aptos_std::any_map::{Self, AnyMap}; use aptos_framework::aggregator_v2::{Self, AggregatorSnapshot, DerivedStringSnapshot}; + use aptos_framework::external_object::{Self, MovingToStateObject}; use aptos_framework::event; use aptos_framework::object::{Self, ConstructorRef, Object}; use aptos_token_objects::collection::{Self, Collection}; use aptos_token_objects::royalty::{Self, Royalty}; + use aptos_token_objects::property_map::{Self, PropertyMap}; #[test_only] use aptos_framework::object::ExtendRef; @@ -53,8 +56,8 @@ module aptos_token_objects::token { /// Was populated until concurrent_token_v2_enabled feature flag was enabled. /// /// Unique identifier within the collection, optional, 0 means unassigned + /// DEPRECATED index: u64, - // DEPRECATED /// A brief description of the token. description: String, /// Deprecated in favor of `name` inside TokenIdentifiers. @@ -62,8 +65,8 @@ module aptos_token_objects::token { /// /// The name of the token, which should be unique within the collection; the length of name /// should be smaller than 128, characters, eg: "Aptos Animal #1234" + /// DEPRECATED name: String, - // DEPRECATED /// The Uniform Resource Identifier (uri) pointing to the JSON file stored in off-chain /// storage; the URL length will likely need a maximum any suggestions? uri: String, @@ -103,6 +106,10 @@ module aptos_token_objects::token { self: address, } + struct CompressionRef has drop, store { + inner: object::DeleteAndRecreateRef, + } + /// Contains the mutated fields name. This makes the life of indexers easier, so that they can /// directly understand the behavior in a writeset. struct MutationEvent has drop, store { @@ -624,6 +631,16 @@ module aptos_token_objects::token { } } + public fun generate_compression_ref(ref: &ConstructorRef): CompressionRef { + CompressionRef { + inner: object::generate_delete_and_recreate_ref(ref), + } + } + + public fun address_from_compression_ref(ref: &CompressionRef): address { + ref.inner.address_from_delete_and_recreate_ref() + } + // Accessors inline fun borrow(token: &Object): &Token acquires Token { @@ -749,7 +766,7 @@ module aptos_token_objects::token { }; if (royalty::exists_at(addr)) { - royalty::delete(addr) + royalty::delete(addr); }; let Token { @@ -1434,4 +1451,143 @@ module aptos_token_objects::token { let collection_address = signer::address_of(&object::generate_signer_for_extending(extend_ref)); object::address_to_object(collection_address) } + + + const ECOLLECTION_ALREADY_COMPRESSED: u64 = 30; + const ECOLLECTION_NOT_COMPRESSED: u64 = 31; + + struct CompressedToken has drop, store { + /// The collection from which this token resides. + collection: Object, + /// Unique identifier within the collection, optional, 0 means unassigned + index: u64, // TODO change to AggregatorSnapshot + /// A brief description of the token. + description: String, + /// The name of the token, which should be unique within the collection; the length of name + /// should be smaller than 128, characters, eg: "Aptos Animal #1234" + name: String, // TODO change to AggregatorSnapshot + /// The Uniform Resource Identifier (uri) pointing to the JSON file stored in off-chain + /// storage; the URL length will likely need a maximum any suggestions? + uri: String, + } + + #[resource_group_member(group = aptos_framework::object::ObjectGroup)] + /// Represents the common fields for a collection. + struct CompressedCollection has key { + } + + public fun collection_enable_compressed_tokens( + constructor_ref: ConstructorRef, // TODO - should we support with ExtendRef with existing collections? + ) { + let object_signer = object::generate_signer(&constructor_ref); + + let compressed_collection = CompressedCollection { + }; + assert!(!exists(signer::address_of(&object_signer)), error::invalid_argument(ECOLLECTION_ALREADY_COMPRESSED)); + move_to(&object_signer, compressed_collection); + } + + public fun compress_token( + // TODO: should we gate it to collection creator? do a permission/ref? anyone? owner of the token only? + // creator: &signer, + compression_ref: CompressionRef, + resources: AnyMap, + mut_permission: P, + ) acquires Token, TokenIdentifiers { + let object_addr = object::address_from_delete_and_recreate_ref(&compression_ref.inner); + + let Token { + collection, + index: deprecated_index, + description, + name: deprecated_name, + uri, + mutation_events, + } = move_from(object_addr); + + // assert!(object::owner(collection) == signer::address_of(creator), error::unauthenticated(ENOT_OWNER)); + assert!(exists(object::object_address(&collection)), error::invalid_argument(ECOLLECTION_NOT_COMPRESSED)); + + event::destroy_handle(mutation_events); + + let (index, name) = if (exists(object_addr)) { + let TokenIdentifiers { + index, + name, + } = move_from(object_addr); + (aggregator_v2::read_snapshot(&index), aggregator_v2::read_derived_string(&name)) + } else { + (deprecated_index, deprecated_name) + }; + + let compressed_token = CompressedToken { + collection, + index, + description, + name, + uri, + }; + + resources.add(compressed_token); + + if (royalty::exists_at(object_addr)) { + resources.add(royalty::delete(object_addr)); + }; + + if (property_map::exists_at(object_addr)) { + resources.add(property_map::delete(object_addr)); + }; + + let CompressionRef { inner } = compression_ref; + external_object::move_existing_object_to_external_storage(inner, resources, mut_permission) + } + + public fun decompress_token( + // TODO: should we gate it to collection creator? do a permission/ref? anyone? owner of the token only? + // creator: &signer + external_bytes: vector, + mut_permission: P, + ): (ConstructorRef, MovingToStateObject) { + let (constructor_ref, resources_to_move) = external_object::move_external_object_to_state(external_bytes, mut_permission); + + let object_signer = object::generate_signer(&constructor_ref); + + let CompressedToken { + collection, + index, + description, + name, + uri, + } = resources_to_move.get_resources_mut().remove(); + + let deprecated_index = 0; + let deprecated_name = string::utf8(b""); + + let token_concurrent = TokenIdentifiers { + index: aggregator_v2::create_snapshot(index), + name: aggregator_v2::create_derived_string(name), + }; + move_to(&object_signer, token_concurrent); + + let token = Token { + collection, + index: deprecated_index, + description, + name: deprecated_name, + uri, + mutation_events: object::new_event_handle(&object_signer), + }; + move_to(&object_signer, token); + + let royalty = any_map::remove_if_present(resources_to_move.get_resources_mut()); + if (option::is_some(&royalty)) { + royalty::init(&constructor_ref, option::extract(&mut royalty)) + }; + let property_map = any_map::remove_if_present(resources_to_move.get_resources_mut()); + if (option::is_some(&property_map)) { + property_map::init(&constructor_ref, option::extract(&mut property_map)) + }; + + (constructor_ref, resources_to_move) + } }