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