Now with fancy Orchestrator Viewer

This commit is contained in:
Marco van Dijk 2022-03-03 11:44:14 +01:00
parent 485b75cad2
commit 0f15927376
7 changed files with 221 additions and 129 deletions

View File

@ -44,52 +44,12 @@ let claimTicketCostL2 = 0;
let withdrawFeeCostL1 = 0; let withdrawFeeCostL1 = 0;
let withdrawFeeCostL2 = 0; let withdrawFeeCostL2 = 0;
// Update info from thegraph every 5 minutes // Update O info from thegraph every 1 minute
const timeoutTheGraph = 300000; const timeoutTheGraph = 60000;
let theGraphGet = 0;
// Address of O info we are want to display // Address of O info we are want to display
const orchQuery = gql` const defaultOrch = "0x847791cbf03be716a7fe9dc8c9affe17bd49ae5e";
{ // Will contain addr, lastGet, and obj of any requested O's
transcoders(where: {id: "0x847791cbf03be716a7fe9dc8c9affe17bd49ae5e"}) { let orchestratorCache = [];
activationRound
deactivationRound
active
lastRewardRound {
id
length
startBlock
endBlock
mintableTokens
volumeETH
volumeUSD
totalActiveStake
totalSupply
participationRate
movedStake
newStake
}
rewardCut
feeShare
pendingFeeShare
pendingRewardCut
totalStake
totalVolumeETH
totalVolumeUSD
serviceURI
delegators {
id
bondedAmount
startRound
}
delegator {
id
bondedAmount
startRound
}
}
}
`;
let orchestratorCache = {};
// Listen to smart contract emitters. Resync with DB every 5 minutes // Listen to smart contract emitters. Resync with DB every 5 minutes
const timeoutEvents = 300000; const timeoutEvents = 300000;
@ -279,14 +239,96 @@ apiRouter.get("/getEvents", async (req, res) => {
} }
}); });
const parseOrchestrator = async function (reqAddr) {
const now = new Date().getTime();
// Default assume it's the first time we request this Orchestrator
let wasCached = false;
let needsUpdate = true;
let orchestratorObj = {};
// First get cached object
for (var orch of orchestratorCache) {
if (orch.addr == reqAddr) {
wasCached = true;
orchestratorObj = orch;
break;
}
}
if (wasCached) {
if (now - orch.lastGet < timeoutTheGraph) {
needsUpdate = false;
}
}
if (!wasCached || needsUpdate) {
const orchQuery = gql`{
transcoders(where: {id: "${reqAddr}"}) {
activationRound
deactivationRound
active
lastRewardRound {
id
length
startBlock
endBlock
mintableTokens
volumeETH
volumeUSD
totalActiveStake
totalSupply
participationRate
movedStake
newStake
}
rewardCut
feeShare
pendingFeeShare
pendingRewardCut
totalStake
totalVolumeETH
totalVolumeUSD
serviceURI
delegators {
id
bondedAmount
startRound
}
delegator {
id
bondedAmount
startRound
}
}
}
`;
orchestratorObj = JSON.stringify(await request("https://api.thegraph.com/subgraphs/name/livepeer/arbitrum-one", orchQuery));
if (wasCached) {
for (var orch of orchestratorCache) {
if (orch.addr == requestedOrchestrator) {
orch = orchestratorObj;
break;
}
}
} else {
orchestratorCache.push(orchestratorObj);
}
}
console.log(orchestratorObj);
return orchestratorObj;
}
apiRouter.get("/getOrchestrator", async (req, res) => { apiRouter.get("/getOrchestrator", async (req, res) => {
try { try {
const now = new Date().getTime(); const reqObj = await parseOrchestrator(defaultOrch);
// Update cmc once their data has expired res.send(reqObj);
if (now - theGraphGet > timeoutTheGraph) { } catch (err) {
orchestratorCache = JSON.stringify(await request("https://api.thegraph.com/subgraphs/name/livepeer/arbitrum-one", orchQuery)); console.log(err);
} res.status(400).send(err);
res.send(orchestratorCache); }
});
apiRouter.post("/getOrchestrator", async (req, res) => {
try {
const reqObj = await parseOrchestrator(req.body.orchAddr);
res.send(reqObj);
} catch (err) { } catch (err) {
res.status(400).send(err); res.status(400).send(err);
} }

View File

@ -4,6 +4,7 @@ import { receiveErrors } from "./error";
export const RECEIVE_QUOTES = "RECEIVE_QUOTES"; export const RECEIVE_QUOTES = "RECEIVE_QUOTES";
export const RECEIVE_BLOCKCHAIN_DATA = "RECEIVE_BLOCKCHAIN_DATA"; export const RECEIVE_BLOCKCHAIN_DATA = "RECEIVE_BLOCKCHAIN_DATA";
export const RECEIVE_EVENTS = "RECEIVE_EVENTS"; export const RECEIVE_EVENTS = "RECEIVE_EVENTS";
export const RECEIVE_CURRENT_ORCHESTRATOR = "RECEIVE_CURRENT_ORCHESTRATOR";
export const RECEIVE_ORCHESTRATOR = "RECEIVE_ORCHESTRATOR"; export const RECEIVE_ORCHESTRATOR = "RECEIVE_ORCHESTRATOR";
const setQuotes = message => ({ const setQuotes = message => ({
@ -16,6 +17,9 @@ const setEvents = message => ({
type: RECEIVE_EVENTS, message type: RECEIVE_EVENTS, message
}); });
const setCurrentOrchestratorInfo = message => ({ const setCurrentOrchestratorInfo = message => ({
type: RECEIVE_CURRENT_ORCHESTRATOR, message
});
const setOrchestratorInfo = message => ({
type: RECEIVE_ORCHESTRATOR, message type: RECEIVE_ORCHESTRATOR, message
}); });
@ -54,3 +58,12 @@ export const getCurrentOrchestratorInfo = () => async dispatch => {
} }
return dispatch(receiveErrors(data)); return dispatch(receiveErrors(data));
}; };
export const getOrchestratorInfo = (orchAddr) => async dispatch => {
const response = await apiUtil.getOrchestratorInfo(orchAddr);
const data = await response.json();
if (response.ok) {
return dispatch(setOrchestratorInfo(data));
}
return dispatch(receiveErrors(data));
};

View File

@ -7,6 +7,7 @@ import { connect } from "react-redux";
import { import {
getQuotes, getCurrentOrchestratorInfo getQuotes, getCurrentOrchestratorInfo
} from "./actions/livepeer"; } from "./actions/livepeer";
import Orchestrator from "./orchestratorViewer";
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return { return {
@ -51,47 +52,6 @@ class Grafana extends React.Component {
} }
} }
console.log(this.props.livepeer);
let rewardCut = 0;
let feeCut = 0;
let totalStake = 0;
let totalVolumeETH = 0;
let totalVolumeUSD = 0;
let delegators = [];
let delegatorsTotalStake = 0;
let selfStake = 0;
let selfStakeRatio = 0;
if (this.props.livepeer.thisOrchestrator) {
if (this.props.livepeer.thisOrchestrator.rewardCut) {
rewardCut = (this.props.livepeer.thisOrchestrator.rewardCut / 10000).toFixed(2);
}
if (this.props.livepeer.thisOrchestrator.feeShare) {
feeCut = (100 - (this.props.livepeer.thisOrchestrator.feeShare / 10000)).toFixed(2);
}
if (this.props.livepeer.thisOrchestrator.totalStake) {
totalStake = parseFloat(this.props.livepeer.thisOrchestrator.totalStake).toFixed(2);
}
if (this.props.livepeer.thisOrchestrator.totalVolumeETH) {
totalVolumeETH = parseFloat(this.props.livepeer.thisOrchestrator.totalVolumeETH * 1).toFixed(4);
}
if (this.props.livepeer.thisOrchestrator.totalVolumeUSD) {
totalVolumeUSD = parseFloat(this.props.livepeer.thisOrchestrator.totalVolumeUSD * 1).toFixed(2);
}
if (this.props.livepeer.thisOrchestrator.delegators && this.props.livepeer.thisOrchestrator.delegator) {
delegators = this.props.livepeer.thisOrchestrator.delegators;
selfStake = parseFloat(this.props.livepeer.thisOrchestrator.delegator.bondedAmount);
{
delegators.map((delObj, idx) => {
delegatorsTotalStake += parseFloat(delObj.bondedAmount);
})
}
selfStakeRatio = ((selfStake / delegatorsTotalStake) * 100).toFixed(2);
delegatorsTotalStake = delegatorsTotalStake.toFixed(2);
selfStake = selfStake.toFixed(2);
}
}
return ( return (
<div className="stroke" style={{ margin: 0, padding: 0 }}> <div className="stroke" style={{ margin: 0, padding: 0 }}>
<div className="row" style={{ margin: 0, padding: 0 }}> <div className="row" style={{ margin: 0, padding: 0 }}>
@ -106,43 +66,22 @@ class Grafana extends React.Component {
<div className="stroke" style={{ marginTop: 0, marginBottom: 5, paddingBottom: 0 }}> <div className="stroke" style={{ marginTop: 0, marginBottom: 5, paddingBottom: 0 }}>
<div className="stroke roundedOpaque" style={{}}> <div className="stroke roundedOpaque" style={{}}>
<div className="row"> <div className="row">
<h2> <img alt="" src="livepeer.png" width="30" height="30" /> <a href="https://explorer.livepeer.org/accounts/0x847791cbf03be716a7fe9dc8c9affe17bd49ae5e/">Livepeer Orchestrator</a></h2> <div className="row">
<img alt="" src="livepeer.png" width="30" height="30" />
<p>${lptPrice}</p>
<p>({lptPriceChange24h}%)</p>
</div>
<div className="row">
<h2> <a href="https://explorer.livepeer.org/accounts/0x847791cbf03be716a7fe9dc8c9affe17bd49ae5e/">Livepeer Orchestrator</a></h2>
</div>
<div className="row">
<img alt="" src="eth.png" width="30" height="30" />
<p>${ethPrice}</p>
<p>({ethPriceChange24h}%)</p>
</div>
</div> </div>
<div className="stroke roundedOpaque" style={{ borderRadius: "1em", backgroundColor: "#111217" }}> <div className="stroke roundedOpaque" style={{ borderRadius: "1em", backgroundColor: "#111217" }}>
<div className="hostInfo" style={{ margin: 0, padding: 0 }}> <Orchestrator thisOrchestrator={this.props.livepeer.thisOrchestrator} />
<div className="stroke" style={{ margin: 0, padding: 0 }}>
<div className="row">
<h3>Price Info</h3>
</div>
<div className="row">
<img alt="" src="livepeer.png" width="30" height="30" />
<p>${lptPrice}</p>
<p>({lptPriceChange24h}%)</p>
</div>
<div className="row">
<img alt="" src="eth.png" width="30" height="30" />
<p>${ethPrice}</p>
<p>({ethPriceChange24h}%)</p>
</div>
</div>
<div className="stroke" style={{ margin: 0, padding: 0 }}>
<div className="row">
<h3>Orchestrator Info</h3>
</div>
<div className="row">
<p>Reward Cut {rewardCut}%</p>
<p>Fee Cut {feeCut}%</p>
</div>
<div className="row">
<p>Total Stake {totalStake} LPT</p>
<p>Earned fees {totalVolumeETH} Eth (${totalVolumeUSD})</p>
</div>
<div className="row">
<p>Self stake {selfStake} LPT (Ratio of {selfStakeRatio}%)</p>
<p>Total stake {delegatorsTotalStake} LPT</p>
</div>
</div>
</div>
<div className="flexContainer" style={{ justifyContent: "center" }}> <div className="flexContainer" style={{ justifyContent: "center" }}>
<iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572077" height="400" frameBorder="0"></iframe> <iframe className="fullGrafana" src="https://grafana.stronk.tech/d-solo/71b6OZ0Gz/orchestrator-overview?orgId=1&refresh=5s&theme=dark&panelId=23763572077" height="400" frameBorder="0"></iframe>
</div> </div>

72
src/orchestratorViewer.js Normal file
View File

@ -0,0 +1,72 @@
import React from "react";
const Orchestrator = (obj) => {
let rewardCut = 0;
let feeCut = 0;
let totalStake = 0;
let totalVolumeETH = 0;
let totalVolumeUSD = 0;
let delegators = [];
let selfStake = 0;
let selfStakeRatio = 0;
if (obj.thisOrchestrator) {
if (obj.thisOrchestrator.rewardCut) {
rewardCut = (obj.thisOrchestrator.rewardCut / 10000).toFixed(2);
}
if (obj.thisOrchestrator.feeShare) {
feeCut = (100 - (obj.thisOrchestrator.feeShare / 10000)).toFixed(2);
}
if (obj.thisOrchestrator.totalStake) {
totalStake = parseFloat(obj.thisOrchestrator.totalStake).toFixed(2);
}
if (obj.thisOrchestrator.totalVolumeETH) {
totalVolumeETH = parseFloat(obj.thisOrchestrator.totalVolumeETH * 1).toFixed(4);
}
if (obj.thisOrchestrator.totalVolumeUSD) {
totalVolumeUSD = parseFloat(obj.thisOrchestrator.totalVolumeUSD * 1).toFixed(2);
}
if (obj.thisOrchestrator.delegators && obj.thisOrchestrator.delegator) {
delegators = obj.thisOrchestrator.delegators;
selfStake = parseFloat(obj.thisOrchestrator.delegator.bondedAmount);
selfStakeRatio = ((selfStake / totalStake) * 100).toFixed(2);
selfStake = selfStake.toFixed(2);
}
}
return (
<div className="hostInfo" style={{ margin: 0, padding: 0 }}>
<div className="stroke" style={{ margin: 0, padding: 0 }}>
<div className="rowAlignLeft">
<h3>Orchestrator Info</h3>
</div>
<div className="rowAlignLeft">
<p>Reward Cut {rewardCut}%</p>
<p>Fee Cut {feeCut}%</p>
</div>
<div className="rowAlignLeft">
<p>Total Stake {totalStake} LPT</p>
</div>
<div className="rowAlignLeft">
<p>Self stake {selfStake} LPT ({selfStakeRatio}%)</p>
</div>
<div className="rowAlignLeft">
<p>Earned fees {totalVolumeETH} Eth (${totalVolumeUSD})</p>
</div>
{
delegators.map((delObj, idx) => {
return (
<div className="rowAlignLeft">
<a href={"https://explorer.livepeer.org/accounts/" + delObj.id}>
<img alt="" src="livepeer.png" width="30" height="30" />
</a>
<p>{parseFloat(delObj.bondedAmount).toFixed(2)} LPT since round {delObj.startRound}</p>
</div>
)
})
}
</div>
</div>
)
}
export default Orchestrator;

View File

@ -2,7 +2,8 @@ import {
RECEIVE_QUOTES, RECEIVE_QUOTES,
RECEIVE_BLOCKCHAIN_DATA, RECEIVE_BLOCKCHAIN_DATA,
RECEIVE_EVENTS, RECEIVE_EVENTS,
RECEIVE_ORCHESTRATOR RECEIVE_ORCHESTRATOR,
RECEIVE_CURRENT_ORCHESTRATOR
} from "../../actions/livepeer"; } from "../../actions/livepeer";
export default (state = {}, { type, message }) => { export default (state = {}, { type, message }) => {
@ -14,8 +15,10 @@ export default (state = {}, { type, message }) => {
return { ...state, blockchains: message }; return { ...state, blockchains: message };
case RECEIVE_EVENTS: case RECEIVE_EVENTS:
return { ...state, events: message }; return { ...state, events: message };
case RECEIVE_ORCHESTRATOR: case RECEIVE_CURRENT_ORCHESTRATOR:
return { ...state, thisOrchestrator: message.transcoders[0] }; return { ...state, thisOrchestrator: message.transcoders[0] };
case RECEIVE_ORCHESTRATOR:
return { ...state, selectedOrchestrator: message.transcoders[0] };
default: default:
return { ...state }; return { ...state };
} }

View File

@ -256,6 +256,19 @@ svg {
margin-right: 10px; margin-right: 10px;
} }
.rowAlignLeft {
box-sizing: border-box;
width: 100%;
text-align: center;
justify-content: center;
align-items: center;
display: flex;
justify-content: flex-start;
vertical-align: middle;
margin-left: 10px;
margin-right: 10px;
}
.flexItem { .flexItem {
padding: 5px; padding: 5px;
width: 20px; width: 20px;

View File

@ -35,3 +35,13 @@ export const getCurrentOrchestratorInfo = () => (
} }
}) })
); );
export const getOrchestratorInfo = (orchAddr) => (
fetch("api/livepeer/getOrchestrator", {
method: "POST",
body: JSON.stringify(orchAddr),
headers: {
"Content-Type": "application/json"
}
})
);