From d94dabe5140dca2414db7e5f5e709e6962338d65 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sun, 5 Jul 2026 13:04:47 +0000 Subject: [PATCH 1/2] serviceability: authorize location and exchange instructions via Permission accounts Migrate Location and Exchange create/update/suspend/resume/delete (and exchange setdevice) to authorize() with the INFRA_ADMIN flag. Behavior preserved via the legacy foundation fallback. --- .../src/processors/exchange/create.rs | 13 ++- .../src/processors/exchange/delete.rs | 14 ++- .../src/processors/exchange/resume.rs | 13 ++- .../src/processors/exchange/setdevice.rs | 14 ++- .../src/processors/exchange/suspend.rs | 14 ++- .../src/processors/exchange/update.rs | 17 ++- .../src/processors/location/create.rs | 17 ++- .../src/processors/location/delete.rs | 17 ++- .../src/processors/location/resume.rs | 14 ++- .../src/processors/location/suspend.rs | 14 ++- .../src/processors/location/update.rs | 14 ++- .../tests/exchange_test.rs | 83 +++++++++++++- .../tests/location_test.rs | 101 +++++++++++++++++- .../sdk/rs/src/commands/exchange/create.rs | 4 +- .../sdk/rs/src/commands/exchange/delete.rs | 4 +- .../sdk/rs/src/commands/exchange/resume.rs | 4 +- .../sdk/rs/src/commands/exchange/setdevice.rs | 16 +-- .../sdk/rs/src/commands/exchange/suspend.rs | 4 +- .../sdk/rs/src/commands/exchange/update.rs | 4 +- .../sdk/rs/src/commands/location/create.rs | 4 +- .../sdk/rs/src/commands/location/delete.rs | 4 +- .../sdk/rs/src/commands/location/resume.rs | 4 +- .../sdk/rs/src/commands/location/suspend.rs | 4 +- .../sdk/rs/src/commands/location/update.rs | 4 +- 24 files changed, 326 insertions(+), 75 deletions(-) diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/create.rs index 3616e79877..b2b5831c31 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/create.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, helper::assign_bgp_community, pda::*, @@ -9,6 +10,7 @@ use crate::{ exchange::{Exchange, ExchangeStatus}, globalconfig::GlobalConfig, globalstate::GlobalState, + permission::permission_flags, }, }; use borsh::BorshSerialize; @@ -92,9 +94,14 @@ pub fn process_create_exchange( let mut globalstate = GlobalState::try_from(globalstate_account)?; globalstate.account_index += 1; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; // We need to access globalconfig in order to assign BGP community let mut globalconfig = GlobalConfig::try_from(globalconfig_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/delete.rs index cc4984b03f..5c7d4daca5 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/delete.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_close, - state::{exchange::Exchange, globalstate::GlobalState}, + state::{exchange::Exchange, globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -64,9 +65,14 @@ pub fn process_delete_exchange( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; let exchange = Exchange::try_from(exchange_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/resume.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/resume.rs index e9f369663b..010a80f5ef 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/resume.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/resume.rs @@ -1,9 +1,11 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, state::{ exchange::{Exchange, ExchangeStatus}, globalstate::GlobalState, + permission::permission_flags, }, }; use borsh::BorshSerialize; @@ -72,9 +74,14 @@ pub fn process_resume_exchange( // Authorization: // - Only accounts in the foundation_allowlist may resume the exchange. - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; // Only resume exchanges that are currently Suspended if exchange.status != ExchangeStatus::Suspended { diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/setdevice.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/setdevice.rs index d3b2ff34ba..647d0b0054 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/setdevice.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/setdevice.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{device::Device, exchange::*, globalstate::GlobalState}, + state::{device::Device, exchange::*, globalstate::GlobalState, permission::permission_flags}, }; use borsh::{BorshDeserialize, BorshSerialize}; use borsh_incremental::BorshDeserializeIncremental; @@ -85,9 +86,14 @@ pub fn process_setdevice_exchange( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; let mut exchange: Exchange = Exchange::try_from(exchange_account)?; let mut device: Device = Device::try_from(device_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/suspend.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/suspend.rs index 390819ed32..f327da3564 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/suspend.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/suspend.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{exchange::*, globalstate::GlobalState}, + state::{exchange::*, globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -69,9 +70,14 @@ pub fn process_suspend_exchange( // Authorization: // - Only accounts in the foundation_allowlist may suspend the exchange. - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; if exchange.status != ExchangeStatus::Activated { #[cfg(test)] diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/update.rs index d4198cf885..b8ca333327 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/exchange/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/exchange/update.rs @@ -1,9 +1,13 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, helper::assign_bgp_community, pda::get_globalconfig_pda, serializer::try_acc_write, - state::{exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState}, + state::{ + exchange::Exchange, globalconfig::GlobalConfig, globalstate::GlobalState, + permission::permission_flags, + }, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -83,9 +87,14 @@ pub fn process_update_exchange( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; // We need to access globalconfig in order to assign BGP community let mut globalconfig = GlobalConfig::try_from(globalconfig_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/location/create.rs b/smartcontract/programs/doublezero-serviceability/src/processors/location/create.rs index 530af328d2..71288a88d1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/location/create.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/location/create.rs @@ -1,9 +1,13 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, pda::*, seeds::{SEED_LOCATION, SEED_PREFIX}, serializer::{try_acc_create, try_acc_write}, - state::{accounttype::AccountType, globalstate::GlobalState, location::*}, + state::{ + accounttype::AccountType, globalstate::GlobalState, location::*, + permission::permission_flags, + }, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -87,9 +91,14 @@ pub fn process_create_location( let mut globalstate = GlobalState::try_from(globalstate_account)?; globalstate.account_index += 1; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; // get the PDA pubkey and bump seed for the account location & check if it matches the account let (expected_pda_account, bump_seed) = get_location_pda(program_id, globalstate.account_index); assert_eq!( diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/location/delete.rs b/smartcontract/programs/doublezero-serviceability/src/processors/location/delete.rs index 0ea8176f28..0402823534 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/location/delete.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/location/delete.rs @@ -1,7 +1,11 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_close, - state::{accounttype::AccountType, globalstate::GlobalState, location::*}, + state::{ + accounttype::AccountType, globalstate::GlobalState, location::*, + permission::permission_flags, + }, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -59,9 +63,14 @@ pub fn process_delete_location( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; let location = Location::try_from(location_account)?; assert_eq!( diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/location/resume.rs b/smartcontract/programs/doublezero-serviceability/src/processors/location/resume.rs index 8834bd00c0..8b8b3a853a 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/location/resume.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/location/resume.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{globalstate::GlobalState, location::*}, + state::{globalstate::GlobalState, location::*, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -58,9 +59,14 @@ pub fn process_resume_location( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; let mut location: Location = Location::try_from(location_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/location/suspend.rs b/smartcontract/programs/doublezero-serviceability/src/processors/location/suspend.rs index cde20107a4..2cb8a71cf1 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/location/suspend.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/location/suspend.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{globalstate::GlobalState, location::*}, + state::{globalstate::GlobalState, location::*, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -59,9 +60,14 @@ pub fn process_suspend_location( // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; let mut location: Location = Location::try_from(location_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/location/update.rs b/smartcontract/programs/doublezero-serviceability/src/processors/location/update.rs index e177b6986a..dba47f3d29 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/location/update.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/location/update.rs @@ -1,7 +1,8 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, serializer::try_acc_write, - state::{globalstate::GlobalState, location::*}, + state::{globalstate::GlobalState, location::*, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -69,9 +70,14 @@ pub fn process_update_location( assert!(location_account.is_writable, "PDA Account is not writable"); // Parse the global state account & check if the payer is in the allowlist let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + // Authorization: INFRA_ADMIN (Permission account) or foundation (legacy). + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::INFRA_ADMIN, + )?; // Parse the location account let mut location: Location = Location::try_from(location_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/tests/exchange_test.rs b/smartcontract/programs/doublezero-serviceability/tests/exchange_test.rs index 6a14393eb4..d024849f7c 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/exchange_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/exchange_test.rs @@ -6,12 +6,17 @@ use doublezero_serviceability::{ allowlist::foundation::add::AddFoundationAllowlistArgs, exchange::{create::*, delete::*, resume::*, suspend::*, update::*}, globalconfig::set::SetGlobalConfigArgs, + permission::create::PermissionCreateArgs, }, resource::ResourceType, - state::{accounttype::AccountType, exchange::*}, + state::{accounttype::AccountType, exchange::*, permission::permission_flags}, }; use solana_program_test::*; -use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Signer}; +use solana_sdk::{ + instruction::AccountMeta, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; mod test_helpers; use test_helpers::*; @@ -986,3 +991,77 @@ async fn test_suspend_exchange_from_suspended_fails() { ); println!("✅ Suspending already-suspended exchange correctly fails with InvalidStatus"); } + +/// A non-foundation key holding an INFRA_ADMIN Permission account can create an +/// exchange — exercises the new Permission-account authorization path. +#[tokio::test] +async fn test_exchange_create_with_permission_account_allowed() { + let (mut banks_client, payer, program_id, globalstate_pubkey, globalconfig_pubkey) = + setup_program_with_globalconfig().await; + + // A key that is NOT in the foundation allowlist, granted INFRA_ADMIN. + let infra_admin = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &infra_admin.pubkey(), + 1_000_000_000, + ) + .await; + + let (permission_pda, _) = get_permission_pda(&program_id, &infra_admin.pubkey()); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { + user_payer: infra_admin.pubkey(), + permissions: permission_flags::INFRA_ADMIN, + }), + vec![ + AccountMeta::new(permission_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // infra_admin creates an exchange, passing its Permission PDA as the optional + // trailing account that authorize() reads. + let globalstate = get_globalstate(&mut banks_client, globalstate_pubkey).await; + let (exchange_pubkey, _) = get_exchange_pda(&program_id, globalstate.account_index + 1); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + let mut tx = create_transaction_with_extra_accounts( + program_id, + &DoubleZeroInstruction::CreateExchange(ExchangeCreateArgs { + code: "permxch".to_string(), + name: "Permissioned Exchange".to_string(), + lat: 1.0, + lng: 2.0, + reserved: 0, + }), + &vec![ + AccountMeta::new(exchange_pubkey, false), + AccountMeta::new(globalconfig_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &infra_admin, + &[AccountMeta::new_readonly(permission_pda, false)], + ); + tx.try_sign(&[&infra_admin], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("INFRA_ADMIN permission holder should be able to create an exchange"); + + let exchange = get_account_data(&mut banks_client, exchange_pubkey) + .await + .expect("exchange") + .get_exchange() + .unwrap(); + assert_eq!(exchange.account_type, AccountType::Exchange); + assert_eq!(exchange.code, "permxch".to_string()); + + println!("✅ CreateExchange with INFRA_ADMIN permission succeeded"); +} diff --git a/smartcontract/programs/doublezero-serviceability/tests/location_test.rs b/smartcontract/programs/doublezero-serviceability/tests/location_test.rs index fc4b904d13..bf26d03b8e 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/location_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/location_test.rs @@ -1,11 +1,17 @@ use doublezero_serviceability::{ instructions::*, pda::*, - processors::location::{create::*, delete::*, resume::*, suspend::*, update::*}, - state::{accounttype::AccountType, location::*}, + processors::{ + location::{create::*, delete::*, resume::*, suspend::*, update::*}, + permission::create::PermissionCreateArgs, + }, + state::{accounttype::AccountType, location::*, permission::permission_flags}, }; use solana_program_test::*; -use solana_sdk::instruction::AccountMeta; +use solana_sdk::{ + instruction::AccountMeta, + signature::{Keypair, Signer}, +}; mod test_helpers; use test_helpers::*; @@ -282,3 +288,92 @@ async fn test_location_delete_from_suspended() { let location_la = get_account_data(&mut banks_client, location_pubkey).await; assert_eq!(location_la, None); } + +/// A non-foundation key holding an INFRA_ADMIN Permission account can create a +/// location — exercises the new Permission-account authorization path. +#[tokio::test] +async fn test_location_create_with_permission_account_allowed() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // A key that is NOT in the foundation allowlist, granted INFRA_ADMIN. + let infra_admin = Keypair::new(); + transfer( + &mut banks_client, + &payer, + &infra_admin.pubkey(), + 1_000_000_000, + ) + .await; + + let (permission_pda, _) = get_permission_pda(&program_id, &infra_admin.pubkey()); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { + user_payer: infra_admin.pubkey(), + permissions: permission_flags::INFRA_ADMIN, + }), + vec![ + AccountMeta::new(permission_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // infra_admin creates a location, passing its Permission PDA as the optional + // trailing account that authorize() reads. + let globalstate = get_globalstate(&mut banks_client, globalstate_pubkey).await; + let (location_pubkey, _) = get_location_pda(&program_id, globalstate.account_index + 1); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + let mut tx = create_transaction_with_extra_accounts( + program_id, + &DoubleZeroInstruction::CreateLocation(LocationCreateArgs { + code: "permloc".to_string(), + name: "Permissioned Location".to_string(), + country: "us".to_string(), + lat: 1.0, + lng: 2.0, + loc_id: 0, + }), + &vec![ + AccountMeta::new(location_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &infra_admin, + &[AccountMeta::new_readonly(permission_pda, false)], + ); + tx.try_sign(&[&infra_admin], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("INFRA_ADMIN permission holder should be able to create a location"); + + let location = get_account_data(&mut banks_client, location_pubkey) + .await + .expect("location") + .get_location() + .unwrap(); + assert_eq!(location.account_type, AccountType::Location); + assert_eq!(location.code, "permloc".to_string()); + + println!("✅ CreateLocation with INFRA_ADMIN permission succeeded"); +} diff --git a/smartcontract/sdk/rs/src/commands/exchange/create.rs b/smartcontract/sdk/rs/src/commands/exchange/create.rs index 5cec15d186..be5ac0eccf 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/create.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/create.rs @@ -30,7 +30,7 @@ impl CreateExchangeCommand { let (pda_pubkey, _) = get_exchange_pda(&client.get_program_id(), globalstate.account_index + 1); client - .execute_transaction( + .execute_authorized_transaction( DoubleZeroInstruction::CreateExchange(ExchangeCreateArgs { code, name: self.name.clone(), @@ -71,7 +71,7 @@ mod tests { let (pda_pubkey, _) = get_exchange_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CreateExchange(ExchangeCreateArgs { code: "test_exchange".to_string(), diff --git a/smartcontract/sdk/rs/src/commands/exchange/delete.rs b/smartcontract/sdk/rs/src/commands/exchange/delete.rs index 57c46dfa9c..b92d42942f 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/delete.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/delete.rs @@ -31,7 +31,7 @@ impl DeleteExchangeCommand { )); } - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::DeleteExchange(ExchangeDeleteArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -89,7 +89,7 @@ mod tests { .returning(move |_| Ok(AccountData::Exchange(exchange.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteExchange(ExchangeDeleteArgs {})), predicate::eq(vec![ diff --git a/smartcontract/sdk/rs/src/commands/exchange/resume.rs b/smartcontract/sdk/rs/src/commands/exchange/resume.rs index 7672895743..d3729c580f 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/resume.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/resume.rs @@ -15,7 +15,7 @@ impl ResumeExchangeCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::ResumeExchange(ExchangeResumeArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -47,7 +47,7 @@ mod tests { let (pda_pubkey, _) = get_exchange_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::ResumeExchange(ExchangeResumeArgs {})), predicate::eq(vec![ diff --git a/smartcontract/sdk/rs/src/commands/exchange/setdevice.rs b/smartcontract/sdk/rs/src/commands/exchange/setdevice.rs index 8f498a4fe5..5da7bab134 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/setdevice.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/setdevice.rs @@ -37,7 +37,7 @@ impl SetDeviceExchangeCommand { if (exchange.device1_pk == Pubkey::default() && self.device1_pubkey.is_some()) || (exchange.device1_pk != Pubkey::default() && self.device1_pubkey.is_none()) { - signature = client.execute_transaction( + signature = client.execute_authorized_transaction( DoubleZeroInstruction::SetDeviceExchange(ExchangeSetDeviceArgs { index: 1, set: if self.device1_pubkey.is_some() { @@ -57,7 +57,7 @@ impl SetDeviceExchangeCommand { if (exchange.device2_pk == Pubkey::default() && self.device2_pubkey.is_some()) || (exchange.device2_pk != Pubkey::default() && self.device2_pubkey.is_none()) { - signature = client.execute_transaction( + signature = client.execute_authorized_transaction( DoubleZeroInstruction::SetDeviceExchange(ExchangeSetDeviceArgs { index: 2, set: if self.device2_pubkey.is_some() { @@ -131,7 +131,7 @@ mod tests { .returning(move |_| Ok(AccountData::Exchange(exchange.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( @@ -193,7 +193,7 @@ mod tests { .returning(move |_| Ok(AccountData::Exchange(exchange.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( @@ -257,7 +257,7 @@ mod tests { .returning(move |_| Ok(AccountData::Exchange(exchange.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( @@ -276,7 +276,7 @@ mod tests { .returning(|_, _| Ok(Signature::new_unique())); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( @@ -340,7 +340,7 @@ mod tests { .returning(move |_| Ok(AccountData::Exchange(exchange.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetDeviceExchange( ExchangeSetDeviceArgs { @@ -357,7 +357,7 @@ mod tests { .returning(|_, _| Ok(Signature::new_unique())); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .in_sequence(&mut seq) .with( diff --git a/smartcontract/sdk/rs/src/commands/exchange/suspend.rs b/smartcontract/sdk/rs/src/commands/exchange/suspend.rs index c3dc7e4cf9..199e9d9613 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/suspend.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/suspend.rs @@ -15,7 +15,7 @@ impl SuspendExchangeCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SuspendExchange(ExchangeSuspendArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -47,7 +47,7 @@ mod tests { let (pda_pubkey, _) = get_exchange_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SuspendExchange( ExchangeSuspendArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/exchange/update.rs b/smartcontract/sdk/rs/src/commands/exchange/update.rs index 9525e9fe95..e6d54ac4e8 100644 --- a/smartcontract/sdk/rs/src/commands/exchange/update.rs +++ b/smartcontract/sdk/rs/src/commands/exchange/update.rs @@ -30,7 +30,7 @@ impl UpdateExchangeCommand { let (globalconfig_pubkey, _) = get_globalconfig_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::UpdateExchange(ExchangeUpdateArgs { code, name: self.name.to_owned(), @@ -70,7 +70,7 @@ mod tests { let (pda_pubkey, _) = get_exchange_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::UpdateExchange(ExchangeUpdateArgs { code: Some("test_exchange".to_string()), diff --git a/smartcontract/sdk/rs/src/commands/location/create.rs b/smartcontract/sdk/rs/src/commands/location/create.rs index a9aee131c3..be51690fe0 100644 --- a/smartcontract/sdk/rs/src/commands/location/create.rs +++ b/smartcontract/sdk/rs/src/commands/location/create.rs @@ -28,7 +28,7 @@ impl CreateLocationCommand { let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), globalstate.account_index + 1); client - .execute_transaction( + .execute_authorized_transaction( DoubleZeroInstruction::CreateLocation(LocationCreateArgs { code, name: self.name.clone(), @@ -65,7 +65,7 @@ mod tests { let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::CreateLocation(LocationCreateArgs { code: "test_location".to_string(), diff --git a/smartcontract/sdk/rs/src/commands/location/delete.rs b/smartcontract/sdk/rs/src/commands/location/delete.rs index 299b7595b1..1356086d7d 100644 --- a/smartcontract/sdk/rs/src/commands/location/delete.rs +++ b/smartcontract/sdk/rs/src/commands/location/delete.rs @@ -30,7 +30,7 @@ impl DeleteLocationCommand { )); } - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::DeleteLocation(LocationDeleteArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -86,7 +86,7 @@ mod tests { .returning(move |_| Ok(AccountData::Location(location.clone()))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::DeleteLocation(LocationDeleteArgs {})), predicate::eq(vec![ diff --git a/smartcontract/sdk/rs/src/commands/location/resume.rs b/smartcontract/sdk/rs/src/commands/location/resume.rs index f94962301d..d1361f8f13 100644 --- a/smartcontract/sdk/rs/src/commands/location/resume.rs +++ b/smartcontract/sdk/rs/src/commands/location/resume.rs @@ -15,7 +15,7 @@ impl ResumeLocationCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::ResumeLocation(LocationResumeArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -47,7 +47,7 @@ mod tests { let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::ResumeLocation(LocationResumeArgs {})), predicate::eq(vec![ diff --git a/smartcontract/sdk/rs/src/commands/location/suspend.rs b/smartcontract/sdk/rs/src/commands/location/suspend.rs index 0fdef26337..c0c76d6197 100644 --- a/smartcontract/sdk/rs/src/commands/location/suspend.rs +++ b/smartcontract/sdk/rs/src/commands/location/suspend.rs @@ -15,7 +15,7 @@ impl SuspendLocationCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SuspendLocation(LocationSuspendArgs {}), vec![ AccountMeta::new(self.pubkey, false), @@ -47,7 +47,7 @@ mod tests { let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SuspendLocation( LocationSuspendArgs {}, diff --git a/smartcontract/sdk/rs/src/commands/location/update.rs b/smartcontract/sdk/rs/src/commands/location/update.rs index 6cbe3abcbd..9954e7b4d6 100644 --- a/smartcontract/sdk/rs/src/commands/location/update.rs +++ b/smartcontract/sdk/rs/src/commands/location/update.rs @@ -28,7 +28,7 @@ impl UpdateLocationCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::UpdateLocation(LocationUpdateArgs { code, name: self.name.to_owned(), @@ -67,7 +67,7 @@ mod tests { let (pda_pubkey, _) = get_location_pda(&client.get_program_id(), 1); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::UpdateLocation(LocationUpdateArgs { code: Some("test_location".to_string()), From 376957f63f8fbbff6bf918b7e5e9d6fc835c62d9 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sun, 5 Jul 2026 13:04:53 +0000 Subject: [PATCH 2/2] changelog: location/exchange permission migration (#3979) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89870a39a1..53ddcbd3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### Changes +- Serviceability + - Gate Location and Exchange instructions (create/update/suspend/resume/delete, exchange setdevice) on `INFRA_ADMIN` or foundation via `authorize()`. (#3979) - Collector - Harden ledger writes against a slow/degraded RPC endpoint: bound each RPC request (default 15s, `--ledger-rpc-timeout`), size the connection pool above the submitter concurrency (default 128, `--ledger-rpc-max-conns`), and deadline each submission attempt so it fails fast and retries with a fresh blockhash instead of sending an expired one and failing preflight with `BlockhashNotFound`. (#3973) - E2E