diff --git a/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs index 5f46f345d..37ebff30b 100644 --- a/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs +++ b/smartcontract/programs/doublezero-geolocation/src/entrypoint.rs @@ -2,7 +2,8 @@ use crate::{ instructions::GeolocationInstruction, processors::{ geo_probe::{ - create::process_create_geo_probe, delete::process_delete_geo_probe, + add_parent_device::process_add_parent_device, create::process_create_geo_probe, + delete::process_delete_geo_probe, remove_parent_device::process_remove_parent_device, update::process_update_geo_probe, }, program_config::{ @@ -43,6 +44,10 @@ pub fn process_instruction( process_update_geo_probe(program_id, accounts, &args)? } GeolocationInstruction::DeleteGeoProbe => process_delete_geo_probe(program_id, accounts)?, + GeolocationInstruction::AddParentDevice => process_add_parent_device(program_id, accounts)?, + GeolocationInstruction::RemoveParentDevice(args) => { + process_remove_parent_device(program_id, accounts, &args)? + } }; Ok(()) diff --git a/smartcontract/programs/doublezero-geolocation/src/error.rs b/smartcontract/programs/doublezero-geolocation/src/error.rs index 77fcc84b9..1ab6ed9cb 100644 --- a/smartcontract/programs/doublezero-geolocation/src/error.rs +++ b/smartcontract/programs/doublezero-geolocation/src/error.rs @@ -14,6 +14,10 @@ pub enum GeolocationError { InvalidIpAddress = 5, #[error("Maximum parent devices reached")] MaxParentDevicesReached = 6, + #[error("Parent device already exists in probe")] + ParentDeviceAlreadyExists = 7, + #[error("Parent device not found in probe")] + ParentDeviceNotFound = 8, #[error("Invalid serviceability program ID")] InvalidServiceabilityProgramId = 11, #[error("Invalid account code")] @@ -43,6 +47,8 @@ mod tests { (GeolocationError::InvalidCodeLength, 4), (GeolocationError::InvalidIpAddress, 5), (GeolocationError::MaxParentDevicesReached, 6), + (GeolocationError::ParentDeviceAlreadyExists, 7), + (GeolocationError::ParentDeviceNotFound, 8), (GeolocationError::InvalidServiceabilityProgramId, 11), (GeolocationError::InvalidAccountCode, 12), (GeolocationError::ReferenceCountNotZero, 15), diff --git a/smartcontract/programs/doublezero-geolocation/src/instructions.rs b/smartcontract/programs/doublezero-geolocation/src/instructions.rs index 8498b899c..dbd3ccef6 100644 --- a/smartcontract/programs/doublezero-geolocation/src/instructions.rs +++ b/smartcontract/programs/doublezero-geolocation/src/instructions.rs @@ -1,7 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub use crate::processors::{ - geo_probe::{create::CreateGeoProbeArgs, update::UpdateGeoProbeArgs}, + geo_probe::{ + create::CreateGeoProbeArgs, remove_parent_device::RemoveParentDeviceArgs, + update::UpdateGeoProbeArgs, + }, program_config::{init::InitProgramConfigArgs, update::UpdateProgramConfigArgs}, }; @@ -12,6 +15,8 @@ pub enum GeolocationInstruction { CreateGeoProbe(CreateGeoProbeArgs), UpdateGeoProbe(UpdateGeoProbeArgs), DeleteGeoProbe, + AddParentDevice, + RemoveParentDevice(RemoveParentDeviceArgs), } #[cfg(test)] @@ -55,6 +60,12 @@ mod tests { metrics_publisher_pk: None, })); test_instruction(GeolocationInstruction::DeleteGeoProbe); + test_instruction(GeolocationInstruction::AddParentDevice); + test_instruction(GeolocationInstruction::RemoveParentDevice( + RemoveParentDeviceArgs { + device_pk: Pubkey::new_unique(), + }, + )); } #[test] diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/add_parent_device.rs b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/add_parent_device.rs new file mode 100644 index 000000000..aa1b6854c --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/add_parent_device.rs @@ -0,0 +1,86 @@ +use crate::{ + error::GeolocationError, processors::check_foundation_allowlist, serializer::try_acc_write, + state::geo_probe::{GeoProbe, MAX_PARENT_DEVICES}, +}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +pub fn process_add_parent_device(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let probe_account = next_account_info(accounts_iter)?; + let device_account = next_account_info(accounts_iter)?; + let program_config_account = next_account_info(accounts_iter)?; + let serviceability_globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let _system_program = next_account_info(accounts_iter)?; + + if !payer_account.is_signer { + msg!("Payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + + check_foundation_allowlist( + program_config_account, + serviceability_globalstate_account, + payer_account, + program_id, + )?; + + if probe_account.owner != program_id { + msg!("Invalid GeoProbe Account Owner"); + return Err(ProgramError::IllegalOwner); + } + if !probe_account.is_writable { + msg!("GeoProbe account must be writable"); + return Err(ProgramError::InvalidAccountData); + } + + // Validate device_account belongs to the Serviceability program + let serviceability_program_id = crate::serviceability_program_id(); + if *device_account.owner != serviceability_program_id { + msg!( + "Device account owner {} does not match serviceability program {}", + device_account.owner, + serviceability_program_id + ); + return Err(GeolocationError::InvalidServiceabilityProgramId.into()); + } + + // Verify it's a valid, activated Device + let device = doublezero_serviceability::state::device::Device::try_from(device_account)?; + if device.status != doublezero_serviceability::state::device::DeviceStatus::Activated { + msg!( + "Device {} is not activated (status: {:?})", + device_account.key, + device.status + ); + return Err(ProgramError::InvalidAccountData); + } + + let mut probe = GeoProbe::try_from(probe_account)?; + + if probe.parent_devices.contains(device_account.key) { + msg!("Device {} is already a parent device", device_account.key); + return Err(GeolocationError::ParentDeviceAlreadyExists.into()); + } + + if probe.parent_devices.len() >= MAX_PARENT_DEVICES { + msg!( + "Cannot add parent device: already at maximum of {}", + MAX_PARENT_DEVICES + ); + return Err(GeolocationError::MaxParentDevicesReached.into()); + } + + probe.parent_devices.push(*device_account.key); + + try_acc_write(&probe, probe_account, payer_account, accounts)?; + + Ok(()) +} diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/mod.rs b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/mod.rs index fdb2f5561..f6ee62a35 100644 --- a/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/mod.rs +++ b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/mod.rs @@ -1,3 +1,5 @@ +pub mod add_parent_device; pub mod create; pub mod delete; +pub mod remove_parent_device; pub mod update; diff --git a/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/remove_parent_device.rs b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/remove_parent_device.rs new file mode 100644 index 000000000..fed9f55db --- /dev/null +++ b/smartcontract/programs/doublezero-geolocation/src/processors/geo_probe/remove_parent_device.rs @@ -0,0 +1,65 @@ +use crate::{ + error::GeolocationError, processors::check_foundation_allowlist, serializer::try_acc_write, + state::geo_probe::GeoProbe, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Clone)] +pub struct RemoveParentDeviceArgs { + pub device_pk: Pubkey, +} + +pub fn process_remove_parent_device( + program_id: &Pubkey, + accounts: &[AccountInfo], + args: &RemoveParentDeviceArgs, +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + + let probe_account = next_account_info(accounts_iter)?; + let program_config_account = next_account_info(accounts_iter)?; + let serviceability_globalstate_account = next_account_info(accounts_iter)?; + let payer_account = next_account_info(accounts_iter)?; + let _system_program = next_account_info(accounts_iter)?; + + if !payer_account.is_signer { + msg!("Payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + + check_foundation_allowlist( + program_config_account, + serviceability_globalstate_account, + payer_account, + program_id, + )?; + + if probe_account.owner != program_id { + msg!("Invalid GeoProbe Account Owner"); + return Err(ProgramError::IllegalOwner); + } + if !probe_account.is_writable { + msg!("GeoProbe account must be writable"); + return Err(ProgramError::InvalidAccountData); + } + + let mut probe = GeoProbe::try_from(probe_account)?; + + if !probe.parent_devices.contains(&args.device_pk) { + msg!("Device {} is not a parent device", args.device_pk); + return Err(GeolocationError::ParentDeviceNotFound.into()); + } + + probe.parent_devices.retain(|&pk| pk != args.device_pk); + + try_acc_write(&probe, probe_account, payer_account, accounts)?; + + Ok(()) +}