diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index 4b3635f089fb4..5f18309e63d2c 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -39,5 +39,6 @@ mod token_event_store; mod token_objects; mod transaction_fee; mod type_too_large; +mod upgradeable_resource_account_package; mod vector_numeric_address; mod vote; diff --git a/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs new file mode 100644 index 0000000000000..b252c53e2cafc --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/upgradeable_resource_account_package.rs @@ -0,0 +1,154 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::{assert_success, tests::common, MoveHarness}; +use aptos_package_builder::PackageBuilder; +use aptos_types::account_address::{create_resource_address, AccountAddress}; +use aptos_framework::natives::code::{PackageMetadata, UpgradePolicy}; +use move_core_types::parser::parse_struct_tag; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Eq, PartialEq)] +struct SomeResource { + value: u64, +} + +fn custom_build_helper( + deployer_address: AccountAddress, + resource_address: AccountAddress, + package_manager_code: &String, + basic_contract_code: &String, +) -> (PackageMetadata, Vec>) { + // add the named addresses for `deployer` and `upgradeable_resource_account_package` + let mut build_options = aptos_framework::BuildOptions::default(); + build_options + .named_addresses + .insert("deployer".to_string(), deployer_address); + build_options + .named_addresses + .insert("upgradeable_resource_account_package".to_string(), resource_address); + + let mut package_builder = + PackageBuilder::new("Upgradeable Module With Resource Account") + .with_policy(UpgradePolicy::compat()); + package_builder.add_source("package_manager", &package_manager_code); + package_builder.add_source("basic_contract", &basic_contract_code); + package_builder.add_local_dep("AptosFramework", &common::framework_dir_path("aptos-framework").to_string_lossy()); + let pack_dir = package_builder.write_to_temp().unwrap(); + let package = aptos_framework::BuiltPackage::build( + pack_dir.path().to_owned(), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_code(); + let metadata = package + .extract_metadata() + .expect("extracting package metadata must succeed"); + + (metadata, code) +} + +#[test] +fn code_upgrading_using_resource_account() { + let mut h = MoveHarness::new(); + + let deployer = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + let resource_address = create_resource_address(*deployer.address(), &[]); + + // get contract code from file + let package_manager_code = + std::fs::read_to_string( + &common::test_dir_path("../../../move-examples/upgradeable_resource_account_package/sources/package_manager.move") + ).unwrap(); + let basic_contract_code = + std::fs::read_to_string( + &common::test_dir_path("../../../move-examples/upgradeable_resource_account_package/sources/basic_contract.move") + ).unwrap(); + + let (metadata, code) = custom_build_helper( + *deployer.address(), + resource_address, + &package_manager_code, + &basic_contract_code + ); + + // create the resource account and publish the module under the resource account's address + assert_success!(h.run_transaction_payload( + &deployer, + aptos_cached_packages::aptos_stdlib::resource_account_create_resource_account_and_publish_package( + vec![], + bcs::to_bytes(&metadata).expect("PackageMetadata has BCS"), + code.clone(), + ), + )); + + // run the view function and check the result + let bcs_result = h.execute_view_function( + str::parse(&format!( + "0x{}::basic_contract::upgradeable_function", + resource_address + )).unwrap(), + vec![], + vec![], + ).unwrap().pop().unwrap(); + let result = bcs::from_bytes::(&bcs_result).unwrap(); + + const BEFORE_VALUE: u64 = 9000; + const AFTER_VALUE: u64 = 9001; + // run the view function and check the result + assert_eq!(BEFORE_VALUE, result, "assert view function result {} == {}", result, BEFORE_VALUE); + + let (metadata, code) = custom_build_helper( + *deployer.address(), + resource_address, + &package_manager_code, + &basic_contract_code.replace(&BEFORE_VALUE.to_string(), &AFTER_VALUE.to_string()) + ); + // test upgrading the code + assert_success!(h.run_entry_function( + &deployer, + str::parse(&format!( + "0x{}::package_manager::publish_package", + resource_address + )).unwrap(), + vec![], + vec![ + bcs::to_bytes(&bcs::to_bytes(&metadata).unwrap()).unwrap(), + bcs::to_bytes(&code).unwrap(), + ], + )); + + // run the view function and check the result + let bcs_result = h.execute_view_function( + str::parse(&format!( + "0x{}::basic_contract::upgradeable_function", + resource_address + )).unwrap(), + vec![], + vec![], + ).unwrap().pop().unwrap(); + let result = bcs::from_bytes::(&bcs_result).unwrap(); + assert_eq!(AFTER_VALUE, result, "assert view function result {} == {}", result, AFTER_VALUE); + + // test the `move_to_rseource_account(...)` function by moving SomeResource into the resource + // account + assert_success!(h.run_entry_function( + &deployer, + str::parse(&format!( + "0x{}::basic_contract::move_to_resource_account", + resource_address + )).unwrap(), + vec![], + vec![], + )); + + let some_resource = parse_struct_tag(&format!( + "0x{}::basic_contract::SomeResource", + resource_address + )).unwrap(); + let some_resource_value = h + .read_resource::(&resource_address, some_resource) + .unwrap(); + assert_eq!(some_resource_value.value, 42, "assert SomeResource.value == 42"); +} diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml b/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml new file mode 100644 index 0000000000000..f76f0632c5e82 --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/Move.toml @@ -0,0 +1,12 @@ +[package] +name = "Upgradeable Module With Resource Account" +version = "0.0.0" +upgrade_policy = "compatible" + +[addresses] +aptos_framework = "0x1" +deployer = "_" +upgradeable_resource_account_package = "_" + +[dependencies] +AptosFramework = { local = "../../framework/aptos-framework" } diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move b/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move new file mode 100644 index 0000000000000..8e2c78acaca2b --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/sources/basic_contract.move @@ -0,0 +1,33 @@ +module upgradeable_resource_account_package::basic_contract { + use upgradeable_resource_account_package::package_manager; + use std::error; + use std::signer; + + struct SomeResource has key { + value: u64, + } + + /// You are not authorized to perform this action. + const ENOT_AUTHORIZED: u64 = 0; + + #[view] + public fun upgradeable_function(): u64 { + 9000 + } + + // An example of doing something with the resource account that requires its signer + public entry fun move_to_resource_account(deployer: &signer) { + // Only the deployer can call this function. + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + + // Do something with the resource account's signer + // For example, a simple `move_to` call + let resource_signer = package_manager::get_signer(); + move_to( + &resource_signer, + SomeResource { + value: 42, + } + ); + } +} diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move b/aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move new file mode 100644 index 0000000000000..abbcd0450f739 --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/sources/package_manager.move @@ -0,0 +1,79 @@ +module upgradeable_resource_account_package::package_manager { + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::resource_account; + use aptos_std::smart_table::{Self, SmartTable}; + use std::string::String; + use aptos_std::code; + use std::error; + use std::signer; + friend upgradeable_resource_account_package::basic_contract; + + /// The signer is not authorized to deploy this module. + const ENOT_AUTHORIZED: u64 = 0; + + /// Stores permission config such as SignerCapability for controlling the resource account. + struct PermissionConfig has key { + /// Required to obtain the resource account signer. + signer_cap: SignerCapability, + /// Track the addresses created by the modules in this package. + addresses: SmartTable, + } + + /// Initialize PermissionConfig to establish control over the resource account. + /// This function is invoked only when this package is deployed the first time. + fun init_module(resource_signer: &signer) { + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @deployer); + move_to(resource_signer, PermissionConfig { + addresses: smart_table::new(), + signer_cap, + }); + } + + public entry fun publish_package( + deployer: &signer, + package_metadata: vector, + code: vector>, + ) acquires PermissionConfig { + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + code::publish_package_txn(&get_signer(), package_metadata, code); + } + + /// Can be called by friended modules to obtain the resource account signer. + public(friend) fun get_signer(): signer acquires PermissionConfig { + let signer_cap = &borrow_global(@upgradeable_resource_account_package).signer_cap; + account::create_signer_with_capability(signer_cap) + } + + /// Can be called by friended modules to keep track of a system address. + public(friend) fun add_named_address(name: String, object: address) acquires PermissionConfig { + let addresses = &mut borrow_global_mut(@upgradeable_resource_account_package).addresses; + smart_table::add(addresses, name, object); + } + + public fun named_address_exists(name: String): bool acquires PermissionConfig { + smart_table::contains(&safe_permission_config().addresses, name) + } + + public fun get_named_address(name: String): address acquires PermissionConfig { + let addresses = &borrow_global(@upgradeable_resource_account_package).addresses; + *smart_table::borrow(addresses, name) + } + + inline fun safe_permission_config(): &PermissionConfig acquires PermissionConfig { + borrow_global(@upgradeable_resource_account_package) + } + + #[test_only] + public fun initialize_for_test(deployer: &signer) { + let deployer_addr = std::signer::address_of(deployer); + if (!exists(deployer_addr)) { + aptos_framework::timestamp::set_time_has_started_for_testing(&account::create_signer_for_test(@0x1)); + + account::create_account_for_test(deployer_addr); + move_to(deployer, PermissionConfig { + addresses: smart_table::new(), + signer_cap: account::create_test_signer_cap(deployer_addr), + }); + }; + } +} diff --git a/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json b/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json new file mode 100644 index 0000000000000..8bd00e7dddf1f --- /dev/null +++ b/aptos-move/move-examples/upgradeable_resource_account_package/upgrade_contract.json @@ -0,0 +1,17 @@ +{ + "function_id": "f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e81::package_manager::publish_package", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x285570677261646561626c65204d6f64756c652057697468205265736f75726365204163636f756e740100000000000000004045463342424543333046313937354134464542394231314335394242453043314439354231424541373838384139343338314241383033334235433037393633cb011f8b08000000000002ff4d8e410ec2201045f7730ac2deaa0770e1c69d1b13e3a231648451899421d0aa8df1ee42db34061202ccffefd501f5036f74068f0d898d90c7708b68082f8ec49e4d978f936defe24089bba8496cb5e6ceb7129e1493655f42ab2a2f09dd9855819dd57df9d0dc046c6d2e9300351a1329254a67c0d07252d798a92f8e8fa1e4bd96602838ee2996073537161b15270385a3819adca751a87396bc21af6d216c0b61f707f808c71a5d19afaa65de337d39d82ce6bb145ff801354bc5571a010000020f7061636b6167655f6d616e61676572ac081f8b08000000000002ffad56db8edb36107dcf574c50c0900a6337058aa2d05eb0c53640f3d21459f7a5454150d2c8662d890a49add709fcef1992bacb4e9c4dfc625b9adb3967389c42a6758e50576bc553e4718e4ca196b54a90f12491756958c5932d5f6314353f58c14bfa52f0f105d0a7d608bc3252b34cf10277526da3a8f18da28f0f98674b7810eb12d53daf782c7261f687abd3bed302a6a6daa451a40bae0c33b6e23e897db6b28f06e1bdb551a25c47d183fb3e1a3091294ebc5029a9a6911c10ff305302cbf44cf262ae45c212591ac513c2e4225c5e5ec26a43a15d54101a4a6980d7662395f8802918092956b9dc83d9d0dbc2c975e17c299436f0facfb72bf6dbdfab3fdebe7bf3cfebdf23a87ff9196ee0d520c18391541654a80aa1b590a575cdc41a749d6c80eb9938904905ae5299e7c417e54668914183cc1741c4d68981bfbad8f73ef486c26e71dfb4485bc93b7c5f0be561c9d870511e0dddd071d1f9faff2ce155342b7639cab02272b72e284f538aab0978a2901b4a1aefdd0bcfa106979c386d14eab3759ed1a0a3ae7deb2cdbb7b73eefa1a7f94d298ce039c936a783f0a2b6cd2af4a66516e423497e9a5adf1b5460569789b1b2095bf4a3dc121659e67bd86d708cc15af876b11c53e44c28ea11238a061e85a210c2304f42d0b5ab67388285ff110e84cbd10c04a0de9af6b83db1440e3e1e697ff2982659c25d53a30aafba2c05b1c18c9c1bcfb8ec2b9b88351a0925ee669205e172e4dba3ea9f1f9a9a1a61ab9a344b0049b1bd63cf3dd09bf65c079d638ba9e3b08fd9cd4d343ce58647f088091dcaebfad7dbdeca0ea0ee4d6fd05884d41ceef0e8cf31c28909655e068d9c51839cc92ce838879b9b5e8125b82147b3bd0bca522c05a6c164b28403b15ca9d1840a669eca60b146d348476ccf902f9deb9862dbe7f7bc841821e179ee0faa1fadf4bb3dad5f3131bc66810f113ad5865545edb43d87d059f32f624984edd83a9731cfafa7aeb7c1dd39d74178d107ed69edce939f584dc56c27ccc61a36032fe85dc367f1b845acc0b8412933e0a0f7da60d19e92931cd27b56d2259db2c632b0ff684636a74cc6ff53d3466d9cf06c7efb494df416b58111c58c9e3c9fe62e76cff2684cd0fba0b3598245d422393a082c112312183e096dc65c508bc59206fc39048caab13703b5b80e169a67c80667d2dfd8c100902f363c51a36df8d362859d4acf13e93b9d8123e2fc38e2c3e7093e875994b4a1a0c37c8a339ac83370e7c0fe3e2887c5fef0afa13580d9cbfbbfa962a25b1f18ad5fcc1a06b33b657a2fb7064e655266b0a21e1ffe3dd32283e0a56fde23d84681c3707ae94e5776bb61d07e5354949d3acffe65b402d2664d6a5213b688a8f782c58931d7a1be7bf5f493bd6dc629274e2dcd33ae7cc55723e776b9e86fbd2f6c15dfba594c97d669f5ae0d7a8349f1e358870198433b930e2f3e01d480aa05ba0d000000000e62617369635f636f6e7472616374e0031f8b08000000000002ff8d52df6bdb400c7ecf5fa131e86c084d36ca602e8115dab1bd2cd0750fdb18d78b4f718eda27ef7ea4cb4afef729e7b317672f3d30be93f4499f3ea921156a84d056562a94ab1a854547c19628645952305eb4b27c901516c54a3a5d8a928cb7b2f4f034013ec13d179e2ea291867ff672803baf8a02ada5539bd395390446abf33670d52fd4e06daa011be9e0017789cae16c651db080f0f6621a6dfb0e3c9bcde01b059016c1900719fc86acfe830a3c418b764db601bfd10eb8354de63cc2b857e7e1e6f3f24e5c7dbdfbb8bcfdf4fde63a668705cc13b1973fb61a1f7fc67b1b56b52e611dcc48147ec7ac59de81fff17d379fbf1e13852b03f85b362dcf85d6a0489b0a1c77cdecf8f6a8fd868922f44243129a8dd2b3f557d0ec02ed1d74fa1d13439edd2ed26b688bc2d37ff3ca14b635edd01670d6e1f323ba4c6f69ea5d24d00742290d7f75dde9d7f77a3e80a47368fd8bac4b57145229aeea04ad8762392c16f0be7f4d216e03af0cda463bc7d98442a3516527b3c8f3348444ee9a9e23d5ab913409fa816caffb142407c409dc279dee6387437c8d7ec82aba5cbc11271b5e1415fae4cdf2cb019c526683e170ce4ed24d47ded1d63f8d5c475b7ff1668cda0faf547d3fd94ffe02f152d31bf303000000000300000000000000000000000000000000000000000000000000000000000000010e4170746f734672616d65776f726b00000000000000000000000000000000000000000000000000000000000000010b4170746f735374646c696200000000000000000000000000000000000000000000000000000000000000010a4d6f76655374646c696200" + }, + { + "type": "hex", + "value": [ + "0xa11ceb0b060000000d010010021014032453047708057f5707d6018b0308e1044006a1054e10ef05630ad2060d0cdf0692010df107040ff507020001010201030104010501060107010800090800070a0700011206000614040200000000000b000100000c020300000d010400000e050100000f0206000010070100061508010200000616090a02020001170b040004180d0e000619010f020704061a0906020200051b050300031c101000021d070100060007000a000b0002080105000108010105010c01060c010103060c0a020a0a0203070b0302090009010900090102060b03020900090109000106090101060802040b03020801050802060c080202060c05010802010b03020900090101030e62617369635f636f6e74726163740f7061636b6167655f6d616e61676572076163636f756e7404636f6465056572726f72107265736f757263655f6163636f756e74067369676e65720b736d6172745f7461626c6506737472696e67105065726d697373696f6e436f6e66696706537472696e67116164645f6e616d65645f61646472657373116765745f6e616d65645f616464726573730a6765745f7369676e65720b696e69745f6d6f64756c65146e616d65645f616464726573735f6578697374730f7075626c6973685f7061636b6167650a7369676e65725f636170105369676e65724361706162696c697479096164647265737365730a536d6172745461626c650361646406626f72726f771d6372656174655f7369676e65725f776974685f6361706162696c6974791d72657472696576655f7265736f757263655f6163636f756e745f636170036e657708636f6e7461696e730a616464726573735f6f66117065726d697373696f6e5f64656e696564137075626c6973685f7061636b6167655f74786ef409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e810000000000000000000000000000000000000000000000000000000000000001030800000000000000000520f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e8105206fc8d6b07117b68c1cca1f276d25f5287371d012c3018f248d86b950a51da25e126170746f733a3a6d657461646174615f76314f0100000000000000000f454e4f545f415554484f52495a454433546865207369676e6572206973206e6f7420617574686f72697a656420746f206465706c6f792074686973206d6f64756c652e0000000202110802130b03020801050003000100010707012a000f000b000b013800020101000100010707012b0010000b00380114020203000100010507012b001001110802030000000c100a00070211090c040b000c0338020c010b040c020b030b020b0112002d00020401000100010607012b0010000b00380302050104010004100b00110c070221040605090700110d2711020c030e030b010b02110e0200010000000000", + "0xa11ceb0b060000000a010008020804030c1905250a072f950108c401400684022c10b002760aa603050cab033a0000010101020003000408000005000100000601020002080004000109020200030a01030001060c000103010c01050e62617369635f636f6e7472616374056572726f72067369676e65720f7061636b6167655f6d616e616765720c536f6d655265736f75726365186d6f76655f746f5f7265736f757263655f6163636f756e74147570677261646561626c655f66756e6374696f6e0576616c75650a616464726573735f6f66117065726d697373696f6e5f64656e6965640a6765745f7369676e6572f409bfe5ffffce1834d4f6045985c6ce9178380337f4cf3d7964e09115137e8100000000000000000000000000000000000000000000000000000000000000010308000000000000000005206fc8d6b07117b68c1cca1f276d25f5287371d012c3018f248d86b950a51da25e126170746f733a3a6d657461646174615f7631620100000000000000000f454e4f545f415554484f52495a45442e596f7520617265206e6f7420617574686f72697a656420746f20706572666f726d207468697320616374696f6e2e0001147570677261646561626c655f66756e6374696f6e01010000020107030001040003100b00110207012104060509070011032711040c010e01062a0000000000000012002d00020101000001020629230000000000000200" + ] + } + ] +} diff --git a/developer-docs-site/docs/guides/resource-accounts/common-questions.md b/developer-docs-site/docs/guides/resource-accounts/common-questions.md new file mode 100644 index 0000000000000..49765d193fb55 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/common-questions.md @@ -0,0 +1,150 @@ +--- +title: "Common questions" +id: "common-questions" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Common questions +## How are resource accounts created? + +Let's review the two functions used to create resource accounts and what they return. + +First off, note that both creation functions allow for the input of a [**seed**](./common-questions.md#whats-a-seed) byte vector for the [ensuing hash used to compute the resource address](#how-is-the-address-for-a-resource-account-derived). + + + + +The `account.move` version creates a resource account and rotates its authentication key to `0x0`. The resulting resource account doesn't have an associated private key and can only be controlled through the usage of the [SignerCapability](#whats-a-signercapability) returned by the creation function. + +```rust title="Creating a resource account in account.move" +public fun create_resource_account( + source: &signer, + seed: vector +): (signer, SignerCapability) acquires Account { + let resource_addr = create_resource_address(&signer::address_of(source), seed); + + // ... + + // By default, only the SignerCapability should have control over the resource account and not the auth key. + // If the source account wants direct control via auth key, they would need to explicitly rotate the auth key + // of the resource account using the SignerCapability. + rotate_authentication_key_internal(&resource, ZERO_AUTH_KEY); + + let account = borrow_global_mut(resource_addr); + account.signer_capability_offer.for = option::some(resource_addr); + let signer_cap = SignerCapability { account: resource_addr }; + (resource, signer_cap) +} +``` + + + +The `create_resource_account` function in `resource_account.move` below creates a resource account and rotates its authentication key to the `optional_auth_key` argument. If this field is an empty vector, the authentication key is rotated to the `origin` account's authentication key. + +```rust title="Creating a manually controlled resource account in resource_account.move" +public entry fun create_resource_account( + origin: &signer, + seed: vector, + optional_auth_key: vector, +) acquires Container { + let (resource, signer_cap) = account::create_resource_account(origin, seed); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + signer_cap, + optional_auth_key, + ); +} +``` +The resource account created from this is functionally very similar to a user account that has had its authentication key rotated (see: [Rotating an authentication key](../account-management/key-rotation.md)), because it cannot yet be controlled programmatically and can still be controlled by a private key. + +However, there does exist a SignerCapability for the resource account, it just isn't being used yet. To enable programmatic control, you would need to [retrieve the SignerCapability.](./managing-resource-accounts#retrieving-a-signercapability) + +The end result of a creating a resource account with `create_resource_account(...)` in `resource_account.move` and then retrieving the SignerCapability is the same as creating the resource account with `create_resource_account(...)` in `account.move`. + + + +## What's a seed? + +A seed is an optional user-specified byte vector that is input during the creation of a resource account. The seed is used to ensure that the resulting resource account's address is unique. + +Since the hash function used to derive the resource account's address is deterministic, providing the same seed and source address will always result in the same resource account address. + +This also means that without a seed, if you were to try to generate multiple resource accounts from a single source account, you would end up with the same address each time due to a collision in the hashing computation. + +Thus, the `seed: vector` argument facilitates the creation of multiple resource accounts from a single source account and also allows for deterministically deriving a resource account address given an address and a seed byte vector. + +## What's a SignerCapability? + +A SignerCapability is a simple but powerful resource that allows a developer to programmatically manage a resource account. This is achieved with the `create_signer_with_capability` function: + +```rust +public fun create_signer_with_capability(capability: &SignerCapability): signer { + let addr = &capability.account; + create_signer(*addr) +} +``` + +The SignerCapability resource doesn't actually *do* anything special on its own, it's juts an abstract representation of permission to generate a [`signer`](../../move/book/signer.md) primitive for the account it was created for. + +It contains a single field called `account`, which is just the address that it has permission to generate a `signer` for: + +```rust +struct SignerCapability has drop, store { + account: address +} +``` + +Since it only has the abilities `drop` and `store`, it can't be copied, meaning only `account.move` itself can manage the new creation of a `SignerCapability`. The inner `account` field cannot be altered post creation, so it can only sign for the resource account it was initially created for. + + +## What's stopping someone from using my SignerCapability? + +You might be wondering "*Why does this work? Isn't it dangerous to be able to create a signer for an account so easily?*" + +Move's [privileged struct operations](../../move/book/structs-and-resources#privileged-struct-operations) require that creating structs and accessing their inner fields can only occur from within the module that defines the struct. This means that unless the developer provides a public accessor function to a stored `SignerCapability`, there is no way for another account to gain access to it. + +:::warning +Be mindful of properly gating access to a function that uses a `SignerCapability` and be extra careful when returning a `signer` from a public function, since this gives the caller unrestricted access to control the account. +::: + +A good rule of thumb is to by default set all functions that return a `SignerCapability` or a `signer` to internal private functions unless you've very carefully thought about the implications. + +```rust title="An example of a private function that returns a signer" +use aptos_std::resource_account; + +// The function name `internal_get_signer()` is functionally no different than +// `get_signer()`, but it signifies to any developer that comes across it that +// it shouldn't be publically accessible without careful thought. +fun internal_get_signer(): signer { + // Borrow the signer cap you've stored somewhere + let signer_cap = borrow_global(@your_contract).signer_cap; + + // Return the signer generated from it + resource_account::create_signer_with_capability(&signer_cap) +} +``` + +## How is the address for a resource account derived? + +When a resource account is created, the address is derived from a SHA3-256 hash of the requesting account's address, a byte scheme to identify it as a resource account, and an optional user-specified byte vector [**seed**](./common-questions.md#whats-a-seed). Here is the implementation of `create_resource_address` function in `account.move`: +```rust +/// This is a helper function to compute resource addresses. Computation of the address +/// involves the use of a cryptographic hash operation and should be use thoughtfully. +public fun create_resource_address(source: &address, seed: vector): address { + let bytes = bcs::to_bytes(source); + vector::append(&mut bytes, seed); + vector::push_back(&mut bytes, DERIVE_RESOURCE_ACCOUNT_SCHEME); + from_bcs::to_address(hash::sha3_256(bytes)) +} +``` + +## Why am I getting the `EACCOUNT_ALREADY_EXISTS` error? + +If you're getting this error while trying to create a resource account, it's because you have already created a resource account with that specific [**seed**](./common-questions.md#whats-a-seed). + +This error occurs because there's a collision in the output of the hashing function used to derive a resource account's address. + +To fix it, you need to change the seed or the function you're calling will continue to unsuccessfully attempt to create an account at an address that already exists. diff --git a/developer-docs-site/docs/guides/resource-accounts/index.md b/developer-docs-site/docs/guides/resource-accounts/index.md new file mode 100644 index 0000000000000..4d1a37bd5d56d --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/index.md @@ -0,0 +1,10 @@ +--- +title: "Resource Accounts" +--- + +# Resource Accounts + +- ### [Overview](./overview) +- ### [Managing resource accounts](./managing-resource-accounts) +- ### [Publishing an upgradeable module](./publishing-an-upgradeable-module) +- ### [Common questions](./common-questions) diff --git a/developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md b/developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md new file mode 100644 index 0000000000000..8f720c913d006 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/managing-resource-accounts.md @@ -0,0 +1,184 @@ +--- +title: "Managing resource accounts" +id: "managing-resource-accounts" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Managing resource accounts + +In this section we're going to explore the various mechanisms underlying resource accounts and how to utilize them to manage resources programmatically. + +There are two distinct ways to manage a resource account: + +1. The authentication key is rotated to a separate account that can control it manually by signing for it +2. The authentication key is rotated to **0x0** and is controlled programmatically with a [**SignerCapability**](./common-questions#whats-a-signercapability) + +:::info +The `create_resource_account(...)` functions in [`account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/account.move) and [`resource_account.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/resource_account.move) are different, with the `account.move` version creating a programmatically controlled resource account and the `resource_account.move` version creating a manually controlled resource account. +::: + +If you'd like to better understand how the creation functions work, check out [how are resource accounts created.](../resource-accounts/common-questions#how-are-resource-accounts-created) + +## Using a SignerCapability + +The [**SignerCapability**](./common-questions#whats-a-signercapability) resource is the most crucial part of how resource accounts work. + +When a SignerCapability is created and later retrieved, the authentication key of the resource account it was created for is rotated to **0x0**, which gives the [Move VM](../../reference/glossary/#move-virtual-machine-mvm) the capability to generate the resource account's signer from a SignerCapability. You can store this SignerCapability and retrieve it later to create an authorized signer for the resource account. + +Here is a very basic example that demonstrates how you'd use a SignerCapability in a Move contract: + +```rust +// Define a resource we can store the SignerCapability at +struct SignerCap has key { + signer_cap: SignerCapability, +} + +public entry fun store_signer_capability(creator: &signer) { + // We move `SignerCap` to an account's resources. We can even move it to the resource account itself: + let (resource_signer, signer_cap) = account::create_resource_account(creator, b"seed bytes"); + move_to(resource_signer, SignerCap { + signer_cap, + }); +} + +// Generate the resource account's signer with the SignerCapability +public entry fun sign_with_resource_account(creator: &signer) acquires SignerCap { + let resource_address = account::create_resource_address(signer::address_of(creator), b"seed bytes"); + let signer_cap = borrow_global(resource_account_address); + let resource_signer = account::create_signer_with_capability(signer_cap); + + // Here we'd do something with the resource_signer that we can only do with its `signer` primitive +} +``` +Utilizing a resource account in this way is the fundamental process for automating the generation and retrieval of resources on-chain. + +If you're wondering how the **SignerCapability** permission model works, head over to [what's stopping someone from using my SignerCapability?](./common-questions#whats-stopping-someone-from-using-my-signercapability) + +## Retrieving a SignerCapability + +Say you create a resource account with one of the `resource_account.move` functions. The `SignerCapability` exists for the account but you need to retrieve it- you can do this by calling `retrieve_resource_account_cap`. + +Here's an example of how you could achieve this: + +```rust title="Retrieve and store a SignerCapability" +struct SignerCap has key { + signer_cap: SignerCapability, +} + +// `source_addr` is the address of the resource account creator +public entry fun retrieve_cap(resource_signer: &signer, source_addr: address) acquires SignerCap { + // Retrieve the SignerCapability + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, source_addr); + // Move it into the resource signer's account resources for use later + move_to(resource_signer, SignerCap { + signer_cap, + }); +} +``` +:::warning +Make sure to store a retrieved `SignerCapability` somewhere because the `retrieve_resource_account_cap` function can only be called once, since the `SignerCapability` returned is ephemeral and will be dropped if not stored. + +Without access to a `SignerCapability`, there is no way to generate a signature for the account, effectively locking you out of it forever. +::: + +## Publishing modules with resource accounts + +Publishing modules with resource account gives developers the ability to separate the logic and resources of their smart contracts from their normal user accounts. It also offers them the ability to publish immutable, open source contracts that other developers can use without fear of the contract being altered. + +Below we detail the various ways to publish a module to a resource account. + +### Publishing an immutable module with a resource account + +One of the most common usages of resource accounts is publishing a module with them. This function in `resource_account.move` is called by a user account to create a resource account and publish a module with it. + +```rust title="Helper function in resource_account.move to publish a package with a resource account" +public entry fun create_resource_account_and_publish_package( + origin: &signer, + seed: vector, + metadata_serialized: vector, + code: vector>, +) acquires Container { + let (resource, signer_cap) = account::create_resource_account(origin, seed); + aptos_framework::code::publish_package_txn(&resource, metadata_serialized, code); + rotate_account_authentication_key_and_store_capability( + origin, + resource, + signer_cap, + ZERO_AUTH_KEY, + ); +} +``` + +:::warning Immutable Contracts +By default, publishing a module with a resource account will result in an immutable module. This is because the **SignerCapability** is retrieved and dropped during the publishing process. +::: + +If you'd like to publish an upgradeable module with a resource account, see the section below. + +### Publishing an upgradeable module with a resource account + +Publishing an upgradeable module with a resource account involves a few steps to setup the contract to prepare it for publication and then the actual process of publishing and upgrading. In this section we'll explain how it works first and then how to do it after. + +The key to making an upgradeable contract with a resource account is to use Aptos Move's **init_module** function. The **init_module** function is a unique function that will **only** run the first time a contract is published. + +It's always a private function with a single argument (the module publisher, in the form of `&signer`) with no return value. + +```rust title="init_module function signature" +fun init_module(publisher: &signer) { + // ... +} +``` + +We can use this function to retrieve the SignerCapability and store it somewhere for use later, but since the resource that stores SignerCapability is not accessible externally, we must write a function call to interface with the SignerCapability. + +```rust title="Using init_module and publish_package in a contract to function as an interface for the developer" +module upgradeable_resource_account_package::package_manager { + use aptos_framework::account::{Self, SignerCapability}; + use aptos_framework::resource_account; + use aptos_std::code; + use std::signer; + + /// You are not authorized to upgrade this module. + const ENOT_AUTHORIZED: u64 = 0; + + // Declare our SignerCap struct to store our resource account's SignerCapability. + struct SignerCap has key { + signer_cap: SignerCapability, + } + + fun init_module(resource_signer: &signer) { + // Note that we must deploy the module with `deployer` as a named address, otherwise the contract can't find the resource account's owner. + let signer_cap = resource_account::retrieve_resource_account_cap(resource_signer, @deployer); + + // We move the SignerCap to the resource account. + // Note that this means the SignerCap resource is stored at the same address that the module is. + move_to(resource_signer, SignerCap { + signer_cap, + }); + } + + // This function is integral to making the contract upgradeable. Without it, the SignerCapability is still stored, but + // there is no way to actually use it to upgrade the module, making the module effectively immutable. + public entry fun publish_package( + deployer: &signer, + package_metadata: vector, + code: vector>, + ) acquires PermissionConfig { + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + code::publish_package_txn(&get_signer(), package_metadata, code); + } +} +``` + +There are several things going on here to take note of: + +1. We retrieve and store the SignerCapability in the `SignerCap` struct. +2. We add an `publish_package(...)` function that acts as an interface to the package publishing function. It allows the **deployer** of the module to upgrade it despite not being the direct owner of the module. +3. The **deployer** is the developer's account- the one that calls `create_resource_account_and_publish_package(...)` and owns the resource account but not the module itself. It is a named address here, signified with `@deployer`, so we must specify it as a named address upon publication. +4. We gate access to the `publish_package` function by asserting that the signer is the original `@deployer`. + +Publishing this module from the Aptos CLI would look like this: + +If you're looking for an example of how to do this, see the [publishing an upgradeable module example.](./publishing-an-upgradeable-module.md) diff --git a/developer-docs-site/docs/guides/resource-accounts/overview.md b/developer-docs-site/docs/guides/resource-accounts/overview.md new file mode 100644 index 0000000000000..9ed7be750a420 --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/overview.md @@ -0,0 +1,65 @@ +--- +title: "Overview" +id: "overview" +--- + +# Overview + +## What is a resource account? + +A [resource account](../../move/move-on-aptos/resource-accounts.md) is an [account](../../concepts/accounts/) that's used to store and manage [resources](../../concepts/resources/) independent of a user. It can be used as a simple storage account or it can be utilized to programmatically manage resources from within a smart contract. + +In a more general sense, the programmable management of resource accounts facilitates a trustless exchange model between two parties. Resource accounts act as trustless, programmable third-party escrows, which are a fundamental building block in the creation of smart contracts. + +## How are resource accounts used? + +Resource accounts are used in two ways: + +1. General resource management, like using them to [publish smart contracts to a non-user account](./managing-resource-accounts#publishing-modules-with-resource-accounts), and +2. Automating resource management in a smart contract by generating the [signer](../../move/book/signer.md) for an account without a private key + +Sometimes when writing smart contracts, developers need to programmatically automate [the approval of an account](../../concepts/accounts.md#access-control-with-signers) delegated to resource management in order to create seamless smart contracts that only require one call. + +Resource accounts can relinquish their ability to generate a signature with a [private key](https://en.wikipedia.org/wiki/Public-key_cryptography) by delegating it to a [**SignerCapability**](./managing-resource-accounts#using-a-signercapability) resource, which is used to generate an on-chain `signer` primitive for the account. + +:::tip +As we may seem to refer to them interchangeably, it's important to note that an account's signature for a transaction is not exactly the same thing as a `signer`. In the context of a Move module (aka a smart contract), a `signer` is the on-chain, primitive data type used to represent an account's approval to process a given transaction, whereas a signature is the off-chain representation of this approval. +::: + +Think of it this way: the ***signature*** for a transaction function `public entry fun foo(sender: &signer)` is converted to a ***signer*** and represented as the input argument `sender` in an [entry function.](../../move/book/functions#entry-modifier) + +## A real-world use case: a vesting contract + +A [vesting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/framework/aptos-framework/sources/vesting.move#L929) is a smart contract that automates the timed disbursement of allotted funds to a designated receiver. + +A real world analogous example of this is a trust fund where a child receives money over time from their grandparents, or a compensation package offered by a company that rewards an employee with vested stock options over time. + +To explore conceptually how we'd create something like this with a smart contract, let's evaluate the analogous equivalents in terms of who is sending and receiving funds. + +In a trust fund, there is a sending party (the grantor), the receiving party (the beneficiary), and the third-party (the trustee) that handles the disbursement of funds. Smart contracts can be employed to programmatically manage funds, effectively acting as the third-party trustee in this example. + +The general process for this is: + +1. The grantor locks up the initial funds and sets rules on how much the beneficiary can receive over time +2. As time passes, the beneficiary requests funds +3. If there are funds available based on the amount of time that has passed, the funds are transferred to the beneficiary +4. The beneficiary repeats step 3 periodically until the funds are depleted + +The two active parties here are the trustee and the beneficiary. Conceptually, we can think of these two parties as two separate accounts on-chain. Managing an account's resources requires the signed approval of the account, meaning a smart contract that facilitates periodically releasing vested funds would require the approval of both the sender and the receiver accounts whenever funds are to be dispersed. + +Requiring multiple signers to call an [entry function](../../move/book/functions/#entry-modifier) is logistically complex not only for the developer but for the end users as well. Both of the users would need to sign off on the transfer transaction every vesting period. Since these would be asynchronous, manual approvals, one user calling the function will always be waiting on the other. + +However, with resource accounts, we can integrate programmatic control of these funds, gating access to the funds with the smart contract's internal logic. Whenever the receiver requests to receive funds, the smart contract evaluates if there are any funds to disperse based on time and the amount of funds left. If there are funds ready to be dispersed, the contract internally generates a signer for the resource account holding the funds in order to withdraw them from it. + +This internally generated `signer` primitive is created with the [**SignerCapability**](./managing-resource-accounts#using-a-signercapability) resource, thus fully automating the process for the receiver- completing the exchange of funds with only one account's signature! + +## More examples of how to use resource accounts +- [Defi swap contract](https://github.com/aptos-labs/aptos-core/blob/c1d87b8a3f17059311d4bdf83b953d12c61c14c0/aptos-move/move-examples/resource_account/sources/simple_defi.move#L39): an automated escrow contract where the resource account acts as an automated and trustless 3rd party escrow. + +- [Automated token minting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/move-examples/post_mint_reveal_nft/sources/minting.move#L181C49-L181C65): a minting contract where the resource account is the collection creator and mints/sends out tokens on request. + +- [Multi-signature accounts](https://github.com/aptos-labs/aptos-core/blob/4f9b69b6592f58e57691944b888461c2a93ffe7a/aptos-move/framework/aptos-framework/sources/multisig_account.move#L993): using a resource account as a shared account for multiple users, controlled through decentralized voting mechanisms. + +- [Coin disbursement through a shared account](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/move-examples/shared_account/sources/shared_account.move#L48): a coin disbursement contract where coins sent to a resource account are distributed to multiple accounts according to a fixed percentage. + +- [Vesting contract](https://github.com/aptos-labs/aptos-core/blob/49400cbf0bc63d5e86a54d0c0a1ee2b74c5ea7ec/aptos-move/framework/aptos-framework/sources/vesting.move#L929): a vesting contract that disburses a fixed % of coins over a set amount of time. diff --git a/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md new file mode 100644 index 0000000000000..8e2e909944fea --- /dev/null +++ b/developer-docs-site/docs/guides/resource-accounts/publishing-an-upgradeable-module.md @@ -0,0 +1,196 @@ +--- +title: "Publishing an upgradeable module" +id: "publishing-an-upgradeable-module" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Publishing an upgradeable module + +The objective of this tutorial is to show a concrete example of how to publish an upgradeable module with a resource account. + +In order to follow along with this tutorial, make sure you have: + +* The [Aptos CLI](../../tools/aptos-cli) +* The [Aptos core repository.](https:/github.com/aptos-labs/aptos-core) The modules we'll use are located in the `aptos-move/move-examples` section of the repository. + +## Overview of the publishing process + +Each of the following steps will be explained in detail in their corresponding sections: + +1. Publish the module with a resource account +2. Run the `upgradeable_function` view function and see what it returns +3. Change the view function in the contract so we can observe a tangible change post-publish +4. Get the module metadata and bytecode from the `aptos move build-publish-package` command +5. Run the `publish_package` function to upgrade the module +6. Run the view function again to observe the new return value + +First navigate to the correct directory: +```shell title="Navigate to your local directory" +cd ~/aptos-core/aptos-move/move-examples/upgradeable_resource_account_package +``` + +Then create a `deployer` profile initialized to devnet: +```shell +aptos init --profile deployer +``` + +Enter devnet when prompted and leave the private key empty so it will generate an account for you. When we write `deployer` in our commands, it will automatically use this profile. + +### 1. Publish the module + +```shell +aptos move create-resource-account-and-publish-package \ + --address-name upgradeable_resource_account_package --seed '' \ + --named-addresses deployer=deployer \ + --profile deployer +``` + +:::info +Read more about the [**seed**](./common-questions.md#whats-a-seed) argument here. +::: + +The `--address-name` flag marks the following string as named address the resource account's address will appear as in the contract. + +That is, the resource account created from this command will correspond to the `@upgradeable_resource_account_package` address in our module. + +When you run this command, it will ask you something like this: + +``` +Do you want to publish this package under the resource account's address +be326762ddd27624743223991c2223027621e62b7d0849a40a970fa2df385da9? +[yes/no] > +``` + +Enter yes and copy that address down somewhere. That's our resource account address where the contract is deployed. Now you can run the view function! + +### 2. Run the view function + +Replace `RESOURCE_ACCOUNT_ADDRESS`` with the address you got from step #1 and run the following command: + +```shell title="View the value returned from upgradeable_function()" +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function --profile deployer +``` + +It should output: +```json +Result: [ + 9000 +] +``` + +### 3. Change the view function + +Now let's change the value returned in the view function from `9000` to `9001` so we can observe a difference in the upgraded contract: + +```rust +#[view] +public fun upgradeable_function(): u64 { + 9001 +} +``` + +Save that file, and then follow step #4 to get the package metadata and bytecode in JSON format. + +### 4. Get the new bytecode for the module + +```shell +aptos move build-publish-payload --json-output-file upgrade_contract.json --named-addresses upgradeable_resource_account_package=RESOURCE_ACCOUNT_ADDRESS,deployer=deployer +``` + +Replace `RESOURCE_ACCOUNT_ADDRESS` with your resource account address and run the command. Once you do this, there will now be a `upgrade_contract.json` file with the bytecode output of the new, upgraded module in it. + +Since we made our own `upgrade_contract` function that wraps the `0x1::code::publish_package_txn`, we need to change the function call value to our publish package function. + +After editing `upgrade_contract.json`, your `function_id` value should look something like below: + +```json title="Change the function_id value in upgrade_contract.json to call your resource account's publish package function" +{ + "function_id": "RESOURCE_ACCOUNT_ADDRESS::package_manager::publish_package", + "type_args": [], + "args": [ + { + "type": "hex", + "value": "0x2155...6200" + }, + { + "type": "hex", + "value": [ + "0xa11c...0000" + ] + } + ] +} +``` + +Make sure to change the `RESOURCE_ACCOUNT_ADDRESS` to your specific resource account address. + +### 5. Run the upgrade_contract function + +```shell +aptos move run --json-file upgrade_contract.json --profile deployer +``` + +Confirm yes to publish the upgraded module. + +### 6. Run the upgraded view function + +```shell +aptos move view --function-id RESOURCE_ACCOUNT_ADDRESS::basic_contract::upgradeable_function --profile deployer +``` + +You should get: + +```json +Result: [ + 9001 +] +``` + +Now you know how to publish an upgradeable module with a resource account! + +## Extra features in the package manager module + +You may have noticed there are other features in the `package_manager.move` module. These are helper functions to manage retrieving the `signer` for a resource account used to publish a package module and additionally track addresses generated from the contract in a `SmartTable`. + +The `get_signer()` function utilizes the friend keyword to gate access to the signer. +```rust title="The get_signer() function can only be called by friends or other functions in package_manager.move" +public(friend) fun get_signer(): signer acquires PermissionConfig { + let signer_cap = &borrow_global(@upgradeable_resource_account_package).signer_cap; + account::create_signer_with_capability(signer_cap) +} +``` + +If you wanted to give another one of your modules access to this function this function, you'd simply declare the module as a friend at the top of your module, and then call it in that module wherever you need: + +```rust +// package_manager.move +module upgradeable_resource_account_package::package_manager { + friend upgradeable_resource_account_package::basic_contract; + // ... +} + +// basic_contract.move +module upgradeable_resource_account_package::basic_contract { + use upgradeable_resource_account_package::package_manager; + // ... + public fun move_to_resource_account(deployer: &signer) { + // Only the deployer can call this function. + assert!(signer::address_of(deployer) == @deployer, error::permission_denied(ENOT_AUTHORIZED)); + + // Do something with the resource account's signer + // For example, a simple `move_to` call + let resource_signer = package_manager::get_signer(); + move_to( + &resource_signer, + SomeResource { + value: 42, + } + ); + } +} +``` +Read more about [friend function declarations here.](../../move/book/friends/#friend-declaration) + +Use the package manager contract as needed in your own packages. Keep in mind that you must deploy the `package_manager.move` module with your package in order to correctly utilize the module. diff --git a/developer-docs-site/docusaurus.config.js b/developer-docs-site/docusaurus.config.js index 5ca15cd3c37c2..25dba04088850 100644 --- a/developer-docs-site/docusaurus.config.js +++ b/developer-docs-site/docusaurus.config.js @@ -159,8 +159,8 @@ const config = { to: "/category/nft", }, { - label: "Examples", - to: "/category/examples", + label: "Guides", + to: "/category/guides", }, { label: "Build E2E Dapp on Aptos", diff --git a/developer-docs-site/sidebars.js b/developer-docs-site/sidebars.js index adbb1f3a932d5..d1a239dee22ba 100644 --- a/developer-docs-site/sidebars.js +++ b/developer-docs-site/sidebars.js @@ -336,17 +336,32 @@ const sidebars = { }, { type: "category", - label: "Examples", + label: "Guides", collapsible: true, collapsed: true, link: { type: "generated-index", - title: "Examples", - description: "Examples for all the various concepts and tooling used to build on Aptos.", - slug: "/category/examples", - keywords: ["examples"], + title: "Guides", + description: "Guides for all the various concepts and tooling used to build on Aptos.", + slug: "/category/guides", + keywords: ["guides"], }, - items: ["guides/account-management/key-rotation"], + items: [ + "guides/account-management/key-rotation", + { + type: "category", + label: "Resource accounts", + link: { type: "doc", id: "guides/resource-accounts/index" }, + collapsible: true, + collapsed: true, + items: [ + "guides/resource-accounts/overview", + "guides/resource-accounts/managing-resource-accounts", + "guides/resource-accounts/publishing-an-upgradeable-module", + "guides/resource-accounts/common-questions", + ], + }, + ], }, { type: "category",