diff --git a/src/Solnet.Programs/Solnet.Programs.csproj b/src/Solnet.Programs/Solnet.Programs.csproj index 03e2b51f..90bd387c 100644 --- a/src/Solnet.Programs/Solnet.Programs.csproj +++ b/src/Solnet.Programs/Solnet.Programs.csproj @@ -13,5 +13,9 @@ + + + + diff --git a/src/Solnet.Programs/Stake/StakeProgram.cs b/src/Solnet.Programs/Stake/StakeProgram.cs index b606ba35..6b06ded4 100644 --- a/src/Solnet.Programs/Stake/StakeProgram.cs +++ b/src/Solnet.Programs/Stake/StakeProgram.cs @@ -28,7 +28,7 @@ public static class StakeProgram /// /// Stake Config ID /// - public static readonly PublicKey ConfigKey = new("StakeConfig11111111111111111111111111111111"); + public static readonly PublicKey ConfigKey = new("StakeConfig11111111111111111111111111111111"); /// /// The program's name. /// diff --git a/src/Solnet.Programs/StakePool/Models/AccountType.cs b/src/Solnet.Programs/StakePool/Models/AccountType.cs new file mode 100644 index 00000000..576a234a --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/AccountType.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Enum representing the account type managed by the program. + /// + public enum AccountType : byte + { + /// + /// If the account has not been initialized, the enum will be 0. + /// + Uninitialized = 0, + + /// + /// Stake pool. + /// + StakePool = 1, + + /// + /// Validator stake list. + /// + ValidatorList = 2 + } +} diff --git a/src/Solnet.Programs/StakePool/Models/Fee.cs b/src/Solnet.Programs/StakePool/Models/Fee.cs new file mode 100644 index 00000000..7a22acc6 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/Fee.cs @@ -0,0 +1,133 @@ +using System.Numerics; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Fee rate as a ratio, minted on UpdateStakePoolBalance as a proportion of the rewards. + /// If either the numerator or the denominator is 0, the fee is considered to be 0. + /// + public class Fee + { + /// + /// Denominator of the fee ratio. + /// + public ulong Denominator { get; set; } + + /// + /// Numerator of the fee ratio. + /// + public ulong Numerator { get; set; } + + /// + /// Returns true if the fee is considered zero (either numerator or denominator is zero). + /// + public bool IsZero => Denominator == 0 || Numerator == 0; + + /// + /// Initializes a new instance of the class. + /// + public Fee() { } + + /// + /// Initializes a new instance of the class with the specified numerator and denominator. + /// + /// + /// + public Fee(ulong numerator, ulong denominator) + { + Numerator = numerator; + Denominator = denominator; + } + + /// + /// Applies the fee's rates to a given amount, returning the amount to be subtracted as fees. + /// Returns 0 if denominator is 0 or amount is 0, or null if overflow occurs. + /// + public ulong? Apply(ulong amount) + { + if (Denominator == 0 || amount == 0) + return 0; + + try + { + // Use BigInteger to avoid overflow + BigInteger amt = new BigInteger(amount); + BigInteger numerator = new BigInteger(Numerator); + BigInteger denominator = new BigInteger(Denominator); + + BigInteger feeNumerator = amt * numerator; + // Ceiling division: (feeNumerator + denominator - 1) / denominator + BigInteger result = (feeNumerator + denominator - 1) / denominator; + + if (result < 0 || result > ulong.MaxValue) + return null; + + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Checks withdrawal fee restrictions, throws StakePoolFeeException if not met. + /// + public void CheckWithdrawal(Fee oldWithdrawalFee) + { + // Constants as per SPL Stake Pool program + var WITHDRAWAL_BASELINE_FEE = new Fee(1, 1000); // 0.1% + var MAX_WITHDRAWAL_FEE_INCREASE = new Fee(3, 2); // 1.5x + + ulong oldNum, oldDenom; + if (oldWithdrawalFee.Denominator == 0 || oldWithdrawalFee.Numerator == 0) + { + oldNum = WITHDRAWAL_BASELINE_FEE.Numerator; + oldDenom = WITHDRAWAL_BASELINE_FEE.Denominator; + } + else + { + oldNum = oldWithdrawalFee.Numerator; + oldDenom = oldWithdrawalFee.Denominator; + } + + // Check that new_fee / old_fee <= MAX_WITHDRAWAL_FEE_INCREASE + try + { + BigInteger left = (BigInteger)oldNum * Denominator * MAX_WITHDRAWAL_FEE_INCREASE.Numerator; + BigInteger right = (BigInteger)Numerator * oldDenom * MAX_WITHDRAWAL_FEE_INCREASE.Denominator; + + if (left < right) + { + throw new StakePoolFeeException("Fee increase exceeds maximum allowed."); + } + } + catch + { + throw new StakePoolFeeException("Calculation failure in withdrawal fee check."); + } + } + + /// + /// Returns a string representation of the fee. + /// + public override string ToString() + { + if (Numerator > 0 && Denominator > 0) + return $"{Numerator}/{Denominator}"; + return "none"; + } + } + + /// + /// Exception for stake pool fee errors. + /// + public class StakePoolFeeException : System.Exception + { + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// + public StakePoolFeeException(string message) : base(message) { } + } +} diff --git a/src/Solnet.Programs/StakePool/Models/FeeType.cs b/src/Solnet.Programs/StakePool/Models/FeeType.cs new file mode 100644 index 00000000..229262c5 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/FeeType.cs @@ -0,0 +1,166 @@ +using System; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// The type of fees that can be set on the stake pool. + /// + public abstract class FeeType + { + /// + /// Represents a referral fee type with a specified percentage. + /// + /// This class is used to define a referral fee as a percentage value. The percentage is + /// immutable and must be specified at the time of instantiation. + public class SolReferral : FeeType + { + /// + /// Gets the percentage value represented as a byte. + /// + public byte Percentage { get; } + /// + /// Initializes a new instance of the class with the specified referral + /// percentage. + /// + /// The referral percentage to be applied. Must be a value between 0 and 100, inclusive. + public SolReferral(byte percentage) => Percentage = percentage; + } + + /// + /// Represents a referral fee type with a specified percentage for staking rewards. + /// + /// This class is used to define a referral fee as a percentage of staking rewards. The + /// percentage value is immutable and must be specified at the time of instantiation. + public class StakeReferral : FeeType + { + /// + /// Gets the percentage value represented as a byte. + /// + public byte Percentage { get; } + /// + /// Represents a referral with a specified stake percentage. + /// + /// The parameter defines the proportion of the + /// stake allocated to the referral. + /// The percentage of the stake associated with the referral. Must be a value between 0 and 100. + public StakeReferral(byte percentage) => Percentage = percentage; + } + + /// + /// Represents an epoch in the fee structure, associated with a specific fee. + /// + /// This class is used to define a specific epoch and its corresponding fee. It inherits + /// from the base class. + public class Epoch : FeeType + { + /// + /// Gets the fee associated with the transaction. + /// + public Fee Fee { get; } + /// + /// Represents a specific epoch with an associated fee. + /// + /// The fee associated with the epoch. Cannot be null. + public Epoch(Fee fee) => Fee = fee; + } + + /// + /// Represents a withdrawal of staked funds, including an associated fee. + /// + /// This class encapsulates the details of a stake withdrawal operation, including the + /// fee applied to the withdrawal. It inherits from . + public class StakeWithdrawal : FeeType + { + /// + /// Gets the fee associated with the transaction. + /// + public Fee Fee { get; } + /// + /// Initializes a new instance of the class with the specified fee. + /// + /// The fee associated with the stake withdrawal. This value cannot be null. + public StakeWithdrawal(Fee fee) => Fee = fee; + } + + /// + /// Represents a deposit fee type specific to Solana transactions. + /// + /// This class encapsulates the fee information for a Solana deposit transaction. It + /// inherits from the base class. + public class SolDeposit : FeeType + { + /// + /// Gets the fee associated with the transaction. + /// + public Fee Fee { get; } + /// + /// Initializes a new instance of the class with the specified fee. + /// + /// The fee associated with the deposit. This value cannot be null. + public SolDeposit(Fee fee) => Fee = fee; + } + + /// + /// Represents a deposit required for staking, associated with a specific fee. + /// + /// This class encapsulates the concept of a staking deposit, which includes a fee that + /// must be paid. It inherits from , providing additional context for fee-related + /// operations. + public class StakeDeposit : FeeType + { + /// + /// Gets the fee associated with the transaction. + /// + public Fee Fee { get; } + /// + /// Initializes a new instance of the class with the specified fee. + /// + /// The fee associated with the stake deposit. Cannot be null. + public StakeDeposit(Fee fee) => Fee = fee; + } + + /// + /// Represents a withdrawal operation for Solana (SOL) that includes an associated fee. + /// + /// This class encapsulates the details of a Solana withdrawal, including the fee + /// required for the transaction. + public class SolWithdrawal : FeeType + { + /// + /// Gets the fee associated with the transaction. + /// + public Fee Fee { get; } + /// + /// Initializes a new instance of the class with the specified fee. + /// + /// The fee associated with the withdrawal. This value cannot be null. + public SolWithdrawal(Fee fee) => Fee = fee; + } + + /// + /// Checks if the provided fee is too high. + /// + public bool IsTooHigh() + { + return this switch + { + SolReferral s => s.Percentage > 100, + StakeReferral s => s.Percentage > 100, + Epoch e => e.Fee.Numerator > e.Fee.Denominator, + StakeWithdrawal s => s.Fee.Numerator > s.Fee.Denominator, + SolWithdrawal s => s.Fee.Numerator > s.Fee.Denominator, + SolDeposit s => s.Fee.Numerator > s.Fee.Denominator, + StakeDeposit s => s.Fee.Numerator > s.Fee.Denominator, + _ => false + }; + } + + /// + /// Returns true if the contained fee can only be updated earliest on the next epoch. + /// + public bool CanOnlyChangeNextEpoch() + { + return this is StakeWithdrawal or SolWithdrawal or Epoch; + } + } +} diff --git a/src/Solnet.Programs/StakePool/Models/FundingType.cs b/src/Solnet.Programs/StakePool/Models/FundingType.cs new file mode 100644 index 00000000..ea742cef --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/FundingType.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Represents the type of funding operation in the stake pool. + /// + public enum FundingType + { + /// + /// A deposit of a stake account. + /// + StakeDeposit = 0, + + /// + /// A deposit of SOL tokens. + /// + SolDeposit = 1, + + /// + /// A withdrawal of SOL tokens. + /// + SolWithdraw = 2, + } +} diff --git a/src/Solnet.Programs/StakePool/Models/FutureEpoch.cs b/src/Solnet.Programs/StakePool/Models/FutureEpoch.cs new file mode 100644 index 00000000..fa136db5 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/FutureEpoch.cs @@ -0,0 +1,99 @@ +using System; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Wrapper type that "counts down" epochs, similar to Rust's Option, with three states. + /// + /// The type of the value being wrapped. + public abstract class FutureEpoch + { + private FutureEpoch() { } + + /// + /// Represents the None state (no value set). + /// + public sealed class None : FutureEpoch + { + internal None() { } + } + + /// + /// Value is ready after the next epoch boundary. + /// + public sealed class One : FutureEpoch + { + /// + /// Gets the value stored in the current instance. + /// + public T Value { get; } + /// + /// Initializes a new instance of the class with the specified value. + /// + /// The value to be assigned to the instance. + public One(T value) => Value = value; + } + + /// + /// Value is ready after two epoch boundaries. + /// + public sealed class Two : FutureEpoch + { + /// + /// Gets the value stored in the current instance. + /// + public T Value { get; } + /// + /// Initializes a new instance of the class with the specified value. + /// + /// The value to initialize the instance with. + public Two(T value) => Value = value; + } + + /// + /// Create a new value to be unlocked in two epochs. + /// + public static FutureEpoch New(T value) => new Two(value); + + /// + /// Returns the value if it's ready (i.e., in the One state), otherwise null. + /// + public T? Get() + { + return this is One one ? one.Value : default; + } + + /// + /// Update the epoch, to be done after getting the underlying value. + /// + public FutureEpoch UpdateEpoch() + { + return this switch + { + None => this, + One => new None(), + Two two => new One(two.Value), + _ => throw new InvalidOperationException() + }; + } + + /// + /// Converts the FutureEpoch to an Option-like value (null if None, value otherwise). + /// + public T? ToOption() + { + return this switch + { + None => default, + One one => one.Value, + Two two => two.Value, + _ => default + }; + } + + /// + /// Returns a None instance. + /// + public static FutureEpoch NoneValue { get; } = new None(); + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/StakePool/Models/PodStakeStatus.cs b/src/Solnet.Programs/StakePool/Models/PodStakeStatus.cs new file mode 100644 index 00000000..6fdc9691 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/PodStakeStatus.cs @@ -0,0 +1,115 @@ +using System; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Wrapper struct that can be used as a Pod, containing a byte that should be a valid StakeStatus underneath. + /// + public struct PodStakeStatus : IEquatable + { + public byte Value; + + /// + /// Represents the status of a pod's stake as a byte value. + /// + /// The byte value representing the pod's stake status. + public PodStakeStatus(byte value) + { + Value = value; + } + + /// + /// Creates a new instance of from a value. + /// + /// + /// + public static PodStakeStatus FromStakeStatus(StakeStatus status) => new PodStakeStatus((byte)status); + + /// + /// Converts the current value to a enumeration. + /// + /// A value that corresponds to the current value. + /// Thrown if the current value does not correspond to a valid enumeration value. + public StakeStatus ToStakeStatus() + { + if (Enum.IsDefined(typeof(StakeStatus), Value)) + return (StakeStatus)Value; + throw new InvalidOperationException("Invalid StakeStatus value."); + } + + /// + /// Downgrade the status towards ready for removal by removing the validator stake. + /// + public void RemoveValidatorStake() + { + var status = ToStakeStatus(); + StakeStatus newStatus = status switch + { + StakeStatus.Active or StakeStatus.DeactivatingTransient or StakeStatus.ReadyForRemoval => status, + StakeStatus.DeactivatingAll => StakeStatus.DeactivatingTransient, + StakeStatus.DeactivatingValidator => StakeStatus.ReadyForRemoval, + _ => throw new InvalidOperationException("Invalid StakeStatus value.") + }; + Value = (byte)newStatus; + } + + /// + /// Downgrade the status towards ready for removal by removing the transient stake. + /// + public void RemoveTransientStake() + { + var status = ToStakeStatus(); + StakeStatus newStatus = status switch + { + StakeStatus.Active or StakeStatus.DeactivatingValidator or StakeStatus.ReadyForRemoval => status, + StakeStatus.DeactivatingAll => StakeStatus.DeactivatingValidator, + StakeStatus.DeactivatingTransient => StakeStatus.ReadyForRemoval, + _ => throw new InvalidOperationException("Invalid StakeStatus value.") + }; + Value = (byte)newStatus; + } + + /// + /// Converts a instance to a instance. + /// + /// The instance to convert. + public static explicit operator PodStakeStatus(StakeStatus status) => FromStakeStatus(status); + + /// + /// Converts a instance to a instance. + /// + /// The instance to convert. + public static explicit operator StakeStatus(PodStakeStatus pod) + { + return pod.ToStakeStatus(); + } + + /// + /// Determines whether the specified object is equal to the current instance. + /// + /// The object to compare with the current instance. + /// if the specified object is of type PodStakeStatus and is equal to the current + /// instance; otherwise, . + public override bool Equals(object obj) => obj is PodStakeStatus other && Equals(other); + + /// + /// Determines whether the current instance is equal to another instance. + /// + /// The instance to compare with the current instance. + /// if the of the current instance is equal to the of the specified instance; otherwise, . + public bool Equals(PodStakeStatus other) => Value == other.Value; + + /// + /// Returns a hash code for the current object. + /// + /// The hash code is derived from the property. It is suitable for + /// use in hashing algorithms and data structures such as hash tables. + /// An integer that represents the hash code for the current object. + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(PodStakeStatus left, PodStakeStatus right) => left.Equals(right); + + public static bool operator !=(PodStakeStatus left, PodStakeStatus right) => !(left == right); + } +} diff --git a/src/Solnet.Programs/StakePool/Models/PreferredValidatorType.cs b/src/Solnet.Programs/StakePool/Models/PreferredValidatorType.cs new file mode 100644 index 00000000..1043b0de --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/PreferredValidatorType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Represents the type of preferred validator in a stake pool. + /// + public enum PreferredValidatorType + { + /// + /// Preferred validator for deposit operations. + /// + Deposit, + + /// + /// Preferred validator for withdraw operations. + /// + Withdraw, + } +} diff --git a/src/Solnet.Programs/StakePool/Models/StakePool.cs b/src/Solnet.Programs/StakePool/Models/StakePool.cs new file mode 100644 index 00000000..335ada52 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/StakePool.cs @@ -0,0 +1,328 @@ +using Solnet.Programs.TokenSwap.Models; +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using static Solnet.Programs.Models.Stake.State; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Represents a Stake Pool in the Solana blockchain. + /// + public class StakePool + { + /// + /// Gets or sets the type of the account. + /// + public AccountType AccountType { get; set; } + + /// + /// The public key of the stake pool. + /// + public PublicKey Manager { get; set; } + + /// + /// The public key of the staker. + /// + public PublicKey Staker { get; set; } + + /// + /// The public key of the Deposit Authority. + /// + public PublicKey StakeDepositAuthority { get; set; } + + /// + /// Gets or sets the bump seed associated with the stake withdrawal operation. + /// + public PublicKey StakeWithdrawBumpSeed { get; set; } + + /// + /// The public key of the validator list. + /// + public PublicKey ValidatorList { get; set; } + + /// + /// The public key of the reserve stake account. + /// + public PublicKey ReserveStake { get; set; } + + /// + /// The public key of the pool mint. + /// + public PublicKey PoolMint { get; set; } + + /// + /// The public key of the manager fee account. + /// + public PublicKey ManagerFeeAccount { get; set; } + + /// + /// The public key of the token program ID. + /// + public PublicKey TokenProgramId { get; set; } + + /// + /// The total lamports in the stake pool. + /// + public ulong TotalLamports { get; set; } + + /// + /// The total supply of pool tokens. + /// + public ulong PoolTokenSupply { get; set; } + + /// + /// The epoch of the last update. + /// + public ulong LastUpdateEpoch { get; set; } + + /// + /// The lockup configuration for the stake pool. + /// + public Lockup Lockup { get; set; } + + /// + /// The fee for the current epoch. + /// + public Fee EpochFee { get; set; } + + /// + /// The fee for the next epoch. + /// + public Fee NextEpochFee { get; set; } + + /// + /// The preferred validator vote address for deposits. + /// + public PublicKey PreferredDepositValidatorVoteAddress { get; set; } + + /// + /// The preferred validator vote address for withdrawals. + /// + public PublicKey PreferredWithdrawValidatorVoteAddress { get; set; } + + /// + /// The fee for stake deposits. + /// + public Fee StakeDepositFee { get; set; } + + /// + /// The fee for stake withdrawals. + /// + public Fee StakeWithdrawalFee { get; set; } + + /// + /// The fee for the next stake withdrawals. + /// + public Fee NextStakeWithdrawalFees { get; set; } + + /// + /// The referral fee for stake operations. + /// + public byte StakeReferralFee { get; set; } + + /// + /// The public key of the SOL deposit authority. + /// + public PublicKey SolDepositAuthority { get; set; } + + /// + /// The fee for SOL deposits. + /// + public Fee SolDepositFee { get; set; } + + /// + /// The referral fee for SOL operations. + /// + public byte SolReferralFee { get; set; } + + /// + /// The public key of the SOL withdrawal authority. + /// + public PublicKey SolWithdrawAuthority { get; set; } + + /// + /// The fee for SOL withdrawals. + /// + public Fee SolWithdrawalFee { get; set; } + + /// + /// The fee for the next SOL withdrawals. + /// + public Fee NextSolWithdrawalFee { get; set; } + + /// + /// The pool token supply at the last epoch. + /// + public ulong LastEpochPoolTokenSupply { get; set; } + + /// + /// The total lamports at the last epoch. + /// + public ulong LastEpochTotalLamports { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public StakePool() + { + // Initialize properties + } + + /// + /// Calculates the pool tokens that should be minted for a deposit of stake lamports. + /// + public ulong? CalcPoolTokensForDeposit(ulong stakeLamports) + { + if (TotalLamports == 0 || PoolTokenSupply == 0) + return stakeLamports; + + try + { + BigInteger tokens = (BigInteger)stakeLamports * PoolTokenSupply; + BigInteger result = tokens / TotalLamports; + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Calculates the lamports amount on withdrawal. + /// + public ulong? CalcLamportsWithdrawAmount(ulong poolTokens) + { + BigInteger numerator = (BigInteger)poolTokens * TotalLamports; + BigInteger denominator = PoolTokenSupply; + if (denominator == 0 || numerator < denominator) + return 0; + try + { + BigInteger result = numerator / denominator; + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Calculates pool tokens to be deducted as stake withdrawal fees. + /// + public ulong? CalcPoolTokensStakeWithdrawalFee(ulong poolTokens) + { + return StakeWithdrawalFee?.Apply(poolTokens); + } + + /// + /// Calculates pool tokens to be deducted as SOL withdrawal fees. + /// + public ulong? CalcPoolTokensSolWithdrawalFee(ulong poolTokens) + { + return SolWithdrawalFee?.Apply(poolTokens); + } + + /// + /// Calculates pool tokens to be deducted as stake deposit fees. + /// + public ulong? CalcPoolTokensStakeDepositFee(ulong poolTokensMinted) + { + return StakeDepositFee?.Apply(poolTokensMinted); + } + + /// + /// Calculates pool tokens to be deducted from deposit fees as referral fees. + /// + public ulong? CalcPoolTokensStakeReferralFee(ulong stakeDepositFee) + { + try + { + BigInteger result = (BigInteger)stakeDepositFee * StakeReferralFee / 100; + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Calculates pool tokens to be deducted as SOL deposit fees. + /// + public ulong? CalcPoolTokensSolDepositFee(ulong poolTokensMinted) + { + return SolDepositFee?.Apply(poolTokensMinted); + } + + /// + /// Calculates pool tokens to be deducted from SOL deposit fees as referral fees. + /// + public ulong? CalcPoolTokensSolReferralFee(ulong solDepositFee) + { + try + { + BigInteger result = (BigInteger)solDepositFee * SolReferralFee / 100; + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Calculates the fee in pool tokens that goes to the manager for a given reward lamports. + /// + public ulong? CalcEpochFeeAmount(ulong rewardLamports) + { + if (rewardLamports == 0) + return 0; + + BigInteger totalLamports = (BigInteger)TotalLamports + rewardLamports; + var feeLamports = EpochFee?.Apply(rewardLamports) ?? 0; + + if (totalLamports == feeLamports || PoolTokenSupply == 0) + return rewardLamports; + + try + { + BigInteger result = (BigInteger)PoolTokenSupply * feeLamports / + (totalLamports - feeLamports); + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + + /// + /// Gets the current value of pool tokens, rounded up. + /// + public ulong? GetLamportsPerPoolToken() + { + try + { + BigInteger result = ((BigInteger)TotalLamports + PoolTokenSupply - 1) / PoolTokenSupply; + if (result < 0 || result > ulong.MaxValue) return null; + return (ulong)result; + } + catch + { + return null; + } + } + } +} diff --git a/src/Solnet.Programs/StakePool/Models/StakePoolExtensions.cs b/src/Solnet.Programs/StakePool/Models/StakePoolExtensions.cs new file mode 100644 index 00000000..d4caec24 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/StakePoolExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + // TODO: This needs to be implemented after Token2022 + //public static class StakePoolExtensions + //{ + + /// + /// Checks if the given extension is supported for the stake pool mint. + /// + /// The extension type to check. + /// True if supported, false otherwise. + //public static bool IsExtensionSupportedForMint(ExtensionType extensionType) + //{ + // // Note: This list must match the Rust SUPPORTED_EXTENSIONS array. + // return extensionType == ExtensionType.Uninitialized + // || extensionType == ExtensionType.TransferFeeConfig + // || extensionType == ExtensionType.ConfidentialTransferMint + // || extensionType == ExtensionType.ConfidentialTransferFeeConfig + // || extensionType == ExtensionType.DefaultAccountState + // || extensionType == ExtensionType.InterestBearingConfig + // || extensionType == ExtensionType.MetadataPointer + // || extensionType == ExtensionType.TokenMetadata; + //} + // /// + // /// Checks if the given extension is supported for the stake pool's fee account. + // /// + // /// The extension type to check. + // /// True if supported, false otherwise. + // public static bool IsExtensionSupportedForFeeAccount(ExtensionType extensionType) + // { + // // Note: This does not include ConfidentialTransferAccount for the same reason as in Rust. + // return extensionType == ExtensionType.Uninitialized + // || extensionType == ExtensionType.TransferFeeAmount + // || extensionType == ExtensionType.ImmutableOwner + // || extensionType == ExtensionType.CpiGuard; + // } + //} +} diff --git a/src/Solnet.Programs/StakePool/Models/StakeStatus.cs b/src/Solnet.Programs/StakePool/Models/StakeStatus.cs new file mode 100644 index 00000000..4b2f85ce --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/StakeStatus.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Represents the status of a stake account in the stake pool. + /// + public enum StakeStatus : byte + { + /// + /// Stake account is active, there may be a transient stake as well. + /// + Active = 0, + + /// + /// Only transient stake account exists, when a transient stake is deactivating during validator removal. + /// + DeactivatingTransient = 1, + + /// + /// No more validator stake accounts exist, entry ready for removal during UpdateStakePoolBalance. + /// + ReadyForRemoval = 2, + + /// + /// Only the validator stake account is deactivating, no transient stake account exists. + /// + DeactivatingValidator = 3, + + /// + /// Both the transient and validator stake account are deactivating, when a validator is removed with a transient stake active. + /// + DeactivatingAll = 4 + } +} diff --git a/src/Solnet.Programs/StakePool/Models/StakeWithdrawSource.cs b/src/Solnet.Programs/StakePool/Models/StakeWithdrawSource.cs new file mode 100644 index 00000000..cb940f2e --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/StakeWithdrawSource.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Withdrawal type, figured out during process_withdraw_stake. + /// + internal enum StakeWithdrawSource : byte + { + /// + /// Some of an active stake account, but not all. + /// + Active = 0, + + /// + /// Some of a transient stake account. + /// + Transient = 1, + + /// + /// Take a whole validator stake account. + /// + ValidatorRemoval = 2 + } +} diff --git a/src/Solnet.Programs/StakePool/Models/ValidatorList.cs b/src/Solnet.Programs/StakePool/Models/ValidatorList.cs new file mode 100644 index 00000000..2ca053d5 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/ValidatorList.cs @@ -0,0 +1,99 @@ +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Storage list for all validator stake accounts in the pool. + /// + public class ValidatorList + { + /// + /// Data outside of the validator list, separated out for cheaper deserializations. + /// + public ValidatorListHeader Header { get; set; } + + /// + /// List of stake info for each validator in the pool. + /// + public List Validators { get; set; } + + /// + /// Creates an empty instance containing space for validators. + /// + /// Maximum number of validators. + /// A new instance of . + public static ValidatorList New(uint maxValidators) + { + return new ValidatorList + { + Header = new ValidatorListHeader + { + AccountType = AccountType.ValidatorList, + MaxValidators = maxValidators + }, + Validators = Enumerable.Repeat(new ValidatorStakeInfo(), (int)maxValidators).ToList() + }; + } + + /// + /// Calculate the number of validator entries that fit in the provided buffer length. + /// Assumes that defines a constant LEN for header length. + /// + /// The total buffer length. + /// The maximum number of validator entries. + public static int CalculateMaxValidators(int bufferLength) + { + // Add 4 additional bytes to the serialized header length (as in the original Rust code). + int headerSize = (new ValidatorListHeader()).GetSerializedLength() + 4; + return (bufferLength - headerSize) / ValidatorStakeInfo.Length; + } + + /// + /// Checks if the list contains a validator with the given vote account address. + /// + /// The vote account public key. + /// true if found; otherwise, false. + public bool Contains(PublicKey voteAccountAddress) + { + return Validators.Any(x => x.VoteAccountAddress.Equals(voteAccountAddress)); + } + + /// + /// Finds a mutable reference to the with the given vote account address. + /// + /// The vote account public key. + /// + /// A reference to the matching if found; otherwise, null. + /// + public ValidatorStakeInfo FindMut(PublicKey voteAccountAddress) + { + return Validators.FirstOrDefault(x => x.VoteAccountAddress.Equals(voteAccountAddress)); + } + + /// + /// Finds an immutable reference to the with the given vote account address. + /// + /// The vote account public key. + /// + /// A reference to the matching if found; otherwise, null. + /// + public ValidatorStakeInfo Find(PublicKey voteAccountAddress) + { + return Validators.FirstOrDefault(x => x.VoteAccountAddress.Equals(voteAccountAddress)); + } + + /// + /// Checks if the list contains any validator with active stake. + /// + /// true if any validator's active stake lamports are greater than zero; otherwise, false. + public bool HasActiveStake() + { + return Validators.Any(x => x.ActiveStakeLamports > 0); + } + } +} diff --git a/src/Solnet.Programs/StakePool/Models/ValidatorListHeader.cs b/src/Solnet.Programs/StakePool/Models/ValidatorListHeader.cs new file mode 100644 index 00000000..cb886b9b --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/ValidatorListHeader.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Helper type to deserialize just the start of a ValidatorList. + /// + public class ValidatorListHeader + { + /// + /// Account type, must be ValidatorList currently. + /// + public AccountType AccountType { get; set; } + + /// + /// Maximum allowable number of validators. + /// + public uint MaxValidators { get; set; } + + /// + /// Dynamically calculates the serialized length of the header. + /// + /// The length in bytes. + public int GetSerializedLength() + { + // For example, assume: + // AccountType: 1 byte + // MaxValidators: 4 bytes + return 1 + 4; + } + } +} diff --git a/src/Solnet.Programs/StakePool/Models/ValidatorStakeInfo.cs b/src/Solnet.Programs/StakePool/Models/ValidatorStakeInfo.cs new file mode 100644 index 00000000..1886dbb6 --- /dev/null +++ b/src/Solnet.Programs/StakePool/Models/ValidatorStakeInfo.cs @@ -0,0 +1,165 @@ +using System; +using Solnet.Wallet; + +namespace Solnet.Programs.StakePool.Models +{ + /// + /// Information about a validator in the pool. + /// + /// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS + /// THERE'S AN EXTREMELY GOOD REASON. + /// + /// To save on BPF instructions, the serialized bytes are reinterpreted with a + /// bytemuck transmute, which means that this structure cannot have any + /// undeclared alignment-padding in its representation. + /// + public class ValidatorStakeInfo + { + /// + /// Represents the fixed length of the data structure. + /// + /// This constant defines the length as 73 and is intended to be used wherever the fixed + /// size is required. + public const int Length = 73; + + /// + /// Amount of lamports on the validator stake account, including rent. + /// + public ulong ActiveStakeLamports { get; set; } + + /// + /// Amount of transient stake delegated to this validator. + /// + public ulong TransientStakeLamports { get; set; } + + /// + /// Last epoch the active and transient stake lamports fields were updated. + /// + public ulong LastUpdateEpoch { get; set; } + + /// + /// Transient account seed suffix, used to derive the transient stake account address. + /// + public ulong TransientSeedSuffix { get; set; } + + /// + /// Unused space, initially meant to specify the end of seed suffixes. + /// + public uint Unused { get; set; } + + /// + /// Validator account seed suffix (0 means None). + /// + public uint ValidatorSeedSuffix { get; set; } + + /// + /// Status of the validator stake account. + /// + public PodStakeStatus Status { get; set; } + + /// + /// Validator vote account address. + /// + public PublicKey VoteAccountAddress { get; set; } + + /// + /// Get the total lamports on this validator (active and transient). + /// Returns null if overflow occurs. + /// + public ulong? StakeLamports() + { + try + { + checked + { + return ActiveStakeLamports + TransientStakeLamports; + } + } + catch (OverflowException) + { + return null; + } + } + + /// + /// Performs a very cheap comparison, for checking if this validator stake info matches the vote account address. + /// + public static bool MemcmpPubkey(ReadOnlySpan data, PublicKey voteAddress) + { + // VoteAccountAddress is at offset 41, length 32 + return data.Slice(41, 32).SequenceEqual(voteAddress.KeyBytes); + } + + /// + /// Checks if this validator stake info has more active lamports than some limit. + /// + public static bool ActiveLamportsGreaterThan(ReadOnlySpan data, ulong lamports) + { + // ActiveStakeLamports is at offset 0, length 8 + ulong value = BitConverter.ToUInt64(data.Slice(0, 8)); + return value > lamports; + } + + /// + /// Checks if this validator stake info has more transient lamports than some limit. + /// + public static bool TransientLamportsGreaterThan(ReadOnlySpan data, ulong lamports) + { + // TransientStakeLamports is at offset 8, length 8 + ulong value = BitConverter.ToUInt64(data.Slice(8, 8)); + return value > lamports; + } + + /// + /// Check that the validator stake info is valid (not removed). + /// + public static bool IsNotRemoved(ReadOnlySpan data) + { + // Status is at offset 40, 1 byte + return (StakeStatus)data[40] != StakeStatus.ReadyForRemoval; + } + + /// + /// Packs this instance into a 73-byte array. + /// + public byte[] Pack() + { + var data = new byte[Length]; + int offset = 0; + + BitConverter.GetBytes(ActiveStakeLamports).CopyTo(data, offset); offset += 8; + BitConverter.GetBytes(TransientStakeLamports).CopyTo(data, offset); offset += 8; + BitConverter.GetBytes(LastUpdateEpoch).CopyTo(data, offset); offset += 8; + BitConverter.GetBytes(TransientSeedSuffix).CopyTo(data, offset); offset += 8; + BitConverter.GetBytes(Unused).CopyTo(data, offset); offset += 4; + BitConverter.GetBytes(ValidatorSeedSuffix).CopyTo(data, offset); offset += 4; + data[offset++] = Status.Value; + VoteAccountAddress.KeyBytes.CopyTo(data, offset); + + return data; + } + + /// + /// Unpacks a 73-byte array into a ValidatorStakeInfo instance. + /// + public static ValidatorStakeInfo Unpack(ReadOnlySpan data) + { + if (data.Length != Length) + throw new ArgumentException($"Data must be {Length} bytes", nameof(data)); + + int offset = 0; + var info = new ValidatorStakeInfo + { + ActiveStakeLamports = BitConverter.ToUInt64(data.Slice(offset, 8)), + TransientStakeLamports = BitConverter.ToUInt64(data.Slice(offset += 8, 8)), + LastUpdateEpoch = BitConverter.ToUInt64(data.Slice(offset += 8, 8)), + TransientSeedSuffix = BitConverter.ToUInt64(data.Slice(offset += 8, 8)), + Unused = BitConverter.ToUInt32(data.Slice(offset += 8, 4)), + ValidatorSeedSuffix = BitConverter.ToUInt32(data.Slice(offset += 4, 4)), + Status = new PodStakeStatus(data[offset += 4]), + VoteAccountAddress = new PublicKey(data.Slice(offset + 1, 32).ToArray()) + }; + return info; + } + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/StakePool/StakePoolProgram.cs b/src/Solnet.Programs/StakePool/StakePoolProgram.cs new file mode 100644 index 00000000..7d31d353 --- /dev/null +++ b/src/Solnet.Programs/StakePool/StakePoolProgram.cs @@ -0,0 +1,2626 @@ +using Solnet.Rpc.Models; +using Solnet.Wallet; +using System.Collections.Generic; +using Solnet.Programs.Abstract; +using System; +using System.Text; +using Solnet.Programs.StakePool.Models; +using static Solnet.Programs.Models.Stake.State; +using System.Linq; + +namespace Solnet.Programs.StakePool +{ + /// + /// Implements the Stake Pool Program methods. + /// + /// For more information see: + /// https://spl.solana.com/stake-pool + /// https://docs.rs/spl-stake-pool/latest/spl_stake_pool/ + /// + /// + public class StakePoolProgram: BaseProgram + { + /// + /// SPL Stake Pool Program ID + /// + public static readonly PublicKey StakePoolProgramIdKey = new("SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy"); + + /// + /// Mpl Token Metadata Program ID + /// + public static readonly PublicKey MplTokenMetadataProgramIdKey = new("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + + /// + /// SPL Stake Pool Program Name + /// + public static readonly string StakePoolProgramName = "Stake Pool Program"; + + // Instance vars + + /// + /// The owner key required to use as the fee account owner. + /// + public virtual PublicKey OwnerKey => new("HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN"); + + /// + /// Represents the byte array encoding of the ASCII string "withdraw". + /// + /// This field is used to identify the "withdraw" authority in a byte array format. It is + /// encoded using ASCII encoding. + private static readonly byte[] AUTHORITY_WITHDRAW = Encoding.ASCII.GetBytes("withdraw"); + + /// + /// Represents the seed for deposit authority. + /// + /// This seed identifies the "deposit" authority for a stake pool in a byte array format. + private static readonly byte[] AUTHORITY_DEPOSIT = Encoding.ASCII.GetBytes("deposit"); + + /// + /// Represents the prefix used for generating transient stake seeds. + /// + /// This prefix is encoded as an ASCII byte array and is used in conjunction with other + /// data to generate unique transient stake seeds. The value is constant and cannot be modified. + private static readonly byte[] TRANSIENT_STAKE_SEED_PREFIX = + Encoding.ASCII.GetBytes("transient"); + + /// + /// Represents the seed for ephemeral stake account. + /// + private static readonly byte[] EPHEMERAL_STAKE_SEED_PREFIX = Encoding.ASCII.GetBytes("ephemeral"); + + /// + /// Stake Pool account layout size. + /// + public static readonly ulong StakePoolAccountDataSize = 255; + + /// + /// Minimum amount of staked lamports required in a validator stake account to + /// allow for merges without a mismatch on credits observed. + /// + public const ulong MINIMUM_ACTIVE_STAKE = 1_000_000; + + /// + /// Minimum amount of lamports in the reserve. + /// + public const ulong MINIMUM_RESERVE_LAMPORTS = 0; + + /// + /// Maximum amount of validator stake accounts to update per + /// UpdateValidatorListBalance instruction, based on compute limits. + /// + public const int MAX_VALIDATORS_TO_UPDATE = 5; + + /// + /// Maximum factor by which a withdrawal fee can be increased per epoch, + /// protecting stakers from malicious users. + /// If current fee is 0, WITHDRAWAL_BASELINE_FEE is used as the baseline. + /// + public static readonly Fee MAX_WITHDRAWAL_FEE_INCREASE = new Fee(3, 2); + + /// + /// Drop-in baseline fee when evaluating withdrawal fee increases when fee is 0. + /// + public static readonly Fee WITHDRAWAL_BASELINE_FEE = new Fee(1, 1000); + + /// + /// The maximum number of transient stake accounts respecting transaction account limits. + /// + public const int MAX_TRANSIENT_STAKE_ACCOUNTS = 10; + + /// + /// Represents the Stake Pool Program, a specialized program for managing stake pools on the Solana blockchain. + /// + /// This program provides functionality for interacting with stake pools, including + /// creating, managing, and querying stake pool accounts. It is identified by the program ID key and name + /// specific to the Stake Pool Program. + public StakePoolProgram() : base(StakePoolProgramIdKey, StakePoolProgramName) { } + + /// + /// Initializes a new instance of the class with the specified program ID. + /// + /// The public key identifying the stake pool program. + public StakePoolProgram(PublicKey programId) : base(programId, StakePoolProgramName) { } + + #region Transaction Methods + + /// + /// Creates an Initialize instruction (initialize a new stake pool). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction Initialize( + PublicKey stakePoolAccount, + PublicKey manager, + PublicKey staker, + PublicKey withdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey poolMint, + PublicKey managerPoolAccount, + PublicKey tokenProgramId, + Fee fee, + Fee withdrawalFee, + Fee depositFee, + Fee referralFee, + PublicKey depositAuthority = null, + uint? maxValidators = null + ) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeInitializeData(fee, withdrawalFee, depositFee, referralFee, maxValidators); + // Prepare the accounts for the instruction + var keys = new List + { + AccountMeta.Writable(stakePoolAccount, false), + AccountMeta.ReadOnly(manager, true), + AccountMeta.ReadOnly(staker, false), + AccountMeta.ReadOnly(withdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.ReadOnly(reserveStake, false), + AccountMeta.Writable(poolMint, false), + AccountMeta.Writable(managerPoolAccount, false), + AccountMeta.ReadOnly(tokenProgramId, false) + }; + + if (depositAuthority != null) + { + keys.Add(AccountMeta.ReadOnly(depositAuthority, false)); + } + + // Return the transaction instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates an AddValidatorToPool instruction (add validator stake account to the pool). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction AddValidatorToPool( + PublicKey stakePoolAccount, + PublicKey staker, + PublicKey reserve, + PublicKey stakePoolWithdraw, + PublicKey validatorList, + PublicKey stakeAccount, + PublicKey validatorAccount, + uint? seed = null + ) + { + // dont allow zero seed values + if (seed == 0) + throw new ArgumentException("Value must be nonzero.", nameof(seed)); + + // Prepare the instruction data + var data = StakePoolProgramData.EncodeAddValidatorToPoolData(seed); + + // Prepare the accounts for the instruction + List keys = new() + { + AccountMeta.Writable(stakePoolAccount, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.Writable(reserve, false), + AccountMeta.ReadOnly(stakePoolWithdraw, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(stakeAccount, false), + AccountMeta.ReadOnly(validatorAccount, false), + AccountMeta.ReadOnly(SysVars.RentKey, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates an RemoveValidatorFromPool instruction (remove validator stake account from the pool). + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction RemoveValidatorFromPool( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdraw, + PublicKey validatorList, + PublicKey stakeAccount, + PublicKey transientStakeAccount + ) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeRemoveValidatorFromPoolData(); + + // Prepare the accounts that will be involved in this instruction + List keys = new() + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdraw, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(stakeAccount, false), + AccountMeta.Writable(transientStakeAccount, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Return the TransactionInstruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates the 'DecreaseValidatorStake' instruction (rebalance from validator account to transient account). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [Obsolete("Use 'decrease_validator_stake_with_reserve' instead")] + public virtual TransactionInstruction DecreaseValidatorStake( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey validatorStake, + PublicKey transientStake, + ulong lamports, + ulong transientStakeSeed) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeDecreaseValidatorStakeData(lamports, transientStakeSeed); + + // Prepare the accounts for the instruction + var keys = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(validatorStake, false), + AccountMeta.Writable(transientStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.RentKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Return the transaction instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates the 'DecreaseAdditionalValidatorStake' instruction (rebalance from validator account to transient account). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction DecreaseAdditionalValidatorStake( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey validatorStake, + PublicKey ephemeralStake, + PublicKey transientStake, + ulong lamports, + ulong transientStakeSeed, + ulong ephemeralStakeSeed) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeDecreaseAdditionalValidatorStakeData(lamports, transientStakeSeed, ephemeralStakeSeed); + + // Prepare the accounts for the instruction + var keys = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.Writable(validatorStake, false), + AccountMeta.Writable(ephemeralStake, false), + AccountMeta.Writable(transientStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Return the transaction instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates the 'DecreaseValidatorStakeWithReserve' instruction (rebalance from validator account to transient account). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction DecreaseValidatorStakeWithReserve( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey validatorStake, + PublicKey transientStake, + ulong lamports, + ulong transientStakeSeed) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeDecreaseValidatorStakeWithReserveData(lamports, transientStakeSeed); + + // Prepare the accounts for the instruction + var keys = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.Writable(validatorStake, false), + AccountMeta.Writable(transientStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Return the transaction instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates the 'IncreaseValidatorStake' instruction (rebalance from transient account to validator account). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction IncreaseValidatorStake + ( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey transientStake, + PublicKey validatorStake, + ulong lamports, + ulong transientStakeSeed + ) + { + var data = StakePoolProgramData.EncodeIncreaseValidatorStakeData(lamports, transientStakeSeed); + + var keys = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.Writable(transientStake, false), + AccountMeta.ReadOnly(validatorStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.RentKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ConfigKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates the 'IncreaseAdditionalValidatorStake' instruction (rebalance from validator account to transient account). + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction IncreaseAdditionalValidatorStake( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey ephemeralStake, + PublicKey transientStake, + PublicKey validatorStake, + PublicKey validator, + ulong lamports, + ulong transientStakeSeed, + ulong ephemeralStakeSeed) + { + // Prepare the instruction data + var data = StakePoolProgramData.EncodeIncreaseAdditionalValidatorStakeData(lamports, transientStakeSeed, ephemeralStakeSeed); + + // Prepare the accounts for the instruction + var keys = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.Writable(ephemeralStake, false), + AccountMeta.Writable(transientStake, false), + AccountMeta.ReadOnly(validatorStake, false), + AccountMeta.ReadOnly(validator, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ConfigKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Return the transaction instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + + /// + /// Creates the 'Redelegate' instruction (rebalance from one validator account to another). + /// + /// + /// This instruction is deprecated since 2.0.0. The stake redelegate instruction will not be enabled. + /// + [Obsolete("The stake redelegate instruction used in this will not be enabled. Since 2.0.0", true)] + public static TransactionInstruction Redelegate( + PublicKey stakePool, + PublicKey staker, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorList, + PublicKey reserveStake, + PublicKey sourceValidatorStake, + PublicKey sourceTransientStake, + PublicKey ephemeralStake, + PublicKey destinationTransientStake, + PublicKey destinationValidatorStake, + PublicKey validator, + ulong lamports, + ulong sourceTransientStakeSeed, + ulong ephemeralStakeSeed, + ulong destinationTransientStakeSeed) + { + var accounts = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorList, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.Writable(sourceValidatorStake, false), + AccountMeta.Writable(sourceTransientStake, false), + AccountMeta.Writable(ephemeralStake, false), + AccountMeta.Writable(destinationTransientStake, false), + AccountMeta.ReadOnly(destinationValidatorStake, false), + AccountMeta.ReadOnly(validator, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ConfigKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false) + }; + + var data = StakePoolProgramData.EncodeRedelegateData(lamports, sourceTransientStakeSeed, ephemeralStakeSeed, destinationTransientStakeSeed); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + +#nullable enable + + /// + /// Creates the 'SetPreferredDepositValidator' instruction (set preferred deposit validator). + /// + /// + /// + /// + /// + /// Optional public key; if provided, it must be in the keys list. + /// + public virtual TransactionInstruction SetPreferredDepositValidator( + PublicKey stakePoolAddress, + PublicKey staker, + PublicKey validatorListAddress, + PreferredValidatorType validatorType, + PublicKey? validatorVoteAddress = null) + { + // Prepare Account Metas. + var keys = new List + { + AccountMeta.Writable(stakePoolAddress, false), + AccountMeta.ReadOnly(staker, true), + AccountMeta.ReadOnly(validatorListAddress, false) + }; + + // Fix: If a validator vote address is provided, add it to the keys list instead of encoding into data. + if (validatorVoteAddress != null) + { + keys.Add(AccountMeta.ReadOnly(validatorVoteAddress, false)); + } + + // Encode only the instruction discriminator and validator type. + var data = StakePoolProgramData.EncodeSetPreferredValidatorData(validatorType); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = keys, + Data = data + }; + } + + /// + /// Creates an 'AddValidatorToPoolWithVote' instruction given an existing stake pool and vote account. + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction AddValidatorToPoolWithVote( + Models.StakePool stakePool, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + uint? seed = null) + { + // dont allow zero seed values + if (seed == 0) + throw new ArgumentException("Value must be nonzero.", nameof(seed)); + + // Find the program address for the withdraw authority + if (!PublicKey.TryFindProgramAddress([stakePoolAddress.KeyBytes], ProgramIdKey, out var poolWithdrawalAuthority, out var nonce)) + throw new InvalidProgramException(); + // Find the stake account address for the validator using the vote account and seed + if (!PublicKey.TryFindProgramAddress([voteAccountAddress.KeyBytes], ProgramIdKey, out var stakeAccountAddress, out var _)) + throw new InvalidProgramException(); + + // Generate the instruction to add the validator to the pool + return AddValidatorToPool( + StakePoolProgramIdKey, + stakePool.Staker, + stakePool.ReserveStake, + poolWithdrawalAuthority, + stakePool.ValidatorList, + stakeAccountAddress, + voteAccountAddress, + seed + ); + } + + + /// + /// Creates an 'RemoveValidatorFromPoolWithVote' instruction given an existing stake pool and vote account. + /// + /// + /// + /// + /// + /// + /// + public virtual TransactionInstruction RemoveValidatorFromPoolWithVote( + Models.StakePool stakePool, // Fully qualify the type to avoid ambiguity + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + uint? validatorStakeSeed = null, + ulong transientStakeSeed = 0) + { + // dont allow zero seed values + if (validatorStakeSeed == 0) + throw new ArgumentException("Value must be nonzero.", nameof(validatorStakeSeed)); + + // Find the program address for the withdraw authority + if (!PublicKey.TryFindProgramAddress([stakePoolAddress.KeyBytes], ProgramIdKey, out var poolWithdrawalAuthority, out var nonce)) + throw new InvalidProgramException(); + + // Find the stake account address for the validator using the vote account and seed + if (!PublicKey.TryFindProgramAddress([voteAccountAddress.KeyBytes], ProgramIdKey, out var stakeAccountAddress, out var _)) + throw new InvalidProgramException(); + + // Find the transient stake account using the vote account, stake pool address, and transient stake seed + if (!PublicKey.TryFindProgramAddress([voteAccountAddress.KeyBytes], ProgramIdKey, out var transientStakeAccount, out var _)) + throw new InvalidProgramException(); + + // Create the RemoveValidatorFromPool instruction + return RemoveValidatorFromPool( + StakePoolProgramIdKey, + stakePool.Staker, + poolWithdrawalAuthority, + stakePool.ValidatorList, + stakeAccountAddress, + transientStakeAccount + ); + } + + /// + /// Creates an 'IncreaseValidatorStakeWithVote' instruction given an existing stake pool and vote account. + /// + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction IncreaseValidatorStakeWithVote( + Models.StakePool stakePool, + PublicKey stakePoolAddress,PublicKey voteAccountAddress, + ulong lamports, + uint? validatorStakeSeed, + ulong transientStakeSeed + ) + { + // dont allow zero seed values + if (validatorStakeSeed == 0) + throw new ArgumentException("Value must be nonzero.", nameof(validatorStakeSeed)); + + // Find the addresses using helper methods + var poolWithdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + var transientStakeAddress = FindTransientStakeProgramAddress(voteAccountAddress, stakePoolAddress, transientStakeSeed); + var validatorStakeAddress = FindStakeProgramAddress(voteAccountAddress, stakePoolAddress, validatorStakeSeed); + + // Create the instruction accounts (same structure as the Rust version) + var accounts = new List + { + AccountMeta.ReadOnly(stakePoolAddress, false), + AccountMeta.Writable(stakePool.Staker, true), + AccountMeta.ReadOnly(poolWithdrawAuthority, false), + AccountMeta.ReadOnly(stakePool.ValidatorList, false), + AccountMeta.ReadOnly(stakePool.ReserveStake, false), + AccountMeta.ReadOnly(transientStakeAddress, false), + AccountMeta.ReadOnly(validatorStakeAddress, false), + AccountMeta.ReadOnly(voteAccountAddress, false) + }; + + // Serialize the instruction data (this is similar to `borsh::to_vec` in Rust) + var data = SerializeIncreaseValidatorStakeData(lamports, transientStakeSeed); + + // Return the instruction + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates a DecreaseValidatorStake instruction (rebalance from validator account to transient account) + /// given an existing stake pool and vote account. + /// + /// The stake pool model. Provides staker, validator list, and reserve stake. + /// The address of the stake pool. + /// The vote account address. + /// The amount of lamports. + /// + /// An optional nonzero seed for the validator stake account (represented by a uint?; zero is disallowed). + /// + /// The transient stake seed. + /// The corresponding transaction instruction. + public virtual TransactionInstruction DecreaseValidatorStakeWithVote( + Models.StakePool stakePool, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + ulong lamports, + uint? validatorStakeSeed, + ulong transientStakeSeed) + { + // Ensure the optional validator stake seed is nonzero. + if (validatorStakeSeed == 0) + throw new ArgumentException("Value must be nonzero.", nameof(validatorStakeSeed)); + + // Find the pool withdrawal authority. + var poolWithdrawalAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + + // Find the validator stake address using the vote account and the seed. + var validatorStakeAddress = FindStakeProgramAddress(voteAccountAddress, stakePoolAddress, validatorStakeSeed); + + // Find the transient stake address. + var transientStakeAddress = FindTransientStakeProgramAddress(voteAccountAddress, stakePoolAddress, transientStakeSeed); + + // Construct the instruction by calling the already implemented decrease_validator_stake_with_reserve method. + return DecreaseValidatorStakeWithReserve( + stakePoolAddress, + stakePool.Staker, + poolWithdrawalAuthority, + stakePool.ValidatorList, + stakePool.ReserveStake, + validatorStakeAddress, + transientStakeAddress, + lamports, + transientStakeSeed); + } + /// + /// Finds the program-derived address for the withdraw authority. + /// + /// + /// + public static PublicKey FindWithdrawAuthorityProgramAddress(PublicKey stakePoolAddress) + { + // Seeds must be provided in the exact same order used in Rust. + var seeds = new[] + { + stakePoolAddress.KeyBytes, // stake pool pubkey as bytes + AUTHORITY_WITHDRAW // "withdraw" + }; + + // Solnet exposes FindProgramAddress; it yields the PDA and out‑param bump. + if (!PublicKey.TryFindProgramAddress(seeds, StakePoolProgramIdKey, out PublicKey address, out byte bump)) + { + throw new InvalidProgramException(); + } + + return address; + } + + /// + /// Serializes the data for the 'IncreaseValidatorStake' instruction. + /// + /// + /// + /// + public static byte[] SerializeIncreaseValidatorStakeData(ulong lamports, ulong transientStakeSeed) + { + // Placeholder: actual serialization would be required based on your program's structure. + return new byte[] { (byte)lamports, (byte)transientStakeSeed }; + } + + /// + /// Finds the stake program address for a validator's vote account. + /// + /// + /// + /// + /// + public static PublicKey FindStakeProgramAddress( + PublicKey voteAccountAddress, + PublicKey stakePoolAddress, + uint? validatorStakeSeed + ) + { + if (validatorStakeSeed.HasValue && validatorStakeSeed.Value == 0) + throw new ArgumentException("Seed must be non‑zero (Rust NonZeroU32).", nameof(validatorStakeSeed)); + + // Convert the seed (if provided) to little‑endian bytes. + byte[] seedBytes = Array.Empty(); + if (validatorStakeSeed.HasValue) + { + seedBytes = BitConverter.GetBytes(validatorStakeSeed.Value); // platform‑endian -> little‑endian + if (!BitConverter.IsLittleEndian) Array.Reverse(seedBytes); // ensure LE on big‑endian CPUs + } + + // Seeds must be passed in the exact order used in Rust. + var seeds = new[] + { + voteAccountAddress.KeyBytes, // vote‑account pubkey + stakePoolAddress.KeyBytes, + seedBytes // may be empty + }; + + // Fix: Correctly call TryFindProgramAddress with all required parameters + if (!PublicKey.TryFindProgramAddress(seeds, StakePoolProgramIdKey, out PublicKey pda, out byte bump)) + { + throw new InvalidProgramException(); + } + + return pda; + } + + /// + /// Finds the transient stake program address for a given vote account and stake pool. + /// + /// + /// + /// + /// + public static PublicKey FindTransientStakeProgramAddress( + PublicKey voteAccountAddress, + PublicKey stakePoolAddress, + ulong transientStakeSeed) + { + // Convert the u64 seed to little‑endian bytes (8 bytes). + byte[] seedBytes = BitConverter.GetBytes(transientStakeSeed); + if (!BitConverter.IsLittleEndian) + Array.Reverse(seedBytes); // ensure LE on big‑endian machines + + // Build seed list in the exact order used in Rust. + var seeds = new List + { + TRANSIENT_STAKE_SEED_PREFIX, + voteAccountAddress.KeyBytes, + stakePoolAddress.KeyBytes, + seedBytes + }; + + // Fix: Correctly call TryFindProgramAddress with all required parameters + if (!PublicKey.TryFindProgramAddress(seeds, StakePoolProgramIdKey, out PublicKey pda, out byte bump)) + { + throw new InvalidProgramException(); + } + + return pda; + } + + /// + /// Generates the deposit authority program address for the stake pool. + /// + /// The stake pool public key. + /// A tuple of the derived deposit authority public key and the bump seed. + public static (PublicKey, byte) FindDepositAuthorityProgramAddress(PublicKey stakePoolAddress) + { + // Seeds must be in the exact order: stake pool address bytes followed by AUTHORITY_DEPOSIT. + var seeds = new[] { stakePoolAddress.KeyBytes, AUTHORITY_DEPOSIT }; + if (!PublicKey.TryFindProgramAddress(seeds, StakePoolProgramIdKey, out PublicKey address, out byte bump)) + throw new InvalidProgramException("Unable to find deposit authority program address"); + return (address, bump); + } + + /// + /// Generates the ephemeral program address for stake pool redelegation. + /// + /// The stake pool public key. + /// The seed used to generate the ephemeral stake address. + /// A tuple of the derived ephemeral stake public key and the bump seed. + public static (PublicKey, byte) FindEphemeralStakeProgramAddress(PublicKey stakePoolAddress, ulong seed) + { + byte[] seedBytes = BitConverter.GetBytes(seed); + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(seedBytes); + } + + var seeds = new List + { + EPHEMERAL_STAKE_SEED_PREFIX, + stakePoolAddress.KeyBytes, + seedBytes + }; + + if (!PublicKey.TryFindProgramAddress(seeds, StakePoolProgramIdKey, out PublicKey address, out byte bump)) + { + throw new InvalidProgramException("Unable to find ephemeral stake program address"); + } + + return (address, bump); + } + + /// + /// Gets the minimum delegation required by a stake account in a stake pool. + /// + /// The minimum delegation defined by the stake program. + /// The greater value between stakeProgramMinimumDelegation and MINIMUM_ACTIVE_STAKE. + public static ulong MinimumDelegation(ulong stakeProgramMinimumDelegation) + { + return Math.Max(stakeProgramMinimumDelegation, MINIMUM_ACTIVE_STAKE); + } + + /// + /// Gets the stake amount under consideration when calculating pool token conversions. + /// + /// The metadata instance containing the rent-exempt reserve value. + /// + /// The sum of meta.RentExemptReserve and MINIMUM_RESERVE_LAMPORTS. If addition overflows, + /// returns ulong.MaxValue. + /// + public static ulong MinimumReserveLamports(Meta meta) + { + ulong reserve = meta.RentExemptReserve; + ulong addition = MINIMUM_RESERVE_LAMPORTS; + // Implement saturating addition: if adding addition to reserve would overflow, return ulong.MaxValue. + if (ulong.MaxValue - reserve < addition) + { + return ulong.MaxValue; + } + + return reserve + addition; + } + + /// + /// Creates an IncreaseAdditionalValidatorStake instruction (rebalance from validator account to transient account) + /// given an existing stake pool, validator list and vote account. + /// + /// The stake pool model containing staker, validator list, and reserve stake. + /// The validator list used to locate the corresponding validator info. + /// The address of the stake pool. + /// The vote account address. + /// The amount of lamports. + /// The ephemeral stake seed. + /// The transaction instruction to increase additional validator stake. + public TransactionInstruction IncreaseAdditionalValidatorStakeWithList( + Models.StakePool stakePool, + ValidatorList validatorList, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + ulong lamports, + ulong ephemeralStakeSeed) + { + var validatorInfo = validatorList.Find(voteAccountAddress); + if (validatorInfo == null) + throw new ArgumentException("Invalid instruction data: vote account was not found in the validator list.", nameof(voteAccountAddress)); + + ulong transientStakeSeed = (ulong)validatorInfo.TransientSeedSuffix; + uint? validatorStakeSeed = validatorInfo.ValidatorSeedSuffix; // assume this property returns a uint + if (validatorStakeSeed == 0) + throw new ArgumentException("Invalid instruction data: validator stake seed cannot be zero.", nameof(validatorStakeSeed)); + + return IncreaseAdditionalValidatorStakeWithVote( + stakePool, + stakePoolAddress, + voteAccountAddress, + lamports, + validatorStakeSeed, + transientStakeSeed, + ephemeralStakeSeed); + } + + /// + /// Creates an IncreaseAdditionalValidatorStake instruction given an existing stake pool and vote account. + /// This helper derives the necessary addresses and serializes the instruction data. + /// + /// The stake pool model. + /// The stake pool public key. + /// The validator vote account public key. + /// The amount of lamports. + /// + /// An optional nonzero seed for the validator stake account (zero is disallowed). + /// + /// The transient stake seed. + /// The ephemeral stake seed. + /// The constructed transaction instruction. + public static TransactionInstruction IncreaseAdditionalValidatorStakeWithVote( + Models.StakePool stakePool, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + ulong lamports, + uint? validatorStakeSeed, + ulong transientStakeSeed, + ulong ephemeralStakeSeed) + { + // Derive the pool withdraw authority. + PublicKey poolWithdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + + // Derive the ephemeral stake address using stake pool address and ephemeral seed. + PublicKey ephemeralStakeAddress = FindEphemeralStakeProgramAddress(stakePoolAddress, ephemeralStakeSeed).Item1; + + // Derive the transient stake address using the vote account and stake pool address. + PublicKey transientStakeAddress = FindTransientStakeProgramAddress(voteAccountAddress, stakePoolAddress, transientStakeSeed); + + // Derive the validator stake address using the vote account and stake pool address. + PublicKey validatorStakeAddress = FindStakeProgramAddress(voteAccountAddress, stakePoolAddress, validatorStakeSeed); + + // Build the account metas. + var accounts = new List + { + AccountMeta.ReadOnly(stakePoolAddress, false), + AccountMeta.Writable(stakePool.Staker, true), + AccountMeta.ReadOnly(poolWithdrawAuthority, false), + AccountMeta.ReadOnly(stakePool.ValidatorList, false), + AccountMeta.ReadOnly(stakePool.ReserveStake, false), + AccountMeta.Writable(ephemeralStakeAddress, false), + AccountMeta.Writable(transientStakeAddress, false), + AccountMeta.ReadOnly(validatorStakeAddress, false), + AccountMeta.ReadOnly(voteAccountAddress, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ConfigKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Serialize instruction data. + byte[] data = StakePoolProgramData.EncodeIncreaseAdditionalValidatorStakeData(lamports, transientStakeSeed, ephemeralStakeSeed); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates a DecreaseAdditionalValidatorStake instruction given an existing stake pool, validator list and vote account. + /// + /// The stake pool model containing staker, validator list, and reserve stake. + /// The list of validator stake info. + /// The stake pool public key. + /// The validator vote account public key. + /// The amount of lamports to withdraw. + /// The ephemeral stake seed. + /// The constructed transaction instruction. + public TransactionInstruction DecreaseAdditionalValidatorStakeWithList( + Models.StakePool stakePool, + ValidatorList validatorList, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + ulong lamports, + ulong ephemeralStakeSeed) + { + var validatorInfo = validatorList.Find(voteAccountAddress); + if (validatorInfo == null) + throw new ArgumentException("Invalid instruction data: vote account was not found in the validator list.", nameof(voteAccountAddress)); + + ulong transientStakeSeed = validatorInfo.TransientSeedSuffix; + uint? validatorStakeSeed = validatorInfo.ValidatorSeedSuffix; + if (validatorStakeSeed == 0) + throw new ArgumentException("Invalid instruction data: validator stake seed cannot be zero.", nameof(validatorStakeSeed)); + + return DecreaseAdditionalValidatorStakeWithVote( + stakePool, + stakePoolAddress, + voteAccountAddress, + lamports, + validatorStakeSeed, + transientStakeSeed, + ephemeralStakeSeed); + } + + /// + /// Creates a DecreaseAdditionalValidatorStake instruction given an existing stake pool and vote account. + /// This helper derives the necessary addresses and serializes the instruction data. Its output is analogous to the Rust + /// function `decrease_additional_validator_stake_with_vote`. + /// + /// The stake pool model. + /// The stake pool public key. + /// The validator vote account public key. + /// The amount of lamports to withdraw. + /// + /// An optional nonzero seed for the validator stake account (zero is disallowed). + /// + /// The transient stake seed. + /// The ephemeral stake seed. + /// The constructed transaction instruction. + public static TransactionInstruction DecreaseAdditionalValidatorStakeWithVote( + Models.StakePool stakePool, + PublicKey stakePoolAddress, + PublicKey voteAccountAddress, + ulong lamports, + uint? validatorStakeSeed, + ulong transientStakeSeed, + ulong ephemeralStakeSeed) + { + // Derive the pool withdraw authority. + PublicKey poolWithdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + + // Derive the ephemeral stake address using the stake pool address and ephemeral stake seed. + PublicKey ephemeralStakeAddress = FindEphemeralStakeProgramAddress(stakePoolAddress, ephemeralStakeSeed).Item1; + + // Derive the transient stake address using the vote account and stake pool address. + PublicKey transientStakeAddress = FindTransientStakeProgramAddress(voteAccountAddress, stakePoolAddress, transientStakeSeed); + + // Derive the validator stake address using the vote account, stake pool address, and validator stake seed. + PublicKey validatorStakeAddress = FindStakeProgramAddress(voteAccountAddress, stakePoolAddress, validatorStakeSeed); + + // Build the instruction accounts in the same order as in the Rust version. + var accounts = new List + { + AccountMeta.ReadOnly(stakePoolAddress, false), + AccountMeta.Writable(stakePool.Staker, true), + AccountMeta.ReadOnly(poolWithdrawAuthority, false), + AccountMeta.ReadOnly(stakePool.ValidatorList, false), + AccountMeta.ReadOnly(stakePool.ReserveStake, false), + AccountMeta.Writable(ephemeralStakeAddress, false), + AccountMeta.Writable(transientStakeAddress, false), + AccountMeta.ReadOnly(validatorStakeAddress, false), + AccountMeta.ReadOnly(voteAccountAddress, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ConfigKey, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + }; + + // Serialize instruction data; this method should follow your program's custom layout. + byte[] data = StakePoolProgramData.EncodeDecreaseAdditionalValidatorStakeData(lamports, transientStakeSeed, ephemeralStakeSeed); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates an UpdateValidatorListBalance instruction to update the balance of validators in a stake pool. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [Obsolete("please use UpdateValidatorListBalanceChunk")] + public static TransactionInstruction UpdateValidatorListBalance( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorListAddress, + PublicKey reserveStake, + ValidatorList validatorList, + IEnumerable validatorVoteAccounts, + uint startIndex, + bool noMerge) + { + // Build the fixed part of the account metas. + var accounts = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorListAddress, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false) + }; + + // Append each validator's stake and transient stake accounts if found in the validator list. + foreach (var voteAccount in validatorVoteAccounts) + { + var validatorStakeInfo = validatorList.Find(voteAccount); + if (validatorStakeInfo != null) + { + uint? validatorSeed = validatorStakeInfo.ValidatorSeedSuffix != 0 + ? (uint?)validatorStakeInfo.ValidatorSeedSuffix + : null; + PublicKey validatorStakeAccount = FindStakeProgramAddress(voteAccount, stakePool, validatorSeed); + PublicKey transientStakeAccount = FindTransientStakeProgramAddress(voteAccount, stakePool, validatorStakeInfo.TransientSeedSuffix); + accounts.Add(AccountMeta.Writable(validatorStakeAccount, false)); + accounts.Add(AccountMeta.Writable(transientStakeAccount, false)); + } + } + + byte[] data = StakePoolProgramData.EncodeUpdateValidatorListBalance(startIndex, noMerge); + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates an UpdateValidatorListBalanceChunk instruction to update a chunk of validators in a stake pool. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction UpdateValidatorListBalanceChunk( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorListAddress, + PublicKey reserveStake, + ValidatorList validatorList, + int len, + int startIndex, + bool noMerge) + { + // Verify slice bounds. + if (startIndex < 0 || startIndex + len > validatorList.Validators.Count) + throw new ArgumentException("Invalid instruction data: slice out of bounds", nameof(validatorList)); + + // Build the fixed part of the accounts list. + var accounts = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(validatorListAddress, false), + AccountMeta.Writable(reserveStake, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false) + }; + + // Get the requested slice of validators. + var subSlice = validatorList.Validators.GetRange(startIndex, len); + foreach (var validator in subSlice) + { + // Ensure the validator stake seed is nonzero. + uint? seed = validator.ValidatorSeedSuffix; + if (seed == 0) + throw new ArgumentException("Invalid instruction data: validator stake seed cannot be zero", nameof(validator)); + + // Derive the validator stake account. + PublicKey validatorStakeAccount = FindStakeProgramAddress( + validator.VoteAccountAddress, + stakePool, + seed); + + // Derive the transient stake account. + PublicKey transientStakeAccount = FindTransientStakeProgramAddress( + validator.VoteAccountAddress, + stakePool, + validator.TransientSeedSuffix); + + accounts.Add(AccountMeta.Writable(validatorStakeAccount, false)); + accounts.Add(AccountMeta.Writable(transientStakeAccount, false)); + } + + // Serialize the instruction data. + byte[] data = StakePoolProgramData.EncodeUpdateValidatorListBalance((uint)startIndex, noMerge); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates an UpdateStaleValidatorListBalanceChunk instruction to update a chunk of validators in a stake pool + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction? UpdateStaleValidatorListBalanceChunk( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey validatorListAddress, + PublicKey reserveStake, + ValidatorList validatorList, + int len, + int startIndex, + bool noMerge, + ulong currentEpoch) + { + // Verify the requested range is within the validator list bounds. + if (startIndex < 0 || startIndex + len > validatorList.Validators.Count) + throw new ArgumentException("Invalid instruction data: slice out of bounds", nameof(validatorList)); + + // Get the sub-slice of validators. + var subSlice = validatorList.Validators.GetRange(startIndex, len); + + // Check if every validator's LastUpdateEpoch is greater than or equal to the current epoch. + if (subSlice.All(info => info.LastUpdateEpoch >= currentEpoch)) + { + return null; + } + + // Otherwise, return the update instruction wrapped in a non-null value. + return UpdateValidatorListBalanceChunk( + stakePool, + stakePoolWithdrawAuthority, + validatorListAddress, + reserveStake, + validatorList, + len, + startIndex, + noMerge); + } + + /// + /// Creates an UpdateStakePoolBalance instruction (update the balance of the stake pool). + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static TransactionInstruction UpdateStakePoolBalance( + PublicKey stakePool, + PublicKey withdrawAuthority, + PublicKey validatorListStorage, + PublicKey reserveStake, + PublicKey managerFeeAccount, + PublicKey stakePoolMint, + PublicKey tokenProgramId) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(withdrawAuthority, false), + AccountMeta.Writable(validatorListStorage, false), + AccountMeta.ReadOnly(reserveStake, false), + AccountMeta.Writable(managerFeeAccount, false), + AccountMeta.Writable(stakePoolMint, false), + AccountMeta.ReadOnly(tokenProgramId, false) + }; + + // Serialize the instruction data. This assumes that the method + // 'EncodeUpdateStakePoolBalance' encodes the unit variant for this instruction. + byte[] data = StakePoolProgramData.EncodeUpdateStakePoolBalance(); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates a CleanupRemovedValidatorEntries instruction (removes entries from the validator list). + /// + /// The stake pool public key. + /// The validator list storage public key. + /// The constructed transaction instruction. + public static TransactionInstruction CleanupRemovedValidatorEntries( + PublicKey stakePool, + PublicKey validatorListStorage) + { + var accounts = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.Writable(validatorListStorage, false) + }; + + byte[] data = StakePoolProgramData.EncodeCleanupRemovedValidatorEntries(); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates all UpdateValidatorListBalance and UpdateStakePoolBalance instructions + /// for fully updating a stake pool each epoch. + /// + /// The stake pool model. + /// The validator list. + /// The address of the stake pool. + /// Flag indicating whether merging should be bypassed. + /// + /// A tuple where the first element is the list of update validator list instructions + /// and the second element is the final list of instructions (update stake pool balance and cleanup). + /// + public static (List updateListInstructions, List finalInstructions) UpdateStakePool( + Models.StakePool stakePool, + ValidatorList validatorList, + PublicKey stakePoolAddress, + bool noMerge) + { + // Derive the withdraw authority using the helper method. + var withdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + + // Build the update list instructions by processing the validator list in chunks. + var updateListInstructions = new List(); + const int maxValidatorsToUpdate = MAX_VALIDATORS_TO_UPDATE; // MAX_VALIDATORS_TO_UPDATE is defined in the class. + + // Iterate over the validators in chunks. + for (int i = 0; i < validatorList.Validators.Count; i += maxValidatorsToUpdate) + { + int chunkLen = Math.Min(maxValidatorsToUpdate, validatorList.Validators.Count - i); + // The start index for this chunk is simply 'i' as validators are grouped in chunks of maxValidatorsToUpdate. + var instruction = UpdateValidatorListBalanceChunk( + stakePoolAddress, + withdrawAuthority, + stakePool.ValidatorList, + stakePool.ReserveStake, + validatorList, + chunkLen, + i, + noMerge); + updateListInstructions.Add(instruction); + } + + // Create the final instructions: + // 1. UpdateStakePoolBalance instruction. + // 2. CleanupRemovedValidatorEntries instruction. + var finalInstructions = new List + { + UpdateStakePoolBalance( + stakePoolAddress, + withdrawAuthority, + stakePool.ValidatorList, + stakePool.ReserveStake, + stakePool.ManagerFeeAccount, + stakePool.PoolMint, + stakePool.TokenProgramId), + CleanupRemovedValidatorEntries( + stakePoolAddress, + stakePool.ValidatorList) + }; + + return (updateListInstructions, finalInstructions); + } + + /// + /// Creates the UpdateValidatorListBalance instructions only for validators on the validator list + /// that have not been updated for the current epoch, along with the UpdateStakePoolBalance and + /// CleanupRemovedValidatorEntries instructions for fully updating the stake pool. + /// + /// The stake pool model. + /// The validator list. + /// The stake pool address. + /// Indicates whether merging should be bypassed. + /// The current epoch. + /// + /// A tuple where the first element is the list of update instructions for individual validator groups + /// and the second element contains the final instructions for updating the stake pool balance and cleaning up. + /// + public static (List updateListInstructions, List finalInstructions) UpdateStaleStakePool( + Models.StakePool stakePool, + ValidatorList validatorList, + PublicKey stakePoolAddress, + bool noMerge, + ulong currentEpoch) + { + // Derive the withdraw authority using the helper method. + var withdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePoolAddress); + + // Build the update list instructions by processing validators in chunks. + var updateListInstructions = new List(); + const int maxValidatorsToUpdate = MAX_VALIDATORS_TO_UPDATE; // Defined in this class. + + for (int i = 0; i < validatorList.Validators.Count; i += maxValidatorsToUpdate) + { + int chunkLen = Math.Min(maxValidatorsToUpdate, validatorList.Validators.Count - i); + // Call the stale update instruction helper for the current chunk. + var instruction = UpdateStaleValidatorListBalanceChunk( + stakePoolAddress, + withdrawAuthority, + stakePool.ValidatorList, + stakePool.ReserveStake, + validatorList, + chunkLen, + i, + noMerge, + currentEpoch); + // Add the instruction only if it's non-null. + if (instruction != null) + { + updateListInstructions.Add(instruction); + } + } + + // Final instructions: update stake pool balance and clean up removed validator entries. + var finalInstructions = new List + { + UpdateStakePoolBalance( + stakePoolAddress, + withdrawAuthority, + stakePool.ValidatorList, + stakePool.ReserveStake, + stakePool.ManagerFeeAccount, + stakePool.PoolMint, + stakePool.TokenProgramId), + CleanupRemovedValidatorEntries( + stakePoolAddress, + stakePool.ValidatorList) + }; + + return (updateListInstructions, finalInstructions); + } + + /// + /// Creates the DepositStake internal instructions. + /// + /// This method builds the account list and then prepends authorize instructions for deposit stake + /// if a deposit authority is provided (or derives it if not), then appends the rest of the accounts required + /// for deposit and encodes a DepositStake (or DepositStakeWithSlippage) instruction. + /// + /// The stake pool account. + /// The account for validator list storage. + /// Optional deposit authority; if null, it is derived. + /// The stake pool withdraw authority. + /// The deposit stake account address. + /// The withdraw authority for the deposit stake account. + /// The validator stake account. + /// The reserve stake account. + /// The destination account for pool tokens. + /// The manager fee pool token account. + /// The referrer’s pool token account. + /// The pool mint account. + /// The token program Id. + /// Optional minimum pool tokens desired; if provided, creates a slippage‐checking deposit. + /// A list of transaction instructions. + public static List DepositStakeInternal( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey? stakePoolDepositAuthority, + PublicKey stakePoolWithdrawAuthority, + PublicKey depositStakeAddress, + PublicKey depositStakeWithdrawAuthority, + PublicKey validatorStakeAccount, + PublicKey reserveStakeAccount, + PublicKey poolTokensTo, + PublicKey managerFeeAccount, + PublicKey referrerPoolTokensAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong? minimumPoolTokensOut) + { + var instructions = new List(); + // Begin building accounts list with stake pool and validator list storage. + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.Writable(validatorListStorage, false) + }; + + // Handle deposit authority. + if (stakePoolDepositAuthority != null) + { + // If provided mark it as a signer. + accounts.Add(AccountMeta.ReadOnly(stakePoolDepositAuthority, true)); + + // Add two authorize instructions to set the deposit stake's staker and withdrawer using the provided authority. + instructions.Add(StakeProgram.Authorize( + depositStakeAddress, + depositStakeWithdrawAuthority, + stakePoolDepositAuthority, + StakeAuthorize.Staker, + null)); + + instructions.Add(StakeProgram.Authorize( + depositStakeAddress, + depositStakeWithdrawAuthority, + stakePoolDepositAuthority, + StakeAuthorize.Withdrawer, + null)); + } + else + { + // Otherwise, derive the deposit authority for the stake pool. + var (derivedDepositAuthority, _) = FindDepositAuthorityProgramAddress(stakePool); + accounts.Add(AccountMeta.ReadOnly(derivedDepositAuthority, false)); + + instructions.Add(StakeProgram.Authorize( + depositStakeAddress, + depositStakeWithdrawAuthority, + derivedDepositAuthority, + StakeAuthorize.Staker, + null)); + + instructions.Add(StakeProgram.Authorize( + depositStakeAddress, + depositStakeWithdrawAuthority, + derivedDepositAuthority, + StakeAuthorize.Withdrawer, + null)); + } + + // Append the remaining accounts. + accounts.AddRange(new List + { + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(depositStakeAddress, false), + AccountMeta.Writable(validatorStakeAccount, false), + AccountMeta.Writable(reserveStakeAccount, false), + AccountMeta.Writable(poolTokensTo, false), + AccountMeta.Writable(managerFeeAccount, false), + AccountMeta.Writable(referrerPoolTokensAccount, false), + AccountMeta.Writable(poolMint, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + AccountMeta.ReadOnly(tokenProgramId, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false) + }); + + // Depending on whether a minimum pool token output is required, encode the appropriate instruction. + TransactionInstruction depositInstruction; + if (minimumPoolTokensOut.HasValue) + { + depositInstruction = new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeDepositStakeWithSlippage(minimumPoolTokensOut.Value) + }; + } + else + { + depositInstruction = new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeDepositStake() + }; + } + + instructions.Add(depositInstruction); + return instructions; + } + + /// + /// Creates instructions required to deposit into a stake pool, given a stake account owned by the user. + /// + /// The stake pool account. + /// The account for validator list storage. + /// The stake pool withdraw authority. + /// The deposit stake account address. + /// The withdraw authority for the deposit stake account. + /// The validator stake account. + /// The reserve stake account. + /// The destination account for pool tokens. + /// The manager fee pool token account. + /// The referrer's pool token account. + /// The pool mint account. + /// The token program Id. + /// A list of transaction instructions. + public static List DepositStake( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey stakePoolWithdrawAuthority, + PublicKey depositStakeAddress, + PublicKey depositStakeWithdrawAuthority, + PublicKey validatorStakeAccount, + PublicKey reserveStakeAccount, + PublicKey poolTokensTo, + PublicKey managerFeeAccount, + PublicKey referrerPoolTokensAccount, + PublicKey poolMint, + PublicKey tokenProgramId) + { + return DepositStakeInternal( + stakePool, + validatorListStorage, + null, // no deposit authority provided + stakePoolWithdrawAuthority, + depositStakeAddress, + depositStakeWithdrawAuthority, + validatorStakeAccount, + reserveStakeAccount, + poolTokensTo, + managerFeeAccount, + referrerPoolTokensAccount, + poolMint, + tokenProgramId, + null // no minimum pool tokens out (no slippage check) + ); + } + + /// + /// Creates instructions to deposit into a stake pool with slippage. + /// + /// The stake pool account. + /// The account for validator list storage. + /// The stake pool withdraw authority. + /// The deposit stake account address. + /// The withdraw authority for the deposit stake account. + /// The validator stake account. + /// The reserve stake account. + /// The destination account for pool tokens. + /// The manager fee pool token account. + /// The referrer's pool token account. + /// The pool mint account. + /// The token program Id. + /// The minimum pool tokens desired. + /// A list of transaction instructions. + public static List DepositStakeWithSlippage( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey stakePoolWithdrawAuthority, + PublicKey depositStakeAddress, + PublicKey depositStakeWithdrawAuthority, + PublicKey validatorStakeAccount, + PublicKey reserveStakeAccount, + PublicKey poolTokensTo, + PublicKey managerFeeAccount, + PublicKey referrerPoolTokensAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong minimumPoolTokensOut) + { + return DepositStakeInternal( + stakePool, + validatorListStorage, + null, // no deposit authority provided + stakePoolWithdrawAuthority, + depositStakeAddress, + depositStakeWithdrawAuthority, + validatorStakeAccount, + reserveStakeAccount, + poolTokensTo, + managerFeeAccount, + referrerPoolTokensAccount, + poolMint, + tokenProgramId, + minimumPoolTokensOut + ); + } + + /// + /// Creates an instruction to deposit SOL directly into a stake pool with a slippage constraint, + /// requiring the deposit authority's signature (as needed for private pools). + /// + /// The stake pool account. + /// The SOL deposit authority which must sign the instruction. + /// The stake pool withdraw authority. + /// The reserve stake account. + /// The source account from which SOL lamports will be deducted. + /// The destination pool tokens account. + /// The manager fee pool token account. + /// The referrer’s pool token account. + /// The pool mint account. + /// The token program Id. + /// The amount of lamports to deposit. + /// The minimum number of pool tokens expected (slippage constraint). + /// A transaction instruction for the SOL deposit operation. + public static TransactionInstruction DepositSolWithAuthorityAndSlippage( + PublicKey stakePool, + PublicKey solDepositAuthority, + PublicKey stakePoolWithdrawAuthority, + PublicKey reserveStakeAccount, + PublicKey lamportsFrom, + PublicKey poolTokensTo, + PublicKey managerFeeAccount, + PublicKey referrerPoolTokensAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong lamportsIn, + ulong minimumPoolTokensOut) + { + return DepositSolInternal( + stakePool, + stakePoolWithdrawAuthority, + reserveStakeAccount, + lamportsFrom, + poolTokensTo, + managerFeeAccount, + referrerPoolTokensAccount, + poolMint, + tokenProgramId, + solDepositAuthority, // deposit authority provided + lamportsIn, + minimumPoolTokensOut + ); + } + + /// + /// Creates instructions required to withdraw from a stake pool by splitting a stake account. + /// When a minimum lamports output is provided, a slippage check is enforced. + /// + /// The stake pool account. + /// The validator list storage account. + /// The stake pool withdraw authority. + /// The stake account to split. + /// The stake account to receive the split stake. + /// The user's stake authority. + /// The user's transfer authority (signer). + /// The user's pool token account. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// Optional minimum lamports expected on withdrawal (slippage check). + /// A transaction instruction for the stake withdrawal. + public static TransactionInstruction WithdrawStakeInternal( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey stakePoolWithdraw, + PublicKey stakeToSplit, + PublicKey stakeToReceive, + PublicKey userStakeAuthority, + PublicKey userTransferAuthority, + PublicKey userPoolTokenAccount, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn, + ulong? minimumLamportsOut) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.Writable(validatorListStorage, false), + AccountMeta.ReadOnly(stakePoolWithdraw, false), + AccountMeta.Writable(stakeToSplit, false), + AccountMeta.Writable(stakeToReceive, false), + AccountMeta.ReadOnly(userStakeAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, true), + AccountMeta.Writable(userPoolTokenAccount, false), + AccountMeta.Writable(managerFeeAccount, false), + AccountMeta.Writable(poolMint, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(tokenProgramId, false), + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false) + }; + + byte[] data; + if (minimumLamportsOut.HasValue) + { + data = StakePoolProgramData.EncodeWithdrawStakeWithSlippage(poolTokensIn, minimumLamportsOut.Value); + } + else + { + data = StakePoolProgramData.EncodeWithdrawStake(poolTokensIn); + } + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates a 'WithdrawStake' instruction. + /// + /// The stake pool account. + /// The validator list storage account. + /// The stake pool withdraw authority. + /// The stake account to split. + /// The stake account to receive the split stake. + /// The user's stake authority. + /// The user's transfer authority (signer). + /// The user's pool token account. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// A transaction instruction for stake withdrawal. + public static TransactionInstruction WithdrawStake( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey stakePoolWithdrawAuthority, + PublicKey stakeToSplit, + PublicKey stakeToReceive, + PublicKey userStakeAuthority, + PublicKey userTransferAuthority, + PublicKey userPoolTokenAccount, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn) + { + return WithdrawStakeInternal( + stakePool, + validatorListStorage, + stakePoolWithdrawAuthority, + stakeToSplit, + stakeToReceive, + userStakeAuthority, + userTransferAuthority, + userPoolTokenAccount, + managerFeeAccount, + poolMint, + tokenProgramId, + poolTokensIn, + null // no minimum lamports out (no slippage check) + ); + } + + /// + /// Creates a 'WithdrawStakeWithSlippage' instruction. + /// + /// The stake pool account. + /// The validator list storage account. + /// The stake pool withdraw authority. + /// The stake account to split. + /// The stake account to receive the split stake. + /// The user's stake authority. + /// The user's transfer authority (signer). + /// The user's pool token account. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// The minimum lamports expected on withdrawal. + /// A transaction instruction for stake withdrawal with slippage check. + public static TransactionInstruction WithdrawStakeWithSlippage( + PublicKey stakePool, + PublicKey validatorListStorage, + PublicKey stakePoolWithdrawAuthority, + PublicKey stakeToSplit, + PublicKey stakeToReceive, + PublicKey userStakeAuthority, + PublicKey userTransferAuthority, + PublicKey userPoolTokenAccount, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn, + ulong minimumLamportsOut) + { + return WithdrawStakeInternal( + stakePool, + validatorListStorage, + stakePoolWithdrawAuthority, + stakeToSplit, + stakeToReceive, + userStakeAuthority, + userTransferAuthority, + userPoolTokenAccount, + managerFeeAccount, + poolMint, + tokenProgramId, + poolTokensIn, + minimumLamportsOut + ); + } + + /// + /// Creates instructions required to withdraw SOL directly from a stake pool, + /// optionally enforcing a slippage constraint. + /// + /// The stake pool account. + /// The stake pool withdraw authority. + /// The user's transfer authority (signer). + /// The account from which pool tokens will be deducted. + /// The reserve stake account. + /// The destination account for SOL lamports. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// + /// Optional SOL withdraw authority; if provided, it must sign the instruction. + /// + /// The amount of pool tokens to redeem. + /// + /// Optional minimum lamports expected on withdrawal (for slippage check). + /// + /// A transaction instruction for SOL withdrawal. + public static TransactionInstruction WithdrawSolInternal( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey userTransferAuthority, + PublicKey poolTokensFrom, + PublicKey reserveStakeAccount, + PublicKey lamportsTo, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + PublicKey? solWithdrawAuthority, + ulong poolTokensIn, + ulong? minimumLamportsOut) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, true), + AccountMeta.Writable(poolTokensFrom, false), + AccountMeta.Writable(reserveStakeAccount, false), + AccountMeta.Writable(lamportsTo, false), + AccountMeta.Writable(managerFeeAccount, false), + AccountMeta.Writable(poolMint, false), + AccountMeta.ReadOnly(SysVars.ClockKey, false), + AccountMeta.ReadOnly(SysVars.StakeHistoryKey, false), + // Assuming StakeProgram.ProgramIdKey returns a PublicKey for the stake program. + AccountMeta.ReadOnly(StakeProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(tokenProgramId, false) + }; + + if (solWithdrawAuthority != null) + { + accounts.Add(AccountMeta.ReadOnly(solWithdrawAuthority, true)); + } + + byte[] data; + if (minimumLamportsOut.HasValue) + { + data = StakePoolProgramData.EncodeWithdrawSolWithSlippage(poolTokensIn, minimumLamportsOut.Value); + } + else + { + data = StakePoolProgramData.EncodeWithdrawSol(poolTokensIn); + } + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates instruction required to withdraw SOL directly from a stake pool. + /// + /// The stake pool account. + /// The stake pool withdraw authority. + /// The user's transfer authority (signer). + /// The account from which pool tokens will be deducted. + /// The reserve stake account. + /// The destination account for SOL lamports. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// A transaction instruction for SOL withdrawal. + public static TransactionInstruction WithdrawSol( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey userTransferAuthority, + PublicKey poolTokensFrom, + PublicKey reserveStakeAccount, + PublicKey lamportsTo, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn) + { + return WithdrawSolInternal( + stakePool, + stakePoolWithdrawAuthority, + userTransferAuthority, + poolTokensFrom, + reserveStakeAccount, + lamportsTo, + managerFeeAccount, + poolMint, + tokenProgramId, + null, // SOL withdraw authority: not provided + poolTokensIn, + null // minimum lamports out: not provided + ); + } + + /// + /// Creates an instruction required to withdraw SOL directly from a stake pool with slippage constraints. + /// + /// The stake pool account. + /// The stake pool withdraw authority. + /// The user's transfer authority (signer). + /// The account from which pool tokens will be deducted. + /// The reserve stake account. + /// The destination account for SOL lamports. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// The minimum lamports expected on withdrawal (slippage constraint). + /// A transaction instruction for stake withdrawal with slippage check. + public static TransactionInstruction WithdrawSolWithSlippage( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey userTransferAuthority, + PublicKey poolTokensFrom, + PublicKey reserveStakeAccount, + PublicKey lamportsTo, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn, + ulong minimumLamportsOut) + { + return WithdrawSolInternal( + stakePool, + stakePoolWithdrawAuthority, + userTransferAuthority, + poolTokensFrom, + reserveStakeAccount, + lamportsTo, + managerFeeAccount, + poolMint, + tokenProgramId, + null, // SOL withdraw authority: not provided + poolTokensIn, + minimumLamportsOut + ); + } + + /// + /// Creates an instruction required to withdraw SOL directly from a stake pool. + /// The difference with WithdrawSol() is that the SOL withdraw authority must sign this instruction. + /// + /// The stake pool account. + /// The SOL withdraw authority (must sign). + /// The stake pool withdraw authority. + /// The user's transfer authority (signer). + /// The account from which pool tokens will be deducted. + /// The reserve stake account. + /// The destination account for SOL lamports. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// A transaction instruction for SOL withdrawal with the required SOL withdraw authority signature. + public static TransactionInstruction WithdrawSolWithAuthority( + PublicKey stakePool, + PublicKey solWithdrawAuthority, + PublicKey stakePoolWithdrawAuthority, + PublicKey userTransferAuthority, + PublicKey poolTokensFrom, + PublicKey reserveStakeAccount, + PublicKey lamportsTo, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn) + { + return WithdrawSolInternal( + stakePool, + stakePoolWithdrawAuthority, + userTransferAuthority, + poolTokensFrom, + reserveStakeAccount, + lamportsTo, + managerFeeAccount, + poolMint, + tokenProgramId, + solWithdrawAuthority, // Provide the SOL withdraw authority which must sign + poolTokensIn, + null // No minimum lamports out (no slippage check) + ); + } + + /// + /// Creates an instruction required to withdraw SOL directly from a stake pool with a slippage constraint. + /// The difference with WithdrawSol() is that the SOL withdraw authority must sign this instruction. + /// + /// The stake pool account. + /// The SOL withdraw authority which must sign the instruction. + /// The stake pool withdraw authority. + /// The user's transfer authority (signer). + /// The account from which pool tokens will be deducted. + /// The reserve stake account. + /// The destination account for SOL lamports. + /// The manager fee account. + /// The pool mint account. + /// The token program Id. + /// The amount of pool tokens to redeem. + /// The minimum lamports expected on withdrawal (slippage constraint). + /// A transaction instruction for SOL withdrawal with the required SOL withdraw authority signature and slippage check. + public static TransactionInstruction WithdrawSolWithAuthorityAndSlippage( + PublicKey stakePool, + PublicKey solWithdrawAuthority, + PublicKey stakePoolWithdrawAuthority, + PublicKey userTransferAuthority, + PublicKey poolTokensFrom, + PublicKey reserveStakeAccount, + PublicKey lamportsTo, + PublicKey managerFeeAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + ulong poolTokensIn, + ulong minimumLamportsOut) + { + return WithdrawSolInternal( + stakePool, + stakePoolWithdrawAuthority, + userTransferAuthority, + poolTokensFrom, + reserveStakeAccount, + lamportsTo, + managerFeeAccount, + poolMint, + tokenProgramId, + solWithdrawAuthority, // SOL withdraw authority must sign + poolTokensIn, + minimumLamportsOut // enforce slippage check + ); + } + + /// + /// Creates a 'Set Manager' instruction. + /// + /// The stake pool account. + /// The current manager (must sign). + /// The new manager (must sign). + /// The new fee receiver account. + /// A transaction instruction to set the manager. + public static TransactionInstruction SetManager( + PublicKey stakePool, + PublicKey manager, + PublicKey newManager, + PublicKey newFeeReceiver) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(manager, true), + AccountMeta.ReadOnly(newManager, true), + AccountMeta.ReadOnly(newFeeReceiver, false) + }; + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeSetManager() // encode the SetManager variant + }; + } + + /// + /// Creates a 'Set Fee' instruction. + /// + /// The stake pool account. + /// The manager account (must sign). + /// The fee to be set. + /// A transaction instruction to set the fee. + public static TransactionInstruction SetFee( + PublicKey stakePool, + PublicKey manager, + Fee fee) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(manager, true) + }; + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeSetFee(fee) + }; + } + + /// + /// Creates a 'Set Staker' instruction. + /// + /// The stake pool account. + /// The current staker (must sign). + /// The new staker to be set. + /// A transaction instruction to set the staker. + public static TransactionInstruction SetStaker( + PublicKey stakePool, + PublicKey setStakerAuthority, + PublicKey newStaker) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(setStakerAuthority, true), + AccountMeta.ReadOnly(newStaker, false) + }; + + // Fix: Remove the public key from the data since it is provided in keys + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeSetStaker() // now only encodes the discriminator + }; + } + + /// + /// Creates a 'SetFundingAuthority' instruction. + /// + /// The stake pool account. + /// The manager account (must sign). + /// + /// The new SOL deposit authority (optional). If provided, it is added as a read-only account. + /// + /// The funding type to be set. + /// A transaction instruction to set the funding authority. + public static TransactionInstruction SetFundingAuthority( + PublicKey stakePool, + PublicKey manager, + PublicKey? newSolDepositAuthority, + FundingType fundingType) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(manager, true) + }; + + if(newSolDepositAuthority != null) + { + accounts.Add(AccountMeta.ReadOnly(newSolDepositAuthority, false)); + } + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = StakePoolProgramData.EncodeSetFundingAuthority(fundingType) + }; + } + + /// + /// Creates an instruction to update metadata in the MPL token metadata program + /// account for the pool token. + /// + /// The stake pool account. + /// The manager account (must sign). + /// The pool mint account. + /// The new name for the pool token. + /// The new symbol for the pool token. + /// The new URI for the pool token metadata. + /// A transaction instruction for updating token metadata. + public static TransactionInstruction UpdateTokenMetadata( + PublicKey stakePool, + PublicKey manager, + PublicKey poolMint, + string name, + string symbol, + string uri) + { + // Derive the stake pool withdraw authority. + PublicKey stakePoolWithdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePool); + // Derive the metadata account for the pool mint. + (PublicKey tokenMetadata, byte bump) = FindMetadataAccount(poolMint); + + var accounts = new List + { + AccountMeta.ReadOnly(stakePool, false), + AccountMeta.ReadOnly(manager, true), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(tokenMetadata, false), + // InlineMplTokenMetadata.Id() returns the MPL token metadata program ID. + AccountMeta.ReadOnly(MplTokenMetadataProgramIdKey, false) + }; + + byte[] data = StakePoolProgramData.EncodeUpdateTokenMetadata(name, symbol, uri); + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates an instruction to create metadata using the MPL token metadata + /// program for the pool token. + /// + /// The stake pool account. + /// The manager account (must sign). + /// The pool mint account. + /// The account paying for the transaction (must sign). + /// The name for the pool token. + /// The symbol for the pool token. + /// The URI for the pool token metadata. + /// A transaction instruction for creating token metadata. + public static TransactionInstruction CreateTokenMetadata( + PublicKey stakePool, + PublicKey manager, + PublicKey poolMint, + PublicKey payer, + string name, + string symbol, + string uri) + { + // Derive the stake pool withdraw authority. + PublicKey stakePoolWithdrawAuthority = FindWithdrawAuthorityProgramAddress(stakePool); + + // Derive the metadata account for the pool mint. + (PublicKey tokenMetadata, byte bump) = FindMetadataAccount(poolMint); + + // Build the accounts as required by the MPL Token Metadata program. + var accounts = new List + { + // The stake pool account (read-only) + AccountMeta.ReadOnly(stakePool, false), + // The manager must sign (read-only) + AccountMeta.ReadOnly(manager, true), + // The derived withdraw authority (read-only) + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + // The pool mint (read-only) + AccountMeta.ReadOnly(poolMint, false), + // The payer (writable and signer) + AccountMeta.Writable(payer, true), + // The token metadata account (writable) + AccountMeta.Writable(tokenMetadata, false), + // The MPL Token Metadata program + AccountMeta.ReadOnly(MplTokenMetadataProgramIdKey, false), + // The system program + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false) + }; + + // Encode the instruction data for creating token metadata. + byte[] data = StakePoolProgramData.EncodeCreateTokenMetadata(name, symbol, uri); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey, + Keys = accounts, + Data = data + }; + } + + /// + /// Creates an instruction required to deposit SOL directly into a stake pool. + /// + /// The stake pool account. + /// The stake pool withdraw authority. + /// The reserve stake account. + /// The source account for SOL lamports (must sign). + /// The account to receive pool tokens. + /// The manager fee account. + /// The referrer’s pool token account. + /// The pool mint account. + /// The token program account. + /// + /// Optional SOL deposit authority; if provided, it must sign. + /// + /// The amount of SOL lamports to deposit. + /// + /// Optional minimum pool tokens expected (for slippage protection). + /// + /// A transaction instruction for SOL deposit. + public static TransactionInstruction DepositSolInternal( + PublicKey stakePool, + PublicKey stakePoolWithdrawAuthority, + PublicKey reserveStakeAccount, + PublicKey lamportsFrom, + PublicKey poolTokensTo, + PublicKey managerFeeAccount, + PublicKey referrerPoolTokensAccount, + PublicKey poolMint, + PublicKey tokenProgramId, + PublicKey? solDepositAuthority, + ulong lamportsIn, + ulong? minimumPoolTokensOut) + { + var accounts = new List + { + AccountMeta.Writable(stakePool, false), + AccountMeta.ReadOnly(stakePoolWithdrawAuthority, false), + AccountMeta.Writable(reserveStakeAccount, false), + AccountMeta.Writable(lamportsFrom, true), + AccountMeta.Writable(poolTokensTo, false), + AccountMeta.Writable(managerFeeAccount, false), + AccountMeta.Writable(referrerPoolTokensAccount, false), + AccountMeta.Writable(poolMint, false), + AccountMeta.ReadOnly(SystemProgram.ProgramIdKey, false), + AccountMeta.ReadOnly(tokenProgramId, false) + }; + + if (solDepositAuthority != null) + { + accounts.Add(AccountMeta.ReadOnly(solDepositAuthority, true)); + } + + byte[] data = minimumPoolTokensOut.HasValue + ? StakePoolProgramData.EncodeDepositSolWithSlippage(lamportsIn, minimumPoolTokensOut.Value) + : StakePoolProgramData.EncodeDepositSol(lamportsIn); + + return new TransactionInstruction + { + ProgramId = StakePoolProgramIdKey.KeyBytes, + Keys = accounts, + Data = data + }; + } + + /// + /// Finds the metadata account for a given mint using the MPL Token Metadata program. + /// + /// The mint public key. + /// A tuple containing the metadata account public key and the bump seed. + public static (PublicKey, byte) FindMetadataAccount(PublicKey mint) + { + // Seeds must be in the same order as in the Rust function: + // 1. The UTF8 bytes for "metadata" + // 2. The MPL Token Metadata Program ID bytes. + // 3. The mint's public key bytes. + var seeds = new List + { + Encoding.ASCII.GetBytes("metadata"), + MplTokenMetadataProgramIdKey.KeyBytes, + mint.KeyBytes + }; + + if (!PublicKey.TryFindProgramAddress(seeds, MplTokenMetadataProgramIdKey, out PublicKey metadataAccount, out byte bump)) + { + throw new InvalidProgramException("Unable to find metadata account for the provided mint."); + } + + return (metadataAccount, bump); + } + + #endregion + } +} diff --git a/src/Solnet.Programs/StakePool/StakePoolProgramData.cs b/src/Solnet.Programs/StakePool/StakePoolProgramData.cs new file mode 100644 index 00000000..cf66fbae --- /dev/null +++ b/src/Solnet.Programs/StakePool/StakePoolProgramData.cs @@ -0,0 +1,687 @@ +using Solnet.Programs.StakePool.Models; +using Solnet.Programs.Utilities; +using Solnet.Wallet; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Solnet.Programs.StakePool +{ + internal static class StakePoolProgramData + { + /// + /// The offset at which the value which defines the program method begins. + /// + internal const int MethodOffset = 0; + + + /// + /// Encodes the 'Initialize' instruction data. + /// + /// + /// + /// + /// + /// + /// + internal static byte[] EncodeInitializeData(Fee fee, Fee withdrawalFee, Fee depositFee, Fee referralFee, uint? maxValidators) + { + byte[] data = new byte[72]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.Initialize, MethodOffset); + data.WriteU64(fee.Numerator, MethodOffset + 4); // Serialize Fee as Numerator and Denominator + data.WriteU64(fee.Denominator, MethodOffset + 12); + data.WriteU64(withdrawalFee.Numerator, MethodOffset + 20); + data.WriteU64(withdrawalFee.Denominator, MethodOffset + 28); + data.WriteU64(depositFee.Numerator, MethodOffset + 36); + data.WriteU64(depositFee.Denominator, MethodOffset + 44); + data.WriteU64(referralFee.Numerator, MethodOffset + 52); + data.WriteU64(referralFee.Denominator, MethodOffset + 60); + data.WriteU32(maxValidators ?? 0, MethodOffset + 68); + return data; + } + + + /// + /// Encodes the 'AddValidatorToPool' instruction data. + /// + /// + /// + internal static byte[] EncodeAddValidatorToPoolData(uint? seed) + { + byte[] data = new byte[8]; + var seedValue = seed ?? 0; + data.WriteU32((uint)StakePoolProgramInstructions.Values.AddValidatorToPool, MethodOffset); + data.WriteU32(seedValue, MethodOffset + 4); + // Assuming the enum or data structure you're sending in the instruction is properly serialized here + return data; // Example encoding, adjust based on actual data + } + + /// + /// Encodes the 'AddValidatorToPoolWithVote' instruction data. + /// + /// + internal static byte[] EncodeRemoveValidatorFromPoolData() + { + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.RemoveValidatorFromPool, MethodOffset); + // Here you would implement the serialization of removeData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + + /// + /// Encodes the 'DecreaseValidatorStake' instruction data. + /// + internal static byte[] EncodeDecreaseValidatorStakeData(ulong lamports, ulong transientStakeSeed) + { + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DecreaseValidatorStake, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(transientStakeSeed, MethodOffset + 12); + // Here you would implement the serialization of decreaseData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + + + /// + /// Encodes the 'DecreaseAdditionalValidatorStake' instruction data. + /// + internal static byte[] EncodeDecreaseAdditionalValidatorStakeData(ulong lamports, ulong transientStakeSeed, ulong ephemeralStakeSeed) + { + byte[] data = new byte[28]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DecreaseAdditionalValidatorStake, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(transientStakeSeed, MethodOffset + 12); + data.WriteU64(ephemeralStakeSeed, MethodOffset + 20); + // Here you would implement the serialization of decreaseData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + + /// + /// Encodes the 'DecreaseValidatorStakeWithReserve' instruction data. + /// + internal static byte[] EncodeDecreaseValidatorStakeWithReserveData(ulong lamports, ulong transientStakeSeed) + { + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DecreaseValidatorStakeWithReserve, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(transientStakeSeed, MethodOffset + 12); + // Implement the serialization of decreaseData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + + /// + /// Encodes the 'IncreaseValidatorStake' instruction data. + /// + /// + internal static byte[] EncodeIncreaseValidatorStakeData(ulong lamports, ulong transientStakeSeed) + { + byte[] data = new byte[28]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.IncreaseValidatorStake, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(transientStakeSeed, MethodOffset + 12); + // Implement the serialization of increaseData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + + /// + /// Encodes the 'IncreaseAdditionalValidatorStake' instruction data. + /// + internal static byte[] EncodeIncreaseAdditionalValidatorStakeData(ulong lamports, ulong transientStakeSeed, ulong ephemeralStakeSeed) + { + byte[] data = new byte[28]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.IncreaseValidatorStake, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(transientStakeSeed, MethodOffset + 12); + data.WriteU64(ephemeralStakeSeed, MethodOffset + 20); + // Implement the serialization of increaseData (e.g., using Borsh or another method) + return data; // Example: return the serialized byte array + } + +#nullable enable + /// + /// Encodes the 'SetPreferredDepositValidator' instruction data. + /// + internal static byte[] EncodeSetPreferredValidatorData(PreferredValidatorType validatorType) + { + // Allocate 8 bytes: 4 bytes for the discriminator and 4 bytes for the validator type. + byte[] data = new byte[8]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetPreferredValidator, MethodOffset); + data.WriteU32((uint)validatorType, MethodOffset + 4); + return data; + } + + internal static byte[] EncodeSetFeeData(FeeType feeType) + { + // 4 bytes for method, 1 for discriminant, up to 16 for Fee, or 1 for byte + var buffer = new List(); + buffer.AddRange(BitConverter.GetBytes((uint)StakePoolProgramInstructions.Values.SetFee)); + + // Discriminant and value + switch (feeType) + { + case FeeType.SolReferral solReferral: + buffer.Add(0); // Discriminant for SolReferral + buffer.Add(solReferral.Percentage); + break; + case FeeType.StakeReferral stakeReferral: + buffer.Add(1); + buffer.Add(stakeReferral.Percentage); + break; + case FeeType.Epoch epoch: + buffer.Add(2); + buffer.AddRange(BitConverter.GetBytes(epoch.Fee.Numerator)); + buffer.AddRange(BitConverter.GetBytes(epoch.Fee.Denominator)); + break; + case FeeType.StakeWithdrawal stakeWithdrawal: + buffer.Add(3); + buffer.AddRange(BitConverter.GetBytes(stakeWithdrawal.Fee.Numerator)); + buffer.AddRange(BitConverter.GetBytes(stakeWithdrawal.Fee.Denominator)); + break; + case FeeType.SolWithdrawal solWithdrawal: + buffer.Add(4); + buffer.AddRange(BitConverter.GetBytes(solWithdrawal.Fee.Numerator)); + buffer.AddRange(BitConverter.GetBytes(solWithdrawal.Fee.Denominator)); + break; + case FeeType.SolDeposit solDeposit: + buffer.Add(5); + buffer.AddRange(BitConverter.GetBytes(solDeposit.Fee.Numerator)); + buffer.AddRange(BitConverter.GetBytes(solDeposit.Fee.Denominator)); + break; + case FeeType.StakeDeposit stakeDeposit: + buffer.Add(6); + buffer.AddRange(BitConverter.GetBytes(stakeDeposit.Fee.Numerator)); + buffer.AddRange(BitConverter.GetBytes(stakeDeposit.Fee.Denominator)); + break; + default: + throw new ArgumentException("Unknown FeeType variant"); + } + + return buffer.ToArray(); + } + + /// + /// Encodes the 'SetFee' instruction data using a Fee object. + /// + /// The fee to set. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeSetFee(Fee fee) + { + // Allocate 20 bytes: 4 bytes for the discriminator and 16 bytes for the fee (8 for numerator and 8 for denominator). + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetFee, MethodOffset); + data.WriteU64(fee.Numerator, MethodOffset + 4); + data.WriteU64(fee.Denominator, MethodOffset + 12); + return data; + } + + /// + /// Encodes the 'Redelegate' instruction data. + /// + internal static byte[] EncodeRedelegateData(ulong lamports, ulong sourceTransientStakeSeed, ulong ephemeralStakeSeed, ulong destinationTransientStakeSeed) + { + // 4 bytes for discriminator + 8 bytes each for 4 fields = 36 bytes total. + byte[] data = new byte[36]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.Redelegate, MethodOffset); + data.WriteU64(lamports, MethodOffset + 4); + data.WriteU64(sourceTransientStakeSeed, MethodOffset + 12); + data.WriteU64(ephemeralStakeSeed, MethodOffset + 20); + data.WriteU64(destinationTransientStakeSeed, MethodOffset + 28); + return data; + } + + /// + /// Encodes the 'UpdateStakePoolBalance' instruction data. + /// + internal static byte[] EncodeUpdateStakePoolBalance() + { + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.UpdateStakePoolBalance, MethodOffset); + return data; + } + + /// + /// Encodes the 'UpdateValidatorListBalance' instruction data. + /// + /// The starting index in the validator list. + /// If true, merging is disabled. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeUpdateValidatorListBalance(uint startIndex, bool noMerge) + { + // Allocate 9 bytes: 4 for the method discriminator, + // 4 for the start index, and 1 for the no-merge flag. + byte[] data = new byte[9]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.UpdateValidatorListBalance, MethodOffset); + data.WriteU32(startIndex, MethodOffset + 4); + data[MethodOffset + 8] = noMerge ? (byte)1 : (byte)0; + return data; + } + + /// + /// Encodes the 'CleanupRemovedValidatorEntries' instruction data. + /// + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeCleanupRemovedValidatorEntries() + { + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.CleanupRemovedValidatorEntries, MethodOffset); + return data; + } + + /// + /// Encodes the 'DepositStakeWithSlippage' instruction data. + /// + /// The minimum pool tokens expected on deposit (slippage constraint). + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeDepositStakeWithSlippage(ulong minimumPoolTokensOut) + { + // Allocate 12 bytes: + // 4 bytes for the instruction discriminator and 8 bytes for the minimum pool tokens out value. + byte[] data = new byte[12]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DepositStakeWithSlippage, MethodOffset); + data.WriteU64(minimumPoolTokensOut, MethodOffset + 4); + return data; + } + + /// + /// Encodes the 'DepositStake' instruction data. + /// + /// A byte array containing the encoded instruction data for DepositStake. + internal static byte[] EncodeDepositStake() + { + // Allocate 4 bytes for the instruction discriminator. + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DepositStake, MethodOffset); + return data; + } + + /// + /// Encodes the 'DepositSol' instruction data (without slippage). + /// + /// The amount of SOL lamports being deposited. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeDepositSol(ulong lamportsIn) + { + // Allocate 12 bytes: 4 for the discriminator and 8 for lamportsIn. + byte[] data = new byte[12]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DepositSol, MethodOffset); + data.WriteU64(lamportsIn, MethodOffset + 4); + return data; + } + + /// + /// Encodes the 'DepositSolWithSlippage' instruction data. + /// + /// The amount of SOL lamports being deposited. + /// The minimum pool tokens expected on deposit. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeDepositSolWithSlippage(ulong lamportsIn, ulong minimumPoolTokensOut) + { + // Allocate 20 bytes: 4 bytes for the discriminator, 8 for lamportsIn, 8 for minimumPoolTokensOut. + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.DepositSolWithSlippage, MethodOffset); + data.WriteU64(lamportsIn, MethodOffset + 4); + data.WriteU64(minimumPoolTokensOut, MethodOffset + 12); + return data; + } + + /// + /// Encodes the 'CreateTokenMetadata' instruction data. + /// + /// The name of the token. + /// The token symbol. + /// The URI for the token metadata. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeCreateTokenMetadata(string name, string symbol, string uri) + { + // Encode string fields as UTF8 byte arrays. + byte[] nameBytes = Encoding.UTF8.GetBytes(name); + byte[] symbolBytes = Encoding.UTF8.GetBytes(symbol); + byte[] uriBytes = Encoding.UTF8.GetBytes(uri); + + // Total length: + // 4 bytes for discriminator + + // 4 bytes (name length) + name bytes + + // 4 bytes (symbol length) + symbol bytes + + // 4 bytes (uri length) + uri bytes. + int totalLength = 4 + (4 + nameBytes.Length) + (4 + symbolBytes.Length) + (4 + uriBytes.Length); + byte[] data = new byte[totalLength]; + + int offset = 0; + // Write the discriminator. + data.WriteU32((uint)StakePoolProgramInstructions.Values.CreateTokenMetadata, offset); + offset += 4; + + // Write the name. + data.WriteU32((uint)nameBytes.Length, offset); + offset += 4; + nameBytes.CopyTo(data, offset); + offset += nameBytes.Length; + + // Write the symbol. + data.WriteU32((uint)symbolBytes.Length, offset); + offset += 4; + symbolBytes.CopyTo(data, offset); + offset += symbolBytes.Length; + + // Write the URI. + data.WriteU32((uint)uriBytes.Length, offset); + offset += 4; + uriBytes.CopyTo(data, offset); + + return data; + } + + /// + /// Encodes the 'UpdateTokenMetadata' instruction data. + /// + /// The new name for the pool token. + /// The new symbol for the pool token. + /// The new URI for the pool token metadata. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeUpdateTokenMetadata(string name, string symbol, string uri) + { + // Convert the string fields to UTF8 byte arrays. + byte[] nameBytes = Encoding.UTF8.GetBytes(name); + byte[] symbolBytes = Encoding.UTF8.GetBytes(symbol); + byte[] uriBytes = Encoding.UTF8.GetBytes(uri); + + // Compute total length: + // 4 bytes for discriminator + + // 4 bytes (name length) + name bytes + + // 4 bytes (symbol length) + symbol bytes + + // 4 bytes (uri length) + uri bytes. + int totalLength = 4 + (4 + nameBytes.Length) + (4 + symbolBytes.Length) + (4 + uriBytes.Length); + byte[] data = new byte[totalLength]; + + int offset = 0; + // Write the discriminator. Ensure that your StakePoolProgramInstructions enum has a value for UpdateTokenMetadata. + data.WriteU32((uint)StakePoolProgramInstructions.Values.UpdateTokenMetadata, offset); + offset += 4; + + // Write 'name'. + data.WriteU32((uint)nameBytes.Length, offset); + offset += 4; + nameBytes.CopyTo(data, offset); + offset += nameBytes.Length; + + // Write 'symbol'. + data.WriteU32((uint)symbolBytes.Length, offset); + offset += 4; + symbolBytes.CopyTo(data, offset); + offset += symbolBytes.Length; + + // Write 'uri'. + data.WriteU32((uint)uriBytes.Length, offset); + offset += 4; + uriBytes.CopyTo(data, offset); + + return data; + } + + /// + /// Encodes the 'SetFundingAuthority' instruction data. + /// + /// The funding type to be set. + /// The new authority public key. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeSetFundingAuthority(FundingType fundingType, PublicKey newAuthority) + { + // Allocate 37 bytes: + // 4 bytes for the discriminator, + // 1 byte for the funding type (as a byte), + // 32 bytes for the new authority public key. + byte[] data = new byte[37]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetFundingAuthority, MethodOffset); + + // Write funding type enum value as a byte. + data[MethodOffset + 4] = (byte)fundingType; + + // Write the new authority public key. + newAuthority.KeyBytes.CopyTo(data, MethodOffset + 5); + + return data; + } + + /// + /// Encodes the 'SetFundingAuthority' instruction data. + /// + /// The funding type. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeSetFundingAuthority(FundingType fundingType) + { + // Allocate 5 bytes: 4 bytes for the method discriminator and 1 byte for the funding type. + byte[] data = new byte[5]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetFundingAuthority, MethodOffset); + data[MethodOffset + 4] = (byte)fundingType; + return data; + } + + /// + /// Encodes the 'SetStaker' instruction data. + /// + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeSetStaker() + { + // Allocate 4 bytes for the discriminator only. + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetStaker, MethodOffset); + return data; + } + + /// + /// Encodes the 'SetManager' instruction data. + /// + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeSetManager() + { + // Allocate 4 bytes for the instruction discriminator. + byte[] data = new byte[4]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.SetManager, MethodOffset); + return data; + } + + /// + /// Encodes the 'WithdrawSolWithSlippage' instruction data. + /// + /// The amount of pool tokens being withdrawn. + /// The minimum lamports expected on withdrawal. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeWithdrawSolWithSlippage(ulong poolTokensIn, ulong minimumLamportsOut) + { + // Allocate 20 bytes: + // 4 bytes for the instruction discriminator, + // 8 bytes for the poolTokensIn amount, + // 8 bytes for the minimum lamports out value. + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.WithdrawSolWithSlippage, MethodOffset); + data.WriteU64(poolTokensIn, MethodOffset + 4); + data.WriteU64(minimumLamportsOut, MethodOffset + 12); + return data; + } + + /// + /// Encodes the 'WithdrawSol' instruction data (without slippage). + /// + /// The amount of pool tokens to be redeemed. + /// a byte array containing the encoded instruction data. + internal static byte[] EncodeWithdrawSol(ulong poolTokensIn) + { + // Allocate 12 bytes: 4 bytes for the instruction discriminator and 8 for poolTokensIn. + byte[] data = new byte[12]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.WithdrawSol, MethodOffset); + data.WriteU64(poolTokensIn, MethodOffset + 4); + return data; + } + + /// + /// Encodes the 'WithdrawStake' instruction data (without slippage). + /// + /// The amount of pool tokens to redeem. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeWithdrawStake(ulong poolTokensIn) + { + // Allocate 12 bytes: 4 bytes for the instruction discriminator and 8 for poolTokensIn. + byte[] data = new byte[12]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.WithdrawStake, MethodOffset); + data.WriteU64(poolTokensIn, MethodOffset + 4); + return data; + } + + /// + /// Encodes the 'WithdrawStakeWithSlippage' instruction data. + /// + /// The amount of pool tokens to redeem. + /// The minimum lamports expected on withdrawal. + /// A byte array containing the encoded instruction data. + internal static byte[] EncodeWithdrawStakeWithSlippage(ulong poolTokensIn, ulong minimumLamportsOut) + { + // Allocate 20 bytes: + // 4 bytes for the instruction discriminator, + // 8 bytes for poolTokensIn, + // 8 bytes for minimumLamportsOut. + byte[] data = new byte[20]; + data.WriteU32((uint)StakePoolProgramInstructions.Values.WithdrawStakeWithSlippage, MethodOffset); + data.WriteU64(poolTokensIn, MethodOffset + 4); + data.WriteU64(minimumLamportsOut, MethodOffset + 12); + return data; + } + + /// + /// Decodes the 'initialize' instruction data. + /// + internal static void DecodeInitializeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Fee assessed as percentage of perceived rewards", data.GetU64(4)); + decodedInstruction.Values.Add("Fee charged per withdrawal as percentage of withdrawal", data.GetU64(12)); + decodedInstruction.Values.Add("Fee charged per deposit as percentage of deposit", data.GetU64(20)); + decodedInstruction.Values.Add("Percentage [0-100] of deposit_fee that goes to referrer", data.GetU64(28)); + decodedInstruction.Values.Add("Maximum expected number of validators", data.GetU32(36)); + + } + + /// + /// Decodes the 'AddValidatorToPool' instruction data. + /// + internal static void DecodeAddValidatorToPoolData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + // The seed is at offset 4 (after the 4-byte method discriminator) + decodedInstruction.Values.Add("Seed", data.GetU32(4)); + } + + /// + /// Decodes the 'DecreaseValidatorStake' instruction data. + /// + internal static void DecodeDecreaseValidatorStakeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Lamports", data.GetU64(4)); + decodedInstruction.Values.Add("Transient Stake Seed", data.GetU64(12)); + } + + /// + /// Decodes the 'DecreaseAdditionalValidatorStake' instruction data. + /// + internal static void DecodeDecreaseAdditionalValidatorStakeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Lamports", data.GetU64(4)); + decodedInstruction.Values.Add("Transient Stake Seed", data.GetU64(12)); + decodedInstruction.Values.Add("Ephemeral Stake Seed", data.GetU64(20)); + } + + /// + /// Decodes the 'DecreaseValidatorStakeWithReserve' instruction data. + /// + internal static void DecodeDecreaseValidatorStakeWithReserveData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Lamports", data.GetU64(4)); + decodedInstruction.Values.Add("Transient Stake Seed", data.GetU64(12)); + } + + /// + /// Decodes the 'IncreaseValidatorStake' instruction data. + /// + internal static void DecodeIncreaseValidatorStakeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Lamports", data.GetU64(4)); + decodedInstruction.Values.Add("Transient Stake Seed", data.GetU64(12)); + } + + /// + /// Decodes the 'IncreaseAdditionalValidatorStake' instruction data. + /// + internal static void DecodeIncreaseAdditionalValidatorStakeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Lamports", data.GetU64(4)); + decodedInstruction.Values.Add("Transient Stake Seed", data.GetU64(12)); + decodedInstruction.Values.Add("Ephemeral Stake Seed", data.GetU64(20)); + } + + /// + /// Decodes the 'SetPreferredDepositValidator' instruction data. + /// + internal static void DecodeSetPreferredValidatorData(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Validator Type", (PreferredValidatorType)data.GetU32(4)); + if (data.Length >= 8 + PublicKey.PublicKeyLength) + { + var keyBytes = data.Slice(8, PublicKey.PublicKeyLength).ToArray(); + decodedInstruction.Values.Add("Validator Vote Address", new PublicKey(keyBytes)); + } + } + + /// + /// Decodes the 'UpdateValidatorListBalance' instruction data. + /// + /// The to populate. + /// The instruction data as a read-only span. + /// The list of account public keys associated with the instruction. + /// Indices of the keys related to the instruction. + internal static void DecodeUpdateValidatorListBalance(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + uint startIndex = data.GetU32(MethodOffset + 4); + bool noMerge = data[MethodOffset + 8] != 0; + decodedInstruction.Values.Add("Start Index", startIndex); + decodedInstruction.Values.Add("No Merge", noMerge); + } + + /// + /// Decodes the 'DepositStakeWithSlippage' instruction data. + /// + /// The to populate. + /// The instruction data as a read-only span. + /// The list of account public keys associated with the instruction. + /// Indices of the keys related to the instruction. + internal static void DecodeDepositStakeWithSlippage(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + // The minimum pool tokens out is stored 4 bytes after the discriminator. + ulong minimumPoolTokensOut = data.GetU64(MethodOffset + 4); + decodedInstruction.Values.Add("Minimum Pool Tokens Out", minimumPoolTokensOut); + } + + /// + /// Decodes the 'SetFundingAuthority' instruction data. + /// + /// The to populate. + /// The instruction data as a read-only span. + /// The list of account public keys associated with the instruction. + /// Indices of the keys related to the instruction. + internal static void DecodeSetFundingAuthority(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + // The funding type is stored at offset 4 (after the 4-byte discriminator) + decodedInstruction.Values.Add("Funding Type", (FundingType)data[MethodOffset + 4]); + } + + /// + /// Decodes the 'SetStaker' instruction data. + /// + /// The to populate. + /// The instruction data as a read-only span. + /// The list of account public keys associated with the instruction. + /// Indices of the keys related to the instruction. + internal static void DecodeSetStaker(DecodedInstruction decodedInstruction, ReadOnlySpan data, IList keys, byte[] keyIndices) + { + // Extract the new staker public key from offset 4. + var keyBytes = data.Slice(MethodOffset + 4, PublicKey.PublicKeyLength).ToArray(); + decodedInstruction.Values.Add("New Staker", new PublicKey(keyBytes)); + } + } +} diff --git a/src/Solnet.Programs/StakePool/StakePoolProgramInstructions.cs b/src/Solnet.Programs/StakePool/StakePoolProgramInstructions.cs new file mode 100644 index 00000000..49424c7d --- /dev/null +++ b/src/Solnet.Programs/StakePool/StakePoolProgramInstructions.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Solnet.Programs.StakePool +{ + /// + /// Represents the instruction types for the along with a friendly name so as not to use reflection. + /// + /// + /// For more information see: + /// https://spl.solana.com/stake-pool + /// https://docs.rs/spl-stake-pool/latest/spl_stake_pool/ + /// + internal static class StakePoolProgramInstructions + { + /// + /// Represents the user-friendly names for the instruction types for the . + /// + internal static readonly Dictionary Names = new() + { + { Values.Initialize, "Initialize" }, + { Values.AddValidatorToPool, "Add Validator To Pool" }, + { Values.RemoveValidatorFromPool, "Remove Validator From Pool" }, + { Values.DecreaseValidatorStake, "Decrease Validator Stake" }, + { Values.IncreaseValidatorStake, "Increase Validator Stake" }, + { Values.SetPreferredValidator, "Set Preferred Deposit Validator" }, + { Values.UpdateValidatorListBalance, "Update Validator List Balance" }, + { Values.UpdateStakePoolBalance, "Update Stake Pool Balance" }, + { Values.CleanupRemovedValidatorEntries, "Cleanup Removed Validator Entries" }, + { Values.DecreaseValidatorStakeWithReserve, "Decrease Validator Stake With Reserve" }, + { Values.CreateTokenMetadata, "Create Token Metadata" }, + { Values.UpdateTokenMetadata, "Update Token Metadata" }, + { Values.DepositStake, "Deposit some stake into the pool" }, + { Values.WithdrawStake, "Withdraw Stake" }, + { Values.DecreaseAdditionalValidatorStake, "Decrease Additional Validator Stake" }, + { Values.SetManager, "Set Manager" }, + { Values.Redelegate, "Redelegate active stake on a validator" }, + { Values.DepositStakeWithSlippage, "Deposit some stake into the pool, with a specified slippage" }, + { Values.WithdrawStakeWithSlippage, "Withdraw the token from the pool at the current ratio" }, + { Values.DepositSolWithSlippage, "Deposit SOL directly into the pool's reserve account, with a specified slippage constraint." }, + { Values.SetFee, "Set Fee" }, + { Values.SetStaker, "Set Staker" }, + { Values.DepositSol, "Deposit Sol" }, + { Values.SetFundingAuthority, "Set Funding Authority" }, + { Values.WithdrawSol, "Withdraw Sol" }, + { Values.IncreaseAdditionalValidatorStake, "Increase Additional Validator Stake" }, + }; + + /// + /// Represents the instruction types for the . + /// + internal enum Values : uint + { + /// + /// Initializes a new StakePool. + /// + Initialize = 0, + + /// + /// (Staker only) Adds stake account delegated to validator to the pool's list of managed validators. + /// The stake account will have the rent-exempt amount plus max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation()). + /// It is funded from the stake pool reserve. + /// Userdata: optional non-zero u32 seed used for generating the validator stake address. + /// + AddValidatorToPool = 1, + + /// + /// (Staker only) Removes validator from the pool, deactivating its stake. + /// Only succeeds if the validator stake account has the minimum of max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation()) plus the rent-exempt amount. + /// + RemoveValidatorFromPool = 2, + + /// + /// (Deprecated since v0.7.0, use instead) + /// (Staker only) Decrease active stake on a validator, eventually moving it to the reserve. + /// Internally, this instruction splits a validator stake account into its corresponding transient stake account and deactivates it. + /// + DecreaseValidatorStake = 3, + + /// + /// (Staker only) Increase stake on a validator from the reserve account. + /// Internally, this instruction splits reserve stake into a transient stake account and delegates to the appropriate validator. + /// will do the work of merging once it's ready. + /// Userdata: amount of lamports to increase on the given validator. + /// The actual amount split into the transient stake account is: lamports + stake_rent_exemption. + /// The rent-exemption of the stake account is withdrawn back to the reserve after it is merged. + /// + IncreaseValidatorStake = 4, + + /// + /// (Staker only) Set the preferred deposit or withdraw stake account for the stake pool. + /// In order to avoid users abusing the stake pool as a free conversion between SOL staked on different validators, + /// the staker can force all deposits and/or withdraws to go to one chosen account, or unset that account. + /// + SetPreferredValidator = 5, + + /// + /// Updates balances of validator and transient stake accounts in the pool. + /// While going through the pairs of validator and transient stake accounts, if the transient stake is inactive, + /// it is merged into the reserve stake account. If the transient stake is active and has matching credits observed, + /// it is merged into the canonical validator stake account. In all other states, nothing is done, and the balance is simply added to the canonical stake account balance. + /// + UpdateValidatorListBalance = 6, + + /// + /// Updates total pool balance based on balances in the reserve and validator list. + /// + UpdateStakePoolBalance = 7, + + /// + /// Cleans up validator stake account entries marked as ReadyForRemoval. + /// + CleanupRemovedValidatorEntries = 8, + + /// + /// Deposit some stake into the pool. The output is a "pool" token representing ownership into the pool. Inputs are converted to the current ratio. + /// + DepositStake = 9, + + /// + /// Withdraw the token from the pool at the current ratio. + /// Succeeds if the stake account has enough SOL to cover the desired amount of pool tokens, and if the withdrawal keeps the total staked amount above the minimum of rent-exempt amount + max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation()). + /// When allowing withdrawals, the order of priority goes: preferred withdraw validator stake account (if set), validator stake accounts, transient stake accounts, reserve stake account OR totally remove validator stake accounts. + /// Userdata: amount of pool tokens to withdraw. + /// + WithdrawStake = 10, + + /// + /// (Manager only) Update manager. + /// + SetManager = 11, + + /// + /// (Manager only) Update fee. + /// + SetFee = 12, + + /// + /// (Manager or staker only) Update staker. + /// + SetStaker = 13, + + /// + /// Deposit SOL directly into the pool's reserve account. The output is a "pool" token representing ownership into the pool. Inputs are converted to the current ratio. + /// + DepositSol = 14, + + /// + /// (Manager only) Update SOL deposit, stake deposit, or SOL withdrawal authority. + /// + SetFundingAuthority = 15, + + /// + /// Withdraw SOL directly from the pool's reserve account. Fails if the reserve does not have enough SOL. + /// + WithdrawSol = 16, + + /// + /// Create token metadata for the stake-pool token in the metaplex-token program. + /// + CreateTokenMetadata = 17, + + /// + /// Update token metadata for the stake-pool token in the metaplex-token program. + /// + UpdateTokenMetadata = 18, + + /// + /// (Staker only) Increase stake on a validator again in an epoch. + /// Works regardless if the transient stake account exists. + /// Internally, this instruction splits reserve stake into an ephemeral stake account, activates it, then merges or splits it into the transient stake account delegated to the appropriate validator. + /// will do the work of merging once it's ready. + /// Userdata: amount of lamports to increase on the given validator. + /// The actual amount split into the transient stake account is: lamports + stake_rent_exemption. + /// The rent-exemption of the stake account is withdrawn back to the reserve after it is merged. + /// + IncreaseAdditionalValidatorStake = 19, + + /// + /// (Staker only) Decrease active stake again from a validator, eventually moving it to the reserve. + /// Works regardless if the transient stake account already exists. + /// Internally, this instruction: withdraws rent-exempt reserve lamports from the reserve into the ephemeral stake, splits a validator stake account into an ephemeral stake account, deactivates the ephemeral account, merges or splits the ephemeral account into the transient stake account delegated to the appropriate validator. + /// + DecreaseAdditionalValidatorStake = 20, + + /// + /// (Staker only) Decrease active stake on a validator, eventually moving it to the reserve. + /// Internally, this instruction: withdraws enough lamports to make the transient account rent-exempt, splits from a validator stake account into a transient stake account, deactivates the transient stake account. + /// + DecreaseValidatorStakeWithReserve = 21, + + /// + /// (Staker only) Redelegate active stake on a validator, eventually moving it to another. + /// Internally, this instruction splits a validator stake account into its corresponding transient stake account, redelegates it to an ephemeral stake account, then merges that stake into the destination transient stake account. + /// + Redelegate = 22, + + /// + /// Deposit some stake into the pool, with a specified slippage constraint. The output is a "pool" token representing ownership into the pool. Inputs are converted at the current ratio. + /// + DepositStakeWithSlippage = 23, + + /// + /// Withdraw the token from the pool at the current ratio, specifying a minimum expected output lamport amount. + /// Succeeds if the stake account has enough SOL to cover the desired amount of pool tokens, and if the withdrawal keeps the total staked amount above the minimum of rent-exempt amount + max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation()). + /// Userdata: amount of pool tokens to withdraw. + /// + WithdrawStakeWithSlippage = 24, + + /// + /// Deposit SOL directly into the pool's reserve account, with a specified slippage constraint. The output is a "pool" token representing ownership into the pool. Inputs are converted at the current ratio. + /// + DepositSolWithSlippage = 25, + + /// + /// Withdraw SOL directly from the pool's reserve account. Fails if the reserve does not have enough SOL or if the slippage constraint is not met. + /// + WithdrawSolWithSlippage = 26, + } + } +} diff --git a/test/Solnet.Programs.Test/StakePoolProgramTest.cs b/test/Solnet.Programs.Test/StakePoolProgramTest.cs new file mode 100644 index 00000000..21b97ff0 --- /dev/null +++ b/test/Solnet.Programs.Test/StakePoolProgramTest.cs @@ -0,0 +1,452 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Solnet.Programs.StakePool; +using Solnet.Programs.StakePool.Models; +using Solnet.Wallet; +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Solnet.Programs.Test +{ + [TestClass] + public class StakePoolProgramTest + { + private static readonly PublicKey StakePool = new("11111111111111111111111111111111"); + private static readonly PublicKey Manager = new("22222222222222222222222222222222"); + private static readonly PublicKey Staker = new("33333333333333333333333333333333"); + private static readonly PublicKey WithdrawAuthority = new("44444444444444444444444444444444"); + private static readonly PublicKey ValidatorList = new("55555555555555555555555555555555"); + private static readonly PublicKey ReserveStake = new("66666666666666666666666666666666"); + private static readonly PublicKey PoolMint = new("77777777777777777777777777777777"); + private static readonly PublicKey ManagerPoolAccount = new("88888888888888888888888888888888"); + private static readonly PublicKey TokenProgramId = new("99999999999999999999999999999999"); + private static readonly PublicKey DepositAuthority = new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + private static readonly PublicKey StakeAccount = new("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + private static readonly PublicKey ValidatorAccount = new("cccccccccccccccccccccccccccccccc"); + private static readonly PublicKey TransientStakeAccount = new("dddddddddddddddddddddddddddddddd"); + private static readonly PublicKey EphemeralStake = new("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + private static readonly PublicKey Validator = new("ffffffffffffffffffffffffffffffff"); + // For SetStaker and SetManager tests using different keys. + private static readonly PublicKey NewStaker = new("11111111111111111111111111111112"); + private static readonly PublicKey NewManager = new("22222222222222222222222222222223"); + private static readonly PublicKey NewFeeReceiver = new("33333333333333333333333333333334"); + private static readonly PublicKey NewDepositAuthority = new("44444444444444444444444444444445"); + private static readonly PublicKey UserPoolTokenAccount = new("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"); + + + private static Fee DummyFee() => new Fee(1, 100); // use a dummy fee with nonzero values + + + [TestMethod] + public void Initialize_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.Initialize( + StakePool, Manager, Staker, WithdrawAuthority, ValidatorList, ReserveStake, PoolMint, + ManagerPoolAccount, TokenProgramId, DummyFee(), DummyFee(), DummyFee(), DummyFee(), + DepositAuthority, 42); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count >= 4); // At least the required keys + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void AddValidatorToPool_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.AddValidatorToPool( + StakePool, Staker, ReserveStake, WithdrawAuthority, ValidatorList, + StakeAccount, ValidatorAccount, 123); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void RemoveValidatorFromPool_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.RemoveValidatorFromPool( + StakePool, Staker, WithdrawAuthority, ValidatorList, StakeAccount, TransientStakeAccount); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void DecreaseValidatorStake_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.DecreaseValidatorStakeWithReserve( + StakePool, Staker, WithdrawAuthority, ValidatorList, ReserveStake, StakeAccount, + TransientStakeAccount, 1000, 55); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void DecreaseAdditionalValidatorStake_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.DecreaseAdditionalValidatorStake( + StakePool, Staker, WithdrawAuthority, ValidatorList, ReserveStake, StakeAccount, + EphemeralStake, TransientStakeAccount, 1000, 55, 77); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void DecreaseValidatorStakeWithReserve_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.DecreaseValidatorStakeWithReserve( + StakePool, Staker, WithdrawAuthority, ValidatorList, ReserveStake, StakeAccount, + TransientStakeAccount, 1000, 55); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void IncreaseValidatorStake_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.IncreaseValidatorStake( + StakePool, Staker, WithdrawAuthority, ValidatorList, ReserveStake, TransientStakeAccount, + StakeAccount, 1000, 55); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void IncreaseAdditionalValidatorStake_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + var instr = program.IncreaseAdditionalValidatorStake( + StakePool, Staker, WithdrawAuthority, ValidatorList, ReserveStake, EphemeralStake, + TransientStakeAccount, StakeAccount, Validator, 1000, 55, 77); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void SetPreferredDepositValidator_CreatesCorrectInstruction() + { + var program = new StakePoolProgram(); + // Passing Validator as the optional validator vote address causes the keys count to be 4. + var instr = program.SetPreferredDepositValidator( + StakePool, Staker, ValidatorList, PreferredValidatorType.Deposit, Validator); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + // Expecting 4 keys because the optional parameter is provided. + Assert.AreEqual(4, instr.Keys.Count); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void SetFee_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.SetFee(StakePool, Manager, DummyFee()); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count >= 2); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void SetStaker_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.SetStaker(StakePool, Staker, NewStaker); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + // Expecting 3 keys as per the implementation + Assert.AreEqual(3, instr.Keys.Count); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void SetManager_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.SetManager(StakePool, Manager, NewManager, NewFeeReceiver); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + // Expecting 4 keys per the SetManager instruction + Assert.AreEqual(4, instr.Keys.Count); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void SetFundingAuthority_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.SetFundingAuthority(StakePool, Manager, NewDepositAuthority, FundingType.SolDeposit); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + // Keys: StakePool, Manager, and NewDepositAuthority should be provided + Assert.IsTrue(instr.Keys.Count >= 2); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void DepositStake_CreatesCorrectInstruction() + { + var instrList = StakePoolProgram.DepositStake( + StakePool, ValidatorList, WithdrawAuthority, StakeAccount, + DepositAuthority, ValidatorAccount, ReserveStake, PoolMint, + ManagerPoolAccount, DepositAuthority, PoolMint, TokenProgramId); + + Assert.IsTrue(instrList.Count > 0); + // Ensure that at least one instruction uses the StakePoolProgram ID. + Assert.IsTrue(instrList.Any(i => i.ProgramId.SequenceEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes)), + "None of the instructions use the stake pool program ID."); + + // Additionally ensure each instruction has keys and nonempty data. + foreach (var instr in instrList) + { + Assert.IsTrue(instr.Keys.Count > 0, "Instruction has no keys."); + Assert.IsTrue(instr.Data.Length > 0, "Instruction has empty data."); + } + } + + [TestMethod] + public void DepositStakeWithSlippage_CreatesCorrectInstruction() + { + var instrList = StakePoolProgram.DepositStakeWithSlippage( + StakePool, ValidatorList, WithdrawAuthority, StakeAccount, + DepositAuthority, ValidatorAccount, ReserveStake, PoolMint, + ManagerPoolAccount, DepositAuthority, PoolMint, TokenProgramId, 1000); + + Assert.IsTrue(instrList.Count > 0); + // Check that at least one instruction uses the stake pool program ID. + Assert.IsTrue(instrList.Any(i => i.ProgramId.SequenceEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes)), + "None of the instructions use the stake pool program ID."); + + // Additionally ensure each instruction has nonempty keys and data. + foreach (var instr in instrList) + { + Assert.IsTrue(instr.Keys.Count > 0, "Instruction has no keys."); + Assert.IsTrue(instr.Data.Length > 0, "Instruction has empty data."); + } + } + + [TestMethod] + public void WithdrawStake_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawStake( + StakePool, // stakePool + ValidatorList, // validatorListStorage + WithdrawAuthority, // stakePoolWithdrawAuthority + StakeAccount, // stakeToSplit + TransientStakeAccount, // stakeToReceive + Staker, // userStakeAuthority + Manager, // userTransferAuthority + UserPoolTokenAccount, // userPoolTokenAccount + ManagerPoolAccount, // managerFeeAccount + PoolMint, // poolMint + TokenProgramId, // tokenProgramId + 2000); // poolTokensIn + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void WithdrawStakeWithSlippage_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawStakeWithSlippage( + StakePool, // stakePool + ValidatorList, // validatorListStorage + WithdrawAuthority, // stakePoolWithdrawAuthority + StakeAccount, // stakeToSplit + TransientStakeAccount, // stakeToReceive + Staker, // userStakeAuthority + Manager, // userTransferAuthority + UserPoolTokenAccount, // userPoolTokenAccount + ManagerPoolAccount, // managerFeeAccount + PoolMint, // poolMint + TokenProgramId, // tokenProgramId + 2000, // poolTokensIn + 1500); // minimumLamportsOut + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void WithdrawSol_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawSol( + StakePool, WithdrawAuthority, Manager, StakeAccount, ReserveStake, + PoolMint, ManagerPoolAccount, PoolMint, TokenProgramId, 3000); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void WithdrawSolWithSlippage_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawSolWithSlippage( + StakePool, WithdrawAuthority, Manager, StakeAccount, ReserveStake, + PoolMint, ManagerPoolAccount, PoolMint, TokenProgramId, 3000, 2500); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + // Updated WithdrawSolWithAuthority test + [TestMethod] + public void WithdrawSolWithAuthority_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawSolWithAuthority( + StakePool, DepositAuthority, WithdrawAuthority, Manager, StakeAccount, ReserveStake, + PoolMint, ManagerPoolAccount, PoolMint, TokenProgramId, 3000); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Any(x => x.PublicKey.Equals(DepositAuthority) && x.IsSigner), + "DepositAuthority is not found with IsSigner true in the account metas."); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void WithdrawSolWithAuthorityAndSlippage_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.WithdrawSolWithAuthorityAndSlippage( + StakePool, DepositAuthority, WithdrawAuthority, Manager, StakeAccount, ReserveStake, + PoolMint, ManagerPoolAccount, PoolMint, TokenProgramId, 3000, 2500); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Any(x => x.PublicKey.Equals(DepositAuthority) && x.IsSigner), + "DepositAuthority is not found with IsSigner true in the account metas."); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void UpdateValidatorListBalanceChunk_CreatesCorrectInstruction() + { + // For update test, we simulate a validator list with dummy validators. + var dummyValidatorList = new ValidatorList + { + Validators = new System.Collections.Generic.List + { + new ValidatorStakeInfo { VoteAccountAddress = new PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"), ValidatorSeedSuffix = 1, TransientSeedSuffix = 10 }, + new ValidatorStakeInfo { VoteAccountAddress = new PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"), ValidatorSeedSuffix = 2, TransientSeedSuffix = 20 }, + new ValidatorStakeInfo { VoteAccountAddress = new PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3"), ValidatorSeedSuffix = 3, TransientSeedSuffix = 30 } + } + }; + + var instr = StakePoolProgram.UpdateValidatorListBalanceChunk( + StakePool, WithdrawAuthority, ValidatorList, ReserveStake, + dummyValidatorList, 2, 0, true); + + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count > 0); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void UpdateStakePoolBalance_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.UpdateStakePoolBalance( + StakePool, WithdrawAuthority, ValidatorList, ReserveStake, ManagerPoolAccount, PoolMint, TokenProgramId); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count >= 7); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void CleanupRemovedValidatorEntries_CreatesCorrectInstruction() + { + var instr = StakePoolProgram.CleanupRemovedValidatorEntries(StakePool, ValidatorList); + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Keys.Count == 2); + Assert.IsTrue(instr.Data.Length > 0); + } + + [TestMethod] + public void UpdateStaleStakePool_CreatesCorrectInstructions() + { + // Create a dummy validator list with one outdated validator. + var dummyValidatorList = new ValidatorList + { + Validators = new System.Collections.Generic.List + { + new ValidatorStakeInfo { VoteAccountAddress = new PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"), ValidatorSeedSuffix = 1, TransientSeedSuffix = 10, LastUpdateEpoch = 10 }, + new ValidatorStakeInfo { VoteAccountAddress = new PublicKey("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"), ValidatorSeedSuffix = 2, TransientSeedSuffix = 20, LastUpdateEpoch = 5 } + } + }; + + var result = StakePoolProgram.UpdateStaleStakePool( + new StakePool.Models.StakePool { Staker = Staker, ValidatorList = ValidatorList, ReserveStake = ReserveStake, ManagerFeeAccount = ManagerPoolAccount, PoolMint = PoolMint, TokenProgramId = TokenProgramId }, + dummyValidatorList, StakePool, true, 6); + + Assert.IsNotNull(result); + Assert.IsTrue(result.updateListInstructions.Count >= 1); + Assert.IsTrue(result.finalInstructions.Count >= 2); + foreach (var instr in result.updateListInstructions) + { + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Data.Length > 0); + } + foreach (var instr in result.finalInstructions) + { + CollectionAssert.AreEqual(StakePoolProgram.StakePoolProgramIdKey.KeyBytes, instr.ProgramId); + Assert.IsTrue(instr.Data.Length > 0); + } + } + } + + [TestClass] + public class StakePoolModelsTest + { + [TestMethod] + public void Fee_StoresCorrectValues() + { + // Arrange + var fee = new Fee(100, 1000); + + // Act & Assert + Assert.AreEqual(100UL, fee.Numerator, "Fee numerator not stored correctly."); + Assert.AreEqual(1000UL, fee.Denominator, "Fee denominator not stored correctly."); + } + + [TestMethod] + public void Fee_DefaultIsZero() + { + // Arrange + var fee = new Fee(); + + // Act & Assert + Assert.AreEqual(0UL, fee.Numerator, "Default fee numerator should be zero."); + Assert.AreEqual(0UL, fee.Denominator, "Default fee denominator should be zero."); + } + + [TestMethod] + public void PreferredValidatorType_ValuesAreConsistent() + { + // Assuming that PreferredValidatorType is an enum with defined underlying values, + // you might for example have: + // Deposit = 0, Withdraw = 1 (adjust as per your actual enum definition). + uint depositValue = (uint)PreferredValidatorType.Deposit; + uint withdrawValue = (uint)PreferredValidatorType.Withdraw; + + // Assert that they are different and non-negative. + Assert.AreNotEqual(depositValue, withdrawValue, "PreferredValidatorType values must differ."); + Assert.IsTrue(depositValue < withdrawValue, "Expected Deposit value to be less than Withdraw value."); + } + + [TestMethod] + public void FundingType_ValuesAreConsistent() + { + byte solDeposit = (byte)FundingType.SolDeposit; + Assert.AreEqual(1, solDeposit, "FundingType.SolDeposit is expected to be 1."); + } + } +} \ No newline at end of file