diff --git a/clients/js/jito_tip_router/accounts/ballotBox.ts b/clients/js/jito_tip_router/accounts/ballotBox.ts new file mode 100644 index 00000000..447e1b51 --- /dev/null +++ b/clients/js/jito_tip_router/accounts/ballotBox.ts @@ -0,0 +1,171 @@ +/** + * 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 { + getBallotDecoder, + getBallotEncoder, + getBallotTallyDecoder, + getBallotTallyEncoder, + getOperatorVoteDecoder, + getOperatorVoteEncoder, + type Ballot, + type BallotArgs, + type BallotTally, + type BallotTallyArgs, + type OperatorVote, + type OperatorVoteArgs, +} from '../types'; + +export type BallotBox = { + discriminator: bigint; + ncn: Address; + ncnEpoch: bigint; + bump: number; + slotCreated: bigint; + slotConsensusReached: bigint; + reserved: Array; + operatorsVoted: bigint; + uniqueBallots: bigint; + winningBallot: Ballot; + operatorVotes: Array; + ballotTallies: Array; +}; + +export type BallotBoxArgs = { + discriminator: number | bigint; + ncn: Address; + ncnEpoch: number | bigint; + bump: number; + slotCreated: number | bigint; + slotConsensusReached: number | bigint; + reserved: Array; + operatorsVoted: number | bigint; + uniqueBallots: number | bigint; + winningBallot: BallotArgs; + operatorVotes: Array; + ballotTallies: Array; +}; + +export function getBallotBoxEncoder(): Encoder { + return getStructEncoder([ + ['discriminator', getU64Encoder()], + ['ncn', getAddressEncoder()], + ['ncnEpoch', getU64Encoder()], + ['bump', getU8Encoder()], + ['slotCreated', getU64Encoder()], + ['slotConsensusReached', getU64Encoder()], + ['reserved', getArrayEncoder(getU8Encoder(), { size: 128 })], + ['operatorsVoted', getU64Encoder()], + ['uniqueBallots', getU64Encoder()], + ['winningBallot', getBallotEncoder()], + ['operatorVotes', getArrayEncoder(getOperatorVoteEncoder(), { size: 32 })], + ['ballotTallies', getArrayEncoder(getBallotTallyEncoder(), { size: 32 })], + ]); +} + +export function getBallotBoxDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU64Decoder()], + ['ncn', getAddressDecoder()], + ['ncnEpoch', getU64Decoder()], + ['bump', getU8Decoder()], + ['slotCreated', getU64Decoder()], + ['slotConsensusReached', getU64Decoder()], + ['reserved', getArrayDecoder(getU8Decoder(), { size: 128 })], + ['operatorsVoted', getU64Decoder()], + ['uniqueBallots', getU64Decoder()], + ['winningBallot', getBallotDecoder()], + ['operatorVotes', getArrayDecoder(getOperatorVoteDecoder(), { size: 32 })], + ['ballotTallies', getArrayDecoder(getBallotTallyDecoder(), { size: 32 })], + ]); +} + +export function getBallotBoxCodec(): Codec { + return combineCodec(getBallotBoxEncoder(), getBallotBoxDecoder()); +} + +export function decodeBallotBox( + encodedAccount: EncodedAccount +): Account; +export function decodeBallotBox( + encodedAccount: MaybeEncodedAccount +): MaybeAccount; +export function decodeBallotBox( + encodedAccount: EncodedAccount | MaybeEncodedAccount +): Account | MaybeAccount { + return decodeAccount( + encodedAccount as MaybeEncodedAccount, + getBallotBoxDecoder() + ); +} + +export async function fetchBallotBox( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchMaybeBallotBox(rpc, address, config); + assertAccountExists(maybeAccount); + return maybeAccount; +} + +export async function fetchMaybeBallotBox( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config); + return decodeBallotBox(maybeAccount); +} + +export async function fetchAllBallotBox( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchAllMaybeBallotBox(rpc, addresses, config); + assertAccountsExist(maybeAccounts); + return maybeAccounts; +} + +export async function fetchAllMaybeBallotBox( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config); + return maybeAccounts.map((maybeAccount) => decodeBallotBox(maybeAccount)); +} diff --git a/clients/js/jito_tip_router/accounts/index.ts b/clients/js/jito_tip_router/accounts/index.ts index 549d9e82..9e306afb 100644 --- a/clients/js/jito_tip_router/accounts/index.ts +++ b/clients/js/jito_tip_router/accounts/index.ts @@ -6,6 +6,7 @@ * @see https://github.com/kinobi-so/kinobi */ +export * from './ballotBox'; export * from './epochSnapshot'; export * from './ncnConfig'; export * from './operatorSnapshot'; diff --git a/clients/js/jito_tip_router/accounts/operatorSnapshot.ts b/clients/js/jito_tip_router/accounts/operatorSnapshot.ts index 32b0633e..76a521ba 100644 --- a/clients/js/jito_tip_router/accounts/operatorSnapshot.ts +++ b/clients/js/jito_tip_router/accounts/operatorSnapshot.ts @@ -56,6 +56,7 @@ export type OperatorSnapshot = { slotCreated: bigint; slotFinalized: bigint; isActive: number; + ncnOperatorIndex: bigint; operatorIndex: bigint; operatorFeeBps: number; vaultOperatorDelegationCount: bigint; @@ -75,6 +76,7 @@ export type OperatorSnapshotArgs = { slotCreated: number | bigint; slotFinalized: number | bigint; isActive: number; + ncnOperatorIndex: number | bigint; operatorIndex: number | bigint; operatorFeeBps: number; vaultOperatorDelegationCount: number | bigint; @@ -95,6 +97,7 @@ export function getOperatorSnapshotEncoder(): Encoder { ['slotCreated', getU64Encoder()], ['slotFinalized', getU64Encoder()], ['isActive', getBoolEncoder()], + ['ncnOperatorIndex', getU64Encoder()], ['operatorIndex', getU64Encoder()], ['operatorFeeBps', getU16Encoder()], ['vaultOperatorDelegationCount', getU64Encoder()], @@ -119,6 +122,7 @@ export function getOperatorSnapshotDecoder(): Decoder { ['slotCreated', getU64Decoder()], ['slotFinalized', getU64Decoder()], ['isActive', getBoolDecoder()], + ['ncnOperatorIndex', getU64Decoder()], ['operatorIndex', getU64Decoder()], ['operatorFeeBps', getU16Decoder()], ['vaultOperatorDelegationCount', getU64Decoder()], diff --git a/clients/js/jito_tip_router/errors/jitoTipRouter.ts b/clients/js/jito_tip_router/errors/jitoTipRouter.ts index 45acb0fa..74b44962 100644 --- a/clients/js/jito_tip_router/errors/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/errors/jitoTipRouter.ts @@ -74,16 +74,30 @@ export const JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED = 0x2216; // 8726 export const JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS = 0x2217; // 8727 /** DuplicateVaultOperatorDelegation: Duplicate vault operator delegation */ export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION = 0x2218; // 8728 +/** DuplicateVoteCast: Duplicate Vote Cast */ +export const JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST = 0x2219; // 8729 +/** OperatorVotesFull: Operator votes full */ +export const JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL = 0x221a; // 8730 +/** BallotTallyFull: Merkle root tally full */ +export const JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL = 0x221b; // 8731 +/** ConsensusAlreadyReached: Consensus already reached */ +export const JITO_TIP_ROUTER_ERROR__CONSENSUS_ALREADY_REACHED = 0x221c; // 8732 +/** ConsensusNotReached: Consensus not reached */ +export const JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED = 0x221d; // 8733 export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW + | typeof JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL | 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__CONSENSUS_ALREADY_REACHED + | typeof JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED | typeof JITO_TIP_ROUTER_ERROR__DENOMINATOR_IS_ZERO | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VAULT_OPERATOR_DELEGATION + | typeof JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST | typeof JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN | typeof JITO_TIP_ROUTER_ERROR__INCORRECT_NCN @@ -95,6 +109,7 @@ export type JitoTipRouterError = | typeof JITO_TIP_ROUTER_ERROR__NO_MINTS_IN_TABLE | typeof JITO_TIP_ROUTER_ERROR__NO_OPERATORS | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED + | typeof JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE | typeof JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS | typeof JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL @@ -111,13 +126,17 @@ let jitoTipRouterErrorMessages: Record | undefined; if (process.env.NODE_ENV !== 'production') { jitoTipRouterErrorMessages = { [JITO_TIP_ROUTER_ERROR__ARITHMETIC_OVERFLOW]: `Overflow`, + [JITO_TIP_ROUTER_ERROR__BALLOT_TALLY_FULL]: `Merkle root tally full`, [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__CONSENSUS_ALREADY_REACHED]: `Consensus already reached`, + [JITO_TIP_ROUTER_ERROR__CONSENSUS_NOT_REACHED]: `Consensus not reached`, [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__DUPLICATE_VAULT_OPERATOR_DELEGATION]: `Duplicate vault operator delegation`, + [JITO_TIP_ROUTER_ERROR__DUPLICATE_VOTE_CAST]: `Duplicate Vote Cast`, [JITO_TIP_ROUTER_ERROR__FEE_CAP_EXCEEDED]: `Fee cap exceeded`, [JITO_TIP_ROUTER_ERROR__INCORRECT_FEE_ADMIN]: `Incorrect fee admin`, [JITO_TIP_ROUTER_ERROR__INCORRECT_NCN]: `Incorrect NCN`, @@ -129,6 +148,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__NO_OPERATORS]: `No operators in ncn`, [JITO_TIP_ROUTER_ERROR__OPERATOR_FINALIZED]: `Operator is already finalized - should not happen`, + [JITO_TIP_ROUTER_ERROR__OPERATOR_VOTES_FULL]: `Operator votes full`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_MINTS_FOR_TABLE]: `Too many mints for table`, [JITO_TIP_ROUTER_ERROR__TOO_MANY_VAULT_OPERATOR_DELEGATIONS]: `Too many vault operator delegations`, [JITO_TIP_ROUTER_ERROR__TRACKED_MINT_LIST_FULL]: `Tracked mints are at capacity`, diff --git a/clients/js/jito_tip_router/programs/jitoTipRouter.ts b/clients/js/jito_tip_router/programs/jitoTipRouter.ts index 29a98a9d..3c570619 100644 --- a/clients/js/jito_tip_router/programs/jitoTipRouter.ts +++ b/clients/js/jito_tip_router/programs/jitoTipRouter.ts @@ -29,6 +29,7 @@ export const JITO_TIP_ROUTER_PROGRAM_ADDRESS = 'Fv9aHCgvPQSr4jg9W8eTS6Ys1SNmh2qjyATrbsjEMaSH' as Address<'Fv9aHCgvPQSr4jg9W8eTS6Ys1SNmh2qjyATrbsjEMaSH'>; export enum JitoTipRouterAccount { + BallotBox, EpochSnapshot, OperatorSnapshot, NcnConfig, diff --git a/clients/js/jito_tip_router/types/ballot.ts b/clients/js/jito_tip_router/types/ballot.ts new file mode 100644 index 00000000..b268678a --- /dev/null +++ b/clients/js/jito_tip_router/types/ballot.ts @@ -0,0 +1,46 @@ +/** + * 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, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; + +export type Ballot = { + merkleRoot: ReadonlyUint8Array; + reserved: ReadonlyUint8Array; +}; + +export type BallotArgs = Ballot; + +export function getBallotEncoder(): Encoder { + return getStructEncoder([ + ['merkleRoot', fixEncoderSize(getBytesEncoder(), 32)], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getBallotDecoder(): Decoder { + return getStructDecoder([ + ['merkleRoot', fixDecoderSize(getBytesDecoder(), 32)], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getBallotCodec(): Codec { + return combineCodec(getBallotEncoder(), getBallotDecoder()); +} diff --git a/clients/js/jito_tip_router/types/ballotTally.ts b/clients/js/jito_tip_router/types/ballotTally.ts new file mode 100644 index 00000000..99e151e1 --- /dev/null +++ b/clients/js/jito_tip_router/types/ballotTally.ts @@ -0,0 +1,67 @@ +/** + * 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, + fixDecoderSize, + fixEncoderSize, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU128Decoder, + getU128Encoder, + getU64Decoder, + getU64Encoder, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; +import { + getBallotDecoder, + getBallotEncoder, + type Ballot, + type BallotArgs, +} from '.'; + +export type BallotTally = { + ballot: Ballot; + stakeWeight: bigint; + tally: bigint; + reserved: ReadonlyUint8Array; +}; + +export type BallotTallyArgs = { + ballot: BallotArgs; + stakeWeight: number | bigint; + tally: number | bigint; + reserved: ReadonlyUint8Array; +}; + +export function getBallotTallyEncoder(): Encoder { + return getStructEncoder([ + ['ballot', getBallotEncoder()], + ['stakeWeight', getU128Encoder()], + ['tally', getU64Encoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getBallotTallyDecoder(): Decoder { + return getStructDecoder([ + ['ballot', getBallotDecoder()], + ['stakeWeight', getU128Decoder()], + ['tally', getU64Decoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getBallotTallyCodec(): Codec { + return combineCodec(getBallotTallyEncoder(), getBallotTallyDecoder()); +} diff --git a/clients/js/jito_tip_router/types/index.ts b/clients/js/jito_tip_router/types/index.ts index e93016f1..b49207d9 100644 --- a/clients/js/jito_tip_router/types/index.ts +++ b/clients/js/jito_tip_router/types/index.ts @@ -6,9 +6,12 @@ * @see https://github.com/kinobi-so/kinobi */ +export * from './ballot'; +export * from './ballotTally'; export * from './configAdminRole'; export * from './fee'; export * from './fees'; export * from './mintEntry'; +export * from './operatorVote'; export * from './vaultOperatorStakeWeight'; export * from './weightEntry'; diff --git a/clients/js/jito_tip_router/types/operatorVote.ts b/clients/js/jito_tip_router/types/operatorVote.ts new file mode 100644 index 00000000..f594c9d6 --- /dev/null +++ b/clients/js/jito_tip_router/types/operatorVote.ts @@ -0,0 +1,70 @@ +/** + * 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, + fixDecoderSize, + fixEncoderSize, + getAddressDecoder, + getAddressEncoder, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU128Decoder, + getU128Encoder, + getU16Decoder, + getU16Encoder, + getU64Decoder, + getU64Encoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type ReadonlyUint8Array, +} from '@solana/web3.js'; + +export type OperatorVote = { + operator: Address; + slotVoted: bigint; + stakeWeight: bigint; + ballotIndex: number; + reserved: ReadonlyUint8Array; +}; + +export type OperatorVoteArgs = { + operator: Address; + slotVoted: number | bigint; + stakeWeight: number | bigint; + ballotIndex: number; + reserved: ReadonlyUint8Array; +}; + +export function getOperatorVoteEncoder(): Encoder { + return getStructEncoder([ + ['operator', getAddressEncoder()], + ['slotVoted', getU64Encoder()], + ['stakeWeight', getU128Encoder()], + ['ballotIndex', getU16Encoder()], + ['reserved', fixEncoderSize(getBytesEncoder(), 64)], + ]); +} + +export function getOperatorVoteDecoder(): Decoder { + return getStructDecoder([ + ['operator', getAddressDecoder()], + ['slotVoted', getU64Decoder()], + ['stakeWeight', getU128Decoder()], + ['ballotIndex', getU16Decoder()], + ['reserved', fixDecoderSize(getBytesDecoder(), 64)], + ]); +} + +export function getOperatorVoteCodec(): Codec { + return combineCodec(getOperatorVoteEncoder(), getOperatorVoteDecoder()); +} diff --git a/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs new file mode 100644 index 00000000..01c1b216 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/accounts/ballot_box.rs @@ -0,0 +1,76 @@ +//! 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::{Ballot, BallotTally, OperatorVote}; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BallotBox { + pub discriminator: u64, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub ncn: Pubkey, + pub ncn_epoch: u64, + pub bump: u8, + pub slot_created: u64, + pub slot_consensus_reached: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 128], + pub operators_voted: u64, + pub unique_ballots: u64, + pub winning_ballot: Ballot, + pub operator_votes: [OperatorVote; 32], + pub ballot_tallies: [BallotTally; 32], +} + +impl BallotBox { + #[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 BallotBox { + 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 BallotBox { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + Ok(Self::deserialize(buf)?) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountSerialize for BallotBox {} + +#[cfg(feature = "anchor")] +impl anchor_lang::Owner for BallotBox { + fn owner() -> Pubkey { + crate::JITO_TIP_ROUTER_ID + } +} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::IdlBuild for BallotBox {} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::Discriminator for BallotBox { + const DISCRIMINATOR: [u8; 8] = [0; 8]; +} 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 7b099a2d..aa0f2538 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/mod.rs @@ -4,6 +4,7 @@ //! //! +pub(crate) mod r#ballot_box; pub(crate) mod r#epoch_snapshot; pub(crate) mod r#ncn_config; pub(crate) mod r#operator_snapshot; @@ -11,6 +12,6 @@ pub(crate) mod r#tracked_mints; pub(crate) mod r#weight_table; pub use self::{ - r#epoch_snapshot::*, r#ncn_config::*, r#operator_snapshot::*, r#tracked_mints::*, - r#weight_table::*, + r#ballot_box::*, r#epoch_snapshot::*, r#ncn_config::*, r#operator_snapshot::*, + r#tracked_mints::*, r#weight_table::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs b/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs index d392fc61..a30fbde3 100644 --- a/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs +++ b/clients/rust/jito_tip_router/src/generated/accounts/operator_snapshot.rs @@ -28,6 +28,7 @@ pub struct OperatorSnapshot { pub slot_created: u64, pub slot_finalized: u64, pub is_active: bool, + pub ncn_operator_index: u64, pub operator_index: u64, pub operator_fee_bps: u16, pub vault_operator_delegation_count: u64, 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 60dd7d63..ce806cbf 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 @@ -99,6 +99,21 @@ pub enum JitoTipRouterError { /// 8728 - Duplicate vault operator delegation #[error("Duplicate vault operator delegation")] DuplicateVaultOperatorDelegation = 0x2218, + /// 8729 - Duplicate Vote Cast + #[error("Duplicate Vote Cast")] + DuplicateVoteCast = 0x2219, + /// 8730 - Operator votes full + #[error("Operator votes full")] + OperatorVotesFull = 0x221A, + /// 8731 - Merkle root tally full + #[error("Merkle root tally full")] + BallotTallyFull = 0x221B, + /// 8732 - Consensus already reached + #[error("Consensus already reached")] + ConsensusAlreadyReached = 0x221C, + /// 8733 - Consensus not reached + #[error("Consensus not reached")] + ConsensusNotReached = 0x221D, } impl solana_program::program_error::PrintProgramError for JitoTipRouterError { diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot.rs b/clients/rust/jito_tip_router/src/generated/types/ballot.rs new file mode 100644 index 00000000..b954536b --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/ballot.rs @@ -0,0 +1,15 @@ +//! 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}; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Ballot { + pub merkle_root: [u8; 32], + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} diff --git a/clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs b/clients/rust/jito_tip_router/src/generated/types/ballot_tally.rs new file mode 100644 index 00000000..265dcf1f --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/ballot_tally.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 crate::generated::types::Ballot; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BallotTally { + pub ballot: Ballot, + pub stake_weight: u128, + pub tally: u64, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} 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 f19946a2..82ebe6f4 100644 --- a/clients/rust/jito_tip_router/src/generated/types/mod.rs +++ b/clients/rust/jito_tip_router/src/generated/types/mod.rs @@ -4,14 +4,17 @@ //! //! +pub(crate) mod r#ballot; +pub(crate) mod r#ballot_tally; 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#operator_vote; pub(crate) mod r#vault_operator_stake_weight; pub(crate) mod r#weight_entry; pub use self::{ - r#config_admin_role::*, r#fee::*, r#fees::*, r#mint_entry::*, r#vault_operator_stake_weight::*, - r#weight_entry::*, + r#ballot::*, r#ballot_tally::*, r#config_admin_role::*, r#fee::*, r#fees::*, r#mint_entry::*, + r#operator_vote::*, r#vault_operator_stake_weight::*, r#weight_entry::*, }; diff --git a/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs new file mode 100644 index 00000000..341cb174 --- /dev/null +++ b/clients/rust/jito_tip_router/src/generated/types/operator_vote.rs @@ -0,0 +1,23 @@ +//! 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 OperatorVote { + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub operator: Pubkey, + pub slot_voted: u64, + pub stake_weight: u128, + pub ballot_index: u16, + #[cfg_attr(feature = "serde", serde(with = "serde_with::As::"))] + pub reserved: [u8; 64], +} diff --git a/core/src/ballot_box.rs b/core/src/ballot_box.rs new file mode 100644 index 00000000..0895acc4 --- /dev/null +++ b/core/src/ballot_box.rs @@ -0,0 +1,397 @@ +use bytemuck::{Pod, Zeroable}; +use jito_bytemuck::{ + types::{PodU128, PodU16, PodU64}, + AccountDeserialize, Discriminator, +}; +use shank::{ShankAccount, ShankType}; +use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey}; +use spl_math::precise_number::PreciseNumber; + +use crate::{constants::PRECISE_CONSENSUS, discriminators::Discriminators, error::TipRouterError}; + +#[derive(Debug, Clone, PartialEq, Eq, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct Ballot { + merkle_root: [u8; 32], + reserved: [u8; 64], +} + +impl Default for Ballot { + fn default() -> Self { + Self { + merkle_root: [0; 32], + reserved: [0; 64], + } + } +} + +impl Ballot { + pub const fn new(root: [u8; 32]) -> Self { + Self { + merkle_root: root, + reserved: [0; 64], + } + } + + pub const fn root(&self) -> [u8; 32] { + self.merkle_root + } + + pub fn is_valid(&self) -> bool { + self.merkle_root.iter().any(|&b| b != 0) + } +} + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct BallotTally { + ballot: Ballot, + stake_weight: PodU128, + tally: PodU64, + reserved: [u8; 64], +} + +impl Default for BallotTally { + fn default() -> Self { + Self { + ballot: Ballot::default(), + stake_weight: PodU128::from(0), + tally: PodU64::from(0), + reserved: [0; 64], + } + } +} + +impl BallotTally { + pub fn new(ballot: Ballot, stake_weight: u128) -> Self { + Self { + ballot, + stake_weight: PodU128::from(stake_weight), + tally: PodU64::from(1), + reserved: [0; 64], + } + } + + pub const fn ballot(&self) -> Ballot { + self.ballot + } + + pub fn stake_weight(&self) -> u128 { + self.stake_weight.into() + } + + pub fn tally(&self) -> u64 { + self.tally.into() + } + + pub fn is_empty(&self) -> bool { + self.stake_weight() == 0 + } + + pub fn increment_tally(&mut self, stake_weight: u128) -> Result<(), TipRouterError> { + self.stake_weight = PodU128::from( + self.stake_weight() + .checked_add(stake_weight) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + self.tally = PodU64::from( + self.tally() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, ShankType)] +#[repr(C)] +pub struct OperatorVote { + operator: Pubkey, + slot_voted: PodU64, + stake_weight: PodU128, + ballot_index: PodU16, + reserved: [u8; 64], +} + +impl Default for OperatorVote { + fn default() -> Self { + Self { + operator: Pubkey::default(), + slot_voted: PodU64::from(0), + stake_weight: PodU128::from(0), + ballot_index: PodU16::from(0), + reserved: [0; 64], + } + } +} + +impl OperatorVote { + pub fn new( + ballot_index: usize, + operator: Pubkey, + current_slot: u64, + stake_weight: u128, + ) -> Self { + Self { + operator, + ballot_index: PodU16::from(ballot_index as u16), + slot_voted: PodU64::from(current_slot), + stake_weight: PodU128::from(stake_weight), + reserved: [0; 64], + } + } + + pub const fn operator(&self) -> Pubkey { + self.operator + } + + pub fn slot_voted(&self) -> u64 { + self.slot_voted.into() + } + + pub fn stake_weight(&self) -> u128 { + self.stake_weight.into() + } + + pub fn ballot_index(&self) -> u16 { + self.ballot_index.into() + } + + pub fn is_empty(&self) -> bool { + self.stake_weight() == 0 + } +} + +// PDA'd ["epoch_snapshot", NCN, NCN_EPOCH_SLOT] +#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] +#[repr(C)] +pub struct BallotBox { + ncn: Pubkey, + + ncn_epoch: PodU64, + + bump: u8, + + slot_created: PodU64, + slot_consensus_reached: PodU64, + + reserved: [u8; 128], + + operators_voted: PodU64, + unique_ballots: PodU64, + + winning_ballot: Ballot, + + //TODO fix 32 -> MAX_OPERATORS + operator_votes: [OperatorVote; 32], + ballot_tallies: [BallotTally; 32], +} + +impl Discriminator for BallotBox { + const DISCRIMINATOR: u8 = Discriminators::EpochSnapshot as u8; +} + +impl BallotBox { + pub fn new(ncn: Pubkey, ncn_epoch: u64, bump: u8, current_slot: u64) -> Self { + Self { + ncn, + ncn_epoch: PodU64::from(ncn_epoch), + bump, + slot_created: PodU64::from(current_slot), + slot_consensus_reached: PodU64::from(0), + operators_voted: PodU64::from(0), + unique_ballots: PodU64::from(0), + winning_ballot: Ballot::default(), + //TODO fix 32 -> MAX_OPERATORS + operator_votes: [OperatorVote::default(); 32], + ballot_tallies: [BallotTally::default(); 32], + reserved: [0; 128], + } + } + + pub fn seeds(ncn: &Pubkey, ncn_epoch: u64) -> Vec> { + Vec::from_iter( + [ + b"ballot_box".to_vec(), + ncn.to_bytes().to_vec(), + ncn_epoch.to_le_bytes().to_vec(), + ] + .iter() + .cloned(), + ) + } + + pub fn find_program_address( + program_id: &Pubkey, + ncn: &Pubkey, + ncn_epoch: u64, + ) -> (Pubkey, u8, Vec>) { + let seeds = Self::seeds(ncn, ncn_epoch); + let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id); + (pda, bump, seeds) + } + + pub fn load( + program_id: &Pubkey, + ncn: &Pubkey, + ncn_epoch: u64, + epoch_snapshot: &AccountInfo, + expect_writable: bool, + ) -> Result<(), ProgramError> { + if epoch_snapshot.owner.ne(program_id) { + msg!("Ballot box account has an invalid owner"); + return Err(ProgramError::InvalidAccountOwner); + } + if epoch_snapshot.data_is_empty() { + msg!("Ballot box account data is empty"); + return Err(ProgramError::InvalidAccountData); + } + if expect_writable && !epoch_snapshot.is_writable { + msg!("Ballot box account is not writable"); + return Err(ProgramError::InvalidAccountData); + } + if epoch_snapshot.data.borrow()[0].ne(&Self::DISCRIMINATOR) { + msg!("Ballot box account discriminator is invalid"); + return Err(ProgramError::InvalidAccountData); + } + if epoch_snapshot + .key + .ne(&Self::find_program_address(program_id, ncn, ncn_epoch).0) + { + msg!("Ballot box account is not at the correct PDA"); + return Err(ProgramError::InvalidAccountData); + } + Ok(()) + } + + pub fn slot_consensus_reached(&self) -> u64 { + self.slot_consensus_reached.into() + } + + pub fn unique_ballots(&self) -> u64 { + self.unique_ballots.into() + } + + pub fn operators_voted(&self) -> u64 { + self.operators_voted.into() + } + + pub fn is_consensus_reached(&self) -> bool { + self.slot_consensus_reached() > 0 + } + + pub fn get_winning_ballot(&self) -> Result { + if self.winning_ballot.is_valid() { + Ok(self.winning_ballot) + } else { + Err(TipRouterError::ConsensusNotReached) + } + } + + fn increment_or_create_ballot_tally( + &mut self, + ballot: &Ballot, + stake_weight: u128, + ) -> Result { + let mut tally_index: usize = 0; + for tally in self.ballot_tallies.iter_mut() { + if tally.ballot.eq(ballot) { + tally.increment_tally(stake_weight)?; + return Ok(tally_index); + } + + if tally.is_empty() { + *tally = BallotTally::new(*ballot, stake_weight); + + self.unique_ballots = PodU64::from( + self.unique_ballots() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + return Ok(tally_index); + } + + tally_index = tally_index + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?; + } + + Err(TipRouterError::BallotTallyFull) + } + + pub fn cast_vote( + &mut self, + operator: Pubkey, + ballot: Ballot, + stake_weight: u128, + current_slot: u64, + ) -> Result<(), TipRouterError> { + let ballot_index = self.increment_or_create_ballot_tally(&ballot, stake_weight)?; + + for vote in self.operator_votes.iter_mut() { + if vote.operator().eq(&operator) { + return Err(TipRouterError::DuplicateVoteCast); + } + + if vote.is_empty() { + let operator_vote = + OperatorVote::new(ballot_index, operator, current_slot, stake_weight); + *vote = operator_vote; + + self.operators_voted = PodU64::from( + self.operators_voted() + .checked_add(1) + .ok_or(TipRouterError::ArithmeticOverflow)?, + ); + + return Ok(()); + } + } + + Err(TipRouterError::OperatorVotesFull) + } + + // Should be called anytime a new vote is cast + pub fn tally_votes( + &mut self, + total_stake_weight: u128, + current_slot: u64, + ) -> Result<(), TipRouterError> { + if self.slot_consensus_reached() != 0 { + return Ok(()); + } + + let max_tally = self + .ballot_tallies + .iter() + .max_by_key(|t| t.stake_weight()) + .unwrap(); + + let ballot_stake_weight = max_tally.stake_weight(); + let precise_ballot_stake_weight = + PreciseNumber::new(ballot_stake_weight).ok_or(TipRouterError::NewPreciseNumberError)?; + let precise_total_stake_weight = + PreciseNumber::new(total_stake_weight).ok_or(TipRouterError::NewPreciseNumberError)?; + + let ballot_percentage_of_total = precise_ballot_stake_weight + .checked_div(&precise_total_stake_weight) + .ok_or(TipRouterError::DenominatorIsZero)?; + + let target_precise_percentage = + PreciseNumber::new(PRECISE_CONSENSUS).ok_or(TipRouterError::NewPreciseNumberError)?; + + let consensus_reached = + ballot_percentage_of_total.greater_than_or_equal(&target_precise_percentage); + + if consensus_reached { + self.slot_consensus_reached = PodU64::from(current_slot); + + self.winning_ballot = max_tally.ballot(); + } + + Ok(()) + } +} diff --git a/core/src/constants.rs b/core/src/constants.rs index 01586c40..df8fbe01 100644 --- a/core/src/constants.rs +++ b/core/src/constants.rs @@ -1,3 +1,4 @@ pub const MAX_FEE_BPS: u64 = 10_000; pub const MAX_OPERATORS: usize = 256; pub const MAX_VAULT_OPERATOR_DELEGATIONS: usize = 64; +pub const PRECISE_CONSENSUS: u128 = 666_666_666_666; diff --git a/core/src/discriminators.rs b/core/src/discriminators.rs index 65111c2b..15b8bdae 100644 --- a/core/src/discriminators.rs +++ b/core/src/discriminators.rs @@ -1,9 +1,13 @@ #[repr(u8)] pub enum Discriminators { - NCNConfig = 1, - WeightTable = 2, - TrackedMints = 3, - EpochSnapshot = 4, - OperatorSnapshot = 5, - VaultOperatorDelegationSnapshot = 6, + // Configs + NCNConfig = 0x01, + TrackedMints = 0x02, + // Snapshots + WeightTable = 0x10, + EpochSnapshot = 0x11, + OperatorSnapshot = 0x12, + // Voting + BallotBox = 0x20, + // Distribution } diff --git a/core/src/epoch_snapshot.rs b/core/src/epoch_snapshot.rs index db300f6f..64278019 100644 --- a/core/src/epoch_snapshot.rs +++ b/core/src/epoch_snapshot.rs @@ -12,7 +12,7 @@ use crate::{ discriminators::Discriminators, error::TipRouterError, fees::Fees, weight_table::WeightTable, }; -// PDA'd ["EPOCH_SNAPSHOT", NCN, NCN_EPOCH_SLOT] +// PDA'd ["epoch_snapshot", NCN, NCN_EPOCH_SLOT] #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] #[repr(C)] pub struct EpochSnapshot { @@ -191,7 +191,7 @@ impl EpochSnapshot { } } -// PDA'd ["OPERATOR_SNAPSHOT", OPERATOR, NCN, NCN_EPOCH_SLOT] +// PDA'd ["operator_snapshot", OPERATOR, NCN, NCN_EPOCH_SLOT] #[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)] #[repr(C)] pub struct OperatorSnapshot { @@ -205,6 +205,7 @@ pub struct OperatorSnapshot { is_active: PodBool, + ncn_operator_index: PodU64, operator_index: PodU64, operator_fee_bps: PodU16, @@ -277,6 +278,7 @@ impl OperatorSnapshot { bump: u8, current_slot: u64, is_active: bool, + ncn_operator_index: u64, operator_index: u64, operator_fee_bps: u16, vault_operator_delegation_count: u64, @@ -293,6 +295,7 @@ impl OperatorSnapshot { slot_created: PodU64::from(current_slot), slot_finalized: PodU64::from(0), is_active: PodBool::from(is_active), + ncn_operator_index: PodU64::from(ncn_operator_index), operator_index: PodU64::from(operator_index), operator_fee_bps: PodU16::from(operator_fee_bps), vault_operator_delegation_count: PodU64::from(vault_operator_delegation_count), @@ -311,6 +314,7 @@ impl OperatorSnapshot { ncn_epoch: u64, bump: u8, current_slot: u64, + ncn_operator_index: u64, operator_index: u64, operator_fee_bps: u16, vault_count: u64, @@ -322,6 +326,7 @@ impl OperatorSnapshot { bump, current_slot, true, + ncn_operator_index, operator_index, operator_fee_bps, vault_count, @@ -334,6 +339,7 @@ impl OperatorSnapshot { ncn_epoch: u64, bump: u8, current_slot: u64, + ncn_operator_index: u64, operator_index: u64, ) -> Result { let mut snapshot = Self::new( @@ -343,6 +349,7 @@ impl OperatorSnapshot { bump, current_slot, false, + ncn_operator_index, operator_index, 0, 0, diff --git a/core/src/error.rs b/core/src/error.rs index b7ba61b0..4c5f7da6 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -64,6 +64,16 @@ pub enum TipRouterError { TooManyVaultOperatorDelegations, #[error("Duplicate vault operator delegation")] DuplicateVaultOperatorDelegation, + #[error("Duplicate Vote Cast")] + DuplicateVoteCast, + #[error("Operator votes full")] + OperatorVotesFull, + #[error("Merkle root tally full")] + BallotTallyFull, + #[error("Consensus already reached")] + ConsensusAlreadyReached, + #[error("Consensus not reached")] + ConsensusNotReached, } impl DecodeError for TipRouterError { diff --git a/core/src/lib.rs b/core/src/lib.rs index 8a15379f..ac782715 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod ballot_box; pub mod constants; pub mod discriminators; pub mod epoch_snapshot; diff --git a/idl/jito_tip_router.json b/idl/jito_tip_router.json index 7f59e84b..dbac2f45 100644 --- a/idl/jito_tip_router.json +++ b/idl/jito_tip_router.json @@ -566,6 +566,89 @@ } ], "accounts": [ + { + "name": "BallotBox", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ncn", + "type": "publicKey" + }, + { + "name": "ncnEpoch", + "type": { + "defined": "PodU64" + } + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "slotCreated", + "type": { + "defined": "PodU64" + } + }, + { + "name": "slotConsensusReached", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 128 + ] + } + }, + { + "name": "operatorsVoted", + "type": { + "defined": "PodU64" + } + }, + { + "name": "uniqueBallots", + "type": { + "defined": "PodU64" + } + }, + { + "name": "winningBallot", + "type": { + "defined": "Ballot" + } + }, + { + "name": "operatorVotes", + "type": { + "array": [ + { + "defined": "OperatorVote" + }, + 32 + ] + } + }, + { + "name": "ballotTallies", + "type": { + "array": [ + { + "defined": "BallotTally" + }, + 32 + ] + } + } + ] + } + }, { "name": "EpochSnapshot", "type": { @@ -686,6 +769,12 @@ "defined": "PodBool" } }, + { + "name": "ncnOperatorIndex", + "type": { + "defined": "PodU64" + } + }, { "name": "operatorIndex", "type": { @@ -870,6 +959,106 @@ } ], "types": [ + { + "name": "Ballot", + "type": { + "kind": "struct", + "fields": [ + { + "name": "merkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "BallotTally", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ballot", + "type": { + "defined": "Ballot" + } + }, + { + "name": "stakeWeight", + "type": { + "defined": "PodU128" + } + }, + { + "name": "tally", + "type": { + "defined": "PodU64" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "OperatorVote", + "type": { + "kind": "struct", + "fields": [ + { + "name": "operator", + "type": "publicKey" + }, + { + "name": "slotVoted", + "type": { + "defined": "PodU64" + } + }, + { + "name": "stakeWeight", + "type": { + "defined": "PodU128" + } + }, + { + "name": "ballotIndex", + "type": { + "defined": "PodU16" + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, { "name": "VaultOperatorStakeWeight", "type": { @@ -1190,6 +1379,31 @@ "code": 8728, "name": "DuplicateVaultOperatorDelegation", "msg": "Duplicate vault operator delegation" + }, + { + "code": 8729, + "name": "DuplicateVoteCast", + "msg": "Duplicate Vote Cast" + }, + { + "code": 8730, + "name": "OperatorVotesFull", + "msg": "Operator votes full" + }, + { + "code": 8731, + "name": "BallotTallyFull", + "msg": "Merkle root tally full" + }, + { + "code": 8732, + "name": "ConsensusAlreadyReached", + "msg": "Consensus already reached" + }, + { + "code": 8733, + "name": "ConsensusNotReached", + "msg": "Consensus not reached" } ], "metadata": { diff --git a/program/src/initialize_operator_snapshot.rs b/program/src/initialize_operator_snapshot.rs index 0c2d84ef..a2d72c8f 100644 --- a/program/src/initialize_operator_snapshot.rs +++ b/program/src/initialize_operator_snapshot.rs @@ -84,7 +84,7 @@ pub fn process_initialize_operator_snapshot( )?; //TODO move to helper function - let is_active: bool = { + let (is_active, ncn_operator_index): (bool, u64) = { let ncn_operator_state_data = ncn_operator_state.data.borrow(); let ncn_operator_state_account = NcnOperatorState::try_from_slice_unchecked(&ncn_operator_state_data)?; @@ -97,7 +97,9 @@ pub fn process_initialize_operator_snapshot( .operator_opt_in_state .is_active(current_slot, ncn_epoch_length); - ncn_operator_okay && operator_ncn_okay + let ncn_operator_index = ncn_operator_state_account.index(); + + (ncn_operator_okay && operator_ncn_okay, ncn_operator_index) }; let vault_count = { @@ -128,6 +130,7 @@ pub fn process_initialize_operator_snapshot( ncn_epoch, operator_snapshot_bump, current_slot, + ncn_operator_index, operator_index, operator_fee_bps, vault_count, @@ -139,6 +142,7 @@ pub fn process_initialize_operator_snapshot( ncn_epoch, operator_snapshot_bump, current_slot, + ncn_operator_index, operator_index, )? };