rubic-sdk
Version:
Simplify dApp creation
503 lines • 22.6 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvmWeb3Public = void 0;
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const errors_1 = require("../../../../../common/errors");
const native_tokens_1 = require("../../../../../common/tokens/constants/native-tokens");
const decorators_1 = require("../../../../../common/utils/decorators");
const p_timeout_1 = __importDefault(require("../../../../../common/utils/p-timeout"));
const healthcheck_1 = require("../../../constants/healthcheck");
const blockchain_name_1 = require("../../../models/blockchain-name");
const web3_private_1 = require("../../../web3-private-service/web3-private/web3-private");
const erc_20_token_abi_1 = require("./constants/erc-20-token-abi");
const evm_multicall_abi_1 = require("./constants/evm-multicall-abi");
const tx_status_1 = require("../models/tx-status");
const web3_public_1 = require("../web3-public");
const evm_web3_pure_1 = require("../../../web3-pure/typed-web3-pure/evm-web3-pure/evm-web3-pure");
const default_http_client_1 = require("../../../../http-client/default-http-client");
const uni_v3_permit2_abi_1 = require("./constants/uni-v3-permit2-abi");
/**
* Class containing methods for calling contracts in order to obtain information from the blockchain.
* To send transaction or execute contract method use {@link Web3Private}.
*/
class EvmWeb3Public extends web3_public_1.Web3Public {
get web3Provider() {
return this.web3;
}
constructor(web3, blockchainName, httpClient) {
super(blockchainName);
this.web3 = web3;
this.httpClient = httpClient;
this.tokenContractAbi = erc_20_token_abi_1.ERC20_TOKEN_ABI;
}
setProvider(provider) {
this.web3.setProvider(provider);
}
async healthCheck(timeoutMs = 4000) {
if (!(0, healthcheck_1.isBlockchainHealthcheckAvailable)(this.blockchainName)) {
return true;
}
const healthcheckData = healthcheck_1.HEALTHCHECK[this.blockchainName];
const contract = new this.web3.eth.Contract(healthcheckData.contractAbi, healthcheckData.contractAddress);
try {
const result = await (0, p_timeout_1.default)(contract.methods[healthcheckData.method]().call(), timeoutMs);
return result === healthcheckData.expected;
}
catch (e) {
if (e instanceof errors_1.TimeoutError) {
console.debug(`${this.blockchainName} node healthcheck timeout (${timeoutMs}ms) has occurred.`);
}
else {
console.debug(`${this.blockchainName} node healthcheck fail: ${e}`);
}
return false;
}
}
async getBalance(userAddress, tokenAddress) {
const isToken = tokenAddress && !evm_web3_pure_1.EvmWeb3Pure.isNativeAddress(tokenAddress);
const balance = isToken
? await this.getTokenBalance(userAddress, tokenAddress)
: await this.web3.eth.getBalance(userAddress);
return new bignumber_js_1.default(balance);
}
async getTokenBalance(userAddress, tokenAddress) {
const contract = new this.web3.eth.Contract(this.tokenContractAbi, tokenAddress);
const balance = await contract.methods.balanceOf(userAddress).call();
return new bignumber_js_1.default(balance);
}
async getTransactionCount(walletAddress) {
return this.web3.eth.getTransactionCount(walletAddress);
}
async getAllowance(tokenAddress, ownerAddress, spenderAddress) {
try {
const contract = new this.web3.eth.Contract(this.tokenContractAbi, tokenAddress);
const allowance = await contract.methods.allowance(ownerAddress, spenderAddress).call();
return new bignumber_js_1.default(allowance);
}
catch (err) {
console.error(err);
return new bignumber_js_1.default(0);
}
}
async getAllowanceAndExpirationOnPermit2(tokenAddress, walletAddress, spenderAddress, permit2Address) {
try {
const contract = new this.web3.eth.Contract(uni_v3_permit2_abi_1.UNI_V3_PERMIT_2_ABI, permit2Address);
const res = (await contract.methods['allowance'](walletAddress, tokenAddress, spenderAddress).call());
return [new bignumber_js_1.default(res.amount), res.expiration];
}
catch (err) {
console.error(err);
return [new bignumber_js_1.default(0), '0'];
}
}
async multicallContractsMethods(contractAbi, contractsData) {
if (this.multicallAddress) {
try {
const calls = contractsData.map(({ contractAddress, methodsData }) => {
const contract = new this.web3.eth.Contract(contractAbi, contractAddress);
return methodsData.map(({ methodName, methodArguments }) => ({
callData: contract.methods[methodName](...methodArguments).encodeABI(),
target: contractAddress
}));
});
const outputs = await this.multicall(calls.flat());
let outputIndex = 0;
return contractsData.map(contractData => contractData.methodsData.map(methodData => {
const methodOutputAbi = contractAbi.find(funcSignature => funcSignature.name === methodData.methodName).outputs;
const output = outputs[outputIndex];
if (!output) {
throw new errors_1.RubicSdkError('Output has to be defined');
}
outputIndex++;
return {
success: output.success,
output: output.success && output.returnData.length > 2
? this.web3.eth.abi.decodeParameters(methodOutputAbi, output.returnData)[0]
: null
};
}));
}
catch (err) {
if (this.allowMultipleRequests(err)) {
return this.multicallContractsMethodsByOne(contractAbi, contractsData);
}
throw err;
}
}
return this.multicallContractsMethodsByOne(contractAbi, contractsData);
}
allowMultipleRequests(err) {
return ((err instanceof Error && err.message.includes('unsigned transaction')) ||
this.blockchainName === blockchain_name_1.BLOCKCHAIN_NAME.ZETACHAIN ||
(err instanceof Error && err.message.includes('out of gas')));
}
/**
* Executes multiple calls in the single contract call.
* @param calls Multicall calls data list.
* @returns Result of calls execution.
*/
async multicall(calls) {
const contract = new this.web3.eth.Contract(evm_multicall_abi_1.EVM_MULTICALL_ABI, this.multicallAddress);
return contract.methods.tryAggregate(false, calls).call();
}
multicallContractsMethodsByOne(contractAbi, contractsData) {
return Promise.all(contractsData.map(contractData => {
const contract = new this.web3.eth.Contract(contractAbi, contractData.contractAddress);
return Promise.all(contractData.methodsData.map(async (methodData) => {
try {
const output = (await contract.methods[methodData.methodName](...methodData.methodArguments).call());
return {
success: true,
output
};
}
catch {
return {
success: false,
output: null
};
}
}));
}));
}
async callContractMethod(contractAddress, contractAbi, methodName, methodArguments = [], options = {}) {
const contract = new this.web3.eth.Contract(contractAbi, contractAddress);
const callableContract = contract.methods[methodName](...methodArguments);
return callableContract.call({
...(options.from && { from: options.from }),
...(options.value && { value: options.value }),
...(options.gasPrice && { gasPrice: options.gasPrice }),
...(options.gas && { gas: options.gas })
});
}
/**
* Predicts the volume of gas required to execute the contract method.
* @param contractAbi Abi of smart-contract.
* @param contractAddress Address of smart-contract.
* @param methodName Method which execution gas limit is to be calculated.
* @param methodArguments Arguments of the contract method.
* @param fromAddress The address for which the gas calculation will be called.
* @param value The value transferred for the call “transaction” in wei.
* @returns Estimated gas limit.
*/
async getEstimatedGas(contractAbi, contractAddress, methodName, methodArguments, fromAddress, value) {
const contract = new this.web3.eth.Contract(contractAbi, contractAddress);
try {
const gasLimit = await contract.methods[methodName](...methodArguments).estimateGas({
from: fromAddress,
...(value && { value })
});
return new bignumber_js_1.default(gasLimit);
}
catch (err) {
console.debug(err);
return null;
}
}
async getEstimatedGasByData(fromAddress, toAddress, options) {
try {
const gasLimit = await this.web3.eth.estimateGas({
from: fromAddress,
to: toAddress,
value: web3_private_1.Web3Private.stringifyAmount(options.value || 0),
...(options.gas && { gas: web3_private_1.Web3Private.stringifyAmount(options.gas) }),
...(options.data && { data: options.data })
});
return new bignumber_js_1.default(gasLimit);
}
catch (err) {
console.debug(err);
return null;
}
}
/**
* Get estimated gas of several contract method executions via rpc batch request.
* @param fromAddress Sender address.
* @param callsData Transactions parameters.
* @returns List of contract execution estimated gases.
* If the execution of the method in the real blockchain would not be reverted,
* then the list item would be equal to the predicted gas limit.
* Else (if you have not enough balance, allowance ...) then the list item would be equal to null.
*/
async batchEstimatedGas(fromAddress, callsData) {
try {
const rpcCallsData = callsData.map(callData => ({
rpcMethod: 'eth_estimateGas',
params: {
from: fromAddress,
to: callData.to,
data: callData.data,
...(callData.value && {
value: `0x${parseInt(callData.value).toString(16)}`
})
}
}));
const result = await this.rpcBatchRequest(rpcCallsData);
return result.map(value => (value ? new bignumber_js_1.default(value) : null));
}
catch (e) {
console.error(e);
return callsData.map(() => null);
}
}
/**
* Sends batch request to rpc provider directly.
* @see {@link https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json&uiSchema%5BappBar%5D%5Bui:splitView%5D=false&uiSchema%5BappBar%5D%5Bui:input%5D=false&uiSchema%5BappBar%5D%5Bui:examplesDropdown%5D=false|EthereumJSON-RPC}
* @param rpcCallsData Rpc methods and parameters list.
* @returns Rpc batch request call result sorted in order of input parameters.
*/
async rpcBatchRequest(rpcCallsData) {
const seed = Date.now();
const batch = rpcCallsData.map((callData, index) => ({
id: seed + index,
jsonrpc: '2.0',
method: callData.rpcMethod,
params: [{ ...callData.params }]
}));
const httpClient = await this.getHttpClient();
const response = await httpClient.post(this.web3.currentProvider.host, batch);
return response.sort((a, b) => a.id - b.id).map(item => (item.error ? null : item.result));
}
/**
* Returns httpClient if it exists or imports the axios client.
*/
async getHttpClient() {
if (!this.httpClient) {
this.httpClient = await default_http_client_1.DefaultHttpClient.getInstance();
}
return this.httpClient;
}
/**
* Gets mined transaction receipt.
* @param hash Transaction hash
*/
async getTransactionReceipt(hash) {
return this.web3.eth.getTransactionReceipt(hash);
}
async getTransactionStatus(hash) {
const txReceipt = await this.getTransactionReceipt(hash);
if (txReceipt === null) {
return tx_status_1.TX_STATUS.PENDING;
}
if (txReceipt.status) {
return tx_status_1.TX_STATUS.SUCCESS;
}
return tx_status_1.TX_STATUS.FAIL;
}
/**
* Calculates the average price per unit of gas according to web3.
* @returns Average gas price in wei.
*/
async getGasPrice() {
return this.web3.eth.getGasPrice();
}
/**
* Estimates average maxPriorityFeePerGas for EIP-1559 transactions based on last 20 blocks.
* @see {@link https://docs.alchemy.com/docs/how-to-build-a-gas-fee-estimator-using-eip-1559}
* @returns Average maxPriorityFeePerGas in wei
*/
async getMaxPriorityFeePerGas() {
const HISTORICAL_BLOCKS = 20;
const feeHistory = await this.web3.eth.getFeeHistory(HISTORICAL_BLOCKS, 'pending', [50]);
const blocks = feeHistory.reward.map(x => x.map(reward => Number(reward)));
const rewardSum = blocks
.map(x => x[0])
.reduce((acc, v) => acc + (v || 0), 0);
return Math.round(rewardSum / blocks.length);
}
/**
* Calculates EIP-1559 specific gas details.
* @see {@link https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/src.ts/index.ts#L235}
* @returns block baseFee, average maxPriorityFeePerGas, and maxFeePerGas.
*/
async getPriorityFeeGas() {
const block = await this.getBlock('latest');
let lastBaseFeePerGas = null;
let maxFeePerGas = null;
let maxPriorityFeePerGas = null;
if (block && block.baseFeePerGas) {
try {
lastBaseFeePerGas = this.getBaseFee(block);
maxPriorityFeePerGas = await this.getMaxPriorityFeePerGas();
maxFeePerGas = block.baseFeePerGas * 2 + maxPriorityFeePerGas;
}
catch (err) {
console.debug(err);
}
}
return {
baseFee: lastBaseFeePerGas?.toFixed(),
maxFeePerGas: maxFeePerGas?.toFixed(),
maxPriorityFeePerGas: maxPriorityFeePerGas?.toFixed()
};
}
/**
* Calculates base fee for a given block, based on EIP-1559 base fee formula
* @see {@link https://eips.ethereum.org/EIPS/eip-1559}
* @param block Block details
* @returns Base fee for a given block
*/
getBaseFee(block) {
if (!block.baseFeePerGas)
return null;
const BASE_FEE_MAX_CHANGE_DENOMINATOR = 8;
const parentGasUsed = block.gasUsed;
const parentGasTarget = block.gasLimit;
const parentBaseFeePerGas = block.baseFeePerGas;
let lastBaseFeePerGas = null;
if (parentGasUsed === parentGasTarget) {
lastBaseFeePerGas = block.baseFeePerGas;
}
else if (parentGasUsed > parentGasTarget) {
const gasUsedDelta = parentGasUsed - parentGasTarget;
const baseFeePerGasDelta = Math.max((parentBaseFeePerGas * gasUsedDelta) /
parentGasTarget /
BASE_FEE_MAX_CHANGE_DENOMINATOR, 1);
lastBaseFeePerGas = parentBaseFeePerGas + baseFeePerGasDelta;
}
else {
const gasUsedDelta = parentGasTarget - parentGasUsed;
const baseFeePerGasDelta = (parentBaseFeePerGas * gasUsedDelta) /
parentGasTarget /
BASE_FEE_MAX_CHANGE_DENOMINATOR;
lastBaseFeePerGas = parentBaseFeePerGas - baseFeePerGasDelta;
}
return lastBaseFeePerGas;
}
/**
* Gets block by block id.
* @param [blockId] Block id: hash, number ... Default is 'latest'.
* @returns Block by blockId parameter.
*/
getBlock(blockId = 'latest') {
return this.web3.eth.getBlock(blockId);
}
async getBlockNumber() {
return this.web3.eth.getBlockNumber();
}
async getPastEvents(contractAddress, contractAbi, eventName, options) {
const contract = new this.web3.eth.Contract(contractAbi, contractAddress);
const blockNumber = options.toBlock === 'latest' ? await this.getBlockNumber() : options.toBlock;
return contract.getPastEvents(eventName, {
fromBlock: blockNumber - options.blocksAmount,
toBlock: blockNumber
});
}
/**
* Will call smart contract method in the EVM without sending any transaction.
* @param contractAddress Contract address.
* @param contractAbi Contract ABI.
* @param methodName Method name.
* @param methodArguments Method arguments.
* @param options Sender address and value.
* @returns Transaction receipt.
*/
async staticCallContractMethod(contractAddress, contractAbi, methodName, methodArguments, options) {
const contract = new this.web3.eth.Contract(contractAbi, contractAddress);
return new Promise((resolve, reject) => {
contract.methods[methodName](...methodArguments).call({
from: options?.from,
...(options?.value && { value: options.value })
}, (error, result) => {
if (result) {
resolve(result);
}
if (error) {
reject(error);
}
});
});
}
async getTokensBalances(userAddress, tokensAddresses) {
const indexOfNativeCoin = tokensAddresses.findIndex(evm_web3_pure_1.EvmWeb3Pure.isNativeAddress);
const promises = [];
if (indexOfNativeCoin !== -1) {
tokensAddresses.splice(indexOfNativeCoin, 1);
promises[1] = this.getBalance(userAddress);
}
promises[0] = this.multicallContractsMethods(this.tokenContractAbi, tokensAddresses.map(tokenAddress => ({
contractAddress: tokenAddress,
methodsData: [
{
methodName: 'balanceOf',
methodArguments: [userAddress]
}
]
})));
const results = await Promise.all(promises);
const tokensBalances = results[0].map(tokenResults => {
const { success, output } = tokenResults[0];
return success ? new bignumber_js_1.default(output) : new bignumber_js_1.default(0);
});
if (indexOfNativeCoin !== -1) {
tokensBalances.splice(indexOfNativeCoin, 0, results[1]);
}
return tokensBalances;
}
async callForTokensInfo(tokenAddresses, tokenFields = ['decimals', 'symbol', 'name']) {
const nativeTokenIndex = tokenAddresses.findIndex(address => this.Web3Pure.isNativeAddress(address));
const filteredTokenAddresses = tokenAddresses.filter((_, index) => index !== nativeTokenIndex);
const contractsData = filteredTokenAddresses.map(contractAddress => ({
contractAddress,
methodsData: tokenFields.map(methodName => ({
methodName,
methodArguments: []
}))
}));
const results = contractsData.length
? await this.multicallContractsMethods(this.tokenContractAbi, contractsData)
: [];
const tokens = results.map((tokenFieldsResults, tokenIndex) => {
const tokenAddress = tokenAddresses[tokenIndex];
return tokenFieldsResults.reduce((acc, field, fieldIndex) => {
if (!field.success) {
throw new errors_1.RubicSdkError(`Cannot retrieve information about ${tokenAddress}`);
}
return {
...acc,
[tokenFields[fieldIndex]]: field.success ? field.output : undefined
};
}, {});
});
if (nativeTokenIndex === -1) {
return tokens;
}
const blockchainNativeToken = native_tokens_1.nativeTokensList[this.blockchainName];
const nativeToken = {
...blockchainNativeToken,
decimals: blockchainNativeToken.decimals.toString()
};
tokens.splice(nativeTokenIndex, 0, nativeToken);
return tokens;
}
async getTxDecodedLogData(hash, inputs, key) {
const receipt = await this.getTransactionReceipt(hash);
let decodedData = {};
for (const log of receipt.logs) {
try {
const data = this.web3.eth.abi.decodeLog(inputs, log.data, log.topics.slice(1));
if (data?.[key]) {
decodedData = data;
}
}
catch {
continue;
}
}
return decodedData[key];
}
}
exports.EvmWeb3Public = EvmWeb3Public;
__decorate([
decorators_1.Cache
], EvmWeb3Public.prototype, "callForTokensInfo", null);
//# sourceMappingURL=evm-web3-public.js.map