View ticket redemptions (WIP)

Added Ticket Broker contract to watch for ticket redemptions
Make links open in  a new tab
Slight formatting updates
This commit is contained in:
Marco van Dijk 2022-03-11 13:40:28 +01:00
parent 9fa66f5683
commit ad5fe145cc
16 changed files with 1570 additions and 244 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,35 @@
import mongoose from 'mongoose';
const TicketSchema = new mongoose.Schema({
address: {
type: String,
required: true
},
transactionHash: {
type: String,
required: true
},
transactionUrl: {
type: String,
required: true
},
name: {
type: String,
required: true
},
data: {
type: Object,
required: true
},
blockNumber: {
type: Number,
required: true
},
blockTime: {
type: Number,
required: true
}
}, { timestamps: false });
const Ticket = mongoose.model('Ticket', TicketSchema);
export default Ticket;

View File

@ -1,6 +1,8 @@
import express from "express"; import express from "express";
import Event from '../models/event'; import Event from '../models/event';
import Block from '../models/block'; import Block from '../models/block';
import Ticket from '../models/ticketEvent'
const apiRouter = express.Router(); const apiRouter = express.Router();
import { import {
API_CMC, API_L1_HTTP, API_L2_HTTP, API_L2_WS, API_CMC, API_L1_HTTP, API_L2_HTTP, API_L2_WS,
@ -76,19 +78,32 @@ let delegatorCache = [];
// Listen to smart contract emitters. Only re-syncs on boot! // Listen to smart contract emitters. Only re-syncs on boot!
let eventsCache = []; let eventsCache = [];
let latestMissedDuringSync = 0; let latestBlockInChain = 0;
let lastBlockDataAdded = 0; let lastBlockEvents = 0;
let lastBlockTickets = 0;
let syncCache = []; let syncCache = [];
let ticketsCache = [];
let ticketsSyncCache = [];
// https://arbiscan.io/address/0x35Bcf3c30594191d53231E4FF333E8A770453e40#events // https://arbiscan.io/address/0x35Bcf3c30594191d53231E4FF333E8A770453e40#events
let BondingManagerTargetJson; let BondingManagerTargetJson;
let BondingManagerTargetAbi; let BondingManagerTargetAbi;
let BondingManagerProxyAddr; let BondingManagerProxyAddr;
let contractInstance; let bondingManagerContract;
let TicketBrokerTargetJson;
let TicketBrokerTargetAbi;
let TicketBrokerTargetAddr;
let ticketBrokerContract;
if (!CONF_SIMPLE_MODE) { if (!CONF_SIMPLE_MODE) {
// Listen for events on the bonding manager contract
BondingManagerTargetJson = fs.readFileSync('src/abi/BondingManagerTarget.json'); BondingManagerTargetJson = fs.readFileSync('src/abi/BondingManagerTarget.json');
BondingManagerTargetAbi = JSON.parse(BondingManagerTargetJson); BondingManagerTargetAbi = JSON.parse(BondingManagerTargetJson);
BondingManagerProxyAddr = "0x35Bcf3c30594191d53231E4FF333E8A770453e40"; BondingManagerProxyAddr = "0x35Bcf3c30594191d53231E4FF333E8A770453e40";
contractInstance = new web3layer2WS.eth.Contract(BondingManagerTargetAbi.abi, BondingManagerProxyAddr); bondingManagerContract = new web3layer2WS.eth.Contract(BondingManagerTargetAbi.abi, BondingManagerProxyAddr);
// Listen for events on the ticket broker contract
TicketBrokerTargetJson = fs.readFileSync('src/abi/TicketBrokerTarget.json');
TicketBrokerTargetAbi = JSON.parse(TicketBrokerTargetJson);
TicketBrokerTargetAddr = "0xa8bB618B1520E284046F3dFc448851A1Ff26e41B";
ticketBrokerContract = new web3layer2WS.eth.Contract(TicketBrokerTargetAbi.abi, TicketBrokerTargetAddr);
} }
let blockCache = []; let blockCache = [];
@ -117,11 +132,13 @@ const getBlock = async function (blockNumber) {
// Set special flag to make sure also get blocks that pass us by while we are syncing // Set special flag to make sure also get blocks that pass us by while we are syncing
let isSyncing = true; let isSyncing = true;
let isSyncRunning = false; let isEventSyncing = false;
let isTicketSyncing = false;
// Start Listening for live updates // Start Listening for live updates
var BondingManagerProxyListener; var BondingManagerProxyListener;
var TicketBrokerProxyListener;
if (!CONF_SIMPLE_MODE) { if (!CONF_SIMPLE_MODE) {
BondingManagerProxyListener = contractInstance.events.allEvents(async (error, event) => { BondingManagerProxyListener = bondingManagerContract.events.allEvents(async (error, event) => {
try { try {
if (error) { if (error) {
throw error throw error
@ -157,14 +174,50 @@ if (!CONF_SIMPLE_MODE) {
} }
}); });
console.log("Listening for events on " + BondingManagerProxyAddr); console.log("Listening for events on " + BondingManagerProxyAddr);
TicketBrokerProxyListener = ticketBrokerContract.events.allEvents(async (error, event) => {
try {
if (error) {
throw error
}
if (isSyncing) {
console.log('Received new ticket event on block ' + event.blockNumber + " during sync");
} else {
console.log('Received new ticket event on block ' + event.blockNumber);
}
const thisBlock = await getBlock(event.blockNumber);
// Push obj of event to cache and create a new entry for it in the DB
const eventObj = {
address: event.address,
transactionHash: event.transactionHash,
transactionUrl: "https://arbiscan.io/tx/" + event.transactionHash,
name: event.event,
data: event.returnValues,
blockNumber: thisBlock.number,
blockTime: thisBlock.timestamp
}
if (!isSyncing) {
if (!CONF_DISABLE_DB) {
const dbObj = new Ticket(eventObj);
await dbObj.save();
}
ticketsCache.push(eventObj);
} else {
ticketsSyncCache.push(eventObj);
}
}
catch (err) {
console.log("FATAL ERROR: ", err);
}
});
console.log("Listening for tickets on " + TicketBrokerTargetAddr);
} }
// Does the syncing // Syncs events database
const doSync = function () { const syncEvents = function () {
console.log("Starting sync process"); console.log("Starting sync process for Bonding Manager events");
isSyncRunning = true; isEventSyncing = true;
// Then do a sync from last found until latest known // Then do a sync from last found until latest known
contractInstance.getPastEvents("allEvents", { fromBlock: lastBlockDataAdded + 1, toBlock: 'latest' }, async (error, events) => { bondingManagerContract.getPastEvents("allEvents", { fromBlock: lastBlockEvents + 1, toBlock: 'latest' }, async (error, events) => {
try { try {
if (error) { if (error) {
throw error throw error
@ -172,8 +225,8 @@ const doSync = function () {
let size = events.length; let size = events.length;
console.log("Parsing " + size + " events"); console.log("Parsing " + size + " events");
for (const event of events) { for (const event of events) {
if (event.blockNumber > lastBlockDataAdded) { if (event.blockNumber > lastBlockEvents) {
lastBlockDataAdded = event.blockNumber; lastBlockEvents = event.blockNumber;
} }
const thisBlock = await getBlock(event.blockNumber); const thisBlock = await getBlock(event.blockNumber);
const eventObj = { const eventObj = {
@ -195,7 +248,46 @@ const doSync = function () {
catch (err) { catch (err) {
console.log("FATAL ERROR: ", err); console.log("FATAL ERROR: ", err);
} }
isSyncRunning = false; isEventSyncing = false;
});
}
// Syncs tickets database
const syncTickets = function () {
console.log("Starting sync process for Ticket Broker events");
isTicketSyncing = true;
// Then do a sync from last found until latest known
ticketBrokerContract.getPastEvents("allEvents", { fromBlock: lastBlockTickets + 1, toBlock: 'latest' }, async (error, events) => {
try {
if (error) {
throw error
}
let size = events.length;
console.log("Parsing " + size + " tickets");
for (const event of events) {
if (event.blockNumber > lastBlockTickets) {
lastBlockTickets = event.blockNumber;
}
const thisBlock = await getBlock(event.blockNumber);
const eventObj = {
address: event.address,
transactionHash: event.transactionHash,
transactionUrl: "https://arbiscan.io/tx/" + event.transactionHash,
name: event.event,
data: event.returnValues,
blockNumber: thisBlock.number,
blockTime: thisBlock.timestamp
}
if (!CONF_DISABLE_DB) {
const dbObj = new Ticket(eventObj);
await dbObj.save();
}
ticketsCache.push(eventObj);
}
}
catch (err) {
console.log("FATAL ERROR: ", err);
}
isTicketSyncing = false;
}); });
} }
function sleep(ms) { function sleep(ms) {
@ -206,6 +298,13 @@ function sleep(ms) {
const handleSync = async function () { const handleSync = async function () {
// First collection -> cache // First collection -> cache
// Get all parsed blocks
blockCache = await Block.find({}, {
blockNumber: 1,
blockTime: 1
});
console.log("Retrieved existing Blocks of size " + blockCache.length);
// Get all parsed Events
eventsCache = await Event.find({}, { eventsCache = await Event.find({}, {
address: 1, address: 1,
transactionHash: 1, transactionHash: 1,
@ -217,31 +316,54 @@ const handleSync = async function () {
_id: 0 _id: 0
}); });
console.log("Retrieved existing Events of size " + eventsCache.length); console.log("Retrieved existing Events of size " + eventsCache.length);
// Get all parsedTickets
ticketsCache = await Ticket.find({}, {
address: 1,
transactionHash: 1,
transactionUrl: 1,
name: 1,
data: 1,
blockNumber: 1,
blockTime: 1,
_id: 0
});
console.log("Retrieved existing Tickets of size " + ticketsCache.length);
// Then determine latest block number parsed based on collection // Then determine latest block number parsed based on collection
for (var idx = 0; idx < eventsCache.length; idx++) { for (var idx = 0; idx < eventsCache.length; idx++) {
const thisBlock = eventsCache[idx]; const thisBlock = eventsCache[idx];
if (thisBlock.blockNumber > lastBlockDataAdded) { if (thisBlock.blockNumber > lastBlockEvents) {
lastBlockDataAdded = thisBlock.blockNumber; lastBlockEvents = thisBlock.blockNumber;
} }
} }
console.log("Latest Event block parsed is " + lastBlockEvents);
// Then determine latest block number parsed based on collection
for (var idx = 0; idx < ticketsCache.length; idx++) {
const thisBlock = ticketsCache[idx];
if (thisBlock.blockNumber > lastBlockTickets) {
lastBlockTickets = thisBlock.blockNumber;
}
}
console.log("Latest Ticket block parsed is " + lastBlockTickets);
// Get latest block in chain // Get latest block in chain
const latestBlock = await web3layer2.eth.getBlockNumber(); const latestBlock = await web3layer2.eth.getBlockNumber();
if (latestBlock > latestMissedDuringSync) { if (latestBlock > latestBlockInChain) {
latestMissedDuringSync = latestBlock; latestBlockInChain = latestBlock;
} }
console.log("Parsed up to block " + lastBlockDataAdded + " out of " + latestMissedDuringSync + " blocks"); console.log("Latest L2 Eth block is " + latestBlockInChain);
// Get all parsed blocks console.log("Needs to sync " + (latestBlockInChain - lastBlockEvents) + " blocks for Events sync");
blockCache = await Block.find({}, { console.log("Needs to sync " + (latestBlockInChain - lastBlockTickets) + " blocks for Tickets sync");
blockNumber: 1, syncTickets();
blockTime: 1 syncEvents();
}); while (isEventSyncing || isTicketSyncing) {
console.log("Retrieved existing Blocks of size " + blockCache.length); await sleep(3000);
doSync(); if (isEventSyncing){
while (isSyncRunning) { console.log("Parsed " + lastBlockEvents + " out of " + latestBlockInChain + " blocks for Event sync");
await sleep(1000); }
console.log("Parsed " + lastBlockDataAdded + " out of " + latestMissedDuringSync + " blocks"); if (isTicketSyncing){
console.log("Parsed " + lastBlockTickets + " out of " + latestBlockInChain + " blocks for Ticket sync");
}
} }
while (syncCache.length) { while (syncCache.length || ticketsSyncCache.length) {
const liveEvents = syncCache; const liveEvents = syncCache;
syncCache = []; syncCache = [];
for (const eventObj of liveEvents) { for (const eventObj of liveEvents) {
@ -252,16 +374,27 @@ const handleSync = async function () {
} }
eventsCache.push(eventObj); eventsCache.push(eventObj);
} }
const liveTickets = ticketsSyncCache;
ticketsSyncCache = [];
for (const eventObj of liveTickets) {
console.log("Parsing ticket received while syncing");
if (!CONF_DISABLE_DB) {
const dbObj = new Ticket(eventObj);
await dbObj.save();
}
ticketsCache.push(eventObj);
}
} }
console.log('done syncing') console.log('done syncing')
isSyncing = false; isSyncing = false;
}; };
if (!isSyncRunning && !CONF_SIMPLE_MODE && !CONF_DISABLE_SYNC) { if (!isEventSyncing && !CONF_SIMPLE_MODE && !CONF_DISABLE_SYNC) {
handleSync(); handleSync();
} }
// Splits of raw CMC object into coin quote data // Splits of raw CMC object into coin quote data
const parseCmc = async function () { const parseCmc = async function () {
return;
try { try {
cmcCache = await cmcClient.getTickers({ limit: 200 }); cmcCache = await cmcClient.getTickers({ limit: 200 });
for (var idx = 0; idx < cmcCache.data.length; idx++) { for (var idx = 0; idx < cmcCache.data.length; idx++) {
@ -422,6 +555,15 @@ apiRouter.get("/getEvents", async (req, res) => {
} }
}); });
// Exports list of smart contract ticket events
apiRouter.get("/getTickets", async (req, res) => {
try {
res.send(ticketsCache);
} catch (err) {
res.status(400).send(err);
}
});
// Gets info on a given Orchestrator // Gets info on a given Orchestrator
const parseOrchestrator = async function (reqAddr) { const parseOrchestrator = async function (reqAddr) {
reqAddr = reqAddr.toLowerCase(); reqAddr = reqAddr.toLowerCase();

View File

@ -7,11 +7,11 @@ const Block = (obj) => {
const [thisDate, thisTime] = dateObj.toISOString().split('T'); const [thisDate, thisTime] = dateObj.toISOString().split('T');
return ( return (
<div className="rowAlignLeft" style={{ width: '100%', marginTop: '1em' }}> <div className="rowAlignLeft" style={{ width: '100%', marginTop: '1em' }}>
<a className="selectOrch" style={{cursor: 'alias'}} href={obj.url}> <a className="selectOrch" style={{cursor: 'alias'}} target="_blank" href={obj.url}>
<img alt="" src="arb.svg" width="30em" height="30em" /> <img alt="" src="arb.svg" width="30em" height="30em" />
</a> </a>
<span className="rowAlignRight elipsText"> <span className="rowAlignRight elipsText">
<a className="selectOrch" style={{cursor: 'alias'}} href={"https://arbiscan.io/block/" + obj.block}> <a className="selectOrch" style={{cursor: 'alias'}} target="_blank" href={"https://arbiscan.io/block/" + obj.block}>
🔗{obj.block} 🔗{obj.block}
</a> </a>
<p className="darkText">📅{thisDate} - {thisTime.split('.')[0]} </p> <p className="darkText">📅{thisDate} - {thisTime.split('.')[0]} </p>

View File

@ -4,7 +4,7 @@ import ReactTooltip from "react-tooltip";
const Address = (obj) => { const Address = (obj) => {
return ( return (
<div className="rowAlignLeft" style={{ width: 'unset', margin: 0 }}> <div className="rowAlignLeft" style={{ width: 'unset', margin: 0 }}>
<a className="selectOrchLight" href={"https://explorer.livepeer.org/accounts/" + obj.address} data-tip data-for={obj.seed} > <a className="selectOrchLight" target="_blank" href={"https://explorer.livepeer.org/accounts/" + obj.address} data-tip data-for={obj.seed} >
<div className="rowAlignLeft" style={{ width: 'unset', margin: 0 }}> <div className="rowAlignLeft" style={{ width: 'unset', margin: 0 }}>
<img alt="" src="livepeer.png" width="20" height="20" /> <img alt="" src="livepeer.png" width="20" height="20" />
<span className="elipsText elipsOnMobile">{obj.address}</span> <span className="elipsText elipsOnMobile">{obj.address}</span>

View File

@ -17,7 +17,7 @@ const OrchDelegatorViewer = (obj) => {
{ {
delegators.map((delObj, idx) => { delegators.map((delObj, idx) => {
return ( return (
<div className={obj.forceVertical ? "flexContainer forceWrap" : "flexContainer"} key={"delegator" + idx} style={{ margin: 0, textAlign: 'center',alignItems: 'center', justifyContent:'center' }}> <div className="flexContainer forceWrap" key={"delegator" + idx} style={{ margin: 0, textAlign: 'center',alignItems: 'center', justifyContent:'center' }}>
<Address address={delObj.id} seed={"delegator" + idx + delObj.id} /> <Address address={delObj.id} seed={"delegator" + idx + delObj.id} />
<div className="rowAlignRight" style={{ margin: 0 }}> <div className="rowAlignRight" style={{ margin: 0 }}>
<p className="darkText">{parseFloat(delObj.bondedAmount).toFixed(2)} LPT since round {delObj.startRound}</p> <p className="darkText">{parseFloat(delObj.bondedAmount).toFixed(2)} LPT since round {delObj.startRound}</p>

View File

@ -9,6 +9,9 @@ const stakeColour = "rgba(56, 23, 122, 0.3)";
const unbondColour = "rgba(122, 23, 51, 0.3)"; const unbondColour = "rgba(122, 23, 51, 0.3)";
const claimColour = "rgba(77, 91, 42, 0.3)"; const claimColour = "rgba(77, 91, 42, 0.3)";
const ticketTransferColour = "rgba(88, 91, 42, 0.3)";
const ticketRedeemColour = "rgba(42, 91, 44, 0.3)";
const thresholdStaking = 0.001; const thresholdStaking = 0.001;
const thresholdFees = 0.00009; const thresholdFees = 0.00009;
@ -18,6 +21,7 @@ export const RECEIVE_EVENTS = "RECEIVE_EVENTS";
export const RECEIVE_CURRENT_ORCHESTRATOR = "RECEIVE_CURRENT_ORCHESTRATOR"; export const RECEIVE_CURRENT_ORCHESTRATOR = "RECEIVE_CURRENT_ORCHESTRATOR";
export const RECEIVE_ORCHESTRATOR = "RECEIVE_ORCHESTRATOR"; export const RECEIVE_ORCHESTRATOR = "RECEIVE_ORCHESTRATOR";
export const CLEAR_ORCHESTRATOR = "CLEAR_ORCHESTRATOR"; export const CLEAR_ORCHESTRATOR = "CLEAR_ORCHESTRATOR";
export const RECEIVE_TICKETS = "RECEIVE_TICKETS";
const setQuotes = message => ({ const setQuotes = message => ({
type: RECEIVE_QUOTES, message type: RECEIVE_QUOTES, message
@ -37,6 +41,9 @@ const setOrchestratorInfo = message => ({
const clearOrchestratorInfo = () => ({ const clearOrchestratorInfo = () => ({
type: CLEAR_ORCHESTRATOR type: CLEAR_ORCHESTRATOR
}) })
const setTickets = message => ({
type: RECEIVE_TICKETS, message
});
export const getQuotes = () => async dispatch => { export const getQuotes = () => async dispatch => {
const response = await apiUtil.getQuotes(); const response = await apiUtil.getQuotes();
@ -204,7 +211,7 @@ export const getEvents = () => async dispatch => {
transactionUrl: currentUrl, transactionUrl: currentUrl,
transactionBlock: currentBlock, transactionBlock: currentBlock,
transactionTime: currentTime, transactionTime: currentTime,
eventValue: amount eventValue: amount
}); });
} else if (eventObj.name === "WithdrawFees") { } else if (eventObj.name === "WithdrawFees") {
const amount = parseFloat(eventObj.data.amount) / 1000000000000000000; const amount = parseFloat(eventObj.data.amount) / 1000000000000000000;
@ -349,6 +356,83 @@ export const getEvents = () => async dispatch => {
return dispatch(receiveErrors(data)); return dispatch(receiveErrors(data));
}; };
export const getTickets = () => async dispatch => {
const response = await apiUtil.getTickets();
const data = await response.json();
// Combine raw list of events into a list of useful Events
if (response.ok) {
let finalTicketList = [];
// Current transaction we are processing
let txCounter = 0;
let currentTx = "";
let currentUrl = "";
let currentBlock = 0;
let currentTime = 0;
// Parse Tickets
{
for (const eventObj of data.slice(0).reverse()) {
if (currentTx === "") {
currentTx = eventObj.transactionHash;
currentUrl = eventObj.transactionUrl;
currentBlock = eventObj.blockNumber;
currentTime = eventObj.blockTime;
}
// New transaction found
if (currentTx !== eventObj.transactionHash) {
// Reset event data
txCounter++;
currentTx = eventObj.transactionHash;
currentUrl = eventObj.transactionUrl;
currentBlock = eventObj.blockNumber;
currentTime = eventObj.blockTime;
}
// Always split off WithdrawStake as a separate Withdraw Event
if (eventObj.name === "WinningTicketRedeemed") {
const amount = parseFloat(eventObj.data.faceValue) / 1000000000000000000;
const txt = " redeemed a winning ticket worth " + amount.toFixed(4) + " Eth";
finalTicketList.push({
eventType: "Withdraw",
eventDescription: txt,
eventCaller: eventObj.data.recipient.toLowerCase(),
eventFrom: eventObj.data.sender.toLowerCase(),
eventTo: "",
eventColour: ticketRedeemColour,
transactionHash: currentTx,
transactionUrl: currentUrl,
transactionBlock: currentBlock,
transactionTime: currentTime,
eventValue: amount
});
} else if (eventObj.name === "WinningTicketTransfer") {
// For now lets just ignore these, they are boring
continue;
const amount = parseFloat(eventObj.data.amount) / 1000000000000000000;
const txt = " broadcaster payed out " + amount.toFixed(4) + " Eth";
finalTicketList.push({
eventType: "TransferTicket",
eventDescription: txt,
eventCaller: eventObj.data.sender.toLowerCase(),
eventFrom: "",
eventTo: eventObj.data.recipient.toLowerCase(),
eventColour: ticketTransferColour,
transactionHash: currentTx,
transactionUrl: currentUrl,
transactionBlock: currentBlock,
transactionTime: currentTime,
eventValue: amount
});
} else {
console.log("UNIMPLEMENTED: " + eventObj.name);
}
}
}
// NOTE: We are throwing away the very oldest Ticket now, which should be fine.
// We can fix this once above wall of text becomes a separate function
return dispatch(setTickets(finalTicketList));
}
return dispatch(receiveErrors(data));
};
export const getCurrentOrchestratorInfo = () => async dispatch => { export const getCurrentOrchestratorInfo = () => async dispatch => {
const response = await apiUtil.getCurrentOrchestratorInfo(); const response = await apiUtil.getCurrentOrchestratorInfo();
const data = await response.json(); const data = await response.json();
@ -362,9 +446,9 @@ export const getOrchestratorInfo = (orchAddr) => async dispatch => {
const response = await apiUtil.getOrchestratorInfo(orchAddr); const response = await apiUtil.getOrchestratorInfo(orchAddr);
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {
if (data && data.id){ if (data && data.id) {
return dispatch(setOrchestratorInfo(data)); return dispatch(setOrchestratorInfo(data));
}else{ } else {
const response = await apiUtil.getOrchestratorByDelegator(orchAddr); const response = await apiUtil.getOrchestratorByDelegator(orchAddr);
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok) {

View File

@ -25,8 +25,8 @@ const EventButton = (obj) => {
if (obj.eventObj.eventTo) { if (obj.eventObj.eventTo) {
eventTo = eventTo =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}> <div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>To</p> <span>To&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:</span>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventTo}> <a className="selectOrch" style={{ cursor: 'alias', marginLeft: '0.5em' }} target="_blank" href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventTo}>
<img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} /> <img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} />
</a> </a>
<button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventTo) }} > <button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventTo) }} >
@ -40,8 +40,8 @@ const EventButton = (obj) => {
if (obj.eventObj.eventFrom) { if (obj.eventObj.eventFrom) {
eventFrom = eventFrom =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}> <div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>From</p> <span>From&nbsp;&nbsp;:</span>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventFrom}> <a className="selectOrch" style={{ cursor: 'alias', marginLeft: '0.5em' }} target="_blank" href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventFrom}>
<img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} /> <img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} />
</a> </a>
<button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventFrom) }} > <button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventFrom) }} >
@ -55,15 +55,15 @@ const EventButton = (obj) => {
if (obj.eventObj.eventCaller) { if (obj.eventObj.eventCaller) {
eventCaller = eventCaller =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}> <div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>Caller</p> <span>Caller&nbsp;:</span>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventCaller}> <a className="selectOrch" style={{ cursor: 'alias', marginLeft: '0.5em' }} target="_blank" href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventCaller}>
<img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} /> <img alt="" src="livepeer.png" width="20em" height="20em" style={{ margin: 0 }} />
</a> </a>
<button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventCaller) }} > <button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventCaller) }} >
<span className="elipsText">🔎</span> <span className="elipsText">🔎</span>
</button> </button>
<button className="selectOrch" style={{ margin: 0, padding: 0, cursor: 'help' }} onClick={() => { dispatch(getOrchestratorInfo(obj.eventObj.eventCaller)) }} > <button className="selectOrch" style={{ margin: 0, padding: 0, cursor: 'help' }} onClick={() => { dispatch(getOrchestratorInfo(obj.eventObj.eventCaller)) }} >
<p className="elipsText elipsOnMobileExtra">{obj.eventObj.eventCaller}</p> <span className="elipsText elipsOnMobileExtra">{obj.eventObj.eventCaller}</span>
</button> </button>
</div> </div>
} }

View File

@ -6,6 +6,7 @@ import { getOrchestratorInfo, clearOrchestrator } from "./actions/livepeer";
import EventViewer from "./eventViewer"; import EventViewer from "./eventViewer";
import Orchestrator from "./orchestratorViewer"; import Orchestrator from "./orchestratorViewer";
import Stat from "./statViewer"; import Stat from "./statViewer";
import TicketViewer from './ticketViewer';
// Shows the EventViewer and other Livepeer related info // Shows the EventViewer and other Livepeer related info
const defaultMaxShown = 100; const defaultMaxShown = 100;
@ -15,6 +16,7 @@ const Livepeer = (obj) => {
const [maxAmount, setMaxAmount] = useState(defaultMaxShown); const [maxAmount, setMaxAmount] = useState(defaultMaxShown);
const [prefill, setPrefill] = useSearchParams(); const [prefill, setPrefill] = useSearchParams();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [showTickets, setShowTickets] = useState("");
const dispatch = useDispatch(); const dispatch = useDispatch();
const livepeer = useSelector((state) => state.livepeerstate); const livepeer = useSelector((state) => state.livepeerstate);
const [redirectToHome, setRedirectToHome] = useState(false); const [redirectToHome, setRedirectToHome] = useState(false);
@ -129,6 +131,18 @@ const Livepeer = (obj) => {
eventsList = livepeer.events; eventsList = livepeer.events;
} }
let ticketList = [];
let ticketBit;
if (livepeer.tickets) {
ticketList = livepeer.tickets;
}
if (showTickets) {
ticketBit =
<div className="rightContent">
<TicketViewer tickets={ticketList} forceVertical={true} />
</div>
}
let thisOrchObj; let thisOrchObj;
let headerString; let headerString;
if (livepeer.selectedOrchestrator) { if (livepeer.selectedOrchestrator) {
@ -164,6 +178,7 @@ const Livepeer = (obj) => {
</div > </div >
} }
return ( return (
<div style={{ margin: 0, padding: 0, height: '100%', width: '100%', overflow: 'hidden' }}> <div style={{ margin: 0, padding: 0, height: '100%', width: '100%', overflow: 'hidden' }}>
<div id='header'> <div id='header'>
@ -184,25 +199,34 @@ const Livepeer = (obj) => {
}}> }}>
<h4> Clear</h4> <h4> Clear</h4>
</button> </button>
<button className="homeButton" style={{ padding: 0, paddingRight: '1em', paddingLeft: '1em' }} onClick={() => { <p>Tickets</p>
setShowSidebar(!showSidebar); <div className="toggle-container" onClick={() => setShowTickets(!showTickets)}>
}}> <div className={`dialog-button ${showTickets ? "" : "disabled"}`}>
<h4>🔎 Sidebar</h4> {showTickets ? "Show" : "Hide"}
</button> </div>
<button className="homeButton" style={{ padding: 0, paddingRight: '1em', paddingLeft: '1em' }} onClick={() => { </div>
setShowFilter(!showFilter); <p>Sidebar</p>
}}> <div className="toggle-container" onClick={() => setShowSidebar(!showSidebar)}>
<h4>🛠 Filter</h4> <div className={`dialog-button ${showSidebar ? "" : "disabled"}`}>
</button> {showSidebar ? "Show" : "Hide"}
</div>
</div>
<p>Filter</p>
<div className="toggle-container" onClick={() => setShowFilter(!showFilter)}>
<div className={`dialog-button ${showFilter ? "" : "disabled"}`}>
{showFilter ? "Show" : "Hide"}
</div>
</div>
</div> </div>
</div> </div>
<div id='bodyContent'> <div id='bodyContent'>
{sidebar} {sidebar}
<div className="mainContent"> <div className="mainContent">
<EventViewer events={eventsList} searchTerm={searchTerm} setSearchTerm={setSearchTerm} <EventViewer events={eventsList} searchTerm={searchTerm} setSearchTerm={setSearchTerm}
forceVertical={true} showFilter={showFilter} setAmountFilter={setAmountFilter} amountFilter={amountFilter} forceVertical={true} showFilter={showFilter} setAmountFilter={setAmountFilter} amountFilter={amountFilter}
maxAmount={maxAmount} setMaxAmount={setMaxAmount}/> maxAmount={maxAmount} setMaxAmount={setMaxAmount} />
</div> </div>
{ticketBit}
</div> </div>
</div > </div >
); );

View File

@ -4,7 +4,7 @@ import {
getVisitorStats getVisitorStats
} from "./actions/user"; } from "./actions/user";
import { import {
getQuotes, getBlockchainData, getEvents, getCurrentOrchestratorInfo getQuotes, getBlockchainData, getEvents, getCurrentOrchestratorInfo, getTickets
} from "./actions/livepeer"; } from "./actions/livepeer";
import { login } from "./actions/session"; import { login } from "./actions/session";
@ -24,6 +24,7 @@ const Startup = (obj) => {
dispatch(getEvents()); dispatch(getEvents());
dispatch(getBlockchainData()); dispatch(getBlockchainData());
dispatch(getCurrentOrchestratorInfo()); dispatch(getCurrentOrchestratorInfo());
dispatch(getTickets());
}); });
} }

View File

@ -41,7 +41,7 @@ const Orchestrator = (obj) => {
totalVolumeUSD={obj.thisOrchestrator.totalVolumeUSD} totalVolumeUSD={obj.thisOrchestrator.totalVolumeUSD}
delegator={obj.thisOrchestrator.delegator} delegator={obj.thisOrchestrator.delegator}
/> />
<OrchDelegatorViewer delegators={obj.thisOrchestrator.delegators} forceVertical={obj.forceVertical} /> <OrchDelegatorViewer delegators={obj.thisOrchestrator.delegators} />
</div> </div>
</div> </div>
) )
@ -57,7 +57,7 @@ const Orchestrator = (obj) => {
totalVolumeUSD={obj.thisOrchestrator.totalVolumeUSD} totalVolumeUSD={obj.thisOrchestrator.totalVolumeUSD}
delegator={obj.thisOrchestrator.delegator} delegator={obj.thisOrchestrator.delegator}
/> />
<OrchDelegatorViewer delegators={obj.thisOrchestrator.delegators} forceVertical={obj.forceVertical} /> <OrchDelegatorViewer delegators={obj.thisOrchestrator.delegators} />
</div> </div>
</div> </div>
) )

View File

@ -4,7 +4,8 @@ import {
RECEIVE_EVENTS, RECEIVE_EVENTS,
RECEIVE_ORCHESTRATOR, RECEIVE_ORCHESTRATOR,
RECEIVE_CURRENT_ORCHESTRATOR, RECEIVE_CURRENT_ORCHESTRATOR,
CLEAR_ORCHESTRATOR CLEAR_ORCHESTRATOR,
RECEIVE_TICKETS
} from "../../actions/livepeer"; } from "../../actions/livepeer";
export default (state = {}, { type, message }) => { export default (state = {}, { type, message }) => {
@ -22,6 +23,8 @@ export default (state = {}, { type, message }) => {
return { ...state, selectedOrchestrator: message }; return { ...state, selectedOrchestrator: message };
case CLEAR_ORCHESTRATOR: case CLEAR_ORCHESTRATOR:
return { ...state, selectedOrchestrator: null }; return { ...state, selectedOrchestrator: null };
case RECEIVE_TICKETS:
return { ...state, tickets: message };
default: default:
return { ...state }; return { ...state };
} }

View File

@ -171,6 +171,12 @@ svg {
flex-basis: 0; flex-basis: 0;
flex-grow: 999; flex-grow: 999;
} }
.rightContent {
overflow: hidden;
justify-content: center;
align-content: center;
align-items: center;
}
.fullGrafana { .fullGrafana {
@ -508,6 +514,46 @@ svg {
border-radius: 1em; border-radius: 1em;
} }
.toggle-container {
width: 70px;
background-color: #c4c4c4;
cursor: pointer;
user-select: none;
border-radius: 3px;
padding: 2px;
height: 32px;
position: relative;
}
.dialog-button {
font-size: 14px;
line-height: 16px;
font-weight: bold;
cursor: pointer;
background-color: #002b49;
color: white;
padding: 8px 12px;
border-radius: 18px;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
min-width: 46px;
display: flex;
justify-content: center;
align-items: center;
width: 38px;
min-width: unset;
border-radius: 3px;
box-sizing: border-box;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
position: absolute;
left: 34px;
transition: all 0.3s ease;
}
.disabled {
background-color: #384e5c;
left: 2px;
}
@media (max-aspect-ratio: 1/1) { @media (max-aspect-ratio: 1/1) {
.fullGrafana { .fullGrafana {
width: calc(100vw - 2em); width: calc(100vw - 2em);

49
src/ticketViewer.js Normal file
View File

@ -0,0 +1,49 @@
import React, { useState } from "react";
import EventButton from "./eventButton";
import ScrollContainer from 'react-indiana-drag-scroll';
const TicketViewer = (obj) => {
console.log("Rendering TicketViewer");
let unfiltered = 0;
let prevBlock = 0;
let ticketList = [];
for (const ticketObj of obj.tickets) {
unfiltered++;
if (prevBlock === ticketObj.transactionBlock) {
ticketList.push(<EventButton
key={ticketObj.transactionHash + unfiltered}
eventObj={ticketObj}
setSearchTerm={obj.setSearchTerm}
/>);
} else {
prevBlock = ticketObj.transactionBlock;
ticketList.push(<EventButton
key={ticketObj.transactionHash + unfiltered}
eventObj={ticketObj}
isFirstOfBlock={prevBlock}
time={ticketObj.transactionTime}
setSearchTerm={obj.setSearchTerm}
/>);
}
}
return (
<div className="strokeSmollLeft" style={{ padding: 0, margin: 0, height: 'calc( 100vh - 50px)' }}>
<div className="row" style={{ padding: 0, margin: 0, width: '100%', height: '100%' }}>
<div className="stroke roundedOpaque" style={{ padding: 0, margin: 0, width: 'unset', height: '100%', marginRight: '1em', overflow: 'hidden', marginTop: '1em', overflowX: 'scroll' }}>
<div className="content-wrapper" style={{ width: '100%' }}>
<ScrollContainer className="overflow-container" hideScrollbars={false}>
<div className="overflow-content" style={{ cursor: 'grab', paddingTop: 0 }}>
<div className={obj.forceVertical ? "flexContainer forceWrap" : "flexContainer"} style={{ margin: 0, textAlign: 'center', alignItems: 'center', justifyContent: 'center' }}>
{ticketList}
</div>
</div>
</ScrollContainer>
</div>
</div>
</div>
</div>
)
}
export default TicketViewer;

View File

@ -27,6 +27,15 @@ export const getEvents = () => (
}) })
); );
export const getTickets = () => (
fetch("api/livepeer/getTickets", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
})
);
export const getCurrentOrchestratorInfo = () => ( export const getCurrentOrchestratorInfo = () => (
fetch("api/livepeer/getOrchestrator", { fetch("api/livepeer/getOrchestrator", {
method: "GET", method: "GET",