Attestation: Add ops owner and set-is-active ix (#295)
* Attestation: Add ops owner and set-is-active ix * Update solana/pyth2wormhole/client/src/cli.rs Co-authored-by: Stanisław Drozd <stan@nexantic.com> * Fix typos * Add a test without owner Co-authored-by: Stanisław Drozd <stan@nexantic.com>
This commit is contained in:
parent
bf032f07c2
commit
77083cf760
|
@ -58,6 +58,8 @@ pub enum Action {
|
|||
/// Option<> makes sure not specifying this flag does not imply "false"
|
||||
#[clap(long = "is-active")]
|
||||
is_active: Option<bool>,
|
||||
#[clap(long = "ops-owner")]
|
||||
ops_owner_addr: Option<Pubkey>,
|
||||
},
|
||||
#[clap(
|
||||
about = "Use an existing pyth2wormhole program to attest product price information to another chain"
|
||||
|
@ -115,6 +117,10 @@ pub enum Action {
|
|||
new_pyth_owner_addr: Option<Pubkey>,
|
||||
#[clap(long = "is-active")]
|
||||
is_active: Option<bool>,
|
||||
#[clap(long = "ops-owner")]
|
||||
ops_owner_addr: Option<Pubkey>,
|
||||
#[clap(long = "remove-ops-owner", conflicts_with = "ops_owner_addr")]
|
||||
remove_ops_owner: bool,
|
||||
},
|
||||
#[clap(
|
||||
about = "Migrate existing pyth2wormhole program settings to a newer format version. Client version must match the deployed contract."
|
||||
|
@ -130,4 +136,21 @@ pub enum Action {
|
|||
},
|
||||
#[clap(about = "Print out emitter address for the specified pyth2wormhole contract")]
|
||||
GetEmitter,
|
||||
#[clap(
|
||||
about = "Set the value of is_active config as ops_owner"
|
||||
)]
|
||||
SetIsActive {
|
||||
/// Current ops owner keypair path
|
||||
#[clap(
|
||||
long,
|
||||
default_value = "~/.config/solana/id.json",
|
||||
help = "Keypair file for the current ops owner"
|
||||
)]
|
||||
ops_owner: String,
|
||||
#[clap(
|
||||
index = 1,
|
||||
possible_values = ["true", "false"],
|
||||
)]
|
||||
new_is_active: String,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,6 +163,45 @@ pub fn gen_set_config_tx(
|
|||
Ok(tx_signed)
|
||||
}
|
||||
|
||||
|
||||
pub fn gen_set_is_active_tx(
|
||||
payer: Keypair,
|
||||
p2w_addr: Pubkey,
|
||||
ops_owner: Keypair,
|
||||
new_is_active: bool,
|
||||
latest_blockhash: Hash,
|
||||
) -> Result<Transaction, ErrBox> {
|
||||
let payer_pubkey = payer.pubkey();
|
||||
|
||||
let acc_metas = vec![
|
||||
// config
|
||||
AccountMeta::new(
|
||||
P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr),
|
||||
false,
|
||||
),
|
||||
// ops_owner
|
||||
AccountMeta::new(ops_owner.pubkey(), true),
|
||||
// payer
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
];
|
||||
|
||||
let ix_data = (
|
||||
pyth2wormhole::instruction::Instruction::SetIsActive,
|
||||
new_is_active,
|
||||
);
|
||||
|
||||
let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
|
||||
|
||||
let signers = vec![&ops_owner, &payer];
|
||||
let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
|
||||
&[ix],
|
||||
Some(&payer_pubkey),
|
||||
&signers,
|
||||
latest_blockhash,
|
||||
);
|
||||
Ok(tx_signed)
|
||||
}
|
||||
|
||||
pub fn gen_migrate_tx(
|
||||
payer: Keypair,
|
||||
p2w_addr: Pubkey,
|
||||
|
|
|
@ -97,6 +97,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
pyth_owner_addr,
|
||||
wh_prog,
|
||||
is_active,
|
||||
ops_owner_addr,
|
||||
} => {
|
||||
let tx = gen_init_tx(
|
||||
payer,
|
||||
|
@ -107,6 +108,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
pyth_owner: pyth_owner_addr,
|
||||
is_active: is_active.unwrap_or(true),
|
||||
max_batch_size: P2W_MAX_BATCH_SIZE,
|
||||
ops_owner: ops_owner_addr,
|
||||
},
|
||||
latest_blockhash,
|
||||
)?;
|
||||
|
@ -114,7 +116,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
.send_and_confirm_transaction_with_spinner(&tx)
|
||||
.await?;
|
||||
println!(
|
||||
"Initialized with conifg:\n{:?}",
|
||||
"Initialized with config:\n{:?}",
|
||||
get_config_account(&rpc_client, &p2w_addr).await?
|
||||
);
|
||||
}
|
||||
|
@ -127,8 +129,17 @@ async fn main() -> Result<(), ErrBox> {
|
|||
new_wh_prog,
|
||||
new_pyth_owner_addr,
|
||||
is_active,
|
||||
ops_owner_addr,
|
||||
remove_ops_owner,
|
||||
} => {
|
||||
let old_config = get_config_account(&rpc_client, &p2w_addr).await?;
|
||||
|
||||
let new_ops_owner = if remove_ops_owner {
|
||||
None
|
||||
} else {
|
||||
ops_owner_addr
|
||||
};
|
||||
|
||||
let tx = gen_set_config_tx(
|
||||
payer,
|
||||
p2w_addr,
|
||||
|
@ -139,6 +150,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
pyth_owner: new_pyth_owner_addr.unwrap_or(old_config.pyth_owner),
|
||||
is_active: is_active.unwrap_or(old_config.is_active),
|
||||
max_batch_size: P2W_MAX_BATCH_SIZE,
|
||||
ops_owner: new_ops_owner,
|
||||
},
|
||||
latest_blockhash,
|
||||
)?;
|
||||
|
@ -146,7 +158,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
.send_and_confirm_transaction_with_spinner(&tx)
|
||||
.await?;
|
||||
println!(
|
||||
"Applied conifg:\n{:?}",
|
||||
"Applied config:\n{:?}",
|
||||
get_config_account(&rpc_client, &p2w_addr).await?
|
||||
);
|
||||
}
|
||||
|
@ -161,7 +173,7 @@ async fn main() -> Result<(), ErrBox> {
|
|||
.send_and_confirm_transaction_with_spinner(&tx)
|
||||
.await?;
|
||||
println!(
|
||||
"Applied conifg:\n{:?}",
|
||||
"Applied config:\n{:?}",
|
||||
get_config_account(&rpc_client, &p2w_addr).await?
|
||||
);
|
||||
}
|
||||
|
@ -196,7 +208,23 @@ async fn main() -> Result<(), ErrBox> {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
Action::GetEmitter => unreachable! {},
|
||||
Action::GetEmitter => unreachable! {}, // It is handled early in this function.
|
||||
Action::SetIsActive { ops_owner, new_is_active } => {
|
||||
let tx = gen_set_is_active_tx(
|
||||
payer,
|
||||
p2w_addr,
|
||||
read_keypair_file(&*shellexpand::tilde(&ops_owner))?,
|
||||
new_is_active.eq_ignore_ascii_case("true"),
|
||||
latest_blockhash,
|
||||
)?;
|
||||
rpc_client
|
||||
.send_and_confirm_transaction_with_spinner(&tx)
|
||||
.await?;
|
||||
println!(
|
||||
"Applied config:\n{:?}",
|
||||
get_config_account(&rpc_client, &p2w_addr).await?
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -47,6 +47,7 @@ async fn test_happy_path() -> Result<(), p2wc::ErrBoxSend> {
|
|||
// Authorities
|
||||
let p2w_owner = Pubkey::new_unique();
|
||||
let pyth_owner = Pubkey::new_unique();
|
||||
let ops_owner = Pubkey::new_unique();
|
||||
|
||||
// On-chain state
|
||||
let p2w_config = Pyth2WormholeConfig {
|
||||
|
@ -55,6 +56,7 @@ async fn test_happy_path() -> Result<(), p2wc::ErrBoxSend> {
|
|||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner,
|
||||
is_active: true,
|
||||
ops_owner: Some(ops_owner),
|
||||
};
|
||||
|
||||
let bridge_config = BridgeData {
|
||||
|
|
|
@ -60,6 +60,7 @@ async fn test_migrate_works() -> Result<(), solitaire::ErrBox> {
|
|||
wh_prog: wh_fixture_program_id,
|
||||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
info!("Before ProgramTest::new()");
|
||||
|
@ -114,6 +115,7 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
|
|||
// Authorities
|
||||
let p2w_owner = Keypair::new();
|
||||
let pyth_owner = Pubkey::new_unique();
|
||||
let ops_owner = Keypair::new();
|
||||
|
||||
// On-chain state
|
||||
let old_p2w_config = OldPyth2WormholeConfig {
|
||||
|
@ -121,6 +123,7 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
|
|||
wh_prog: wh_fixture_program_id,
|
||||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
let new_p2w_config = Pyth2WormholeConfig {
|
||||
|
@ -129,6 +132,7 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
|
|||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner,
|
||||
is_active: true,
|
||||
ops_owner: Some(ops_owner.pubkey()),
|
||||
};
|
||||
|
||||
info!("Before ProgramTest::new()");
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
pub mod fixtures;
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
use p2wc::get_config_account;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
account::Account,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
signature::Signer,
|
||||
signer::keypair::Keypair,
|
||||
};
|
||||
|
||||
use pyth2wormhole::config::{
|
||||
P2WConfigAccount,
|
||||
Pyth2WormholeConfig,
|
||||
};
|
||||
use pyth2wormhole_client as p2wc;
|
||||
use solitaire::{
|
||||
processors::seeded::Seeded,
|
||||
AccountState,
|
||||
BorshSerialize,
|
||||
};
|
||||
|
||||
fn clone_keypair(keypair: &Keypair) -> Keypair {
|
||||
// Unwrap as we are surely copying a keypair and we are in test env.
|
||||
Keypair::from_bytes(keypair.to_bytes().as_ref()).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_setting_is_active_works() -> Result<(), p2wc::ErrBoxSend> {
|
||||
// Programs
|
||||
let p2w_program_id = Pubkey::new_unique();
|
||||
let wh_fixture_program_id = Pubkey::new_unique();
|
||||
|
||||
// Authorities
|
||||
let p2w_owner = Pubkey::new_unique();
|
||||
let pyth_owner = Pubkey::new_unique();
|
||||
let ops_owner = Keypair::new();
|
||||
|
||||
// On-chain state
|
||||
let p2w_config = Pyth2WormholeConfig {
|
||||
owner: p2w_owner,
|
||||
wh_prog: wh_fixture_program_id,
|
||||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner,
|
||||
is_active: true,
|
||||
ops_owner: Some(ops_owner.pubkey()),
|
||||
};
|
||||
|
||||
// Populate test environment
|
||||
let mut p2w_test = ProgramTest::new(
|
||||
"pyth2wormhole",
|
||||
p2w_program_id,
|
||||
processor!(pyth2wormhole::instruction::solitaire),
|
||||
);
|
||||
|
||||
// Plant a filled config account
|
||||
let p2w_config_bytes = p2w_config.try_to_vec()?;
|
||||
let p2w_config_account = Account {
|
||||
lamports: Rent::default().minimum_balance(p2w_config_bytes.len()),
|
||||
data: p2w_config_bytes,
|
||||
owner: p2w_program_id,
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
};
|
||||
let p2w_config_addr =
|
||||
P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id);
|
||||
|
||||
p2w_test.add_account(p2w_config_addr, p2w_config_account);
|
||||
|
||||
let mut ctx = p2w_test.start_with_context().await;
|
||||
|
||||
// Setting to false should work
|
||||
let set_is_active_false_tx = p2wc::gen_set_is_active_tx(
|
||||
clone_keypair(&ctx.payer),
|
||||
p2w_program_id,
|
||||
clone_keypair(&ops_owner),
|
||||
false,
|
||||
ctx.last_blockhash,
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
ctx.banks_client.process_transaction(set_is_active_false_tx).await?;
|
||||
|
||||
let config = ctx.banks_client.
|
||||
get_account_data_with_borsh::<Pyth2WormholeConfig>(p2w_config_addr).await?;
|
||||
|
||||
assert!(!config.is_active);
|
||||
|
||||
// Setting to true should work
|
||||
let set_is_active_true_tx = p2wc::gen_set_is_active_tx(
|
||||
clone_keypair(&ctx.payer),
|
||||
p2w_program_id,
|
||||
clone_keypair(&ops_owner),
|
||||
true,
|
||||
ctx.last_blockhash,
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
ctx.banks_client.process_transaction(set_is_active_true_tx).await?;
|
||||
|
||||
let config = ctx.banks_client.
|
||||
get_account_data_with_borsh::<Pyth2WormholeConfig>(p2w_config_addr).await?;
|
||||
|
||||
assert!(config.is_active);
|
||||
|
||||
// A wrong signer cannot handle it
|
||||
|
||||
let set_is_active_true_tx = p2wc::gen_set_is_active_tx(
|
||||
clone_keypair(&ctx.payer),
|
||||
p2w_program_id,
|
||||
clone_keypair(&ctx.payer),
|
||||
true,
|
||||
ctx.last_blockhash,
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
assert!(ctx.banks_client.process_transaction(set_is_active_true_tx).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_setting_is_active_does_not_work_without_ops_owner() -> Result<(), p2wc::ErrBoxSend> {
|
||||
// Programs
|
||||
let p2w_program_id = Pubkey::new_unique();
|
||||
let wh_fixture_program_id = Pubkey::new_unique();
|
||||
|
||||
// Authorities
|
||||
let p2w_owner = Pubkey::new_unique();
|
||||
let pyth_owner = Keypair::new();
|
||||
|
||||
// On-chain state
|
||||
let p2w_config = Pyth2WormholeConfig {
|
||||
owner: p2w_owner,
|
||||
wh_prog: wh_fixture_program_id,
|
||||
max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
|
||||
pyth_owner: pyth_owner.pubkey(),
|
||||
is_active: true,
|
||||
ops_owner: None,
|
||||
};
|
||||
|
||||
// Populate test environment
|
||||
let mut p2w_test = ProgramTest::new(
|
||||
"pyth2wormhole",
|
||||
p2w_program_id,
|
||||
processor!(pyth2wormhole::instruction::solitaire),
|
||||
);
|
||||
|
||||
// Plant a filled config account
|
||||
let p2w_config_bytes = p2w_config.try_to_vec()?;
|
||||
let p2w_config_account = Account {
|
||||
lamports: Rent::default().minimum_balance(p2w_config_bytes.len()),
|
||||
data: p2w_config_bytes,
|
||||
owner: p2w_program_id,
|
||||
executable: false,
|
||||
rent_epoch: 0,
|
||||
};
|
||||
let p2w_config_addr =
|
||||
P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id);
|
||||
|
||||
p2w_test.add_account(p2w_config_addr, p2w_config_account);
|
||||
|
||||
let mut ctx = p2w_test.start_with_context().await;
|
||||
|
||||
// No one could should be able to handle
|
||||
// For example pyth_owner is used here.
|
||||
let set_is_active_true_tx = p2wc::gen_set_is_active_tx(
|
||||
clone_keypair(&ctx.payer),
|
||||
p2w_program_id,
|
||||
pyth_owner,
|
||||
true,
|
||||
ctx.last_blockhash,
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
assert!(ctx.banks_client.process_transaction(set_is_active_true_tx).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -34,9 +34,10 @@ use solitaire::{
|
|||
};
|
||||
|
||||
/// Aliases for current config schema (to migrate into)
|
||||
pub type Pyth2WormholeConfig = Pyth2WormholeConfigV2;
|
||||
pub type Pyth2WormholeConfig = Pyth2WormholeConfigV3;
|
||||
pub type P2WConfigAccount<'b, const IsInitialized: AccountState> =
|
||||
P2WConfigAccountV2<'b, IsInitialized>;
|
||||
P2WConfigAccountV3<'b, IsInitialized>;
|
||||
|
||||
|
||||
impl Owned for Pyth2WormholeConfig {
|
||||
fn owner(&self) -> AccountOwner {
|
||||
|
@ -45,8 +46,8 @@ impl Owned for Pyth2WormholeConfig {
|
|||
}
|
||||
|
||||
/// Aliases for previous config schema (to migrate from)
|
||||
pub type OldPyth2WormholeConfig = Pyth2WormholeConfigV1;
|
||||
pub type OldP2WConfigAccount<'b> = P2WConfigAccountV1<'b, { AccountState::Initialized }>; // Old config must always be initialized
|
||||
pub type OldPyth2WormholeConfig = Pyth2WormholeConfigV2;
|
||||
pub type OldP2WConfigAccount<'b> = P2WConfigAccountV2<'b, { AccountState::Initialized }>; // Old config must always be initialized
|
||||
|
||||
impl Owned for OldPyth2WormholeConfig {
|
||||
fn owner(&self) -> AccountOwner {
|
||||
|
@ -117,3 +118,51 @@ impl From<Pyth2WormholeConfigV1> for Pyth2WormholeConfigV2 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Added ops_owner which can toggle the is_active field
|
||||
#[derive(Clone, Default, BorshDeserialize, BorshSerialize)]
|
||||
#[cfg_attr(feature = "client", derive(Debug))]
|
||||
pub struct Pyth2WormholeConfigV3 {
|
||||
/// Authority owning this contract
|
||||
pub owner: Pubkey,
|
||||
/// Wormhole bridge program
|
||||
pub wh_prog: Pubkey,
|
||||
/// Authority owning Pyth price data
|
||||
pub pyth_owner: Pubkey,
|
||||
/// How many product/price pairs can be sent and attested at once
|
||||
///
|
||||
/// Important: Whenever the corresponding logic in attest.rs
|
||||
/// changes its expected number of symbols per batch, this config
|
||||
/// must be updated accordingly on-chain.
|
||||
pub max_batch_size: u16,
|
||||
|
||||
/// If set to false, attest() will reject all calls unconditionally
|
||||
pub is_active: bool,
|
||||
|
||||
// If the ops_owner exists, it can toggle the value of `is_active`
|
||||
pub ops_owner: Option<Pubkey>,
|
||||
}
|
||||
|
||||
pub type P2WConfigAccountV3<'b, const IsInitialized: AccountState> =
|
||||
Derive<Data<'b, Pyth2WormholeConfigV3, { IsInitialized }>, "pyth2wormhole-config-v3">;
|
||||
|
||||
impl From<Pyth2WormholeConfigV2> for Pyth2WormholeConfigV3 {
|
||||
fn from(old: Pyth2WormholeConfigV2) -> Self {
|
||||
let Pyth2WormholeConfigV2 {
|
||||
owner,
|
||||
wh_prog,
|
||||
pyth_owner,
|
||||
max_batch_size,
|
||||
is_active,
|
||||
} = old;
|
||||
|
||||
Self {
|
||||
owner,
|
||||
wh_prog,
|
||||
pyth_owner,
|
||||
max_batch_size,
|
||||
is_active: true,
|
||||
ops_owner: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ pub mod initialize;
|
|||
pub mod message;
|
||||
pub mod migrate;
|
||||
pub mod set_config;
|
||||
pub mod set_is_active;
|
||||
|
||||
use solitaire::solitaire;
|
||||
|
||||
|
@ -27,6 +28,11 @@ pub use set_config::{
|
|||
SetConfig,
|
||||
};
|
||||
|
||||
pub use set_is_active::{
|
||||
set_is_active,
|
||||
SetIsActive,
|
||||
};
|
||||
|
||||
pub use pyth_client;
|
||||
|
||||
solitaire! {
|
||||
|
@ -34,4 +40,5 @@ solitaire! {
|
|||
Initialize => initialize,
|
||||
SetConfig => set_config,
|
||||
Migrate => migrate,
|
||||
SetIsActive => set_is_active
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
use solitaire::{
|
||||
trace,
|
||||
AccountState,
|
||||
ExecutionContext,
|
||||
FromAccounts,
|
||||
Info,
|
||||
Keyed,
|
||||
Mut,
|
||||
Peel,
|
||||
Result as SoliResult,
|
||||
Signer,
|
||||
SolitaireError,
|
||||
};
|
||||
|
||||
use crate::config::{
|
||||
P2WConfigAccount,
|
||||
Pyth2WormholeConfig,
|
||||
};
|
||||
|
||||
#[derive(FromAccounts)]
|
||||
pub struct SetIsActive<'b> {
|
||||
/// Current config used by the program
|
||||
pub config: Mut<P2WConfigAccount<'b, { AccountState::Initialized }>>,
|
||||
/// Current owner authority of the program
|
||||
pub ops_owner: Mut<Signer<Info<'b>>>,
|
||||
/// Payer account for updating the account data
|
||||
pub payer: Mut<Signer<Info<'b>>>,
|
||||
}
|
||||
|
||||
/// Alters the current settings of pyth2wormhole
|
||||
pub fn set_is_active(
|
||||
_ctx: &ExecutionContext,
|
||||
accs: &mut SetIsActive,
|
||||
new_is_active: bool,
|
||||
) -> SoliResult<()> {
|
||||
let cfg_struct: &mut Pyth2WormholeConfig = &mut accs.config; // unpack Data via nested Deref impls
|
||||
match &cfg_struct.ops_owner {
|
||||
None => Err(SolitaireError::InvalidOwner(*accs.ops_owner.info().key)),
|
||||
Some(current_ops_owner) => {
|
||||
if current_ops_owner != accs.ops_owner.info().key {
|
||||
trace!(
|
||||
"Ops owner account mismatch (expected {:?})",
|
||||
current_ops_owner
|
||||
);
|
||||
return Err(SolitaireError::InvalidOwner(
|
||||
*accs.ops_owner.info().key,
|
||||
));
|
||||
}
|
||||
|
||||
cfg_struct.is_active = new_is_active;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue