diff --git a/hapi/src/config/eos.config.js b/hapi/src/config/eos.config.js index 53d2a5ad..3e941363 100644 --- a/hapi/src/config/eos.config.js +++ b/hapi/src/config/eos.config.js @@ -46,6 +46,7 @@ module.exports = { nodeTable: process.env.HAPI_EOS_BP_JSON_ON_CHAIN_TABLE2 || 'node' }, knownNetworks: { + fio: 'fio', lacchain: 'lacchain', libre: 'libre', telos: 'telos', diff --git a/hapi/src/services/cpu.service.js b/hapi/src/services/cpu.service.js index a3f22c23..8f935274 100644 --- a/hapi/src/services/cpu.service.js +++ b/hapi/src/services/cpu.service.js @@ -6,7 +6,7 @@ const { } = require('../utils') const { eosConfig } = require('../config') -const saveBenchmark = async payload => { +const saveBenchmark = async (payload) => { const mutation = ` mutation ($account: String!, $usage: Int!) { insert_cpu_one (object: {account: $account, usage: $usage}) { @@ -39,7 +39,11 @@ const cleanOldBenchmarks = async () => { } const worker = async () => { - if (!eosConfig.eosmechanics.account || !eosConfig.eosmechanics.password) { + if ( + eosConfig.eosmechanics.account === ' ' || + !eosConfig.eosmechanics.account || + !eosConfig.eosmechanics.password + ) { return } @@ -49,7 +53,7 @@ const worker = async () => { await saveBenchmark({ account: block.producer, usage: block?.transactions.find( - trx => trx?.trx?.id === transaction.processed.id + (trx) => trx?.trx?.id === transaction.processed.id ).cpu_usage_us }) } catch (error) { diff --git a/hapi/src/services/fio.service.js b/hapi/src/services/fio.service.js new file mode 100644 index 00000000..6a0e16cd --- /dev/null +++ b/hapi/src/services/fio.service.js @@ -0,0 +1,256 @@ +const { StatusCodes } = require('http-status-codes') + +const { axiosUtil, eosUtil, sequelizeUtil, producerUtil } = require('../utils') +const { eosConfig } = require('../config') + +const getProducers = async () => { + let producers = [] + let totalVoteWeight + let hasMore = true + let nextKey + + try { + while (hasMore) { + const { + rows, + more + } = await eosUtil.getTableRows({ + code: 'eosio', + table: 'producers', + scope: 'eosio', + limit: 100, + json: true, + lower_bound: nextKey + }) + + hasMore = !!more + nextKey = more + producers.push(...rows) + } + + const { + rows, + } = await eosUtil.getTableRows({ + code: 'eosio', + table: 'global', + scope: 'eosio', + limit: 1, + json: true, + lower_bound: nextKey + }) + + totalVoteWeight = parseFloat(rows?.at(0)?.total_producer_vote_weight) + } catch (error) { + console.error('PRODUCER SYNC ERROR', error) + producers = await getProducersFromDB() + + return await getBPJsons(producers) + } + producers = producers + .filter(producer => !!producer.is_active) + .sort((a, b) => { + if (parseInt(a.total_votes) > parseInt(b.total_votes)) { + return -1 + } + + if (parseInt(a.total_votes) < parseInt(b.total_votes)) { + return 1 + } + + return 0 + }) + + const rewards = await producerUtil.getExpectedRewards( + producers, + totalVoteWeight + ) + const nonPaidStandby = { vote_rewards: 0, block_rewards: 0, total_rewards: 0 } + + producers = producers.map((producer, index) => { + return { + owner: producer.owner, + ...(rewards[producer.owner] || nonPaidStandby), + total_votes: producer.total_votes, + total_votes_percent: producer.total_votes / totalVoteWeight, + total_votes_eos: producerUtil.getVotes(producer.total_votes), + rank: index + 1, + producer_key: producer.producer_public_key, + url: producer.url, + unpaid_blocks: producer.unpaid_blocks, + last_claim_time: producer.last_claim_time, + location: producer.location, + producer_authority: producer.producer_authority, + is_active: !!producer.is_active, + fio_address: producer.fio_address, + addresshash: producer.addresshash, + last_bpclaim: producer.last_bpclaim?.toString() || '0' + } + }) + producers = await getBPJsons(producers) + + return producers +} + +const getBPJsons = async (producers = []) => { + const isEosNetwork = eosConfig.chainId === eosConfig.eosChainId + let topProducers = producers.slice(0, eosConfig.eosTopLimit) + + topProducers = await Promise.all( + topProducers.map(async producer => { + let bpJson = {} + let bpJsonUrl = '' + let healthStatus = [] + + if (producer.url && producer.url.length > 3) { + const producerUrl = getProducerUrl(producer) + const chains = await getChains(producerUrl) + const chainUrl = chains[eosConfig.chainId] + bpJsonUrl = getBPJsonUrl(producerUrl, chainUrl || '/bp.json') + + try { + bpJson = await getBPJson(bpJsonUrl) + } catch (error) { + if (error.code === 'ECONNABORTED') { + return { + ...producer, + bp_json_url: bpJsonUrl, + health_status: healthStatus, + bp_json: bpJson + } + } + } + + if (bpJson && !chainUrl && !isEosNetwork) { + const { org, producer_account_name: name } = bpJson + + bpJson = { + ...(org && { org }), + ...(name && { producer_account_name: name }) + } + } + + healthStatus = await getProducerHealthStatus({ + ...producer, + producerUrl, + bpJson, + }) + } + + return { + ...producer, + bp_json_url: bpJsonUrl, + health_status: healthStatus, + bp_json: bpJson + } + }) + ) + + return topProducers.concat(producers.slice(eosConfig.eosTopLimit)) +} + +const getBPJsonUrl = (producerUrl, chainUrl) => { + return `${producerUrl}/${chainUrl}`.replace(/(?<=:\/\/.*)((\/\/))/, '/') +} + +const getBPJson = async bpJsonUrl => { + const { data: _bpJson } = await axiosUtil.instance.get(bpJsonUrl) + const bpJson = !!_bpJson && typeof _bpJson === 'object' ? _bpJson : {} + + return bpJson +} + +const getProducerUrl = producer => { + let producerUrl = producer?.url?.replace(/(“|'|”|")/g, '') || '' + + if (!producerUrl.startsWith('http')) { + producerUrl = `http://${producerUrl}` + } + + return producerUrl +} + +const getChains = async producerUrl => { + const chainsUrl = `${producerUrl}/chains.json`.replace( + /(?<=:\/\/.*)((\/\/))/, + '/' + ) + + try { + const { + data: { chains } + } = await axiosUtil.instance.get(chainsUrl) + + return chains ?? {} + } catch (error) { + return {} + } +} + +const isNonCompliant = producer => { + return !Object.keys(producer.bpJson).length && producer.total_rewards >= 100 +} + +const getProducerHealthStatus = async producer => { + const healthStatus = [] + const {bpJson, producerUrl} = producer + + if (isNonCompliant(producer)) { + const response = await producerUtil.getUrlStatus(producerUrl) + + healthStatus.push({ name: 'bpJson', valid: false }) + healthStatus.push({ + name: 'website', + valid: response?.status === StatusCodes.OK, + response: { + status: response?.status, + statusText: response?.statusText || 'No response' + } + }) + + return healthStatus + } + + if (!bpJson || !Object.keys(bpJson).length) return [] + + healthStatus.push({ + name: 'bpJson', + valid: true + }) + healthStatus.push({ + name: 'organization_name', + valid: !!bpJson.org?.candidate_name + }) + healthStatus.push({ + name: 'email', + valid: !!bpJson.org?.email + }) + healthStatus.push({ + name: 'website', + valid: !!bpJson.org?.website + }) + healthStatus.push({ + name: 'logo_256', + valid: !!bpJson?.org?.branding?.logo_256 + }) + healthStatus.push({ + name: 'country', + valid: !!bpJson?.org?.location?.country + }) + + return healthStatus +} + +const getProducersFromDB = async () => { + const [producers] = await sequelizeUtil.query(` + SELECT * + FROM producer + ORDER BY rank ASC + ; + `) + + return producers +} + +module.exports = { + getProducers +} diff --git a/hapi/src/services/fioProducers.js b/hapi/src/services/fioProducers.js deleted file mode 100644 index 00605fba..00000000 --- a/hapi/src/services/fioProducers.js +++ /dev/null @@ -1,84 +0,0 @@ -const axios = require('axios'); - -// FIO API endpoint (replace with the correct endpoint if needed) -const FIO_API_URL = 'https://testnet.fioprotocol.io/v1/chain/get_table_rows'; - -const getProducers = async () => { - let producers = []; - let totalVoteWeight; - let hasMore = true; - let nextKey; - - try { - while (hasMore) { - const response = await axios.post(FIO_API_URL, { - code: 'fio.system', // Contract name - table: 'producers', // Table name - scope: 'fio', // Scope - limit: 100, - json: true, - lower_bound: nextKey - }); - - if (response.status !== 200) { - throw new Error(`Request failed with status code ${response.status}`); - } - - const { - rows, - more, - total_producer_vote_weight: _totalVoteWeight - } = response.data; - - if (!rows) { - throw new Error('Response data does not contain rows'); - } - - hasMore = !!more; - nextKey = more; - totalVoteWeight = parseFloat(_totalVoteWeight); - producers.push(...rows); - } - } catch (error) { - console.error('PRODUCER SYNC ERROR', error.message); - return; - } - - producers = producers - .filter(producer => !!producer.is_active) - .sort((a, b) => { - if (parseFloat(a.total_votes) > parseFloat(b.total_votes)) { - return -1; - } - - if (parseFloat(a.total_votes) < parseFloat(b.total_votes)) { - return 1; - } - - return 0; - }); - - producers = producers.map((producer, index) => { - return { - id: producer.id, - owner: producer.owner, - fio_address: producer.fio_address, - total_votes: producer.total_votes, - total_votes_percent: producer.total_votes / totalVoteWeight, - total_votes_eos: producer.total_votes, - rank: index + 1, - producer_public_key: producer.producer_public_key, - url: producer.url, - unpaid_blocks: producer.unpaid_blocks, - last_claim_time: producer.last_claim_time, - location: producer.location, - is_active: !!producer.is_active - }; - }); - - console.log(producers); - return producers; -} - -// Call the function to test it -getProducers(); diff --git a/hapi/src/services/producer.service.js b/hapi/src/services/producer.service.js index 530b28ce..ec0244ad 100644 --- a/hapi/src/services/producer.service.js +++ b/hapi/src/services/producer.service.js @@ -4,6 +4,7 @@ const { hasuraUtil, sequelizeUtil, producerUtil } = require('../utils') const { eosConfig, workersConfig } = require('../config') const lacchainService = require('./lacchain.service') +const fioService = require('./fio.service') const eosioService = require('./eosio.service') const nodeService = require('./node.service') const statsService = require('./stats.service') @@ -24,7 +25,7 @@ const updateBPJSONs = async (producers = []) => { const updateProducers = async (producers = []) => { const upsertMutation = ` mutation ($producers: [producer_insert_input!]!) { - insert_producer(objects: $producers, on_conflict: {constraint: producer_owner_key, update_columns: [ producer_key, unpaid_blocks,last_claim_time, url, location, producer_authority, is_active, total_votes, total_votes_percent, total_votes_eos, vote_rewards,block_rewards, total_rewards, endpoints, rank, bp_json_url]}) { + insert_producer(objects: $producers, on_conflict: {constraint: producer_owner_key, update_columns: [ producer_key, unpaid_blocks,last_claim_time, url, location, producer_authority, is_active, total_votes, total_votes_percent, total_votes_eos, vote_rewards,block_rewards, total_rewards, endpoints, rank, bp_json_url, fio_address, addresshash, last_bpclaim]}) { affected_rows, returning { id, @@ -84,6 +85,9 @@ const syncProducers = async () => { case eosConfig.knownNetworks.lacchain: producers = await lacchainService.getProducers() break + case eosConfig.knownNetworks.fio: + producers = await fioService.getProducers() + break default: producers = await eosioService.getProducers() break diff --git a/hapi/src/utils/eos.util.js b/hapi/src/utils/eos.util.js index 0fc0d5f8..51b12f84 100644 --- a/hapi/src/utils/eos.util.js +++ b/hapi/src/utils/eos.util.js @@ -218,7 +218,7 @@ const getCurrencyBalance = (code, account, symbol) => eosApi.getCurrencyBalance(code, account, symbol) const getTableRows = options => - eosApi.getTableRows({ json: true, ...options }) + callEosApi('getTableRows', async eosApi => eosApi.getTableRows({ json: true, ...options })) const getProducerSchedule = () => eosApi.getProducerSchedule({}) diff --git a/hapi/src/utils/producer.util.js b/hapi/src/utils/producer.util.js index b0a10b23..eb9a393d 100644 --- a/hapi/src/utils/producer.util.js +++ b/hapi/src/utils/producer.util.js @@ -126,6 +126,9 @@ const getExpectedRewards = async (producers, totalVotes) => { case eosConfig.knownNetworks.telos: rewards = await getTelosRewards(producers) break + case eosConfig.knownNetworks.fio: + rewards = await getFioRewards(producers) + break default: rewards = await getEOSIORewards(producers, totalVotes) break @@ -278,6 +281,12 @@ const getEOSIORewards = async (producers, totalVotes) => { return producersRewards } +const getFioRewards = async (producers) => { + const producersRewards = [] + // ToDo : Calculate producer Rewards Based on FIO System Contracts + return producersRewards +} + const getVotes = (votes) => { switch (eosConfig.networkName) { case eosConfig.knownNetworks.telos: diff --git a/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/down.sql b/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/down.sql new file mode 100644 index 00000000..f780b3ee --- /dev/null +++ b/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."producer" add column "fio_address" varchar +-- null unique; diff --git a/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/up.sql b/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/up.sql new file mode 100644 index 00000000..411304c3 --- /dev/null +++ b/hasura/migrations/default/1719953232361_alter_table_public_producer_add_column_fio_address/up.sql @@ -0,0 +1,2 @@ +alter table "public"."producer" add column "fio_address" varchar + null unique; diff --git a/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/down.sql b/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/down.sql new file mode 100644 index 00000000..099a3d2c --- /dev/null +++ b/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."producer" add column "addresshash" varchar +-- null; diff --git a/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/up.sql b/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/up.sql new file mode 100644 index 00000000..7b6e801f --- /dev/null +++ b/hasura/migrations/default/1719953258027_alter_table_public_producer_add_column_addresshash/up.sql @@ -0,0 +1,2 @@ +alter table "public"."producer" add column "addresshash" varchar + null; diff --git a/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/down.sql b/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/down.sql new file mode 100644 index 00000000..588d8388 --- /dev/null +++ b/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."producer" add column "last_bpclaim" varchar +-- null; diff --git a/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/up.sql b/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/up.sql new file mode 100644 index 00000000..b4d7099c --- /dev/null +++ b/hasura/migrations/default/1719953328741_alter_table_public_producer_add_column_last_bpclaim/up.sql @@ -0,0 +1,2 @@ +alter table "public"."producer" add column "last_bpclaim" varchar + null; diff --git a/makefile b/makefile index dac1d2bb..11c7c659 100644 --- a/makefile +++ b/makefile @@ -90,7 +90,7 @@ stop: start: make start-postgres - make start-wallet +# make start-wallet make start-hapi # make start-hapi-evm make start-hasura diff --git a/webapp/src/components/ProducersChart/index.js b/webapp/src/components/ProducersChart/index.js index 0c61d5d6..eeff10f6 100644 --- a/webapp/src/components/ProducersChart/index.js +++ b/webapp/src/components/ProducersChart/index.js @@ -183,7 +183,7 @@ const CustomTooltip = memo(({ active, payload }) => { {t('name')}:{' '} {' '} - {payload[0].payload.owner} + {payload[0].payload.name || payload[0].payload.owner} {generalConfig.useRewards && ( diff --git a/webapp/src/gql/producer.gql.js b/webapp/src/gql/producer.gql.js index cd0344a1..8e94134d 100644 --- a/webapp/src/gql/producer.gql.js +++ b/webapp/src/gql/producer.gql.js @@ -35,6 +35,7 @@ export const PRODUCERS_QUERY = gql` type value } + fio_address } } ` diff --git a/webapp/src/routes/Home/BlockProducerInfo.js b/webapp/src/routes/Home/BlockProducerInfo.js index 7510fdbc..bae28dfe 100644 --- a/webapp/src/routes/Home/BlockProducerInfo.js +++ b/webapp/src/routes/Home/BlockProducerInfo.js @@ -48,7 +48,8 @@ const BlockProducerInfo = ({ t, classes }) => { return { logo: data?.bp_json?.org?.branding?.logo_256, url: data?.url, - owner: item.producer_name || data.owner, + name: data?.fio_address || data?.bp_json?.producer_account_name, + owner: item.producer_name || data.owner, rewards: data.total_rewards || 0, total_votes_percent: data.total_votes_percent * 100 || 0, value: 20, @@ -88,7 +89,7 @@ const BlockProducerInfo = ({ t, classes }) => { header lowercase title={t('currentProducer')} - value={info.head_block_producer} + value={schedule.producers?.find(p => p.owner === info.head_block_producer)?.name || info.head_block_producer} />