From 3fd07d4b0bda1f441891d67f8be98d01c5b113c0 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Fri, 8 Nov 2024 17:51:10 -0500 Subject: [PATCH] Register mints --- .../js/jito_tip_router/accounts/ncnConfig.ts | 8 + .../jito_tip_router/errors/jitoTipRouter.ts | 8 + .../js/jito_tip_router/instructions/index.ts | 1 + .../instructions/registerMint.ts | 260 ++++++++ .../jito_tip_router/programs/jitoTipRouter.ts | 10 +- clients/js/jito_tip_router/types/index.ts | 1 + clients/js/jito_tip_router/types/mintEntry.ts | 43 ++ .../src/generated/accounts/ncn_config.rs | 4 +- .../src/generated/errors/jito_tip_router.rs | 6 + .../src/generated/instructions/mod.rs | 3 +- .../generated/instructions/register_mint.rs | 567 ++++++++++++++++++ .../src/generated/types/mint_entry.rs | 19 + .../src/generated/types/mod.rs | 3 +- core/src/error.rs | 4 + core/src/instruction.rs | 11 + core/src/ncn_config.rs | 92 ++- idl/jito_tip_router.json | 89 +++ program/src/initialize_weight_table.rs | 22 +- program/src/lib.rs | 7 +- program/src/register_mint.rs | 69 +++ 20 files changed, 1196 insertions(+), 31 deletions(-) create mode 100644 clients/js/jito_tip_router/instructions/registerMint.ts create mode 100644 clients/js/jito_tip_router/types/mintEntry.ts create mode 100644 clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs create mode 100644 clients/rust/jito_tip_router/src/generated/types/mint_entry.rs create mode 100644 program/src/register_mint.rs diff --git a/clients/js/jito_tip_router/accounts/ncnConfig.ts b/clients/js/jito_tip_router/accounts/ncnConfig.ts index 6299e668..54af994e 100644 --- a/clients/js/jito_tip_router/accounts/ncnConfig.ts +++ b/clients/js/jito_tip_router/accounts/ncnConfig.ts @@ -37,8 +37,12 @@ import { import { getFeesDecoder, getFeesEncoder, + getMintEntryDecoder, + getMintEntryEncoder, type Fees, type FeesArgs, + type MintEntry, + type MintEntryArgs, } from '../types'; export type NcnConfig = { @@ -47,6 +51,7 @@ export type NcnConfig = { tieBreakerAdmin: Address; feeAdmin: Address; fees: Fees; + mintList: Array; bump: number; reserved: Array; }; @@ -57,6 +62,7 @@ export type NcnConfigArgs = { tieBreakerAdmin: Address; feeAdmin: Address; fees: FeesArgs; + mintList: Array; bump: number; reserved: Array; }; @@ -68,6 +74,7 @@ export function getNcnConfigEncoder(): Encoder { ['tieBreakerAdmin', getAddressEncoder()], ['feeAdmin', getAddressEncoder()], ['fees', getFeesEncoder()], + ['mintList', getArrayEncoder(getMintEntryEncoder(), { size: 64 })], ['bump', getU8Encoder()], ['reserved', getArrayEncoder(getU8Encoder(), { size: 127 })], ]); @@ -80,6 +87,7 @@ export function getNcnConfigDecoder(): Decoder { ['tieBreakerAdmin', getAddressDecoder()], ['feeAdmin', getAddressDecoder()], ['fees', getFeesDecoder()], + ['mintList', getArrayDecoder(getMintEntryDecoder(), { size: 64 })], ['bump', getU8Decoder()], ['reserved', getArrayDecoder(getU8Decoder(), { size: 127 })], ]); diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index 45ac3fa5..214b57d8 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -42,6 +42,10 @@ export const JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH = 0x2206; / export const JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH = 0x2207; // 8711 /** InvalidMintForWeightTable: Invalid mint for weight table */ export const JITO_TIP_ROUTER_ERROR__INVALID_MINT_FOR_WEIGHT_TABLE = 0x2208; // 8712 +/** ConfigMintsNotUpdated: Config supported mints do not match NCN Vault Count */ +export const JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED = 0x2209; // 8713 +/** ConfigMintListFull: NCN config vaults are at capacity */ +export const JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL = 0x220a; // 8714 /** FeeCapExceeded: Fee cap exceeded */ export const JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED = 0x2300; // 8960 /** IncorrectNcnAdmin: Incorrect NCN Admin */ @@ -55,6 +59,8 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW | typeof JITO_TIP_ROUTER_ERROR__CANNOT_CREATE_FUTURE_WEIGHT_TABLES | typeof JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR + | typeof JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL + | typeof JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED | typeof JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED @@ -77,6 +83,8 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW]: `Overflow`, [JITO_TIP_ROUTER_ERROR__CANNOT_CREATE_FUTURE_WEIGHT_TABLES]: `Cannnot create future weight tables`, [JITO_TIP_ROUTER_ERROR__CAST_TO_IMPRECISE_NUMBER_ERROR]: `Cast to imprecise number error`, + [JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL]: `NCN config vaults are at capacity`, + [JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED]: `Config supported mints do not match NCN Vault Count`, [JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO]: `Zero in the denominator`, [JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE]: `Duplicate mints in table`, [JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED]: `Fee cap exceeded`, diff --git a/clients/js/jito_tip_router/instructions/index.ts b/clients/js/jito_tip_router/instructions/index.ts index 5be64079..30486715 100644 --- a/clients/js/jito_tip_router/instructions/index.ts +++ b/clients/js/jito_tip_router/instructions/index.ts @@ -9,5 +9,6 @@ export * from './adminUpdateWeightTable'; export * from './initializeNCNConfig'; export * from './initializeWeightTable'; +export * from './registerMint'; export * from './setConfigFees'; export * from './setNewAdmin'; diff --git a/clients/js/jito_tip_router/instructions/registerMint.ts b/clients/js/jito_tip_router/instructions/registerMint.ts new file mode 100644 index 00000000..a7cb3e51 --- /dev/null +++ b/clients/js/jito_tip_router/instructions/registerMint.ts @@ -0,0 +1,260 @@ +/** + * 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 IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type WritableAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const REGISTER_MINT_DISCRIMINATOR = 5; + +export function getRegisterMintDiscriminatorBytes() { + return getU8Encoder().encode(REGISTER_MINT_DISCRIMINATOR); +} + +export type RegisterMintInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountRestakingConfig extends string | IAccountMeta = string, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountVault extends string | IAccountMeta = string, + TAccountVaultNcnTicket extends string | IAccountMeta = string, + TAccountNcnVaultTicket extends string | IAccountMeta = string, + TAccountRestakingProgramId extends string | IAccountMeta = string, + TAccountVaultProgramId extends string | IAccountMeta = string, + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountRestakingConfig extends string + ? ReadonlyAccount + : TAccountRestakingConfig, + TAccountNcnConfig extends string + ? WritableAccount + : TAccountNcnConfig, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountVault extends string + ? ReadonlyAccount + : TAccountVault, + TAccountVaultNcnTicket extends string + ? ReadonlyAccount + : TAccountVaultNcnTicket, + TAccountNcnVaultTicket extends string + ? ReadonlyAccount + : TAccountNcnVaultTicket, + TAccountRestakingProgramId extends string + ? ReadonlyAccount + : TAccountRestakingProgramId, + TAccountVaultProgramId extends string + ? ReadonlyAccount + : TAccountVaultProgramId, + ...TRemainingAccounts, + ] + >; + +export type RegisterMintInstructionData = { discriminator: number }; + +export type RegisterMintInstructionDataArgs = {}; + +export function getRegisterMintInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([['discriminator', getU8Encoder()]]), + (value) => ({ ...value, discriminator: REGISTER_MINT_DISCRIMINATOR }) + ); +} + +export function getRegisterMintInstructionDataDecoder(): Decoder { + return getStructDecoder([['discriminator', getU8Decoder()]]); +} + +export function getRegisterMintInstructionDataCodec(): Codec< + RegisterMintInstructionDataArgs, + RegisterMintInstructionData +> { + return combineCodec( + getRegisterMintInstructionDataEncoder(), + getRegisterMintInstructionDataDecoder() + ); +} + +export type RegisterMintInput< + TAccountRestakingConfig extends string = string, + TAccountNcnConfig extends string = string, + TAccountNcn extends string = string, + TAccountVault extends string = string, + TAccountVaultNcnTicket extends string = string, + TAccountNcnVaultTicket extends string = string, + TAccountRestakingProgramId extends string = string, + TAccountVaultProgramId extends string = string, +> = { + restakingConfig: Address; + ncnConfig: Address; + ncn: Address; + vault: Address; + vaultNcnTicket: Address; + ncnVaultTicket: Address; + restakingProgramId: Address; + vaultProgramId: Address; +}; + +export function getRegisterMintInstruction< + TAccountRestakingConfig extends string, + TAccountNcnConfig extends string, + TAccountNcn extends string, + TAccountVault extends string, + TAccountVaultNcnTicket extends string, + TAccountNcnVaultTicket extends string, + TAccountRestakingProgramId extends string, + TAccountVaultProgramId extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: RegisterMintInput< + TAccountRestakingConfig, + TAccountNcnConfig, + TAccountNcn, + TAccountVault, + TAccountVaultNcnTicket, + TAccountNcnVaultTicket, + TAccountRestakingProgramId, + TAccountVaultProgramId + >, + config?: { programAddress?: TProgramAddress } +): RegisterMintInstruction< + TProgramAddress, + TAccountRestakingConfig, + TAccountNcnConfig, + TAccountNcn, + TAccountVault, + TAccountVaultNcnTicket, + TAccountNcnVaultTicket, + TAccountRestakingProgramId, + TAccountVaultProgramId +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + restakingConfig: { + value: input.restakingConfig ?? null, + isWritable: false, + }, + ncnConfig: { value: input.ncnConfig ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + vault: { value: input.vault ?? null, isWritable: false }, + vaultNcnTicket: { value: input.vaultNcnTicket ?? null, isWritable: false }, + ncnVaultTicket: { value: input.ncnVaultTicket ?? null, isWritable: false }, + restakingProgramId: { + value: input.restakingProgramId ?? null, + isWritable: false, + }, + vaultProgramId: { value: input.vaultProgramId ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.restakingConfig), + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.vault), + getAccountMeta(accounts.vaultNcnTicket), + getAccountMeta(accounts.ncnVaultTicket), + getAccountMeta(accounts.restakingProgramId), + getAccountMeta(accounts.vaultProgramId), + ], + programAddress, + data: getRegisterMintInstructionDataEncoder().encode({}), + } as RegisterMintInstruction< + TProgramAddress, + TAccountRestakingConfig, + TAccountNcnConfig, + TAccountNcn, + TAccountVault, + TAccountVaultNcnTicket, + TAccountNcnVaultTicket, + TAccountRestakingProgramId, + TAccountVaultProgramId + >; + + return instruction; +} + +export type ParsedRegisterMintInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + restakingConfig: TAccountMetas[0]; + ncnConfig: TAccountMetas[1]; + ncn: TAccountMetas[2]; + vault: TAccountMetas[3]; + vaultNcnTicket: TAccountMetas[4]; + ncnVaultTicket: TAccountMetas[5]; + restakingProgramId: TAccountMetas[6]; + vaultProgramId: TAccountMetas[7]; + }; + data: RegisterMintInstructionData; +}; + +export function parseRegisterMintInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedRegisterMintInstruction { + if (instruction.accounts.length < 8) { + // 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: { + restakingConfig: getNextAccount(), + ncnConfig: getNextAccount(), + ncn: getNextAccount(), + vault: getNextAccount(), + vaultNcnTicket: getNextAccount(), + ncnVaultTicket: getNextAccount(), + restakingProgramId: getNextAccount(), + vaultProgramId: getNextAccount(), + }, + data: getRegisterMintInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index c3f29cd8..4a2d3d89 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -16,6 +16,7 @@ import { type ParsedAdminUpdateWeightTableInstruction, type ParsedInitializeNCNConfigInstruction, type ParsedInitializeWeightTableInstruction, + type ParsedRegisterMintInstruction, type ParsedSetConfigFeesInstruction, type ParsedSetNewAdminInstruction, } from '../instructions'; @@ -34,6 +35,7 @@ export enum JitoTipRouterInstruction { SetNewAdmin, InitializeWeightTable, AdminUpdateWeightTable, + RegisterMint, } export function identifyJitoTipRouterInstruction( @@ -55,6 +57,9 @@ export function identifyJitoTipRouterInstruction( if (containsBytes(data, getU8Encoder().encode(4), 0)) { return JitoTipRouterInstruction.AdminUpdateWeightTable; } + if (containsBytes(data, getU8Encoder().encode(5), 0)) { + return JitoTipRouterInstruction.RegisterMint; + } throw new Error( 'The provided instruction could not be identified as a jitoTipRouter instruction.' ); @@ -77,4 +82,7 @@ export type ParsedJitoTipRouterInstruction< } & ParsedInitializeWeightTableInstruction) | ({ instructionType: JitoTipRouterInstruction.AdminUpdateWeightTable; - } & ParsedAdminUpdateWeightTableInstruction); + } & ParsedAdminUpdateWeightTableInstruction) + | ({ + instructionType: JitoTipRouterInstruction.RegisterMint; + } & ParsedRegisterMintInstruction); diff --git a/clients/js/jito_tip_router/types/index.ts b/clients/js/jito_tip_router/types/index.ts index a78e6058..eb8f884c 100644 --- a/clients/js/jito_tip_router/types/index.ts +++ b/clients/js/jito_tip_router/types/index.ts @@ -9,4 +9,5 @@ export * from './configAdminRole'; export * from './fee'; export * from './fees'; +export * from './mintEntry'; export * from './weightEntry'; diff --git a/clients/js/jito_tip_router/types/mintEntry.ts b/clients/js/jito_tip_router/types/mintEntry.ts new file mode 100644 index 00000000..0934db01 --- /dev/null +++ b/clients/js/jito_tip_router/types/mintEntry.ts @@ -0,0 +1,43 @@ +/** + * 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, + getAddressDecoder, + getAddressEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + type Address, + type Codec, + type Decoder, + type Encoder, +} from '@solana/web3.js'; + +export type MintEntry = { mint: Address; vaultIndex: bigint }; + +export type MintEntryArgs = { mint: Address; vaultIndex: number | bigint }; + +export function getMintEntryEncoder(): Encoder { + return getStructEncoder([ + ['mint', getAddressEncoder()], + ['vaultIndex', getU64Encoder()], + ]); +} + +export function getMintEntryDecoder(): Decoder { + return getStructDecoder([ + ['mint', getAddressDecoder()], + ['vaultIndex', getU64Decoder()], + ]); +} + +export function getMintEntryCodec(): Codec { + return combineCodec(getMintEntryEncoder(), getMintEntryDecoder()); +} diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs b/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs index eaca31b0..d1884b28 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/ncn_config.rs @@ -7,7 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::pubkey::Pubkey; -use crate::generated::types::Fees; +use crate::generated::types::{Fees, MintEntry}; #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -29,6 +29,8 @@ pub struct NcnConfig { )] pub fee_admin: Pubkey, pub fees: Fees, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub mint_list: [MintEntry; 64], pub bump: u8, #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] pub reserved: [u8; 127], diff --git a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs index e0944766..f8d40e1a 100644 --- a/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs +++ b/clients/rust/jito_tip_router/src/generated/errors/jito_tip_router.rs @@ -51,6 +51,12 @@ pub enum JitoTipRouterError { /// 8712 - Invalid mint for weight table #[error("Invalid mint for weight table")] InvalidMintForWeightTable = 0x2208, + /// 8713 - Config supported mints do not match NCN Vault Count + #[error("Config supported mints do not match NCN Vault Count")] + ConfigMintsNotUpdated = 0x2209, + /// 8714 - NCN config vaults are at capacity + #[error("NCN config vaults are at capacity")] + ConfigMintListFull = 0x220A, /// 8960 - Fee cap exceeded #[error("Fee cap exceeded")] FeeCapExceeded = 0x2300, 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 271bac30..812f5ead 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs @@ -7,10 +7,11 @@ pub(crate) mod r#admin_update_weight_table; pub(crate) mod r#initialize_n_c_n_config; pub(crate) mod r#initialize_weight_table; +pub(crate) mod r#register_mint; pub(crate) mod r#set_config_fees; pub(crate) mod r#set_new_admin; pub use self::{ r#admin_update_weight_table::*, r#initialize_n_c_n_config::*, r#initialize_weight_table::*, - r#set_config_fees::*, r#set_new_admin::*, + r#register_mint::*, r#set_config_fees::*, r#set_new_admin::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs b/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs new file mode 100644 index 00000000..b3847c7b --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs @@ -0,0 +1,567 @@ +//! 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. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; + +/// Accounts. +pub struct RegisterMint { + pub restaking_config: solana_program::pubkey::Pubkey, + + pub ncn_config: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub vault: solana_program::pubkey::Pubkey, + + pub vault_ncn_ticket: solana_program::pubkey::Pubkey, + + pub ncn_vault_ticket: solana_program::pubkey::Pubkey, + + pub restaking_program_id: solana_program::pubkey::Pubkey, + + pub vault_program_id: solana_program::pubkey::Pubkey, +} + +impl RegisterMint { + pub fn instruction(&self) -> solana_program::instruction::Instruction { + self.instruction_with_remaining_accounts(&[]) + } + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + remaining_accounts: &[solana_program::instruction::AccountMeta], + ) -> solana_program::instruction::Instruction { + let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.vault, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.vault_ncn_ticket, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_vault_ticket, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.restaking_program_id, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.vault_program_id, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let data = RegisterMintInstructionData::new().try_to_vec().unwrap(); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct RegisterMintInstructionData { + discriminator: u8, +} + +impl RegisterMintInstructionData { + pub fn new() -> Self { + Self { discriminator: 5 } + } +} + +impl Default for RegisterMintInstructionData { + fn default() -> Self { + Self::new() + } +} + +/// Instruction builder for `RegisterMint`. +/// +/// ### Accounts: +/// +/// 0. `[]` restaking_config +/// 1. `[writable]` ncn_config +/// 2. `[]` ncn +/// 3. `[]` vault +/// 4. `[]` vault_ncn_ticket +/// 5. `[]` ncn_vault_ticket +/// 6. `[]` restaking_program_id +/// 7. `[]` vault_program_id +#[derive(Clone, Debug, Default)] +pub struct RegisterMintBuilder { + restaking_config: Option, + ncn_config: Option, + ncn: Option, + vault: Option, + vault_ncn_ticket: Option, + ncn_vault_ticket: Option, + restaking_program_id: Option, + vault_program_id: Option, + __remaining_accounts: Vec, +} + +impl RegisterMintBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn restaking_config( + &mut self, + restaking_config: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.restaking_config = Some(restaking_config); + self + } + #[inline(always)] + pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn_config = Some(ncn_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 vault(&mut self, vault: solana_program::pubkey::Pubkey) -> &mut Self { + self.vault = Some(vault); + self + } + #[inline(always)] + pub fn vault_ncn_ticket( + &mut self, + vault_ncn_ticket: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.vault_ncn_ticket = Some(vault_ncn_ticket); + self + } + #[inline(always)] + pub fn ncn_vault_ticket( + &mut self, + ncn_vault_ticket: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.ncn_vault_ticket = Some(ncn_vault_ticket); + 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 vault_program_id( + &mut self, + vault_program_id: solana_program::pubkey::Pubkey, + ) -> &mut Self { + self.vault_program_id = Some(vault_program_id); + 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 = RegisterMint { + restaking_config: self.restaking_config.expect("restaking_config is not set"), + ncn_config: self.ncn_config.expect("ncn_config is not set"), + ncn: self.ncn.expect("ncn is not set"), + vault: self.vault.expect("vault is not set"), + vault_ncn_ticket: self.vault_ncn_ticket.expect("vault_ncn_ticket is not set"), + ncn_vault_ticket: self.ncn_vault_ticket.expect("ncn_vault_ticket is not set"), + restaking_program_id: self + .restaking_program_id + .expect("restaking_program_id is not set"), + vault_program_id: self.vault_program_id.expect("vault_program_id is not set"), + }; + + accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) + } +} + +/// `register_mint` CPI accounts. +pub struct RegisterMintCpiAccounts<'a, 'b> { + pub restaking_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault_ncn_ticket: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_vault_ticket: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program_id: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault_program_id: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `register_mint` CPI instruction. +pub struct RegisterMintCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault_ncn_ticket: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_vault_ticket: &'b solana_program::account_info::AccountInfo<'a>, + + pub restaking_program_id: &'b solana_program::account_info::AccountInfo<'a>, + + pub vault_program_id: &'b solana_program::account_info::AccountInfo<'a>, +} + +impl<'a, 'b> RegisterMintCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: RegisterMintCpiAccounts<'a, 'b>, + ) -> Self { + Self { + __program: program, + restaking_config: accounts.restaking_config, + ncn_config: accounts.ncn_config, + ncn: accounts.ncn, + vault: accounts.vault, + vault_ncn_ticket: accounts.vault_ncn_ticket, + ncn_vault_ticket: accounts.ncn_vault_ticket, + restaking_program_id: accounts.restaking_program_id, + vault_program_id: accounts.vault_program_id, + } + } + #[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(8 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.vault.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.vault_ncn_ticket.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn_vault_ticket.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.restaking_program_id.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.vault_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 data = RegisterMintInstructionData::new().try_to_vec().unwrap(); + + let instruction = solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + }; + let mut account_infos = Vec::with_capacity(8 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.restaking_config.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.vault.clone()); + account_infos.push(self.vault_ncn_ticket.clone()); + account_infos.push(self.ncn_vault_ticket.clone()); + account_infos.push(self.restaking_program_id.clone()); + account_infos.push(self.vault_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 `RegisterMint` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[]` restaking_config +/// 1. `[writable]` ncn_config +/// 2. `[]` ncn +/// 3. `[]` vault +/// 4. `[]` vault_ncn_ticket +/// 5. `[]` ncn_vault_ticket +/// 6. `[]` restaking_program_id +/// 7. `[]` vault_program_id +#[derive(Clone, Debug)] +pub struct RegisterMintCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(RegisterMintCpiBuilderInstruction { + __program: program, + restaking_config: None, + ncn_config: None, + ncn: None, + vault: None, + vault_ncn_ticket: None, + ncn_vault_ticket: None, + restaking_program_id: None, + vault_program_id: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn restaking_config( + &mut self, + restaking_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.restaking_config = Some(restaking_config); + self + } + #[inline(always)] + pub fn ncn_config( + &mut self, + ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_config = Some(ncn_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 vault(&mut self, vault: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.vault = Some(vault); + self + } + #[inline(always)] + pub fn vault_ncn_ticket( + &mut self, + vault_ncn_ticket: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.vault_ncn_ticket = Some(vault_ncn_ticket); + self + } + #[inline(always)] + pub fn ncn_vault_ticket( + &mut self, + ncn_vault_ticket: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.ncn_vault_ticket = Some(ncn_vault_ticket); + 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 vault_program_id( + &mut self, + vault_program_id: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.vault_program_id = Some(vault_program_id); + 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 instruction = RegisterMintCpi { + __program: self.instruction.__program, + + restaking_config: self + .instruction + .restaking_config + .expect("restaking_config is not set"), + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + vault: self.instruction.vault.expect("vault is not set"), + + vault_ncn_ticket: self + .instruction + .vault_ncn_ticket + .expect("vault_ncn_ticket is not set"), + + ncn_vault_ticket: self + .instruction + .ncn_vault_ticket + .expect("ncn_vault_ticket is not set"), + + restaking_program_id: self + .instruction + .restaking_program_id + .expect("restaking_program_id is not set"), + + vault_program_id: self + .instruction + .vault_program_id + .expect("vault_program_id is not set"), + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct RegisterMintCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + restaking_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + vault: Option<&'b solana_program::account_info::AccountInfo<'a>>, + vault_ncn_ticket: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn_vault_ticket: Option<&'b solana_program::account_info::AccountInfo<'a>>, + restaking_program_id: Option<&'b solana_program::account_info::AccountInfo<'a>>, + vault_program_id: Option<&'b solana_program::account_info::AccountInfo<'a>>, + /// 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/mint_entry.rs b/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs new file mode 100644 index 00000000..4b789100 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs @@ -0,0 +1,19 @@ +//! 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. +//! +//! + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MintEntry { + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub mint: Pubkey, + pub vault_index: u64, +} 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 ea35df80..36abae20 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mod.rs @@ -7,6 +7,7 @@ pub(crate) mod r#config_admin_role; pub(crate) mod r#fee; pub(crate) mod r#fees; +pub(crate) mod r#mint_entry; pub(crate) mod r#weight_entry; -pub use self::{r#config_admin_role::*, r#fee::*, r#fees::*, r#weight_entry::*}; +pub use self::{r#config_admin_role::*, r#fee::*, r#fees::*, r#mint_entry::*, r#weight_entry::*}; diff --git a/core/src/error.rs b/core/src/error.rs index 21fe13a6..bf430c1e 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -32,6 +32,10 @@ pub enum TipRouterError { WeightMintsDoNotMatchMintHash, #[error("Invalid mint for weight table")] InvalidMintForWeightTable, + #[error("Config supported mints do not match NCN Vault Count")] + ConfigMintsNotUpdated, + #[error("NCN config vaults are at capacity")] + ConfigMintListFull, #[error("Fee cap exceeded")] FeeCapExceeded = 0x2300, #[error("Incorrect NCN Admin")] diff --git a/core/src/instruction.rs b/core/src/instruction.rs index 169247ab..a29ba963 100644 --- a/core/src/instruction.rs +++ b/core/src/instruction.rs @@ -73,4 +73,15 @@ pub enum WeightTableInstruction { ncn_epoch: u64, weight: u128, }, + + /// Registers a mint with the NCN config + #[account(0, name = "restaking_config")] + #[account(1, writable, name = "ncn_config")] + #[account(2, name = "ncn")] + #[account(3, name = "vault")] + #[account(4, name = "vault_ncn_ticket")] + #[account(5, name = "ncn_vault_ticket")] + #[account(6, name = "restaking_program_id")] + #[account(7, name = "vault_program_id")] + RegisterMint, } diff --git a/core/src/ncn_config.rs b/core/src/ncn_config.rs index d7f72bb0..9a091364 100644 --- a/core/src/ncn_config.rs +++ b/core/src/ncn_config.rs @@ -1,21 +1,38 @@ -// Global configuration for the tip router - -// Contains: -// Main NCN address - updatable? -// Admins -// - config admin - should this be here? or just use main NCN admin? -// - Weight table upload admin (hot wallet) (this exists in NCN, do we want it here too? since custom weight table) -// - Tie breaker admin (hot wallet) (depending on tie breaker process?) -// DAO fee share -// NCN fee share +use std::collections::HashSet; use bytemuck::{Pod, Zeroable}; -use jito_bytemuck::{AccountDeserialize, Discriminator}; +use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator}; use shank::{ShankAccount, ShankType}; use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; use crate::{discriminators::Discriminators, fees::Fees}; +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)] +#[repr(C)] +pub struct MintEntry { + mint: Pubkey, + vault_index: PodU64, +} + +impl MintEntry { + pub fn new(mint: Pubkey, vault_index: u64) -> Self { + Self { + mint, + vault_index: PodU64::from(vault_index), + } + } + + pub fn vault_index(&self) -> u64 { + self.vault_index.into() + } +} + +impl Default for MintEntry { + fn default() -> Self { + Self::new(Pubkey::default(), 0) + } +} + #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] #[repr(C)] pub struct NcnConfig { @@ -28,10 +45,12 @@ pub struct NcnConfig { pub fees: Fees, + /// List of mints associated with this NCN + mint_list: [MintEntry; 64], // TODO is this the right size + /// Bump seed for the PDA pub bump: u8, - - /// Reserved space + // /// Reserved space reserved: [u8; 127], } @@ -40,17 +59,13 @@ impl Discriminator for NcnConfig { } impl NcnConfig { - pub const fn new( - ncn: Pubkey, - tie_breaker_admin: Pubkey, - fee_admin: Pubkey, - fees: Fees, - ) -> Self { + pub fn new(ncn: Pubkey, tie_breaker_admin: Pubkey, fee_admin: Pubkey, fees: Fees) -> Self { Self { ncn, tie_breaker_admin, fee_admin, fees, + mint_list: [MintEntry::default(); 64], bump: 0, reserved: [0; 127], } @@ -69,6 +84,45 @@ impl NcnConfig { (address, bump, seeds) } + pub fn add_mint(&mut self, mint: Pubkey, vault_index: u64) -> Result<(), ProgramError> { + // Check if (mint, vault_index) is already in the list + if self + .mint_list + .iter() + .any(|m| m.mint == mint && m.vault_index() == vault_index) + { + return Ok(()); + } + + // Insert at the first empty slot + let mint_entry = self + .mint_list + .iter_mut() + .find(|m| m.mint == MintEntry::default().mint) + .ok_or(ProgramError::InvalidAccountData)?; // TODO list already full error + + *mint_entry = MintEntry::new(mint, vault_index); + Ok(()) + } + + pub fn mint_count(&self) -> usize { + self.mint_list + .iter() + .filter(|m| m.mint != Pubkey::default()) + .count() + } + + pub fn get_unique_mints(&self) -> Vec { + let mut unique_mints: HashSet = HashSet::new(); + self.mint_list + .iter() + .filter(|m| m.mint != Pubkey::default()) + .for_each(|m| { + unique_mints.insert(m.mint); + }); + unique_mints.into_iter().collect() + } + /// Loads the NCN [`Config`] account /// /// # Arguments diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 3fd2c53b..ebca9e67 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -262,6 +262,56 @@ "type": "u8", "value": 4 } + }, + { + "name": "RegisterMint", + "accounts": [ + { + "name": "restakingConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "ncnConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "vault", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultNcnTicket", + "isMut": false, + "isSigner": false + }, + { + "name": "ncnVaultTicket", + "isMut": false, + "isSigner": false + }, + { + "name": "restakingProgramId", + "isMut": false, + "isSigner": false + }, + { + "name": "vaultProgramId", + "isMut": false, + "isSigner": false + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 5 + } } ], "accounts": [ @@ -288,6 +338,17 @@ "defined": "Fees" } }, + { + "name": "mintList", + "type": { + "array": [ + { + "defined": "MintEntry" + }, + 64 + ] + } + }, { "name": "bump", "type": "u8" @@ -410,6 +471,24 @@ ] } }, + { + "name": "MintEntry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "vaultIndex", + "type": { + "defined": "PodU64" + } + } + ] + } + }, { "name": "WeightEntry", "type": { @@ -535,6 +614,16 @@ "name": "InvalidMintForWeightTable", "msg": "Invalid mint for weight table" }, + { + "code": 8713, + "name": "ConfigMintsNotUpdated", + "msg": "Config supported mints do not match NCN Vault Count" + }, + { + "code": 8714, + "name": "ConfigMintListFull", + "msg": "NCN config vaults are at capacity" + }, { "code": 8960, "name": "FeeCapExceeded", diff --git a/program/src/initialize_weight_table.rs b/program/src/initialize_weight_table.rs index 9ef5ef43..d3a28fba 100644 --- a/program/src/initialize_weight_table.rs +++ b/program/src/initialize_weight_table.rs @@ -5,7 +5,7 @@ use jito_jsm_core::{ create_account, loader::{load_signer, load_system_account, load_system_program}, }; -use jito_restaking_core::config::Config; +use jito_restaking_core::{config::Config, ncn::Ncn}; use jito_tip_router_core::{ error::TipRouterError, ncn_config::NcnConfig, weight_table::WeightTable, }; @@ -29,6 +29,7 @@ pub fn process_initialize_weight_table( NcnConfig::load(program_id, ncn.key, ncn_config, false)?; Config::load(restaking_program_id.key, restaking_config, false)?; + Ncn::load(restaking_program_id.key, ncn, false)?; let ncn_epoch_length = { let config_data = restaking_config.data.borrow(); @@ -36,12 +37,15 @@ pub fn process_initialize_weight_table( config.epoch_length() }; - let _todo_pubkeys = { - let ncn_config_data = ncn_config.data.borrow(); - let ncn_config = NcnConfig::try_from_slice_unchecked(&ncn_config_data)?; - ncn_config.bump + let vault_count = { + let ncn_data = ncn.data.borrow(); + let ncn = Ncn::try_from_slice_unchecked(&ncn_data)?; + ncn.vault_count() }; + let ncn_config_data: std::cell::Ref<'_, &mut [u8]> = ncn_config.data.borrow(); + let ncn_config = NcnConfig::try_from_slice_unchecked(&ncn_config_data)?; + load_system_account(weight_table, true)?; load_system_program(system_program)?; load_signer(payer, true)?; @@ -75,6 +79,11 @@ pub fn process_initialize_weight_table( return Err(ProgramError::InvalidAccountData); } + if vault_count as usize != ncn_config.mint_count() { + msg!("Vault count does not match supported mint count"); + return Err(ProgramError::InvalidAccountData); + } + msg!( "Initializing Weight Table {} for NCN: {} at epoch: {}", weight_table.key, @@ -97,8 +106,7 @@ pub fn process_initialize_weight_table( *weight_table_account = WeightTable::new(*ncn.key, ncn_epoch, current_slot, weight_table_bump); - //TODO pass in st_mint list from config - weight_table_account.initalize_weight_table(&[])?; + weight_table_account.initalize_weight_table(&ncn_config.get_unique_mints())?; Ok(()) } diff --git a/program/src/lib.rs b/program/src/lib.rs index 77761aa2..34a6155b 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,6 +1,7 @@ mod admin_update_weight_table; mod initialize_ncn_config; mod initialize_weight_table; +mod register_mint; mod set_config_fees; mod set_new_admin; @@ -18,7 +19,7 @@ use solana_security_txt::security_txt; use crate::{ admin_update_weight_table::process_admin_update_weight_table, initialize_ncn_config::process_initialize_ncn_config, - initialize_weight_table::process_initialize_weight_table, + initialize_weight_table::process_initialize_weight_table, register_mint::process_register_mint, set_config_fees::process_set_config_fees, }; @@ -101,5 +102,9 @@ pub fn process_instruction( msg!("Instruction: SetNewAdmin"); process_set_new_admin(program_id, accounts, role) } + WeightTableInstruction::RegisterMint => { + msg!("Instruction: RegisterMint"); + process_register_mint(program_id, accounts) + } } } diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs new file mode 100644 index 00000000..ef6c2ddc --- /dev/null +++ b/program/src/register_mint.rs @@ -0,0 +1,69 @@ +use jito_bytemuck::AccountDeserialize; +use jito_restaking_core::{config::Config, ncn::Ncn, ncn_vault_ticket::NcnVaultTicket}; +use jito_tip_router_core::ncn_config::NcnConfig; +use jito_vault_core::{vault::Vault, vault_ncn_ticket::VaultNcnTicket}; +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, + sysvar::{clock::Clock, Sysvar}, +}; + +pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let [restaking_config, ncn_config, ncn, vault, vault_ncn_ticket, ncn_vault_ticket, restaking_program_id, vault_program_id] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + NcnConfig::load(program_id, ncn.key, ncn_config, true)?; + Ncn::load(restaking_program_id.key, ncn, false)?; + Vault::load(vault_program_id.key, vault, false)?; + VaultNcnTicket::load(vault_program_id.key, vault_ncn_ticket, vault, ncn, false)?; + NcnVaultTicket::load( + restaking_program_id.key, + ncn_vault_ticket, + ncn, + vault, + false, + )?; + + let epoch_length = { + let restaking_config_data = restaking_config.data.borrow(); + Config::try_from_slice_unchecked(&restaking_config_data)?.epoch_length() + }; + + let slot = Clock::get()?.slot; + + // Verify tickets are active + let vault_ncn_ticket_data = vault_ncn_ticket.data.borrow(); + let vault_ncn_ticket = VaultNcnTicket::try_from_slice_unchecked(&vault_ncn_ticket_data)?; + if !vault_ncn_ticket + .state + .is_active_or_cooldown(slot, epoch_length) + { + msg!("Vault NCN ticket is not enabled"); + return Err(ProgramError::InvalidAccountData); + } + + let ncn_vault_ticket_data = ncn_vault_ticket.data.borrow(); + let ncn_vault_ticket = NcnVaultTicket::try_from_slice_unchecked(&ncn_vault_ticket_data)?; + if !ncn_vault_ticket + .state + .is_active_or_cooldown(slot, epoch_length) + { + msg!("NCN vault ticket is not enabled"); + return Err(ProgramError::InvalidAccountData); + } + + let vault_data = vault.data.borrow(); + let vault = Vault::try_from_slice_unchecked(&vault_data)?; + + let mut ncn_config_data = ncn_config.try_borrow_mut_data()?; + let ncn_config = NcnConfig::try_from_slice_unchecked_mut(&mut ncn_config_data)?; + ncn_config.add_mint(vault.supported_mint, vault.vault_index())?; + + Ok(()) +}