diff --git a/feeds-ui/src/components/networkGraph/NetworkGraph.component.js b/feeds-ui/src/components/networkGraph/NetworkGraph.component.js index 9e8e21e85a5..66cc325f0b2 100644 --- a/feeds-ui/src/components/networkGraph/NetworkGraph.component.js +++ b/feeds-ui/src/components/networkGraph/NetworkGraph.component.js @@ -81,6 +81,7 @@ const NetworkGraph = ({
+
diff --git a/feeds-ui/src/components/networkGraph/NetworkGraph.d3.js b/feeds-ui/src/components/networkGraph/NetworkGraph.d3.js index c04398fc2a7..907c7e4b628 100644 --- a/feeds-ui/src/components/networkGraph/NetworkGraph.d3.js +++ b/feeds-ui/src/components/networkGraph/NetworkGraph.d3.js @@ -40,7 +40,11 @@ export default class NetworkGraph { .style('opacity', 0) } - updateNodes(nodes) { + updateNodes(updatedNodes) { + // Prevent from update redux store + // Fix it after D3 refactor + const nodes = JSON.parse(JSON.stringify(updatedNodes)) + const updateData = this.svg .select('.network-graph__nodes') .selectAll('g.network-graph__node-group') @@ -189,6 +193,7 @@ export default class NetworkGraph { this.oracleTooltip.select('.price').text('') this.oracleTooltip.select('.date').text('') this.oracleTooltip.select('.block').text('') + this.oracleTooltip.select('.gas').text('') this.oracleTooltip.select('.name').text(() => d.name) @@ -211,6 +216,10 @@ export default class NetworkGraph { this.oracleTooltip .select('.block') .text(() => `Block: ${d.state.meta.blockNumber}`) + + this.oracleTooltip + .select('.gas') + .text(() => `Gas Price (Gwei): ${d.state.meta.gasPrice}`) } } diff --git a/feeds-ui/src/components/oracleTable/OracleTable.component.js b/feeds-ui/src/components/oracleTable/OracleTable.component.js new file mode 100644 index 00000000000..abe580c7bb2 --- /dev/null +++ b/feeds-ui/src/components/oracleTable/OracleTable.component.js @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from 'react' +import { humanizeUnixTimestamp } from 'utils' +import _ from 'lodash' +import { Table, Icon } from 'antd' + +const OracleTable = ({ + networkGraphState, + networkGraphNodes, + fetchEthGasPrice, + ethGasPrice, +}) => { + const [data, setData] = useState() + const [gasPrice, setGasPrice] = useState() + + useEffect(() => { + fetchEthGasPrice() + }, [fetchEthGasPrice]) + + useEffect(() => { + if (ethGasPrice) { + setGasPrice(ethGasPrice.fast / 10) + } + }, [ethGasPrice]) + + useEffect(() => { + const mergedData = networkGraphNodes + .filter(node => node.type === 'oracle') + .map(oracle => { + const state = _.find(networkGraphState, { sender: oracle.address }) + return { + oracle: oracle, + state, + key: oracle.id, + } + }) + setData(mergedData) + }, [networkGraphState, networkGraphNodes]) + + const columns = [ + { + title: 'Oracle', + dataIndex: 'oracle.name', + key: 'name', + sorter: (a, b) => + a.oracle.name.localeCompare(b && b.oracle && b.oracle.name), + }, + { + title: 'Answer', + dataIndex: 'state.responseFormatted', + key: 'answer', + sorter: (a, b) => { + if (!a.state || !b.state) return + + return a.state.responseFormatted - b.state.responseFormatted + }, + }, + { + title: 'Gas Price (Gwei)', + dataIndex: 'state.meta.gasPrice', + key: 'gas', + sorter: (a, b) => { + if (!a.state || !b.state) return + return a.state.meta.gasPrice - b.state.meta.gasPrice + }, + defaultSortOrder: 'descend', + }, + { + title: 'Date', + dataIndex: 'state.meta.timestamp', + key: 'timestamp', + render: timestamp => humanizeUnixTimestamp(timestamp), + }, + ] + + return ( +
+

Oracles data

+
+

+ Recommended gas price:{' '} + {gasPrice} Gwei +

+
+ }} + /> + + ) +} + +export default OracleTable diff --git a/feeds-ui/src/components/oracleTable/OracleTable.enhanced.js b/feeds-ui/src/components/oracleTable/OracleTable.enhanced.js new file mode 100644 index 00000000000..141d2eaabca --- /dev/null +++ b/feeds-ui/src/components/oracleTable/OracleTable.enhanced.js @@ -0,0 +1,19 @@ +import OracleTable from './OracleTable.component' +import { connect } from 'react-redux' + +import { + aggregationSelectors, + aggregationOperations, +} from 'state/ducks/aggregation' + +const mapStateToProps = state => ({ + networkGraphNodes: aggregationSelectors.networkGraphNodes(state), + networkGraphState: aggregationSelectors.networkGraphState(state), + ethGasPrice: state.aggregation.ethGasPrice, +}) + +const mapDispatchToProps = { + fetchEthGasPrice: aggregationOperations.fetchEthGasPrice, +} + +export default connect(mapStateToProps, mapDispatchToProps)(OracleTable) diff --git a/feeds-ui/src/components/oracleTable/index.js b/feeds-ui/src/components/oracleTable/index.js new file mode 100644 index 00000000000..3326c2baa30 --- /dev/null +++ b/feeds-ui/src/components/oracleTable/index.js @@ -0,0 +1 @@ +export { default as OracleTable } from './OracleTable.enhanced' diff --git a/feeds-ui/src/contracts/AggregationContract.js b/feeds-ui/src/contracts/AggregationContract.js index 58e6d48a485..bb26f1c08c6 100644 --- a/feeds-ui/src/contracts/AggregationContract.js +++ b/feeds-ui/src/contracts/AggregationContract.js @@ -116,6 +116,19 @@ export default class AggregationContract { }) } + async addGasPriceToLogs(logs) { + if (!logs) return logs + + const logsWithGasPriceOps = logs.map(async log => { + const tx = await this.provider.getTransaction(log.meta.transactionHash) + // eslint-disable-next-line require-atomic-updates + log.meta.gasPrice = ethers.utils.formatUnits(tx.gasPrice, 'gwei') + return log + }) + + return Promise.all(logsWithGasPriceOps) + } + async oracleResponseLogs({ answerId, fromBlock }) { const answerIdHex = ethers.utils.hexlify(answerId) @@ -198,7 +211,11 @@ export default class AggregationContract { }), ) const logWithTimestamp = await this.addBlockTimestampToLogs([logged]) - return callback ? callback(logWithTimestamp[0]) : logWithTimestamp[0] + const logWithGasPrice = await this.addGasPriceToLogs( + logWithTimestamp, + ).then(l => l[0]) + + return callback ? callback(logWithGasPrice) : logWithGasPrice }), ) } diff --git a/feeds-ui/src/pages/EthUsdPage.js b/feeds-ui/src/pages/EthUsdPage.js index fa732eb3222..bf52c463d02 100644 --- a/feeds-ui/src/pages/EthUsdPage.js +++ b/feeds-ui/src/pages/EthUsdPage.js @@ -8,6 +8,7 @@ import { NetworkGraph } from 'components/networkGraph' import { NetworkGraphInfo } from 'components/networkGraphInfo' import { AnswerHistory } from 'components/answerHistory' import { DeviationHistory } from 'components/deviationHistory' +import { OracleTable } from 'components/oracleTable' const OPTIONS = { contractAddress: '0x79fEbF6B9F76853EDBcBc913e6aAE8232cFB9De9', @@ -42,6 +43,7 @@ const NetworkPage = ({ initContract, clearState }) => { + ) } diff --git a/feeds-ui/src/pages/Ropsten.js b/feeds-ui/src/pages/Ropsten.js index 8b07261ef0d..e4f75757e66 100644 --- a/feeds-ui/src/pages/Ropsten.js +++ b/feeds-ui/src/pages/Ropsten.js @@ -6,6 +6,7 @@ import { NetworkGraphInfo } from 'components/networkGraphInfo' import { AnswerHistory } from 'components/answerHistory' import { DeviationHistory } from 'components/deviationHistory' import withRopsten from 'enhancers/withRopsten' +import { OracleTable } from 'components/oracleTable' const NetworkPage = ({ initContract, clearState, options }) => { useEffect(() => { @@ -25,6 +26,7 @@ const NetworkPage = ({ initContract, clearState, options }) => { + ) } diff --git a/feeds-ui/src/state/ducks/aggregation/actions.js b/feeds-ui/src/state/ducks/aggregation/actions.js index 5ec91118c67..30922358624 100755 --- a/feeds-ui/src/state/ducks/aggregation/actions.js +++ b/feeds-ui/src/state/ducks/aggregation/actions.js @@ -63,3 +63,8 @@ export const setOptions = payload => ({ export const clearState = () => ({ type: types.CLEAR_STATE, }) + +export const setEthGasPrice = payload => ({ + type: types.ETHGAS_PRICE, + payload, +}) diff --git a/feeds-ui/src/state/ducks/aggregation/operations.js b/feeds-ui/src/state/ducks/aggregation/operations.js index e2bef80030f..1f13b14b14e 100755 --- a/feeds-ui/src/state/ducks/aggregation/operations.js +++ b/feeds-ui/src/state/ducks/aggregation/operations.js @@ -65,10 +65,16 @@ const fetchOracleResponseById = request => { const logs = await contractInstance.oracleResponseLogs(request) const withTimestamp = await contractInstance.addBlockTimestampToLogs(logs) + const withGasAndTimeStamp = await contractInstance.addGasPriceToLogs( + withTimestamp, + ) - const uniquePayload = _.uniqBy([...withTimestamp, ...currentLogs], l => { - return l.sender - }) + const uniquePayload = _.uniqBy( + [...withGasAndTimeStamp, ...currentLogs], + l => { + return l.sender + }, + ) dispatch(actions.setOracleResponse(uniquePayload)) } catch { @@ -312,4 +318,16 @@ const fetchJobId = address => { } } -export { initContract, clearState, fetchJobId } +const fetchEthGasPrice = () => { + return async dispatch => { + try { + const data = await fetch('https://ethgasstation.info/json/ethgasAPI.json') + const jsonData = await data.json() + dispatch(actions.setEthGasPrice(jsonData)) + } catch { + console.error('Could not fetch gas price') + } + } +} + +export { initContract, clearState, fetchJobId, fetchEthGasPrice } diff --git a/feeds-ui/src/state/ducks/aggregation/reducers.js b/feeds-ui/src/state/ducks/aggregation/reducers.js index e8bda73aa4e..b2882b93bd7 100755 --- a/feeds-ui/src/state/ducks/aggregation/reducers.js +++ b/feeds-ui/src/state/ducks/aggregation/reducers.js @@ -13,6 +13,7 @@ export const initialState = { minimumResponses: null, updateHeight: null, answerHistory: null, + ethGasPrice: null, } function clearState(state) { @@ -101,6 +102,12 @@ const reducer = (state = initialState, action) => { answerHistory: action.payload, } + case types.ETHGAS_PRICE: + return { + ...state, + ethGasPrice: action.payload, + } + default: return state } diff --git a/feeds-ui/src/state/ducks/aggregation/types.js b/feeds-ui/src/state/ducks/aggregation/types.js index 84d5755e2de..285f45b825b 100755 --- a/feeds-ui/src/state/ducks/aggregation/types.js +++ b/feeds-ui/src/state/ducks/aggregation/types.js @@ -12,3 +12,4 @@ export const REQUEST_TIME = 'aggregation/REQUEST_TIME' export const MINUMUM_RESPONSES = 'aggregation/MINUMUM_RESPONSES' export const UPDATE_HEIGHT = 'aggregation/UPDATE_HEIGHT' export const ANSWER_HISTORY = 'aggregation/ANSWER_HISTORY' +export const ETHGAS_PRICE = 'aggregation/ETHGAS_PRICE' diff --git a/feeds-ui/src/theme.css b/feeds-ui/src/theme.css index d1ae42c4c2d..64926578e24 100644 --- a/feeds-ui/src/theme.css +++ b/feeds-ui/src/theme.css @@ -17338,7 +17338,7 @@ span.ant-radio + * { color: rgba(0, 0, 0, 0.85); font-weight: 500; text-align: left; - background: #fafafa; + background: #f1f1f1; border-bottom: 1px solid #e8e8e8; transition: background 0.3s ease; } @@ -17424,12 +17424,12 @@ span.ant-radio + * { .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters .anticon-filter.ant-table-filter-open, .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters .ant-table-filter-icon.ant-table-filter-open { color: rgba(0, 0, 0, 0.45); - background: #e5e5e5; + background: #dddddd; } .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:hover, .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:hover { color: rgba(0, 0, 0, 0.45); - background: #e5e5e5; + background: #dddddd; } .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:active, .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:active { @@ -17439,11 +17439,11 @@ span.ant-radio + * { cursor: pointer; } .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-sorters:hover { - background: #f2f2f2; + background: #e9e9e9; } .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .anticon-filter, .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .ant-table-filter-icon { - background: #f2f2f2; + background: #e9e9e9; } .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-up:not(.on), .ant-table-thead > tr > th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-down:not(.on) { @@ -17500,11 +17500,11 @@ span.ant-radio + * { .ant-table-tbody > tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) > td, .ant-table-thead > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) > td, .ant-table-tbody > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) > td { - background: #f0f6ff; + background: #e6e6e6; } .ant-table-thead > tr.ant-table-row-selected > td.ant-table-column-sort, .ant-table-tbody > tr.ant-table-row-selected > td.ant-table-column-sort { - background: #fafafa; + background: #e6e6e6; } .ant-table-thead > tr:hover.ant-table-row-selected > td, .ant-table-tbody > tr:hover.ant-table-row-selected > td { @@ -17512,7 +17512,7 @@ span.ant-radio + * { } .ant-table-thead > tr:hover.ant-table-row-selected > td.ant-table-column-sort, .ant-table-tbody > tr:hover.ant-table-row-selected > td.ant-table-column-sort { - background: #fafafa; + background: #e6e6e6; } .ant-table-thead > tr:hover { background: none; @@ -17570,7 +17570,7 @@ span.ant-radio + * { background: #fafafa; } .ant-table-thead > tr > th.ant-table-column-sort { - background: #f5f5f5; + background: #f1f1f1; } .ant-table-tbody > tr > td.ant-table-column-sort { background: rgba(0, 0, 0, 0.01); @@ -17587,7 +17587,7 @@ span.ant-radio + * { } .ant-table-header { overflow: hidden; - background: #fafafa; + background: #f1f1f1; } .ant-table-header table { border-radius: 4px 4px 0 0; @@ -19531,7 +19531,7 @@ textarea.ant-time-picker-input { border-radius: 0; } .ant-transfer-customize-list .ant-table-wrapper .ant-table-small > .ant-table-content > .ant-table-body > table > .ant-table-thead > tr > th { - background: #fafafa; + background: #f1f1f1; } .ant-transfer-customize-list .ant-table-wrapper .ant-table-small > .ant-table-content .ant-table-row:last-child td { border-bottom: 1px solid #e8e8e8; @@ -21651,3 +21651,22 @@ hr { font-size: 12px; font-weight: 200; } +.oracle-table { + margin: 120px 0; +} +.oracle-table-header { + font-weight: 200; + position: relative; +} +.oracle-table .ant-table-placeholder { + background: transparent; +} +.oracle-table .gas-price-info { + padding: 20px; + margin: 20px 0; + border: 1px solid #d0d0d0; + font-size: 16px; +} +.oracle-table .gas-price-info h4 { + margin: 0; +} diff --git a/feeds-ui/src/theme/network-graph.less b/feeds-ui/src/theme/network-graph.less index 9ca0f0cd577..868cfb28381 100644 --- a/feeds-ui/src/theme/network-graph.less +++ b/feeds-ui/src/theme/network-graph.less @@ -345,3 +345,26 @@ font-weight: 200; } } + +.oracle-table { + margin: 120px 0; + + &-header { + font-weight: 200; + position: relative; + } + + .ant-table-placeholder { + background: transparent; + } + + .gas-price-info { + padding: 20px; + margin: 20px 0; + border: 1px solid #d0d0d0; + font-size: 16px; + h4 { + margin: 0; + } + } +} diff --git a/feeds-ui/src/theme/theme.less b/feeds-ui/src/theme/theme.less index 878bac7ae0c..bb7ecb66c6b 100644 --- a/feeds-ui/src/theme/theme.less +++ b/feeds-ui/src/theme/theme.less @@ -5,3 +5,7 @@ @primary-color: #2a59da; @link-color: #2a59da; +@table-row-hover-bg: #e6e6e6; +@table-header-bg: #f1f1f1; +@table-body-selected-sort-bg: #e6e6e6; +@table-header-sort-bg: #f1f1f1;