prepareWithdrawAccounts static method

Future<List<WithdrawAccount>> prepareWithdrawAccounts({
  1. required SolanaRPC connection,
  2. required StakePoolAccount stakePool,
  3. required SolAddress stakePoolAddress,
  4. required BigInt amount,
  5. bool skipFee = false,
  6. Comparator<ValidatorAccount>? compareFn,
})

Implementation

static Future<List<WithdrawAccount>> prepareWithdrawAccounts({
  required SolanaRPC connection,
  required StakePoolAccount stakePool,
  required SolAddress stakePoolAddress,
  required BigInt amount,
  bool skipFee = false,
  Comparator<ValidatorAccount>? compareFn,
}) async {
  final validatorList = await connection.request(
      SolanaRPCGetStakePoolValidatorListAccount(
          address: stakePool.validatorList.address));
  if (validatorList == null || validatorList.validators.isEmpty) {
    throw const MessageException("Validator list account does not found.");
  }

  final minBalanceForRentExemption = await connection.request(
      SolanaRPCGetMinimumBalanceForRentExemption(
          size: StakeProgramConst.stakeProgramSpace.toInt()));
  final minBalance =
      minBalanceForRentExemption + StakePoolProgramConst.minimumActiveStake;

  final List<ValidatorAccount> accounts = [];

  // Prepare accounts
  for (final validator in validatorList.validators) {
    if (validator.status != ValidatorStakeInfoStatus.active) {
      continue;
    }

    final stakeAccountAddress = StakePoolProgramUtils.findStakeProgramAddress(
      voteAccountAddress: validator.voteAccountAddress,
      stakePoolAddress: stakePoolAddress,
    );

    if (validator.activeStakeLamports != BigInt.zero) {
      final isPreferred =
          stakePool.preferredWithdrawValidatorVoteAddress?.address ==
              validator.voteAccountAddress.address;
      accounts.add(ValidatorAccount(
        type: isPreferred
            ? ValidatorAccountType.preferred
            : ValidatorAccountType.active,
        voteAddress: validator.voteAccountAddress,
        stakeAddress: stakeAccountAddress.address,
        lamports: validator.activeStakeLamports,
      ));
    }

    final transientStakeLamports =
        validator.transientStakeLamports - minBalance;
    if (transientStakeLamports > BigInt.zero) {
      final transientStakeAccountAddress =
          StakePoolProgramUtils.findTransientStakeProgramAddress(
        voteAccountAddress: validator.voteAccountAddress,
        stakePoolAddress: stakePoolAddress,
        seed: validator.transientSeedSuffixStart,
      );
      accounts.add(ValidatorAccount(
        type: ValidatorAccountType.transient,
        voteAddress: validator.voteAccountAddress,
        stakeAddress: transientStakeAccountAddress.address,
        lamports: transientStakeLamports,
      ));
    }
  }

  // Sort from highest to lowest balance
  accounts.sort(compareFn ?? (a, b) => b.lamports.compareTo(a.lamports));

  final reserveStake = await connection
      .request(SolanaRPCGetAccountInfo(account: stakePool.reserveStake));

  final reserveStakeBalance =
      (reserveStake?.lamports ?? BigInt.zero) - minBalanceForRentExemption;
  if (reserveStakeBalance > BigInt.zero) {
    accounts.add(
      ValidatorAccount(
        type: ValidatorAccountType.reserve,
        stakeAddress: stakePool.reserveStake,
        lamports: reserveStakeBalance,
      ),
    );
  }

  // Prepare the list of accounts to withdraw from
  final withdrawFrom = <WithdrawAccount>[];
  BigInt remainingAmount = amount;

  final fee = stakePool.stakeWithdrawalFee;
  final inverseFee = StakePoolFee(
    numerator: fee.denominator - fee.numerator,
    denominator: fee.denominator,
  );

  for (final type in ValidatorAccountType.values) {
    final filteredAccounts = accounts.where((a) => a.type == type);
    for (final i in filteredAccounts) {
      if (i.lamports <= minBalance &&
          type == ValidatorAccountType.transient) {
        continue;
      }
      BigInt availableForWithdrawal =
          calcPoolTokensForDeposit(stakePool, i.lamports);
      if (!skipFee && inverseFee.numerator != BigInt.zero) {
        availableForWithdrawal = BigRational(
          availableForWithdrawal * inverseFee.denominator,
          denominator: inverseFee.numerator,
        ).toBigInt();
      }

      final poolAmount = availableForWithdrawal < remainingAmount
          ? availableForWithdrawal
          : remainingAmount;
      if (poolAmount <= BigInt.zero) {
        continue;
      }

      // Those accounts will be withdrawn completely with `claim` instruction
      withdrawFrom.add(WithdrawAccount(
          stakeAddress: i.stakeAddress,
          voteAddress: i.voteAddress,
          poolAmount: poolAmount));
      remainingAmount -= poolAmount;

      if (remainingAmount == BigInt.zero) {
        break;
      }
    }

    if (remainingAmount == BigInt.zero) {
      break;
    }
  }

  // Not enough stake to withdraw the specified amount
  if (remainingAmount > BigInt.zero) {
    throw const MessageException(
        'No stake accounts found in this pool with enough balance to withdraw.');
  }

  return withdrawFrom;
}