prepareWithdrawAccounts static method

Future<List<WithdrawAccount>> prepareWithdrawAccounts({
  1. required SolanaProvider 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 SolanaProvider 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 SolanaPluginException(
      'Validator list account does not found.',
    );
  }

  final minBalanceForRentExemption = await connection.request(
    SolanaRequestGetMinimumBalanceForRentExemption(
      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(
    SolanaRequestGetAccountInfo(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 SolanaPluginException(
      'No stake accounts found in this pool with enough balance to withdraw.',
    );
  }

  return withdrawFrom;
}