diff --git a/Cargo.lock b/Cargo.lock index bfef318334f..f70aa2a7657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11450,6 +11450,7 @@ dependencies = [ "prost 0.13.3", "rand 0.8.5", "rand_chacha 0.3.1", + "registry-canister", "reqwest 0.12.8", "rolling-file", "rosetta-core", diff --git a/rs/rosetta-api/icp/BUILD.bazel b/rs/rosetta-api/icp/BUILD.bazel index da51dfa29e6..388bfcbdfdb 100644 --- a/rs/rosetta-api/icp/BUILD.bazel +++ b/rs/rosetta-api/icp/BUILD.bazel @@ -79,6 +79,7 @@ DEV_DEPENDENCIES = [ "//rs/nns/governance/init", "//rs/nns/handlers/root/impl:root", "//rs/nns/test_utils", + "//rs/registry/canister", "//rs/rosetta-api/icp:rosetta-api", "//rs/rosetta-api/icp/client:ic-icp-rosetta-client", "//rs/rosetta-api/icp/ledger_canister_blocks_synchronizer/test_utils", @@ -168,9 +169,11 @@ rust_test_suite_with_extra_srcs( "//rs/canister_sandbox", "//rs/canister_sandbox:sandbox_launcher", "//rs/ledger_suite/icp/ledger:ledger-canister-wasm-notify-method", - "//rs/nns/governance:governance-canister", + "//rs/nns/governance:governance-canister-test", + "//rs/nns/handlers/lifeline/impl:lifeline_canister", "//rs/nns/handlers/root/impl:root-canister", "//rs/pocket_ic_server:pocket-ic-server", + "//rs/registry/canister:registry-canister", "//rs/replica", "//rs/rosetta-api/icp:ic-rosetta-api-rosetta-blocks", "//rs/rosetta-api/icp:rosetta-api", @@ -184,8 +187,10 @@ rust_test_suite_with_extra_srcs( "ROSETTA_BIN_PATH": "$(rootpath //rs/rosetta-api/icp:ic-rosetta-api-rosetta-blocks)", "SANDBOX_LAUNCHER": "$(rootpath //rs/canister_sandbox:sandbox_launcher)", "ICP_LEDGER_DEPLOYED_VERSION_WASM_PATH": "$(rootpath @mainnet_icp_ledger_canister//file)", - "GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister)", + "GOVERNANCE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/governance:governance-canister-test)", "ROOT_CANISTER_WASM_PATH": "$(rootpath //rs/nns/handlers/root/impl:root-canister)", + "REGISTRY_CANISTER_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", + "LIFELINE_CANISTER_WASM_PATH": "$(rootpath //rs/nns/handlers/lifeline/impl:lifeline_canister)", }, extra_srcs = glob([ "tests/system_tests/common/*.rs", diff --git a/rs/rosetta-api/icp/Cargo.toml b/rs/rosetta-api/icp/Cargo.toml index baf8963db53..a013e7f430c 100644 --- a/rs/rosetta-api/icp/Cargo.toml +++ b/rs/rosetta-api/icp/Cargo.toml @@ -38,6 +38,7 @@ num-bigint = { workspace = true } on_wire = { path = "../../rust_canisters/on_wire" } prometheus = { workspace = true } rand = { workspace = true } +registry-canister = { path = "../../registry/canister" } reqwest = { workspace = true } rolling-file = { workspace = true } rosetta-core = { path = "../common/rosetta_core" } diff --git a/rs/rosetta-api/icp/client/src/lib.rs b/rs/rosetta-api/icp/client/src/lib.rs index e5e0fe455f9..934728fbbc7 100644 --- a/rs/rosetta-api/icp/client/src/lib.rs +++ b/rs/rosetta-api/icp/client/src/lib.rs @@ -11,6 +11,7 @@ use ic_rosetta_api::models::ConstructionMetadataRequestOptions; use ic_rosetta_api::models::ConstructionPayloadsRequestMetadata; use ic_rosetta_api::models::OperationIdentifier; use ic_rosetta_api::request_types::ChangeAutoStakeMaturityMetadata; +use ic_rosetta_api::request_types::DisburseMetadata; use ic_rosetta_api::request_types::NeuronIdentifierMetadata; use ic_rosetta_api::request_types::RequestType; use ic_rosetta_api::request_types::SetDissolveTimestampMetadata; @@ -327,6 +328,35 @@ impl RosettaClient { }]) } + pub async fn build_disburse_neuron_operations( + signer_principal: Principal, + neuron_index: u64, + recipient: Option, + ) -> anyhow::Result> { + Ok(vec![Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type_: "DISBURSE".to_string(), + status: None, + account: Some(rosetta_core::identifiers::AccountIdentifier::from( + AccountIdentifier::new(PrincipalId(signer_principal), None), + )), + amount: None, + coin_change: None, + metadata: Some( + DisburseMetadata { + neuron_index, + recipient, + } + .try_into() + .map_err(|e| anyhow::anyhow!("Failed to convert metadata: {:?}", e))?, + ), + }]) + } + pub async fn network_list(&self) -> anyhow::Result { self.call_endpoint("/network/list", &MetadataRequest { metadata: None }) .await @@ -899,6 +929,33 @@ impl RosettaClient { ) .await } + + /// If a neuron is in the state DISSOLVED you can disburse the neuron with this function. + pub async fn disburse_neuron( + &self, + network_identifier: NetworkIdentifier, + signer_keypair: &T, + disburse_neuron_args: RosettaDisburseNeuronArgs, + ) -> anyhow::Result + where + T: RosettaSupportedKeyPair, + { + let disburse_neuron_operations = RosettaClient::build_disburse_neuron_operations( + signer_keypair.generate_principal_id()?.0, + disburse_neuron_args.neuron_index, + disburse_neuron_args.recipient, + ) + .await?; + + self.make_submit_and_wait_for_transaction( + signer_keypair, + network_identifier, + disburse_neuron_operations, + None, + None, + ) + .await + } } pub struct RosettaTransferArgs { @@ -1083,6 +1140,41 @@ impl RosettaIncreaseNeuronStakeArgs { } } +pub struct RosettaIncreaseNeuronStakeArgsBuilder { + additional_stake: Nat, + neuron_index: Option, + // The subaccount from which the ICP should be transferred + from_subaccount: Option<[u8; 32]>, +} + +impl RosettaIncreaseNeuronStakeArgsBuilder { + pub fn new(additional_stake: Nat) -> Self { + Self { + additional_stake, + neuron_index: None, + from_subaccount: None, + } + } + + pub fn with_neuron_index(mut self, neuron_index: u64) -> Self { + self.neuron_index = Some(neuron_index); + self + } + + pub fn with_from_subaccount(mut self, from_subaccount: Subaccount) -> Self { + self.from_subaccount = Some(from_subaccount); + self + } + + pub fn build(self) -> RosettaIncreaseNeuronStakeArgs { + RosettaIncreaseNeuronStakeArgs { + additional_stake: self.additional_stake, + neuron_index: self.neuron_index, + from_subaccount: self.from_subaccount, + } + } +} + pub struct RosettaChangeAutoStakeMaturityArgs { pub neuron_index: Option, pub requested_setting_for_auto_stake_maturity: bool, @@ -1122,38 +1214,39 @@ impl RosettaChangeAutoStakeMaturityArgsBuilder { } } } +pub struct RosettaDisburseNeuronArgs { + pub neuron_index: u64, + pub recipient: Option, +} -pub struct RosettaIncreaseNeuronStakeArgsBuilder { - additional_stake: Nat, - neuron_index: Option, - // The subaccount from which the ICP should be transferred - from_subaccount: Option<[u8; 32]>, +impl RosettaDisburseNeuronArgs { + pub fn builder(neuron_index: u64) -> RosettaDisburseNeuronArgsBuilder { + RosettaDisburseNeuronArgsBuilder::new(neuron_index) + } } -impl RosettaIncreaseNeuronStakeArgsBuilder { - pub fn new(additional_stake: Nat) -> Self { +pub struct RosettaDisburseNeuronArgsBuilder { + neuron_index: u64, + recipient: Option, +} + +impl RosettaDisburseNeuronArgsBuilder { + pub fn new(neuron_index: u64) -> Self { Self { - additional_stake, - neuron_index: None, - from_subaccount: None, + neuron_index, + recipient: None, } } - pub fn with_neuron_index(mut self, neuron_index: u64) -> Self { - self.neuron_index = Some(neuron_index); + pub fn with_recipient(mut self, recipient: AccountIdentifier) -> Self { + self.recipient = Some(recipient); self } - pub fn with_from_subaccount(mut self, from_subaccount: Subaccount) -> Self { - self.from_subaccount = Some(from_subaccount); - self - } - - pub fn build(self) -> RosettaIncreaseNeuronStakeArgs { - RosettaIncreaseNeuronStakeArgs { - additional_stake: self.additional_stake, + pub fn build(self) -> RosettaDisburseNeuronArgs { + RosettaDisburseNeuronArgs { neuron_index: self.neuron_index, - from_subaccount: self.from_subaccount, + recipient: self.recipient, } } } diff --git a/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs b/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs index 4dd59ea837d..a27b8c7e2fa 100644 --- a/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs +++ b/rs/rosetta-api/icp/tests/system_tests/common/system_test_environment.rs @@ -1,5 +1,6 @@ use crate::common::utils::get_custom_agent; -use crate::common::utils::wait_for_rosetta_to_sync_up_to_block; +use crate::common::utils::get_test_agent; +use crate::common::utils::wait_for_rosetta_to_catch_up_with_icp_ledger; use crate::common::{ constants::{DEFAULT_INITIAL_BALANCE, STARTING_CYCLES_PER_CANISTER}, utils::test_identity, @@ -16,12 +17,16 @@ use ic_icrc1_test_utils::LedgerEndpointArg; use ic_icrc1_tokens_u256::U256; use ic_ledger_test_utils::build_ledger_wasm; use ic_ledger_test_utils::pocket_ic_helpers::ledger::LEDGER_CANISTER_ID; +use ic_nns_common::init::LifelineCanisterInitPayloadBuilder; use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_nns_constants::LIFELINE_CANISTER_ID; +use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_constants::ROOT_CANISTER_ID; use ic_nns_governance_init::GovernanceCanisterInitPayloadBuilder; use ic_nns_handler_root::init::RootCanisterInitPayloadBuilder; use ic_nns_test_utils::common::build_governance_wasm; +use ic_nns_test_utils::common::build_lifeline_wasm; +use ic_nns_test_utils::common::build_registry_wasm; use ic_nns_test_utils::common::build_root_wasm; use ic_rosetta_test_utils::path_from_env; use ic_types::PrincipalId; @@ -32,6 +37,7 @@ use num_traits::cast::ToPrimitive; use pocket_ic::CanisterSettings; use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; use prost::Message; +use registry_canister::init::RegistryCanisterInitPayloadBuilder; use rosetta_core::identifiers::NetworkIdentifier; use std::collections::HashMap; use tempfile::TempDir; @@ -132,14 +138,6 @@ impl RosettaTestingEnvironment { } pub async fn restart_rosetta_node(mut self, options: RosettaOptions) -> Self { - let ledger_tip = self - .rosetta_client - .network_status(self.network_identifier.clone()) - .await - .unwrap() - .current_block_identifier - .index; - self.rosetta_context.kill_rosetta_process(); let rosetta_bin = path_from_env("ROSETTA_BIN_PATH"); @@ -149,13 +147,12 @@ impl RosettaTestingEnvironment { self.rosetta_client = RosettaClient::from_str_url(&format!("http://localhost:{}", self.rosetta_context.port)) .expect("Unable to parse url"); - wait_for_rosetta_to_sync_up_to_block( + wait_for_rosetta_to_catch_up_with_icp_ledger( &self.rosetta_client, self.network_identifier.clone(), - ledger_tip, + &get_test_agent(self.pocket_ic.url().unwrap().port().unwrap()).await, ) - .await - .unwrap(); + .await; self } } @@ -280,7 +277,9 @@ impl RosettaTestingEnvironmentBuilder { Some(nns_root_canister_controller), ) .await; - + pocket_ic + .add_cycles(nns_root_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; let governance_canister_wasm = build_governance_wasm(); let governance_canister_id = Principal::from(GOVERNANCE_CANISTER_ID); let governance_canister_controller = ROOT_CANISTER_ID.get().0; @@ -316,6 +315,61 @@ impl RosettaTestingEnvironmentBuilder { .advance_time(std::time::Duration::from_secs(60)) .await; pocket_ic.tick().await; + + let nns_lifeline_canister_wasm = build_lifeline_wasm(); + let nns_lifeline_canister_id = Principal::from(LIFELINE_CANISTER_ID); + let nns_lifeline_canister_controller = ROOT_CANISTER_ID.get().0; + let nns_lifeline_canister = pocket_ic + .create_canister_with_id( + Some(nns_lifeline_canister_controller), + Some(CanisterSettings { + controllers: Some(vec![nns_lifeline_canister_controller]), + ..Default::default() + }), + nns_lifeline_canister_id, + ) + .await + .expect("Unable to create the NNS Lifeline canister"); + + pocket_ic + .install_canister( + nns_lifeline_canister, + nns_lifeline_canister_wasm.bytes().to_vec(), + Encode!(&LifelineCanisterInitPayloadBuilder::new().build()).unwrap(), + Some(nns_lifeline_canister_controller), + ) + .await; + pocket_ic + .add_cycles(nns_lifeline_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; + + let nns_registry_canister_wasm = build_registry_wasm(); + let nns_registry_canister_id = Principal::from(REGISTRY_CANISTER_ID); + let nns_registry_canister_controller = ROOT_CANISTER_ID.get().0; + let nns_registry_canister = pocket_ic + .create_canister_with_id( + Some(nns_registry_canister_controller), + Some(CanisterSettings { + controllers: Some(vec![nns_registry_canister_controller]), + ..Default::default() + }), + nns_registry_canister_id, + ) + .await + .expect("Unable to create the NNS Registry canister"); + + pocket_ic + .install_canister( + nns_registry_canister, + nns_registry_canister_wasm.bytes().to_vec(), + Encode!(&RegistryCanisterInitPayloadBuilder::new().build()).unwrap(), + Some(nns_registry_canister_controller), + ) + .await; + + pocket_ic + .add_cycles(nns_registry_canister_id, STARTING_CYCLES_PER_CANISTER) + .await; } let replica_url = pocket_ic.make_live(None).await; @@ -384,19 +438,12 @@ impl RosettaTestingEnvironmentBuilder { .unwrap(); // Wait for rosetta to catch up with the ledger - if let Some(last_block_idx) = block_idxes.last() { - let rosetta_last_block_idx = wait_for_rosetta_to_sync_up_to_block( - &rosetta_client, - network_identifier.clone(), - *last_block_idx, - ) - .await; - assert_eq!( - Some(*last_block_idx), - rosetta_last_block_idx, - "Wait for rosetta sync failed." - ); - } + wait_for_rosetta_to_catch_up_with_icp_ledger( + &rosetta_client, + network_identifier.clone(), + &get_test_agent(replica_port).await, + ) + .await; RosettaTestingEnvironment { pocket_ic, diff --git a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs index ecaf0867c0e..1d98d82455b 100644 --- a/rs/rosetta-api/icp/tests/system_tests/common/utils.rs +++ b/rs/rosetta-api/icp/tests/system_tests/common/utils.rs @@ -10,6 +10,7 @@ use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_nns_constants::LEDGER_CANISTER_ID; use ic_nns_governance::pb::v1::ListNeurons; use ic_nns_governance::pb::v1::ListNeuronsResponse; +use ic_nns_governance_api::pb::v1::GovernanceError; use ic_rosetta_api::convert::to_hash; use icp_ledger::GetBlocksArgs; use icp_ledger::QueryEncodedBlocksResponse; @@ -190,3 +191,17 @@ pub async fn list_neurons(agent: &Agent) -> ListNeuronsResponse { ) .unwrap() } + +pub async fn update_neuron(agent: &Agent, neuron: ic_nns_governance_api::pb::v1::Neuron) { + let result = Decode!( + &agent + .update(&GOVERNANCE_CANISTER_ID.into(), "update_neuron") + .with_arg(Encode!(&neuron).unwrap()) + .call_and_wait() + .await + .unwrap(), + Option + ) + .unwrap(); + assert!(result.is_none(), "Failed to update neuron: {:?}", result); +} diff --git a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs index 248b1e8b8b6..c59a09c3453 100644 --- a/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs +++ b/rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs @@ -1,3 +1,4 @@ +use crate::common::utils::update_neuron; use crate::common::utils::wait_for_rosetta_to_catch_up_with_icp_ledger; use crate::common::{ system_test_environment::RosettaTestingEnvironment, @@ -6,14 +7,17 @@ use crate::common::{ use ic_agent::{identity::BasicIdentity, Identity}; use ic_icp_rosetta_client::RosettaChangeAutoStakeMaturityArgs; use ic_icp_rosetta_client::RosettaIncreaseNeuronStakeArgs; -use ic_icp_rosetta_client::{RosettaCreateNeuronArgs, RosettaSetNeuronDissolveDelayArgs}; +use ic_icp_rosetta_client::{ + RosettaCreateNeuronArgs, RosettaDisburseNeuronArgs, RosettaSetNeuronDissolveDelayArgs, +}; use ic_nns_governance::pb::v1::neuron::DissolveState; -use ic_rosetta_api::request::transaction_operation_results::TransactionOperationResults; +use ic_rosetta_api::{ + models::AccountBalanceRequest, + request::transaction_operation_results::TransactionOperationResults, +}; use ic_types::PrincipalId; -use icp_ledger::AccountIdentifier; -use icp_ledger::DEFAULT_TRANSFER_FEE; +use icp_ledger::{AccountIdentifier, DEFAULT_TRANSFER_FEE}; use lazy_static::lazy_static; -use rosetta_core::request_types::AccountBalanceRequest; use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, @@ -499,3 +503,151 @@ fn test_change_auto_stake_maturity() { assert!(neuron.auto_stake_maturity.is_none()); }); } + +#[test] +fn test_disburse_neuron() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let initial_balance = 100_000_000_000; + let env = RosettaTestingEnvironment::builder() + .with_initial_balances( + vec![( + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()), + // A hundred million ICP should be enough + icp_ledger::Tokens::from_e8s(initial_balance), + )] + .into_iter() + .collect(), + ) + .with_governance_canister() + .build() + .await; + + // Stake the minimum amount 100 million e8s + let staked_amount = initial_balance/10; + let neuron_index = 0; + let from_subaccount = [0; 32]; + + env.rosetta_client + .create_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaCreateNeuronArgs::builder(staked_amount.into()) + .with_from_subaccount(from_subaccount) + .with_neuron_index(neuron_index) + .build(), + ) + .await + .unwrap(); + // See if the neuron was created successfully + let agent = get_test_agent(env.pocket_ic.url().unwrap().port().unwrap()).await; + + TransactionOperationResults::try_from( + env.rosetta_client + .start_dissolving_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + neuron_index, + ) + .await + .unwrap() + .metadata, + ) + .unwrap(); + + let mut neuron = list_neurons(&agent).await.full_neurons[0].to_owned(); + // If we try to disburse the neuron when it is not yet DISSOLVED we expect an error + match env + .rosetta_client + .disburse_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaDisburseNeuronArgs::builder(neuron_index) + .with_recipient(TEST_IDENTITY.sender().unwrap().into()) + .build(), + ) + .await + { + Err(e) if e.to_string().contains(&format!("Could not disburse: PreconditionFailed: Neuron {} has NOT been dissolved. It is in state Dissolving",neuron.id.unwrap().id)) => (), + Err(e) => panic!("Unexpected error: {}", e), + Ok(_) => panic!("Expected an error but got success"), + } + // Let rosetta catch up with the transfer that happended when creating the neuron + wait_for_rosetta_to_catch_up_with_icp_ledger( + &env.rosetta_client, + env.network_identifier.clone(), + &agent, + ).await; + let balance_before_disburse = env + .rosetta_client + .account_balance( + AccountBalanceRequest::builder( + env.network_identifier.clone(), + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()).into(), + ) + .build(), + ) + .await + .unwrap() + .balances + .first() + .unwrap() + .clone() + .value.parse::().unwrap(); + + // We now update the neuron so it is in state DISSOLVED + let now = env.pocket_ic.get_time().await.duration_since(UNIX_EPOCH).unwrap().as_secs(); + neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds(now - 1)); + update_neuron(&agent, neuron.into()).await; + + match list_neurons(&agent).await.full_neurons[0].dissolve_state.unwrap() { + DissolveState::WhenDissolvedTimestampSeconds (d) => { + // The neuron should now be in DISSOLVED state + assert!(d panic!( + "Neuron should be in DissolveDelaySeconds state, but is instead: {:?}", + k + ), + } + + // Now we should be able to disburse the neuron + env.rosetta_client + .disburse_neuron( + env.network_identifier.clone(), + &(*TEST_IDENTITY).clone(), + RosettaDisburseNeuronArgs::builder(neuron_index) + .with_recipient(TEST_IDENTITY.sender().unwrap().into()) + .build(), + ) + .await + .unwrap(); + + // Wait for the ledger to sync up to the block where the disbursement happened + wait_for_rosetta_to_catch_up_with_icp_ledger( + &env.rosetta_client, + env.network_identifier.clone(), + &agent, + ) + .await; + + // The recipient should have received the disbursed amount + let balance_after_disburse = env + .rosetta_client + .account_balance( + AccountBalanceRequest::builder( + env.network_identifier.clone(), + AccountIdentifier::from(TEST_IDENTITY.sender().unwrap()).into(), + ) + .build(), + ) + .await + .unwrap() + .balances + .first() + .unwrap().clone() + .value.parse::().unwrap(); + // The balance should be the same as before the creation of the neuron minus the transfer fee + assert_eq!(balance_after_disburse, balance_before_disburse + staked_amount - DEFAULT_TRANSFER_FEE.get_e8s()); + }); +} diff --git a/rs/tests/Cargo.toml b/rs/tests/Cargo.toml index 49eb5b53ddb..6051f67739b 100644 --- a/rs/tests/Cargo.toml +++ b/rs/tests/Cargo.toml @@ -195,10 +195,6 @@ path = "financial_integrations/rosetta/rosetta_make_transactions_test.rs" name = "ic-systest-rosetta-network-test" path = "financial_integrations/rosetta/rosetta_network_test.rs" -[[bin]] -name = "ic-systest-rosetta-neuron-disburse-test" -path = "financial_integrations/rosetta/rosetta_neuron_disburse_test.rs" - [[bin]] name = "ic-systest-rosetta-neuron-dissolve-test" path = "financial_integrations/rosetta/rosetta_neuron_dissolve_test.rs" diff --git a/rs/tests/financial_integrations/rosetta/BUILD.bazel b/rs/tests/financial_integrations/rosetta/BUILD.bazel index e8792bcc3cb..8646f37569f 100644 --- a/rs/tests/financial_integrations/rosetta/BUILD.bazel +++ b/rs/tests/financial_integrations/rosetta/BUILD.bazel @@ -78,27 +78,6 @@ system_test_nns( deps = DEPENDENCIES + ["//rs/tests"], ) -system_test_nns( - name = "rosetta_neuron_disburse_test", - extra_head_nns_tags = [], # don't run the head_nns variant on nightly since it aleady runs on long_test. - flaky = True, - proc_macro_deps = MACRO_DEPENDENCIES, - tags = [ - "k8s", - "long_test", # since it takes longer than 5 minutes. - ], - target_compatible_with = ["@platforms//os:linux"], # requires libssh that does not build on Mac OS - runtime_deps = - GUESTOS_RUNTIME_DEPS + - UNIVERSAL_VM_RUNTIME_DEPS + [ - "//rs/rosetta-api/icp:ic-rosetta-api", - "//rs/rosetta-api/icp:rosetta_image.tar", - "//rs/tests:rosetta_workspace", - "@rosetta-cli//:rosetta-cli", - ], - deps = DEPENDENCIES + ["//rs/tests"], -) - system_test_nns( name = "rosetta_neuron_dissolve_test", flaky = True, diff --git a/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs b/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs deleted file mode 100644 index 4a738f442f8..00000000000 --- a/rs/tests/financial_integrations/rosetta/rosetta_neuron_disburse_test.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[rustfmt::skip] -use anyhow::Result; - -use ic_system_test_driver::driver::group::SystemTestGroup; -use ic_system_test_driver::driver::test_env::TestEnv; -use ic_system_test_driver::systest; -use ic_tests::rosetta_tests; -use rosetta_tests::setup::{ROSETTA_TESTS_OVERALL_TIMEOUT, ROSETTA_TESTS_PER_TEST_TIMEOUT}; -use rosetta_tests::tests; - -fn main() -> Result<()> { - SystemTestGroup::new() - .with_setup(group_setup) - .with_overall_timeout(ROSETTA_TESTS_OVERALL_TIMEOUT) - .with_timeout_per_test(ROSETTA_TESTS_PER_TEST_TIMEOUT) - .add_test(systest!(tests::neuron_disburse::test)) - .execute_from_args()?; - Ok(()) -} - -fn group_setup(_env: TestEnv) {} diff --git a/rs/tests/src/rosetta_tests/tests.rs b/rs/tests/src/rosetta_tests/tests.rs index 4e6485e0135..58d4cea2c0b 100644 --- a/rs/tests/src/rosetta_tests/tests.rs +++ b/rs/tests/src/rosetta_tests/tests.rs @@ -3,7 +3,6 @@ pub mod list_known_neurons; pub mod list_neurons; pub mod make_transaction; pub mod network; -pub mod neuron_disburse; pub mod neuron_dissolve; pub mod neuron_follow; pub mod neuron_hotkey; diff --git a/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs b/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs deleted file mode 100644 index 4e97ad50bdb..00000000000 --- a/rs/tests/src/rosetta_tests/tests/neuron_disburse.rs +++ /dev/null @@ -1,300 +0,0 @@ -use crate::rosetta_tests::{ - ledger_client::LedgerClient, - lib::{ - check_balance, create_ledger_client, do_multiple_txn, make_user_ed25519, - one_day_from_now_nanos, sign_txn, to_public_key, NeuronDetails, - }, - rosetta_client::RosettaApiClient, - setup::setup, - test_neurons::TestNeurons, -}; -use ic_ledger_core::{ - tokens::{CheckedAdd, CheckedSub}, - Tokens, -}; -use ic_nns_governance_api::pb::v1::{neuron::DissolveState, Neuron}; -use ic_rosetta_api::{ - models::{ConstructionPayloadsResponse, SignedTransaction}, - request::{ - request_result::RequestResult, transaction_operation_results::TransactionOperationResults, - Request, - }, - request_types::{Disburse, Status}, -}; -use ic_rosetta_test_utils::RequestInfo; -use ic_system_test_driver::{driver::test_env::TestEnv, util::block_on}; -use icp_ledger::{AccountIdentifier, DEFAULT_TRANSFER_FEE}; -use rosetta_core::objects::ObjectMap; -use serde_json::json; -use slog::Logger; -use std::{collections::HashMap, str::FromStr, sync::Arc, time::UNIX_EPOCH}; - -const PORT: u32 = 8104; -const VM_NAME: &str = "neuron-disburse"; - -pub fn test(env: TestEnv) { - let logger = env.logger(); - - let mut ledger_balances = HashMap::new(); - - // Create neurons. - let neuron_setup = |neuron: &mut Neuron| { - neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds(0)) - }; - - let one_year_from_now = 60 * 60 * 24 * 365 - + std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let mut neurons = TestNeurons::new(2000, &mut ledger_balances); - - let neuron1 = neurons.create(neuron_setup); - let neuron2 = neurons.create(neuron_setup); - let neuron3 = neurons.create(neuron_setup); - let neuron4 = neurons.create(|neuron| { - neuron.dissolve_state = Some(DissolveState::WhenDissolvedTimestampSeconds( - one_year_from_now, - )) - }); - let neuron5 = neurons.create(neuron_setup); - let neuron6 = neurons.create(neuron_setup); - let neuron7 = neurons.create(neuron_setup); - - // Create Rosetta and ledger clients. - let neurons = neurons.get_neurons(); - let client = setup(&env, PORT, VM_NAME, Some(ledger_balances), Some(neurons)); - let ledger_client = create_ledger_client(&env, &client); - - block_on(async { - test_disburse_raw(&client, &ledger_client, &neuron1, None, None, &logger) - .await - .expect("Failed test raw disburse"); - - test_disburse(&client, &ledger_client, &neuron2, None, None) - .await - .expect("Failed test disburse"); - - // Disburse to custom recipient. - let (recipient, _, _, _) = make_user_ed25519(102); - test_disburse(&client, &ledger_client, &neuron3, None, Some(recipient)) - .await - .expect("Failed test disburse to custom recipient"); - - // Disburse before neuron is dissolved (fail expected). - test_disburse(&client, &ledger_client, &neuron4, None, Some(recipient)) - .await - .unwrap_err(); - - // Disburse an amount. - test_disburse( - &client, - &ledger_client, - &neuron5, - Some(Tokens::new(5, 0).unwrap()), - None, - ) - .await - .expect("Failed test disburse an amount"); - - // Disburse full stake. - test_disburse( - &client, - &ledger_client, - &neuron6, - Some(Tokens::new(10, 0).unwrap()), - None, - ) - .await - .expect("Failed test disburse full stake"); - - // Disburse more than staked amount. - test_disburse( - &client, - &ledger_client, - &neuron7, - Some(Tokens::new(11, 0).unwrap()), - None, - ) - .await - .unwrap_err() - }); -} - -#[allow(clippy::too_many_arguments)] -async fn test_disburse( - ros: &RosettaApiClient, - ledger_client: &LedgerClient, - neuron_info: &NeuronDetails, - amount: Option, - recipient: Option, -) -> Result<(), ic_rosetta_api::models::Error> { - let neuron = &neuron_info.neuron; - let acc = neuron_info.account_id; - let key_pair = Arc::new(neuron_info.key_pair.clone()); - let neuron_index = neuron_info.neuron_subaccount_identifier; - - let pre_disburse = ledger_client.get_account_balance(acc).await; - let (_, tip_idx) = ledger_client.get_tip().await; - - let res = do_multiple_txn( - ros, - &[RequestInfo { - request: Request::Disburse(Disburse { - account: acc, - amount, - recipient, - neuron_index, - }), - sender_keypair: Arc::clone(&key_pair), - }], - false, - Some(one_day_from_now_nanos()), - None, - ) - .await - .map(|(tx_id, results, _)| { - assert!(!tx_id.is_transfer()); - assert!(matches!( - results.operations.first().unwrap(), - RequestResult { - _type: Request::Disburse(_), - status: Status::Completed, - .. - } - )); - results - })?; - - let amount = amount.unwrap_or_else(|| Tokens::from_e8s(neuron.cached_neuron_stake_e8s)); - - let expected_idx = tip_idx + 1; - - if let Some(h) = res.last_block_index() { - assert_eq!(h, expected_idx); - } - let _ = ros.wait_for_block_at(expected_idx).await.unwrap(); - - // governance assumes the default fee for disburse and that's why this check uses the - // DEFAULT_TRANSFER_FEE. - check_balance( - ros, - ledger_client, - &recipient.unwrap_or(acc), - pre_disburse - .checked_add(&amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn test_disburse_raw( - ros: &RosettaApiClient, - ledger_client: &LedgerClient, - neuron_info: &NeuronDetails, - amount: Option, - recipient: Option, - _logger: &Logger, -) -> Result<(), ic_rosetta_api::models::Error> { - let neuron = &neuron_info.neuron; - let acc = neuron_info.account_id; - let key_pair = Arc::new(neuron_info.key_pair.clone()); - let neuron_index = neuron_info.neuron_subaccount_identifier; - - let pre_disburse = ledger_client.get_account_balance(acc).await; - let (_, tip_idx) = ledger_client.get_tip().await; - let req = json!({ - "network_identifier": &ros.network_id(), - "operations": [ - { - "operation_identifier": { - "index": 0 - }, - "type": "DISBURSE", - "account": { - "address": &acc - }, - "metadata": { - "neuron_index": &neuron_index - } - } - ] - }); - let req = req.to_string(); - - let metadata: ObjectMap = serde_json::from_slice( - &ros.raw_construction_endpoint("metadata", req.as_bytes()) - .await - .unwrap() - .0, - ) - .unwrap(); - - let mut req: ObjectMap = serde_json::from_str(&req).unwrap(); - req.insert("metadata".to_string(), metadata.into()); - req.insert( - "public_keys".to_string(), - serde_json::to_value(vec![to_public_key(&key_pair)]).unwrap(), - ); - - let payloads: ConstructionPayloadsResponse = serde_json::from_slice( - &ros.raw_construction_endpoint("payloads", &serde_json::to_vec_pretty(&req).unwrap()) - .await - .unwrap() - .0, - ) - .unwrap(); - - let signed = sign_txn(ros, &[key_pair.clone()], payloads).await.unwrap(); - - let hash_res = ros - .construction_hash(signed.signed_transaction.clone()) - .await - .unwrap()?; - - let submit_res = ros - .construction_submit(SignedTransaction::from_str(&signed.signed_transaction).unwrap()) - .await - .unwrap()?; - - assert_eq!( - hash_res.transaction_identifier, - submit_res.transaction_identifier - ); - - for op in TransactionOperationResults::try_from(submit_res.metadata) - .unwrap() - .operations - .iter() - { - assert_eq!( - op.status.as_ref().expect("Expecting status to be set."), - "COMPLETED", - "Operation didn't complete." - ); - } - - let amount = amount.unwrap_or_else(|| Tokens::from_e8s(neuron.cached_neuron_stake_e8s)); - let expected_idx = tip_idx + 1; - let _ = ros.wait_for_block_at(expected_idx).await.unwrap(); - - // governance assumes the default fee for disburse and that's why this check uses the - // DEFAULT_TRANSFER_FEE. - check_balance( - ros, - ledger_client, - &recipient.unwrap_or(acc), - pre_disburse - .checked_add(&amount) - .unwrap() - .checked_sub(&DEFAULT_TRANSFER_FEE) - .unwrap(), - ) - .await; - Ok(()) -}