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 Event from '../models/event';
import Block from '../models/block';
import Ticket from '../models/ticketEvent'
const apiRouter = express.Router();
import {
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!
let eventsCache = [];
let latestMissedDuringSync = 0;
let lastBlockDataAdded = 0;
let latestBlockInChain = 0;
let lastBlockEvents = 0;
let lastBlockTickets = 0;
let syncCache = [];
let ticketsCache = [];
let ticketsSyncCache = [];
// https://arbiscan.io/address/0x35Bcf3c30594191d53231E4FF333E8A770453e40#events
let BondingManagerTargetJson;
let BondingManagerTargetAbi;
let BondingManagerProxyAddr;
let contractInstance;
let bondingManagerContract;
let TicketBrokerTargetJson;
let TicketBrokerTargetAbi;
let TicketBrokerTargetAddr;
let ticketBrokerContract;
if (!CONF_SIMPLE_MODE) {
// Listen for events on the bonding manager contract
BondingManagerTargetJson = fs.readFileSync('src/abi/BondingManagerTarget.json');
BondingManagerTargetAbi = JSON.parse(BondingManagerTargetJson);
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 = [];
@ -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
let isSyncing = true;
let isSyncRunning = false;
let isEventSyncing = false;
let isTicketSyncing = false;
// Start Listening for live updates
var BondingManagerProxyListener;
var TicketBrokerProxyListener;
if (!CONF_SIMPLE_MODE) {
BondingManagerProxyListener = contractInstance.events.allEvents(async (error, event) => {
BondingManagerProxyListener = bondingManagerContract.events.allEvents(async (error, event) => {
try {
if (error) {
throw error
@ -157,14 +174,50 @@ if (!CONF_SIMPLE_MODE) {
}
});
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
const doSync = function () {
console.log("Starting sync process");
isSyncRunning = true;
// Syncs events database
const syncEvents = function () {
console.log("Starting sync process for Bonding Manager events");
isEventSyncing = true;
// 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 {
if (error) {
throw error
@ -172,8 +225,8 @@ const doSync = function () {
let size = events.length;
console.log("Parsing " + size + " events");
for (const event of events) {
if (event.blockNumber > lastBlockDataAdded) {
lastBlockDataAdded = event.blockNumber;
if (event.blockNumber > lastBlockEvents) {
lastBlockEvents = event.blockNumber;
}
const thisBlock = await getBlock(event.blockNumber);
const eventObj = {
@ -195,7 +248,46 @@ const doSync = function () {
catch (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) {
@ -206,6 +298,13 @@ function sleep(ms) {
const handleSync = async function () {
// 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({}, {
address: 1,
transactionHash: 1,
@ -217,31 +316,54 @@ const handleSync = async function () {
_id: 0
});
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
for (var idx = 0; idx < eventsCache.length; idx++) {
const thisBlock = eventsCache[idx];
if (thisBlock.blockNumber > lastBlockDataAdded) {
lastBlockDataAdded = thisBlock.blockNumber;
if (thisBlock.blockNumber > lastBlockEvents) {
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
const latestBlock = await web3layer2.eth.getBlockNumber();
if (latestBlock > latestMissedDuringSync) {
latestMissedDuringSync = latestBlock;
if (latestBlock > latestBlockInChain) {
latestBlockInChain = latestBlock;
}
console.log("Parsed up to block " + lastBlockDataAdded + " out of " + latestMissedDuringSync + " blocks");
// Get all parsed blocks
blockCache = await Block.find({}, {
blockNumber: 1,
blockTime: 1
});
console.log("Retrieved existing Blocks of size " + blockCache.length);
doSync();
while (isSyncRunning) {
await sleep(1000);
console.log("Parsed " + lastBlockDataAdded + " out of " + latestMissedDuringSync + " blocks");
console.log("Latest L2 Eth block is " + latestBlockInChain);
console.log("Needs to sync " + (latestBlockInChain - lastBlockEvents) + " blocks for Events sync");
console.log("Needs to sync " + (latestBlockInChain - lastBlockTickets) + " blocks for Tickets sync");
syncTickets();
syncEvents();
while (isEventSyncing || isTicketSyncing) {
await sleep(3000);
if (isEventSyncing){
console.log("Parsed " + lastBlockEvents + " out of " + latestBlockInChain + " blocks for Event sync");
}
if (isTicketSyncing){
console.log("Parsed " + lastBlockTickets + " out of " + latestBlockInChain + " blocks for Ticket sync");
}
}
while (syncCache.length) {
while (syncCache.length || ticketsSyncCache.length) {
const liveEvents = syncCache;
syncCache = [];
for (const eventObj of liveEvents) {
@ -252,16 +374,27 @@ const handleSync = async function () {
}
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')
isSyncing = false;
};
if (!isSyncRunning && !CONF_SIMPLE_MODE && !CONF_DISABLE_SYNC) {
if (!isEventSyncing && !CONF_SIMPLE_MODE && !CONF_DISABLE_SYNC) {
handleSync();
}
// Splits of raw CMC object into coin quote data
const parseCmc = async function () {
return;
try {
cmcCache = await cmcClient.getTickers({ limit: 200 });
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
const parseOrchestrator = async function (reqAddr) {
reqAddr = reqAddr.toLowerCase();

View File

@ -7,11 +7,11 @@ const Block = (obj) => {
const [thisDate, thisTime] = dateObj.toISOString().split('T');
return (
<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" />
</a>
<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}
</a>
<p className="darkText">📅{thisDate} - {thisTime.split('.')[0]} </p>

View File

@ -4,7 +4,7 @@ import ReactTooltip from "react-tooltip";
const Address = (obj) => {
return (
<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 }}>
<img alt="" src="livepeer.png" width="20" height="20" />
<span className="elipsText elipsOnMobile">{obj.address}</span>

View File

@ -17,7 +17,7 @@ const OrchDelegatorViewer = (obj) => {
{
delegators.map((delObj, idx) => {
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} />
<div className="rowAlignRight" style={{ margin: 0 }}>
<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 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 thresholdFees = 0.00009;
@ -18,6 +21,7 @@ export const RECEIVE_EVENTS = "RECEIVE_EVENTS";
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";
const setQuotes = message => ({
type: RECEIVE_QUOTES, message
@ -37,6 +41,9 @@ const setOrchestratorInfo = message => ({
const clearOrchestratorInfo = () => ({
type: CLEAR_ORCHESTRATOR
})
const setTickets = message => ({
type: RECEIVE_TICKETS, message
});
export const getQuotes = () => async dispatch => {
const response = await apiUtil.getQuotes();
@ -204,7 +211,7 @@ export const getEvents = () => async dispatch => {
transactionUrl: currentUrl,
transactionBlock: currentBlock,
transactionTime: currentTime,
eventValue: amount
eventValue: amount
});
} else if (eventObj.name === "WithdrawFees") {
const amount = parseFloat(eventObj.data.amount) / 1000000000000000000;
@ -349,6 +356,83 @@ export const getEvents = () => async dispatch => {
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 => {
const response = await apiUtil.getCurrentOrchestratorInfo();
const data = await response.json();
@ -362,9 +446,9 @@ export const getOrchestratorInfo = (orchAddr) => async dispatch => {
const response = await apiUtil.getOrchestratorInfo(orchAddr);
const data = await response.json();
if (response.ok) {
if (data && data.id){
if (data && data.id) {
return dispatch(setOrchestratorInfo(data));
}else{
} else {
const response = await apiUtil.getOrchestratorByDelegator(orchAddr);
const data = await response.json();
if (response.ok) {

View File

@ -25,8 +25,8 @@ const EventButton = (obj) => {
if (obj.eventObj.eventTo) {
eventTo =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>To</p>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventTo}>
<span>To&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;:</span>
<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 }} />
</a>
<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) {
eventFrom =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>From</p>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventFrom}>
<span>From&nbsp;&nbsp;:</span>
<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 }} />
</a>
<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) {
eventCaller =
<div className="rowAlignLeft" style={{ width: '100%', margin: 0, marginLeft: '0.5em' }}>
<p>Caller</p>
<a className="selectOrch" style={{ cursor: 'alias' }} href={"https://explorer.livepeer.org/accounts/" + obj.eventObj.eventCaller}>
<span>Caller&nbsp;:</span>
<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 }} />
</a>
<button className="selectOrch" style={{ margin: 0, padding: '0.5em', cursor: 'pointer' }} onClick={() => { obj.setSearchTerm(obj.eventObj.eventCaller) }} >
<span className="elipsText">🔎</span>
</button>
<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>
</div>
}

View File

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

View File

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

View File

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

View File

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

View File

@ -171,6 +171,12 @@ svg {
flex-basis: 0;
flex-grow: 999;
}
.rightContent {
overflow: hidden;
justify-content: center;
align-content: center;
align-items: center;
}
.fullGrafana {
@ -508,6 +514,46 @@ svg {
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) {
.fullGrafana {
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 = () => (
fetch("api/livepeer/getOrchestrator", {
method: "GET",