@everstake/wallet-sdk-solana
Version:
Solana - Everstake Wallet SDK
1,044 lines (932 loc) • 31.3 kB
text/typescript
/**
* Copyright (c) 2025, Everstake.
* Licensed under the BSD-3-Clause License. See LICENSE file for details.
*/
import {
Address,
Account,
createAddressWithSeed,
createDefaultRpcTransport,
createSolanaRpcFromTransport,
mainnet,
devnet,
ClusterUrl,
address,
TransactionMessageWithBlockhashLifetime,
createNoopSigner,
pipe,
CompilableTransactionMessage,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
RpcFromTransport,
SolanaRpcApiFromTransport,
RpcTransportFromClusterUrl,
IInstruction,
generateKeyPair,
createSignerFromKeyPair,
partiallySignTransactionMessageWithSigners,
parseBase64RpcAccount,
prependTransactionMessageInstruction,
} from '@solana/kit';
import {
getCreateAccountWithSeedInstruction,
getCreateAccountInstruction,
getTransferSolInstruction,
getAllocateWithSeedInstruction,
} from '@solana-program/system';
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from '@solana-program/compute-budget';
import { Blockchain } from '../../utils';
import { ERROR_MESSAGES } from './constants/errors';
import {
DEVNET_VALIDATOR_ADDRESS,
FILTER_DATA_SIZE,
FILTER_OFFSET,
MAINNET_VALIDATOR_ADDRESS,
MIN_AMOUNT,
Network,
StakeState,
STAKE_ACCOUNT_V2_SIZE,
ADDRESS_DEFAULT,
STAKE_HISTORY_ACCOUNT,
STAKE_CONFIG_ACCOUNT,
MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT,
MAX_CLAIM_ACCOUNTS,
MAX_DEACTIVATE_ACCOUNTS,
} from './constants';
import {
ApiResponse,
CreateAccountResponse,
ClaimResponse,
StakeResponse,
Delegations,
UnstakeResponse,
Params,
RpcConfig,
} from './types';
import {
getWithdrawInstruction,
getDelegateStakeInstruction,
getDeactivateInstruction,
getInitializeInstruction,
getSplitInstruction,
STAKE_PROGRAM_ADDRESS,
decodeStakeStateAccount,
StakeStateAccount,
StakeStateV2,
} from '@solana-program/stake';
/**
* The `Solana` class extends the `Blockchain` class and provides methods for interacting with the Solana blockchain.
*
* @property connection - The connection to the Solana blockchain.
* @property ERROR_MESSAGES - The error messages for the Solana class.
* @property ORIGINAL_ERROR_MESSAGES - The original error messages for the Solana class.
* @throws Throws an error if there's an issue establishing the connection.
*/
export class Solana extends Blockchain {
private connection!: RpcFromTransport<
SolanaRpcApiFromTransport<RpcTransportFromClusterUrl<ClusterUrl>>,
RpcTransportFromClusterUrl<ClusterUrl>
>;
private validator: Address;
protected ERROR_MESSAGES = ERROR_MESSAGES;
protected ORIGINAL_ERROR_MESSAGES = {};
constructor(network: Network = Network.Mainnet, rpcConfig: RpcConfig = {}) {
super();
if (rpcConfig.rpc && !this.isValidURL(rpcConfig.rpc)) {
throw this.throwError('INVALID_RPC_ERROR');
}
switch (network) {
case Network.Mainnet:
rpcConfig.rpc =
rpcConfig.rpc || mainnet('https://api.mainnet-beta.solana.com');
this.validator = MAINNET_VALIDATOR_ADDRESS;
break;
case Network.Devnet:
rpcConfig.rpc =
rpcConfig.rpc || devnet('https://api.devnet.solana.com');
this.validator = DEVNET_VALIDATOR_ADDRESS;
break;
default:
throw this.throwError('UNSUPPORTED_NETWORK_ERROR');
}
try {
const transport = createDefaultRpcTransport({
url: rpcConfig.rpc,
headers: {
'User-Agent': rpcConfig.userAgent || '',
},
});
this.connection = createSolanaRpcFromTransport(transport);
} catch (error) {
throw this.handleError('CONNECTION_ERROR', error);
}
}
/**
* Creates a new stake account.
*
* @param address - The public key of the account as PublicKey.
* @param lamports - The amount to stake in lamports.
* @param source - stake source
* @param lockup - stake account lockup
*
* @throws Throws an error if the lamports is less than the minimum amount.
* @throws Throws an error if there's an issue creating the stake account.
*
* @returns Returns a promise that resolves with the versioned transaction of the stake account creation and the public key of the stake account.
*
*/
public async createAccount(
sender: string,
amountInLamports: bigint,
source: string,
// lockup: Lockup | null = Lockup.default,
params?: Params,
): Promise<ApiResponse<CreateAccountResponse>> {
// Check if the amount is greater than or equal to the minimum amount
if (amountInLamports < MIN_AMOUNT) {
this.throwError('MIN_AMOUNT_ERROR', MIN_AMOUNT.toString());
}
try {
// Get the minimum balance for rent exemption
const minimumRent = await this.connection
.getMinimumBalanceForRentExemption(
//TODO get from account when it's would be available
BigInt(STAKE_ACCOUNT_V2_SIZE),
)
.send();
// lockup = lockup || Lockup.default;
const [
createAccountInstruction,
initializeInstruction,
stakeAccountPubkey,
] =
source === null
? // TODO fix create account sign
await this.createAccountTx(
address(sender),
BigInt(amountInLamports) + minimumRent,
// lockup,
)
: await this.createAccountWithSeedTx(
address(sender),
BigInt(amountInLamports) + minimumRent,
source,
// lockup,
);
let transactionMessage = await this.baseTx(sender, params);
transactionMessage = appendTransactionMessageInstruction(
createAccountInstruction,
transactionMessage,
);
transactionMessage = appendTransactionMessageInstruction(
initializeInstruction,
transactionMessage,
);
const signedTransactionMessage =
source === null
? await partiallySignTransactionMessageWithSigners(transactionMessage)
: transactionMessage;
return {
result: {
transaction: signedTransactionMessage,
stakeAccount: stakeAccountPubkey,
},
};
} catch (error) {
throw this.handleError('CREATE_ACCOUNT_ERROR', error);
}
}
/**
* Delegates a specified amount from a stake account to a validator.
*
* @param address - The public key of the account.
* @param lamports - The amount in lamports to be delegated.
* @param stakeAccount - The public key of the stake account.
*
* @throws Throws an error if the amount is less than the minimum amount, or if there's an issue during the delegation process.
*
* @returns Returns a promise that resolves with the delegation transaction.
*
*/
public async delegate(
sender: string,
lamports: bigint,
stakeAccount: string,
params?: Params,
): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
if (lamports < MIN_AMOUNT) {
this.throwError('MIN_AMOUNT_ERROR', MIN_AMOUNT.toString());
}
try {
const delegateInstruction = getDelegateStakeInstruction({
stake: address(stakeAccount),
vote: this.validator,
stakeHistory: STAKE_HISTORY_ACCOUNT,
unused: STAKE_CONFIG_ACCOUNT,
stakeAuthority: createNoopSigner(address(sender)),
});
let transactionMessage = await this.baseTx(sender, params);
transactionMessage = appendTransactionMessageInstruction(
delegateInstruction,
transactionMessage,
);
return { result: transactionMessage };
} catch (error) {
throw this.handleError('DELEGATE_ERROR', error);
}
}
/**
* Deactivates a stake account.
*
* @param address - The public key of the account.
* @param stakeAccountPublicKey - The public key of the stake account.
* @throws Throws an error if there's an issue during the deactivation process.
* @returns Returns a promise that resolves with the deactivation transaction.
*
*/
public async deactivate(
sender: string,
stakeAccountPublicKey: string,
params?: Params,
): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
try {
const deactivateInstruction = getDeactivateInstruction({
stake: address(stakeAccountPublicKey),
stakeAuthority: createNoopSigner(address(sender)),
});
let transactionMessage = await this.baseTx(sender, params);
transactionMessage = appendTransactionMessageInstruction(
deactivateInstruction,
transactionMessage,
);
return { result: transactionMessage };
} catch (error) {
throw this.handleError('DEACTIVATE_ERROR', error);
}
}
/**
* Withdraws a specified amount from a stake account.
*
* @param address - The public key of the account.
* @param stakeAccountPublicKey - The public key of the stake account.
* @param stakeBalance - The amount in lamports to be withdrawn from the stake account.
*
* @throws Throws an error if there's an issue during the withdrawal process.
*
* @returns Returns a promise that resolves with the withdrawal transaction.
*
*/
public async withdraw(
sender: Address,
stakeAccountPublicKey: Address,
stakeBalance: bigint,
params?: Params,
): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
try {
// Create the withdraw instruction
const withdrawInstruction = getWithdrawInstruction({
stake: stakeAccountPublicKey,
recipient: sender,
stakeHistory: STAKE_HISTORY_ACCOUNT,
withdrawAuthority: createNoopSigner(address(sender)),
args: stakeBalance,
});
let transactionMessage = await this.baseTx(sender, params);
transactionMessage = appendTransactionMessageInstruction(
withdrawInstruction,
transactionMessage,
);
return { result: transactionMessage };
} catch (error) {
throw this.handleError('WITHDRAW_ERROR', error);
}
}
/**
* Fetches the delegations of a given account.
*
* @param address - The public key of the account.
*
* @throws Throws an error if there's an issue fetching the delegations.
*
* @returns Returns a promise that resolves with the delegations of the account.
*
*/
public async getDelegations(
address: string,
): Promise<ApiResponse<Delegations>> {
try {
// Fetch the accounts
const accounts = await this.connection
.getProgramAccounts(STAKE_PROGRAM_ADDRESS, {
encoding: 'base64',
filters: [
{
dataSize: FILTER_DATA_SIZE, // Token account size
},
{
memcmp: {
bytes: address,
encoding: 'base58',
offset: FILTER_OFFSET,
},
},
],
})
.send();
const acs = accounts.map((account) => {
const acc = parseBase64RpcAccount(account.pubkey, account.account);
return decodeStakeStateAccount(acc);
});
return { result: acs };
} catch (error) {
throw this.handleError('GET_DELEGATIONS_ERROR', error);
}
}
/**
* Stakes a certain amount of lamports.
*
* @param sender - The public key of the sender.
* @param lamports - The number of lamports to stake.
* @param source - stake source
* @param lockup - stake account lockup
* @returns A promise that resolves to a VersionedTransaction object.
*/
async stake(
sender: string,
lamports: bigint,
source: string,
// lockup: Lockup | null = Lockup.default,
params?: Params,
): Promise<ApiResponse<StakeResponse>> {
try {
// lockup = lockup || Lockup.default;
// Get the minimum balance for rent exemption
const minimumRent = await this.connection
.getMinimumBalanceForRentExemption(
//TODO get from account when would be added
BigInt(STAKE_ACCOUNT_V2_SIZE),
)
.send();
const [
createStakeAccountInstruction,
initializeStakeAccountInstruction,
stakeAccountPublicKey,
] =
source === null
? // TODO fix create account sign
await this.createAccountTx(
address(sender),
BigInt(lamports) + minimumRent,
// lockup,
)
: await this.createAccountWithSeedTx(
address(sender),
BigInt(lamports) + minimumRent,
source,
// lockup,
);
const delegateInstruction = getDelegateStakeInstruction({
stake: stakeAccountPublicKey,
vote: this.validator,
stakeHistory: STAKE_HISTORY_ACCOUNT,
unused: STAKE_CONFIG_ACCOUNT,
stakeAuthority: createNoopSigner(address(sender)),
});
let transactionMessage = await this.baseTx(sender, params);
transactionMessage = appendTransactionMessageInstruction(
createStakeAccountInstruction,
transactionMessage,
);
transactionMessage = appendTransactionMessageInstruction(
initializeStakeAccountInstruction,
transactionMessage,
);
transactionMessage = appendTransactionMessageInstruction(
delegateInstruction,
transactionMessage,
);
const signedTransactionMessage =
source === null
? await partiallySignTransactionMessageWithSigners(transactionMessage)
: transactionMessage;
return {
result: {
stakeTx: signedTransactionMessage,
stakeAccount: stakeAccountPublicKey,
},
};
} catch (error) {
throw this.handleError('STAKE_ERROR', error);
}
}
/**
* Create account Tx, public key and array of keypair.
*
* @param address - The public key of the account.
* @param lamports - The number of lamports to stake.
* @param lockup - The stake account lockup
*
* @throws Throws an error if there's an issue creating an account.
*
* @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
*
*/
private async createAccountTx(
authorityPublicKey: Address,
lamports: bigint,
// lockup: Lockup,
): Promise<[IInstruction, IInstruction, Address]> {
const stakeAccountKeyPair = await generateKeyPair();
const signer = await createSignerFromKeyPair(stakeAccountKeyPair);
const createAccountInstruction = getCreateAccountInstruction({
payer: createNoopSigner(authorityPublicKey),
newAccount: signer,
lamports: lamports,
// TODO get from package
space: STAKE_ACCOUNT_V2_SIZE,
programAddress: STAKE_PROGRAM_ADDRESS,
});
const initializeInstruction = getInitializeInstruction(
/** Uninitialized stake account */
{
stake: signer.address,
arg0: {
staker: authorityPublicKey,
withdrawer: authorityPublicKey,
},
arg1: {
//TODO use default
unixTimestamp: 0,
epoch: 0,
custodian: ADDRESS_DEFAULT,
},
},
);
return [createAccountInstruction, initializeInstruction, signer.address];
}
/**
* Create account Tx, public key and array of keypair using seed.
*
* @param authorityPublicKey - The public key of the account.
* @param lamports - The number of lamports to stake.
* @param source - The stake source
* @param lockup - The stake account lockup
*
* @throws Throws an error if there's an issue creating an account.
*
* @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
*
*/
private async createAccountWithSeedTx(
authorityPublicKey: Address,
lamports: bigint,
source: string,
// lockup: Lockup,
): Promise<[IInstruction, IInstruction, Address]> {
// Format source to
const seed = this.formatSource(source || '');
const stakeAccountPubkey = await createAddressWithSeed({
baseAddress: authorityPublicKey,
programAddress: STAKE_PROGRAM_ADDRESS,
seed: seed,
});
const createAccountInstruction = getCreateAccountWithSeedInstruction({
payer: createNoopSigner(authorityPublicKey),
newAccount: stakeAccountPubkey,
baseAccount: createNoopSigner(authorityPublicKey),
base: address(authorityPublicKey),
seed: seed,
amount: lamports,
// TODO get from package
space: STAKE_ACCOUNT_V2_SIZE,
programAddress: STAKE_PROGRAM_ADDRESS,
});
const initializeInstruction = getInitializeInstruction(
/** Uninitialized stake account */
{
stake: stakeAccountPubkey,
arg0: {
staker: authorityPublicKey,
withdrawer: authorityPublicKey,
},
arg1: {
//TODO implement Lockup
unixTimestamp: 0,
epoch: 0,
custodian: ADDRESS_DEFAULT,
},
},
);
return [
createAccountInstruction,
initializeInstruction,
stakeAccountPubkey,
];
}
/** unstake - unstake
* @param {string} sender - account blockchain address (staker)
* @param {bigint} lamports - lamport amount
* @param {string} source - stake source
* @returns {Promise<object>} Promise object with Versioned Tx
*/
public async unstake(
sender: string,
lamports: bigint,
source: string,
params?: Params,
): Promise<ApiResponse<UnstakeResponse>> {
try {
const stakeAccounts = (await this.getDelegations(sender)).result;
const epoch =
params?.epoch || (await this.connection.getEpochInfo().send()).epoch;
const tm = this.timestampInSec();
let unstakeAmount = lamports;
let totalActiveStake: bigint = 0n;
const activeStakeAccounts = stakeAccounts.filter((acc) => {
if (acc.data.state.__kind !== 'Stake') {
return false;
}
const isActive = !(
isLockupInForce(acc.data, epoch, BigInt(tm)) ||
stakeAccountState(acc.data, epoch) !== StakeState.Active
);
if (isActive) {
totalActiveStake =
totalActiveStake + acc.data.state.fields[1].delegation.stake;
}
return isActive;
});
if (totalActiveStake < lamports)
throw this.throwError('NOT_ENOUGH_ACTIVE_STAKE_ERROR');
// ASC sort if num of accounts less than threshold otherwise DESC sorting
activeStakeAccounts.sort((a, b): number => {
const stakeA = isStake(a.data.state)
? a.data.state.fields[1].delegation.stake
: 0n;
const stakeB = isStake(b.data.state)
? b.data.state.fields[1].delegation.stake
: 0n;
if (activeStakeAccounts.length < MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT) {
return Number(stakeA - stakeB);
}
return Number(stakeB - stakeA);
});
const accountsToDeactivate: Delegations = [];
const accountsToSplit: [Account<StakeStateAccount, Address>, bigint][] =
[];
let i = 0;
while (lamports > 0n && i < activeStakeAccounts.length) {
const acc = activeStakeAccounts[i];
if (acc === undefined || !isStake(acc.data.state)) {
i++;
continue;
}
const stakeAmount = acc.data.state.fields[1].delegation.stake;
// If reminder amount less than min stake amount stake account automatically become disabled
const isBelowThreshold =
stakeAmount <= lamports || stakeAmount - lamports < MIN_AMOUNT;
if (isBelowThreshold) {
accountsToDeactivate.push(acc);
lamports = lamports - stakeAmount;
i++;
// Max num of deactivate instructions reached
if (accountsToDeactivate.length === MAX_DEACTIVATE_ACCOUNTS) {
unstakeAmount -= lamports;
break;
}
continue;
}
// Max num of deactivate instructions with split reached
if (accountsToDeactivate.length > MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT) {
unstakeAmount -= lamports;
break;
}
accountsToSplit.push([acc, lamports]);
break;
}
const senderPublicKey = address(sender);
let transactionMessage = await this.baseTx(sender, params);
// Get the minimum balance for rent exemption. Send request only if split required
const minimumRent =
accountsToSplit.length > 0
? await this.connection
.getMinimumBalanceForRentExemption(
//TODO get from account when it's would be available
BigInt(STAKE_ACCOUNT_V2_SIZE),
)
.send()
: 0n;
for (const acc of accountsToSplit) {
const [splitInstructions, newStakeAccountPubkey] = await this.split(
senderPublicKey,
acc[1],
acc[0].address,
source,
// Need additional value for rent
minimumRent,
);
splitInstructions.forEach(
(splitInstruction) =>
(transactionMessage = appendTransactionMessageInstruction(
splitInstruction,
transactionMessage,
)),
);
const deactivateInstruction = getDeactivateInstruction({
stake: newStakeAccountPubkey,
stakeAuthority: createNoopSigner(address(sender)),
});
transactionMessage = appendTransactionMessageInstruction(
deactivateInstruction,
transactionMessage,
);
}
accountsToDeactivate.forEach((acc) => {
const deactivateInstruction = getDeactivateInstruction({
stake: acc.address,
stakeAuthority: createNoopSigner(address(sender)),
});
transactionMessage = appendTransactionMessageInstruction(
deactivateInstruction,
transactionMessage,
);
});
if (transactionMessage.instructions.length === 0) {
this.handleError('UNSTAKE_ERROR', 'zero instructions');
}
return {
result: { unstakeTx: transactionMessage, unstakeAmount: unstakeAmount },
};
} catch (error) {
throw this.handleError('UNSTAKE_ERROR', error);
}
}
/**
* Split existing account to create a new one
*
* @param authorityPublicKey - The public key of the account.
* @param lamports - The number of lamports to stake.
* @param oldStakeAccountPubkey -The public key of the old account.
* @param source - The stake source
*
* @throws Throws an error if there's an issue splitting an account.
*
* @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
*
*/
private async split(
authorityPublicKey: Address,
lamports: bigint,
oldStakeAccountPubkey: Address,
source: string,
rentExemptReserve?: bigint,
): Promise<[Array<IInstruction>, Address]> {
// Format source to
const seed = this.formatSource(source);
const newStakeAccountPubkey = await createAddressWithSeed({
baseAddress: authorityPublicKey,
programAddress: STAKE_PROGRAM_ADDRESS,
seed,
});
const instructions: Array<IInstruction> = [];
// TODO add support split w\o seed
const allocateWithSeedInstruction = getAllocateWithSeedInstruction({
newAccount: newStakeAccountPubkey,
baseAccount: createNoopSigner(address(authorityPublicKey)),
base: authorityPublicKey,
seed: seed,
//TODO get from library if possible
space: STAKE_ACCOUNT_V2_SIZE,
programAddress: STAKE_PROGRAM_ADDRESS,
});
instructions.push(allocateWithSeedInstruction);
//If creates new account need to top up balance by rent amount
if (rentExemptReserve && rentExemptReserve > 0) {
const rentTransferInstruction = getTransferSolInstruction({
source: createNoopSigner(authorityPublicKey),
destination: newStakeAccountPubkey,
amount: rentExemptReserve,
});
instructions.push(rentTransferInstruction);
}
const splitInstruction = getSplitInstruction({
stake: oldStakeAccountPubkey,
splitStake: newStakeAccountPubkey,
stakeAuthority: createNoopSigner(authorityPublicKey),
args: lamports,
});
instructions.push(splitInstruction);
return [instructions, newStakeAccountPubkey];
}
/**
* Claim makes withdrawal from all sender's deactivated accounts.
*
* @param sender - The sender solana address.
*
* @throws Throws an error if there's an issue while claiming a stake.
*
* @returns Returns a promise that resolves with a Versioned Transaction.
*
*/
public async claim(
sender: string,
params?: Params,
): Promise<ApiResponse<ClaimResponse>> {
try {
const delegations = await this.getDelegations(sender);
const epoch =
params?.epoch || (await this.connection.getEpochInfo().send()).epoch;
const tm = this.timestampInSec();
const deactivatedStakeAccounts = delegations.result.filter((acc) => {
return (
!isLockupInForce(acc.data, epoch, BigInt(tm)) &&
stakeAccountState(acc.data, epoch) === StakeState.Deactivated
);
});
if (deactivatedStakeAccounts.length === 0)
throw this.throwError('NOTHING_TO_CLAIM_ERROR');
let transactionMessage = await this.baseTx(sender, params);
let totalClaimableStake = 0n;
let accountsForClaim = 0;
for (const acc of deactivatedStakeAccounts) {
// Create the withdraw instruction
const withdrawInstruction = getWithdrawInstruction({
stake: acc.address,
recipient: address(sender),
stakeHistory: STAKE_HISTORY_ACCOUNT,
withdrawAuthority: createNoopSigner(address(sender)),
args: acc.lamports,
});
transactionMessage = appendTransactionMessageInstruction(
withdrawInstruction,
transactionMessage,
);
totalClaimableStake += acc.lamports;
accountsForClaim++;
if (accountsForClaim === MAX_CLAIM_ACCOUNTS) {
break;
}
}
return {
result: {
claimTx: transactionMessage,
totalClaimAmount: totalClaimableStake,
},
};
} catch (error) {
throw this.handleError('CLAIM_ERROR', error);
}
}
/**
* Merge two accounts into a new one
*
* @param authorityPublicKey - The public key of the account.
* @param stakeAccount1 - The public key of the first account.
* @param stakeAccount2 - The public key of the second account.
*
* @throws Throws an error if there's an issue while merging an account.
*
* @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
*
*/
// private async merge(
// authorityPublicKey: PublicKey,
// stakeAccount1: PublicKey,
// stakeAccount2: PublicKey,
// ) {
// const mergeStakeAccountTx = StakeProgram.merge({
// stakePubkey: stakeAccount1,
// sourceStakePubKey: stakeAccount2,
// authorizedPubkey: authorityPublicKey,
// });
// return [mergeStakeAccountTx];
// }
private async baseTx(
sender: string,
params?: Params,
): Promise<
CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime
> {
const finalLatestBlockhash =
params?.finalLatestBlockhash ||
(await this.connection.getLatestBlockhash().send()).value;
let transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(address(sender), tx),
(tx) =>
setTransactionMessageLifetimeUsingBlockhash(finalLatestBlockhash, tx),
);
if (
params?.computeUnitLimit !== undefined &&
params?.computeUnitLimit > 0
) {
const unitLimitInstruction = getSetComputeUnitLimitInstruction({
/** Transaction compute unit limit used for prioritization fees. */
units: params?.computeUnitLimit,
});
transactionMessage = prependTransactionMessageInstruction(
unitLimitInstruction,
transactionMessage,
);
}
if (
params?.сomputeUnitPrice !== undefined &&
params?.сomputeUnitPrice > 0
) {
const unitPriceInstruction = getSetComputeUnitPriceInstruction({
/** Transaction compute unit price used for prioritization fees. */
microLamports: params?.сomputeUnitPrice,
});
transactionMessage = prependTransactionMessageInstruction(
unitPriceInstruction,
transactionMessage,
);
}
return transactionMessage;
}
/**
* Generate a unique source for crating an account.
*
* @param source - source ID.
*
* @returns Returns a unique source for an account.
*
*/
private formatSource(source: string): string {
const timestamp = new Date().getTime();
source = `everstake ${source}:${timestamp}`;
return source;
}
/**
* Generate timestamp in seconds.
*
* @returns Returns a timestamp in seconds.
*
*/
private timestampInSec(): number {
return (Date.now() / 1000) | 0;
}
}
//TODO think about export of this methods
/**
* Determins the current state of a stake account given the current epoch
* @param currentEpoch
* @returns `stakeAccount`'s stake state`string`
*/
export function stakeAccountState(
account: StakeStateAccount,
currentEpoch: bigint,
): string {
//TODO check
if (account.state.__kind !== 'Stake') {
return StakeState.Inactive;
}
const activationEpoch = account.state.fields[1].delegation.activationEpoch;
const deactivationEpoch =
account.state.fields[1].delegation.deactivationEpoch;
if (activationEpoch > currentEpoch) {
return StakeState.Inactive;
}
if (activationEpoch === currentEpoch) {
// if you activate then deactivate in the same epoch,
// deactivationEpoch === activationEpoch.
// if you deactivate then activate again in the same epoch,
// the deactivationEpoch will be reset to EPOCH_MAX
if (deactivationEpoch === activationEpoch) return StakeState.Inactive;
return StakeState.Activating;
}
// activationEpoch < currentEpochBN
if (deactivationEpoch > currentEpoch) return StakeState.Active;
if (deactivationEpoch === currentEpoch) return StakeState.Deactivating;
return StakeState.Deactivated;
}
/**
* Check if lockup is in force
* @param currEpoch current epoch.
* @param currUnixTimestamp current unix timetamp.
* @returns a bool type result.
*/
export function isLockupInForce(
account: StakeStateAccount,
currEpoch: bigint,
currUnixTimestamp: bigint,
): boolean {
if (
account.state.__kind !== 'Stake' &&
account.state.__kind !== 'Initialized'
) {
return false;
}
const { unixTimestamp, epoch } = account.state.fields[0].lockup;
return unixTimestamp > currUnixTimestamp || epoch > currEpoch;
}
export function isStake(
state: StakeStateV2,
): state is Extract<StakeStateV2, { __kind: 'Stake' }> {
return state.__kind === 'Stake';
}