From 7ca1fd6e7252308adc222eb7a397e2f8ed955589 Mon Sep 17 00:00:00 2001 From: Evan Batsell <ebatsell@gmail.com> Date: Thu, 7 Nov 2024 14:34:02 -0500 Subject: [PATCH] set new admins instruction --- .../js/jito_tip_router/instructions/index.ts | 1 + .../instructions/setNewAdmin.ts | 240 +++++++++ .../jito_tip_router/programs/jitoTipRouter.ts | 10 +- .../jito_tip_router/types/configAdminRole.ts | 38 ++ clients/js/jito_tip_router/types/index.ts | 1 + .../src/generated/instructions/mod.rs | 3 +- .../generated/instructions/set_new_admin.rs | 476 ++++++++++++++++++ .../src/generated/types/config_admin_role.rs | 26 + .../src/generated/types/mod.rs | 3 +- core/src/instruction.rs | 15 + idl/jito_tip_router.json | 56 +++ .../tests/fixtures/restaking_client.rs | 2 +- .../tests/fixtures/tip_router_client.rs | 44 +- integration_tests/tests/tip_router/mod.rs | 1 + .../tests/tip_router/set_new_admin.rs | 85 ++++ program/src/lib.rs | 6 + program/src/set_new_admin.rs | 56 +++ 17 files changed, 1058 insertions(+), 5 deletions(-) create mode 100644 clients/js/jito_tip_router/instructions/setNewAdmin.ts create mode 100644 clients/js/jito_tip_router/types/configAdminRole.ts create mode 100644 clients/rust/jito_tip_router/src/generated/instructions/set_new_admin.rs create mode 100644 clients/rust/jito_tip_router/src/generated/types/config_admin_role.rs create mode 100644 integration_tests/tests/tip_router/set_new_admin.rs diff --git a/clients/js/jito_tip_router/instructions/index.ts b/clients/js/jito_tip_router/instructions/index.ts index 83168165..0f849b93 100644 --- a/clients/js/jito_tip_router/instructions/index.ts +++ b/clients/js/jito_tip_router/instructions/index.ts @@ -10,4 +10,5 @@ export * from './finalizeWeightTable'; export * from './initializeConfig'; export * from './initializeWeightTable'; export * from './setConfigFees'; +export * from './setNewAdmin'; export * from './updateWeightTable'; diff --git a/clients/js/jito_tip_router/instructions/setNewAdmin.ts b/clients/js/jito_tip_router/instructions/setNewAdmin.ts new file mode 100644 index 00000000..c2272f32 --- /dev/null +++ b/clients/js/jito_tip_router/instructions/setNewAdmin.ts @@ -0,0 +1,240 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IAccountSignerMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type TransactionSigner, + type WritableAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; +import { + getConfigAdminRoleDecoder, + getConfigAdminRoleEncoder, + type ConfigAdminRole, + type ConfigAdminRoleArgs, +} from '../types'; + +export const SET_NEW_ADMIN_DISCRIMINATOR = 5; + +export function getSetNewAdminDiscriminatorBytes() { + return getU8Encoder().encode(SET_NEW_ADMIN_DISCRIMINATOR); +} + +export type SetNewAdminInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountConfig extends string | IAccountMeta<string> = string, + TAccountNcn extends string | IAccountMeta<string> = string, + TAccountNcnAdmin extends string | IAccountMeta<string> = string, + TAccountNewAdmin extends string | IAccountMeta<string> = string, + TAccountRestakingProgramId extends string | IAccountMeta<string> = string, + TRemainingAccounts extends readonly IAccountMeta<string>[] = [], +> = IInstruction<TProgram> & + IInstructionWithData<Uint8Array> & + IInstructionWithAccounts< + [ + TAccountConfig extends string + ? WritableAccount<TAccountConfig> + : TAccountConfig, + TAccountNcn extends string ? ReadonlyAccount<TAccountNcn> : TAccountNcn, + TAccountNcnAdmin extends string + ? ReadonlySignerAccount<TAccountNcnAdmin> & + IAccountSignerMeta<TAccountNcnAdmin> + : TAccountNcnAdmin, + TAccountNewAdmin extends string + ? ReadonlyAccount<TAccountNewAdmin> + : TAccountNewAdmin, + TAccountRestakingProgramId extends string + ? ReadonlyAccount<TAccountRestakingProgramId> + : TAccountRestakingProgramId, + ...TRemainingAccounts, + ] + >; + +export type SetNewAdminInstructionData = { + discriminator: number; + role: ConfigAdminRole; +}; + +export type SetNewAdminInstructionDataArgs = { role: ConfigAdminRoleArgs }; + +export function getSetNewAdminInstructionDataEncoder(): Encoder<SetNewAdminInstructionDataArgs> { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['role', getConfigAdminRoleEncoder()], + ]), + (value) => ({ ...value, discriminator: SET_NEW_ADMIN_DISCRIMINATOR }) + ); +} + +export function getSetNewAdminInstructionDataDecoder(): Decoder<SetNewAdminInstructionData> { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['role', getConfigAdminRoleDecoder()], + ]); +} + +export function getSetNewAdminInstructionDataCodec(): Codec< + SetNewAdminInstructionDataArgs, + SetNewAdminInstructionData +> { + return combineCodec( + getSetNewAdminInstructionDataEncoder(), + getSetNewAdminInstructionDataDecoder() + ); +} + +export type SetNewAdminInput< + TAccountConfig extends string = string, + TAccountNcn extends string = string, + TAccountNcnAdmin extends string = string, + TAccountNewAdmin extends string = string, + TAccountRestakingProgramId extends string = string, +> = { + config: Address<TAccountConfig>; + ncn: Address<TAccountNcn>; + ncnAdmin: TransactionSigner<TAccountNcnAdmin>; + newAdmin: Address<TAccountNewAdmin>; + restakingProgramId: Address<TAccountRestakingProgramId>; + role: SetNewAdminInstructionDataArgs['role']; +}; + +export function getSetNewAdminInstruction< + TAccountConfig extends string, + TAccountNcn extends string, + TAccountNcnAdmin extends string, + TAccountNewAdmin extends string, + TAccountRestakingProgramId extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: SetNewAdminInput< + TAccountConfig, + TAccountNcn, + TAccountNcnAdmin, + TAccountNewAdmin, + TAccountRestakingProgramId + >, + config?: { programAddress?: TProgramAddress } +): SetNewAdminInstruction< + TProgramAddress, + TAccountConfig, + TAccountNcn, + TAccountNcnAdmin, + TAccountNewAdmin, + TAccountRestakingProgramId +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + config: { value: input.config ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + ncnAdmin: { value: input.ncnAdmin ?? null, isWritable: false }, + newAdmin: { value: input.newAdmin ?? null, isWritable: false }, + restakingProgramId: { + value: input.restakingProgramId ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.config), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.ncnAdmin), + getAccountMeta(accounts.newAdmin), + getAccountMeta(accounts.restakingProgramId), + ], + programAddress, + data: getSetNewAdminInstructionDataEncoder().encode( + args as SetNewAdminInstructionDataArgs + ), + } as SetNewAdminInstruction< + TProgramAddress, + TAccountConfig, + TAccountNcn, + TAccountNcnAdmin, + TAccountNewAdmin, + TAccountRestakingProgramId + >; + + return instruction; +} + +export type ParsedSetNewAdminInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address<TProgram>; + accounts: { + config: TAccountMetas[0]; + ncn: TAccountMetas[1]; + ncnAdmin: TAccountMetas[2]; + newAdmin: TAccountMetas[3]; + restakingProgramId: TAccountMetas[4]; + }; + data: SetNewAdminInstructionData; +}; + +export function parseSetNewAdminInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction<TProgram> & + IInstructionWithAccounts<TAccountMetas> & + IInstructionWithData<Uint8Array> +): ParsedSetNewAdminInstruction<TProgram, TAccountMetas> { + if (instruction.accounts.length < 5) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + config: getNextAccount(), + ncn: getNextAccount(), + ncnAdmin: getNextAccount(), + newAdmin: getNextAccount(), + restakingProgramId: getNextAccount(), + }, + data: getSetNewAdminInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index af5d229a..7320b4f6 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -17,6 +17,7 @@ import { type ParsedInitializeConfigInstruction, type ParsedInitializeWeightTableInstruction, type ParsedSetConfigFeesInstruction, + type ParsedSetNewAdminInstruction, type ParsedUpdateWeightTableInstruction, } from '../instructions'; @@ -34,6 +35,7 @@ export enum JitoTipRouterInstruction { UpdateWeightTable, FinalizeWeightTable, SetConfigFees, + SetNewAdmin, } export function identifyJitoTipRouterInstruction( @@ -55,6 +57,9 @@ export function identifyJitoTipRouterInstruction( if (containsBytes(data, getU8Encoder().encode(4), 0)) { return JitoTipRouterInstruction.SetConfigFees; } + if (containsBytes(data, getU8Encoder().encode(5), 0)) { + return JitoTipRouterInstruction.SetNewAdmin; + } throw new Error( 'The provided instruction could not be identified as a jitoTipRouter instruction.' ); @@ -77,4 +82,7 @@ export type ParsedJitoTipRouterInstruction< } & ParsedFinalizeWeightTableInstruction<TProgram>) | ({ instructionType: JitoTipRouterInstruction.SetConfigFees; - } & ParsedSetConfigFeesInstruction<TProgram>); + } & ParsedSetConfigFeesInstruction<TProgram>) + | ({ + instructionType: JitoTipRouterInstruction.SetNewAdmin; + } & ParsedSetNewAdminInstruction<TProgram>); diff --git a/clients/js/jito_tip_router/types/configAdminRole.ts b/clients/js/jito_tip_router/types/configAdminRole.ts new file mode 100644 index 00000000..e00adbc2 --- /dev/null +++ b/clients/js/jito_tip_router/types/configAdminRole.ts @@ -0,0 +1,38 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + getEnumDecoder, + getEnumEncoder, + type Codec, + type Decoder, + type Encoder, +} from '@solana/web3.js'; + +export enum ConfigAdminRole { + FeeAdmin, + TieBreakerAdmin, +} + +export type ConfigAdminRoleArgs = ConfigAdminRole; + +export function getConfigAdminRoleEncoder(): Encoder<ConfigAdminRoleArgs> { + return getEnumEncoder(ConfigAdminRole); +} + +export function getConfigAdminRoleDecoder(): Decoder<ConfigAdminRole> { + return getEnumDecoder(ConfigAdminRole); +} + +export function getConfigAdminRoleCodec(): Codec< + ConfigAdminRoleArgs, + ConfigAdminRole +> { + return combineCodec(getConfigAdminRoleEncoder(), getConfigAdminRoleDecoder()); +} diff --git a/clients/js/jito_tip_router/types/index.ts b/clients/js/jito_tip_router/types/index.ts index ccefe80b..a78e6058 100644 --- a/clients/js/jito_tip_router/types/index.ts +++ b/clients/js/jito_tip_router/types/index.ts @@ -6,6 +6,7 @@ * @see https://github.com/kinobi-so/kinobi */ +export * from './configAdminRole'; export * from './fee'; export * from './fees'; export * from './weightEntry'; diff --git a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs index 1261260b..ad897c05 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs @@ -8,9 +8,10 @@ pub(crate) mod r#finalize_weight_table; pub(crate) mod r#initialize_config; pub(crate) mod r#initialize_weight_table; pub(crate) mod r#set_config_fees; +pub(crate) mod r#set_new_admin; pub(crate) mod r#update_weight_table; pub use self::{ r#finalize_weight_table::*, r#initialize_config::*, r#initialize_weight_table::*, - r#set_config_fees::*, r#update_weight_table::*, + r#set_config_fees::*, r#set_new_admin::*, r#update_weight_table::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/instructions/set_new_admin.rs b/clients/rust/jito_tip_router/src/generated/instructions/set_new_admin.rs new file mode 100644 index 00000000..aa1021d0 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/set_new_admin.rs @@ -0,0 +1,476 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! <https://github.com/kinobi-so/kinobi> + +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::generated::types::ConfigAdminRole; + +/// Accounts. +pub struct SetNewAdmin { + pub config: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub ncn_admin: solana_program::pubkey::Pubkey, + + pub new_admin: solana_program::pubkey::Pubkey, + + pub restaking_program_id: solana_program::pubkey::Pubkey, +} + +impl SetNewAdmin { + pub fn instruction( + &self, + args: SetNewAdminInstructionArgs, + ) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: SetNewAdminInstructionArgs, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + self.config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_admin, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.new_admin, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_program_id, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = SetNewAdminInstructionData::new().try_to_vec().unwrap(); + let mut args = args.try_to_vec().unwrap(); + data.append(&mut args); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct SetNewAdminInstructionData { + discriminator: u8, +} + +impl SetNewAdminInstructionData { + pub fn new() -> Self { + Self { discriminator: 5 } + } +} + +impl Default for SetNewAdminInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SetNewAdminInstructionArgs { + pub role: ConfigAdminRole, +} + +/// Instruction builder for `SetNewAdmin`. +/// +/// ### Accounts: +/// +/// 0. `[writable]` config +/// 1. `[]` ncn +/// 2. `[signer]` ncn_admin +/// 3. `[]` new_admin +/// 4. `[]` restaking_program_id +#[derive(Clone, Debug, Default)] +pub struct SetNewAdminBuilder { + config: Option<solana_program::pubkey::Pubkey>, + ncn: Option<solana_program::pubkey::Pubkey>, + ncn_admin: Option<solana_program::pubkey::Pubkey>, + new_admin: Option<solana_program::pubkey::Pubkey>, + restaking_program_id: Option<solana_program::pubkey::Pubkey>, + role: Option<ConfigAdminRole>, + __remaining_accounts: Vec<solana_program::instruction::AccountMeta>, +} + +impl SetNewAdminBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn config(&mut self, config: solana_program::pubkey::Pubkey) -> &mut Self { + self.config = Some(config); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn ncn_admin(&mut self, ncn_admin: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_admin = Some(ncn_admin); + self + } + #[inline(always)] + pub fn new_admin(&mut self, new_admin: solana_program::pubkey::Pubkey) -> &mut Self { + self.new_admin = Some(new_admin); + self + } + #[inline(always)] + pub fn restaking_program_id( + &mut self, + restaking_program_id: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.restaking_program_id = Some(restaking_program_id); + self + } + #[inline(always)] + pub fn role(&mut self, role: ConfigAdminRole) -> &mut Self { + self.role = Some(role); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_program::instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_program::instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_program::instruction::Instruction { + let accounts = SetNewAdmin { + config: self.config.expect("config is not set"), + ncn: self.ncn.expect("ncn is not set"), + ncn_admin: self.ncn_admin.expect("ncn_admin is not set"), + new_admin: self.new_admin.expect("new_admin is not set"), + restaking_program_id: self + .restaking_program_id + .expect("restaking_program_id is not set"), + }; + let args = SetNewAdminInstructionArgs { + role: self.role.clone().expect("role is not set"), + }; + + accounts.instruction_with_remaining_accounts(args, &self.__remaining_accounts) + } +} + +/// `set_new_admin` CPI accounts. +pub struct SetNewAdminCpiAccounts<'a, 'b> { + pub config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub new_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program_id: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `set_new_admin` CPI instruction. +pub struct SetNewAdminCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub new_admin: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program_id: &'b solana_program::account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: SetNewAdminInstructionArgs, +} + +impl<'a, 'b> SetNewAdminCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: SetNewAdminCpiAccounts<'a, 'b>, + args: SetNewAdminInstructionArgs, + ) -> Self { + Self { + __program: program, + config: accounts.config, + ncn: accounts.ncn, + ncn_admin: accounts.ncn_admin, + new_admin: accounts.new_admin, + restaking_program_id: accounts.restaking_program_id, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program::entrypoint::ProgramResult { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn_admin.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.new_admin.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_program_id.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_program::instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = SetNewAdminInstructionData::new().try_to_vec().unwrap(); + let mut args = self.__args.try_to_vec().unwrap(); + data.append(&mut args); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(5 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.config.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.ncn_admin.clone()); + account_infos.push(self.new_admin.clone()); + account_infos.push(self.restaking_program_id.clone()); + remaining_accounts + .iter() + .for_each(|remaining_account| account_infos.push(remaining_account.0.clone())); + + if signers_seeds.is_empty() { + solana_program::program::invoke(&instruction, &account_infos) + } else { + solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds) + } + } +} + +/// Instruction builder for `SetNewAdmin` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable]` config +/// 1. `[]` ncn +/// 2. `[signer]` ncn_admin +/// 3. `[]` new_admin +/// 4. `[]` restaking_program_id +#[derive(Clone, Debug)] +pub struct SetNewAdminCpiBuilder<'a, 'b> { + instruction: Box<SetNewAdminCpiBuilderInstruction<'a, 'b>>, +} + +impl<'a, 'b> SetNewAdminCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(SetNewAdminCpiBuilderInstruction { + __program: program, + config: None, + ncn: None, + ncn_admin: None, + new_admin: None, + restaking_program_id: None, + role: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn config( + &mut self, + config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.config = Some(config); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn ncn_admin( + &mut self, + ncn_admin: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_admin = Some(ncn_admin); + self + } + #[inline(always)] + pub fn new_admin( + &mut self, + new_admin: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.new_admin = Some(new_admin); + self + } + #[inline(always)] + pub fn restaking_program_id( + &mut self, + restaking_program_id: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.restaking_program_id = Some(restaking_program_id); + self + } + #[inline(always)] + pub fn role(&mut self, role: ConfigAdminRole) -> &mut Self { + self.instruction.role = Some(role); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_program::account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction + .__remaining_accounts + .push((account, is_writable, is_signer)); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program::entrypoint::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program::entrypoint::ProgramResult { + let args = SetNewAdminInstructionArgs { + role: self.instruction.role.clone().expect("role is not set"), + }; + let instruction = SetNewAdminCpi { + __program: self.instruction.__program, + + config: self.instruction.config.expect("config is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + ncn_admin: self.instruction.ncn_admin.expect("ncn_admin is not set"), + + new_admin: self.instruction.new_admin.expect("new_admin is not set"), + + restaking_program_id: self + .instruction + .restaking_program_id + .expect("restaking_program_id is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct SetNewAdminCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn_admin: Option<&'b solana_program::account_info::AccountInfo<'a>>, + new_admin: Option<&'b solana_program::account_info::AccountInfo<'a>>, + restaking_program_id: Option<&'b solana_program::account_info::AccountInfo<'a>>, + role: Option<ConfigAdminRole>, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: Vec<( + &'b solana_program::account_info::AccountInfo<'a>, + bool, + bool, + )>, +} diff --git a/clients/rust/jito_tip_router/src/generated/types/config_admin_role.rs b/clients/rust/jito_tip_router/src/generated/types/config_admin_role.rs new file mode 100644 index 00000000..352c002a --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/config_admin_role.rs @@ -0,0 +1,26 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! <https://github.com/kinobi-so/kinobi> + +use borsh::{BorshDeserialize, BorshSerialize}; +use num_derive::FromPrimitive; + +#[derive( + BorshSerialize, + BorshDeserialize, + Clone, + Debug, + Eq, + PartialEq, + Copy, + PartialOrd, + Hash, + FromPrimitive, +)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum ConfigAdminRole { + FeeAdmin, + TieBreakerAdmin, +} diff --git a/clients/rust/jito_tip_router/src/generated/types/mod.rs b/clients/rust/jito_tip_router/src/generated/types/mod.rs index be538071..ea35df80 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mod.rs @@ -4,8 +4,9 @@ //! //! <https://github.com/kinobi-so/kinobi> +pub(crate) mod r#config_admin_role; pub(crate) mod r#fee; pub(crate) mod r#fees; pub(crate) mod r#weight_entry; -pub use self::{r#fee::*, r#fees::*, r#weight_entry::*}; +pub use self::{r#config_admin_role::*, r#fee::*, r#fees::*, r#weight_entry::*}; diff --git a/core/src/instruction.rs b/core/src/instruction.rs index 313ddbd2..3411ff17 100644 --- a/core/src/instruction.rs +++ b/core/src/instruction.rs @@ -2,6 +2,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankInstruction; use solana_program::pubkey::Pubkey; +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub enum ConfigAdminRole { + FeeAdmin, + TieBreakerAdmin, +} + #[rustfmt::skip] #[derive(Debug, BorshSerialize, BorshDeserialize, ShankInstruction)] pub enum WeightTableInstruction { @@ -63,4 +69,13 @@ pub enum WeightTableInstruction { new_fee_wallet: Option<Pubkey>, }, + /// Sets a new secondary admin for the NCN + #[account(0, writable, name = "config")] + #[account(1, name = "ncn")] + #[account(2, signer, name = "ncn_admin")] + #[account(3, name = "new_admin")] + #[account(4, name = "restaking_program_id")] + SetNewAdmin { + role: ConfigAdminRole, + }, } diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 2c3d914d..787445c6 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -239,6 +239,48 @@ "type": "u8", "value": 4 } + }, + { + "name": "SetNewAdmin", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "ncnAdmin", + "isMut": false, + "isSigner": true + }, + { + "name": "newAdmin", + "isMut": false, + "isSigner": false + }, + { + "name": "restakingProgramId", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "role", + "type": { + "defined": "ConfigAdminRole" + } + } + ], + "discriminant": { + "type": "u8", + "value": 5 + } } ], "accounts": [ @@ -402,6 +444,20 @@ } ] } + }, + { + "name": "ConfigAdminRole", + "type": { + "kind": "enum", + "variants": [ + { + "name": "FeeAdmin" + }, + { + "name": "TieBreakerAdmin" + } + ] + } } ], "errors": [ diff --git a/integration_tests/tests/fixtures/restaking_client.rs b/integration_tests/tests/fixtures/restaking_client.rs index be5f49e0..9128bf8b 100644 --- a/integration_tests/tests/fixtures/restaking_client.rs +++ b/integration_tests/tests/fixtures/restaking_client.rs @@ -57,7 +57,7 @@ impl RestakingProgramClient { let restaking_config_pubkey = Config::find_program_address(&jito_restaking_program::id()).0; let restaking_config_admin = Keypair::new(); - self.airdrop(&restaking_config_admin.pubkey(), 1.0).await?; + self.airdrop(&restaking_config_admin.pubkey(), 10.0).await?; self.initialize_config(&restaking_config_pubkey, &restaking_config_admin) .await?; diff --git a/integration_tests/tests/fixtures/tip_router_client.rs b/integration_tests/tests/fixtures/tip_router_client.rs index 42ee956b..3a53ba8e 100644 --- a/integration_tests/tests/fixtures/tip_router_client.rs +++ b/integration_tests/tests/fixtures/tip_router_client.rs @@ -1,5 +1,8 @@ use jito_bytemuck::AccountDeserialize; -use jito_tip_router_client::instructions::{InitializeConfigBuilder, SetConfigFeesBuilder}; +use jito_tip_router_client::{ + instructions::{InitializeConfigBuilder, SetConfigFeesBuilder, SetNewAdminBuilder}, + types::ConfigAdminRole, +}; use jito_tip_router_core::{error::TipRouterError, ncn_config::NcnConfig}; use solana_program::{ instruction::InstructionError, native_token::sol_to_lamports, pubkey::Pubkey, @@ -158,6 +161,45 @@ impl TipRouterClient { )) .await } + + pub async fn do_set_new_admin( + &mut self, + role: ConfigAdminRole, + new_admin: Pubkey, + ncn_root: &NcnRoot, + ) -> TestResult<()> { + let config_pda = + NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn_root.ncn_pubkey).0; + self.airdrop(&ncn_root.ncn_admin.pubkey(), 1.0).await?; + self.set_new_admin(config_pda, role, new_admin, ncn_root) + .await + } + + pub async fn set_new_admin( + &mut self, + config_pda: Pubkey, + role: ConfigAdminRole, + new_admin: Pubkey, + ncn_root: &NcnRoot, + ) -> TestResult<()> { + let ix = SetNewAdminBuilder::new() + .config(config_pda) + .ncn(ncn_root.ncn_pubkey) + .ncn_admin(ncn_root.ncn_admin.pubkey()) + .new_admin(new_admin) + .restaking_program_id(jito_restaking_program::id()) + .role(role) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&ncn_root.ncn_admin.pubkey()), + &[&ncn_root.ncn_admin], + blockhash, + )) + .await + } } #[inline(always)] diff --git a/integration_tests/tests/tip_router/mod.rs b/integration_tests/tests/tip_router/mod.rs index 78c5fcc4..04d688fa 100644 --- a/integration_tests/tests/tip_router/mod.rs +++ b/integration_tests/tests/tip_router/mod.rs @@ -1,2 +1,3 @@ mod initialize_ncn_config; mod set_config_fees; +mod set_new_admin; diff --git a/integration_tests/tests/tip_router/set_new_admin.rs b/integration_tests/tests/tip_router/set_new_admin.rs new file mode 100644 index 00000000..14f19c09 --- /dev/null +++ b/integration_tests/tests/tip_router/set_new_admin.rs @@ -0,0 +1,85 @@ +mod tests { + use jito_tip_router_client::types::ConfigAdminRole; + use jito_tip_router_core::{error::TipRouterError, ncn_config::NcnConfig}; + use solana_program::pubkey::Pubkey; + use solana_sdk::{instruction::InstructionError, signature::Keypair}; + + use crate::fixtures::{ + assert_ix_error, restaking_client::NcnRoot, test_builder::TestBuilder, + tip_router_client::assert_tip_router_error, TestResult, + }; + + #[tokio::test] + async fn test_set_new_admin_success() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let ncn_root = fixture.setup_ncn().await?; + + tip_router_client + .do_initialize_config(ncn_root.ncn_pubkey, &ncn_root.ncn_admin) + .await?; + + let new_fee_admin = Pubkey::new_unique(); + tip_router_client + .do_set_new_admin(ConfigAdminRole::FeeAdmin, new_fee_admin, &ncn_root) + .await?; + + let config = tip_router_client.get_config(ncn_root.ncn_pubkey).await?; + assert_eq!(config.fee_admin, new_fee_admin); + + let new_tie_breaker = Pubkey::new_unique(); + tip_router_client + .do_set_new_admin(ConfigAdminRole::TieBreakerAdmin, new_tie_breaker, &ncn_root) + .await?; + + let config = tip_router_client.get_config(ncn_root.ncn_pubkey).await?; + assert_eq!(config.tie_breaker_admin, new_tie_breaker); + Ok(()) + } + + #[tokio::test] + async fn test_set_new_admin_incorrect_accounts() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let ncn_root = fixture.setup_ncn().await?; + + tip_router_client + .do_initialize_config(ncn_root.ncn_pubkey, &ncn_root.ncn_admin) + .await?; + + fixture.warp_slot_incremental(1).await?; + let mut restaking_program_client = fixture.restaking_program_client(); + let wrong_ncn_root = restaking_program_client.do_initialize_ncn().await?; + + let result = tip_router_client + .set_new_admin( + NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn_root.ncn_pubkey, + ) + .0, + ConfigAdminRole::FeeAdmin, + Pubkey::new_unique(), + &wrong_ncn_root, + ) + .await; + + assert_ix_error(result, InstructionError::InvalidAccountData); + + let wrong_ncn_root = NcnRoot { + ncn_pubkey: ncn_root.ncn_pubkey, + ncn_admin: Keypair::new(), + }; + + let result = tip_router_client + .do_set_new_admin( + ConfigAdminRole::FeeAdmin, + Pubkey::new_unique(), + &wrong_ncn_root, + ) + .await; + + assert_tip_router_error(result, TipRouterError::IncorrectNcnAdmin); + Ok(()) + } +} diff --git a/program/src/lib.rs b/program/src/lib.rs index a008b7e4..f91459d0 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -2,11 +2,13 @@ mod finalize_weight_table; mod initialize_ncn_config; mod initialize_weight_table; mod set_config_fees; +mod set_new_admin; mod update_weight_table; use borsh::BorshDeserialize; use const_str_to_pubkey::str_to_pubkey; use jito_tip_router_core::instruction::WeightTableInstruction; +use set_new_admin::process_set_new_admin; use solana_program::{ account_info::AccountInfo, declare_id, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, @@ -113,5 +115,9 @@ pub fn process_instruction( new_fee_wallet, ) } + WeightTableInstruction::SetNewAdmin { role } => { + msg!("Instruction: SetNewAdmin"); + process_set_new_admin(program_id, accounts, role) + } } } diff --git a/program/src/set_new_admin.rs b/program/src/set_new_admin.rs index e69de29b..1e1f09f0 100644 --- a/program/src/set_new_admin.rs +++ b/program/src/set_new_admin.rs @@ -0,0 +1,56 @@ +use jito_bytemuck::{AccountDeserialize, Discriminator}; +use jito_jsm_core::loader::load_signer; +use jito_restaking_core::ncn::Ncn; +use jito_tip_router_core::{ + error::TipRouterError, instruction::ConfigAdminRole, ncn_config::NcnConfig, +}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, + pubkey::Pubkey, +}; + +pub fn process_set_new_admin( + program_id: &Pubkey, + accounts: &[AccountInfo], + role: ConfigAdminRole, +) -> ProgramResult { + let [config, ncn_account, ncn_admin, new_admin, restaking_program_id] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + load_signer(ncn_admin, true)?; + + NcnConfig::load(program_id, ncn_account.key, config, true)?; + Ncn::load(restaking_program_id.key, ncn_account, false)?; + + let mut config_data = config.try_borrow_mut_data()?; + if config_data[0] != NcnConfig::DISCRIMINATOR { + return Err(ProgramError::InvalidAccountData); + } + let config = NcnConfig::try_from_slice_unchecked_mut(&mut config_data)?; + + // Verify NCN and Admin + if config.ncn != *ncn_account.key { + return Err(TipRouterError::IncorrectNcn.into()); + } + + let ncn_data = ncn_account.data.borrow(); + let ncn = Ncn::try_from_slice_unchecked(&ncn_data)?; + + if ncn.admin != *ncn_admin.key { + return Err(TipRouterError::IncorrectNcnAdmin.into()); + } + + match role { + ConfigAdminRole::FeeAdmin => { + config.fee_admin = *new_admin.key; + msg!("Fee admin set to {:?}", new_admin.key); + } + ConfigAdminRole::TieBreakerAdmin => { + config.tie_breaker_admin = *new_admin.key; + msg!("Tie breaker admin set to {:?}", new_admin.key); + } + } + + Ok(()) +}