From d8e3f941aebf3d3504f2091cffd928353da26a40 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 6 Mar 2023 15:15:12 +0100 Subject: [PATCH 1/5] feat(Upgradable): enable batched fn call after deploy --- near-plugins-derive/src/upgradable.rs | 16 +++++++++++++--- near-plugins/src/upgradable.rs | 13 +++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/near-plugins-derive/src/upgradable.rs b/near-plugins-derive/src/upgradable.rs index 7f264e9..be573e7 100644 --- a/near-plugins-derive/src/upgradable.rs +++ b/near-plugins-derive/src/upgradable.rs @@ -180,7 +180,7 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { } #[#cratename::access_control_any(roles(#(#acl_roles_code_deployers),*))] - fn up_deploy_code(&mut self) -> near_sdk::Promise { + fn up_deploy_code(&mut self, function_call_args: Option<#cratename::upgradable::FunctionCallArgs>) -> near_sdk::Promise { let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp) .unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging timestamp isn't set")); @@ -194,8 +194,18 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream { ); } - near_sdk::Promise::new(near_sdk::env::current_account_id()) - .deploy_contract(self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code"))) + let code = self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code")); + let promise = near_sdk::Promise::new(near_sdk::env::current_account_id()) + .deploy_contract(code); + match function_call_args { + None => promise, + Some(args) => { + // Execute the `DeployContract` and `FunctionCall` actions in a batch + // transaction to make a failure of the function call roll back the code + // deployment. + promise.function_call(args.function_name, args.arguments, args.amount, args.gas) + }, + } } #[#cratename::access_control_any(roles(#(#acl_roles_duration_initializers),*))] diff --git a/near-plugins/src/upgradable.rs b/near-plugins/src/upgradable.rs index bb422ea..50f9b85 100644 --- a/near-plugins/src/upgradable.rs +++ b/near-plugins/src/upgradable.rs @@ -57,7 +57,7 @@ //! [time between scheduling and execution]: https://docs.near.org/sdk/rust/promises/intro use crate::events::{AsEvent, EventMetadata}; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{AccountId, CryptoHash, Promise}; +use near_sdk::{AccountId, Balance, CryptoHash, Gas, Promise}; /// Trait describing the functionality of the _Upgradable_ plugin. pub trait Upgradable { @@ -103,7 +103,7 @@ pub trait Upgradable { /// specified via the `code_deployers` field of the `Upgradable` macro's `access_control_roles` /// attribute. The example contract (accessible via the `README`) shows how access control roles /// can be defined and passed on to the `Upgradable` macro. - fn up_deploy_code(&mut self) -> Promise; + fn up_deploy_code(&mut self, function_call_args: Option) -> Promise; /// Initializes the duration of the delay for deploying the staged code. It defaults to zero if /// code is staged before the staging duration is initialized. Once the staging duration has @@ -150,6 +150,15 @@ pub struct UpgradableDurationStatus { pub new_staging_duration_timestamp: Option, } +// TODO add docs +#[derive(Deserialize, Serialize, Debug)] +pub struct FunctionCallArgs { + pub function_name: String, + pub arguments: Vec, + pub amount: Balance, + pub gas: Gas, +} + /// Event emitted when the code is staged #[derive(Serialize, Clone)] struct StageCode { From a8b38756191dba7f27d7f9ceca5ebd40bf2a9ad5 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 6 Mar 2023 15:19:04 +0100 Subject: [PATCH 2/5] test: update existing tests --- .../tests/common/upgradable_contract.rs | 6 +++++- near-plugins-derive/tests/upgradable.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/near-plugins-derive/tests/common/upgradable_contract.rs b/near-plugins-derive/tests/common/upgradable_contract.rs index d2289bb..16d39d8 100644 --- a/near-plugins-derive/tests/common/upgradable_contract.rs +++ b/near-plugins-derive/tests/common/upgradable_contract.rs @@ -1,4 +1,4 @@ -use near_plugins::upgradable::UpgradableDurationStatus; +use near_plugins::upgradable::{FunctionCallArgs, UpgradableDurationStatus}; use near_sdk::serde_json::json; use near_sdk::CryptoHash; @@ -71,9 +71,13 @@ impl UpgradableContract { pub async fn up_deploy_code( &self, caller: &Account, + function_call_args: Option, ) -> workspaces::Result { caller .call(self.contract.id(), "up_deploy_code") + .args_json(json!({ + "function_call_args": function_call_args, + })) .max_gas() .transact() .await diff --git a/near-plugins-derive/tests/upgradable.rs b/near-plugins-derive/tests/upgradable.rs index fd651d3..65347fd 100644 --- a/near-plugins-derive/tests/upgradable.rs +++ b/near-plugins-derive/tests/upgradable.rs @@ -419,7 +419,7 @@ async fn test_deploy_code_without_delay() -> anyhow::Result<()> { setup.assert_staged_code(Some(code)).await; // Deploy staged code. - let res = setup.upgradable_contract.up_deploy_code(&dao).await?; + let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_success_with_unit_return(res); Ok(()) @@ -447,7 +447,7 @@ async fn test_deploy_code_and_call_method() -> anyhow::Result<()> { setup.assert_staged_code(Some(code)).await; // Deploy staged code. - let res = setup.upgradable_contract.up_deploy_code(&dao).await?; + let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_success_with_unit_return(res); // The newly deployed contract defines the function `is_upgraded`. Calling it successfully @@ -483,7 +483,7 @@ async fn test_deploy_code_with_delay() -> anyhow::Result<()> { fast_forward_beyond(&worker, staging_duration).await; // Deploy staged code. - let res = setup.upgradable_contract.up_deploy_code(&dao).await?; + let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_success_with_unit_return(res); Ok(()) @@ -513,7 +513,7 @@ async fn test_deploy_code_with_delay_failure_too_early() -> anyhow::Result<()> { fast_forward_beyond(&worker, sdk_duration_from_secs(1)).await; // Verify trying to deploy staged code fails. - let res = setup.upgradable_contract.up_deploy_code(&dao).await?; + let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_failure_with(res, ERR_MSG_DEPLOY_CODE_TOO_EARLY); Ok(()) @@ -538,7 +538,7 @@ async fn test_deploy_code_permission_failure() -> anyhow::Result<()> { // call this method. let res = setup .upgradable_contract - .up_deploy_code(&setup.unauth_account) + .up_deploy_code(&setup.unauth_account, None) .await?; assert_insufficient_acl_permissions( res, @@ -577,7 +577,7 @@ async fn test_deploy_code_empty_failure() -> anyhow::Result<()> { // The staging timestamp is set when staging code and removed when unstaging code. So when there // is no code staged, there is no staging timestamp. Hence the error message regarding a missing // staging timestamp is expected. - let res = setup.upgradable_contract.up_deploy_code(&dao).await?; + let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_failure_with(res, ERR_MSG_NO_STAGING_TS); Ok(()) @@ -825,7 +825,7 @@ async fn test_acl_permission_scope() -> anyhow::Result<()> { // deploy code. let res = setup .upgradable_contract - .up_deploy_code(&setup.unauth_account) + .up_deploy_code(&setup.unauth_account, None) .await?; assert_insufficient_acl_permissions( res, From dd716bf49393f2bcd20d83ecd3894a427796a706 Mon Sep 17 00:00:00 2001 From: Moritz Date: Mon, 6 Mar 2023 17:35:46 +0100 Subject: [PATCH 3/5] test: add contract with state migration --- .../upgradable_state_migration/Cargo.toml | 21 ++++++ .../upgradable_state_migration/Makefile | 8 +++ .../upgradable_state_migration/rust-toolchain | 3 + .../upgradable_state_migration/src/lib.rs | 72 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 near-plugins-derive/tests/contracts/upgradable_state_migration/Cargo.toml create mode 100644 near-plugins-derive/tests/contracts/upgradable_state_migration/Makefile create mode 100644 near-plugins-derive/tests/contracts/upgradable_state_migration/rust-toolchain create mode 100644 near-plugins-derive/tests/contracts/upgradable_state_migration/src/lib.rs diff --git a/near-plugins-derive/tests/contracts/upgradable_state_migration/Cargo.toml b/near-plugins-derive/tests/contracts/upgradable_state_migration/Cargo.toml new file mode 100644 index 0000000..19c14d4 --- /dev/null +++ b/near-plugins-derive/tests/contracts/upgradable_state_migration/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "upgradable_state_migration" +version = "0.0.0" +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-plugins = { path = "../../../../near-plugins" } +near-sdk = "4.1.0" + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = true + +[workspace] diff --git a/near-plugins-derive/tests/contracts/upgradable_state_migration/Makefile b/near-plugins-derive/tests/contracts/upgradable_state_migration/Makefile new file mode 100644 index 0000000..aa2cf20 --- /dev/null +++ b/near-plugins-derive/tests/contracts/upgradable_state_migration/Makefile @@ -0,0 +1,8 @@ +build: + cargo build --target wasm32-unknown-unknown --release + +# Helpful for debugging. Requires `cargo-expand`. +expand: + cargo expand > expanded.rs + +.PHONY: build expand diff --git a/near-plugins-derive/tests/contracts/upgradable_state_migration/rust-toolchain b/near-plugins-derive/tests/contracts/upgradable_state_migration/rust-toolchain new file mode 100644 index 0000000..2f3cf78 --- /dev/null +++ b/near-plugins-derive/tests/contracts/upgradable_state_migration/rust-toolchain @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.66.1" +components = ["clippy", "rustfmt"] diff --git a/near-plugins-derive/tests/contracts/upgradable_state_migration/src/lib.rs b/near-plugins-derive/tests/contracts/upgradable_state_migration/src/lib.rs new file mode 100644 index 0000000..cd55082 --- /dev/null +++ b/near-plugins-derive/tests/contracts/upgradable_state_migration/src/lib.rs @@ -0,0 +1,72 @@ +//! A simple contract to be deployed via `Upgradable`. It requires [state migration]. +//! +//! [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state + +use near_plugins::{access_control, AccessControlRole, AccessControllable, Upgradable}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{env, near_bindgen, PanicOnDefault}; + +/// Roles correspond to those defined in the initial contract `../upgradable`, to make permissions +/// granted before the upgrade remain valid. +#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum Role { + DAO, + CodeStager, + CodeDeployer, + DurationManager, +} + +/// The struct differs from the one defined in the initial contract `../upgradable`, hence [state +/// migration] is required. +/// +/// [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state +#[access_control(role_type(Role))] +#[near_bindgen] +#[derive(Upgradable, PanicOnDefault, BorshDeserialize, BorshSerialize)] +#[upgradable(access_control_roles( + code_stagers(Role::CodeStager, Role::DAO), + code_deployers(Role::CodeDeployer, Role::DAO), + duration_initializers(Role::DurationManager, Role::DAO), + duration_update_stagers(Role::DurationManager, Role::DAO), + duration_update_appliers(Role::DurationManager, Role::DAO), +))] +pub struct Contract { + is_migrated: bool, +} + +#[near_bindgen] +impl Contract { + /// Migrates state from [`OldContract`] to [`Contract`]. + /// + /// It follows the state migration pattern described [here]. + /// + /// [here]: https://docs.near.org/develop/upgrade#migrating-the-state + #[private] + #[init(ignore_state)] + pub fn migrate() -> Self { + // Ensure old state can be read and deserialized. + let _: OldContract = env::state_read().expect("Should be able to load old state"); + + Self { is_migrated: true } + } + + /// A migration method that fails on purpose to test the rollback mechanism of + /// `Upgradable::up_deploy_code`. + #[private] + #[init(ignore_state)] + pub fn migrate_with_failure() -> Self { + env::panic_str("Failing migration on purpose"); + } + + /// This method is _not_ defined in the initial contract, so calling it successfully proves the + /// contract defined in this file was deployed and the old state was migrated. + pub fn is_migrated(&self) -> bool { + self.is_migrated + } +} + +/// Corresponds to the state defined in the initial `../upgradable` contract. +#[derive(BorshDeserialize)] +pub struct OldContract; From 24d624f8fbac8882052227012369a1d733a6c703 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 7 Mar 2023 09:06:17 +0100 Subject: [PATCH 4/5] test: deploy and call migration function --- near-plugins-derive/tests/common/utils.rs | 5 + near-plugins-derive/tests/upgradable.rs | 119 ++++++++++++++++++++-- 2 files changed, 118 insertions(+), 6 deletions(-) diff --git a/near-plugins-derive/tests/common/utils.rs b/near-plugins-derive/tests/common/utils.rs index c8c1688..ef3ebd9 100644 --- a/near-plugins-derive/tests/common/utils.rs +++ b/near-plugins-derive/tests/common/utils.rs @@ -49,6 +49,11 @@ where assert_eq!(actual, expected); } +/// Asserts transaction failure due `MethodNotFound` error. +pub fn assert_method_not_found_failure(res: ExecutionFinalResult) { + assert_failure_with(res, "Action #0: MethodResolveError(MethodNotFound)"); +} + /// Asserts transaction failure due to `method` being `#[private]`. pub fn assert_private_method_failure(res: ExecutionFinalResult, method: &str) { let err = res diff --git a/near-plugins-derive/tests/upgradable.rs b/near-plugins-derive/tests/upgradable.rs index 65347fd..97ad58e 100644 --- a/near-plugins-derive/tests/upgradable.rs +++ b/near-plugins-derive/tests/upgradable.rs @@ -6,12 +6,13 @@ use anyhow::Ok; use common::access_controllable_contract::AccessControllableContract; use common::upgradable_contract::UpgradableContract; use common::utils::{ - assert_failure_with, assert_insufficient_acl_permissions, assert_success_with, - assert_success_with_unit_return, fast_forward_beyond, get_transaction_block, - sdk_duration_from_secs, + assert_failure_with, assert_insufficient_acl_permissions, assert_method_not_found_failure, + assert_success_with, assert_success_with_unit_return, fast_forward_beyond, + get_transaction_block, sdk_duration_from_secs, }; +use near_plugins::upgradable::FunctionCallArgs; use near_sdk::serde_json::json; -use near_sdk::{CryptoHash, Duration, Timestamp}; +use near_sdk::{CryptoHash, Duration, Gas, Timestamp}; use std::path::Path; use workspaces::network::Sandbox; use workspaces::result::ExecutionFinalResult; @@ -19,6 +20,7 @@ use workspaces::{Account, AccountId, Contract, Worker}; const PROJECT_PATH: &str = "./tests/contracts/upgradable"; const PROJECT_PATH_2: &str = "./tests/contracts/upgradable_2"; +const PROJECT_PATH_STATE_MIGRATION: &str = "./tests/contracts/upgradable_state_migration"; const ERR_MSG_NO_STAGING_TS: &str = "Upgradable: staging timestamp isn't set"; const ERR_MSG_DEPLOY_CODE_TOO_EARLY: &str = "Upgradable: Deploy code too early: staging ends on"; @@ -169,6 +171,18 @@ impl Setup { .await } + async fn call_is_migrated(&self, caller: &Account) -> workspaces::Result { + // `is_migrated` could be called via `view`, however here it is called via `transact` so we + // get an `ExecutionFinalResult` that can be passed to `assert_*` methods from + // `common::utils`. It is acceptable since all we care about is whether the method exists + // and can be called successfully. + caller + .call(self.contract.id(), "is_migrated") + .max_gas() + .transact() + .await + } + /// Calls the contract's `is_set_up` method and asserts it returns `true`. Panics on failure. async fn assert_is_set_up(&self, caller: &Account) { let res = caller @@ -426,7 +440,8 @@ async fn test_deploy_code_without_delay() -> anyhow::Result<()> { } /// Verifies the upgrade was successful by calling a method that's available only on the upgraded -/// contract. Ensures the new contract can be deployed and state migration succeeds. +/// contract. Ensures the new contract can be deployed and state remains valid without +/// explicit state migration. #[tokio::test] async fn test_deploy_code_and_call_method() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; @@ -435,7 +450,7 @@ async fn test_deploy_code_and_call_method() -> anyhow::Result<()> { // Verify function `is_upgraded` is not defined in the initial contract. let res = setup.call_is_upgraded(&setup.unauth_account).await?; - assert_failure_with(res, "Action #0: MethodResolveError(MethodNotFound)"); + assert_method_not_found_failure(res); // Compile the other version of the contract and stage its code. let code = common::repo::compile_project(Path::new(PROJECT_PATH_2), "upgradable_2").await?; @@ -458,6 +473,94 @@ async fn test_deploy_code_and_call_method() -> anyhow::Result<()> { Ok(()) } +/// Deploys a new version of the contract that requires state migration and verifies the migration +/// succeeded. +#[tokio::test] +async fn test_deploy_code_with_migration() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let dao = worker.dev_create_account().await?; + let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?; + + // Verify function `is_migrated` is not defined in the initial contract. + let res = setup.call_is_migrated(&setup.unauth_account).await?; + assert_method_not_found_failure(res); + + // Compile the other version of the contract and stage its code. + let code = common::repo::compile_project( + Path::new(PROJECT_PATH_STATE_MIGRATION), + "upgradable_state_migration", + ) + .await?; + let res = setup + .upgradable_contract + .up_stage_code(&dao, code.clone()) + .await?; + assert_success_with_unit_return(res); + setup.assert_staged_code(Some(code)).await; + + // Deploy staged code and call the new contract's `migrate` method. + let function_call_args = FunctionCallArgs { + function_name: "migrate".to_string(), + arguments: Vec::new(), + amount: 0, + gas: Gas::ONE_TERA, + }; + let res = setup + .upgradable_contract + .up_deploy_code(&dao, Some(function_call_args)) + .await?; + assert_success_with_unit_return(res); + + // The newly deployed contract defines the function `is_migrated`. Calling it successfully + // verifies the staged contract is deployed and state migration succeeded. + let res = setup.call_is_migrated(&setup.unauth_account).await?; + assert_success_with(res, true); + + Ok(()) +} + +/// Deploys a new version of the contract and, batched with the `DeployContractAction`, calls a +/// migration method that fails. Verifies the failure rolls back the deployment, i.e. the initial +/// code remains active. +#[tokio::test] +async fn test_deploy_code_with_migration_failure_rollback() -> anyhow::Result<()> { + let worker = workspaces::sandbox().await?; + let dao = worker.dev_create_account().await?; + let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?; + + // Compile the other version of the contract and stage its code. + let code = common::repo::compile_project( + Path::new(PROJECT_PATH_STATE_MIGRATION), + "upgradable_state_migration", + ) + .await?; + let res = setup + .upgradable_contract + .up_stage_code(&dao, code.clone()) + .await?; + assert_success_with_unit_return(res); + setup.assert_staged_code(Some(code)).await; + + // Deploy staged code and call the new contract's `migrate_with_failure` method. + let function_call_args = FunctionCallArgs { + function_name: "migrate_with_failure".to_string(), + arguments: Vec::new(), + amount: 0, + gas: Gas::ONE_TERA, + }; + let res = setup + .upgradable_contract + .up_deploy_code(&dao, Some(function_call_args)) + .await?; + assert_failure_with(res, "Failing migration on purpose"); + + // Verify `code` wasn't deployed by calling a function that is defined only in the initial + // contract but not in the contract contract corresponding to `code`. + setup.assert_is_set_up(&setup.unauth_account).await; + + Ok(()) +} + #[tokio::test] async fn test_deploy_code_with_delay() -> anyhow::Result<()> { let worker = workspaces::sandbox().await?; @@ -516,6 +619,10 @@ async fn test_deploy_code_with_delay_failure_too_early() -> anyhow::Result<()> { let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?; assert_failure_with(res, ERR_MSG_DEPLOY_CODE_TOO_EARLY); + // Verify `code` wasn't deployed by calling a function that is defined only in the initial + // contract but not in the contract contract corresponding to `code`. + setup.assert_is_set_up(&setup.unauth_account).await; + Ok(()) } From 698295e698fc0054994d5f55471ca93d8ab08ea8 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 7 Mar 2023 16:23:08 +0100 Subject: [PATCH 5/5] docs: describe new behavior --- near-plugins/src/upgradable.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/near-plugins/src/upgradable.rs b/near-plugins/src/upgradable.rs index 50f9b85..9d8c8ff 100644 --- a/near-plugins/src/upgradable.rs +++ b/near-plugins/src/upgradable.rs @@ -23,10 +23,13 @@ //! method. The documentation of these methods and the [example contract] explain how to define and //! whitelist roles to manage authorization for the `Upgradable` plugin. //! -//! There may be several reasons to protect `deploy_code`. For example if an upgrade requires -//! migration or initialization. In that case, it is recommended to run a batched transaction where -//! [`Upgradable::up_deploy_code`] is called first, and then a function that executes the migration -//! or initialization. +//! ## State migration +//! +//! Upgrading a contract might require [state migration]. The `Upgradable` plugin allows to attach a +//! function call to code deployments. Using this mechanism, state migration can be carried out by +//! calling a migration function. If the function fails, the deployment is rolled back and the +//! initial code remains active. More detailed information is available in the documentation of +//! [`Upgradable::up_deploy_code`]. //! //! ## Stale staged code //! @@ -53,6 +56,7 @@ //! `near-plugins-derive` does not support it. //! //! [example contract]: ../../near-plugins-derive/tests/contracts/upgradable/src/lib.rs +//! [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state //! [batch transaction]: https://docs.near.org/concepts/basics/transactions/overview //! [time between scheduling and execution]: https://docs.near.org/sdk/rust/promises/intro use crate::events::{AsEvent, EventMetadata}; @@ -98,11 +102,24 @@ pub trait Upgradable { /// Allows an authorized account to deploy the staged code. It panics if no code is staged. /// + /// If `function_call_args` are provided, code is deployed in a batch promise that contains the + /// `DeployContractAction` followed by `FunctionCallAction`. In case the function call fails, + /// the deployment is rolled back and the initial code remains active. For this purpose, + /// batching the actions mentioned above is required due to the [asynchronous design] of NEAR. + /// + /// Attaching a function call can be useful, for example, if deploying the staged code requires + /// [state migration]. It can be achieved by calling a migration function defined in the new + /// version of the contract. A failure during state migration can leave the contract in a broken + /// state, which is avoided by the roleback mechanism described above. + /// /// In the default implementation, this method is protected by access control provided by the /// `AccessControllable` plugin. The roles which may successfully call this method are /// specified via the `code_deployers` field of the `Upgradable` macro's `access_control_roles` /// attribute. The example contract (accessible via the `README`) shows how access control roles /// can be defined and passed on to the `Upgradable` macro. + /// + /// [asynchronous design]: https://docs.near.org/concepts/basics/transactions/overview + /// [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state fn up_deploy_code(&mut self, function_call_args: Option) -> Promise; /// Initializes the duration of the delay for deploying the staged code. It defaults to zero if @@ -150,12 +167,17 @@ pub struct UpgradableDurationStatus { pub new_staging_duration_timestamp: Option, } -// TODO add docs +/// Specifies a function call to be appended to the actions of a promise via +/// [`near_sdk::Promise::function_call`]). #[derive(Deserialize, Serialize, Debug)] pub struct FunctionCallArgs { + /// The name of the function to call. pub function_name: String, + /// The arguments to pass to the function. pub arguments: Vec, + /// The amount of tokens to transfer to the receiver. pub amount: Balance, + /// The gas limit for the function call. pub gas: Gas, }