From 3fd07d4b0bda1f441891d67f8be98d01c5b113c0 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Fri, 8 Nov 2024 17:51:10 -0500 Subject: [PATCH 1/9] 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 6299e66..54af994 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 45ac3fa..214b57d 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 5be6407..3048671 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 0000000..a7cb3e5 --- /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 c3f29cd..4a2d3d8 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 a78e605..eb8f884 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 0000000..0934db0 --- /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 eaca31b..d1884b2 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 e094476..f8d40e1 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 271bac3..812f5ea 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 0000000..b3847c7 --- /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 0000000..4b78910 --- /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 ea35df8..36abae2 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 21fe13a..bf430c1 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 169247a..a29ba96 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 d7f72bb..9a09136 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 3fd2c53..ebca9e6 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 9ef5ef4..d3a28fb 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 77761aa..34a6155 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 0000000..ef6c2dd --- /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(()) +} From 01bee5add42a9538fc3102a71cb8cb2c98aca3d3 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 12 Nov 2024 17:19:37 -0500 Subject: [PATCH 2/9] Move tracked_mints to its own account --- clients/js/jito_tip_router/accounts/index.ts | 1 + .../js/jito_tip_router/accounts/ncnConfig.ts | 8 - .../jito_tip_router/accounts/trackedMints.ts | 135 ++++++ .../jito_tip_router/errors/jitoTipRouter.ts | 8 + .../js/jito_tip_router/instructions/index.ts | 1 + .../instructions/initializeTrackedMints.ts | 229 +++++++++ .../instructions/registerMint.ts | 28 +- .../jito_tip_router/programs/jitoTipRouter.ts | 11 +- clients/js/jito_tip_router/types/mintEntry.ts | 36 +- .../src/generated/accounts/mod.rs | 3 +- .../src/generated/accounts/ncn_config.rs | 4 +- .../src/generated/accounts/tracked_mints.rs | 69 +++ .../src/generated/errors/jito_tip_router.rs | 6 + .../instructions/initialize_tracked_mints.rs | 437 ++++++++++++++++++ .../src/generated/instructions/mod.rs | 5 +- .../generated/instructions/register_mint.rs | 41 +- .../src/generated/types/mint_entry.rs | 7 +- core/src/discriminators.rs | 1 + core/src/error.rs | 4 + core/src/instruction.rs | 10 +- core/src/lib.rs | 1 + core/src/ncn_config.rs | 82 +--- core/src/tracked_mints.rs | 224 +++++++++ idl/jito_tip_router.json | 113 ++++- program/src/initialize_tracked_mints.rs | 53 +++ program/src/initialize_weight_table.rs | 14 +- program/src/lib.rs | 6 + program/src/register_mint.rs | 12 +- 28 files changed, 1400 insertions(+), 149 deletions(-) create mode 100644 clients/js/jito_tip_router/accounts/trackedMints.ts create mode 100644 clients/js/jito_tip_router/instructions/initializeTrackedMints.ts create mode 100644 clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs create mode 100644 clients/rust/jito_tip_router/src/generated/instructions/initialize_tracked_mints.rs create mode 100644 core/src/tracked_mints.rs create mode 100644 program/src/initialize_tracked_mints.rs diff --git a/clients/js/jito_tip_router/accounts/index.ts b/clients/js/jito_tip_router/accounts/index.ts index 033882c..4c53566 100644 --- a/clients/js/jito_tip_router/accounts/index.ts +++ b/clients/js/jito_tip_router/accounts/index.ts @@ -7,4 +7,5 @@ */ export * from './ncnConfig'; +export * from './trackedMints'; export * from './weightTable'; diff --git a/clients/js/jito_tip_router/accounts/ncnConfig.ts b/clients/js/jito_tip_router/accounts/ncnConfig.ts index 54af994..6299e66 100644 --- a/clients/js/jito_tip_router/accounts/ncnConfig.ts +++ b/clients/js/jito_tip_router/accounts/ncnConfig.ts @@ -37,12 +37,8 @@ import { import { getFeesDecoder, getFeesEncoder, - getMintEntryDecoder, - getMintEntryEncoder, type Fees, type FeesArgs, - type MintEntry, - type MintEntryArgs, } from '../types'; export type NcnConfig = { @@ -51,7 +47,6 @@ export type NcnConfig = { tieBreakerAdmin: Address; feeAdmin: Address; fees: Fees; - mintList: Array; bump: number; reserved: Array; }; @@ -62,7 +57,6 @@ export type NcnConfigArgs = { tieBreakerAdmin: Address; feeAdmin: Address; fees: FeesArgs; - mintList: Array; bump: number; reserved: Array; }; @@ -74,7 +68,6 @@ export function getNcnConfigEncoder(): Encoder { ['tieBreakerAdmin', getAddressEncoder()], ['feeAdmin', getAddressEncoder()], ['fees', getFeesEncoder()], - ['mintList', getArrayEncoder(getMintEntryEncoder(), { size: 64 })], ['bump', getU8Encoder()], ['reserved', getArrayEncoder(getU8Encoder(), { size: 127 })], ]); @@ -87,7 +80,6 @@ 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/accounts/trackedMints.ts b/clients/js/jito_tip_router/accounts/trackedMints.ts new file mode 100644 index 0000000..a275f53 --- /dev/null +++ b/clients/js/jito_tip_router/accounts/trackedMints.ts @@ -0,0 +1,135 @@ +/** + * 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 { + assertAccountExists, + assertAccountsExist, + combineCodec, + decodeAccount, + fetchEncodedAccount, + fetchEncodedAccounts, + getAddressDecoder, + getAddressEncoder, + getArrayDecoder, + getArrayEncoder, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + type Account, + type Address, + type Codec, + type Decoder, + type EncodedAccount, + type Encoder, + type FetchAccountConfig, + type FetchAccountsConfig, + type MaybeAccount, + type MaybeEncodedAccount, +} from '@solana/web3.js'; +import { + getMintEntryDecoder, + getMintEntryEncoder, + type MintEntry, + type MintEntryArgs, +} from '../types'; + +export type TrackedMints = { + discriminator: bigint; + ncn: Address; + bump: number; + reserved: Array; + stMintList: Array; +}; + +export type TrackedMintsArgs = { + discriminator: number | bigint; + ncn: Address; + bump: number; + reserved: Array; + stMintList: Array; +}; + +export function getTrackedMintsEncoder(): Encoder { + return getStructEncoder([ + ['discriminator', getU64Encoder()], + ['ncn', getAddressEncoder()], + ['bump', getU8Encoder()], + ['reserved', getArrayEncoder(getU8Encoder(), { size: 7 })], + ['stMintList', getArrayEncoder(getMintEntryEncoder(), { size: 64 })], + ]); +} + +export function getTrackedMintsDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU64Decoder()], + ['ncn', getAddressDecoder()], + ['bump', getU8Decoder()], + ['reserved', getArrayDecoder(getU8Decoder(), { size: 7 })], + ['stMintList', getArrayDecoder(getMintEntryDecoder(), { size: 64 })], + ]); +} + +export function getTrackedMintsCodec(): Codec { + return combineCodec(getTrackedMintsEncoder(), getTrackedMintsDecoder()); +} + +export function decodeTrackedMints( + encodedAccount: EncodedAccount +): Account; +export function decodeTrackedMints( + encodedAccount: MaybeEncodedAccount +): MaybeAccount; +export function decodeTrackedMints( + encodedAccount: EncodedAccount | MaybeEncodedAccount +): Account | MaybeAccount { + return decodeAccount( + encodedAccount as MaybeEncodedAccount, + getTrackedMintsDecoder() + ); +} + +export async function fetchTrackedMints( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchMaybeTrackedMints(rpc, address, config); + assertAccountExists(maybeAccount); + return maybeAccount; +} + +export async function fetchMaybeTrackedMints( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config); + return decodeTrackedMints(maybeAccount); +} + +export async function fetchAllTrackedMints( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchAllMaybeTrackedMints(rpc, addresses, config); + assertAccountsExist(maybeAccounts); + return maybeAccounts; +} + +export async function fetchAllMaybeTrackedMints( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config); + return maybeAccounts.map((maybeAccount) => decodeTrackedMints(maybeAccount)); +} diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index 214b57d..a446993 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -46,6 +46,10 @@ export const JITO_TIP_ROUTER_ERROR__INVALID_MINT_FOR_WEIGHT_TABLE = 0x2208; // 8 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 +/** TrackedMintListFull: Tracked mints are at capacity */ +export const JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL = 0x220b; // 8715 +/** VaultIndexAlreadyInUse: Vault index already in use by a different mint */ +export const JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE = 0x220c; // 8716 /** FeeCapExceeded: Fee cap exceeded */ export const JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED = 0x2300; // 8960 /** IncorrectNcnAdmin: Incorrect NCN Admin */ @@ -73,6 +77,8 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__NEW_PRECISE_NUMBER_ERROR | typeof JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE + | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL + | typeof JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_TABLE_ALREADY_INITIALIZED; @@ -97,6 +103,8 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__NEW_PRECISE_NUMBER_ERROR]: `New precise number error`, [JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE]: `There are no mints in the table`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE]: `Too many mints for table`, + [JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL]: `Tracked mints are at capacity`, + [JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE]: `Vault index already in use by a different mint`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH]: `Weight mints do not match - length`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH]: `Weight mints do not match - mint hash`, [JITO_TIP_ROUTER_ERROR__WEIGHT_TABLE_ALREADY_INITIALIZED]: `Weight table already initialized`, diff --git a/clients/js/jito_tip_router/instructions/index.ts b/clients/js/jito_tip_router/instructions/index.ts index 3048671..b46aa8b 100644 --- a/clients/js/jito_tip_router/instructions/index.ts +++ b/clients/js/jito_tip_router/instructions/index.ts @@ -8,6 +8,7 @@ export * from './adminUpdateWeightTable'; export * from './initializeNCNConfig'; +export * from './initializeTrackedMints'; export * from './initializeWeightTable'; export * from './registerMint'; export * from './setConfigFees'; diff --git a/clients/js/jito_tip_router/instructions/initializeTrackedMints.ts b/clients/js/jito_tip_router/instructions/initializeTrackedMints.ts new file mode 100644 index 0000000..a113a59 --- /dev/null +++ b/clients/js/jito_tip_router/instructions/initializeTrackedMints.ts @@ -0,0 +1,229 @@ +/** + * 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 TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/web3.js'; +import { JITO_TIP_ROUTER_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const INITIALIZE_TRACKED_MINTS_DISCRIMINATOR = 6; + +export function getInitializeTrackedMintsDiscriminatorBytes() { + return getU8Encoder().encode(INITIALIZE_TRACKED_MINTS_DISCRIMINATOR); +} + +export type InitializeTrackedMintsInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountNcnConfig extends string | IAccountMeta = string, + TAccountTrackedMints extends string | IAccountMeta = string, + TAccountNcn extends string | IAccountMeta = string, + TAccountPayer extends string | IAccountMeta = string, + TAccountSystemProgram extends + | string + | IAccountMeta = '11111111111111111111111111111111', + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountNcnConfig extends string + ? ReadonlyAccount + : TAccountNcnConfig, + TAccountTrackedMints extends string + ? WritableAccount + : TAccountTrackedMints, + TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountPayer extends string + ? WritableSignerAccount & + IAccountSignerMeta + : TAccountPayer, + TAccountSystemProgram extends string + ? ReadonlyAccount + : TAccountSystemProgram, + ...TRemainingAccounts, + ] + >; + +export type InitializeTrackedMintsInstructionData = { discriminator: number }; + +export type InitializeTrackedMintsInstructionDataArgs = {}; + +export function getInitializeTrackedMintsInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([['discriminator', getU8Encoder()]]), + (value) => ({ + ...value, + discriminator: INITIALIZE_TRACKED_MINTS_DISCRIMINATOR, + }) + ); +} + +export function getInitializeTrackedMintsInstructionDataDecoder(): Decoder { + return getStructDecoder([['discriminator', getU8Decoder()]]); +} + +export function getInitializeTrackedMintsInstructionDataCodec(): Codec< + InitializeTrackedMintsInstructionDataArgs, + InitializeTrackedMintsInstructionData +> { + return combineCodec( + getInitializeTrackedMintsInstructionDataEncoder(), + getInitializeTrackedMintsInstructionDataDecoder() + ); +} + +export type InitializeTrackedMintsInput< + TAccountNcnConfig extends string = string, + TAccountTrackedMints extends string = string, + TAccountNcn extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, +> = { + ncnConfig: Address; + trackedMints: Address; + ncn: Address; + payer: TransactionSigner; + systemProgram?: Address; +}; + +export function getInitializeTrackedMintsInstruction< + TAccountNcnConfig extends string, + TAccountTrackedMints extends string, + TAccountNcn extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TProgramAddress extends Address = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, +>( + input: InitializeTrackedMintsInput< + TAccountNcnConfig, + TAccountTrackedMints, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram + >, + config?: { programAddress?: TProgramAddress } +): InitializeTrackedMintsInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountTrackedMints, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram +> { + // Program address. + const programAddress = + config?.programAddress ?? JITO_TIP_ROUTER_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + ncnConfig: { value: input.ncnConfig ?? null, isWritable: false }, + trackedMints: { value: input.trackedMints ?? null, isWritable: true }, + ncn: { value: input.ncn ?? null, isWritable: false }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Resolve default values. + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = + '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>; + } + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.trackedMints), + getAccountMeta(accounts.ncn), + getAccountMeta(accounts.payer), + getAccountMeta(accounts.systemProgram), + ], + programAddress, + data: getInitializeTrackedMintsInstructionDataEncoder().encode({}), + } as InitializeTrackedMintsInstruction< + TProgramAddress, + TAccountNcnConfig, + TAccountTrackedMints, + TAccountNcn, + TAccountPayer, + TAccountSystemProgram + >; + + return instruction; +} + +export type ParsedInitializeTrackedMintsInstruction< + TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + ncnConfig: TAccountMetas[0]; + trackedMints: TAccountMetas[1]; + ncn: TAccountMetas[2]; + payer: TAccountMetas[3]; + systemProgram: TAccountMetas[4]; + }; + data: InitializeTrackedMintsInstructionData; +}; + +export function parseInitializeTrackedMintsInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedInitializeTrackedMintsInstruction { + 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: { + ncnConfig: getNextAccount(), + trackedMints: getNextAccount(), + ncn: getNextAccount(), + payer: getNextAccount(), + systemProgram: getNextAccount(), + }, + data: getInitializeTrackedMintsInstructionDataDecoder().decode( + instruction.data + ), + }; +} diff --git a/clients/js/jito_tip_router/instructions/registerMint.ts b/clients/js/jito_tip_router/instructions/registerMint.ts index a7cb3e5..9164154 100644 --- a/clients/js/jito_tip_router/instructions/registerMint.ts +++ b/clients/js/jito_tip_router/instructions/registerMint.ts @@ -36,7 +36,7 @@ export function getRegisterMintDiscriminatorBytes() { export type RegisterMintInstruction< TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, TAccountRestakingConfig extends string | IAccountMeta = string, - TAccountNcnConfig extends string | IAccountMeta = string, + TAccountTrackedMints extends string | IAccountMeta = string, TAccountNcn extends string | IAccountMeta = string, TAccountVault extends string | IAccountMeta = string, TAccountVaultNcnTicket extends string | IAccountMeta = string, @@ -51,9 +51,9 @@ export type RegisterMintInstruction< TAccountRestakingConfig extends string ? ReadonlyAccount : TAccountRestakingConfig, - TAccountNcnConfig extends string - ? WritableAccount - : TAccountNcnConfig, + TAccountTrackedMints extends string + ? WritableAccount + : TAccountTrackedMints, TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, TAccountVault extends string ? ReadonlyAccount @@ -101,7 +101,7 @@ export function getRegisterMintInstructionDataCodec(): Codec< export type RegisterMintInput< TAccountRestakingConfig extends string = string, - TAccountNcnConfig extends string = string, + TAccountTrackedMints extends string = string, TAccountNcn extends string = string, TAccountVault extends string = string, TAccountVaultNcnTicket extends string = string, @@ -110,7 +110,7 @@ export type RegisterMintInput< TAccountVaultProgramId extends string = string, > = { restakingConfig: Address; - ncnConfig: Address; + trackedMints: Address; ncn: Address; vault: Address; vaultNcnTicket: Address; @@ -121,7 +121,7 @@ export type RegisterMintInput< export function getRegisterMintInstruction< TAccountRestakingConfig extends string, - TAccountNcnConfig extends string, + TAccountTrackedMints extends string, TAccountNcn extends string, TAccountVault extends string, TAccountVaultNcnTicket extends string, @@ -132,7 +132,7 @@ export function getRegisterMintInstruction< >( input: RegisterMintInput< TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountVault, TAccountVaultNcnTicket, @@ -144,7 +144,7 @@ export function getRegisterMintInstruction< ): RegisterMintInstruction< TProgramAddress, TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountVault, TAccountVaultNcnTicket, @@ -162,7 +162,7 @@ export function getRegisterMintInstruction< value: input.restakingConfig ?? null, isWritable: false, }, - ncnConfig: { value: input.ncnConfig ?? null, isWritable: true }, + trackedMints: { value: input.trackedMints ?? null, isWritable: true }, ncn: { value: input.ncn ?? null, isWritable: false }, vault: { value: input.vault ?? null, isWritable: false }, vaultNcnTicket: { value: input.vaultNcnTicket ?? null, isWritable: false }, @@ -182,7 +182,7 @@ export function getRegisterMintInstruction< const instruction = { accounts: [ getAccountMeta(accounts.restakingConfig), - getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.trackedMints), getAccountMeta(accounts.ncn), getAccountMeta(accounts.vault), getAccountMeta(accounts.vaultNcnTicket), @@ -195,7 +195,7 @@ export function getRegisterMintInstruction< } as RegisterMintInstruction< TProgramAddress, TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountVault, TAccountVaultNcnTicket, @@ -214,7 +214,7 @@ export type ParsedRegisterMintInstruction< programAddress: Address; accounts: { restakingConfig: TAccountMetas[0]; - ncnConfig: TAccountMetas[1]; + trackedMints: TAccountMetas[1]; ncn: TAccountMetas[2]; vault: TAccountMetas[3]; vaultNcnTicket: TAccountMetas[4]; @@ -247,7 +247,7 @@ export function parseRegisterMintInstruction< programAddress: instruction.programAddress, accounts: { restakingConfig: getNextAccount(), - ncnConfig: getNextAccount(), + trackedMints: getNextAccount(), ncn: getNextAccount(), vault: getNextAccount(), vaultNcnTicket: getNextAccount(), diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index 4a2d3d8..5d846c6 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -15,6 +15,7 @@ import { import { type ParsedAdminUpdateWeightTableInstruction, type ParsedInitializeNCNConfigInstruction, + type ParsedInitializeTrackedMintsInstruction, type ParsedInitializeWeightTableInstruction, type ParsedRegisterMintInstruction, type ParsedSetConfigFeesInstruction, @@ -26,6 +27,7 @@ export const JITO_TIP_ROUTER_PROGRAM_ADDRESS = export enum JitoTipRouterAccount { NcnConfig, + TrackedMints, WeightTable, } @@ -36,6 +38,7 @@ export enum JitoTipRouterInstruction { InitializeWeightTable, AdminUpdateWeightTable, RegisterMint, + InitializeTrackedMints, } export function identifyJitoTipRouterInstruction( @@ -60,6 +63,9 @@ export function identifyJitoTipRouterInstruction( if (containsBytes(data, getU8Encoder().encode(5), 0)) { return JitoTipRouterInstruction.RegisterMint; } + if (containsBytes(data, getU8Encoder().encode(6), 0)) { + return JitoTipRouterInstruction.InitializeTrackedMints; + } throw new Error( 'The provided instruction could not be identified as a jitoTipRouter instruction.' ); @@ -85,4 +91,7 @@ export type ParsedJitoTipRouterInstruction< } & ParsedAdminUpdateWeightTableInstruction) | ({ instructionType: JitoTipRouterInstruction.RegisterMint; - } & ParsedRegisterMintInstruction); + } & ParsedRegisterMintInstruction) + | ({ + instructionType: JitoTipRouterInstruction.InitializeTrackedMints; + } & ParsedInitializeTrackedMintsInstruction); diff --git a/clients/js/jito_tip_router/types/mintEntry.ts b/clients/js/jito_tip_router/types/mintEntry.ts index 0934db0..9f27177 100644 --- a/clients/js/jito_tip_router/types/mintEntry.ts +++ b/clients/js/jito_tip_router/types/mintEntry.ts @@ -10,31 +10,59 @@ import { combineCodec, getAddressDecoder, getAddressEncoder, + getArrayDecoder, + getArrayEncoder, getStructDecoder, getStructEncoder, + getU128Decoder, + getU128Encoder, getU64Decoder, getU64Encoder, + getU8Decoder, + getU8Encoder, type Address, type Codec, type Decoder, type Encoder, } from '@solana/web3.js'; -export type MintEntry = { mint: Address; vaultIndex: bigint }; +export type MintEntry = { + stMint: Address; + vaultIndex: bigint; + weight: bigint; + slotSet: bigint; + slotUpdated: bigint; + reserved: Array; +}; -export type MintEntryArgs = { mint: Address; vaultIndex: number | bigint }; +export type MintEntryArgs = { + stMint: Address; + vaultIndex: number | bigint; + weight: number | bigint; + slotSet: number | bigint; + slotUpdated: number | bigint; + reserved: Array; +}; export function getMintEntryEncoder(): Encoder { return getStructEncoder([ - ['mint', getAddressEncoder()], + ['stMint', getAddressEncoder()], ['vaultIndex', getU64Encoder()], + ['weight', getU128Encoder()], + ['slotSet', getU64Encoder()], + ['slotUpdated', getU64Encoder()], + ['reserved', getArrayEncoder(getU8Encoder(), { size: 128 })], ]); } export function getMintEntryDecoder(): Decoder { return getStructDecoder([ - ['mint', getAddressDecoder()], + ['stMint', getAddressDecoder()], ['vaultIndex', getU64Decoder()], + ['weight', getU128Decoder()], + ['slotSet', getU64Decoder()], + ['slotUpdated', getU64Decoder()], + ['reserved', getArrayDecoder(getU8Decoder(), { size: 128 })], ]); } diff --git a/clients/rust/jito_tip_router/src/generated/accounts/mod.rs b/clients/rust/jito_tip_router/src/generated/accounts/mod.rs index 6feb3df..e34e139 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/mod.rs @@ -5,6 +5,7 @@ //! pub(crate) mod r#ncn_config; +pub(crate) mod r#tracked_mints; pub(crate) mod r#weight_table; -pub use self::{r#ncn_config::*, r#weight_table::*}; +pub use self::{r#ncn_config::*, r#tracked_mints::*, r#weight_table::*}; 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 d1884b2..eaca31b 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, MintEntry}; +use crate::generated::types::Fees; #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -29,8 +29,6 @@ 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/accounts/tracked_mints.rs b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs new file mode 100644 index 0000000..332743c --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs @@ -0,0 +1,69 @@ +//! 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; + +use crate::generated::types::MintEntry; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TrackedMints { + pub discriminator: u64, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub ncn: Pubkey, + pub bump: u8, + pub reserved: [u8; 7], + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub st_mint_list: [MintEntry; 64], +} + +impl TrackedMints { + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + let mut data = data; + Self::deserialize(&mut data) + } +} + +impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for TrackedMints { + type Error = std::io::Error; + + fn try_from( + account_info: &solana_program::account_info::AccountInfo<'a>, + ) -> Result { + let mut data: &[u8] = &(*account_info.data).borrow(); + Self::deserialize(&mut data) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountDeserialize for TrackedMints { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + Ok(Self::deserialize(buf)?) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountSerialize for TrackedMints {} + +#[cfg(feature = "anchor")] +impl anchor_lang::Owner for TrackedMints { + fn owner() -> Pubkey { + crate::JITO_TIP_ROUTER_ID + } +} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::IdlBuild for TrackedMints {} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::Discriminator for TrackedMints { + const DISCRIMINATOR: [u8; 8] = [0; 8]; +} 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 f8d40e1..3a3792c 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 @@ -57,6 +57,12 @@ pub enum JitoTipRouterError { /// 8714 - NCN config vaults are at capacity #[error("NCN config vaults are at capacity")] ConfigMintListFull = 0x220A, + /// 8715 - Tracked mints are at capacity + #[error("Tracked mints are at capacity")] + TrackedMintListFull = 0x220B, + /// 8716 - Vault index already in use by a different mint + #[error("Vault index already in use by a different mint")] + VaultIndexAlreadyInUse = 0x220C, /// 8960 - Fee cap exceeded #[error("Fee cap exceeded")] FeeCapExceeded = 0x2300, diff --git a/clients/rust/jito_tip_router/src/generated/instructions/initialize_tracked_mints.rs b/clients/rust/jito_tip_router/src/generated/instructions/initialize_tracked_mints.rs new file mode 100644 index 0000000..b41c70a --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/instructions/initialize_tracked_mints.rs @@ -0,0 +1,437 @@ +//! 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 InitializeTrackedMints { + pub ncn_config: solana_program::pubkey::Pubkey, + + pub tracked_mints: solana_program::pubkey::Pubkey, + + pub ncn: solana_program::pubkey::Pubkey, + + pub payer: solana_program::pubkey::Pubkey, + + pub system_program: solana_program::pubkey::Pubkey, +} + +impl InitializeTrackedMints { + 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(5 + remaining_accounts.len()); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn_config, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.tracked_mints, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.ncn, false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + self.payer, true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let data = InitializeTrackedMintsInstructionData::new() + .try_to_vec() + .unwrap(); + + solana_program::instruction::Instruction { + program_id: crate::JITO_TIP_ROUTER_ID, + accounts, + data, + } + } +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct InitializeTrackedMintsInstructionData { + discriminator: u8, +} + +impl InitializeTrackedMintsInstructionData { + pub fn new() -> Self { + Self { discriminator: 6 } + } +} + +impl Default for InitializeTrackedMintsInstructionData { + fn default() -> Self { + Self::new() + } +} + +/// Instruction builder for `InitializeTrackedMints`. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` tracked_mints +/// 2. `[]` ncn +/// 3. `[writable, signer]` payer +/// 4. `[optional]` system_program (default to `11111111111111111111111111111111`) +#[derive(Clone, Debug, Default)] +pub struct InitializeTrackedMintsBuilder { + ncn_config: Option, + tracked_mints: Option, + ncn: Option, + payer: Option, + system_program: Option, + __remaining_accounts: Vec, +} + +impl InitializeTrackedMintsBuilder { + pub fn new() -> Self { + Self::default() + } + #[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 tracked_mints(&mut self, tracked_mints: solana_program::pubkey::Pubkey) -> &mut Self { + self.tracked_mints = Some(tracked_mints); + self + } + #[inline(always)] + pub fn ncn(&mut self, ncn: solana_program::pubkey::Pubkey) -> &mut Self { + self.ncn = Some(ncn); + self + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_program::pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + /// `[optional account, default to '11111111111111111111111111111111']` + #[inline(always)] + pub fn system_program(&mut self, system_program: solana_program::pubkey::Pubkey) -> &mut Self { + self.system_program = Some(system_program); + 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 = InitializeTrackedMints { + ncn_config: self.ncn_config.expect("ncn_config is not set"), + tracked_mints: self.tracked_mints.expect("tracked_mints is not set"), + ncn: self.ncn.expect("ncn is not set"), + payer: self.payer.expect("payer is not set"), + system_program: self + .system_program + .unwrap_or(solana_program::pubkey!("11111111111111111111111111111111")), + }; + + accounts.instruction_with_remaining_accounts(&self.__remaining_accounts) + } +} + +/// `initialize_tracked_mints` CPI accounts. +pub struct InitializeTrackedMintsCpiAccounts<'a, 'b> { + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +/// `initialize_tracked_mints` CPI instruction. +pub struct InitializeTrackedMintsCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + + pub tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, + + pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + + pub payer: &'b solana_program::account_info::AccountInfo<'a>, + + pub system_program: &'b solana_program::account_info::AccountInfo<'a>, +} + +impl<'a, 'b> InitializeTrackedMintsCpi<'a, 'b> { + pub fn new( + program: &'b solana_program::account_info::AccountInfo<'a>, + accounts: InitializeTrackedMintsCpiAccounts<'a, 'b>, + ) -> Self { + Self { + __program: program, + ncn_config: accounts.ncn_config, + tracked_mints: accounts.tracked_mints, + ncn: accounts.ncn, + payer: accounts.payer, + system_program: accounts.system_program, + } + } + #[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_readonly( + *self.ncn_config.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.tracked_mints.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.ncn.key, + false, + )); + accounts.push(solana_program::instruction::AccountMeta::new( + *self.payer.key, + true, + )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.system_program.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 = InitializeTrackedMintsInstructionData::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(5 + 1 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.ncn_config.clone()); + account_infos.push(self.tracked_mints.clone()); + account_infos.push(self.ncn.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.system_program.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 `InitializeTrackedMints` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[]` ncn_config +/// 1. `[writable]` tracked_mints +/// 2. `[]` ncn +/// 3. `[writable, signer]` payer +/// 4. `[]` system_program +#[derive(Clone, Debug)] +pub struct InitializeTrackedMintsCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> InitializeTrackedMintsCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(InitializeTrackedMintsCpiBuilderInstruction { + __program: program, + ncn_config: None, + tracked_mints: None, + ncn: None, + payer: None, + system_program: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[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 tracked_mints( + &mut self, + tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.tracked_mints = Some(tracked_mints); + 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 payer(&mut self, payer: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + 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 = InitializeTrackedMintsCpi { + __program: self.instruction.__program, + + ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + + tracked_mints: self + .instruction + .tracked_mints + .expect("tracked_mints is not set"), + + ncn: self.instruction.ncn.expect("ncn is not set"), + + payer: self.instruction.payer.expect("payer is not set"), + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct InitializeTrackedMintsCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_program::account_info::AccountInfo<'a>, + ncn_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, + tracked_mints: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, + system_program: 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/instructions/mod.rs b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs index 812f5ea..9608d45 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/mod.rs @@ -6,12 +6,13 @@ pub(crate) mod r#admin_update_weight_table; pub(crate) mod r#initialize_n_c_n_config; +pub(crate) mod r#initialize_tracked_mints; 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#register_mint::*, r#set_config_fees::*, r#set_new_admin::*, + r#admin_update_weight_table::*, r#initialize_n_c_n_config::*, r#initialize_tracked_mints::*, + r#initialize_weight_table::*, 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 index b3847c7..667a722 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs @@ -10,7 +10,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub struct RegisterMint { pub restaking_config: solana_program::pubkey::Pubkey, - pub ncn_config: solana_program::pubkey::Pubkey, + pub tracked_mints: solana_program::pubkey::Pubkey, pub ncn: solana_program::pubkey::Pubkey, @@ -40,7 +40,7 @@ impl RegisterMint { false, )); accounts.push(solana_program::instruction::AccountMeta::new( - self.ncn_config, + self.tracked_mints, false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( @@ -98,7 +98,7 @@ impl Default for RegisterMintInstructionData { /// ### Accounts: /// /// 0. `[]` restaking_config -/// 1. `[writable]` ncn_config +/// 1. `[writable]` tracked_mints /// 2. `[]` ncn /// 3. `[]` vault /// 4. `[]` vault_ncn_ticket @@ -108,7 +108,7 @@ impl Default for RegisterMintInstructionData { #[derive(Clone, Debug, Default)] pub struct RegisterMintBuilder { restaking_config: Option, - ncn_config: Option, + tracked_mints: Option, ncn: Option, vault: Option, vault_ncn_ticket: Option, @@ -131,8 +131,8 @@ impl RegisterMintBuilder { self } #[inline(always)] - pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { - self.ncn_config = Some(ncn_config); + pub fn tracked_mints(&mut self, tracked_mints: solana_program::pubkey::Pubkey) -> &mut Self { + self.tracked_mints = Some(tracked_mints); self } #[inline(always)] @@ -199,7 +199,7 @@ impl RegisterMintBuilder { 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"), + tracked_mints: self.tracked_mints.expect("tracked_mints 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"), @@ -218,7 +218,7 @@ impl RegisterMintBuilder { 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 tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, pub ncn: &'b solana_program::account_info::AccountInfo<'a>, @@ -240,7 +240,7 @@ pub struct RegisterMintCpi<'a, 'b> { pub restaking_config: &'b solana_program::account_info::AccountInfo<'a>, - pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + pub tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, pub ncn: &'b solana_program::account_info::AccountInfo<'a>, @@ -263,7 +263,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { Self { __program: program, restaking_config: accounts.restaking_config, - ncn_config: accounts.ncn_config, + tracked_mints: accounts.tracked_mints, ncn: accounts.ncn, vault: accounts.vault, vault_ncn_ticket: accounts.vault_ncn_ticket, @@ -311,7 +311,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { false, )); accounts.push(solana_program::instruction::AccountMeta::new( - *self.ncn_config.key, + *self.tracked_mints.key, false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( @@ -355,7 +355,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { 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.tracked_mints.clone()); account_infos.push(self.ncn.clone()); account_infos.push(self.vault.clone()); account_infos.push(self.vault_ncn_ticket.clone()); @@ -379,7 +379,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { /// ### Accounts: /// /// 0. `[]` restaking_config -/// 1. `[writable]` ncn_config +/// 1. `[writable]` tracked_mints /// 2. `[]` ncn /// 3. `[]` vault /// 4. `[]` vault_ncn_ticket @@ -396,7 +396,7 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { let instruction = Box::new(RegisterMintCpiBuilderInstruction { __program: program, restaking_config: None, - ncn_config: None, + tracked_mints: None, ncn: None, vault: None, vault_ncn_ticket: None, @@ -416,11 +416,11 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { self } #[inline(always)] - pub fn ncn_config( + pub fn tracked_mints( &mut self, - ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, ) -> &mut Self { - self.instruction.ncn_config = Some(ncn_config); + self.instruction.tracked_mints = Some(tracked_mints); self } #[inline(always)] @@ -514,7 +514,10 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { .restaking_config .expect("restaking_config is not set"), - ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + tracked_mints: self + .instruction + .tracked_mints + .expect("tracked_mints is not set"), ncn: self.instruction.ncn.expect("ncn is not set"), @@ -551,7 +554,7 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { 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>>, + tracked_mints: 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>>, 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 index 4b78910..2667883 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs @@ -14,6 +14,11 @@ pub struct MintEntry { feature = "serde", serde(with = "serde_with::As::") )] - pub mint: Pubkey, + pub st_mint: Pubkey, pub vault_index: u64, + pub weight: u128, + pub slot_set: u64, + pub slot_updated: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 128], } diff --git a/core/src/discriminators.rs b/core/src/discriminators.rs index 18f0d31..2196c1a 100644 --- a/core/src/discriminators.rs +++ b/core/src/discriminators.rs @@ -2,4 +2,5 @@ pub enum Discriminators { Config = 1, WeightTable = 2, + TrackedMints = 3, } diff --git a/core/src/error.rs b/core/src/error.rs index bf430c1..39958c2 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -36,6 +36,10 @@ pub enum TipRouterError { ConfigMintsNotUpdated, #[error("NCN config vaults are at capacity")] ConfigMintListFull, + #[error("Tracked mints are at capacity")] + TrackedMintListFull, + #[error("Vault index already in use by a different mint")] + VaultIndexAlreadyInUse, #[error("Fee cap exceeded")] FeeCapExceeded = 0x2300, #[error("Incorrect NCN Admin")] diff --git a/core/src/instruction.rs b/core/src/instruction.rs index a29ba96..76d2f65 100644 --- a/core/src/instruction.rs +++ b/core/src/instruction.rs @@ -76,7 +76,7 @@ pub enum WeightTableInstruction { /// Registers a mint with the NCN config #[account(0, name = "restaking_config")] - #[account(1, writable, name = "ncn_config")] + #[account(1, writable, name = "tracked_mints")] #[account(2, name = "ncn")] #[account(3, name = "vault")] #[account(4, name = "vault_ncn_ticket")] @@ -84,4 +84,12 @@ pub enum WeightTableInstruction { #[account(6, name = "restaking_program_id")] #[account(7, name = "vault_program_id")] RegisterMint, + + /// Initializes the tracked mints account for an NCN + #[account(0, name = "ncn_config")] + #[account(1, writable, name = "tracked_mints")] + #[account(2, name = "ncn")] + #[account(3, writable, signer, name = "payer")] + #[account(4, name = "system_program")] + InitializeTrackedMints, } diff --git a/core/src/lib.rs b/core/src/lib.rs index a75de93..817bbad 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,6 +3,7 @@ pub mod error; pub mod fees; pub mod instruction; pub mod ncn_config; +pub mod tracked_mints; pub mod weight_entry; pub mod weight_table; diff --git a/core/src/ncn_config.rs b/core/src/ncn_config.rs index 9a09136..63bd08d 100644 --- a/core/src/ncn_config.rs +++ b/core/src/ncn_config.rs @@ -1,38 +1,10 @@ -use std::collections::HashSet; - use bytemuck::{Pod, Zeroable}; -use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator}; +use jito_bytemuck::{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 { @@ -45,9 +17,6 @@ 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 @@ -59,13 +28,17 @@ impl Discriminator for NcnConfig { } impl NcnConfig { - pub fn new(ncn: Pubkey, tie_breaker_admin: Pubkey, fee_admin: Pubkey, fees: Fees) -> Self { + pub const 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], } @@ -76,7 +49,7 @@ impl NcnConfig { } pub fn find_program_address(program_id: &Pubkey, ncn: &Pubkey) -> (Pubkey, u8, Vec>) { - let seeds = vec![b"config".to_vec(), ncn.to_bytes().to_vec()]; + let seeds = Self::seeds(ncn); let (address, bump) = Pubkey::find_program_address( &seeds.iter().map(|s| s.as_slice()).collect::>(), program_id, @@ -84,45 +57,6 @@ 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/core/src/tracked_mints.rs b/core/src/tracked_mints.rs new file mode 100644 index 0000000..c9f3bb2 --- /dev/null +++ b/core/src/tracked_mints.rs @@ -0,0 +1,224 @@ +use std::collections::HashSet; + +use bytemuck::{Pod, Zeroable}; +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, error::TipRouterError}; + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)] +#[repr(C)] +pub struct MintEntry { + st_mint: Pubkey, + vault_index: PodU64, +} + +impl MintEntry { + pub fn new(mint: Pubkey, vault_index: u64) -> Self { + Self { + st_mint: 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(), u64::MAX) + } +} + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] +#[repr(C)] +pub struct TrackedMints { + pub ncn: Pubkey, + pub bump: u8, + pub reserved: [u8; 7], + pub st_mint_list: [MintEntry; 64], +} + +impl Discriminator for TrackedMints { + const DISCRIMINATOR: u8 = Discriminators::TrackedMints as u8; +} + +impl TrackedMints { + pub fn new(ncn: Pubkey, bump: u8) -> Self { + Self { + ncn, + bump, + reserved: [0; 7], + st_mint_list: [MintEntry::default(); 64], + } + } + + pub fn seeds(ncn: &Pubkey) -> Vec> { + Vec::from_iter( + [b"tracked_mints".to_vec(), ncn.to_bytes().to_vec()] + .iter() + .cloned(), + ) + } + + pub fn find_program_address(program_id: &Pubkey, ncn: &Pubkey) -> (Pubkey, u8, Vec>) { + let seeds = Self::seeds(ncn); + let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect(); + let (address, bump) = Pubkey::find_program_address(&seeds_iter, program_id); + (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 + .st_mint_list + .iter() + .any(|m| m.st_mint == mint && m.vault_index() == vault_index) + { + return Ok(()); + } + + // Check if vault_index is already in use by a different mint + if self + .st_mint_list + .iter() + .any(|m| m.vault_index() == vault_index) + { + return Err(TipRouterError::VaultIndexAlreadyInUse.into()); + } + + // Insert at the first empty slot + let mint_entry = self + .st_mint_list + .iter_mut() + .find(|m| m.st_mint == MintEntry::default().st_mint) + .ok_or(TipRouterError::TrackedMintListFull)?; + + *mint_entry = MintEntry::new(mint, vault_index); + Ok(()) + } + + pub fn mint_count(&self) -> usize { + self.st_mint_list + .iter() + .filter(|m| m.st_mint != Pubkey::default()) + .count() + } + + pub fn get_unique_mints(&self) -> Vec { + let mut unique_mints: HashSet = HashSet::new(); + self.st_mint_list + .iter() + .filter(|m| m.st_mint != Pubkey::default()) + .for_each(|m| { + unique_mints.insert(m.st_mint); + }); + unique_mints.into_iter().collect() + } + + pub fn load( + program_id: &Pubkey, + ncn: &Pubkey, + account: &AccountInfo, + expect_writable: bool, + ) -> Result<(), ProgramError> { + if account.owner.ne(program_id) { + msg!("Tracked Mints account has an invalid owner"); + return Err(ProgramError::InvalidAccountOwner); + } + + if account.data_is_empty() { + msg!("Tracked Mints account data is empty"); + return Err(ProgramError::InvalidAccountData); + } + + if expect_writable && !account.is_writable { + msg!("Tracked Mints account is not writable"); + return Err(ProgramError::InvalidAccountData); + } + + if account.data.borrow()[0].ne(&Self::DISCRIMINATOR) { + msg!("Tracked Mints account discriminator is invalid"); + return Err(ProgramError::InvalidAccountData); + } + + if account + .key + .ne(&Self::find_program_address(program_id, ncn).0) + { + msg!("Tracked Mints account is not at the correct PDA"); + return Err(ProgramError::InvalidAccountData); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_mint() { + let mut tracked_mints = TrackedMints::new(Pubkey::default(), 0); + let mint = Pubkey::new_unique(); + + assert_eq!(tracked_mints.mint_count(), 0); + tracked_mints.add_mint(mint, 0).unwrap(); + assert_eq!(tracked_mints.mint_count(), 1); + + // Adding same mint with different vault index should succeed + tracked_mints.add_mint(mint, 1).unwrap(); + assert_eq!(tracked_mints.mint_count(), 2); + + // Adding same mint with same vault index should succeed but do nothing + tracked_mints.add_mint(mint, 1).unwrap(); + assert_eq!(tracked_mints.mint_count(), 2); + + // Adding different mint with same vault index should fail + let mint2 = Pubkey::new_unique(); + assert!(tracked_mints.add_mint(mint2, 1).is_err()); + + // Adding to a full list should fail + for i in tracked_mints.mint_count()..tracked_mints.st_mint_list.len() { + tracked_mints + .add_mint(Pubkey::new_unique(), i as u64) + .unwrap(); + } + assert!(tracked_mints.add_mint(Pubkey::new_unique(), 0).is_err()); + } + + #[test] + fn test_mint_count() { + let mut tracked_mints = TrackedMints::new(Pubkey::default(), 0); + assert_eq!(tracked_mints.mint_count(), 0); + + for i in 0..3 { + tracked_mints.add_mint(Pubkey::new_unique(), i).unwrap(); + } + assert_eq!(tracked_mints.mint_count(), 3); + } + + #[test] + fn test_get_unique_mints() { + let mut tracked_mints = TrackedMints::new(Pubkey::default(), 0); + + let mint1 = Pubkey::new_unique(); + let mint2 = Pubkey::new_unique(); + tracked_mints.add_mint(mint1, 0).unwrap(); + tracked_mints.add_mint(mint2, 1).unwrap(); + tracked_mints.add_mint(mint1, 2).unwrap(); + + let unique_mints = tracked_mints.get_unique_mints(); + assert_eq!(unique_mints.len(), 2); + assert!(unique_mints.contains(&mint1)); + assert!(unique_mints.contains(&mint2)); + + // Default pubkeys should not be included + let empty_tracked_mints = TrackedMints::new(Pubkey::default(), 0); + assert_eq!(empty_tracked_mints.get_unique_mints().len(), 0); + } +} diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index ebca9e6..d2d6896 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -272,7 +272,7 @@ "isSigner": false }, { - "name": "ncnConfig", + "name": "trackedMints", "isMut": true, "isSigner": false }, @@ -312,6 +312,41 @@ "type": "u8", "value": 5 } + }, + { + "name": "InitializeTrackedMints", + "accounts": [ + { + "name": "ncnConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "trackedMints", + "isMut": true, + "isSigner": false + }, + { + "name": "ncn", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [], + "discriminant": { + "type": "u8", + "value": 6 + } } ], "accounts": [ @@ -339,15 +374,29 @@ } }, { - "name": "mintList", + "name": "bump", + "type": "u8" + }, + { + "name": "reserved", "type": { "array": [ - { - "defined": "MintEntry" - }, - 64 + "u8", + 127 ] } + } + ] + } + }, + { + "name": "TrackedMints", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ncn", + "type": "publicKey" }, { "name": "bump", @@ -358,7 +407,18 @@ "type": { "array": [ "u8", - 127 + 7 + ] + } + }, + { + "name": "stMintList", + "type": { + "array": [ + { + "defined": "MintEntry" + }, + 64 ] } } @@ -477,7 +537,7 @@ "kind": "struct", "fields": [ { - "name": "mint", + "name": "stMint", "type": "publicKey" }, { @@ -485,6 +545,33 @@ "type": { "defined": "PodU64" } + }, + { + "name": "weight", + "type": { + "defined": "PodU128" + } + }, + { + "name": "slotSet", + "type": { + "defined": "PodU64" + } + }, + { + "name": "slotUpdated", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 128 + ] + } } ] } @@ -624,6 +711,16 @@ "name": "ConfigMintListFull", "msg": "NCN config vaults are at capacity" }, + { + "code": 8715, + "name": "TrackedMintListFull", + "msg": "Tracked mints are at capacity" + }, + { + "code": 8716, + "name": "VaultIndexAlreadyInUse", + "msg": "Vault index already in use by a different mint" + }, { "code": 8960, "name": "FeeCapExceeded", diff --git a/program/src/initialize_tracked_mints.rs b/program/src/initialize_tracked_mints.rs new file mode 100644 index 0000000..84bcd3b --- /dev/null +++ b/program/src/initialize_tracked_mints.rs @@ -0,0 +1,53 @@ +use jito_bytemuck::{AccountDeserialize, Discriminator}; +use jito_jsm_core::{ + create_account, + loader::{load_system_account, load_system_program}, +}; +use jito_tip_router_core::{ncn_config::NcnConfig, tracked_mints::TrackedMints}; +use solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, rent::Rent, sysvar::Sysvar, +}; + +pub fn process_initialize_tracked_mints( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let [ncn_config, tracked_mints, ncn_account, payer, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Verify accounts + load_system_account(tracked_mints, true)?; + load_system_program(system_program)?; + + NcnConfig::load(program_id, ncn_account.key, ncn_config, false)?; + + let (tracked_mints_pda, tracked_mints_bump, mut tracked_mints_seeds) = + TrackedMints::find_program_address(program_id, ncn_account.key); + tracked_mints_seeds.push(vec![tracked_mints_bump]); + + if tracked_mints_pda != *tracked_mints.key { + return Err(ProgramError::InvalidSeeds); + } + + create_account( + payer, + tracked_mints, + system_program, + program_id, + &Rent::get()?, + 8_u64 + .checked_add(std::mem::size_of::() as u64) + .unwrap(), + &tracked_mints_seeds, + )?; + + let mut tracked_mints_data = tracked_mints.try_borrow_mut_data()?; + tracked_mints_data[0] = TrackedMints::DISCRIMINATOR; + let tracked_mints_account = + TrackedMints::try_from_slice_unchecked_mut(&mut tracked_mints_data)?; + *tracked_mints_account = TrackedMints::new(*ncn_account.key, tracked_mints_bump); + + Ok(()) +} diff --git a/program/src/initialize_weight_table.rs b/program/src/initialize_weight_table.rs index d3a28fb..f893b63 100644 --- a/program/src/initialize_weight_table.rs +++ b/program/src/initialize_weight_table.rs @@ -7,7 +7,7 @@ use jito_jsm_core::{ }; use jito_restaking_core::{config::Config, ncn::Ncn}; use jito_tip_router_core::{ - error::TipRouterError, ncn_config::NcnConfig, weight_table::WeightTable, + error::TipRouterError, tracked_mints::TrackedMints, weight_table::WeightTable, }; use solana_program::{ account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, msg, @@ -21,13 +21,13 @@ pub fn process_initialize_weight_table( accounts: &[AccountInfo], first_slot_of_ncn_epoch: Option, ) -> ProgramResult { - let [restaking_config, ncn_config, ncn, weight_table, payer, restaking_program_id, system_program] = + let [restaking_config, tracked_mints, ncn, weight_table, payer, restaking_program_id, system_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; - NcnConfig::load(program_id, ncn.key, ncn_config, false)?; + TrackedMints::load(program_id, ncn.key, tracked_mints, false)?; Config::load(restaking_program_id.key, restaking_config, false)?; Ncn::load(restaking_program_id.key, ncn, false)?; @@ -43,8 +43,8 @@ pub fn process_initialize_weight_table( 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)?; + let tracked_mints_data: std::cell::Ref<'_, &mut [u8]> = tracked_mints.data.borrow(); + let tracked_mints = TrackedMints::try_from_slice_unchecked(&tracked_mints_data)?; load_system_account(weight_table, true)?; load_system_program(system_program)?; @@ -79,7 +79,7 @@ pub fn process_initialize_weight_table( return Err(ProgramError::InvalidAccountData); } - if vault_count as usize != ncn_config.mint_count() { + if vault_count as usize != tracked_mints.mint_count() { msg!("Vault count does not match supported mint count"); return Err(ProgramError::InvalidAccountData); } @@ -106,7 +106,7 @@ pub fn process_initialize_weight_table( *weight_table_account = WeightTable::new(*ncn.key, ncn_epoch, current_slot, weight_table_bump); - weight_table_account.initalize_weight_table(&ncn_config.get_unique_mints())?; + weight_table_account.initalize_weight_table(&tracked_mints.get_unique_mints())?; Ok(()) } diff --git a/program/src/lib.rs b/program/src/lib.rs index 34a6155..68c4a4d 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,5 +1,6 @@ mod admin_update_weight_table; mod initialize_ncn_config; +mod initialize_tracked_mints; mod initialize_weight_table; mod register_mint; mod set_config_fees; @@ -19,6 +20,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_tracked_mints::process_initialize_tracked_mints, initialize_weight_table::process_initialize_weight_table, register_mint::process_register_mint, set_config_fees::process_set_config_fees, }; @@ -106,5 +108,9 @@ pub fn process_instruction( msg!("Instruction: RegisterMint"); process_register_mint(program_id, accounts) } + WeightTableInstruction::InitializeTrackedMints => { + msg!("Instruction: InitializeTrackedMints"); + process_initialize_tracked_mints(program_id, accounts) + } } } diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs index ef6c2dd..1818f5c 100644 --- a/program/src/register_mint.rs +++ b/program/src/register_mint.rs @@ -1,6 +1,6 @@ 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_tip_router_core::tracked_mints::TrackedMints; use jito_vault_core::{vault::Vault, vault_ncn_ticket::VaultNcnTicket}; use solana_program::{ account_info::AccountInfo, @@ -12,13 +12,13 @@ use solana_program::{ }; 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] = + let [restaking_config, tracked_mints, 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)?; + TrackedMints::load(program_id, ncn.key, tracked_mints, 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)?; @@ -61,9 +61,9 @@ pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> P 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())?; + let mut tracked_mints_data = tracked_mints.try_borrow_mut_data()?; + let tracked_mints = TrackedMints::try_from_slice_unchecked_mut(&mut tracked_mints_data)?; + tracked_mints.add_mint(vault.supported_mint, vault.vault_index())?; Ok(()) } From 4e75bca43aa36aa6f0ef005a6079195b29282541 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 12 Nov 2024 17:40:13 -0500 Subject: [PATCH 3/9] working with 2... --- .../jito_tip_router/accounts/trackedMints.ts | 4 +-- clients/js/jito_tip_router/types/mintEntry.ts | 32 ++----------------- .../src/generated/accounts/tracked_mints.rs | 3 +- .../src/generated/types/mint_entry.rs | 5 --- core/src/tracked_mints.rs | 4 +-- idl/jito_tip_router.json | 29 +---------------- 6 files changed, 8 insertions(+), 69 deletions(-) diff --git a/clients/js/jito_tip_router/accounts/trackedMints.ts b/clients/js/jito_tip_router/accounts/trackedMints.ts index a275f53..b6d9052 100644 --- a/clients/js/jito_tip_router/accounts/trackedMints.ts +++ b/clients/js/jito_tip_router/accounts/trackedMints.ts @@ -63,7 +63,7 @@ export function getTrackedMintsEncoder(): Encoder { ['ncn', getAddressEncoder()], ['bump', getU8Encoder()], ['reserved', getArrayEncoder(getU8Encoder(), { size: 7 })], - ['stMintList', getArrayEncoder(getMintEntryEncoder(), { size: 64 })], + ['stMintList', getArrayEncoder(getMintEntryEncoder(), { size: 2 })], ]); } @@ -73,7 +73,7 @@ export function getTrackedMintsDecoder(): Decoder { ['ncn', getAddressDecoder()], ['bump', getU8Decoder()], ['reserved', getArrayDecoder(getU8Decoder(), { size: 7 })], - ['stMintList', getArrayDecoder(getMintEntryDecoder(), { size: 64 })], + ['stMintList', getArrayDecoder(getMintEntryDecoder(), { size: 2 })], ]); } diff --git a/clients/js/jito_tip_router/types/mintEntry.ts b/clients/js/jito_tip_router/types/mintEntry.ts index 9f27177..afe90fd 100644 --- a/clients/js/jito_tip_router/types/mintEntry.ts +++ b/clients/js/jito_tip_router/types/mintEntry.ts @@ -10,48 +10,24 @@ import { combineCodec, getAddressDecoder, getAddressEncoder, - getArrayDecoder, - getArrayEncoder, getStructDecoder, getStructEncoder, - getU128Decoder, - getU128Encoder, getU64Decoder, getU64Encoder, - getU8Decoder, - getU8Encoder, type Address, type Codec, type Decoder, type Encoder, } from '@solana/web3.js'; -export type MintEntry = { - stMint: Address; - vaultIndex: bigint; - weight: bigint; - slotSet: bigint; - slotUpdated: bigint; - reserved: Array; -}; +export type MintEntry = { stMint: Address; vaultIndex: bigint }; -export type MintEntryArgs = { - stMint: Address; - vaultIndex: number | bigint; - weight: number | bigint; - slotSet: number | bigint; - slotUpdated: number | bigint; - reserved: Array; -}; +export type MintEntryArgs = { stMint: Address; vaultIndex: number | bigint }; export function getMintEntryEncoder(): Encoder { return getStructEncoder([ ['stMint', getAddressEncoder()], ['vaultIndex', getU64Encoder()], - ['weight', getU128Encoder()], - ['slotSet', getU64Encoder()], - ['slotUpdated', getU64Encoder()], - ['reserved', getArrayEncoder(getU8Encoder(), { size: 128 })], ]); } @@ -59,10 +35,6 @@ export function getMintEntryDecoder(): Decoder { return getStructDecoder([ ['stMint', getAddressDecoder()], ['vaultIndex', getU64Decoder()], - ['weight', getU128Decoder()], - ['slotSet', getU64Decoder()], - ['slotUpdated', getU64Decoder()], - ['reserved', getArrayDecoder(getU8Decoder(), { size: 128 })], ]); } diff --git a/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs index 332743c..a11320c 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs @@ -20,8 +20,7 @@ pub struct TrackedMints { pub ncn: Pubkey, pub bump: u8, pub reserved: [u8; 7], - #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] - pub st_mint_list: [MintEntry; 64], + pub st_mint_list: [MintEntry; 2], } impl TrackedMints { 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 index 2667883..a68634a 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs @@ -16,9 +16,4 @@ pub struct MintEntry { )] pub st_mint: Pubkey, pub vault_index: u64, - pub weight: u128, - pub slot_set: u64, - pub slot_updated: u64, - #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] - pub reserved: [u8; 128], } diff --git a/core/src/tracked_mints.rs b/core/src/tracked_mints.rs index c9f3bb2..833159e 100644 --- a/core/src/tracked_mints.rs +++ b/core/src/tracked_mints.rs @@ -39,7 +39,7 @@ pub struct TrackedMints { pub ncn: Pubkey, pub bump: u8, pub reserved: [u8; 7], - pub st_mint_list: [MintEntry; 64], + pub st_mint_list: [MintEntry; 2], } impl Discriminator for TrackedMints { @@ -52,7 +52,7 @@ impl TrackedMints { ncn, bump, reserved: [0; 7], - st_mint_list: [MintEntry::default(); 64], + st_mint_list: [MintEntry::default(); 2], } } diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index d2d6896..1a2f1b1 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -418,7 +418,7 @@ { "defined": "MintEntry" }, - 64 + 2 ] } } @@ -545,33 +545,6 @@ "type": { "defined": "PodU64" } - }, - { - "name": "weight", - "type": { - "defined": "PodU128" - } - }, - { - "name": "slotSet", - "type": { - "defined": "PodU64" - } - }, - { - "name": "slotUpdated", - "type": { - "defined": "PodU64" - } - }, - { - "name": "reserved", - "type": { - "array": [ - "u8", - 128 - ] - } } ] } From 853ec390f4fb226c4b4bb99f053366eea03a7521 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Tue, 12 Nov 2024 22:44:18 -0500 Subject: [PATCH 4/9] integration tests' --- .../jito_tip_router/accounts/trackedMints.ts | 4 +- .../src/generated/accounts/tracked_mints.rs | 2 +- core/src/tracked_mints.rs | 4 +- idl/jito_tip_router.json | 2 +- .../tests/fixtures/tip_router_client.rs | 112 +++++++++++++++++- .../tip_router/initialize_tracked_mints.rs | 108 +++++++++++++++++ integration_tests/tests/tip_router/mod.rs | 2 + .../tests/tip_router/register_mint.rs | 107 +++++++++++++++++ 8 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 integration_tests/tests/tip_router/initialize_tracked_mints.rs create mode 100644 integration_tests/tests/tip_router/register_mint.rs diff --git a/clients/js/jito_tip_router/accounts/trackedMints.ts b/clients/js/jito_tip_router/accounts/trackedMints.ts index b6d9052..75d2ad5 100644 --- a/clients/js/jito_tip_router/accounts/trackedMints.ts +++ b/clients/js/jito_tip_router/accounts/trackedMints.ts @@ -63,7 +63,7 @@ export function getTrackedMintsEncoder(): Encoder { ['ncn', getAddressEncoder()], ['bump', getU8Encoder()], ['reserved', getArrayEncoder(getU8Encoder(), { size: 7 })], - ['stMintList', getArrayEncoder(getMintEntryEncoder(), { size: 2 })], + ['stMintList', getArrayEncoder(getMintEntryEncoder(), { size: 16 })], ]); } @@ -73,7 +73,7 @@ export function getTrackedMintsDecoder(): Decoder { ['ncn', getAddressDecoder()], ['bump', getU8Decoder()], ['reserved', getArrayDecoder(getU8Decoder(), { size: 7 })], - ['stMintList', getArrayDecoder(getMintEntryDecoder(), { size: 2 })], + ['stMintList', getArrayDecoder(getMintEntryDecoder(), { size: 16 })], ]); } diff --git a/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs index a11320c..da3fec6 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/tracked_mints.rs @@ -20,7 +20,7 @@ pub struct TrackedMints { pub ncn: Pubkey, pub bump: u8, pub reserved: [u8; 7], - pub st_mint_list: [MintEntry; 2], + pub st_mint_list: [MintEntry; 16], } impl TrackedMints { diff --git a/core/src/tracked_mints.rs b/core/src/tracked_mints.rs index 833159e..e1ea35c 100644 --- a/core/src/tracked_mints.rs +++ b/core/src/tracked_mints.rs @@ -39,7 +39,7 @@ pub struct TrackedMints { pub ncn: Pubkey, pub bump: u8, pub reserved: [u8; 7], - pub st_mint_list: [MintEntry; 2], + pub st_mint_list: [MintEntry; 16], // TODO extend to 64; figure out serde issue } impl Discriminator for TrackedMints { @@ -52,7 +52,7 @@ impl TrackedMints { ncn, bump, reserved: [0; 7], - st_mint_list: [MintEntry::default(); 2], + st_mint_list: [MintEntry::default(); 16], } } diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 1a2f1b1..0a0d60c 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -418,7 +418,7 @@ { "defined": "MintEntry" }, - 2 + 16 ] } } diff --git a/integration_tests/tests/fixtures/tip_router_client.rs b/integration_tests/tests/fixtures/tip_router_client.rs index ce14f56..31531ec 100644 --- a/integration_tests/tests/fixtures/tip_router_client.rs +++ b/integration_tests/tests/fixtures/tip_router_client.rs @@ -2,13 +2,15 @@ use jito_bytemuck::AccountDeserialize; use jito_restaking_core::config::Config; use jito_tip_router_client::{ instructions::{ - AdminUpdateWeightTableBuilder, InitializeNCNConfigBuilder, InitializeWeightTableBuilder, - SetConfigFeesBuilder, SetNewAdminBuilder, + AdminUpdateWeightTableBuilder, InitializeNCNConfigBuilder, InitializeTrackedMintsBuilder, + InitializeWeightTableBuilder, RegisterMintBuilder, SetConfigFeesBuilder, + SetNewAdminBuilder, }, types::ConfigAdminRole, }; use jito_tip_router_core::{ - error::TipRouterError, ncn_config::NcnConfig, weight_table::WeightTable, + error::TipRouterError, ncn_config::NcnConfig, tracked_mints::TrackedMints, + weight_table::WeightTable, }; use solana_program::{ instruction::InstructionError, native_token::sol_to_lamports, pubkey::Pubkey, @@ -64,6 +66,14 @@ impl TipRouterClient { Ok(()) } + pub async fn setup_tip_router(&mut self, ncn_root: &NcnRoot) -> TestResult<()> { + self.do_initialize_config(ncn_root.ncn_pubkey, &ncn_root.ncn_admin) + .await?; + self.do_initialize_tracked_mints(ncn_root.ncn_pubkey) + .await?; + Ok(()) + } + pub async fn get_restaking_config(&mut self) -> TestResult { let restaking_config = Config::find_program_address(&jito_restaking_program::id()).0; let restaking_config_data = self @@ -81,6 +91,17 @@ impl TipRouterClient { Ok(*NcnConfig::try_from_slice_unchecked(config.data.as_slice()).unwrap()) } + pub async fn get_tracked_mints(&mut self, ncn_pubkey: Pubkey) -> TestResult { + let tracked_mints_pda = + TrackedMints::find_program_address(&jito_tip_router_program::id(), &ncn_pubkey).0; + let tracked_mints = self + .banks_client + .get_account(tracked_mints_pda) + .await? + .unwrap(); + Ok(*TrackedMints::try_from_slice_unchecked(tracked_mints.data.as_slice()).unwrap()) + } + pub async fn do_initialize_config( &mut self, ncn: Pubkey, @@ -307,6 +328,91 @@ impl TipRouterClient { )) .await } + + pub async fn do_initialize_tracked_mints(&mut self, ncn: Pubkey) -> TestResult<()> { + let ncn_config = NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn).0; + let tracked_mints = + TrackedMints::find_program_address(&jito_tip_router_program::id(), &ncn).0; + + self.initialize_tracked_mints(&ncn_config, &tracked_mints, &ncn) + .await + } + + pub async fn initialize_tracked_mints( + &mut self, + ncn_config: &Pubkey, + tracked_mints: &Pubkey, + ncn: &Pubkey, + ) -> TestResult<()> { + let ix = InitializeTrackedMintsBuilder::new() + .ncn_config(*ncn_config) + .tracked_mints(*tracked_mints) + .ncn(*ncn) + .payer(self.payer.pubkey()) + .system_program(system_program::id()) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn do_register_mint( + &mut self, + ncn: Pubkey, + vault: Pubkey, + vault_ncn_ticket: Pubkey, + ncn_vault_ticket: Pubkey, + ) -> TestResult<()> { + let restaking_config = Config::find_program_address(&jito_restaking_program::id()).0; + let tracked_mints = + TrackedMints::find_program_address(&jito_tip_router_program::id(), &ncn).0; + + self.register_mint( + restaking_config, + tracked_mints, + ncn, + vault, + vault_ncn_ticket, + ncn_vault_ticket, + ) + .await + } + + pub async fn register_mint( + &mut self, + restaking_config: Pubkey, + tracked_mints: Pubkey, + ncn: Pubkey, + vault: Pubkey, + vault_ncn_ticket: Pubkey, + ncn_vault_ticket: Pubkey, + ) -> TestResult<()> { + let ix = RegisterMintBuilder::new() + .restaking_config(restaking_config) + .tracked_mints(tracked_mints) + .ncn(ncn) + .vault(vault) + .vault_ncn_ticket(vault_ncn_ticket) + .ncn_vault_ticket(ncn_vault_ticket) + .restaking_program_id(jito_restaking_program::id()) + .vault_program_id(jito_vault_program::id()) + .instruction(); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[ix], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } } #[inline(always)] diff --git a/integration_tests/tests/tip_router/initialize_tracked_mints.rs b/integration_tests/tests/tip_router/initialize_tracked_mints.rs new file mode 100644 index 0000000..c53d2f1 --- /dev/null +++ b/integration_tests/tests/tip_router/initialize_tracked_mints.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +mod tests { + use jito_tip_router_core::{ncn_config::NcnConfig, tracked_mints::TrackedMints}; + use solana_program::instruction::InstructionError; + use solana_sdk::{signature::Keypair, signer::Signer}; + + use crate::fixtures::{assert_ix_error, test_builder::TestBuilder, TestResult}; + + #[tokio::test] + async fn test_initialize_tracked_mints_ok() -> 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?; + + tip_router_client + .do_initialize_tracked_mints(ncn_root.ncn_pubkey) + .await?; + + let tracked_mints = tip_router_client + .get_tracked_mints(ncn_root.ncn_pubkey) + .await?; + + assert_eq!(tracked_mints.ncn, ncn_root.ncn_pubkey); + assert_eq!(tracked_mints.mint_count(), 0); + Ok(()) + } + + #[tokio::test] + async fn test_initialize_tracked_mints_wrong_ncn_config_fails() -> 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?; + + // Try to initialize with wrong NCN config + let wrong_ncn_config = Keypair::new().pubkey(); + let (tracked_mints_key, _, _) = TrackedMints::find_program_address( + &jito_tip_router_program::id(), + &ncn_root.ncn_pubkey, + ); + + let transaction_error = tip_router_client + .initialize_tracked_mints(&wrong_ncn_config, &tracked_mints_key, &ncn_root.ncn_pubkey) + .await; + + assert_ix_error(transaction_error, InstructionError::InvalidAccountOwner); + Ok(()) + } + + #[tokio::test] + async fn test_initialize_tracked_mints_wrong_ncn_fails() -> 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?; + + // Try to initialize with wrong NCN + let wrong_ncn = Keypair::new().pubkey(); + let (tracked_mints_key, _, _) = + TrackedMints::find_program_address(&jito_tip_router_program::id(), &wrong_ncn); + + let transaction_error = tip_router_client + .initialize_tracked_mints( + &NcnConfig::find_program_address( + &jito_tip_router_program::id(), + &ncn_root.ncn_pubkey, + ) + .0, + &tracked_mints_key, + &wrong_ncn, + ) + .await; + + assert_ix_error(transaction_error, InstructionError::InvalidAccountData); + Ok(()) + } + + #[tokio::test] + async fn test_initialize_tracked_mints_double_init_fails() -> 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?; + + tip_router_client + .do_initialize_tracked_mints(ncn_root.ncn_pubkey) + .await?; + + fixture.warp_slot_incremental(1).await?; + + // Second initialization should fail + let transaction_error = tip_router_client + .do_initialize_tracked_mints(ncn_root.ncn_pubkey) + .await; + + assert_ix_error(transaction_error, InstructionError::InvalidAccountOwner); + Ok(()) + } +} diff --git a/integration_tests/tests/tip_router/mod.rs b/integration_tests/tests/tip_router/mod.rs index dd9a5a4..85e3e93 100644 --- a/integration_tests/tests/tip_router/mod.rs +++ b/integration_tests/tests/tip_router/mod.rs @@ -1,5 +1,7 @@ mod admin_update_weight_table; mod initialize_ncn_config; +mod initialize_tracked_mints; mod initialize_weight_table; +mod register_mint; mod set_config_fees; mod set_new_admin; diff --git a/integration_tests/tests/tip_router/register_mint.rs b/integration_tests/tests/tip_router/register_mint.rs new file mode 100644 index 0000000..04b1e27 --- /dev/null +++ b/integration_tests/tests/tip_router/register_mint.rs @@ -0,0 +1,107 @@ +#[cfg(test)] +mod tests { + use solana_sdk::{signature::Keypair, signer::Signer}; + + use crate::fixtures::{test_builder::TestBuilder, TestResult}; + + #[tokio::test] + async fn test_register_mint_success() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let ncn_root = fixture.setup_ncn().await?; + + // Setup initial state + tip_router_client.setup_tip_router(&ncn_root).await?; + + // Setup vault and tickets + let vault = Keypair::new(); + let vault_ncn_ticket = Keypair::new(); + let ncn_vault_ticket = Keypair::new(); + + // Register mint + tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault.pubkey(), + vault_ncn_ticket.pubkey(), + ncn_vault_ticket.pubkey(), + ) + .await?; + + // Verify mint was registered by checking tracked mints + let tracked_mints = tip_router_client + .get_tracked_mints(ncn_root.ncn_pubkey) + .await?; + assert_eq!(tracked_mints.mint_count(), 1); + + Ok(()) + } + + #[tokio::test] + async fn test_register_mint_fails_without_initialization() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let ncn_root = fixture.setup_ncn().await?; + + // Try to register mint without initialization + let vault = Keypair::new(); + let vault_ncn_ticket = Keypair::new(); + let ncn_vault_ticket = Keypair::new(); + + let result = tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault.pubkey(), + vault_ncn_ticket.pubkey(), + ncn_vault_ticket.pubkey(), + ) + .await; + + assert!(result.is_err()); + + Ok(()) + } + + #[tokio::test] + async fn test_register_mint_duplicate() -> TestResult<()> { + let mut fixture = TestBuilder::new().await; + let mut tip_router_client = fixture.tip_router_client(); + let ncn_root = fixture.setup_ncn().await?; + + // Setup initial state + tip_router_client.setup_tip_router(&ncn_root).await?; + + // Setup vault and tickets + let vault = Keypair::new(); + let vault_ncn_ticket = Keypair::new(); + let ncn_vault_ticket = Keypair::new(); + + // Register mint first time + tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault.pubkey(), + vault_ncn_ticket.pubkey(), + ncn_vault_ticket.pubkey(), + ) + .await?; + + // Register same mint again + tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault.pubkey(), + vault_ncn_ticket.pubkey(), + ncn_vault_ticket.pubkey(), + ) + .await?; + + // Verify mint was only registered once + let tracked_mints = tip_router_client + .get_tracked_mints(ncn_root.ncn_pubkey) + .await?; + assert_eq!(tracked_mints.mint_count(), 1); + + Ok(()) + } +} From aed3a9c8a08aaafc2f10a39a1e1bcebffcbe9977 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 13 Nov 2024 11:06:34 -0500 Subject: [PATCH 5/9] lock mints --- .../jito_tip_router/errors/jitoTipRouter.ts | 6 +- .../instructions/initializeWeightTable.ts | 28 ++++---- .../instructions/registerMint.ts | 26 ++++++-- .../src/generated/errors/jito_tip_router.rs | 7 +- .../instructions/initialize_weight_table.rs | 41 ++++++------ .../generated/instructions/register_mint.rs | 66 +++++++++++++++---- core/src/error.rs | 2 + core/src/instruction.rs | 13 ++-- idl/jito_tip_router.json | 12 +++- .../tests/fixtures/tip_router_client.rs | 20 ++++-- .../tip_router/admin_update_weight_table.rs | 5 +- .../tip_router/initialize_weight_table.rs | 5 +- .../tests/tip_router/register_mint.rs | 14 ++++ .../tests/tip_router/set_new_admin.rs | 2 + program/src/register_mint.rs | 24 ++++++- 15 files changed, 195 insertions(+), 76 deletions(-) diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index a446993..1053652 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -48,8 +48,10 @@ export const JITO_TIP_ROUTER_ERROR__CONFIG_MINTS_NOT_UPDATED = 0x2209; // 8713 export const JITO_TIP_ROUTER_ERROR__CONFIG_MINT_LIST_FULL = 0x220a; // 8714 /** TrackedMintListFull: Tracked mints are at capacity */ export const JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL = 0x220b; // 8715 +/** TrackedMintsLocked: Tracked mints are locked for the epoch */ +export const JITO_TIP_ROUTER_ERROR__TRACKED_MINTS_LOCKED = 0x220c; // 8716 /** VaultIndexAlreadyInUse: Vault index already in use by a different mint */ -export const JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE = 0x220c; // 8716 +export const JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE = 0x220d; // 8717 /** FeeCapExceeded: Fee cap exceeded */ export const JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED = 0x2300; // 8960 /** IncorrectNcnAdmin: Incorrect NCN Admin */ @@ -78,6 +80,7 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL + | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINTS_LOCKED | typeof JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH | typeof JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH @@ -104,6 +107,7 @@ if (process.env.NODE_ENV !== 'production') { [JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE]: `There are no mints in the table`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE]: `Too many mints for table`, [JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL]: `Tracked mints are at capacity`, + [JITO_TIP_ROUTER_ERROR__TRACKED_MINTS_LOCKED]: `Tracked mints are locked for the epoch`, [JITO_TIP_ROUTER_ERROR__VAULT_INDEX_ALREADY_IN_USE]: `Vault index already in use by a different mint`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_LENGTH]: `Weight mints do not match - length`, [JITO_TIP_ROUTER_ERROR__WEIGHT_MINTS_DO_NOT_MATCH_MINT_HASH]: `Weight mints do not match - mint hash`, diff --git a/clients/js/jito_tip_router/instructions/initializeWeightTable.ts b/clients/js/jito_tip_router/instructions/initializeWeightTable.ts index fb1b904..2b1f90a 100644 --- a/clients/js/jito_tip_router/instructions/initializeWeightTable.ts +++ b/clients/js/jito_tip_router/instructions/initializeWeightTable.ts @@ -45,7 +45,7 @@ export function getInitializeWeightTableDiscriminatorBytes() { export type InitializeWeightTableInstruction< TProgram extends string = typeof JITO_TIP_ROUTER_PROGRAM_ADDRESS, TAccountRestakingConfig extends string | IAccountMeta = string, - TAccountNcnConfig extends string | IAccountMeta = string, + TAccountTrackedMints extends string | IAccountMeta = string, TAccountNcn extends string | IAccountMeta = string, TAccountWeightTable extends string | IAccountMeta = string, TAccountPayer extends string | IAccountMeta = string, @@ -61,9 +61,9 @@ export type InitializeWeightTableInstruction< TAccountRestakingConfig extends string ? ReadonlyAccount : TAccountRestakingConfig, - TAccountNcnConfig extends string - ? ReadonlyAccount - : TAccountNcnConfig, + TAccountTrackedMints extends string + ? ReadonlyAccount + : TAccountTrackedMints, TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, TAccountWeightTable extends string ? WritableAccount @@ -123,7 +123,7 @@ export function getInitializeWeightTableInstructionDataCodec(): Codec< export type InitializeWeightTableInput< TAccountRestakingConfig extends string = string, - TAccountNcnConfig extends string = string, + TAccountTrackedMints extends string = string, TAccountNcn extends string = string, TAccountWeightTable extends string = string, TAccountPayer extends string = string, @@ -131,7 +131,7 @@ export type InitializeWeightTableInput< TAccountSystemProgram extends string = string, > = { restakingConfig: Address; - ncnConfig: Address; + trackedMints: Address; ncn: Address; weightTable: Address; payer: TransactionSigner; @@ -142,7 +142,7 @@ export type InitializeWeightTableInput< export function getInitializeWeightTableInstruction< TAccountRestakingConfig extends string, - TAccountNcnConfig extends string, + TAccountTrackedMints extends string, TAccountNcn extends string, TAccountWeightTable extends string, TAccountPayer extends string, @@ -152,7 +152,7 @@ export function getInitializeWeightTableInstruction< >( input: InitializeWeightTableInput< TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountWeightTable, TAccountPayer, @@ -163,7 +163,7 @@ export function getInitializeWeightTableInstruction< ): InitializeWeightTableInstruction< TProgramAddress, TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountWeightTable, TAccountPayer, @@ -180,7 +180,7 @@ export function getInitializeWeightTableInstruction< value: input.restakingConfig ?? null, isWritable: false, }, - ncnConfig: { value: input.ncnConfig ?? null, isWritable: false }, + trackedMints: { value: input.trackedMints ?? null, isWritable: false }, ncn: { value: input.ncn ?? null, isWritable: false }, weightTable: { value: input.weightTable ?? null, isWritable: true }, payer: { value: input.payer ?? null, isWritable: true }, @@ -208,7 +208,7 @@ export function getInitializeWeightTableInstruction< const instruction = { accounts: [ getAccountMeta(accounts.restakingConfig), - getAccountMeta(accounts.ncnConfig), + getAccountMeta(accounts.trackedMints), getAccountMeta(accounts.ncn), getAccountMeta(accounts.weightTable), getAccountMeta(accounts.payer), @@ -222,7 +222,7 @@ export function getInitializeWeightTableInstruction< } as InitializeWeightTableInstruction< TProgramAddress, TAccountRestakingConfig, - TAccountNcnConfig, + TAccountTrackedMints, TAccountNcn, TAccountWeightTable, TAccountPayer, @@ -240,7 +240,7 @@ export type ParsedInitializeWeightTableInstruction< programAddress: Address; accounts: { restakingConfig: TAccountMetas[0]; - ncnConfig: TAccountMetas[1]; + trackedMints: TAccountMetas[1]; ncn: TAccountMetas[2]; weightTable: TAccountMetas[3]; payer: TAccountMetas[4]; @@ -272,7 +272,7 @@ export function parseInitializeWeightTableInstruction< programAddress: instruction.programAddress, accounts: { restakingConfig: getNextAccount(), - ncnConfig: getNextAccount(), + trackedMints: getNextAccount(), ncn: getNextAccount(), weightTable: getNextAccount(), payer: getNextAccount(), diff --git a/clients/js/jito_tip_router/instructions/registerMint.ts b/clients/js/jito_tip_router/instructions/registerMint.ts index 9164154..c4bd83c 100644 --- a/clients/js/jito_tip_router/instructions/registerMint.ts +++ b/clients/js/jito_tip_router/instructions/registerMint.ts @@ -38,6 +38,7 @@ export type RegisterMintInstruction< TAccountRestakingConfig extends string | IAccountMeta = string, TAccountTrackedMints extends string | IAccountMeta = string, TAccountNcn extends string | IAccountMeta = string, + TAccountWeightTable extends string | IAccountMeta = string, TAccountVault extends string | IAccountMeta = string, TAccountVaultNcnTicket extends string | IAccountMeta = string, TAccountNcnVaultTicket extends string | IAccountMeta = string, @@ -55,6 +56,9 @@ export type RegisterMintInstruction< ? WritableAccount : TAccountTrackedMints, TAccountNcn extends string ? ReadonlyAccount : TAccountNcn, + TAccountWeightTable extends string + ? ReadonlyAccount + : TAccountWeightTable, TAccountVault extends string ? ReadonlyAccount : TAccountVault, @@ -103,6 +107,7 @@ export type RegisterMintInput< TAccountRestakingConfig extends string = string, TAccountTrackedMints extends string = string, TAccountNcn extends string = string, + TAccountWeightTable extends string = string, TAccountVault extends string = string, TAccountVaultNcnTicket extends string = string, TAccountNcnVaultTicket extends string = string, @@ -112,6 +117,7 @@ export type RegisterMintInput< restakingConfig: Address; trackedMints: Address; ncn: Address; + weightTable: Address; vault: Address; vaultNcnTicket: Address; ncnVaultTicket: Address; @@ -123,6 +129,7 @@ export function getRegisterMintInstruction< TAccountRestakingConfig extends string, TAccountTrackedMints extends string, TAccountNcn extends string, + TAccountWeightTable extends string, TAccountVault extends string, TAccountVaultNcnTicket extends string, TAccountNcnVaultTicket extends string, @@ -134,6 +141,7 @@ export function getRegisterMintInstruction< TAccountRestakingConfig, TAccountTrackedMints, TAccountNcn, + TAccountWeightTable, TAccountVault, TAccountVaultNcnTicket, TAccountNcnVaultTicket, @@ -146,6 +154,7 @@ export function getRegisterMintInstruction< TAccountRestakingConfig, TAccountTrackedMints, TAccountNcn, + TAccountWeightTable, TAccountVault, TAccountVaultNcnTicket, TAccountNcnVaultTicket, @@ -164,6 +173,7 @@ export function getRegisterMintInstruction< }, trackedMints: { value: input.trackedMints ?? null, isWritable: true }, ncn: { value: input.ncn ?? null, isWritable: false }, + weightTable: { value: input.weightTable ?? null, isWritable: false }, vault: { value: input.vault ?? null, isWritable: false }, vaultNcnTicket: { value: input.vaultNcnTicket ?? null, isWritable: false }, ncnVaultTicket: { value: input.ncnVaultTicket ?? null, isWritable: false }, @@ -184,6 +194,7 @@ export function getRegisterMintInstruction< getAccountMeta(accounts.restakingConfig), getAccountMeta(accounts.trackedMints), getAccountMeta(accounts.ncn), + getAccountMeta(accounts.weightTable), getAccountMeta(accounts.vault), getAccountMeta(accounts.vaultNcnTicket), getAccountMeta(accounts.ncnVaultTicket), @@ -197,6 +208,7 @@ export function getRegisterMintInstruction< TAccountRestakingConfig, TAccountTrackedMints, TAccountNcn, + TAccountWeightTable, TAccountVault, TAccountVaultNcnTicket, TAccountNcnVaultTicket, @@ -216,11 +228,12 @@ export type ParsedRegisterMintInstruction< restakingConfig: TAccountMetas[0]; trackedMints: TAccountMetas[1]; ncn: TAccountMetas[2]; - vault: TAccountMetas[3]; - vaultNcnTicket: TAccountMetas[4]; - ncnVaultTicket: TAccountMetas[5]; - restakingProgramId: TAccountMetas[6]; - vaultProgramId: TAccountMetas[7]; + weightTable: TAccountMetas[3]; + vault: TAccountMetas[4]; + vaultNcnTicket: TAccountMetas[5]; + ncnVaultTicket: TAccountMetas[6]; + restakingProgramId: TAccountMetas[7]; + vaultProgramId: TAccountMetas[8]; }; data: RegisterMintInstructionData; }; @@ -233,7 +246,7 @@ export function parseRegisterMintInstruction< IInstructionWithAccounts & IInstructionWithData ): ParsedRegisterMintInstruction { - if (instruction.accounts.length < 8) { + if (instruction.accounts.length < 9) { // TODO: Coded error. throw new Error('Not enough accounts'); } @@ -249,6 +262,7 @@ export function parseRegisterMintInstruction< restakingConfig: getNextAccount(), trackedMints: getNextAccount(), ncn: getNextAccount(), + weightTable: getNextAccount(), vault: getNextAccount(), vaultNcnTicket: getNextAccount(), ncnVaultTicket: getNextAccount(), 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 3a3792c..ddc3395 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 @@ -60,9 +60,12 @@ pub enum JitoTipRouterError { /// 8715 - Tracked mints are at capacity #[error("Tracked mints are at capacity")] TrackedMintListFull = 0x220B, - /// 8716 - Vault index already in use by a different mint + /// 8716 - Tracked mints are locked for the epoch + #[error("Tracked mints are locked for the epoch")] + TrackedMintsLocked = 0x220C, + /// 8717 - Vault index already in use by a different mint #[error("Vault index already in use by a different mint")] - VaultIndexAlreadyInUse = 0x220C, + VaultIndexAlreadyInUse = 0x220D, /// 8960 - Fee cap exceeded #[error("Fee cap exceeded")] FeeCapExceeded = 0x2300, diff --git a/clients/rust/jito_tip_router/src/generated/instructions/initialize_weight_table.rs b/clients/rust/jito_tip_router/src/generated/instructions/initialize_weight_table.rs index 1c703fc..b46b10c 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/initialize_weight_table.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/initialize_weight_table.rs @@ -10,7 +10,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; pub struct InitializeWeightTable { pub restaking_config: solana_program::pubkey::Pubkey, - pub ncn_config: solana_program::pubkey::Pubkey, + pub tracked_mints: solana_program::pubkey::Pubkey, pub ncn: solana_program::pubkey::Pubkey, @@ -42,7 +42,7 @@ impl InitializeWeightTable { false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( - self.ncn_config, + self.tracked_mints, false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( @@ -106,7 +106,7 @@ pub struct InitializeWeightTableInstructionArgs { /// ### Accounts: /// /// 0. `[]` restaking_config -/// 1. `[]` ncn_config +/// 1. `[]` tracked_mints /// 2. `[]` ncn /// 3. `[writable]` weight_table /// 4. `[writable, signer]` payer @@ -115,7 +115,7 @@ pub struct InitializeWeightTableInstructionArgs { #[derive(Clone, Debug, Default)] pub struct InitializeWeightTableBuilder { restaking_config: Option, - ncn_config: Option, + tracked_mints: Option, ncn: Option, weight_table: Option, payer: Option, @@ -138,8 +138,8 @@ impl InitializeWeightTableBuilder { self } #[inline(always)] - pub fn ncn_config(&mut self, ncn_config: solana_program::pubkey::Pubkey) -> &mut Self { - self.ncn_config = Some(ncn_config); + pub fn tracked_mints(&mut self, tracked_mints: solana_program::pubkey::Pubkey) -> &mut Self { + self.tracked_mints = Some(tracked_mints); self } #[inline(always)] @@ -199,7 +199,7 @@ impl InitializeWeightTableBuilder { pub fn instruction(&self) -> solana_program::instruction::Instruction { let accounts = InitializeWeightTable { restaking_config: self.restaking_config.expect("restaking_config is not set"), - ncn_config: self.ncn_config.expect("ncn_config is not set"), + tracked_mints: self.tracked_mints.expect("tracked_mints is not set"), ncn: self.ncn.expect("ncn is not set"), weight_table: self.weight_table.expect("weight_table is not set"), payer: self.payer.expect("payer is not set"), @@ -222,7 +222,7 @@ impl InitializeWeightTableBuilder { pub struct InitializeWeightTableCpiAccounts<'a, 'b> { pub restaking_config: &'b solana_program::account_info::AccountInfo<'a>, - pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + pub tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, pub ncn: &'b solana_program::account_info::AccountInfo<'a>, @@ -242,7 +242,7 @@ pub struct InitializeWeightTableCpi<'a, 'b> { pub restaking_config: &'b solana_program::account_info::AccountInfo<'a>, - pub ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + pub tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, pub ncn: &'b solana_program::account_info::AccountInfo<'a>, @@ -266,7 +266,7 @@ impl<'a, 'b> InitializeWeightTableCpi<'a, 'b> { Self { __program: program, restaking_config: accounts.restaking_config, - ncn_config: accounts.ncn_config, + tracked_mints: accounts.tracked_mints, ncn: accounts.ncn, weight_table: accounts.weight_table, payer: accounts.payer, @@ -314,7 +314,7 @@ impl<'a, 'b> InitializeWeightTableCpi<'a, 'b> { false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( - *self.ncn_config.key, + *self.tracked_mints.key, false, )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( @@ -358,7 +358,7 @@ impl<'a, 'b> InitializeWeightTableCpi<'a, 'b> { let mut account_infos = Vec::with_capacity(7 + 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.tracked_mints.clone()); account_infos.push(self.ncn.clone()); account_infos.push(self.weight_table.clone()); account_infos.push(self.payer.clone()); @@ -381,7 +381,7 @@ impl<'a, 'b> InitializeWeightTableCpi<'a, 'b> { /// ### Accounts: /// /// 0. `[]` restaking_config -/// 1. `[]` ncn_config +/// 1. `[]` tracked_mints /// 2. `[]` ncn /// 3. `[writable]` weight_table /// 4. `[writable, signer]` payer @@ -397,7 +397,7 @@ impl<'a, 'b> InitializeWeightTableCpiBuilder<'a, 'b> { let instruction = Box::new(InitializeWeightTableCpiBuilderInstruction { __program: program, restaking_config: None, - ncn_config: None, + tracked_mints: None, ncn: None, weight_table: None, payer: None, @@ -417,11 +417,11 @@ impl<'a, 'b> InitializeWeightTableCpiBuilder<'a, 'b> { self } #[inline(always)] - pub fn ncn_config( + pub fn tracked_mints( &mut self, - ncn_config: &'b solana_program::account_info::AccountInfo<'a>, + tracked_mints: &'b solana_program::account_info::AccountInfo<'a>, ) -> &mut Self { - self.instruction.ncn_config = Some(ncn_config); + self.instruction.tracked_mints = Some(tracked_mints); self } #[inline(always)] @@ -516,7 +516,10 @@ impl<'a, 'b> InitializeWeightTableCpiBuilder<'a, 'b> { .restaking_config .expect("restaking_config is not set"), - ncn_config: self.instruction.ncn_config.expect("ncn_config is not set"), + tracked_mints: self + .instruction + .tracked_mints + .expect("tracked_mints is not set"), ncn: self.instruction.ncn.expect("ncn is not set"), @@ -549,7 +552,7 @@ impl<'a, 'b> InitializeWeightTableCpiBuilder<'a, 'b> { struct InitializeWeightTableCpiBuilderInstruction<'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>>, + tracked_mints: Option<&'b solana_program::account_info::AccountInfo<'a>>, ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, weight_table: Option<&'b solana_program::account_info::AccountInfo<'a>>, payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, 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 index 667a722..ed3496c 100644 --- a/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs +++ b/clients/rust/jito_tip_router/src/generated/instructions/register_mint.rs @@ -14,6 +14,8 @@ pub struct RegisterMint { pub ncn: solana_program::pubkey::Pubkey, + pub weight_table: solana_program::pubkey::Pubkey, + pub vault: solana_program::pubkey::Pubkey, pub vault_ncn_ticket: solana_program::pubkey::Pubkey, @@ -34,7 +36,7 @@ impl RegisterMint { &self, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(9 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new_readonly( self.restaking_config, false, @@ -46,6 +48,10 @@ impl RegisterMint { accounts.push(solana_program::instruction::AccountMeta::new_readonly( self.ncn, false, )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + self.weight_table, + false, + )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( self.vault, false, )); @@ -100,16 +106,18 @@ impl Default for RegisterMintInstructionData { /// 0. `[]` restaking_config /// 1. `[writable]` tracked_mints /// 2. `[]` ncn -/// 3. `[]` vault -/// 4. `[]` vault_ncn_ticket -/// 5. `[]` ncn_vault_ticket -/// 6. `[]` restaking_program_id -/// 7. `[]` vault_program_id +/// 3. `[]` weight_table +/// 4. `[]` vault +/// 5. `[]` vault_ncn_ticket +/// 6. `[]` ncn_vault_ticket +/// 7. `[]` restaking_program_id +/// 8. `[]` vault_program_id #[derive(Clone, Debug, Default)] pub struct RegisterMintBuilder { restaking_config: Option, tracked_mints: Option, ncn: Option, + weight_table: Option, vault: Option, vault_ncn_ticket: Option, ncn_vault_ticket: Option, @@ -141,6 +149,11 @@ impl RegisterMintBuilder { self } #[inline(always)] + pub fn weight_table(&mut self, weight_table: solana_program::pubkey::Pubkey) -> &mut Self { + self.weight_table = Some(weight_table); + self + } + #[inline(always)] pub fn vault(&mut self, vault: solana_program::pubkey::Pubkey) -> &mut Self { self.vault = Some(vault); self @@ -201,6 +214,7 @@ impl RegisterMintBuilder { restaking_config: self.restaking_config.expect("restaking_config is not set"), tracked_mints: self.tracked_mints.expect("tracked_mints is not set"), ncn: self.ncn.expect("ncn is not set"), + weight_table: self.weight_table.expect("weight_table 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"), @@ -222,6 +236,8 @@ pub struct RegisterMintCpiAccounts<'a, 'b> { pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + pub weight_table: &'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>, @@ -244,6 +260,8 @@ pub struct RegisterMintCpi<'a, 'b> { pub ncn: &'b solana_program::account_info::AccountInfo<'a>, + pub weight_table: &'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>, @@ -265,6 +283,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { restaking_config: accounts.restaking_config, tracked_mints: accounts.tracked_mints, ncn: accounts.ncn, + weight_table: accounts.weight_table, vault: accounts.vault, vault_ncn_ticket: accounts.vault_ncn_ticket, ncn_vault_ticket: accounts.ncn_vault_ticket, @@ -305,7 +324,7 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(8 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(9 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new_readonly( *self.restaking_config.key, false, @@ -318,6 +337,10 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { *self.ncn.key, false, )); + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *self.weight_table.key, + false, + )); accounts.push(solana_program::instruction::AccountMeta::new_readonly( *self.vault.key, false, @@ -352,11 +375,12 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { accounts, data, }; - let mut account_infos = Vec::with_capacity(8 + 1 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(9 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.restaking_config.clone()); account_infos.push(self.tracked_mints.clone()); account_infos.push(self.ncn.clone()); + account_infos.push(self.weight_table.clone()); account_infos.push(self.vault.clone()); account_infos.push(self.vault_ncn_ticket.clone()); account_infos.push(self.ncn_vault_ticket.clone()); @@ -381,11 +405,12 @@ impl<'a, 'b> RegisterMintCpi<'a, 'b> { /// 0. `[]` restaking_config /// 1. `[writable]` tracked_mints /// 2. `[]` ncn -/// 3. `[]` vault -/// 4. `[]` vault_ncn_ticket -/// 5. `[]` ncn_vault_ticket -/// 6. `[]` restaking_program_id -/// 7. `[]` vault_program_id +/// 3. `[]` weight_table +/// 4. `[]` vault +/// 5. `[]` vault_ncn_ticket +/// 6. `[]` ncn_vault_ticket +/// 7. `[]` restaking_program_id +/// 8. `[]` vault_program_id #[derive(Clone, Debug)] pub struct RegisterMintCpiBuilder<'a, 'b> { instruction: Box>, @@ -398,6 +423,7 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { restaking_config: None, tracked_mints: None, ncn: None, + weight_table: None, vault: None, vault_ncn_ticket: None, ncn_vault_ticket: None, @@ -429,6 +455,14 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { self } #[inline(always)] + pub fn weight_table( + &mut self, + weight_table: &'b solana_program::account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.weight_table = Some(weight_table); + self + } + #[inline(always)] pub fn vault(&mut self, vault: &'b solana_program::account_info::AccountInfo<'a>) -> &mut Self { self.instruction.vault = Some(vault); self @@ -521,6 +555,11 @@ impl<'a, 'b> RegisterMintCpiBuilder<'a, 'b> { ncn: self.instruction.ncn.expect("ncn is not set"), + weight_table: self + .instruction + .weight_table + .expect("weight_table is not set"), + vault: self.instruction.vault.expect("vault is not set"), vault_ncn_ticket: self @@ -556,6 +595,7 @@ struct RegisterMintCpiBuilderInstruction<'a, 'b> { restaking_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, tracked_mints: Option<&'b solana_program::account_info::AccountInfo<'a>>, ncn: Option<&'b solana_program::account_info::AccountInfo<'a>>, + weight_table: 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>>, diff --git a/core/src/error.rs b/core/src/error.rs index 39958c2..49ab208 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -38,6 +38,8 @@ pub enum TipRouterError { ConfigMintListFull, #[error("Tracked mints are at capacity")] TrackedMintListFull, + #[error("Tracked mints are locked for the epoch")] + TrackedMintsLocked, #[error("Vault index already in use by a different mint")] VaultIndexAlreadyInUse, #[error("Fee cap exceeded")] diff --git a/core/src/instruction.rs b/core/src/instruction.rs index 76d2f65..01215a4 100644 --- a/core/src/instruction.rs +++ b/core/src/instruction.rs @@ -53,7 +53,7 @@ pub enum WeightTableInstruction { /// Initializes the weight table for a given NCN epoch #[account(0, name = "restaking_config")] - #[account(1, name = "ncn_config")] + #[account(1, name = "tracked_mints")] #[account(2, name = "ncn")] #[account(3, writable, name = "weight_table")] #[account(4, writable, signer, name = "payer")] @@ -78,11 +78,12 @@ pub enum WeightTableInstruction { #[account(0, name = "restaking_config")] #[account(1, writable, name = "tracked_mints")] #[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")] + #[account(3, name = "weight_table")] + #[account(4, name = "vault")] + #[account(5, name = "vault_ncn_ticket")] + #[account(6, name = "ncn_vault_ticket")] + #[account(7, name = "restaking_program_id")] + #[account(8, name = "vault_program_id")] RegisterMint, /// Initializes the tracked mints account for an NCN diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 0a0d60c..77bbc07 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -176,7 +176,7 @@ "isSigner": false }, { - "name": "ncnConfig", + "name": "trackedMints", "isMut": false, "isSigner": false }, @@ -281,6 +281,11 @@ "isMut": false, "isSigner": false }, + { + "name": "weightTable", + "isMut": false, + "isSigner": false + }, { "name": "vault", "isMut": false, @@ -691,6 +696,11 @@ }, { "code": 8716, + "name": "TrackedMintsLocked", + "msg": "Tracked mints are locked for the epoch" + }, + { + "code": 8717, "name": "VaultIndexAlreadyInUse", "msg": "Vault index already in use by a different mint" }, diff --git a/integration_tests/tests/fixtures/tip_router_client.rs b/integration_tests/tests/fixtures/tip_router_client.rs index 31531ec..b6ffb94 100644 --- a/integration_tests/tests/fixtures/tip_router_client.rs +++ b/integration_tests/tests/fixtures/tip_router_client.rs @@ -18,6 +18,7 @@ use solana_program::{ }; use solana_program_test::BanksClient; use solana_sdk::{ + clock::Clock, commitment_config::CommitmentLevel, signature::{Keypair, Signer}, system_program, @@ -262,13 +263,14 @@ impl TipRouterClient { let restaking_config_account = self.get_restaking_config().await?; let ncn_epoch = current_slot / restaking_config_account.epoch_length(); - let config_pda = NcnConfig::find_program_address(&jito_tip_router_program::id(), &ncn).0; + let tracked_mints_pda = + TrackedMints::find_program_address(&jito_tip_router_program::id(), &ncn).0; let weight_table = WeightTable::find_program_address(&jito_tip_router_program::id(), &ncn, ncn_epoch).0; let ix = InitializeWeightTableBuilder::new() .restaking_config(restaking_config) - .ncn_config(config_pda) + .tracked_mints(tracked_mints_pda) .ncn(ncn) .weight_table(weight_table) .payer(self.payer.pubkey()) @@ -369,14 +371,22 @@ impl TipRouterClient { vault_ncn_ticket: Pubkey, ncn_vault_ticket: Pubkey, ) -> TestResult<()> { - let restaking_config = Config::find_program_address(&jito_restaking_program::id()).0; + let restaking_config_address = + Config::find_program_address(&jito_restaking_program::id()).0; let tracked_mints = TrackedMints::find_program_address(&jito_tip_router_program::id(), &ncn).0; + let restaking_config = self.get_restaking_config().await?; + let current_slot = self.banks_client.get_sysvar::().await?.slot; + let ncn_epoch = current_slot / restaking_config.epoch_length(); + let weight_table = + WeightTable::find_program_address(&jito_tip_router_program::id(), &ncn, ncn_epoch).0; + self.register_mint( - restaking_config, + restaking_config_address, tracked_mints, ncn, + weight_table, vault, vault_ncn_ticket, ncn_vault_ticket, @@ -389,6 +399,7 @@ impl TipRouterClient { restaking_config: Pubkey, tracked_mints: Pubkey, ncn: Pubkey, + weight_table: Pubkey, vault: Pubkey, vault_ncn_ticket: Pubkey, ncn_vault_ticket: Pubkey, @@ -397,6 +408,7 @@ impl TipRouterClient { .restaking_config(restaking_config) .tracked_mints(tracked_mints) .ncn(ncn) + .weight_table(weight_table) .vault(vault) .vault_ncn_ticket(vault_ncn_ticket) .ncn_vault_ticket(ncn_vault_ticket) diff --git a/integration_tests/tests/tip_router/admin_update_weight_table.rs b/integration_tests/tests/tip_router/admin_update_weight_table.rs index dc91213..adc7d32 100644 --- a/integration_tests/tests/tip_router/admin_update_weight_table.rs +++ b/integration_tests/tests/tip_router/admin_update_weight_table.rs @@ -15,10 +15,7 @@ mod tests { let slot = fixture.clock().await.slot; - //TODO fix when config has mints - tip_router_client - .do_initialize_config(ncn_root.ncn_pubkey, &ncn_root.ncn_admin) - .await?; + tip_router_client.setup_tip_router(&ncn_root).await?; tip_router_client .do_initialize_weight_table(ncn_root.ncn_pubkey, slot) diff --git a/integration_tests/tests/tip_router/initialize_weight_table.rs b/integration_tests/tests/tip_router/initialize_weight_table.rs index 86c747f..3d70c7e 100644 --- a/integration_tests/tests/tip_router/initialize_weight_table.rs +++ b/integration_tests/tests/tip_router/initialize_weight_table.rs @@ -13,10 +13,7 @@ mod tests { let slot = fixture.clock().await.slot; - //TODO fix when config has mints - tip_router_client - .do_initialize_config(ncn_root.ncn_pubkey, &ncn_root.ncn_admin) - .await?; + tip_router_client.setup_tip_router(&ncn_root).await?; tip_router_client .do_initialize_weight_table(ncn_root.ncn_pubkey, slot) diff --git a/integration_tests/tests/tip_router/register_mint.rs b/integration_tests/tests/tip_router/register_mint.rs index 04b1e27..27c6c20 100644 --- a/integration_tests/tests/tip_router/register_mint.rs +++ b/integration_tests/tests/tip_router/register_mint.rs @@ -104,4 +104,18 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_register_mint_fails_with_weight_table() -> 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.setup_tip_router(&ncn_root).await?; + + // TODO create ncn and vault with 1 mint, register mint, initialize weight table + // TODO verify weight table locks register_mint + + Ok(()) + } } diff --git a/integration_tests/tests/tip_router/set_new_admin.rs b/integration_tests/tests/tip_router/set_new_admin.rs index 8f56adc..af6801c 100644 --- a/integration_tests/tests/tip_router/set_new_admin.rs +++ b/integration_tests/tests/tip_router/set_new_admin.rs @@ -29,6 +29,8 @@ mod tests { .await?; assert_eq!(config.fee_admin, new_fee_admin); + fixture.warp_slot_incremental(1).await?; + let new_tie_breaker = Pubkey::new_unique(); tip_router_client .do_set_new_admin(ConfigAdminRole::TieBreakerAdmin, new_tie_breaker, &ncn_root) diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs index 1818f5c..8fe2b0e 100644 --- a/program/src/register_mint.rs +++ b/program/src/register_mint.rs @@ -1,6 +1,9 @@ use jito_bytemuck::AccountDeserialize; +use jito_jsm_core::loader::load_system_account; use jito_restaking_core::{config::Config, ncn::Ncn, ncn_vault_ticket::NcnVaultTicket}; -use jito_tip_router_core::tracked_mints::TrackedMints; +use jito_tip_router_core::{ + error::TipRouterError, tracked_mints::TrackedMints, weight_table::WeightTable, +}; use jito_vault_core::{vault::Vault, vault_ncn_ticket::VaultNcnTicket}; use solana_program::{ account_info::AccountInfo, @@ -8,11 +11,12 @@ use solana_program::{ msg, program_error::ProgramError, pubkey::Pubkey, + system_program, sysvar::{clock::Clock, Sysvar}, }; pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { - let [restaking_config, tracked_mints, ncn, vault, vault_ncn_ticket, ncn_vault_ticket, restaking_program_id, vault_program_id] = + let [restaking_config, tracked_mints, ncn, weight_table, vault, vault_ncn_ticket, ncn_vault_ticket, restaking_program_id, vault_program_id] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -37,6 +41,22 @@ pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> P let slot = Clock::get()?.slot; + let ncn_epoch = slot + .checked_div(epoch_length) + .ok_or(TipRouterError::DenominatorIsZero)?; + + // Once tracked_mints.mint_count() == ncn.vault_count, the weight table can be initialized + // Once the weight table is initialized, you can't add any more mints + if weight_table.owner.eq(&system_program::ID) { + load_system_account(weight_table, false)?; + } else if weight_table.owner.eq(program_id) { + WeightTable::load(program_id, weight_table, ncn, ncn_epoch, false)?; + return Err(TipRouterError::TrackedMintsLocked.into()); + } else { + msg!("Weight table account is not owned by this program or the system program"); + return Err(ProgramError::InvalidAccountOwner); + } + // 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)?; From 1aedf9e46b01e87b282fee011f6865f92ebbca62 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 13 Nov 2024 14:48:38 -0500 Subject: [PATCH 6/9] done --- integration_tests/tests/fixtures/mod.rs | 1 + .../tests/fixtures/restaking_client.rs | 1003 +++++++++- .../tests/fixtures/test_builder.rs | 8 + .../tests/fixtures/vault_client.rs | 1650 ++++++++++++++++- .../tests/tip_router/register_mint.rs | 177 +- program/src/register_mint.rs | 1 + 6 files changed, 2791 insertions(+), 49 deletions(-) diff --git a/integration_tests/tests/fixtures/mod.rs b/integration_tests/tests/fixtures/mod.rs index c15b19b..cfc6717 100644 --- a/integration_tests/tests/fixtures/mod.rs +++ b/integration_tests/tests/fixtures/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; pub mod restaking_client; pub mod test_builder; pub mod tip_router_client; +pub mod vault_client; pub type TestResult = Result; diff --git a/integration_tests/tests/fixtures/restaking_client.rs b/integration_tests/tests/fixtures/restaking_client.rs index 9128bf8..ab0d6b1 100644 --- a/integration_tests/tests/fixtures/restaking_client.rs +++ b/integration_tests/tests/fixtures/restaking_client.rs @@ -1,12 +1,34 @@ -use jito_restaking_core::{config::Config, ncn::Ncn}; -use jito_restaking_sdk::sdk::{initialize_config, initialize_ncn}; +use jito_bytemuck::AccountDeserialize; +use jito_restaking_core::{ + config::Config, ncn::Ncn, ncn_operator_state::NcnOperatorState, + ncn_vault_slasher_ticket::NcnVaultSlasherTicket, ncn_vault_ticket::NcnVaultTicket, + operator::Operator, operator_vault_ticket::OperatorVaultTicket, +}; +use jito_restaking_sdk::{ + error::RestakingError, + instruction::OperatorAdminRole, + sdk::{ + cooldown_ncn_vault_ticket, initialize_config, initialize_ncn, + initialize_ncn_operator_state, initialize_ncn_vault_slasher_ticket, + initialize_ncn_vault_ticket, initialize_operator, initialize_operator_vault_ticket, + ncn_cooldown_operator, ncn_set_admin, ncn_warmup_operator, operator_cooldown_ncn, + operator_set_admin, operator_set_fee, operator_set_secondary_admin, operator_warmup_ncn, + set_config_admin, warmup_ncn_vault_slasher_ticket, warmup_ncn_vault_ticket, + warmup_operator_vault_ticket, + }, +}; +use solana_program::{ + instruction::InstructionError, native_token::sol_to_lamports, pubkey::Pubkey, + system_instruction::transfer, +}; use solana_program_test::BanksClient; use solana_sdk::{ - commitment_config::CommitmentLevel, native_token::sol_to_lamports, pubkey::Pubkey, - signature::Keypair, signer::Signer, system_instruction::transfer, transaction::Transaction, + commitment_config::CommitmentLevel, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, }; -use super::TestResult; +use crate::fixtures::{TestError, TestResult}; #[derive(Debug)] pub struct NcnRoot { @@ -14,6 +36,12 @@ pub struct NcnRoot { pub ncn_admin: Keypair, } +#[derive(Debug)] +pub struct OperatorRoot { + pub operator_pubkey: Pubkey, + pub operator_admin: Keypair, +} + pub struct RestakingProgramClient { banks_client: BanksClient, payer: Keypair, @@ -27,43 +55,189 @@ impl RestakingProgramClient { } } - pub async fn process_transaction(&mut self, tx: &Transaction) -> TestResult<()> { - self.banks_client - .process_transaction_with_preflight_and_commitment( - tx.clone(), - CommitmentLevel::Processed, - ) - .await?; - Ok(()) + pub async fn get_ncn(&mut self, ncn: &Pubkey) -> TestResult { + let account = self + .banks_client + .get_account_with_commitment(*ncn, CommitmentLevel::Processed) + .await? + .unwrap(); + + Ok(*Ncn::try_from_slice_unchecked(account.data.as_slice())?) } - pub async fn airdrop(&mut self, to: &Pubkey, sol: f64) -> TestResult<()> { - let blockhash = self.banks_client.get_latest_blockhash().await?; - self.banks_client - .process_transaction_with_preflight_and_commitment( - Transaction::new_signed_with_payer( - &[transfer(&self.payer.pubkey(), to, sol_to_lamports(sol))], - Some(&self.payer.pubkey()), - &[&self.payer], - blockhash, - ), - CommitmentLevel::Processed, - ) - .await?; - Ok(()) + pub async fn get_config(&mut self, account: &Pubkey) -> TestResult { + let account = self.banks_client.get_account(*account).await?.unwrap(); + Ok(*Config::try_from_slice_unchecked(account.data.as_slice())?) + } + + pub async fn get_ncn_vault_ticket( + &mut self, + ncn: &Pubkey, + vault: &Pubkey, + ) -> TestResult { + let account = + NcnVaultTicket::find_program_address(&jito_restaking_program::id(), ncn, vault).0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*NcnVaultTicket::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_ncn_operator_state( + &mut self, + ncn: &Pubkey, + operator: &Pubkey, + ) -> TestResult { + let account = + NcnOperatorState::find_program_address(&jito_restaking_program::id(), ncn, operator).0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*NcnOperatorState::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_operator(&mut self, account: &Pubkey) -> TestResult { + let account = self.banks_client.get_account(*account).await?.unwrap(); + Ok(*Operator::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_operator_vault_ticket( + &mut self, + operator: &Pubkey, + vault: &Pubkey, + ) -> TestResult { + let account = OperatorVaultTicket::find_program_address( + &jito_restaking_program::id(), + operator, + vault, + ) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*OperatorVaultTicket::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + #[allow(dead_code)] + pub async fn get_operator_ncn_ticket( + &mut self, + operator: &Pubkey, + ncn: &Pubkey, + ) -> TestResult { + let account = + NcnOperatorState::find_program_address(&jito_restaking_program::id(), operator, ncn).0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*NcnOperatorState::try_from_slice_unchecked( + account.data.as_slice(), + )?) } pub async fn do_initialize_config(&mut self) -> TestResult { 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(), 10.0).await?; + self.airdrop(&restaking_config_admin.pubkey(), 1.0).await?; self.initialize_config(&restaking_config_pubkey, &restaking_config_admin) .await?; Ok(restaking_config_admin) } + pub async fn do_initialize_operator(&mut self) -> TestResult { + // create operator + add operator vault + let operator_base = Keypair::new(); + let operator_pubkey = + Operator::find_program_address(&jito_restaking_program::id(), &operator_base.pubkey()) + .0; + let operator_admin = Keypair::new(); + self.airdrop(&operator_admin.pubkey(), 1.0).await?; + self.initialize_operator( + &Config::find_program_address(&jito_restaking_program::id()).0, + &operator_pubkey, + &operator_admin, + &operator_base, + 0, + ) + .await?; + Ok(OperatorRoot { + operator_pubkey, + operator_admin, + }) + } + + pub async fn do_initialize_operator_vault_ticket( + &mut self, + operator_root: &OperatorRoot, + vault_pubkey: &Pubkey, + ) -> TestResult<()> { + let operator_vault_ticket = OperatorVaultTicket::find_program_address( + &jito_restaking_program::id(), + &operator_root.operator_pubkey, + vault_pubkey, + ) + .0; + self.initialize_operator_vault_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &operator_root.operator_pubkey, + vault_pubkey, + &operator_vault_ticket, + &operator_root.operator_admin, + &operator_root.operator_admin, + ) + .await?; + + Ok(()) + } + + pub async fn do_warmup_operator_vault_ticket( + &mut self, + operator_root: &OperatorRoot, + vault_pubkey: &Pubkey, + ) -> TestResult<()> { + let operator_vault_ticket = OperatorVaultTicket::find_program_address( + &jito_restaking_program::id(), + &operator_root.operator_pubkey, + vault_pubkey, + ) + .0; + self.warmup_operator_vault_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &operator_root.operator_pubkey, + vault_pubkey, + &operator_vault_ticket, + &operator_root.operator_admin, + ) + .await + } + + pub async fn warmup_operator_vault_ticket( + &mut self, + config: &Pubkey, + operator: &Pubkey, + vault: &Pubkey, + operator_vault_ticket: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[warmup_operator_vault_ticket( + &jito_restaking_program::id(), + config, + operator, + vault, + operator_vault_ticket, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + pub async fn initialize_config( &mut self, config: &Pubkey, @@ -106,6 +280,429 @@ impl RestakingProgramClient { }) } + pub async fn do_initialize_ncn_vault_ticket( + &mut self, + ncn_root: &NcnRoot, + vault: &Pubkey, + ) -> TestResult<()> { + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + ) + .0; + + self.initialize_ncn_vault_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + vault, + &ncn_vault_ticket, + &ncn_root.ncn_admin, + &self.payer.insecure_clone(), + ) + .await + } + + pub async fn do_warmup_ncn_vault_ticket( + &mut self, + ncn_root: &NcnRoot, + vault: &Pubkey, + ) -> TestResult<()> { + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + ) + .0; + self.warmup_ncn_vault_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + vault, + &ncn_vault_ticket, + &ncn_root.ncn_admin, + ) + .await + } + + pub async fn warmup_ncn_vault_ticket( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + vault: &Pubkey, + ncn_vault_ticket: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[warmup_ncn_vault_ticket( + &jito_restaking_program::id(), + config, + ncn, + vault, + ncn_vault_ticket, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + #[allow(dead_code)] + pub async fn do_cooldown_ncn_vault_ticket( + &mut self, + ncn_root: &NcnRoot, + vault: &Pubkey, + ) -> TestResult<()> { + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + ) + .0; + self.cooldown_ncn_vault_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + vault, + &ncn_vault_ticket, + &ncn_root.ncn_admin, + ) + .await + } + + #[allow(dead_code)] + pub async fn cooldown_ncn_vault_ticket( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + vault: &Pubkey, + ncn_vault_ticket: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[cooldown_ncn_vault_ticket( + &jito_restaking_program::id(), + config, + ncn, + vault, + ncn_vault_ticket, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn do_ncn_warmup_operator( + &mut self, + ncn_root: &NcnRoot, + operator_pubkey: &Pubkey, + ) -> TestResult<()> { + self.ncn_warmup_operator( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + operator_pubkey, + &NcnOperatorState::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + operator_pubkey, + ) + .0, + &ncn_root.ncn_admin, + ) + .await + } + + pub async fn do_ncn_cooldown_operator( + &mut self, + ncn_root: &NcnRoot, + operator_pubkey: &Pubkey, + ) -> TestResult<()> { + self.ncn_cooldown_operator( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + operator_pubkey, + &NcnOperatorState::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + operator_pubkey, + ) + .0, + &ncn_root.ncn_admin, + ) + .await + } + + pub async fn ncn_cooldown_operator( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + operator_pubkey: &Pubkey, + ncn_operator_state: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[ncn_cooldown_operator( + &jito_restaking_program::id(), + config, + ncn, + operator_pubkey, + ncn_operator_state, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn ncn_warmup_operator( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + operator_pubkey: &Pubkey, + ncn_operator_state: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[ncn_warmup_operator( + &jito_restaking_program::id(), + config, + ncn, + operator_pubkey, + ncn_operator_state, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn do_operator_warmup_ncn( + &mut self, + operator_root: &OperatorRoot, + ncn_pubkey: &Pubkey, + ) -> TestResult<()> { + self.operator_warmup_ncn( + &Config::find_program_address(&jito_restaking_program::id()).0, + ncn_pubkey, + &operator_root.operator_pubkey, + &NcnOperatorState::find_program_address( + &jito_restaking_program::id(), + ncn_pubkey, + &operator_root.operator_pubkey, + ) + .0, + &operator_root.operator_admin, + ) + .await + } + + pub async fn operator_warmup_ncn( + &mut self, + config: &Pubkey, + ncn_pubkey: &Pubkey, + operator_pubkey: &Pubkey, + ncn_operator_state: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[operator_warmup_ncn( + &jito_restaking_program::id(), + config, + ncn_pubkey, + operator_pubkey, + ncn_operator_state, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn do_operator_cooldown_ncn( + &mut self, + operator_root: &OperatorRoot, + ncn_pubkey: &Pubkey, + ) -> TestResult<()> { + self.operator_cooldown_ncn( + &Config::find_program_address(&jito_restaking_program::id()).0, + ncn_pubkey, + &operator_root.operator_pubkey, + &NcnOperatorState::find_program_address( + &jito_restaking_program::id(), + ncn_pubkey, + &operator_root.operator_pubkey, + ) + .0, + &operator_root.operator_admin, + ) + .await + } + + pub async fn operator_cooldown_ncn( + &mut self, + config: &Pubkey, + ncn_pubkey: &Pubkey, + operator_pubkey: &Pubkey, + ncn_operator_state: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[operator_cooldown_ncn( + &jito_restaking_program::id(), + config, + ncn_pubkey, + operator_pubkey, + ncn_operator_state, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn do_initialize_ncn_operator_state( + &mut self, + ncn_root: &NcnRoot, + operator: &Pubkey, + ) -> TestResult<()> { + let ncn_operator_state = NcnOperatorState::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + operator, + ) + .0; + + self.initialize_ncn_operator_state( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + operator, + &ncn_operator_state, + &ncn_root.ncn_admin, + &self.payer.insecure_clone(), + ) + .await + } + + pub async fn do_initialize_ncn_vault_slasher_ticket( + &mut self, + ncn_root: &NcnRoot, + vault: &Pubkey, + slasher: &Pubkey, + max_slash_amount: u64, + ) -> TestResult<()> { + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + ) + .0; + let ncn_slasher_ticket = NcnVaultSlasherTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + slasher, + ) + .0; + + self.initialize_ncn_vault_slasher_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + vault, + slasher, + &ncn_vault_ticket, + &ncn_slasher_ticket, + &ncn_root.ncn_admin, + &self.payer.insecure_clone(), + max_slash_amount, + ) + .await + } + + pub async fn do_warmup_ncn_vault_slasher_ticket( + &mut self, + ncn_root: &NcnRoot, + vault: &Pubkey, + slasher: &Pubkey, + ) -> TestResult<()> { + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + ) + .0; + let ncn_slasher_ticket = NcnVaultSlasherTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + vault, + slasher, + ) + .0; + + self.warmup_ncn_vault_slasher_ticket( + &Config::find_program_address(&jito_restaking_program::id()).0, + &ncn_root.ncn_pubkey, + vault, + slasher, + &ncn_vault_ticket, + &ncn_slasher_ticket, + &ncn_root.ncn_admin, + ) + .await + } + + pub async fn warmup_ncn_vault_slasher_ticket( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + vault: &Pubkey, + slasher: &Pubkey, + ncn_vault_ticket: &Pubkey, + ncn_slasher_ticket: &Pubkey, + admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[warmup_ncn_vault_slasher_ticket( + &jito_restaking_program::id(), + config, + ncn, + vault, + slasher, + ncn_vault_ticket, + ncn_slasher_ticket, + &admin.pubkey(), + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + pub async fn initialize_ncn( &mut self, config: &Pubkey, @@ -129,4 +726,354 @@ impl RestakingProgramClient { )) .await } + + pub async fn initialize_ncn_vault_ticket( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + vault: &Pubkey, + ncn_vault_ticket: &Pubkey, + ncn_admin: &Keypair, + payer: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[initialize_ncn_vault_ticket( + &jito_restaking_program::id(), + config, + ncn, + vault, + ncn_vault_ticket, + &ncn_admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[ncn_admin, payer], + blockhash, + )) + .await + } + + pub async fn initialize_ncn_operator_state( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + operator: &Pubkey, + ncn_operator_state: &Pubkey, + ncn_admin: &Keypair, + payer: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[initialize_ncn_operator_state( + &jito_restaking_program::id(), + config, + ncn, + operator, + ncn_operator_state, + &ncn_admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[ncn_admin, payer], + blockhash, + )) + .await + } + + pub async fn initialize_ncn_vault_slasher_ticket( + &mut self, + config: &Pubkey, + ncn: &Pubkey, + vault: &Pubkey, + slasher: &Pubkey, + ncn_vault_ticket: &Pubkey, + ncn_slasher_ticket: &Pubkey, + ncn_admin: &Keypair, + payer: &Keypair, + max_slash_amount: u64, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[initialize_ncn_vault_slasher_ticket( + &jito_restaking_program::id(), + config, + ncn, + vault, + slasher, + ncn_vault_ticket, + ncn_slasher_ticket, + &ncn_admin.pubkey(), + &payer.pubkey(), + max_slash_amount, + )], + Some(&payer.pubkey()), + &[ncn_admin, payer], + blockhash, + )) + .await + } + + pub async fn ncn_set_admin( + &mut self, + ncn: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[ncn_set_admin( + &jito_restaking_program::id(), + ncn, + &old_admin.pubkey(), + &new_admin.pubkey(), + )], + Some(&old_admin.pubkey()), + &[old_admin, new_admin], + blockhash, + )) + .await + } + + pub async fn operator_set_admin( + &mut self, + operator: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[operator_set_admin( + &jito_restaking_program::id(), + operator, + &old_admin.pubkey(), + &new_admin.pubkey(), + )], + Some(&old_admin.pubkey()), + &[old_admin, new_admin], + blockhash, + )) + .await + } + + pub async fn operator_set_secondary_admin( + &mut self, + operator: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + operator_admin_role: OperatorAdminRole, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[operator_set_secondary_admin( + &jito_restaking_program::id(), + operator, + &old_admin.pubkey(), + &new_admin.pubkey(), + operator_admin_role, + )], + Some(&old_admin.pubkey()), + &[old_admin], + blockhash, + )) + .await + } + + pub async fn initialize_operator( + &mut self, + config: &Pubkey, + operator: &Pubkey, + admin: &Keypair, + base: &Keypair, + operator_fee_bps: u16, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[initialize_operator( + &jito_restaking_program::id(), + config, + operator, + &admin.pubkey(), + &base.pubkey(), + operator_fee_bps, + )], + Some(&admin.pubkey()), + &[admin, base], + blockhash, + )) + .await + } + + pub async fn initialize_operator_vault_ticket( + &mut self, + config: &Pubkey, + operator: &Pubkey, + vault: &Pubkey, + operator_vault_ticket: &Pubkey, + admin: &Keypair, + payer: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[initialize_operator_vault_ticket( + &jito_restaking_program::id(), + config, + operator, + vault, + operator_vault_ticket, + &admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[admin, payer], + blockhash, + )) + .await + } + + pub async fn operator_set_fee( + &mut self, + config: &Pubkey, + operator: &Pubkey, + admin: &Keypair, + new_fee_bps: u16, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self.process_transaction(&Transaction::new_signed_with_payer( + &[operator_set_fee( + &jito_restaking_program::id(), + config, + operator, + &admin.pubkey(), + new_fee_bps, + )], + Some(&self.payer.pubkey()), + &[admin, &self.payer], + blockhash, + )) + .await + } + + pub async fn ncn_delegate_token_account( + &mut self, + ncn_pubkey: &Pubkey, + delegate_admin: &Keypair, + token_mint: &Pubkey, + token_account: &Pubkey, + delegate: &Pubkey, + token_program_id: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[jito_restaking_sdk::sdk::ncn_delegate_token_account( + &jito_restaking_program::id(), + ncn_pubkey, + &delegate_admin.pubkey(), + token_mint, + token_account, + delegate, + token_program_id, + )], + Some(&self.payer.pubkey()), + &[&self.payer, delegate_admin], + blockhash, + )) + .await + } + + pub async fn operator_delegate_token_account( + &mut self, + operator_pubkey: &Pubkey, + delegate_admin: &Keypair, + token_mint: &Pubkey, + token_account: &Pubkey, + delegate: &Pubkey, + token_program_id: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[jito_restaking_sdk::sdk::operator_delegate_token_account( + &jito_restaking_program::id(), + operator_pubkey, + &delegate_admin.pubkey(), + token_mint, + token_account, + delegate, + token_program_id, + )], + Some(&self.payer.pubkey()), + &[&self.payer, delegate_admin], + blockhash, + )) + .await + } + + pub async fn process_transaction(&mut self, tx: &Transaction) -> TestResult<()> { + self.banks_client + .process_transaction_with_preflight_and_commitment( + tx.clone(), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn airdrop(&mut self, to: &Pubkey, sol: f64) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[transfer(&self.payer.pubkey(), to, sol_to_lamports(sol))], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn set_config_admin( + &mut self, + config: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.process_transaction(&Transaction::new_signed_with_payer( + &[set_config_admin( + &jito_restaking_program::id(), + config, + &old_admin.pubkey(), + &new_admin.pubkey(), + )], + Some(&old_admin.pubkey()), + &[old_admin], + blockhash, + )) + .await + } +} + +#[track_caller] +#[inline(always)] +pub fn assert_restaking_error( + test_error: Result, + restaking_error: RestakingError, +) { + assert!(test_error.is_err()); + assert_eq!( + test_error.err().unwrap().to_transaction_error().unwrap(), + TransactionError::InstructionError(0, InstructionError::Custom(restaking_error as u32)) + ); } diff --git a/integration_tests/tests/fixtures/test_builder.rs b/integration_tests/tests/fixtures/test_builder.rs index 74545ef..4df80d4 100644 --- a/integration_tests/tests/fixtures/test_builder.rs +++ b/integration_tests/tests/fixtures/test_builder.rs @@ -6,6 +6,7 @@ use solana_program_test::{processor, BanksClientError, ProgramTest, ProgramTestC use super::{ restaking_client::{NcnRoot, RestakingProgramClient}, tip_router_client::TipRouterClient, + vault_client::VaultProgramClient, TestResult, }; @@ -72,6 +73,13 @@ impl TestBuilder { ) } + pub fn vault_client(&self) -> VaultProgramClient { + VaultProgramClient::new( + self.context.banks_client.clone(), + self.context.payer.insecure_clone(), + ) + } + pub async fn setup_ncn(&mut self) -> TestResult { let mut restaking_program_client = self.restaking_program_client(); diff --git a/integration_tests/tests/fixtures/vault_client.rs b/integration_tests/tests/fixtures/vault_client.rs index 06a9639..7f86a88 100644 --- a/integration_tests/tests/fixtures/vault_client.rs +++ b/integration_tests/tests/fixtures/vault_client.rs @@ -1,8 +1,48 @@ use std::{fmt, fmt::Debug}; -use solana_program::pubkey::Pubkey; -use solana_program_test::BanksClient; -use solana_sdk::signature::Keypair; +use borsh::BorshDeserialize; +use jito_bytemuck::AccountDeserialize; +use jito_restaking_core::{ + ncn_vault_slasher_ticket::NcnVaultSlasherTicket, ncn_vault_ticket::NcnVaultTicket, + operator_vault_ticket::OperatorVaultTicket, +}; +use jito_vault_core::{ + config::Config, vault::Vault, vault_ncn_slasher_operator_ticket::VaultNcnSlasherOperatorTicket, + vault_ncn_slasher_ticket::VaultNcnSlasherTicket, vault_ncn_ticket::VaultNcnTicket, + vault_operator_delegation::VaultOperatorDelegation, + vault_staker_withdrawal_ticket::VaultStakerWithdrawalTicket, + vault_update_state_tracker::VaultUpdateStateTracker, +}; +use jito_vault_sdk::{ + error::VaultError, + instruction::{VaultAdminRole, WithdrawalAllocationMethod}, + sdk::{ + add_delegation, cooldown_delegation, initialize_config, initialize_vault, + set_deposit_capacity, warmup_vault_ncn_slasher_ticket, warmup_vault_ncn_ticket, + }, +}; +use log::info; +use solana_program::{ + clock::Clock, + native_token::sol_to_lamports, + program_pack::Pack, + pubkey::Pubkey, + rent::Rent, + system_instruction::{create_account, transfer}, +}; +use solana_program_test::{BanksClient, BanksClientError}; +use solana_sdk::{ + commitment_config::CommitmentLevel, + instruction::InstructionError, + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, +}; +use spl_associated_token_account::{ + get_associated_token_address, instruction::create_associated_token_account_idempotent, +}; +use spl_token::state::Account as SPLTokenAccount; + +use crate::fixtures::{TestError, TestResult}; pub struct VaultRoot { pub vault_pubkey: Pubkey, @@ -19,7 +59,11 @@ impl Debug for VaultRoot { } } -#[allow(dead_code)] +#[derive(Debug)] +pub struct VaultStakerWithdrawalTicketRoot { + pub base: Pubkey, +} + pub struct VaultProgramClient { banks_client: BanksClient, payer: Keypair, @@ -32,4 +76,1602 @@ impl VaultProgramClient { payer, } } + + pub async fn configure_depositor( + &mut self, + vault_root: &VaultRoot, + depositor: &Pubkey, + amount_to_mint: u64, + ) -> TestResult<()> { + self.airdrop(depositor, 100.0).await?; + let vault = self.get_vault(&vault_root.vault_pubkey).await?; + self.create_ata(&vault.supported_mint, depositor).await?; + self.create_ata(&vault.vrt_mint, depositor).await?; + self.mint_spl_to(&vault.supported_mint, depositor, amount_to_mint) + .await?; + + Ok(()) + } + + pub async fn get_config(&mut self, account: &Pubkey) -> Result { + let account = self.banks_client.get_account(*account).await?.unwrap(); + Ok(*Config::try_from_slice_unchecked(account.data.as_slice())?) + } + + pub async fn get_vault(&mut self, account: &Pubkey) -> Result { + let account = self.banks_client.get_account(*account).await?.unwrap(); + Ok(*Vault::try_from_slice_unchecked(account.data.as_slice())?) + } + + pub async fn get_vault_ncn_ticket( + &mut self, + vault: &Pubkey, + ncn: &Pubkey, + ) -> Result { + let account = VaultNcnTicket::find_program_address(&jito_vault_program::id(), vault, ncn).0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*VaultNcnTicket::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_vault_operator_delegation( + &mut self, + vault: &Pubkey, + operator: &Pubkey, + ) -> Result { + let account = VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + vault, + operator, + ) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*VaultOperatorDelegation::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_vault_staker_withdrawal_ticket( + &mut self, + vault: &Pubkey, + staker: &Pubkey, + base: &Pubkey, + ) -> Result { + let account = VaultStakerWithdrawalTicket::find_program_address( + &jito_vault_program::id(), + vault, + base, + ) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + let withdrawal_ticket = + *VaultStakerWithdrawalTicket::try_from_slice_unchecked(account.data.as_slice())?; + assert_eq!(withdrawal_ticket.staker, *staker); + Ok(withdrawal_ticket) + } + + pub async fn get_vault_ncn_slasher_ticket( + &mut self, + vault: &Pubkey, + ncn: &Pubkey, + slasher: &Pubkey, + ) -> Result { + let account = VaultNcnSlasherTicket::find_program_address( + &jito_vault_program::id(), + vault, + ncn, + slasher, + ) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*VaultNcnSlasherTicket::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + #[allow(dead_code)] + pub async fn get_vault_ncn_slasher_operator_ticket( + &mut self, + vault: &Pubkey, + ncn: &Pubkey, + slasher: &Pubkey, + operator: &Pubkey, + epoch: u64, + ) -> Result { + let account = VaultNcnSlasherOperatorTicket::find_program_address( + &jito_vault_program::id(), + vault, + ncn, + slasher, + operator, + epoch, + ) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*VaultNcnSlasherOperatorTicket::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn get_vault_update_state_tracker( + &mut self, + vault: &Pubkey, + epoch: u64, + ) -> Result { + let account = + VaultUpdateStateTracker::find_program_address(&jito_vault_program::id(), vault, epoch) + .0; + let account = self.banks_client.get_account(account).await?.unwrap(); + Ok(*VaultUpdateStateTracker::try_from_slice_unchecked( + account.data.as_slice(), + )?) + } + + pub async fn do_initialize_config(&mut self) -> Result { + let config_admin = Keypair::new(); + + self.airdrop(&config_admin.pubkey(), 1.0).await?; + + let config_pubkey = Config::find_program_address(&jito_vault_program::id()).0; + self.initialize_config(&config_pubkey, &config_admin, &config_admin.pubkey(), 0) + .await?; + + Ok(config_admin) + } + + pub async fn initialize_config( + &mut self, + config: &Pubkey, + config_admin: &Keypair, + program_fee_wallet: &Pubkey, + program_fee_bps: u16, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[initialize_config( + &jito_vault_program::id(), + config, + &config_admin.pubkey(), + &jito_restaking_program::id(), + program_fee_wallet, + program_fee_bps, + )], + Some(&config_admin.pubkey()), + &[config_admin], + blockhash, + )) + .await + } + + pub async fn setup_config_and_vault( + &mut self, + deposit_fee_bps: u16, + withdrawal_fee_bps: u16, + reward_fee_bps: u16, + ) -> Result<(Keypair, VaultRoot), TestError> { + let config_admin = self.do_initialize_config().await?; + let vault_root = self + .do_initialize_vault( + deposit_fee_bps, + withdrawal_fee_bps, + reward_fee_bps, + 9, + &config_admin.pubkey(), + ) + .await?; + + Ok((config_admin, vault_root)) + } + + pub async fn do_initialize_vault( + &mut self, + deposit_fee_bps: u16, + withdrawal_fee_bps: u16, + reward_fee_bps: u16, + decimals: u8, + program_fee_wallet: &Pubkey, + ) -> Result { + let vault_base = Keypair::new(); + + let vault_pubkey = + Vault::find_program_address(&jito_vault_program::id(), &vault_base.pubkey()).0; + + let vrt_mint = Keypair::new(); + let vault_admin = Keypair::new(); + let token_mint = Keypair::new(); + + self.airdrop(&vault_admin.pubkey(), 100.0).await?; + self.create_token_mint(&token_mint, &spl_token::id()) + .await?; + + self.initialize_vault( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_pubkey, + &vrt_mint, + &token_mint, + &vault_admin, + &vault_base, + deposit_fee_bps, + withdrawal_fee_bps, + reward_fee_bps, + decimals, + ) + .await?; + + // for holding the backed asset in the vault + self.create_ata(&token_mint.pubkey(), &vault_pubkey).await?; + // for holding fees + self.create_ata(&vrt_mint.pubkey(), &vault_admin.pubkey()) + .await?; + // for holding program fee + self.create_ata(&vrt_mint.pubkey(), program_fee_wallet) + .await?; + + // for holding program fee + Ok(VaultRoot { + vault_admin, + vault_pubkey, + }) + } + + pub async fn do_initialize_vault_ncn_ticket( + &mut self, + vault_root: &VaultRoot, + ncn: &Pubkey, + ) -> Result<(), TestError> { + let vault_ncn_ticket = VaultNcnTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn, + ) + .0; + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + ncn, + &vault_root.vault_pubkey, + ) + .0; + self.initialize_vault_ncn_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + ncn, + &ncn_vault_ticket, + &vault_ncn_ticket, + &vault_root.vault_admin, + &self.payer.insecure_clone(), + ) + .await?; + + Ok(()) + } + + pub async fn set_capacity( + &mut self, + config: &Pubkey, + vault: &Pubkey, + admin: &Keypair, + capacity: u64, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[set_deposit_capacity( + &jito_vault_program::id(), + config, + vault, + &admin.pubkey(), + capacity, + )], + Some(&admin.pubkey()), + &[&admin], + blockhash, + )) + .await + } + + pub async fn do_warmup_vault_ncn_ticket( + &mut self, + vault_root: &VaultRoot, + ncn: &Pubkey, + ) -> Result<(), TestError> { + let vault_ncn_ticket = VaultNcnTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn, + ) + .0; + + self.warmup_vault_ncn_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + ncn, + &vault_ncn_ticket, + &vault_root.vault_admin, + ) + .await?; + + Ok(()) + } + + pub async fn warmup_vault_ncn_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + ncn: &Pubkey, + vault_ncn_ticket: &Pubkey, + ncn_vault_admin: &Keypair, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[warmup_vault_ncn_ticket( + &jito_vault_program::id(), + config, + vault, + ncn, + vault_ncn_ticket, + &ncn_vault_admin.pubkey(), + )], + Some(&ncn_vault_admin.pubkey()), + &[&ncn_vault_admin], + blockhash, + )) + .await + } + + #[allow(dead_code)] + pub async fn setup_vault_ncn_slasher_operator_ticket( + &mut self, + vault_root: &VaultRoot, + ncn_pubkey: &Pubkey, + slasher: &Pubkey, + operator_pubkey: &Pubkey, + ) -> Result<(), TestError> { + let config = self + .get_config(&Config::find_program_address(&jito_vault_program::id()).0) + .await + .unwrap(); + let clock: Clock = self.banks_client.get_sysvar().await?; + + let vault_ncn_slasher_ticket = VaultNcnSlasherTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + ) + .0; + let vault_ncn_slasher_operator_ticket = + VaultNcnSlasherOperatorTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + operator_pubkey, + clock.slot / config.epoch_length(), + ) + .0; + self.initialize_vault_ncn_slasher_operator_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + operator_pubkey, + &vault_ncn_slasher_ticket, + &vault_ncn_slasher_operator_ticket, + &self.payer.insecure_clone(), + ) + .await + .unwrap(); + + Ok(()) + } + + pub async fn do_initialize_vault_operator_delegation( + &mut self, + vault_root: &VaultRoot, + operator_pubkey: &Pubkey, + ) -> Result<(), TestError> { + let vault_operator_delegation = VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + operator_pubkey, + ) + .0; + let operator_vault_ticket = OperatorVaultTicket::find_program_address( + &jito_restaking_program::id(), + operator_pubkey, + &vault_root.vault_pubkey, + ) + .0; + self.initialize_vault_operator_delegation( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + operator_pubkey, + &operator_vault_ticket, + &vault_operator_delegation, + &vault_root.vault_admin, + &vault_root.vault_admin, + ) + .await?; + + Ok(()) + } + + pub async fn do_initialize_vault_ncn_slasher_ticket( + &mut self, + vault_root: &VaultRoot, + ncn_pubkey: &Pubkey, + slasher: &Pubkey, + ) -> Result<(), TestError> { + let vault_slasher_ticket_pubkey = VaultNcnSlasherTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + ) + .0; + let ncn_slasher_ticket_pubkey = NcnVaultSlasherTicket::find_program_address( + &jito_restaking_program::id(), + ncn_pubkey, + &vault_root.vault_pubkey, + slasher, + ) + .0; + + self.initialize_vault_ncn_slasher_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + &ncn_slasher_ticket_pubkey, + &vault_slasher_ticket_pubkey, + &vault_root.vault_admin, + &vault_root.vault_admin, + ) + .await?; + + Ok(()) + } + + pub async fn do_warmup_vault_ncn_slasher_ticket( + &mut self, + vault_root: &VaultRoot, + ncn_pubkey: &Pubkey, + slasher: &Pubkey, + ) -> Result<(), TestError> { + let vault_slasher_ticket_pubkey = VaultNcnSlasherTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + ) + .0; + + self.warmup_vault_ncn_slasher_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + ncn_pubkey, + slasher, + &vault_slasher_ticket_pubkey, + &vault_root.vault_admin, + ) + .await?; + + Ok(()) + } + + pub async fn warmup_vault_ncn_slasher_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + ncn: &Pubkey, + slasher: &Pubkey, + vault_ncn_slasher_ticket: &Pubkey, + admin: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[warmup_vault_ncn_slasher_ticket( + &jito_vault_program::id(), + config, + vault, + ncn, + slasher, + vault_ncn_slasher_ticket, + &admin.pubkey(), + )], + Some(&admin.pubkey()), + &[admin], + blockhash, + )) + .await + } + + pub async fn do_add_delegation( + &mut self, + vault_root: &VaultRoot, + operator: &Pubkey, + amount: u64, + ) -> Result<(), TestError> { + self.add_delegation( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + operator, + &VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + operator, + ) + .0, + &vault_root.vault_admin, + amount, + ) + .await?; + + Ok(()) + } + + pub async fn initialize_vault( + &mut self, + config: &Pubkey, + vault: &Pubkey, + vrt_mint: &Keypair, + token_mint: &Keypair, + vault_admin: &Keypair, + vault_base: &Keypair, + deposit_fee_bps: u16, + withdrawal_fee_bps: u16, + reward_fee_bps: u16, + decimals: u8, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[initialize_vault( + &jito_vault_program::id(), + config, + vault, + &vrt_mint.pubkey(), + &token_mint.pubkey(), + &vault_admin.pubkey(), + &vault_base.pubkey(), + deposit_fee_bps, + withdrawal_fee_bps, + reward_fee_bps, + decimals, + )], + Some(&vault_admin.pubkey()), + &[&vault_admin, &vrt_mint, &vault_base], + blockhash, + )) + .await + } + + pub async fn initialize_vault_ncn_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + ncn: &Pubkey, + ncn_vault_ticket: &Pubkey, + vault_ncn_ticket: &Pubkey, + admin: &Keypair, + payer: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::initialize_vault_ncn_ticket( + &jito_vault_program::id(), + config, + vault, + ncn, + ncn_vault_ticket, + vault_ncn_ticket, + &admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[admin, payer], + blockhash, + )) + .await + } + + pub async fn initialize_vault_operator_delegation( + &mut self, + config: &Pubkey, + vault: &Pubkey, + operator: &Pubkey, + operator_vault_ticket: &Pubkey, + vault_operator_delegation: &Pubkey, + admin: &Keypair, + payer: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::initialize_vault_operator_delegation( + &jito_vault_program::id(), + config, + vault, + operator, + operator_vault_ticket, + vault_operator_delegation, + &admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[admin, payer], + blockhash, + )) + .await + } + + pub async fn delegate_token_account( + &mut self, + config: &Pubkey, + vault: &Pubkey, + delegate_asset_admin: &Keypair, + token_mint: &Pubkey, + token_account: &Pubkey, + delegate: &Pubkey, + token_program_id: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::delegate_token_account( + &jito_vault_program::id(), + config, + vault, + &delegate_asset_admin.pubkey(), + token_mint, + token_account, + delegate, + token_program_id, + )], + Some(&self.payer.pubkey()), + &[&self.payer, delegate_asset_admin], + blockhash, + )) + .await + } + + pub async fn set_admin( + &mut self, + config: &Pubkey, + vault: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_admin( + &jito_vault_program::id(), + config, + vault, + &old_admin.pubkey(), + &new_admin.pubkey(), + )], + Some(&old_admin.pubkey()), + &[old_admin, new_admin], + blockhash, + )) + .await + } + + pub async fn set_secondary_admin( + &mut self, + config: &Pubkey, + vault: &Pubkey, + admin: &Keypair, + new_admin: &Pubkey, + role: VaultAdminRole, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_secondary_admin( + &jito_vault_program::id(), + config, + vault, + &admin.pubkey(), + new_admin, + role, + )], + Some(&admin.pubkey()), + &[admin], + blockhash, + )) + .await + } + + pub async fn set_fees( + &mut self, + config: &Pubkey, + vault: &Pubkey, + fee_admin: &Keypair, + deposit_fee_bps: Option, + withdrawal_fee_bps: Option, + reward_fee_bps: Option, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_fees( + &jito_vault_program::id(), + config, + vault, + &fee_admin.pubkey(), + deposit_fee_bps, + withdrawal_fee_bps, + reward_fee_bps, + )], + Some(&fee_admin.pubkey()), + &[fee_admin], + blockhash, + )) + .await + } + + pub async fn set_program_fee( + &mut self, + config_admin: &Keypair, + new_fee_bps: u16, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_program_fee( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + &config_admin.pubkey(), + new_fee_bps, + )], + Some(&config_admin.pubkey()), + &[config_admin], + blockhash, + )) + .await + } + + pub async fn do_enqueue_withdrawal( + &mut self, + vault_root: &VaultRoot, + depositor: &Keypair, + amount: u64, + ) -> Result { + let vault = self.get_vault(&vault_root.vault_pubkey).await.unwrap(); + let depositor_vrt_token_account = + get_associated_token_address(&depositor.pubkey(), &vault.vrt_mint); + + let base = Keypair::new(); + let vault_staker_withdrawal_ticket = VaultStakerWithdrawalTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + &base.pubkey(), + ) + .0; + info!( + "vault_staker_withdrawal_ticket: {:?}", + vault_staker_withdrawal_ticket + ); + let vault_staker_withdrawal_ticket_token_account = + get_associated_token_address(&vault_staker_withdrawal_ticket, &vault.vrt_mint); + + self.create_ata(&vault.vrt_mint, &vault_staker_withdrawal_ticket) + .await?; + + self.enqueue_withdrawal( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + &vault_staker_withdrawal_ticket, + &vault_staker_withdrawal_ticket_token_account, + depositor, + &depositor_vrt_token_account, + &base, + amount, + ) + .await?; + + Ok(VaultStakerWithdrawalTicketRoot { + base: base.pubkey(), + }) + } + + pub async fn do_cooldown_delegation( + &mut self, + vault_root: &VaultRoot, + operator: &Pubkey, + amount: u64, + ) -> TestResult<()> { + self.cooldown_delegation( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + operator, + &VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + operator, + ) + .0, + &vault_root.vault_admin, + amount, + ) + .await + } + + pub async fn cooldown_delegation( + &mut self, + config: &Pubkey, + vault: &Pubkey, + operator: &Pubkey, + vault_operator_delegation: &Pubkey, + admin: &Keypair, + amount: u64, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[cooldown_delegation( + &jito_vault_program::id(), + config, + vault, + operator, + vault_operator_delegation, + &admin.pubkey(), + amount, + )], + Some(&self.payer.pubkey()), + &[&self.payer, admin], + blockhash, + )) + .await + } + + pub async fn do_full_vault_update( + &mut self, + vault_pubkey: &Pubkey, + operators: &[Pubkey], + ) -> Result<(), TestError> { + let slot = self.banks_client.get_sysvar::().await?.slot; + + let config = self + .get_config(&Config::find_program_address(&jito_vault_program::id()).0) + .await?; + + let ncn_epoch = slot / config.epoch_length(); + + let vault_update_state_tracker = VaultUpdateStateTracker::find_program_address( + &jito_vault_program::id(), + vault_pubkey, + ncn_epoch, + ) + .0; + self.initialize_vault_update_state_tracker(vault_pubkey, &vault_update_state_tracker) + .await?; + + for i in 0..operators.len() { + let operator_index = (i + ncn_epoch as usize) % operators.len(); + let operator = &operators[operator_index]; + self.crank_vault_update_state_tracker( + vault_pubkey, + operator, + &VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + vault_pubkey, + operator, + ) + .0, + &vault_update_state_tracker, + ) + .await?; + } + + self.close_vault_update_state_tracker( + vault_pubkey, + &vault_update_state_tracker, + slot / config.epoch_length(), + ) + .await?; + + self.update_vault_balance(vault_pubkey).await?; + + Ok(()) + } + + pub async fn do_crank_vault_update_state_tracker( + &mut self, + vault: &Pubkey, + operator: &Pubkey, + ) -> TestResult<()> { + let slot = self.banks_client.get_sysvar::().await?.slot; + let config = self + .get_config(&Config::find_program_address(&jito_vault_program::id()).0) + .await?; + let ncn_epoch = slot / config.epoch_length(); + self.crank_vault_update_state_tracker( + vault, + operator, + &VaultOperatorDelegation::find_program_address( + &jito_vault_program::id(), + vault, + operator, + ) + .0, + &VaultUpdateStateTracker::find_program_address( + &jito_vault_program::id(), + vault, + ncn_epoch, + ) + .0, + ) + .await + } + + pub async fn crank_vault_update_state_tracker( + &mut self, + vault: &Pubkey, + operator: &Pubkey, + vault_operator_delegation: &Pubkey, + vault_update_state_tracker: &Pubkey, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::crank_vault_update_state_tracker( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault, + operator, + vault_operator_delegation, + vault_update_state_tracker, + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await?; + Ok(()) + } + + pub async fn update_vault_balance(&mut self, vault_pubkey: &Pubkey) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + let vault = self.get_vault(vault_pubkey).await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::update_vault_balance( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault_pubkey, + &get_associated_token_address(vault_pubkey, &vault.supported_mint), + &vault.vrt_mint, + &get_associated_token_address(&vault.fee_wallet, &vault.vrt_mint), + &spl_token::ID, + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await?; + + Ok(()) + } + + pub async fn initialize_vault_update_state_tracker( + &mut self, + vault_pubkey: &Pubkey, + vault_update_state_tracker: &Pubkey, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::initialize_vault_update_state_tracker( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault_pubkey, + vault_update_state_tracker, + &self.payer.pubkey(), + WithdrawalAllocationMethod::Greedy, + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await?; + Ok(()) + } + + pub async fn close_vault_update_state_tracker( + &mut self, + vault_pubkey: &Pubkey, + vault_update_state_tracker: &Pubkey, + ncn_epoch: u64, + ) -> TestResult<()> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::close_vault_update_state_tracker( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault_pubkey, + vault_update_state_tracker, + &self.payer.pubkey(), + ncn_epoch, + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn enqueue_withdrawal( + &mut self, + config: &Pubkey, + vault: &Pubkey, + vault_staker_withdrawal_ticket: &Pubkey, + vault_staker_withdrawal_ticket_token_account: &Pubkey, + staker: &Keypair, + staker_vrt_token_account: &Pubkey, + base: &Keypair, + amount: u64, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::enqueue_withdrawal( + &jito_vault_program::id(), + config, + vault, + vault_staker_withdrawal_ticket, + vault_staker_withdrawal_ticket_token_account, + &staker.pubkey(), + staker_vrt_token_account, + &base.pubkey(), + amount, + )], + Some(&staker.pubkey()), + &[staker, base], + blockhash, + )) + .await + } + + pub async fn do_burn_withdrawal_ticket( + &mut self, + vault_root: &VaultRoot, + staker: &Keypair, + vault_staker_withdrawal_ticket_base: &Pubkey, + program_fee_wallet: &Pubkey, + ) -> Result<(), TestError> { + let vault = self.get_vault(&vault_root.vault_pubkey).await.unwrap(); + let vault_staker_withdrawal_ticket = VaultStakerWithdrawalTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + vault_staker_withdrawal_ticket_base, + ) + .0; + + self.burn_withdrawal_ticket( + &Config::find_program_address(&jito_vault_program::id()).0, + &vault_root.vault_pubkey, + &get_associated_token_address(&vault_root.vault_pubkey, &vault.supported_mint), + &vault.vrt_mint, + &staker.pubkey(), + &get_associated_token_address(&staker.pubkey(), &vault.supported_mint), + &vault_staker_withdrawal_ticket, + &get_associated_token_address(&vault_staker_withdrawal_ticket, &vault.vrt_mint), + &get_associated_token_address(&vault.fee_wallet, &vault.vrt_mint), + &get_associated_token_address(program_fee_wallet, &vault.vrt_mint), + ) + .await?; + + Ok(()) + } + + pub async fn burn_withdrawal_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + vault_token_account: &Pubkey, + vrt_mint: &Pubkey, + staker: &Pubkey, + staker_token_account: &Pubkey, + vault_staker_withdrawal_ticket: &Pubkey, + vault_staker_withdrawal_ticket_token_account: &Pubkey, + vault_fee_token_account: &Pubkey, + program_fee_vrt_token_account: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::burn_withdrawal_ticket( + &jito_vault_program::id(), + config, + vault, + vault_token_account, + vrt_mint, + staker, + staker_token_account, + vault_staker_withdrawal_ticket, + vault_staker_withdrawal_ticket_token_account, + vault_fee_token_account, + program_fee_vrt_token_account, + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + )) + .await + } + + pub async fn add_delegation( + &mut self, + config: &Pubkey, + vault: &Pubkey, + operator: &Pubkey, + vault_operator_delegation: &Pubkey, + admin: &Keypair, + amount: u64, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[add_delegation( + &jito_vault_program::id(), + config, + vault, + operator, + vault_operator_delegation, + &admin.pubkey(), + amount, + )], + Some(&admin.pubkey()), + &[admin], + blockhash, + )) + .await + } + + pub async fn do_mint_to( + &mut self, + vault_root: &VaultRoot, + depositor: &Keypair, + amount_in: u64, + min_amount_out: u64, + ) -> TestResult<()> { + let vault = self.get_vault(&vault_root.vault_pubkey).await.unwrap(); + self.mint_to( + &vault_root.vault_pubkey, + &vault.vrt_mint, + depositor, + &get_associated_token_address(&depositor.pubkey(), &vault.supported_mint), + &get_associated_token_address(&vault_root.vault_pubkey, &vault.supported_mint), + &get_associated_token_address(&depositor.pubkey(), &vault.vrt_mint), + &get_associated_token_address(&vault.fee_wallet, &vault.vrt_mint), + None, + amount_in, + min_amount_out, + ) + .await + } + + pub async fn mint_to( + &mut self, + vault: &Pubkey, + vrt_mint: &Pubkey, + depositor: &Keypair, + depositor_token_account: &Pubkey, + vault_token_account: &Pubkey, + depositor_vrt_token_account: &Pubkey, + vault_fee_token_account: &Pubkey, + mint_signer: Option<&Keypair>, + amount_in: u64, + min_amount_out: u64, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + let mut signers = vec![depositor]; + if let Some(signer) = mint_signer { + signers.push(signer); + } + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::mint_to( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault, + vrt_mint, + &depositor.pubkey(), + depositor_token_account, + vault_token_account, + depositor_vrt_token_account, + vault_fee_token_account, + mint_signer.map(|s| s.pubkey()).as_ref(), + amount_in, + min_amount_out, + )], + Some(&depositor.pubkey()), + &signers, + blockhash, + )) + .await + } + + pub async fn initialize_vault_ncn_slasher_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + ncn: &Pubkey, + slasher: &Pubkey, + ncn_slasher_ticket: &Pubkey, + vault_slasher_ticket: &Pubkey, + admin: &Keypair, + payer: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::initialize_vault_ncn_slasher_ticket( + &jito_vault_program::id(), + config, + vault, + ncn, + slasher, + ncn_slasher_ticket, + vault_slasher_ticket, + &admin.pubkey(), + &payer.pubkey(), + )], + Some(&payer.pubkey()), + &[admin, payer], + blockhash, + )) + .await + } + + #[allow(dead_code)] + pub async fn initialize_vault_ncn_slasher_operator_ticket( + &mut self, + config: &Pubkey, + vault: &Pubkey, + ncn: &Pubkey, + slasher: &Pubkey, + operator: &Pubkey, + vault_ncn_slasher_ticket: &Pubkey, + vault_ncn_slasher_operator_ticket: &Pubkey, + payer: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[ + jito_vault_sdk::sdk::initialize_vault_ncn_slasher_operator_ticket( + &jito_vault_program::id(), + config, + vault, + ncn, + slasher, + operator, + vault_ncn_slasher_ticket, + vault_ncn_slasher_operator_ticket, + &payer.pubkey(), + ), + ], + Some(&payer.pubkey()), + &[payer], + blockhash, + )) + .await + } + + pub async fn create_token_metadata( + &mut self, + vault: &Pubkey, + admin: &Keypair, + vrt_mint: &Pubkey, + payer: &Keypair, + metadata: &Pubkey, + name: String, + symbol: String, + uri: String, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::create_token_metadata( + &jito_vault_program::id(), + vault, + &admin.pubkey(), + vrt_mint, + &payer.pubkey(), + metadata, + name, + symbol, + uri, + )], + Some(&payer.pubkey()), + &[admin, payer], + blockhash, + )) + .await + } + + pub async fn update_token_metadata( + &mut self, + vault: &Pubkey, + admin: &Keypair, + vrt_mint: &Pubkey, + metadata: &Pubkey, + name: String, + symbol: String, + uri: String, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::update_token_metadata( + &jito_vault_program::id(), + vault, + &admin.pubkey(), + vrt_mint, + metadata, + name, + symbol, + uri, + )], + Some(&self.payer.pubkey()), + &[&self.payer, admin], + blockhash, + )) + .await + } + + async fn _process_transaction(&mut self, tx: &Transaction) -> Result<(), TestError> { + self.banks_client + .process_transaction_with_preflight_and_commitment( + tx.clone(), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn airdrop(&mut self, to: &Pubkey, sol: f64) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[transfer(&self.payer.pubkey(), to, sol_to_lamports(sol))], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn create_token_mint( + &mut self, + mint: &Keypair, + token_program_id: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + let rent: Rent = self.banks_client.get_sysvar().await?; + let ixs = vec![ + create_account( + &self.payer.pubkey(), + &mint.pubkey(), + rent.minimum_balance(spl_token::state::Mint::LEN), + spl_token::state::Mint::LEN as u64, + token_program_id, + ), + spl_token::instruction::initialize_mint2( + token_program_id, + &mint.pubkey(), + &self.payer.pubkey(), + None, + 9, + ) + .unwrap(), + ]; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &ixs, + Some(&self.payer.pubkey()), + &[&self.payer, mint], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + pub async fn create_ata(&mut self, mint: &Pubkey, owner: &Pubkey) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[create_associated_token_account_idempotent( + &self.payer.pubkey(), + owner, + mint, + &spl_token::id(), + )], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await?; + Ok(()) + } + + /// Mints tokens to an ATA owned by the `to` address + pub async fn mint_spl_to( + &mut self, + mint: &Pubkey, + to: &Pubkey, + amount: u64, + ) -> Result<(), BanksClientError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[ + create_associated_token_account_idempotent( + &self.payer.pubkey(), + to, + mint, + &spl_token::id(), + ), + spl_token::instruction::mint_to( + &spl_token::id(), + mint, + &get_associated_token_address(to, mint), + &self.payer.pubkey(), + &[], + amount, + ) + .unwrap(), + ], + Some(&self.payer.pubkey()), + &[&self.payer], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await + } + + pub async fn get_reward_fee_token_account( + &mut self, + vault: &Pubkey, + ) -> Result { + let vault = self.get_vault(vault).await.unwrap(); + + let vault_fee_token_account = + get_associated_token_address(&vault.fee_wallet, &vault.vrt_mint); + + let account = self + .banks_client + .get_account(vault_fee_token_account) + .await + .unwrap() + .unwrap(); + + Ok(SPLTokenAccount::unpack(&account.data).unwrap()) + } + + pub async fn create_and_fund_reward_vault( + &mut self, + vault: &Pubkey, + rewarder: &Keypair, + amount: u64, + ) -> Result<(), BanksClientError> { + let vault_account = self.get_vault(vault).await.unwrap(); + + let rewarder_token_account = + get_associated_token_address(&rewarder.pubkey(), &vault_account.supported_mint); + + let vault_token_account = + get_associated_token_address(vault, &vault_account.supported_mint); + + let blockhash = self.banks_client.get_latest_blockhash().await?; + self.banks_client + .process_transaction_with_preflight_and_commitment( + Transaction::new_signed_with_payer( + &[ + create_associated_token_account_idempotent( + &rewarder.pubkey(), + &vault_token_account, + &vault_account.supported_mint, + &spl_token::id(), + ), + spl_token::instruction::transfer( + &spl_token::id(), + &rewarder_token_account, + &vault_token_account, + &rewarder.pubkey(), + &[], + amount, + ) + .unwrap(), + ], + Some(&rewarder.pubkey()), + &[&rewarder], + blockhash, + ), + CommitmentLevel::Processed, + ) + .await + } + + pub async fn set_program_fee_wallet( + &mut self, + program_fee_admin: &Keypair, + new_fee_wallet: &Pubkey, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_program_fee_wallet( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + &program_fee_admin.pubkey(), + new_fee_wallet, + )], + Some(&program_fee_admin.pubkey()), + &[program_fee_admin], + blockhash, + )) + .await + } + + pub async fn set_is_paused( + &mut self, + vault: &Pubkey, + admin: &Keypair, + is_paused: bool, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_is_paused( + &jito_vault_program::id(), + &Config::find_program_address(&jito_vault_program::id()).0, + vault, + &admin.pubkey(), + is_paused, + )], + Some(&admin.pubkey()), + &[admin], + blockhash, + )) + .await + } + + pub async fn set_config_admin( + &mut self, + config: &Pubkey, + old_admin: &Keypair, + new_admin: &Keypair, + ) -> Result<(), TestError> { + let blockhash = self.banks_client.get_latest_blockhash().await?; + self._process_transaction(&Transaction::new_signed_with_payer( + &[jito_vault_sdk::sdk::set_config_admin( + &jito_vault_program::id(), + config, + &old_admin.pubkey(), + &new_admin.pubkey(), + )], + Some(&old_admin.pubkey()), + &[old_admin], + blockhash, + )) + .await + } +} + +#[inline(always)] +#[track_caller] +pub fn assert_vault_error(test_error: Result, vault_error: VaultError) { + assert!(test_error.is_err()); + assert_eq!( + test_error.err().unwrap().to_transaction_error().unwrap(), + TransactionError::InstructionError(0, InstructionError::Custom(vault_error as u32)) + ); } diff --git a/integration_tests/tests/tip_router/register_mint.rs b/integration_tests/tests/tip_router/register_mint.rs index 27c6c20..72d7381 100644 --- a/integration_tests/tests/tip_router/register_mint.rs +++ b/integration_tests/tests/tip_router/register_mint.rs @@ -1,5 +1,7 @@ #[cfg(test)] mod tests { + use jito_restaking_core::{config::Config, ncn_vault_ticket::NcnVaultTicket}; + use jito_vault_core::vault_ncn_ticket::VaultNcnTicket; use solana_sdk::{signature::Keypair, signer::Signer}; use crate::fixtures::{test_builder::TestBuilder, TestResult}; @@ -8,23 +10,61 @@ mod tests { async fn test_register_mint_success() -> TestResult<()> { let mut fixture = TestBuilder::new().await; let mut tip_router_client = fixture.tip_router_client(); + let mut vault_client = fixture.vault_client(); + let mut restaking_client = fixture.restaking_program_client(); let ncn_root = fixture.setup_ncn().await?; // Setup initial state tip_router_client.setup_tip_router(&ncn_root).await?; // Setup vault and tickets - let vault = Keypair::new(); - let vault_ncn_ticket = Keypair::new(); - let ncn_vault_ticket = Keypair::new(); + let _ = vault_client.do_initialize_config().await?; + let vault_root = vault_client + .do_initialize_vault(0, 0, 0, 9, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_initialize_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + vault_client + .do_initialize_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + + let vault = vault_root.vault_pubkey; + let vault_ncn_ticket = VaultNcnTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + &ncn_root.ncn_pubkey, + ) + .0; + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + &vault_root.vault_pubkey, + ) + .0; + + fixture.warp_slot_incremental(2).await?; + + vault_client + .do_warmup_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_warmup_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + let restaking_config_pubkey = Config::find_program_address(&jito_restaking_program::id()).0; + let epoch_length = restaking_client + .get_config(&restaking_config_pubkey) + .await? + .epoch_length(); + fixture.warp_slot_incremental(2 * epoch_length).await?; // Register mint tip_router_client .do_register_mint( ncn_root.ncn_pubkey, - vault.pubkey(), - vault_ncn_ticket.pubkey(), - ncn_vault_ticket.pubkey(), + vault, + vault_ncn_ticket, + ncn_vault_ticket, ) .await?; @@ -66,33 +106,73 @@ mod tests { async fn test_register_mint_duplicate() -> TestResult<()> { let mut fixture = TestBuilder::new().await; let mut tip_router_client = fixture.tip_router_client(); + let mut vault_client = fixture.vault_client(); + let mut restaking_client = fixture.restaking_program_client(); let ncn_root = fixture.setup_ncn().await?; // Setup initial state tip_router_client.setup_tip_router(&ncn_root).await?; // Setup vault and tickets - let vault = Keypair::new(); - let vault_ncn_ticket = Keypair::new(); - let ncn_vault_ticket = Keypair::new(); + let _ = vault_client.do_initialize_config().await?; + let vault_root = vault_client + .do_initialize_vault(0, 0, 0, 9, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_initialize_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + vault_client + .do_initialize_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + + let vault = vault_root.vault_pubkey; + let vault_ncn_ticket = VaultNcnTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + &ncn_root.ncn_pubkey, + ) + .0; + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + &vault_root.vault_pubkey, + ) + .0; + + fixture.warp_slot_incremental(2).await?; + + vault_client + .do_warmup_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_warmup_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + let restaking_config_pubkey = Config::find_program_address(&jito_restaking_program::id()).0; + let epoch_length = restaking_client + .get_config(&restaking_config_pubkey) + .await? + .epoch_length(); + fixture.warp_slot_incremental(2 * epoch_length).await?; // Register mint first time tip_router_client .do_register_mint( ncn_root.ncn_pubkey, - vault.pubkey(), - vault_ncn_ticket.pubkey(), - ncn_vault_ticket.pubkey(), + vault, + vault_ncn_ticket, + ncn_vault_ticket, ) .await?; + fixture.warp_slot_incremental(1).await?; + // Register same mint again tip_router_client .do_register_mint( ncn_root.ncn_pubkey, - vault.pubkey(), - vault_ncn_ticket.pubkey(), - ncn_vault_ticket.pubkey(), + vault, + vault_ncn_ticket, + ncn_vault_ticket, ) .await?; @@ -109,12 +189,75 @@ mod tests { async fn test_register_mint_fails_with_weight_table() -> TestResult<()> { let mut fixture = TestBuilder::new().await; let mut tip_router_client = fixture.tip_router_client(); + let mut vault_client = fixture.vault_client(); + let mut restaking_client = fixture.restaking_program_client(); let ncn_root = fixture.setup_ncn().await?; tip_router_client.setup_tip_router(&ncn_root).await?; - // TODO create ncn and vault with 1 mint, register mint, initialize weight table - // TODO verify weight table locks register_mint + let _ = vault_client.do_initialize_config().await?; + let vault_root = vault_client + .do_initialize_vault(0, 0, 0, 9, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_initialize_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + vault_client + .do_initialize_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + + let vault = vault_root.vault_pubkey; + let vault_ncn_ticket = VaultNcnTicket::find_program_address( + &jito_vault_program::id(), + &vault_root.vault_pubkey, + &ncn_root.ncn_pubkey, + ) + .0; + let ncn_vault_ticket = NcnVaultTicket::find_program_address( + &jito_restaking_program::id(), + &ncn_root.ncn_pubkey, + &vault_root.vault_pubkey, + ) + .0; + + fixture.warp_slot_incremental(2).await?; + + vault_client + .do_warmup_vault_ncn_ticket(&vault_root, &ncn_root.ncn_pubkey) + .await?; + restaking_client + .do_warmup_ncn_vault_ticket(&ncn_root, &vault_root.vault_pubkey) + .await?; + let restaking_config_pubkey = Config::find_program_address(&jito_restaking_program::id()).0; + let epoch_length = restaking_client + .get_config(&restaking_config_pubkey) + .await? + .epoch_length(); + fixture.warp_slot_incremental(2 * epoch_length).await?; + + tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault, + vault_ncn_ticket, + ncn_vault_ticket, + ) + .await?; + + tip_router_client + .initialize_weight_table(ncn_root.ncn_pubkey, fixture.clock().await.slot) + .await?; + + let result = tip_router_client + .do_register_mint( + ncn_root.ncn_pubkey, + vault, + vault_ncn_ticket, + ncn_vault_ticket, + ) + .await; + + assert!(result.is_err()); Ok(()) } diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs index 8fe2b0e..ae8fcd8 100644 --- a/program/src/register_mint.rs +++ b/program/src/register_mint.rs @@ -45,6 +45,7 @@ pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> P .checked_div(epoch_length) .ok_or(TipRouterError::DenominatorIsZero)?; + // TODO: is there a way to DOS this by changing program owner to tiprouter or something before weight table is initialized? // Once tracked_mints.mint_count() == ncn.vault_count, the weight table can be initialized // Once the weight table is initialized, you can't add any more mints if weight_table.owner.eq(&system_program::ID) { From cd8e91ddb2c45cd7a28650b77c444d5a7ff4c9b8 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 13 Nov 2024 14:49:31 -0500 Subject: [PATCH 7/9] last --- program/src/register_mint.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs index ae8fcd8..8fe2b0e 100644 --- a/program/src/register_mint.rs +++ b/program/src/register_mint.rs @@ -45,7 +45,6 @@ pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> P .checked_div(epoch_length) .ok_or(TipRouterError::DenominatorIsZero)?; - // TODO: is there a way to DOS this by changing program owner to tiprouter or something before weight table is initialized? // Once tracked_mints.mint_count() == ncn.vault_count, the weight table can be initialized // Once the weight table is initialized, you can't add any more mints if weight_table.owner.eq(&system_program::ID) { From 734fc4a5dd10207a5855477219e473277b313d8c Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 13 Nov 2024 16:03:04 -0500 Subject: [PATCH 8/9] pr --- core/src/tracked_mints.rs | 4 +++- program/src/register_mint.rs | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/src/tracked_mints.rs b/core/src/tracked_mints.rs index e1ea35c..16d9402 100644 --- a/core/src/tracked_mints.rs +++ b/core/src/tracked_mints.rs @@ -12,6 +12,7 @@ use crate::{discriminators::Discriminators, error::TipRouterError}; pub struct MintEntry { st_mint: Pubkey, vault_index: PodU64, + reserved: [u8; 32], } impl MintEntry { @@ -19,6 +20,7 @@ impl MintEntry { Self { st_mint: mint, vault_index: PodU64::from(vault_index), + reserved: [0; 32], } } @@ -38,7 +40,7 @@ impl Default for MintEntry { pub struct TrackedMints { pub ncn: Pubkey, pub bump: u8, - pub reserved: [u8; 7], + pub reserved: [u8; 7], // TODO extend to 127; figure out serde issue pub st_mint_list: [MintEntry; 16], // TODO extend to 64; figure out serde issue } diff --git a/program/src/register_mint.rs b/program/src/register_mint.rs index 8fe2b0e..c3b3f70 100644 --- a/program/src/register_mint.rs +++ b/program/src/register_mint.rs @@ -48,13 +48,17 @@ pub fn process_register_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> P // Once tracked_mints.mint_count() == ncn.vault_count, the weight table can be initialized // Once the weight table is initialized, you can't add any more mints if weight_table.owner.eq(&system_program::ID) { + let expected_pubkey = WeightTable::find_program_address(program_id, ncn.key, ncn_epoch).0; + if weight_table.key.ne(&expected_pubkey) { + msg!("Weight table incorrect PDA"); + return Err(ProgramError::InvalidAccountData); + } load_system_account(weight_table, false)?; - } else if weight_table.owner.eq(program_id) { + } + + if weight_table.owner.eq(program_id) { WeightTable::load(program_id, weight_table, ncn, ncn_epoch, false)?; return Err(TipRouterError::TrackedMintsLocked.into()); - } else { - msg!("Weight table account is not owned by this program or the system program"); - return Err(ProgramError::InvalidAccountOwner); } // Verify tickets are active From 2e075df6dfb0be99364949ddc6ca434435ab92a4 Mon Sep 17 00:00:00 2001 From: Evan Batsell Date: Wed, 13 Nov 2024 16:04:15 -0500 Subject: [PATCH 9/9] asdf --- clients/js/jito_tip_router/types/mintEntry.ts | 19 +++++++++++++++++-- .../src/generated/types/mint_entry.rs | 1 + idl/jito_tip_router.json | 9 +++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/clients/js/jito_tip_router/types/mintEntry.ts b/clients/js/jito_tip_router/types/mintEntry.ts index afe90fd..aade315 100644 --- a/clients/js/jito_tip_router/types/mintEntry.ts +++ b/clients/js/jito_tip_router/types/mintEntry.ts @@ -8,8 +8,12 @@ import { combineCodec, + fixDecoderSize, + fixEncoderSize, getAddressDecoder, getAddressEncoder, + getBytesDecoder, + getBytesEncoder, getStructDecoder, getStructEncoder, getU64Decoder, @@ -18,16 +22,26 @@ import { type Codec, type Decoder, type Encoder, + type ReadonlyUint8Array, } from '@solana/web3.js'; -export type MintEntry = { stMint: Address; vaultIndex: bigint }; +export type MintEntry = { + stMint: Address; + vaultIndex: bigint; + reserved: ReadonlyUint8Array; +}; -export type MintEntryArgs = { stMint: Address; vaultIndex: number | bigint }; +export type MintEntryArgs = { + stMint: Address; + vaultIndex: number | bigint; + reserved: ReadonlyUint8Array; +}; export function getMintEntryEncoder(): Encoder { return getStructEncoder([ ['stMint', getAddressEncoder()], ['vaultIndex', getU64Encoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 32)], ]); } @@ -35,6 +49,7 @@ export function getMintEntryDecoder(): Decoder { return getStructDecoder([ ['stMint', getAddressDecoder()], ['vaultIndex', getU64Decoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 32)], ]); } 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 index a68634a..7100f19 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mint_entry.rs @@ -16,4 +16,5 @@ pub struct MintEntry { )] pub st_mint: Pubkey, pub vault_index: u64, + pub reserved: [u8; 32], } diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 77bbc07..83b6f9b 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -550,6 +550,15 @@ "type": { "defined": "PodU64" } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 32 + ] + } } ] }