@everstake/wallet-sdk-ethereum
Version:
Ethereum - Everstake Wallet SDK
1,039 lines (944 loc) • 33.8 kB
text/typescript
/**
* Copyright (c) 2025, Everstake.
* Licensed under the BSD-3-Clause License. See LICENSE file for details.
*/
import Web3 from 'web3';
import BigNumber from 'bignumber.js';
import { Multicall, ContractCallContext } from 'ethereum-multicall';
import { HttpProvider } from 'web3';
import type { Web3BaseProvider, Contract } from 'web3';
import {
ETH_NETWORK_ADDRESSES,
ETH_GAS_RESERVE,
ETH_MIN_AMOUNT,
UINT16_MAX,
MULTICALL_CONTRACT_ADDRESS,
} from './constants';
import { ABI_CONTRACT_ACCOUNTING, ABI_CONTRACT_POOL } from './abi';
import { ERROR_MESSAGES, ORIGINAL_ERROR_MESSAGES } from './constants/errors';
import type {
EthNetworkType,
EthTransaction,
AggregatedBalances,
} from './types';
import { ValidatorStatus } from './types';
import { Blockchain } from '../../utils';
/**
* The `Ethereum` class extends the `Blockchain` class and provides methods for interacting with the Ethereum network.
*
* It allows you to select a network, initialize it, and retrieve the balance of the contract.
*
* @property {provider} rpcUrl - The RPC URL of the Ethereum network.
* @property {Web3} web3 - The Web3 instance used for interacting with the Ethereum network.
* @property {string} addressContractAccounting - The address of the accounting contract.
* @property {string} addressContractPool - The address of the pool contract.
* @property {string} addressContractWithdrawTreasury - The address of the withdraw treasury contract.
* @property {Contract} contractAccounting - The accounting contract instance.
* @property {Contract} contractPool - The pool contract instance.
* @property ERROR_MESSAGES - The error messages for the Ethereum class.
* @property ORIGINAL_ERROR_MESSAGES - The original error messages for the Ethereum class.
*
*/
export class Ethereum extends Blockchain {
public addressContractAccounting!: string;
public addressContractPool!: string;
public addressContractWithdrawTreasury!: string;
public contractAccounting!: Contract<typeof ABI_CONTRACT_ACCOUNTING>;
public contractPool!: Contract<typeof ABI_CONTRACT_POOL>;
private rpcUrl!: Web3BaseProvider;
private web3!: Web3;
private minAmount = new BigNumber(ETH_MIN_AMOUNT);
protected ERROR_MESSAGES = ERROR_MESSAGES;
protected ORIGINAL_ERROR_MESSAGES = ORIGINAL_ERROR_MESSAGES;
constructor(network: EthNetworkType = 'mainnet', url?: string) {
super();
this.initializeNetwork(network, url);
}
/**
* Retrieves the balance of the contract.
*
* This method calls the `balance` method on the `contractAccounting` contract,
* converts the result from Wei to Ether, and returns the result as a `BigNumber`.
*
* @returns A promise that resolves to the balance of the contract as a `BigNumber`.
*
* @throws Will throw an error if the contract call fails.
*/
public async balance(): Promise<BigNumber> {
try {
const result = await this.contractAccounting.methods.balance().call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('BALANCE_ERROR', error);
}
}
/**
* Fetches the pool pending balance. This balance is always less than 32 ETH.
*
* @returns A Promise that resolves to the pending balance in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async pendingBalance(): Promise<BigNumber> {
try {
const result = await this.contractAccounting.methods
.pendingBalance()
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_BALANCE_ERROR', error);
}
}
/**
* Fetches the pool pending deposited balance. This is the balance deposited into the Beacon deposit contract but validators are still not active.
*
* @returns A Promise that resolves to the pending deposited balance in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async pendingDepositedBalance(): Promise<BigNumber> {
try {
const result = await this.contractAccounting.methods
.pendingDepositedBalance()
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_DEPOSITED_BALANCE_ERROR', error);
}
}
/**
* Fetches the pool restaked rewards which are in pending status.
*
* @returns A Promise that resolves to the pending restaked rewards amount in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async pendingRestakedRewards(): Promise<BigNumber> {
try {
const result = await this.contractAccounting.methods
.pendingRestakedRewards()
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_RESTAKED_REWARDS_ERROR', error);
}
}
/**
* Fetches the pool unclaimed rewards amount which can be restaked.
*
* @returns A Promise that resolves to the unclaimed rewards amount in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async readyforAutocompoundRewardsAmount(): Promise<BigNumber> {
try {
const result = await this.contractAccounting.methods
.readyforAutocompoundRewardsAmount()
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError(
'READY_FOR_AUTOCOMPOUND_REWARDS_AMOUNT_ERROR',
error,
);
}
}
/**
* Fetches the pending balance of a given address.
*
* @param address - The address to fetch the pending balance for.
*
* @returns A Promise that resolves to the pending balance in ether.
*
* @throws Will throw an Error if the contract call fails or address is not valid.
*/
public async pendingBalanceOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.pendingBalanceOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_BALANCE_OF_ERROR', error);
}
}
/**
* Fetches the user's pending deposited balance. This is the balance deposited into the validator but not active yet.
* Pending deposited balance can't be unstaked till validator activation.
*
* @param address - The address to fetch the pending deposited balance for.
*
* @returns A Promise that resolves to the pending deposited balance in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async pendingDepositedBalanceOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.pendingDepositedBalanceOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_DEPOSITED_BALANCE_OF_ERROR', error);
}
}
/**
* Fetches the user's active origin deposited balance.
*
* @param address - The address to fetch the deposited balance for.
*
* @returns A Promise that resolves to the deposited balance in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async depositedBalanceOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.depositedBalanceOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('DEPOSITED_BALANCE_OF_ERROR', error);
}
}
/**
* Fetches the user's restaked rewards in pending state.
*
* @param address - The address to fetch the pending restaked rewards for.
*
* @returns A Promise that resolves to the pending restaked rewards in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async pendingRestakedRewardOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.pendingRestakedRewardOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('PENDING_RESTAKED_REWARD_OF_ERROR', error);
}
}
/**
* Returns total user restaked rewards. Includes rewards in pending state.
*
* @param address - The address to fetch the restaked rewards for.
*
* @returns A Promise that resolves to the restaked rewards in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async restakedRewardOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.restakedRewardOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('RESTAKED_REWARD_OF_ERROR', error);
}
}
/**
* Fetches the pool fee in bips (1/10000).
*
* @returns A Promise that resolves to the pool fee.
*
* @throws Will throw an Error if the contract call fails.
*/
public async getPoolFee(): Promise<BigNumber> {
try {
const poolFee = await this.contractAccounting.methods.getPoolFee().call();
const result = poolFee.toString();
return new BigNumber(result).div(10000);
} catch (error) {
throw this.handleError('GET_POOL_FEE_ERROR', error);
}
}
/**
* Claims all pool rewards and restakes it into the pool.
*
* @param address - The address to perform the autocompound operation for.
*
* @returns A Promise that resolves to a transaction object.
*
* @throws Will throw an Error if the contract call fails or there are no rewards.
*/
public async autocompound(address: string): Promise<EthTransaction> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const rewards = await this.readyforAutocompoundRewardsAmount();
if (rewards.isZero()) this.throwError('NO_REWARDS_MESSAGE');
const gasConsumption = await this.contractAccounting.methods
.autocompound()
.estimateGas({ from: address });
return {
from: address,
to: this.addressContractAccounting,
value: 0,
gasLimit: this.calculateGasLimit(gasConsumption),
data: this.contractAccounting.methods.autocompound().encodeABI(),
};
} catch (error) {
throw this.handleError('AUTOCOMPOUND_ERROR', error);
}
}
/**
* Returns total user autocompound balance. Part of this balance could be in pending state after rewards autocompound.
*
* @param address - The address to fetch the autocompound balance for.
*
* @returns A Promise that resolves to the autocompound balance in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async autocompoundBalanceOf(address: string): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.autocompoundBalanceOf(address)
.call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('AUTOCOMPOUND_BALANCE_OF_ERROR', error);
}
}
/**
* Returns info about withdraw requests queue.
* Includes totally all-time requested withdraw amount,
* actual allowed for interchange with deposits amount,
* all-time withdraw treasury filled amount,
* and all-time claimed by users amount.
*
* @returns A Promise that resolves to an object containing the withdraw request queue parameters.
*
* @throws Will throw an Error if the contract call fails.
*/
public async withdrawRequestQueueParams(): Promise<{
withdrawRequested: BigNumber;
interchangeAllowed: BigNumber;
filled: BigNumber;
claimed: BigNumber;
}> {
try {
const result = await this.contractAccounting.methods
.withdrawRequestQueueParams()
.call();
return {
// Totally all-time requested withdraw amount.
withdrawRequested: this.fromWeiToEther(result[0]),
// Actual allowed for interchange with deposits amount.
interchangeAllowed: this.fromWeiToEther(result[1]),
// All-time withdraw treasury filled amount.
filled: this.fromWeiToEther(result[2]),
// All-time claimed by users amount
claimed: this.fromWeiToEther(result[3]),
};
} catch (error) {
throw this.handleError('WITHDRAW_REQUEST_QUEUE_PARAMS_ERROR', error);
}
}
/**
* Returns user withdraw request info. Includes the actual requested amount and the amount ready for claim.
*
* @param address - The address to fetch the withdraw request info for.
*
* @returns A Promise that resolves to an object containing the requested amount and the amount ready for claim in ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async withdrawRequest(
address: string,
): Promise<{ requested: BigNumber; readyForClaim: BigNumber }> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const result = await this.contractAccounting.methods
.withdrawRequest(address)
.call();
return {
requested: this.fromWeiToEther(result[0]),
readyForClaim: this.fromWeiToEther(result[1]),
};
} catch (error) {
throw this.handleError('WITHDRAW_REQUEST_ERROR', error);
}
}
/**
* Returns aggregated pool balances using multicall contract.
*
* @returns A Promise that resolves to a AggregatedBalances object.
*
* @throws Will throw an Error if the contract call fails.
*/
public async poolBalances(): Promise<AggregatedBalances> {
const methods: string[] = [
'balance',
'pendingBalance',
'pendingDepositedBalance',
'pendingRestakedRewards',
'readyforAutocompoundRewardsAmount',
];
const result: AggregatedBalances = {};
try {
const multicall = new Multicall({
multicallCustomContractAddress: MULTICALL_CONTRACT_ADDRESS,
web3Instance: this.web3,
tryAggregate: true,
});
const contractCallContext: ContractCallContext[] = [];
methods.forEach((method) =>
contractCallContext.push({
reference: method,
contractAddress: this.addressContractAccounting,
/* eslint-disable @typescript-eslint/no-explicit-any */
abi: ABI_CONTRACT_ACCOUNTING as unknown as any[], // TODO check it
calls: [
{ reference: method, methodName: method, methodParameters: [] },
],
}),
);
const results = await multicall.call(contractCallContext);
for (const [key, value] of Object.entries(results.results)) {
result[key] = this.web3.utils.fromWei(
value.callsReturnContext[0]?.returnValues[0].hex,
'ether',
);
}
} catch (error) {
throw this.handleError('POOL_BALANCES_ERROR', error);
}
return result;
}
/**
* Returns aggregated user balances using multicall contract.
*
* @param address - The user address.
*
* @returns A Promise that resolves to a AggregatedBalances object.
*
* @throws Will throw an Error if the contract call fails.
*/
public async userBalances(address: string): Promise<AggregatedBalances> {
const methods = [
'pendingBalanceOf',
'pendingDepositedBalanceOf',
'pendingRestakedRewardOf',
'autocompoundBalanceOf',
'depositedBalanceOf',
];
const result: AggregatedBalances = {};
try {
const multicall = new Multicall({
multicallCustomContractAddress: MULTICALL_CONTRACT_ADDRESS,
web3Instance: this.web3,
tryAggregate: true,
});
const contractCallContext: ContractCallContext[] = [];
methods.forEach((method) =>
contractCallContext.push({
reference: method,
contractAddress: this.addressContractAccounting,
/* eslint-disable @typescript-eslint/no-explicit-any */
abi: ABI_CONTRACT_ACCOUNTING as unknown as any[], // TODO check it
calls: [
{
reference: method,
methodName: method,
methodParameters: [address],
},
],
}),
);
const results = await multicall.call(contractCallContext);
for (const [key, value] of Object.entries(results.results)) {
result[key] = this.web3.utils.fromWei(
value.callsReturnContext[0]?.returnValues[0].hex,
'ether',
);
}
} catch (error) {
throw this.handleError('USER_BALANCES_ERROR', error);
}
return result;
}
/**
* Claims funds requested by withdraw.
*
* @param address - The address to perform the claim operation for.
*
* @returns A Promise that resolves to a transaction object.
*
* @throws Will throw an Error if the contract call fails, there are no funds to claim, or the claim is not yet filled.
*/
public async claimWithdrawRequest(address: string): Promise<EthTransaction> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const rewards = await this.withdrawRequest(address);
if (rewards.requested.isZero()) {
this.throwError('ZERO_UNSTAKE_ERROR');
}
if (!rewards.readyForClaim.eq(rewards.requested)) {
this.throwError('NOT_FILLED_UNSTAKE_MESSAGE');
}
const gasConsumption = await this.contractAccounting.methods
.claimWithdrawRequest()
.estimateGas({ from: address });
return {
from: address,
to: this.addressContractAccounting,
value: 0,
gasLimit: this.calculateGasLimit(gasConsumption),
data: this.contractAccounting.methods
.claimWithdrawRequest()
.encodeABI(),
};
} catch (error) {
throw this.handleError('CLAIM_WITHDRAW_REQUEST_ERROR', error);
}
}
/**
* Returns the number of validators expected to stop.
*
* @returns A Promise that resolves to the number of validators expected to stop.
*
* @throws Will throw an Error if the contract call fails.
*/
public async closeValidatorsStat(): Promise<number> {
try {
const result = await this.contractAccounting.methods
.closeValidatorsStat()
.call();
return Number(result);
} catch (error) {
throw this.handleError('CLOSE_VALIDATORS_STAT_ERROR', error);
}
}
/**
* Stakes funds into pool.
*
* @param address - Sender address.
* @param amount - Stake amount in ETH
* @param source - Stake source. Default is '0'.
*
* @returns A Promise that resolves to the unsigned ETH transaction object.
*
* @throws Will throw an Error if the amount is not a valid, the amount is less than the minimum, or the contract call fails.
*/
public async stake(
address: string,
amount: string,
source: string = '0',
): Promise<EthTransaction> {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
if (typeof amount !== 'string') {
this.throwError('WRONG_TYPE_MESSAGE');
}
const amountWei = this.web3.utils.toWei(amount, 'ether');
if (new BigNumber(amountWei).lt(this.minAmount)) {
this.throwError('MIN_AMOUNT_ERROR', this.minAmount.toString());
}
try {
const gasConsumption = await this.contractPool.methods
.stake(source)
.estimateGas({ from: address, value: amountWei });
// Create the transaction
return {
from: address,
to: this.addressContractPool,
value: Number(amount),
gasLimit: this.calculateGasLimit(gasConsumption),
data: this.contractPool.methods.stake(source).encodeABI(),
};
} catch (error) {
throw this.handleError('STAKE_ERROR', error);
}
}
/**
* Unstake value from active autocompound balance.
* AllowedInterchangeNum is max allowed number interchanges with pending stakers.
* Unstaked immediately if value <= pool pending balance or create withdraw request.
* Interchange disallowed as default.
*
* @param address - Sender address.
* @param amount - Unstake amount in ETH.
* @param allowedInterchangeNum - Max allowed number of interchanges. Default is 0.
* @param source - Unstake source. Default is '0'.
*
* @returns A Promise that resolves to the unsigned ETH transaction object.
*
* @throws Will throw an Error if the amount is not a string, the balance is less than the amount, or the contract call fails.
*/
public async unstake(
address: string,
amount: string,
allowedInterchangeNum: number = 0,
source: string = '0',
): Promise<EthTransaction> {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
if (typeof amount !== 'string') {
this.throwError('WRONG_TYPE_MESSAGE');
}
try {
const balance = await this.autocompoundBalanceOf(address);
// Check for type overflow
if (allowedInterchangeNum > UINT16_MAX) {
allowedInterchangeNum = UINT16_MAX;
}
if (balance.lt(new BigNumber(amount))) {
this.throwError('MAX_AMOUNT_FOR_UNSTAKE_ERROR', balance.toString());
}
const amountWei = this.web3.utils.toWei(amount, 'ether');
const gasConsumption = await this.contractPool.methods
.unstake(amountWei, allowedInterchangeNum, source)
.estimateGas({ from: address });
// Create the transaction
return {
from: address,
value: 0,
to: this.addressContractPool,
gasLimit: this.calculateGasLimit(gasConsumption),
data: this.contractPool.methods
.unstake(amountWei, allowedInterchangeNum, source)
.encodeABI(),
};
} catch (error) {
throw this.handleError('UNSTAKE_ERROR', error);
}
}
/**
* Simulate unstake transaction and return amount of instant unstake.
* Required to compare evaluation of allowedInterchangeNum parameter.
*
* @param address - Sender address.
* @param amount - Unstake amount in ETH.
* @param allowedInterchangeNum - Max allowed number of interchanges. Default is 1.
* @param source - Unstake source. Default is '0'.
*
* @returns A Promise that resolves to a BigNumber representing the instant unstake amount in ETH.
*
* @throws Will throw an Error if the balance is less than the amount or the contract call fails.
*/
public async simulateUnstake(
address: string,
amount: string,
allowedInterchangeNum: number = 1,
source: string = '0',
): Promise<BigNumber> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const balance = await this.autocompoundBalanceOf(address);
// Check for type overflow
if (allowedInterchangeNum > UINT16_MAX) {
allowedInterchangeNum = UINT16_MAX;
}
// Balance greater than or equal to amount
if (balance.lt(amount)) {
this.throwError('MAX_AMOUNT_FOR_UNSTAKE_ERROR', balance.toString());
}
const amountWei = this.web3.utils.toWei(amount, 'ether');
const result = await this.contractPool.methods
.unstake(amountWei, allowedInterchangeNum, source)
.call({ from: address });
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('SIMULATE_UNSTAKE_ERROR', error);
}
}
/**
* Unstakes the pending amount from Autocompound.
*
* @param address - The address from which the amount will be unstaked.
* @param amount - The amount to unstake.
*
* @returns A Promise that resolves to a transaction object.
*
* @throws Will throw an Error if the pending balance is zero, the amount is greater than the pending balance,
* or the pending balance is less than the minimum stake amount.
*/
public async unstakePending(
address: string,
amount: number,
): Promise<EthTransaction> {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
let pendingBalance = await this.pendingBalanceOf(address);
if (pendingBalance.isZero()) {
this.throwError('ZERO_UNSTAKE_MESSAGE');
}
const bnAmount = new BigNumber(amount);
if (bnAmount.gt(pendingBalance)) {
this.throwError(
'AMOUNT_GREATER_THAN_PENDING_BALANCE_ERROR',
pendingBalance.toString(),
);
}
try {
pendingBalance = pendingBalance.minus(bnAmount);
if (!pendingBalance.isZero()) {
const minStake = await this.minStakeAmount();
if (pendingBalance.lt(minStake)) {
this.throwError(
'INSUFFICIENT_PENDING_BALANCE_ERROR',
minStake.toString(),
);
}
}
const amountWei = this.web3.utils.toWei(amount.toString(), 'ether');
const gasConsumption = await this.contractPool.methods
.unstakePending(amountWei)
.estimateGas({ from: address });
// Create the transaction
return {
from: address,
value: 0,
to: this.addressContractPool,
gasLimit: this.calculateGasLimit(gasConsumption),
data: this.contractPool.methods.unstakePending(amountWei).encodeABI(),
};
} catch (error) {
throw this.handleError('UNSTAKE_PENDING_ERROR', error);
}
}
/**
* Activates pending stake by interchange with withdraw request.
*
* @param address - The address from which the stake will be activated.
*
* @returns A Promise that resolves to a transaction object.
*
* @throws Will throw an Error if the contract call fails.
*/
public async activateStake(address: string): Promise<EthTransaction> {
try {
if (!this.isAddress(address)) {
this.throwError('ADDRESS_FORMAT_ERROR');
}
const gasAmount = await this.contractPool.methods
.activateStake()
.estimateGas({ from: address });
// Create the transaction
return {
from: address,
to: this.addressContractPool,
value: 0,
gasLimit: this.calculateGasLimit(gasAmount),
data: this.contractPool.methods.activateStake().encodeABI(),
};
} catch (error) {
throw this.handleError('ACTIVATE_STAKE_ERROR', error);
}
}
/**
* Fetches the number of validators prepared for deposit from the contract pool.
*
* @returns A Promise that resolves to the number of validators prepared for deposit.
*
* @throws Will throw an Error if the contract call fails.
*/
public async getPendingValidatorCount(): Promise<number> {
try {
const result = await this.contractPool.methods
.getPendingValidatorCount()
.call();
return Number(result);
} catch (error) {
throw this.handleError('GET_PENDING_VALIDATOR_COUNT_ERROR', error);
}
}
/**
* Fetches a pending validator's public key by index from the contract pool.
* Note: The list of pending validators is dynamic so ordering is unstable.
*
* @param index - The index of the pending validator to fetch.
*
* @returns A Promise that resolves to the public key of the pending validator.
*
* @throws Will throw an Error if the contract call fails.
*/
public async getPendingValidator(index: number): Promise<string> {
try {
const result = await this.contractPool.methods
.getPendingValidator(index)
.call();
return result.toString();
} catch (error) {
throw this.handleError('GET_PENDING_VALIDATOR_ERROR', error);
}
}
/**
* Fetches the total number of known validators from the contract pool.
* A validator can be in one of the following statuses: pending, deposited, exited.
* Exited validators will be overwritten by new pending validators to optimize memory usage.
*
* @returns A Promise that resolves to the total number of known validators.
*
* @throws Will throw an Error if the contract call fails.
*/
public async getValidatorCount(): Promise<number> {
try {
const result = await this.contractPool.methods.getValidatorCount().call();
return Number(result);
} catch (error) {
throw this.handleError('GET_VALIDATOR_COUNT_ERROR', error);
}
}
/**
* Fetches a validator from the contract pool by its index.
* The result is an object containing the validator's public key and status.
*
* @param index - The index of the validator to fetch.
*
* @returns A Promise that resolves to an object with the validator's public key and status.
*
* @throws Will throw an Error if the contract call fails.
*/
public async getValidator(
index: number,
): Promise<{ pubkey: string; status: string }> {
try {
const result = await this.contractPool.methods.getValidator(index).call();
return {
pubkey: result[0].toString(),
status: this.getStatusFromCode(Number(result[1])),
};
} catch (error) {
throw this.handleError('GET_VALIDATOR_ERROR', error);
}
}
/**
* Fetches the minimum stake amount from the contract pool.
* The result is converted from Wei to Ether using Web3 utilities.
*
* @returns A Promise that resolves to a BigNumber representing the minimum stake amount in Ether.
*
* @throws Will throw an Error if the contract call fails.
*/
public async minStakeAmount(): Promise<BigNumber> {
try {
const result = await this.contractPool.methods.minStakeAmount().call();
return this.fromWeiToEther(result);
} catch (error) {
throw this.handleError('MIN_STAKE_AMOUNT_ERROR', error);
}
}
/**
* Selects and initializes a new network.
*
* This method calls `initializeNetwork` with the provided parameters and returns the current instance,
* allowing for method chaining.
*
* @param network - The network type. This should be one of the keys in `NETWORK_ADDRESSES`.
* @param url - The RPC URL of the network. If not provided, the method will use the URL from `NETWORK_ADDRESSES`.
*
* @returns The current instance of the `Ethereum` class.
*/
public selectNetwork(network: EthNetworkType, url?: string): Ethereum {
this.initializeNetwork(network, url);
return this;
}
/**
* Initializes the network.
*
* This method sets the RPC URL, contract addresses, and initializes the Web3 instance and contracts.
*
* @param network - The network type. This should be one of the keys in `NETWORK_ADDRESSES`.
* @param url - The RPC URL of the network. If not provided, the method will use the URL from `NETWORK_ADDRESSES`.
*
* @throws Will throw an error if the provided network is not supported (i.e., not a key in `NETWORK_ADDRESSES`).
*/
private initializeNetwork(network: EthNetworkType, url?: string): void {
const networkAddresses = ETH_NETWORK_ADDRESSES[network];
if (!networkAddresses) {
this.throwError('NETWORK_NOT_SUPPORTED', network);
}
const providerUrl = url ?? networkAddresses.rpcUrl;
this.rpcUrl = new HttpProvider(providerUrl);
this.addressContractAccounting = networkAddresses.addressContractAccounting;
this.addressContractPool = networkAddresses.addressContractPool;
this.addressContractWithdrawTreasury =
networkAddresses.addressContractWithdrawTreasury;
this.web3 = new Web3(this.rpcUrl);
this.contractAccounting = new this.web3.eth.Contract(
ABI_CONTRACT_ACCOUNTING,
this.addressContractAccounting,
);
this.contractPool = new this.web3.eth.Contract(
ABI_CONTRACT_POOL,
this.addressContractPool,
);
}
/**
* Converts a status code into a human-readable status.
*
* @param code - The status code to convert. '0' means 'unknown', '1' means 'pending', any other value means 'deposited'.
*
* @returns The human-readable status corresponding to the given code.
*/
public getStatusFromCode(code: ValidatorStatus): string {
return ValidatorStatus[code]
? ValidatorStatus[code].toLowerCase()
: 'invalid status';
}
/**
* Converts the given amount from Wei to Ether.
*
* @param amount - The amount in Wei to convert to Ether.
*
* @returns The converted amount in Ether as a BigNumber.
*/
private fromWeiToEther(amount: string | number | bigint): BigNumber {
return new BigNumber(this.web3.utils.fromWei(amount, 'ether'));
}
/**
* Calculates the gas limit by adding a predefined GAS_RESERVE to the given gas consumption.
*
* @param gasConsumption - The amount of gas consumed.
*
* @returns The calculated gas limit as a number.
*/
private calculateGasLimit(gasConsumption: bigint): number {
return new BigNumber(gasConsumption.toString())
.plus(ETH_GAS_RESERVE)
.toNumber();
}
/**
* Checks if a given address has the basic requirements of an Ethereum address format.
*
* @param address - The Ethereum address to validate.
*
* @returns `true` if the address meets basic requirements, otherwise `false`.
*/
private isAddress(address: string): boolean {
return /^(0x)?([0-9a-f]{40})$/.test(address.toLowerCase());
}
}