From ab51a459308c8c50e64ec5a5e4774da2d6730b38 Mon Sep 17 00:00:00 2001 From: Darksome Date: Tue, 3 Feb 2026 14:26:18 +0000 Subject: [PATCH 1/3] feat(admin): ownership commands --- crates/admin_cli/src/main.rs | 6 ++ crates/admin_cli/src/ownership.rs | 68 ++++++++++++++++++++ crates/cluster/src/lib.rs | 28 ++++++++ crates/cluster/src/smart_contract/evm.rs | 8 +++ crates/cluster/src/smart_contract/mod.rs | 13 ++++ crates/cluster/src/smart_contract/testing.rs | 8 +++ 6 files changed, 131 insertions(+) create mode 100644 crates/admin_cli/src/ownership.rs diff --git a/crates/admin_cli/src/main.rs b/crates/admin_cli/src/main.rs index 44069112..951f5f04 100644 --- a/crates/admin_cli/src/main.rs +++ b/crates/admin_cli/src/main.rs @@ -9,6 +9,7 @@ use { mod deploy; mod migration; mod operator; +mod ownership; mod settings; mod view; @@ -41,6 +42,10 @@ enum Command { /// Cluster settings management #[command(subcommand)] Settings(settings::Command), + + /// Cluster ownership management + #[command(subcommand)] + Ownership(ownership::Command), } #[derive(Debug, Args)] @@ -98,6 +103,7 @@ async fn main() -> anyhow::Result<()> { Command::Operator(cmd) => operator::execute(cmd).await, Command::Migration(cmd) => migration::execute(cmd).await, Command::Settings(cmd) => settings::execute(cmd).await, + Command::Ownership(cmd) => ownership::execute(cmd).await, } } diff --git a/crates/admin_cli/src/ownership.rs b/crates/admin_cli/src/ownership.rs new file mode 100644 index 00000000..0196cc03 --- /dev/null +++ b/crates/admin_cli/src/ownership.rs @@ -0,0 +1,68 @@ +use { + crate::ClusterArgs, + anyhow::Context, + clap::{Args, Subcommand}, + wcn_cluster::smart_contract::{AccountAddress, Write as _}, +}; + +#[derive(Debug, Subcommand)] +pub(super) enum Command { + /// Transfer ownership of the WCN Cluster + Transfer(TransferArgs), + + /// Accept ownership of the WCN Cluster + Accept(ClusterArgs), +} + +#[derive(Debug, Args)] +pub(super) struct TransferArgs { + #[command(flatten)] + cluster_args: ClusterArgs, + + /// Address of the new owner of the WCN Cluster + #[arg(long)] + new_owner: AccountAddress, +} + +pub(super) async fn execute(cmd: Command) -> anyhow::Result<()> { + match cmd { + Command::Transfer(args) => transfer_ownership(args).await, + Command::Accept(args) => accept_ownership(args).await, + } +} + +async fn transfer_ownership(args: TransferArgs) -> anyhow::Result<()> { + let cluster = args.cluster_args.connect().await?; + + println!("Transferring ownership to {}", args.new_owner); + + if crate::ask_approval()? { + cluster + .transfer_ownership(args.new_owner) + .await + .context("Cluster::transfer_ownership")?; + } + + println!( + "The complete the transfer the new owner is required to call `wcn_admin ownership accept` \ + command" + ); + + Ok(()) +} + +async fn accept_ownership(args: ClusterArgs) -> anyhow::Result<()> { + let cluster = args.connect().await?; + + cluster + .accept_ownership() + .await + .context("Cluster::accept_ownership")?; + + println!( + "OK. {} is the new owner", + cluster.smart_contract().signer().unwrap() + ); + + Ok(()) +} diff --git a/crates/cluster/src/lib.rs b/crates/cluster/src/lib.rs index 81ddc387..29edd0da 100644 --- a/crates/cluster/src/lib.rs +++ b/crates/cluster/src/lib.rs @@ -491,6 +491,31 @@ where Ok(()) } + /// Calls [`SmartContract::transfer_ownership`]. + pub async fn transfer_ownership( + &self, + new_owner: smart_contract::AccountAddress, + ) -> Result<(), TransferOwnershipError> { + self.using_view(move |view| { + view.ownership() + .require_owner(self.smart_contract_signer()?)?; + + Ok::<_, TransferOwnershipError>(()) + })?; + + self.inner + .smart_contract + .transfer_ownership(new_owner) + .await?; + + Ok(()) + } + + /// Calls [`SmartContract::accept_ownership`]. + pub async fn accept_ownership(&self) -> Result<(), smart_contract::WriteError> { + self.inner.smart_contract.accept_ownership().await + } + fn smart_contract_signer( &self, ) -> Result<&smart_contract::AccountAddress, NoSmartContractSingerError> { @@ -759,6 +784,9 @@ pub enum UpdateSettingsError { /// [`Cluster::transfer_ownership`] error. #[derive(Debug, thiserror::Error)] pub enum TransferOwnershipError { + #[error(transparent)] + NoSigner(#[from] NoSmartContractSingerError), + #[error(transparent)] NotOwner(#[from] ownership::NotOwnerError), diff --git a/crates/cluster/src/smart_contract/evm.rs b/crates/cluster/src/smart_contract/evm.rs index 2fd64066..4db00023 100644 --- a/crates/cluster/src/smart_contract/evm.rs +++ b/crates/cluster/src/smart_contract/evm.rs @@ -218,6 +218,14 @@ impl smart_contract::Write for SmartContract { async fn update_settings(&self, new_settings: Settings) -> WriteResult<()> { check_receipt(self.alloy.updateSettings(new_settings.into())).await } + + async fn transfer_ownership(&self, new_owner: AccountAddress) -> WriteResult<()> { + check_receipt(self.alloy.transferOwnership(new_owner.into())).await + } + + async fn accept_ownership(&self) -> WriteResult<()> { + check_receipt(self.alloy.acceptOwnership()).await + } } async fn check_receipt(call: CallBuilder<&DynProvider, D>) -> WriteResult<()> diff --git a/crates/cluster/src/smart_contract/mod.rs b/crates/cluster/src/smart_contract/mod.rs index 376ec8ee..97fd40d5 100644 --- a/crates/cluster/src/smart_contract/mod.rs +++ b/crates/cluster/src/smart_contract/mod.rs @@ -212,6 +212,19 @@ pub trait Write { /// The implementation MUST emit [`settings::Updated`] event on /// success. fn update_settings(&self, new_settings: Settings) -> impl Future>; + + /// Transfers ownership of the [`Cluster`] to the specified + /// [`AccountAddress`]. + /// + /// The new owner is required to confirm the transfer using + /// [`SmartContract::accept_ownership`]. + fn transfer_ownership( + &self, + new_owner: AccountAddress, + ) -> impl Future>; + + /// Accepts ownership of the [`Cluster`]. + fn accept_ownership(&self) -> impl Future>; } /// Read [`SmartContract`] calls. diff --git a/crates/cluster/src/smart_contract/testing.rs b/crates/cluster/src/smart_contract/testing.rs index 897a5bd4..d1ec701e 100644 --- a/crates/cluster/src/smart_contract/testing.rs +++ b/crates/cluster/src/smart_contract/testing.rs @@ -393,6 +393,14 @@ impl super::Write for FakeSmartContract { }) .await } + + async fn transfer_ownership(&self, _new_owner: AccountAddress) -> WriteResult<()> { + Ok(()) + } + + async fn accept_ownership(&self) -> WriteResult<()> { + Ok(()) + } } impl super::Read for FakeSmartContract { From f8b63f8e00371f0904fd1990f84b8a30651bc801 Mon Sep 17 00:00:00 2001 From: Github Bot Date: Tue, 3 Feb 2026 14:28:49 +0000 Subject: [PATCH 2/3] Bump VERSION to 260203.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 633b435c..065f561f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -260120.0 +260203.0 From 2b27fd64364655bfd52c949c8f6b4b0806081485 Mon Sep 17 00:00:00 2001 From: Darksome Date: Tue, 3 Feb 2026 14:29:08 +0000 Subject: [PATCH 3/3] fix: typo --- crates/admin_cli/src/ownership.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/admin_cli/src/ownership.rs b/crates/admin_cli/src/ownership.rs index 0196cc03..a6a57095 100644 --- a/crates/admin_cli/src/ownership.rs +++ b/crates/admin_cli/src/ownership.rs @@ -44,7 +44,7 @@ async fn transfer_ownership(args: TransferArgs) -> anyhow::Result<()> { } println!( - "The complete the transfer the new owner is required to call `wcn_admin ownership accept` \ + "To complete the transfer the new owner is required to call `wcn_admin ownership accept` \ command" );