diff --git a/backend/package.json b/backend/package.json index 70690fe..63ed9a1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,7 @@ "connect-mongo": "^3.1.2", "crypto-js": "^3.1.9-1", "esm": "^3.2.20", + "ethers": "^5.6.1", "express": "^4.17.1", "express-session": "^1.17.0", "graphql-request": "^4.0.0", diff --git a/backend/src/config.js b/backend/src/config.js index a2bec61..f1a7b04 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -1,4 +1,3 @@ -//Server configuration variables export const { NODE_PORT = 42609, NODE_ENV = 'local', @@ -17,7 +16,10 @@ export const { CONF_TIMEOUT_CMC = 360000, CONF_TIMEOUT_ALCHEMY = 2000, CONF_TIMEOUT_LIVEPEER = 60000, + CONF_TIMEOUT_ENS_DOMAIN = 86400000, + CONF_TIMEOUT_ENS_INFO = 3600000, CONF_DISABLE_SYNC = false, CONF_DISABLE_DB = false, - CONF_DISABLE_CMC = false + CONF_DISABLE_CMC = false, + CONF_DISABLE_ENS = false } = process.env; diff --git a/backend/src/routes/livepeer.js b/backend/src/routes/livepeer.js index 0a329e5..5e5054d 100644 --- a/backend/src/routes/livepeer.js +++ b/backend/src/routes/livepeer.js @@ -8,9 +8,10 @@ import { API_CMC, API_L1_HTTP, API_L2_HTTP, API_L2_WS, CONF_DEFAULT_ORCH, CONF_SIMPLE_MODE, CONF_TIMEOUT_CMC, CONF_TIMEOUT_ALCHEMY, CONF_TIMEOUT_LIVEPEER, CONF_DISABLE_SYNC, - CONF_DISABLE_DB, - CONF_DISABLE_CMC + CONF_DISABLE_DB, CONF_DISABLE_CMC, CONF_TIMEOUT_ENS_DOMAIN, + CONF_TIMEOUT_ENS_INFO, CONF_DISABLE_ENS } from "../config"; + // Do API requests to other API's const https = require('https'); // Read ABI files @@ -45,8 +46,14 @@ if (!CONF_SIMPLE_MODE) { } // For listening to blockchain events +// ENS stuff TODO: CONF_DISABLE_ENS +const { ethers } = require("ethers"); +const provider = new ethers.providers.JsonRpcProvider(API_L1_HTTP); +// const ens = new ENS({ provider: web3layer1, ensAddress: getEnsAddress('1') }); +let ensDomainCache = []; +let ensInfoCache = []; + // Update CoinMarketCap related api calls every 5 minutes -const timeoutCMC = CONF_TIMEOUT_CMC; let cmcPriceGet = 0; let ethPrice = 0; let lptPrice = 0; @@ -54,7 +61,6 @@ let cmcQuotes = {}; let cmcCache = {}; // Update Alchemy related API calls every 2 seconds -const timeoutAlchemy = CONF_TIMEOUT_ALCHEMY; let l2Gwei = 0; let l1Gwei = 0; let l2block = 0; @@ -82,8 +88,6 @@ let commissionFeeCostL2 = 0; let serviceUriFeeCostL1 = 0; let serviceUriFeeCostL2 = 0; -// Update O info from thegraph every 1 minute -const timeoutTheGraph = CONF_TIMEOUT_LIVEPEER; // Will contain addr, lastGet, and obj of any requested O's let orchestratorCache = []; // Contains delegator addr and the address of the O they are bounded to @@ -461,12 +465,12 @@ apiRouter.get("/grafana", async (req, res) => { try { const now = new Date().getTime(); // Update blockchain data if the cached data has expired - if (now - arbGet > timeoutAlchemy) { + if (now - arbGet > CONF_TIMEOUT_ALCHEMY) { await parseEthBlockchain(); arbGet = now; } // Update coin prices once their data has expired - if (now - cmcPriceGet > timeoutCMC) { + if (now - cmcPriceGet > CONF_TIMEOUT_CMC) { await parseCmc(); cmcPriceGet = now; } @@ -502,7 +506,7 @@ apiRouter.get("/cmc", async (req, res) => { try { const now = new Date().getTime(); // Update cmc once their data has expired - if (now - cmcPriceGet > timeoutCMC) { + if (now - cmcPriceGet > CONF_TIMEOUT_CMC) { cmcPriceGet = now; await parseCmc(); } @@ -517,7 +521,7 @@ apiRouter.get("/blockchains", async (req, res) => { try { const now = new Date().getTime(); // Update blockchain data if the cached data has expired - if (now - arbGet > timeoutAlchemy) { + if (now - arbGet > CONF_TIMEOUT_ALCHEMY) { arbGet = now; await parseEthBlockchain(); } @@ -551,7 +555,7 @@ apiRouter.get("/quotes", async (req, res) => { try { const now = new Date().getTime(); // Update cmc once their data has expired - if (now - cmcPriceGet > timeoutCMC) { + if (now - cmcPriceGet > CONF_TIMEOUT_CMC) { cmcPriceGet = now; await parseCmc(); } @@ -596,17 +600,18 @@ const parseOrchestrator = async function (reqAddr) { } } if (wasCached) { - if (now - orchestratorObj.lastGet < timeoutTheGraph) { + if (now - orchestratorObj.lastGet < CONF_TIMEOUT_LIVEPEER) { needsUpdate = false; } } if (!wasCached || needsUpdate) { const orchQuery = gql`{ - transcoders(where: {id: "${reqAddr}"}) { + transcoder(id: "${reqAddr}") { id activationRound deactivationRound active + status lastRewardRound { id length @@ -629,7 +634,7 @@ const parseOrchestrator = async function (reqAddr) { totalVolumeETH totalVolumeUSD serviceURI - delegators { + delegators(first: 1000) { id bondedAmount startRound @@ -643,20 +648,22 @@ const parseOrchestrator = async function (reqAddr) { } `; orchestratorObj = await request("https://api.thegraph.com/subgraphs/name/livepeer/arbitrum-one", orchQuery); - orchestratorObj = orchestratorObj.transcoders[0]; + orchestratorObj = orchestratorObj.transcoder; // Not found if (!orchestratorObj) { return {}; } orchestratorObj.lastGet = now; if (wasCached) { - for (var orch of orchestratorCache) { - if (orch.id == reqAddr) { - orch = orchestratorObj; + for (var idx = 0; idx < orchestratorCache.length; idx++) { + if (orchestratorCache[idx].id == reqAddr) { + console.log("Updating outdated orchestrator " + orchestratorObj.id + " @ " + now); + orchestratorCache[idx] = orchestratorObj; break; } } } else { + console.log("Pushing new orchestrator " + orchestratorObj.id + " @ " + now); orchestratorCache.push(orchestratorObj); } } @@ -714,7 +721,7 @@ const parseDelegator = async function (reqAddr) { } } if (wasCached) { - if (now - delegatorObj.lastGet < timeoutTheGraph) { + if (now - delegatorObj.lastGet < CONF_TIMEOUT_LIVEPEER) { needsUpdate = false; } } @@ -738,13 +745,15 @@ const parseDelegator = async function (reqAddr) { } delegatorObj.lastGet = now; if (wasCached) { - for (var delegator of delegatorCache) { - if (delegator.id == reqAddr) { - delegator = delegatorObj; + for (var idx = 0; idx < delegatorCache.length; idx++) { + if (delegatorCache[idx].id == reqAddr) { + console.log("Updating outdated delegator " + delegatorObj.id + " @ " + now); + delegatorCache[idx] = delegatorObj; break; } } } else { + console.log("Pushing new delegator " + delegatorObj.id + " @ " + now); delegatorCache.push(delegatorObj); } } @@ -804,12 +813,12 @@ apiRouter.get("/prometheus/:orchAddr", async (req, res) => { try { const now = new Date().getTime(); // Update blockchain data if the cached data has expired - if (now - arbGet > timeoutAlchemy) { + if (now - arbGet > CONF_TIMEOUT_ALCHEMY) { await parseEthBlockchain(); arbGet = now; } // Update coin prices once their data has expired - if (now - cmcPriceGet > timeoutCMC) { + if (now - cmcPriceGet > CONF_TIMEOUT_CMC) { await parseCmc(); cmcPriceGet = now; } @@ -950,4 +959,121 @@ apiRouter.get("/prometheus/:orchAddr", async (req, res) => { } }); +const getEnsDomain = async function (addr) { + const now = new Date().getTime(); + let wasInCache = false; + // See if it is cached + for (const thisAddr of ensDomainCache) { + if (thisAddr.address === addr) { + // Check timeout + if (now - thisAddr.timestamp < CONF_TIMEOUT_ENS_DOMAIN ){ + return thisAddr.domain; + } + wasInCache = true; + } + } + // Else get it and cache it + const ensDomain = await provider.lookupAddress(addr.toLowerCase()); + let ensObj; + if (!ensDomain){ + ensObj = { + domain: null, + address: addr, + timestamp: now + }; + } else { + ensObj = { + domain: ensDomain, + address: addr, + timestamp: now + }; + } + if (wasInCache){ + for (var idx = 0; idx < ensDomainCache.length; idx++) { + if (ensDomainCache[idx].address == addr) { + console.log("Updating outdated domain " + ensObj.domain + " owned by " + ensObj.address + " @ " + ensObj.timestamp); + ensDomainCache[idx] = ensObj; + break; + } + } + } else { + console.log("Caching new domain " + ensObj.domain + " owned by " + ensObj.address + " @ " + ensObj.timestamp); + ensDomainCache.push(ensObj); + } + return ensObj.domain; +} + +const getEnsInfo = async function (addr) { + const now = new Date().getTime(); + let wasInCache = false; + // See if it is cached + for (const thisAddr of ensInfoCache) { + if (thisAddr.domain === addr) { + // Check timeout + if (now - thisAddr.timestamp < CONF_TIMEOUT_ENS_INFO ){ + return thisAddr; + } + wasInCache = true; + } + } + // Else get it and cache it + const resolver = await provider.getResolver(addr); + const description = await resolver.getText("description"); + const url = await resolver.getText("url"); + const avatar = await resolver.getAvatar(); + const ensObj = { + domain: addr, + description, + url, + avatar, + timestamp: now + }; + if (wasInCache){ + for (var idx = 0; idx < ensInfoCache.length; idx++) { + if (ensInfoCache[idx].domain == addr) { + console.log("Updating outdated info " + ensObj.domain + " @ " + ensObj.timestamp); + ensInfoCache[idx] = ensObj; + break; + } + } + } else { + console.log("Caching new info " + ensObj.domain + " @ " + ensObj.timestamp); + ensInfoCache.push(ensObj); + } + return ensObj; +} + +// Gets and caches info for a single address +apiRouter.get("/getENS/:orch", async (req, res) => { + try { + // First resolve addr => domain name + const ensDomain = await getEnsDomain(req.params.orch); + if (!ensDomain){ + res.send({domain: null}); + return; + } + // Then resolve address to info + const ensInfo = await getEnsInfo(ensDomain); + res.send(ensInfo); + } catch (err) { + res.status(400).send(err); + } +}); +// Returns entire ENS domain mapping cache +apiRouter.get("/getEnsDomains", async (req, res) => { + try { + res.send(ensDomainCache); + } catch (err) { + res.status(400).send(err); + } +}); +// Returns entire ENS info mapping cache +apiRouter.get("/getEnsInfo", async (req, res) => { + try { + res.send(ensInfoCache); + } catch (err) { + res.status(400).send(err); + } +}); + export default apiRouter; \ No newline at end of file diff --git a/public/ens.png b/public/ens.png new file mode 100644 index 0000000..585dfe6 Binary files /dev/null and b/public/ens.png differ diff --git a/src/actions/livepeer.js b/src/actions/livepeer.js index e448fc2..ba3f162 100644 --- a/src/actions/livepeer.js +++ b/src/actions/livepeer.js @@ -24,6 +24,8 @@ export const RECEIVE_CURRENT_ORCHESTRATOR = "RECEIVE_CURRENT_ORCHESTRATOR"; export const RECEIVE_ORCHESTRATOR = "RECEIVE_ORCHESTRATOR"; export const CLEAR_ORCHESTRATOR = "CLEAR_ORCHESTRATOR"; export const RECEIVE_TICKETS = "RECEIVE_TICKETS"; +export const SET_ALL_ENS_INFO = "SET_ALL_ENS_INFO"; +export const SET_ALL_ENS_DOMAINS = "SET_ALL_ENS_DOMAINS"; const setQuotes = message => ({ type: RECEIVE_QUOTES, message @@ -46,6 +48,13 @@ const clearOrchestratorInfo = () => ({ const setTickets = message => ({ type: RECEIVE_TICKETS, message }); +const setAllEnsInfo = message => ({ + type: SET_ALL_ENS_INFO, message +}); +const setAllEnsDomains = message => ({ + type: SET_ALL_ENS_DOMAINS, message +}); + export const getQuotes = () => async dispatch => { const response = await apiUtil.getQuotes(); @@ -522,4 +531,28 @@ export const getOrchestratorInfo = (orchAddr) => async dispatch => { export const clearOrchestrator = () => async dispatch => { return dispatch(clearOrchestratorInfo({})); -}; \ No newline at end of file +}; + +export const getAllEnsDomains = () => async dispatch => { + const response = await apiUtil.getAllEnsDomains(); + const data = await response.json(); + if (response.ok) { + return dispatch(setAllEnsDomains(data)); + } + return dispatch(receiveErrors(data)); +}; + +export const getAllEnsInfo = () => async dispatch => { + const response = await apiUtil.getAllEnsInfo(); + const data = await response.json(); + if (response.ok) { + return dispatch(setAllEnsInfo(data)); + } + return dispatch(receiveErrors(data)); +}; + +export const getEnsInfo = async (addr) => { + const response = await apiUtil.getEnsInfo(addr); + const data = await response.json(); +}; + \ No newline at end of file diff --git a/src/components/OrchAddressViewer.js b/src/components/OrchAddressViewer.js index b9365c3..c20fbeb 100644 --- a/src/components/OrchAddressViewer.js +++ b/src/components/OrchAddressViewer.js @@ -1,14 +1,94 @@ -import React from "react"; +import React, { useState } from "react"; +import { useSelector } from 'react-redux'; +import { getEnsInfo } from "../actions/livepeer"; +import { + getOrchestratorInfo +} from "../actions/livepeer"; const Address = (obj) => { + const livepeer = useSelector((state) => state.livepeerstate); + const [hasRefreshed, setRefresh] = useState(false); + let thisDomain = null; + let thisInfo = null; + const now = new Date().getTime(); + // Lookup domain in cache + if (livepeer.ensDomainMapping){ + for (const thisAddr of livepeer.ensDomainMapping) { + if (thisAddr.address === obj.address) { + thisDomain = thisAddr; + // Check timeout + if (now - thisAddr.timestamp < 86400000) { + break; + } + // Is outdated + if (!hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + } + // If it was not cached at all + if (thisDomain == null && !hasRefreshed) { + setRefresh(true); + getEnsInfo(obj.address); + } + } + // Lookup current info in cache only if this addr has a mapped ENS domain + if (thisDomain && thisDomain.domain) { + for (const thisAddr of livepeer.ensInfoMapping) { + if (thisAddr.domain === thisDomain.domain) { + thisInfo = thisAddr; + // Check timeout + if (now - thisAddr.timestamp < 86400000) { + break; + } + // Is outdated + if (!hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + } + // If it was not cached at all + if (thisInfo == null && !hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + + let thisName; + let thisIcon; + if (thisInfo) { + thisName = thisInfo.domain; + if (thisInfo.avatar) { + thisIcon = + +
+ +
+
+ } else { + thisIcon = + +
+ +
+
+ } + } else { + thisName = obj.address; + thisIcon = null; + } + return (
- +
- {obj.address}
+ {thisIcon} + {thisName}
) } diff --git a/src/components/eventButton.js b/src/components/eventButton.js index 5f9c0f8..f8bce9a 100644 --- a/src/components/eventButton.js +++ b/src/components/eventButton.js @@ -35,9 +35,9 @@ const EventButton = (obj) => { } return ( -
+
{blockNumber} -
+
{eventCaller} diff --git a/src/components/eventButtonAddress.js b/src/components/eventButtonAddress.js index 7a6edba..dc3fab7 100644 --- a/src/components/eventButtonAddress.js +++ b/src/components/eventButtonAddress.js @@ -1,14 +1,85 @@ -import React from "react"; +import React, { useState } from "react"; import { getOrchestratorInfo } from "../actions/livepeer"; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { getEnsInfo } from "../actions/livepeer"; const EventButtonAddress = (obj) => { const dispatch = useDispatch(); + const livepeer = useSelector((state) => state.livepeerstate); + const [hasRefreshed, setRefresh] = useState(false); + let thisDomain = null; + let thisInfo = null; + const now = new Date().getTime(); + // Lookup domain in cache + if (livepeer.ensDomainMapping) { + for (const thisAddr of livepeer.ensDomainMapping) { + if (thisAddr.address === obj.address) { + thisDomain = thisAddr; + // Check timeout + if (now - thisAddr.timestamp < 86400000) { + break; + } + // Is outdated + if (!hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + } + // If it was not cached at all + if (thisDomain == null && !hasRefreshed) { + setRefresh(true); + getEnsInfo(obj.address); + } + } + // Lookup current info in cache only if this addr has a mapped ENS domain + if (thisDomain && thisDomain.domain) { + for (const thisAddr of livepeer.ensInfoMapping) { + if (thisAddr.domain === thisDomain.domain) { + thisInfo = thisAddr; + // Check timeout + if (now - thisAddr.timestamp < 86400000) { + break; + } + // Is outdated + if (!hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + } + // If it was not cached at all + if (thisInfo == null && !hasRefreshed) { + getEnsInfo(obj.address); + setRefresh(true); + } + } + + let thisName; + let thisIcon; + if (thisInfo) { + thisName =

{thisInfo.domain}

; + if (thisInfo.avatar) { + thisIcon = + + + + } else { + thisIcon = + + + + } + } else { + thisName = {obj.address}; + thisIcon = null; + } return (
+ {thisIcon} @@ -17,7 +88,7 @@ const EventButtonAddress = (obj) => { {obj.name}
) diff --git a/src/pages/livepeer.js b/src/pages/livepeer.js index 43bfc65..c537d9e 100644 --- a/src/pages/livepeer.js +++ b/src/pages/livepeer.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import '../style.css'; import { Navigate, useSearchParams } from "react-router-dom"; -import { useSelector, useDispatch } from 'react-redux' +import { useSelector, useDispatch } from 'react-redux'; import { getOrchestratorInfo, clearOrchestrator } from "../actions/livepeer"; import EventViewer from "../components/eventViewer"; import Orchestrator from "../components/orchestratorViewer"; diff --git a/src/pages/loadingScreen.js b/src/pages/loadingScreen.js index d1a16ce..8131bb9 100644 --- a/src/pages/loadingScreen.js +++ b/src/pages/loadingScreen.js @@ -4,7 +4,8 @@ import { getVisitorStats } from "../actions/user"; import { - getQuotes, getBlockchainData, getEvents, getCurrentOrchestratorInfo, getTickets + getQuotes, getBlockchainData, getEvents, getCurrentOrchestratorInfo, getTickets, + getAllEnsDomains, getAllEnsInfo } from "../actions/livepeer"; import { login } from "../actions/session"; @@ -18,7 +19,7 @@ const Startup = (obj) => { const dispatch = useDispatch(); const refreshAllZeData = () => { - console.log("Refreshing data..."); + console.log("Refreshing Livepeer data..."); batch(() => { dispatch(getQuotes()); dispatch(getEvents()); @@ -35,10 +36,19 @@ const Startup = (obj) => { dispatch(getVisitorStats()); }); } + + const refreshENS = () => { + console.log("Refreshing ENS data..."); + batch(() => { + dispatch(getAllEnsDomains()); + dispatch(getAllEnsInfo()); + }); + } useEffect(() => { refreshLogin(); refreshAllZeData(); + refreshENS(); setIsLoaded(true); if (refreshInterval) { const interval = setInterval(refreshAllZeData, refreshInterval); diff --git a/src/reducers/livepeer/livepeerstate.js b/src/reducers/livepeer/livepeerstate.js index 421cfbb..15da7c1 100644 --- a/src/reducers/livepeer/livepeerstate.js +++ b/src/reducers/livepeer/livepeerstate.js @@ -5,10 +5,21 @@ import { RECEIVE_ORCHESTRATOR, RECEIVE_CURRENT_ORCHESTRATOR, CLEAR_ORCHESTRATOR, - RECEIVE_TICKETS + RECEIVE_TICKETS, + SET_ALL_ENS_INFO, + SET_ALL_ENS_DOMAINS } from "../../actions/livepeer"; -export default (state = {}, { type, message }) => { +export default (state = { + quotes: [], + blockchains: [], + events: [], + thisOrchestrator: null, + selectedOrchestrator: null, + tickets: [], + ensInfoMapping: [], + ensDomainMapping: [] +}, { type, message }) => { Object.freeze(state); switch (type) { case RECEIVE_QUOTES: @@ -25,6 +36,10 @@ export default (state = {}, { type, message }) => { return { ...state, selectedOrchestrator: null }; case RECEIVE_TICKETS: return { ...state, tickets: message }; + case SET_ALL_ENS_INFO: + return { ...state, ensInfoMapping: message }; + case SET_ALL_ENS_DOMAINS: + return { ...state, ensDomainMapping: message }; default: return { ...state }; } diff --git a/src/util/livepeer.js b/src/util/livepeer.js index b80122c..5179024 100644 --- a/src/util/livepeer.js +++ b/src/util/livepeer.js @@ -61,4 +61,31 @@ export const getOrchestratorByDelegator = (delAddr) => ( "Content-Type": "application/json" } }) -); \ No newline at end of file +); + +export const getAllEnsDomains = () => ( + fetch("api/livepeer/getEnsDomains/", { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }) +); + +export const getAllEnsInfo = () => ( + fetch("api/livepeer/getEnsInfo/", { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }) +); + +export const getEnsInfo = (addr) => ( + fetch("api/livepeer/getENS/" + addr, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }) +);