Skip to content

Commit

Permalink
feat(ICP-Rosetta): FI-1535 function for dissolving a neuron (#2107)
Browse files Browse the repository at this point in the history
This MR proposes the following changes:

1. Add a function to start dissolving a neuron to the ICP rosetta client
2. Add a function to stop dissolving a neuron tot he ICP rosetta client

---------

Co-authored-by: Andre Popovitch <andre@popovit.ch>
Co-authored-by: Mathias Björkqvist <mathias.bjorkqvist@dfinity.org>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 23afeac commit 9cc994d
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
104 changes: 104 additions & 0 deletions rs/rosetta-api/icp/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,56 @@ impl RosettaClient {
}])
}

pub async fn build_start_dissolving_operations(
signer_principal: Principal,
neuron_index: u64,
) -> anyhow::Result<Vec<Operation>> {
Ok(vec![Operation {
operation_identifier: OperationIdentifier {
index: 0,
network_index: None,
},
related_operations: None,
type_: "START_DISSOLVING".to_string(),
status: None,
account: Some(rosetta_core::identifiers::AccountIdentifier::from(
AccountIdentifier::new(PrincipalId(signer_principal), None),
)),
amount: None,
coin_change: None,
metadata: Some(
NeuronIdentifierMetadata { neuron_index }
.try_into()
.map_err(|e| anyhow::anyhow!("Failed to convert metadata: {:?}", e))?,
),
}])
}

pub async fn build_stop_dissolving_operations(
signer_principal: Principal,
neuron_index: u64,
) -> anyhow::Result<Vec<Operation>> {
Ok(vec![Operation {
operation_identifier: OperationIdentifier {
index: 0,
network_index: None,
},
related_operations: None,
type_: "STOP_DISSOLVING".to_string(),
status: None,
account: Some(rosetta_core::identifiers::AccountIdentifier::from(
AccountIdentifier::new(PrincipalId(signer_principal), None),
)),
amount: None,
coin_change: None,
metadata: Some(
NeuronIdentifierMetadata { neuron_index }
.try_into()
.map_err(|e| anyhow::anyhow!("Failed to convert metadata: {:?}", e))?,
),
}])
}

pub async fn network_list(&self) -> anyhow::Result<NetworkListResponse> {
self.call_endpoint("/network/list", &MetadataRequest { metadata: None })
.await
Expand Down Expand Up @@ -714,6 +764,60 @@ impl RosettaClient {
)
.await
}

/// If a neuron is in the state NOT_DISSOLVING you start the dissolving process with this function.
/// The neuron will then move to the DISSOLVING state.
pub async fn start_dissolving_neuron<T>(
&self,
network_identifier: NetworkIdentifier,
signer_keypair: &T,
neuron_index: u64,
) -> anyhow::Result<ConstructionSubmitResponse>
where
T: RosettaSupportedKeyPair,
{
let start_dissolving_operations = RosettaClient::build_start_dissolving_operations(
signer_keypair.generate_principal_id()?.0,
neuron_index,
)
.await?;

self.make_submit_and_wait_for_transaction(
signer_keypair,
network_identifier,
start_dissolving_operations,
None,
None,
)
.await
}

/// If a neuron is in the state DISSOLVING you can stop the dissolving process with this function.
/// The neuron will then move to the NOT_DISSOLVING state.
pub async fn stop_dissolving_neuron<T>(
&self,
network_identifier: NetworkIdentifier,
signer_keypair: &T,
neuron_index: u64,
) -> anyhow::Result<ConstructionSubmitResponse>
where
T: RosettaSupportedKeyPair,
{
let stop_dissolving_operations = RosettaClient::build_stop_dissolving_operations(
signer_keypair.generate_principal_id()?.0,
neuron_index,
)
.await?;

self.make_submit_and_wait_for_transaction(
signer_keypair,
network_identifier,
stop_dissolving_operations,
None,
None,
)
.await
}
}

pub struct RosettaTransferArgs {
Expand Down
130 changes: 130 additions & 0 deletions rs/rosetta-api/icp/tests/system_tests/test_cases/neuron_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::common::{
use ic_agent::{identity::BasicIdentity, Identity};
use ic_icp_rosetta_client::{RosettaCreateNeuronArgs, RosettaSetNeuronDissolveDelayArgs};
use ic_nns_governance::pb::v1::neuron::DissolveState;
use ic_rosetta_api::request::transaction_operation_results::TransactionOperationResults;
use ic_types::PrincipalId;
use icp_ledger::AccountIdentifier;
use lazy_static::lazy_static;
Expand Down Expand Up @@ -158,3 +159,132 @@ fn test_set_neuron_dissolve_delay_timestamp() {
assert!(dissolve_delay_timestamp > 0);
});
}

#[test]
fn test_start_and_stop_neuron_dissolve() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
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_tokens(100_000_000).unwrap(),
)]
.into_iter()
.collect(),
)
.with_governance_canister()
.build()
.await;

// Stake the minimum amount 100 million e8s
let staked_amount = 100_000_000u64;
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;
let neuron = list_neurons(&agent).await.full_neurons[0].to_owned();
let dissolve_delay_timestamp = match neuron.dissolve_state.unwrap() {
// When a neuron is created its dissolve delay timestamp is set to two weeks from now and is in NOT DISSOLVING state
DissolveState::DissolveDelaySeconds(dissolve_delay_timestamp) => {
dissolve_delay_timestamp
}
k => panic!(
"Neuron should be in DissolveDelaySeconds state, but is instead: {:?}",
k
),
};
let start_dissolving_response = TransactionOperationResults::try_from(
env.rosetta_client
.start_dissolving_neuron(
env.network_identifier.clone(),
&(*TEST_IDENTITY).clone(),
neuron_index,
)
.await
.unwrap()
.metadata,
)
.unwrap();

// The neuron should now be in DISSOLVING state
assert_eq!(
start_dissolving_response.operations.first().unwrap().status,
Some("COMPLETED".to_owned())
);
let neuron = list_neurons(&agent).await.full_neurons[0].to_owned();
match neuron.dissolve_state.unwrap() {
DissolveState::WhenDissolvedTimestampSeconds(d) => {
assert!(dissolve_delay_timestamp <= d);
}
k => panic!(
"Neuron should be in DissolveDelaySeconds state, but is instead: {:?}",
k
),
};

// When we try to dissolve an already dissolving neuron the response should succeed with no change to the neuron
let start_dissolving_response = TransactionOperationResults::try_from(
env.rosetta_client
.start_dissolving_neuron(
env.network_identifier.clone(),
&(*TEST_IDENTITY).clone(),
neuron_index,
)
.await
.unwrap()
.metadata,
)
.unwrap();
assert_eq!(
start_dissolving_response.operations.first().unwrap().status,
Some("COMPLETED".to_owned())
);
let neuron = list_neurons(&agent).await.full_neurons[0].to_owned();
assert!(
matches!(
neuron.dissolve_state.unwrap(),
DissolveState::WhenDissolvedTimestampSeconds(_)
),
"Neuron should be in WhenDissolvedTimestampSeconds state, but is instead: {:?}",
neuron.dissolve_state.unwrap()
);

// Stop dissolving the neuron
let stop_dissolving_response = TransactionOperationResults::try_from(
env.rosetta_client
.stop_dissolving_neuron(
env.network_identifier.clone(),
&(*TEST_IDENTITY).clone(),
neuron_index,
)
.await
.unwrap()
.metadata,
)
.unwrap();
assert_eq!(
stop_dissolving_response.operations.first().unwrap().status,
Some("COMPLETED".to_owned())
);
let neuron = list_neurons(&agent).await.full_neurons[0].to_owned();
assert!(matches!(
neuron.dissolve_state.unwrap(),
DissolveState::DissolveDelaySeconds(_)
));
});
}

0 comments on commit 9cc994d

Please sign in to comment.