diff --git a/.github/workflows/deploy-jungle-testnet.yaml b/.github/workflows/deploy-jungle-testnet.yaml index 3d0068b9..57e94f75 100644 --- a/.github/workflows/deploy-jungle-testnet.yaml +++ b/.github/workflows/deploy-jungle-testnet.yaml @@ -102,6 +102,9 @@ jobs: HAPI_EOS_EXCHANGE_RATE_API: https://api.coingecko.com/api/v3/simple/price?ids=eos&vs_currencies=usd HAPI_COINGECKO_API_TOKEN_ID: eos HAPI_REWARDS_TOKEN: EOS + HAPI_EOSRATE_GET_STATS_URL: ${{ secrets.HAPI_EOSRATE_GET_STATS_URL }} + HAPI_EOSRATE_GET_STATS_USER: ${{ secrets.HAPI_EOSRATE_GET_STATS_USER }} + HAPI_EOSRATE_GET_STATS_PASSWORD: ${{ secrets.HAPI_EOSRATE_GET_STATS_PASSWORD }} # hasura HASURA_GRAPHQL_ENABLE_CONSOLE: true HASURA_GRAPHQL_DATABASE_URL: ${{ secrets.HASURA_GRAPHQL_DATABASE_URL }} diff --git a/.github/workflows/deploy-mainnet.yaml b/.github/workflows/deploy-mainnet.yaml index f7e15212..967a8b3d 100644 --- a/.github/workflows/deploy-mainnet.yaml +++ b/.github/workflows/deploy-mainnet.yaml @@ -100,6 +100,9 @@ jobs: HAPI_REWARDS_TOKEN: EOS HAPI_RE_CAPTCHA_PROJECT_ID: ${{ secrets.HAPI_RE_CAPTCHA_PROJECT_ID }} HAPI_PUBLIC_RE_CAPTCHA_KEY: ${{ secrets.HAPI_PUBLIC_RE_CAPTCHA_KEY }} + HAPI_EOSRATE_GET_STATS_URL: ${{ secrets.HAPI_EOSRATE_GET_STATS_URL }} + HAPI_EOSRATE_GET_STATS_USER: ${{ secrets.HAPI_EOSRATE_GET_STATS_USER }} + HAPI_EOSRATE_GET_STATS_PASSWORD: ${{ secrets.HAPI_EOSRATE_GET_STATS_PASSWORD }} # hasura HASURA_GRAPHQL_ENABLE_CONSOLE: 'true' diff --git a/docker-compose.yaml b/docker-compose.yaml index 81d68f9c..e2db648e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -64,6 +64,9 @@ services: HAPI_RE_CAPTCHA_PROJECT_ID: '${HAPI_RE_CAPTCHA_PROJECT_ID}' HAPI_PUBLIC_RE_CAPTCHA_KEY: '${HAPI_PUBLIC_RE_CAPTCHA_KEY}' HAPI_CREATE_ACCOUNT_ACTION_NAME: '${HAPI_CREATE_ACCOUNT_ACTION_NAME}' + HAPI_EOSRATE_GET_STATS_URL: '${HAPI_EOSRATE_GET_STATS_URL}' + HAPI_EOSRATE_GET_STATS_USER: '${HAPI_EOSRATE_GET_STATS_USER}' + HAPI_EOSRATE_GET_STATS_PASSWORD: '${HAPI_EOSRATE_GET_STATS_PASSWORD}' hasura: container_name: '${STAGE}-${APP_NAME}-hasura' image: hasura/graphql-engine:v2.16.0.cli-migrations-v3 diff --git a/hapi/src/config/eos.config.js b/hapi/src/config/eos.config.js index 738e0630..c35447db 100644 --- a/hapi/src/config/eos.config.js +++ b/hapi/src/config/eos.config.js @@ -1,15 +1,16 @@ module.exports = { networkName: process.env.HAPI_EOS_API_NETWORK_NAME, apiEndpoints: process.env.HAPI_EOS_API_ENDPOINTS - ? JSON.parse(process.env.HAPI_EOS_API_ENDPOINTS) - : [], + ? JSON.parse(process.env.HAPI_EOS_API_ENDPOINTS) + : [], apiEndpoint: process.env.HAPI_EOS_API_ENDPOINTS - ? JSON.parse(process.env.HAPI_EOS_API_ENDPOINTS)[0] - : '', + ? JSON.parse(process.env.HAPI_EOS_API_ENDPOINTS)[0] + : '', stateHistoryPluginEndpoint: process.env.HAPI_EOS_STATE_HISTORY_PLUGIN_ENDPOINT, chainId: process.env.HAPI_EOS_API_CHAIN_ID, - eosChainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906', + eosChainId: + 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906', eosTopLimit: 150, baseAccount: process.env.HAPI_EOS_BASE_ACCOUNT, baseAccountPassword: process.env.HAPI_EOS_BASE_ACCOUNT_PASSWORD, @@ -41,5 +42,8 @@ module.exports = { knownNetworks: { lacchain: 'lacchain' }, - rewardsToken: process.env.HAPI_REWARDS_TOKEN + rewardsToken: process.env.HAPI_REWARDS_TOKEN, + eosRateUrl: process.env.HAPI_EOSRATE_GET_STATS_URL, + eosRateUser: process.env.HAPI_EOSRATE_GET_STATS_USER, + eosRatePassword: process.env.HAPI_EOSRATE_GET_STATS_PASSWORD } diff --git a/hapi/src/routes/get-eos-rate-stats.route.js b/hapi/src/routes/get-eos-rate-stats.route.js new file mode 100644 index 00000000..dd0fa54c --- /dev/null +++ b/hapi/src/routes/get-eos-rate-stats.route.js @@ -0,0 +1,35 @@ +const { eosConfig } = require('../config') +const { axiosUtil } = require('../utils') + +module.exports = { + method: 'POST', + path: '/get-eos-rate', + handler: async () => { + if ( + !eosConfig.eosRateUrl || + !eosConfig.eosRateUser || + !eosConfig.eosRatePassword + ) { + return [] + } + + const buf = Buffer.from( + `${eosConfig.eosRateUser}:${eosConfig.eosRatePassword}`, + 'utf8' + ) + const auth = buf.toString('base64') + + const { data } = await axiosUtil.instance.post( + eosConfig.eosRateUrl, + { ratesStatsInput: {} }, + { + headers: { Authorization: `Basic ${auth}` } + } + ) + + return data?.getRatesStats?.bpsStats || [] + }, + options: { + auth: false + } +} diff --git a/hapi/src/routes/index.js b/hapi/src/routes/index.js index 7659c589..ebb9797a 100644 --- a/hapi/src/routes/index.js +++ b/hapi/src/routes/index.js @@ -7,6 +7,7 @@ const transactionsRoute = require('./transactions.route') const createFaucetAccountRoute = require('./create-faucet-account.route') const transferFaucetTokensRoute = require('./transfer-faucet-tokens.route') const getProducersInfoRoute = require('./get-producers-info.route') +const getEOSRateStats = require('./get-eos-rate-stats.route') module.exports = [ healthzRoute, @@ -17,5 +18,6 @@ module.exports = [ transactionsRoute, createFaucetAccountRoute, transferFaucetTokensRoute, - getProducersInfoRoute + getProducersInfoRoute, + getEOSRateStats ] diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index d0a790e8..4461801a 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -18,6 +18,10 @@ type Mutation { ): CreateAccountOutput } +type Query { + eosrate_stats: [EOSRateStats] +} + type Mutation { getProducersInfo( bpParams: GetProducersInfoInput! @@ -96,3 +100,14 @@ type GetProducersInfoOutput { producersInfo: [jsonb] } +type EOSRateStats { + ratings_cntr: Int + transparency: Float + average: Float + infrastructure: Float + bp: String + development: Float + community: Float + trustiness: Float +} + diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml index 1153922b..37b0a8a0 100644 --- a/hasura/metadata/actions.yaml +++ b/hasura/metadata/actions.yaml @@ -20,6 +20,10 @@ actions: handler: '{{HASURA_GRAPHQL_ACTION_BASE_URL}}/create-faucet-account' permissions: - role: guest + - name: eosrate_stats + definition: + kind: "" + handler: '{{HASURA_GRAPHQL_ACTION_BASE_URL}}/get-eos-rate' - name: getProducersInfo definition: kind: synchronous @@ -65,4 +69,5 @@ custom_types: - name: CreateAccountOutput - name: TransferFaucetTokensOutput - name: GetProducersInfoOutput + - name: EOSRateStats scalars: [] diff --git a/webapp/src/components/InformationCard/EmptyState.js b/webapp/src/components/InformationCard/EmptyState.js index a87198f5..d95b6c98 100644 --- a/webapp/src/components/InformationCard/EmptyState.js +++ b/webapp/src/components/InformationCard/EmptyState.js @@ -5,7 +5,7 @@ const EmptyState = ({ classes, t }) => { return (
-
+
{ const [anchorEl, setAnchorEl] = useState(null) @@ -49,12 +50,7 @@ const ProducerInformation = ({ info, classes, t }) => { {t('website')}: - - window.open(info?.website, '_blank')} - className={classes.clickableIcon} - /> - + { {t('email')}: - - (window.location = `mailto:${info.email}`)} - className={classes.clickableIcon} - /> - - + { {t('ownershipDisclosure')}: - - window.open(info?.ownership, '_blank')} - className={classes.clickableIcon} - /> - + { {t('codeofconduct')}: - - window.open(info?.code_of_conduct, '_blank')} - className={classes.clickableIcon} - /> - + { {t('chainResources')}: - - window.open(info?.chain, '_blank')} - className={classes.clickableIcon} - /> - - + { +const Stats = ({ missedBlocks, t, classes, votes, rewards, eosRate }) => { if (eosConfig.networkName === 'lacchain') return <> return ( -
+
{t('stats')}
@@ -21,6 +22,20 @@ const Stats = ({ missedBlocks, t, classes, votes, rewards, type }) => { }`}
+ {!!eosRate && ( +
+ + {`${t('EOSRate')}: + ${eosRate.average.toFixed(2)} ${t('average')} + (${eosRate.ratings_cntr} ${t('ratings')})`} + + +
+ )} + {!!generalConfig.historyEnabled && (
diff --git a/webapp/src/components/InformationCard/index.js b/webapp/src/components/InformationCard/index.js index d5e277ca..35271aaf 100644 --- a/webapp/src/components/InformationCard/index.js +++ b/webapp/src/components/InformationCard/index.js @@ -87,6 +87,7 @@ const InformationCard = ({ producer, rank, type }) => { producer.total_rewards || '0', 0, )} + eosRate={producer?.eosRate} /> ({ minWidth: 150, }, }, + ratings: { + [theme.breakpoints.up('lg')]: { + whiteSpace: 'pre-line !important', + }, + }, boxLabel: { alignItems: 'baseline !important', }, @@ -223,6 +228,15 @@ export default (theme) => ({ minWidth: 130, }, }, + stats: { + '& .MuiTypography-body1': { + margin: theme.spacing(1, 0), + display: 'flex', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + }, social: { borderLeft: 'none', width: 100, @@ -240,7 +254,7 @@ export default (theme) => ({ marginRight: theme.spacing(1), }, [theme.breakpoints.up('lg')]: { - minWidth: 150, + minWidth: 120, }, }, dd: { diff --git a/webapp/src/components/VisitSite/index.js b/webapp/src/components/VisitSite/index.js new file mode 100644 index 00000000..0004149b --- /dev/null +++ b/webapp/src/components/VisitSite/index.js @@ -0,0 +1,27 @@ +import React from 'react' +import { makeStyles } from '@mui/styles' +import Tooltip from '@mui/material/Tooltip' +import LaunchIcon from '@mui/icons-material/Launch' + +import styles from './styles' + +const useStyles = makeStyles(styles) + +const VisitSite = ({ title, url, placement = 'left' }) => { + const classes = useStyles() + + return ( + + + + + + ) +} + +export default VisitSite diff --git a/webapp/src/components/VisitSite/styles.js b/webapp/src/components/VisitSite/styles.js new file mode 100644 index 00000000..63177ea5 --- /dev/null +++ b/webapp/src/components/VisitSite/styles.js @@ -0,0 +1,14 @@ +export default (theme) => ({ + link: { + width: '24px', + height: '24px', + marginLeft: theme.spacing(3), + }, + clickableIcon: { + color: 'black', + cursor: 'pointer', + '&:hover': { + color: '#1565c0', + }, + }, +}) diff --git a/webapp/src/gql/producer.gql.js b/webapp/src/gql/producer.gql.js index 2b359d89..7d1808b9 100644 --- a/webapp/src/gql/producer.gql.js +++ b/webapp/src/gql/producer.gql.js @@ -215,3 +215,13 @@ export const ALL_NODES_QUERY = gql` } } ` + +export const EOSRATE_STATS_QUERY = gql` + query { + eosrate_stats { + bp + average + ratings_cntr + } + } +` diff --git a/webapp/src/hooks/customHooks/useBlockProducerState.js b/webapp/src/hooks/customHooks/useBlockProducerState.js index 08f90c6c..0eeee421 100644 --- a/webapp/src/hooks/customHooks/useBlockProducerState.js +++ b/webapp/src/hooks/customHooks/useBlockProducerState.js @@ -1,7 +1,11 @@ import { useState, useEffect } from 'react' -import { useSubscription } from '@apollo/client' +import { useLazyQuery, useSubscription } from '@apollo/client' -import { PRODUCERS_QUERY, BLOCK_TRANSACTIONS_HISTORY } from '../../gql' +import { + PRODUCERS_QUERY, + BLOCK_TRANSACTIONS_HISTORY, + EOSRATE_STATS_QUERY, +} from '../../gql' import { eosConfig } from '../../config' import useSearchState from './useSearchState' @@ -23,15 +27,21 @@ const CHIPS_NAMES = ['all', ...eosConfig.producerTypes] const useBlockProducerState = () => { const [ - { filters, pagination, loading, producers }, + { filters, pagination, producers }, { handleOnSearch, handleOnPageChange, setPagination }, ] = useSearchState({ query: PRODUCERS_QUERY }) const { data: dataHistory, loading: loadingHistory } = useSubscription( BLOCK_TRANSACTIONS_HISTORY, ) + const [loadStats, { loading = true, data: { eosrate_stats: stats } = {} }] = + useLazyQuery(EOSRATE_STATS_QUERY) const [items, setItems] = useState([]) const [missedBlocks, setMissedBlocks] = useState({}) + useEffect(() => { + loadStats({}) + }, [loadStats]) + const chips = CHIPS_NAMES.map((e) => { return { name: e } }) @@ -46,7 +56,11 @@ const useBlockProducerState = () => { ...prev, page: 1, ...filter, - where: { ...where, owner: prev.where?.owner, bp_json: { _is_null: false } }, + where: { + ...where, + owner: prev.where?.owner, + bp_json: { _is_null: false }, + }, })) }, [filters, setPagination]) @@ -54,11 +68,24 @@ const useBlockProducerState = () => { let newItems = producers ?? [] if (eosConfig.networkName === 'lacchain' && filters.name !== 'all') { - newItems = items.filter((producer) => producer.bp_json?.type === filters) + newItems = newItems.filter( + (producer) => producer.bp_json?.type === filters.name, + ) + } + + if (newItems?.length && stats?.length) { + newItems = newItems.map((producer) => { + return { + ...producer, + eosRate: Object.keys(producer.bp_json).length + ? stats.find((rate) => rate.bp === producer.owner) + : undefined, + } + }) } setItems(newItems) - }, [filters, producers, items]) + }, [filters.name, stats, producers]) useEffect(() => { if (dataHistory?.stats.length) { diff --git a/webapp/src/language/en.json b/webapp/src/language/en.json index 0d540a38..ff341d37 100644 --- a/webapp/src/language/en.json +++ b/webapp/src/language/en.json @@ -252,7 +252,9 @@ "peer_keys": "Peer Key", "account_key": "Account Key", "hs_bpJson": "BP Json", - "emptyState": "This block producer does not provide any information." + "emptyState": "This block producer does not provide any information.", + "average": "average rating", + "ratings": "ratings" }, "nodeCardComponent": { "features": "Features", diff --git a/webapp/src/language/es.json b/webapp/src/language/es.json index f38ca56a..fe7b00f4 100644 --- a/webapp/src/language/es.json +++ b/webapp/src/language/es.json @@ -258,7 +258,9 @@ "peer_keys": "Peer", "account_key": "Cuenta", "hs_bpJson": "BP Json", - "emptyState": "Este productor de bloques no proporciona ninguna información." + "emptyState": "Este productor de bloques no proporciona ninguna información.", + "average": "calificación promedio", + "ratings": "calificaciones" }, "nodeCardComponent": { "features": "Características",