ENS support and other stuff

This commit is contained in:
Marco van Dijk 2022-03-18 18:41:28 +01:00
parent 8dc1d2b2c0
commit 114147cdcd
12 changed files with 406 additions and 41 deletions

View File

@ -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",

View File

@ -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;

View File

@ -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;

BIN
public/ens.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -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({}));
};
};
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();
};

View File

@ -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 =
<a className="selectOrchLight" style={{cursor: 'alias'}} target="_blank" rel="noopener noreferrer" href={"https://app.ens.domains/name/" + thisInfo.domain + "/details"} >
<div className="rowAlignLeft">
<img alt="" src={thisInfo.avatar} width="20" height="20" />
</div>
</a >
} else {
thisIcon =
<a className="selectOrchLight" style={{cursor: 'alias'}} target="_blank" rel="noopener noreferrer" href={"https://app.ens.domains/name/" + thisInfo.domain + "/details"} >
<div className="rowAlignLeft">
<img alt="" src="ens.png" width="20" height="20" />
</div>
</a >
}
} else {
thisName = obj.address;
thisIcon = null;
}
return (
<div className="rowAlignLeft">
<a className="selectOrchLight" target="_blank" rel="noopener noreferrer" href={"https://explorer.livepeer.org/accounts/" + obj.address} >
<a className="selectOrchLight" style={{cursor: 'alias'}} target="_blank" rel="noopener noreferrer" href={"https://explorer.livepeer.org/accounts/" + obj.address} >
<div className="rowAlignLeft">
<img alt="" src="livepeer.png" width="20" height="20" />
<span className="elipsText elipsOnMobile">{obj.address}</span>
</div>
</a>
{thisIcon}
<span className="elipsText elipsOnMobile">{thisName}</span>
</div>
)
}

View File

@ -35,9 +35,9 @@ const EventButton = (obj) => {
}
return (
<div className="strokeSmollLeft">
<div className="strokeSmollLeft" style={{ width: '100%', minWidth: '200px', maxWidth: '500px'}}>
{blockNumber}
<div className="strokeSmollLeft" style={{ borderRadius: "1.2em", backgroundColor: obj.eventObj.eventColour, opacity: 0.9, border: '0.1em solid rgba(54, 46, 46, 0.1)', boxShadow: "4px 2px 3px 2px rgba(54, 46, 46, 0.1)" }}>
<div className="strokeSmollLeft" style={{ width: "100%", borderRadius: "1.2em", backgroundColor: obj.eventObj.eventColour, opacity: 0.9, border: '0.1em solid rgba(54, 46, 46, 0.1)', boxShadow: "4px 2px 3px 2px rgba(54, 46, 46, 0.1)" }}>
<div className="halfVerticalDivider" />
<div className="rowAlignLeft">
{eventCaller}

View File

@ -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 = <h4 className="elipsText elipsOnMobileExtra">{thisInfo.domain}</h4>;
if (thisInfo.avatar) {
thisIcon =
<a className="selectOrch" style={{ cursor: 'alias' }} target="_blank" rel="noopener noreferrer" href={"https://app.ens.domains/name/" + thisInfo.domain + "/details"} >
<img alt="" src={thisInfo.avatar} width="20em" height="20em" style={{ margin: 0 }} />
</a >
} else {
thisIcon =
<a className="selectOrch" style={{ cursor: 'alias' }} target="_blank" rel="noopener noreferrer" href={"https://app.ens.domains/name/" + thisInfo.domain + "/details"} >
<img alt="" src="ens.png" width="20em" height="20em" style={{ margin: 0 }} />
</a >
}
} else {
thisName = <span className="elipsText elipsOnMobileExtra">{obj.address}</span>;
thisIcon = null;
}
return (
<div className="rowAlignLeft" style={{ width: '100%' }}>
{thisIcon}
<a className="selectOrch" style={{ cursor: 'alias' }} rel="noopener noreferrer" target="_blank" href={"https://explorer.livepeer.org/accounts/" + obj.address}>
<img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} />
</a>
@ -17,7 +88,7 @@ const EventButtonAddress = (obj) => {
</button>
<span>{obj.name}</span>
<button className="selectOrch" style={{ padding: '0.5em', cursor: 'help' }} onClick={() => { dispatch(getOrchestratorInfo(obj.address)) }} >
<span className="elipsText elipsOnMobileExtra">{obj.address}</span>
{thisName}
</button>
</div>
)

View File

@ -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";

View File

@ -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);

View File

@ -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 };
}

View File

@ -61,4 +61,31 @@ export const getOrchestratorByDelegator = (delAddr) => (
"Content-Type": "application/json"
}
})
);
);
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"
}
})
);