UNPKG

origintrail-node

Version:

OriginTrail Node - Decentralized Knowledge Graph Node Library

1,321 lines (1,154 loc) 47.3 kB
/* eslint-disable no-await-in-loop */ import { ethers, BigNumber } from 'ethers'; import axios from 'axios'; import async from 'async'; import { setTimeout as sleep } from 'timers/promises'; import { SOLIDITY_ERROR_STRING_PREFIX, SOLIDITY_PANIC_CODE_PREFIX, SOLIDITY_PANIC_REASONS, ZERO_PREFIX, TRANSACTION_QUEUE_CONCURRENCY, TRANSACTION_POLLING_TIMEOUT_MILLIS, TRANSACTION_CONFIRMATIONS, WS_RPC_PROVIDER_PRIORITY, HTTP_RPC_PROVIDER_PRIORITY, FALLBACK_PROVIDER_QUORUM, RPC_PROVIDER_STALL_TIMEOUT, CACHED_FUNCTIONS, CACHE_DATA_TYPES, CONTRACTS, CONTRACT_FUNCTION_PRIORITY, TRANSACTION_PRIORITY, CONTRACT_FUNCTION_GAS_LIMIT_INCREASE_FACTORS, ABIs, EXPECTED_TRANSACTION_ERRORS, } from '../../../constants/constants.js'; import Web3ServiceValidator from './web3-service-validator.js'; class Web3Service { async initialize(config, logger) { this.config = config; this.logger = logger; this.contractCallCache = {}; await this.initializeWeb3(); this.initializeTransactionQueues(); await this.initializeContracts(); this.initializeProviderDebugging(); } initializeTransactionQueues(concurrency = TRANSACTION_QUEUE_CONCURRENCY) { this.transactionQueues = {}; for (const operationalWallet of this.operationalWallets) { const transactionQueue = async.priorityQueue((args, cb) => { const { contractInstance, functionName, transactionArgs, gasPrice } = args; this._executeContractFunction( contractInstance, functionName, transactionArgs, gasPrice, operationalWallet, ) .then((result) => { cb({ result }); }) .catch((error) => { cb({ error }); }); }, concurrency); this.transactionQueues[operationalWallet.address] = transactionQueue; } this.transactionQueueOrder = Object.keys(this.transactionQueues); } queueTransaction(contractInstance, functionName, transactionArgs, callback, gasPrice) { const selectedQueue = this.selectTransactionQueue(); const priority = CONTRACT_FUNCTION_PRIORITY[functionName] ?? TRANSACTION_PRIORITY.MEDIUM; this.logger.info(`Calling ${functionName} with priority: ${priority}`); selectedQueue.push( { contractInstance, functionName, transactionArgs, gasPrice, }, priority, callback, ); } removeTransactionQueue(walletAddress) { delete this.transactionQueues[walletAddress]; } getTotalTransactionQueueLength() { let totalLength = 0; Object.values(this.transactionQueues).forEach((queue) => { totalLength += queue.length(); }); return totalLength; } selectTransactionQueue() { const queues = Object.keys(this.transactionQueues).map((wallet) => ({ wallet, length: this.transactionQueues[wallet].length(), })); const minLength = Math.min(...queues.map((queue) => queue.length)); const shortestQueues = queues.filter((queue) => queue.length === minLength); if (shortestQueues.length === 1) { return this.transactionQueues[shortestQueues[0].wallet]; } const selectedQueueWallet = this.transactionQueueOrder.find((roundRobinNext) => shortestQueues.some((shortestQueue) => shortestQueue.wallet === roundRobinNext), ); this.transactionQueueOrder.push( this.transactionQueueOrder .splice(this.transactionQueueOrder.indexOf(selectedQueueWallet), 1) .pop(), ); return this.transactionQueues[selectedQueueWallet]; } getValidOperationalWallets() { const wallets = []; this.config.operationalWallets.forEach((wallet) => { try { wallets.push(new ethers.Wallet(wallet.privateKey, this.provider)); } catch (error) { this.logger.warn( `Invalid evm private key, unable to create wallet instance. Wallet public key: ${wallet.evmAddress}. Error: ${error.message}`, ); } }); return wallets; } getRandomOperationalWallet() { const randomIndex = Math.floor(Math.random() * this.operationalWallets.length); return this.operationalWallets[randomIndex]; } async initializeWeb3() { const providers = []; for (const rpcEndpoint of this.config.rpcEndpoints) { const isWebSocket = rpcEndpoint.startsWith('ws'); const Provider = isWebSocket ? ethers.providers.WebSocketProvider : ethers.providers.JsonRpcProvider; const priority = isWebSocket ? WS_RPC_PROVIDER_PRIORITY : HTTP_RPC_PROVIDER_PRIORITY; try { const provider = new Provider(rpcEndpoint); // eslint-disable-next-line no-await-in-loop await provider.getNetwork(); providers.push({ provider, priority, weight: 1, stallTimeout: RPC_PROVIDER_STALL_TIMEOUT, }); this.logger.debug( `Connected to the blockchain RPC: ${this.maskRpcUrl(rpcEndpoint)}.`, ); } catch (e) { this.logger.warn( `Unable to connect to the blockchain RPC: ${this.maskRpcUrl(rpcEndpoint)}.`, ); } } try { this.provider = new ethers.providers.FallbackProvider( providers, FALLBACK_PROVIDER_QUORUM, ); // eslint-disable-next-line no-await-in-loop await this.providerReady(); } catch (e) { throw new Error( `RPC Fallback Provider initialization failed. Fallback Provider quorum: ${FALLBACK_PROVIDER_QUORUM}. Error: ${e.message}.`, ); } this.operationalWallets = this.getValidOperationalWallets(); if (this.operationalWallets.length === 0) { throw Error( 'Unable to initialize web3 service, all operational wallets provided are invalid', ); } } async initializeContracts() { this.contracts = {}; this.contractAddresses = {}; this.logger.info( `Initializing contracts with hub contract address: ${this.config.hubContractAddress}`, ); this.contracts.Hub = new ethers.Contract( this.config.hubContractAddress, ABIs.Hub, this.operationalWallets[0], ); this.contractAddresses[this.config.hubContractAddress] = this.contracts.Hub; const contractsArray = await this.callContractFunction( this.contracts.Hub, 'getAllContracts', [], ); contractsArray.forEach(([contractName, contractAddress]) => { this.initializeContract(contractName, contractAddress); }); this.assetStorageContracts = {}; const assetStoragesArray = await this.callContractFunction( this.contracts.Hub, 'getAllAssetStorages', [], ); assetStoragesArray.forEach(([, assetStorageAddress]) => { this.initializeAssetStorageContract(assetStorageAddress); }); this.logger.info(`Contracts initialized`); await this.logBalances(); } initializeProviderDebugging() { this.provider.on('debug', (info) => { const { method } = info.request; if (['call', 'estimateGas'].includes(method)) { const contractInstance = this.contractAddresses[info.request.params.transaction.to]; const inputData = info.request.params.transaction.data; const decodedInputData = this._decodeInputData( inputData, contractInstance.interface, ); const functionFragment = contractInstance.interface.getFunction( inputData.slice(0, 10), ); const functionName = functionFragment.name; const inputs = functionFragment.inputs .map((input, i) => { const argName = input.name; const argValue = this._formatArgument(decodedInputData[i]); return `${argName}=${argValue}`; }) .join(', '); if (info.backend.error) { const decodedErrorData = this._decodeErrorData( info.backend.error, contractInstance.interface, ); this.logger.debug( `${functionName}(${inputs}) ${method} has failed; Error: ${decodedErrorData}; ` + `RPC: ${this.maskRpcUrl(info.backend.provider.connection.url)}.`, ); } else if (info.backend.result !== undefined) { let message = `${functionName}(${inputs}) ${method} has been successfully executed; `; if (info.backend.result !== null && method !== 'estimateGas') { try { const decodedResultData = this._decodeResultData( inputData.slice(0, 10), info.backend.result, contractInstance.interface, ); message += `Result: ${decodedResultData}; `; } catch (error) { this.logger.warn( `Unable to decode result data for. Message: ${message}`, ); } } message += `RPC: ${this.maskRpcUrl(info.backend.provider.connection.url)}.`; this.logger.debug(message); } } }); } maskRpcUrl(url) { if (url.includes('apiKey')) { return url.split('apiKey')[0]; } return url; } initializeAssetStorageContract(assetStorageAddress) { this.assetStorageContracts[assetStorageAddress.toLowerCase()] = new ethers.Contract( assetStorageAddress, ABIs.KnowledgeCollectionStorage, this.operationalWallets[0], ); this.contractAddresses[assetStorageAddress] = this.assetStorageContracts[assetStorageAddress.toLowerCase()]; } setContractCallCache(contractName, functionName, value) { if (CACHED_FUNCTIONS[contractName]?.[functionName]) { const type = CACHED_FUNCTIONS[contractName][functionName]; if (!this.contractCallCache[contractName]) { this.contractCallCache[contractName] = {}; } switch (type) { case CACHE_DATA_TYPES.NUMBER: this.contractCallCache[contractName][functionName] = Number(value); break; default: this.contractCallCache[contractName][functionName] = value; } } } getContractCallCache(contractName, functionName) { if ( CACHED_FUNCTIONS[contractName]?.[functionName] && this.contractCallCache[contractName]?.[functionName] ) { return this.contractCallCache[contractName][functionName]; } return null; } initializeContract(contractName, contractAddress) { if (ABIs[contractName] != null) { this.contracts[contractName] = new ethers.Contract( contractAddress, ABIs[contractName], this.operationalWallets[0], ); this.contractAddresses[contractAddress] = this.contracts[contractName]; } } getContractAddress(contractName) { const contract = this.contracts[contractName]; if (!contract) { return null; } return contract.address; } async providerReady() { return this.provider.getNetwork(); } getPublicKeys() { return this.operationalWallets.map((wallet) => wallet.address); } getManagementKey() { return this.config.evmManagementWalletPublicKey; } async logBalances() { for (const wallet of this.operationalWallets) { // eslint-disable-next-line no-await-in-loop const nativeBalance = await this.getNativeTokenBalance(wallet); // eslint-disable-next-line no-await-in-loop const tokenBalance = await this.getTokenBalance(wallet.address); this.logger.info( `Balance of ${wallet.address} is ${nativeBalance} ${this.baseTokenTicker} and ${tokenBalance} ${this.tracTicker}.`, ); } } async getNativeTokenBalance(wallet) { const nativeBalance = await wallet.getBalance(); return Number(ethers.utils.formatEther(nativeBalance)); } async getTokenBalance(publicKey) { const tokenBalance = await this.callContractFunction(this.contracts.Token, 'balanceOf', [ publicKey, ]); return Number(ethers.utils.formatEther(tokenBalance)); } async getBlockNumber() { const latestBlock = await this.provider.getBlock('latest'); return latestBlock.number; } async getIdentityId() { if (this.identityId) { return this.identityId; } const promises = this.operationalWallets.map((wallet) => this.callContractFunction( this.contracts.IdentityStorage, 'getIdentityId', [wallet.address], CONTRACTS.IDENTITY_STORAGE, ).then((identityId) => [wallet.address, Number(identityId)]), ); const results = await Promise.all(promises); this.identityId = 0; const walletWithIdentityZero = []; results.forEach(([publicKey, identityId]) => { this.logger.trace( `Identity id: ${identityId} found for wallet: ${publicKey} on blockchain: ${this.getBlockchainId()}`, ); if (identityId !== 0) { if (this.identityId !== identityId && this.identityId !== 0) { const index = this.operationalWallets.find( (wallet) => wallet.address === publicKey, ); this.operationalWallets.splice(index, 1); this.logger.warn( `Found invalid identity id. Identity id: ${identityId} found for wallet: ${publicKey}, expected identity id: ${ this.identityId } on blockchain: ${this.getBlockchainId()}. Operational wallet will not be used for transactions.`, ); this.removeTransactionQueue(publicKey); } else { this.identityId = identityId; } } else { walletWithIdentityZero.push(publicKey); } }); if (this.identityId !== 0) { walletWithIdentityZero.forEach((publicKey) => { const index = this.operationalWallets.find( (wallet) => wallet.address === publicKey, ); this.operationalWallets.splice(index, 1); this.logger.warn( `Operational wallet: ${publicKey} don't have profile connected to it, expected identity id: ${ this.identityId } on blockchain ${this.getBlockchainId()}`, ); }); } if (this.operationalWallets.length === 0) { throw new Error( `Unable to find valid operational wallets for blockchain implementation: ${this.getBlockchainId()}`, ); } return this.identityId; } async identityIdExists() { const identityId = await this.getIdentityId(); return !!identityId; } async createProfile(peerId) { if (!this.config.nodeName) { throw new Error( 'Missing nodeName in blockchain configuration. Please add it and start the node again.', ); } const maxNumberOfRetries = 3; let retryCount = 0; let profileCreated = false; const retryDelayInSec = 12; while (retryCount + 1 <= maxNumberOfRetries && !profileCreated) { try { // eslint-disable-next-line no-await-in-loop await this._executeContractFunction( this.contracts.Profile, 'createProfile', [ this.getManagementKey(), this.getPublicKeys().slice(1), this.config.nodeName, ethers.utils.hexlify(ethers.utils.toUtf8Bytes(peerId)), this.config.operatorFee, ], null, this.operationalWallets[0], ); this.logger.info( `Profile created with name: ${this.config.nodeName}, wallet: ${ this.operationalWallets[0].address }, on blockchain ${this.getBlockchainId()}`, ); profileCreated = true; } catch (error) { if (error.message.includes('Profile already exists')) { this.logger.info( `Skipping profile creation, already exists on blockchain ${this.getBlockchainId()}.`, ); profileCreated = true; } else if (retryCount + 1 < maxNumberOfRetries) { retryCount += 1; this.logger.warn( `Unable to create profile. Will retry in ${retryDelayInSec}s. Retries left: ${ maxNumberOfRetries - retryCount } on blockchain ${this.getBlockchainId()}. Error: ${error}`, ); // eslint-disable-next-line no-await-in-loop await sleep(retryDelayInSec * 1000); } else { throw error; } } } } async getGasPrice() { try { const response = await axios.get(this.config.gasPriceOracleLink); const gasPriceRounded = Math.round(response.data.standard.maxFee * 1e9); return gasPriceRounded; } catch (error) { return undefined; } } async callContractFunction(contractInstance, functionName, args, contractName = null) { const maxNumberOfRetries = 3; const retryDelayInSec = 12; let retryCount = 0; let result = this.getContractCallCache(contractName, functionName); try { if (!result) { while (retryCount < maxNumberOfRetries) { result = await contractInstance[functionName](...args); const resultIsValid = Web3ServiceValidator.validateResult( functionName, contractName, result, this.logger, ); if (resultIsValid) { this.setContractCallCache(contractName, functionName, result); return result; } if (retryCount === maxNumberOfRetries - 1) { return null; } await sleep(retryDelayInSec * 1000); retryCount += 1; } } } catch (error) { this._decodeContractCallError(contractInstance, functionName, error, args); } return result; } async _executeContractFunction( contractInstance, functionName, args, predefinedGasPrice, operationalWallet, ) { let result; let gasPrice = predefinedGasPrice ?? (await this.getGasPrice()); let gasLimit; let retryCount = 0; const maxRetries = 3; try { /* eslint-disable no-await-in-loop */ gasLimit = await contractInstance.estimateGas[functionName](...args); } catch (error) { this._decodeEstimateGasError(contractInstance, functionName, error, args); } gasLimit = gasLimit ?? ethers.utils.parseUnits('900', 'kwei'); const gasLimitMultiplier = CONTRACT_FUNCTION_GAS_LIMIT_INCREASE_FACTORS[functionName] ?? 1; gasLimit = gasLimit.mul(gasLimitMultiplier * 100).div(100); while (retryCount < maxRetries) { try { this.logger.debug( `Sending signed transaction ${functionName} to the blockchain ${this.getBlockchainId()}` + ` with gas limit: ${gasLimit.toString()} and gasPrice ${gasPrice.toString()}. ` + `Transaction queue length: ${this.getTotalTransactionQueueLength()}. Wallet used: ${ operationalWallet.address }${retryCount > 0 ? ` (retry ${retryCount})` : ''}`, ); const tx = await contractInstance .connect(operationalWallet) [functionName](...args, { gasPrice, gasLimit, }); try { result = await this.provider.waitForTransaction( tx.hash, TRANSACTION_CONFIRMATIONS, TRANSACTION_POLLING_TIMEOUT_MILLIS, ); if (result.status === 0) { await this.provider.call(tx, tx.blockNumber); } } catch (error) { this._decodeWaitForTxError(contractInstance, functionName, error, args); } return result; } catch (error) { const errorMessage = error.message.toLowerCase(); // Check for nonce-related errors if ( errorMessage.includes( EXPECTED_TRANSACTION_ERRORS.NONCE_TOO_LOW.toLowerCase(), ) || errorMessage.includes( EXPECTED_TRANSACTION_ERRORS.REPLACEMENT_UNDERPRICED.toLowerCase(), ) || errorMessage.includes(EXPECTED_TRANSACTION_ERRORS.ALREADY_KNOWN.toLowerCase()) ) { retryCount += 1; if (retryCount < maxRetries) { // Increase gas price by 20% for nonce errors gasPrice = Math.ceil(gasPrice * 1.2); this.logger.warn( `Nonce error detected for ${functionName}. Retrying with increased gas price: ${gasPrice} (retry ${retryCount}/${maxRetries})`, ); continue; } else { this.logger.error( `Max retries (${maxRetries}) reached for nonce error in ${functionName}. Final gas price: ${gasPrice}`, ); } } // If it's not a nonce error or we've exhausted retries, re-throw the error throw error; } } } _decodeEstimateGasError(contractInstance, functionName, error, args) { try { const decodedErrorData = this._decodeErrorData(error, contractInstance.interface); if (error.transaction === undefined) { throw new Error( `Gas estimation for ${functionName} has failed, reason: ${decodedErrorData}`, ); } const functionFragment = contractInstance.interface.getFunction( error.transaction.data.slice(0, 10), ); const inputs = functionFragment.inputs .map((input, i) => { const argName = input.name; const argValue = this._formatArgument(args[i]); return `${argName}=${argValue}`; }) .join(', '); throw new Error( `Gas estimation for ${functionName}(${inputs}) has failed, reason: ${decodedErrorData}`, ); } catch (decodeError) { this.logger.warn(`Unable to decode estimate gas error: ${decodeError}`); throw error; } } _decodeWaitForTxError(contractInstance, functionName, error, args) { try { const decodedErrorData = this._decodeErrorData(error, contractInstance.interface); let sigHash; if (error.transaction) { sigHash = error.transaction.data.slice(0, 10); } else { sigHash = this._getFunctionSighash(contractInstance, functionName, args); } const functionFragment = contractInstance.interface.getFunction(sigHash); const inputs = functionFragment.inputs .map((input, i) => { const argName = input.name; const argValue = this._formatArgument(args[i]); return `${argName}=${argValue}`; }) .join(', '); throw new Error( `Transaction ${functionName}(${inputs}) has been reverted, reason: ${decodedErrorData}`, ); } catch (decodeError) { this.logger.warn(`Unable to decode wait for transaction error: ${decodeError}`); throw error; } } _decodeContractCallError(contractInstance, functionName, error, args) { try { const decodedErrorData = this._decodeErrorData(error, contractInstance.interface); const functionFragment = contractInstance.interface.getFunction( error.transaction.data.slice(0, 10), ); const inputs = functionFragment.inputs .map((input, i) => { const argName = input.name; const argValue = this._formatArgument(args[i]); return `${argName}=${argValue}`; }) .join(', '); throw new Error(`Call ${functionName}(${inputs}) failed, reason: ${decodedErrorData}`); } catch (decodeError) { this.logger.warn(`Unable to decode contract call error: ${decodeError}`); throw error; } } _getFunctionSighash(contractInstance, functionName, args) { const functions = Object.keys(contractInstance.interface.functions) .filter((key) => contractInstance.interface.functions[key].name === functionName) .map((key) => ({ signature: key, ...contractInstance.interface.functions[key] })); for (const func of functions) { try { // Checks if given arguments can be encoded with function ABI inputs // may be useful for overloaded functions as it would help to find // needed function fragment ethers.utils.defaultAbiCoder.encode(func.inputs, args); const sighash = ethers.utils.hexDataSlice( ethers.utils.keccak256(ethers.utils.toUtf8Bytes(func.signature)), 0, 4, ); return sighash; } catch (error) { continue; } } throw new Error('No matching function signature found'); } _getErrorData(error) { let nestedError = error; while (nestedError && nestedError.error) { nestedError = nestedError.error; } const errorData = nestedError.data; if (errorData === undefined) { throw error; } let returnData = typeof errorData === 'string' ? errorData : errorData.data; if (typeof returnData === 'object' && returnData.data) { returnData = returnData.data; } if (returnData === undefined || typeof returnData !== 'string') { throw error; } return returnData; } _decodeInputData(inputData, contractInterface) { if (inputData === ZERO_PREFIX) { return 'Empty input data.'; } return contractInterface.decodeFunctionData(inputData.slice(0, 10), inputData); } _decodeErrorData(evmError, contractInterface) { let errorData; try { errorData = this._getErrorData(evmError); } catch (error) { return error.message; } // Handle empty error data if (errorData === ZERO_PREFIX) { return 'Empty error data.'; } // Handle standard solidity string error if (errorData.startsWith(SOLIDITY_ERROR_STRING_PREFIX)) { const encodedReason = errorData.slice(SOLIDITY_ERROR_STRING_PREFIX.length); try { return ethers.utils.defaultAbiCoder.decode(['string'], `0x${encodedReason}`)[0]; } catch (error) { return error.message; } } // Handle solidity panic code if (errorData.startsWith(SOLIDITY_PANIC_CODE_PREFIX)) { const encodedReason = errorData.slice(SOLIDITY_PANIC_CODE_PREFIX.length); let code; try { [code] = ethers.utils.defaultAbiCoder.decode(['uint256'], `0x${encodedReason}`); } catch (error) { return error.message; } return SOLIDITY_PANIC_REASONS[code] ?? 'Unknown Solidity panic code.'; } // Try parsing a custom error using the contract ABI try { const decodedCustomError = contractInterface.parseError(errorData); const formattedArgs = decodedCustomError.errorFragment.inputs .map((input, i) => { const argName = input.name; const argValue = this._formatArgument(decodedCustomError.args[i]); return `${argName}=${argValue}`; }) .join(', '); return `custom error ${decodedCustomError.name}(${formattedArgs})`; } catch (error) { return `Failed to decode custom error data. Error: ${error}`; } } _decodeResultData(fragment, resultData, contractInterface) { if (resultData === ZERO_PREFIX) { return 'Empty input data.'; } return contractInterface.decodeFunctionResult(fragment, resultData); } _formatArgument(value) { if (value === null || value === undefined) { return 'null'; } if (typeof value === 'string') { return value; } if (typeof value === 'number' || BigNumber.isBigNumber(value)) { return value.toString(); } if (Array.isArray(value)) { return `[${value.map((v) => this._formatArgument(v)).join(', ')}]`; } if (typeof value === 'object') { const formattedEntries = Object.entries(value).map( ([k, v]) => `${k}: ${this._formatArgument(v)}`, ); return `{${formattedEntries.join(', ')}}`; } return value.toString(); } async isAssetStorageContract(contractAddress) { return this.callContractFunction(this.contracts.Hub, 'isAssetStorage(address)', [ contractAddress, ]); } async getKnowledgeCollectionMerkleRootByIndex( assetStorageContractAddress, knowledgeCollectionId, index, ) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); return this.callContractFunction(assetStorageContractInstance, 'getMerkleRootByIndex', [ knowledgeCollectionId, index, ]); } async getKnowledgeCollectionLatestMerkleRoot( assetStorageContractAddress, knowledgeCollectionId, ) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); return this.callContractFunction(assetStorageContractInstance, 'getLatestMerkleRoot', [ knowledgeCollectionId, ]); } async getLatestKnowledgeCollectionId(assetStorageContractAddress) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); const lastKnowledgeCollectionId = await this.callContractFunction( assetStorageContractInstance, 'getLatestKnowledgeCollectionId', [], ); return lastKnowledgeCollectionId; } getAssetStorageContractAddresses() { return Object.keys(this.assetStorageContracts); } async getKnowledgeCollectionMerkleRoots(assetStorageContractAddress, tokenId) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); return this.callContractFunction(assetStorageContractInstance, 'getMerkleRoots', [tokenId]); } // async getKnowledgeAssetOwner(assetContractAddress, tokenId) { // const assetStorageContractInstance = // this.assetStorageContracts[assetContractAddress.toString().toLowerCase()]; // if (!assetStorageContractInstance) // throw new Error('Unknown asset storage contract address'); // return this.callContractFunction(assetStorageContractInstance, 'ownerOf', [tokenId]); // } async getLatestMerkleRootPublisher(assetStorageContractAddress, knowledgeCollectionId) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); const knowledgeCollectionPublisher = await this.callContractFunction( assetStorageContractInstance, 'getLatestMerkleRootPublisher', [knowledgeCollectionId], ); return knowledgeCollectionPublisher; } async getKnowledgeCollectionSize(assetStorageContractAddress, knowledgeCollectionId) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); const knowledgeCollectionSize = await this.callContractFunction( assetStorageContractInstance, 'getByteSize', [knowledgeCollectionId], ); return Number(knowledgeCollectionSize); } async getKnowledgeAssetsRange(assetStorageContractAddress, knowledgeCollectionId) { const assetStorageContractInstance = this.assetStorageContracts[assetStorageContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); const knowledgeAssetsRange = await this.callContractFunction( assetStorageContractInstance, 'getKnowledgeAssetsRange', [knowledgeCollectionId], ); return { startTokenId: Number( knowledgeAssetsRange[0] .sub(BigNumber.from(knowledgeCollectionId - 1).mul('0x0f4240')) .toString(), ), endTokenId: Number( knowledgeAssetsRange[1] .sub(BigNumber.from(knowledgeCollectionId - 1).mul('0x0f4240')) .toString(), ), burned: knowledgeAssetsRange[2].map((burned) => Number( burned .sub(BigNumber.from(knowledgeCollectionId - 1).mul('0x0f4240')) .toString(), ), ), }; } async getMinimumStake() { const minimumStake = await this.callContractFunction( this.contracts.ParametersStorage, 'minimumStake', [], CONTRACTS.PARAMETERS_STORAGE, ); return Number(ethers.utils.formatEther(minimumStake)); } async getMaximumStake() { const maximumStake = await this.callContractFunction( this.contracts.ParametersStorage, 'maximumStake', [], CONTRACTS.PARAMETERS_STORAGE, ); return Number(ethers.utils.formatEther(maximumStake)); } async getShardingTableHead() { return this.callContractFunction(this.contracts.ShardingTableStorage, 'head', []); } async getShardingTableLength() { const nodesCount = await this.callContractFunction( this.contracts.ShardingTableStorage, 'nodesCount', [], ); return Number(nodesCount); } async getShardingTablePage(startingIdentityId, nodesNum) { return this.callContractFunction( this.contracts.ShardingTable, 'getShardingTable(uint72,uint72)', [startingIdentityId, nodesNum], ); } getBlockchainId() { return this.getImplementationName(); } async healthCheck() { try { const gasPrice = await this.operationalWallets[0].getGasPrice(); if (gasPrice) return true; } catch (e) { this.logger.error(`Error on checking blockchain. ${e}`); return false; } return false; } async restartService() { await this.initializeWeb3(); await this.initializeContracts(); } async getBlockchainTimestamp() { return Math.floor(Date.now() / 1000); } async getLatestBlock() { const currentBlock = await this.provider.getBlockNumber(); const blockTimestamp = await this.provider.getBlock(currentBlock); return blockTimestamp; } async getParanetKnowledgeCollectionCount(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'getKnowledgeCollectionsCount', [paranetId], ); } async getParanetKnowledgeCollectionLocatorsWithPagination(paranetId, offset, limit) { return this.callContractFunction( this.contracts.Paranet, 'getKnowledgeCollectionLocatorsWithPagination', [paranetId, offset, limit], ); } async getParanetMetadata(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'getParanetMetadata', [paranetId], CONTRACTS.PARANETS_REGISTRY, ); } async getParanetName(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'getName', [paranetId], CONTRACTS.PARANETS_REGISTRY, ); } async getDescription(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'getDescription', [paranetId], CONTRACTS.PARANETS_REGISTRY, ); } async paranetExists(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'paranetExists', [paranetId], CONTRACTS.PARANETS_REGISTRY, ); } async isPermissionedNode(paranetId, identityId) { return this.callContractFunction(this.contracts.ParanetsRegistry, 'isPermissionedNode', [ paranetId, identityId, ]); } async getNodesAccessPolicy(paranetId) { return this.callContractFunction(this.contracts.ParanetsRegistry, 'getNodesAccessPolicy', [ paranetId, ]); } async getPermissionedNodes(paranetId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'getPermissionedNodes', [paranetId], CONTRACTS.PARANETS_REGISTRY, ); } async getNodeId(identityId) { return this.callContractFunction(this.contracts.ProfileStorage, 'getNodeId', [identityId]); } async signMessage(messageHash) { const wallet = this.getRandomOperationalWallet(); return wallet.signMessage(ethers.utils.arrayify(messageHash)); } async getStakeWeightedAverageAsk() { return this.callContractFunction( this.contracts.AskStorage, 'getStakeWeightedAverageAsk', [], ); } async getTimeUntilNextEpoch() { return this.callContractFunction(this.contracts.Chronos, 'timeUntilNextEpoch', []); } async getEpochLength() { return this.callContractFunction(this.contracts.Chronos, 'epochLength', []); } async isKnowledgeCollectionRegistered(paranetId, knowledgeCollectionId) { return this.callContractFunction( this.contracts.ParanetsRegistry, 'isKnowledgeCollectionRegistered', [paranetId, knowledgeCollectionId], ); } async getActiveProofPeriodStatus() { return this.callContractFunction( this.contracts.RandomSampling, 'getActiveProofPeriodStatus', [], ); } async createChallenge() { return new Promise((resolve) => { this.queueTransaction( this.contracts.RandomSampling, 'createChallenge', [], (result) => { if (result.error || result?.result?.status === 0) { resolve({ success: false, error: result?.error ?? 'Error message not found', }); } else { resolve({ success: true, result: result.result, }); } }, ); }); } async getNodeChallenge(nodeId) { return this.callContractFunction(this.contracts.RandomSamplingStorage, 'getNodeChallenge', [ nodeId, ]); } async submitProof(chunk, merkleProof) { return new Promise((resolve, reject) => { this.queueTransaction( this.contracts.RandomSampling, 'submitProof', [chunk, merkleProof], (result) => { if (result.error) { reject(result.error); } else { resolve({ success: true, result: result.result, }); } }, ); }); } async getNodeEpochProofPeriodScore(nodeId, epoch, proofPeriodStartBlock) { return this.callContractFunction( this.contracts.RandomSamplingStorage, 'getNodeEpochProofPeriodScore', [nodeId, epoch, proofPeriodStartBlock], ); } async getTransaction(txHash) { return this.provider.getTransaction(txHash); } async getBlockTimestamp(blockNumber) { const block = await this.provider.getBlock(blockNumber); return block.timestamp; } async getDelegators(identityId) { return this.callContractFunction(this.contracts.DelegatorsInfo, 'getDelegators', [ identityId, ]); } async hasEverDelegated(identityId, address) { return this.callContractFunction(this.contracts.DelegatorsInfo, 'hasEverDelegatedToNode', [ identityId, address, ]); } async getCurrentEpoch() { return this.callContractFunction(this.contracts.Chronos, 'getCurrentEpoch', []); } async getLastClaimedEpoch(identityId, address) { return this.callContractFunction(this.contracts.DelegatorsInfo, 'getLastClaimedEpoch', [ identityId, address, ]); } async batchClaimDelegatorRewards(identityId, epochs, delegators) { return new Promise((resolve, reject) => { this.queueTransaction( this.contracts.Staking, 'batchClaimDelegatorRewards', [identityId, epochs, delegators], (result) => { if (result.error) { reject(result.error); } else { resolve({ success: true, result: result.result, }); } }, ); }); } async getAssetStorageContractsAddress() { return Object.keys(this.assetStorageContracts); } // SUPPORT FOR OLD CONTRACTS async getLatestAssertionId(assetContractAddress, tokenId) { const assetStorageContractInstance = this.assetStorageContracts[assetContractAddress.toString().toLowerCase()]; if (!assetStorageContractInstance) throw new Error('Unknown asset storage contract address'); return this.callContractFunction(assetStorageContractInstance, 'getLatestAssertionId', [ tokenId, ]); } } export default Web3Service;