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(())
+}