mirror of
https://github.com/stronk-dev/LivepeerEvents.git
synced 2025-07-05 10:45:10 +02:00
Display rounds in the event viewer
This commit is contained in:
parent
fe2e75bb41
commit
2fb915fee4
557
backend/src/abi/RoundsManagerTarget.json
Normal file
557
backend/src/abi/RoundsManagerTarget.json
Normal file
File diff suppressed because one or more lines are too long
@ -5,51 +5,63 @@ const RoundSchema = new mongoose.Schema({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
lengthBlocks: {
|
transactionHash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
blockNumber: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
blockTime: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lengthBlocks: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
startBlock: {
|
startBlock: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
endBlock: {
|
endBlock: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
mintableTokens: {
|
mintableTokens: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
volumeEth: {
|
volumeEth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
volumeUsd: {
|
volumeUsd: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
totalActiveStake: {
|
totalActiveStake: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
totalSupply: {
|
totalSupply: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
participationRate: {
|
participationRate: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
movedStake: {
|
movedStake: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
newStake: {
|
newStake: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: false
|
||||||
}
|
}
|
||||||
}, { timestamps: false });
|
}, { timestamps: false });
|
||||||
|
|
||||||
const Round = mongoose.model('RoundSchema', RoundSchema);
|
const Round = mongoose.model('Round', RoundSchema);
|
||||||
export default Round;
|
export default Round;
|
@ -79,6 +79,10 @@ let TicketBrokerTargetJson;
|
|||||||
let TicketBrokerTargetAbi;
|
let TicketBrokerTargetAbi;
|
||||||
let TicketBrokerTargetAddr;
|
let TicketBrokerTargetAddr;
|
||||||
let ticketBrokerContract;
|
let ticketBrokerContract;
|
||||||
|
let RoundsManagerTargetJson;
|
||||||
|
let RoundsManagerTargetAbi;
|
||||||
|
let RoundsManagerTargetAddr;
|
||||||
|
let roundsManagerContract;
|
||||||
if (!CONF_SIMPLE_MODE) {
|
if (!CONF_SIMPLE_MODE) {
|
||||||
console.log("Loading contracts for smart contract events");
|
console.log("Loading contracts for smart contract events");
|
||||||
// Listen for events on the bonding manager contract
|
// Listen for events on the bonding manager contract
|
||||||
@ -91,6 +95,11 @@ if (!CONF_SIMPLE_MODE) {
|
|||||||
TicketBrokerTargetAbi = JSON.parse(TicketBrokerTargetJson);
|
TicketBrokerTargetAbi = JSON.parse(TicketBrokerTargetJson);
|
||||||
TicketBrokerTargetAddr = "0xa8bB618B1520E284046F3dFc448851A1Ff26e41B";
|
TicketBrokerTargetAddr = "0xa8bB618B1520E284046F3dFc448851A1Ff26e41B";
|
||||||
ticketBrokerContract = new web3layer2.eth.Contract(TicketBrokerTargetAbi.abi, TicketBrokerTargetAddr);
|
ticketBrokerContract = new web3layer2.eth.Contract(TicketBrokerTargetAbi.abi, TicketBrokerTargetAddr);
|
||||||
|
// Listen for events on the rounds manager contract
|
||||||
|
RoundsManagerTargetJson = fs.readFileSync('src/abi/RoundsManagerTarget.json');
|
||||||
|
RoundsManagerTargetAbi = JSON.parse(RoundsManagerTargetJson);
|
||||||
|
RoundsManagerTargetAddr = "0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f";
|
||||||
|
roundsManagerContract = new web3layer2.eth.Contract(RoundsManagerTargetAbi.abi, RoundsManagerTargetAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -155,11 +164,14 @@ let startedInitSync = false;
|
|||||||
let isSyncing = false;
|
let isSyncing = false;
|
||||||
let isEventSyncing = false;
|
let isEventSyncing = false;
|
||||||
let isTicketSyncing = false;
|
let isTicketSyncing = false;
|
||||||
|
let isRoundSyncing = false;
|
||||||
|
|
||||||
let eventsCache = [];
|
let eventsCache = [];
|
||||||
let latestBlockInChain = 0;
|
let latestBlockInChain = 0;
|
||||||
|
let latestL1Block = 0;
|
||||||
let lastBlockEvents = 0;
|
let lastBlockEvents = 0;
|
||||||
let lastBlockTickets = 0;
|
let lastBlockTickets = 0;
|
||||||
|
let lastBlockRounds = 0;
|
||||||
let ticketsCache = [];
|
let ticketsCache = [];
|
||||||
|
|
||||||
let alreadyHasAnyRefresh = {};
|
let alreadyHasAnyRefresh = {};
|
||||||
@ -1439,10 +1451,10 @@ Mutates the Event in the database to contain the round number
|
|||||||
|
|
||||||
let roundCache = [];
|
let roundCache = [];
|
||||||
|
|
||||||
const getRoundInfo = async function (blockNumber) {
|
const getRoundInfo = async function (roundNumber) {
|
||||||
// Get round info from gql
|
// Get round info from gql
|
||||||
const roundQuery = gql`{
|
const roundQuery = gql`{
|
||||||
rounds(where: {startBlock_lte: "${blockNumber}"}) {
|
rounds(where: {id: "${roundNumber}"}) {
|
||||||
id
|
id
|
||||||
length
|
length
|
||||||
startBlock
|
startBlock
|
||||||
@ -1460,56 +1472,97 @@ const getRoundInfo = async function (blockNumber) {
|
|||||||
`;
|
`;
|
||||||
const roundObj = await request("https://api.thegraph.com/subgraphs/name/livepeer/arbitrum-one", roundQuery);
|
const roundObj = await request("https://api.thegraph.com/subgraphs/name/livepeer/arbitrum-one", roundQuery);
|
||||||
// Not found
|
// Not found
|
||||||
if (!roundObj) {
|
if (!roundObj || !roundObj.rounds || !roundObj.rounds.length) {
|
||||||
console.log("No round found at block " + blockNumber);
|
console.log("No round found with number " + roundNumber);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
console.log("This functions is not implemented yet. Logging element to console...")
|
const thisRoundObj = roundObj.rounds[0];
|
||||||
console.log(roundObj);
|
|
||||||
// TODO filter out down to 1 round
|
|
||||||
return roundObj;
|
|
||||||
// Update cache
|
// Update cache
|
||||||
roundCache.push(roundObj);
|
var wasCached = false;
|
||||||
// Only save if the endBlock is elapsed
|
for (var idx = 0; idx < roundCache.length; idx++) {
|
||||||
if (latestBlockInChain > roundObj.endBlock) {
|
if (roundCache[idx].number == roundNumber){
|
||||||
const data = {
|
wasCached = true;
|
||||||
number: roundObj.number,
|
roundCache[idx].lengthBlocks = thisRoundObj.lengthBlocks;
|
||||||
|
roundCache[idx].startBlock = thisRoundObj.startBlock;
|
||||||
|
roundCache[idx].endBlock = thisRoundObj.endBlock;
|
||||||
|
roundCache[idx].mintableTokens = thisRoundObj.mintableTokens;
|
||||||
|
roundCache[idx].volumeEth = thisRoundObj.volumeETH;
|
||||||
|
roundCache[idx].volumeUsd = thisRoundObj.volumeUSD;
|
||||||
|
roundCache[idx].totalActiveStake = thisRoundObj.totalActiveStake;
|
||||||
|
roundCache[idx].totalSupply = thisRoundObj.totalSupply;
|
||||||
|
roundCache[idx].participationRate = thisRoundObj.participationRate;
|
||||||
|
roundCache[idx].movedStake = thisRoundObj.movedStake;
|
||||||
|
roundCache[idx].newStake = thisRoundObj.newStake;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FindAndUpdate
|
||||||
|
if (!CONF_DISABLE_DB) {
|
||||||
|
// Update DB entry
|
||||||
|
const doc = await MonthlyStat.findOneAndUpdate({
|
||||||
|
number: roundNumber
|
||||||
|
}, {
|
||||||
lengthBlocks: roundObj.lengthBlocks,
|
lengthBlocks: roundObj.lengthBlocks,
|
||||||
startBlock: roundObj.startBlock,
|
startBlock: roundObj.startBlock,
|
||||||
endBlock: roundObj.endBlock,
|
endBlock: roundObj.endBlock,
|
||||||
mintableTokens: roundObj.mintableTokens,
|
mintableTokens: roundObj.mintableTokens,
|
||||||
volumeEth: roundObj.volumeEth,
|
volumeEth: roundObj.volumeETH,
|
||||||
volumeUsd: roundObj.volumeUsd,
|
volumeUsd: roundObj.volumeUSD,
|
||||||
totalActiveStake: roundObj.totalActiveStake,
|
totalActiveStake: roundObj.totalActiveStake,
|
||||||
totalSupply: roundObj.totalSupply,
|
totalSupply: roundObj.totalSupply,
|
||||||
participationRate: roundObj.participationRate,
|
participationRate: roundObj.participationRate,
|
||||||
movedStake: roundObj.movedStake,
|
movedStake: roundObj.movedStake,
|
||||||
newStake: roundObj.newStake
|
newStake: roundObj.newStake
|
||||||
}
|
}, {
|
||||||
if (!CONF_DISABLE_DB) {
|
new: true
|
||||||
// TODO only create if nonexistent (find and update with upsert or something)
|
});
|
||||||
const dbObj = new Round(data);
|
if (!wasCached){
|
||||||
await dbObj.save();
|
roundCache.push({
|
||||||
|
number: doc.number,
|
||||||
|
transactionHash: doc.transactionHash,
|
||||||
|
blockNumber: doc.blockNumber,
|
||||||
|
lengthBlocks: doc.lengthBlocks,
|
||||||
|
startBlock: doc.startBlock,
|
||||||
|
endBlock: doc.endBlock,
|
||||||
|
mintableTokens: doc.mintableTokens,
|
||||||
|
volumeETH: doc.volumeETH,
|
||||||
|
volumeUSD: doc.volumeUSD,
|
||||||
|
totalActiveStake: doc.totalActiveStake,
|
||||||
|
totalSupply: doc.totalSupply,
|
||||||
|
participationRate: doc.participationRate,
|
||||||
|
movedStake: doc.movedStake,
|
||||||
|
newStake: doc.newStake
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiRouter.post("/getRoundAtBlock", async (req, res) => {
|
apiRouter.post("/getRoundInfo", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { blockNumber } = req.body;
|
const now = new Date().getTime();
|
||||||
if (blockNumber) {
|
const { roundNumber } = req.body;
|
||||||
console.log("Getting round info for block " + blockNumber);
|
if (roundNumber) {
|
||||||
|
console.log("Getting round info for round " + roundNumber);
|
||||||
// See if it is cached
|
// See if it is cached
|
||||||
for (const thisRound of roundCache) {
|
for (const thisRound of roundCache) {
|
||||||
if (thisRound.startBlock <= blockNumber && thisRound.endBlock >= blockNumber) {
|
if (thisRound.round == roundNumber) {
|
||||||
|
// Check if it contains one of the detailed fields
|
||||||
|
if (thisRound.endBlock) {
|
||||||
|
// Check timeout if the round has not elapsed
|
||||||
|
if (thisRound.endBlock >= latestL1Block
|
||||||
|
&& (now - thisRound.blockTime) > 1800000) {
|
||||||
|
console.log('Round should update round details');
|
||||||
|
break;
|
||||||
|
}
|
||||||
res.send(thisRound);
|
res.send(thisRound);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
console.log('Round should init round details');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get block info from thegraph
|
// Get block info from thegraph
|
||||||
console.log("Getting round info for block " + blockNumber);
|
const thisRoundInfo = await getRoundInfo(roundNumber);
|
||||||
const thisRoundInfo = await getRoundInfo(blockNumber);
|
|
||||||
res.send(thisRoundInfo);
|
res.send(thisRoundInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1539,7 +1592,7 @@ let hasError = false;
|
|||||||
|
|
||||||
// Syncs events database
|
// Syncs events database
|
||||||
const syncEvents = function (toBlock) {
|
const syncEvents = function (toBlock) {
|
||||||
console.log("Starting sync process for Bonding Manager events to block " + toBlock);
|
console.log("Starting sync process for Bonding Manager events for blocks " + lastBlockEvents + "->" + toBlock);
|
||||||
isEventSyncing = true;
|
isEventSyncing = true;
|
||||||
let lastTxSynced = 0;
|
let lastTxSynced = 0;
|
||||||
// Then do a sync from last found until latest known
|
// Then do a sync from last found until latest known
|
||||||
@ -1601,7 +1654,7 @@ const syncEvents = function (toBlock) {
|
|||||||
}
|
}
|
||||||
// Syncs tickets database
|
// Syncs tickets database
|
||||||
const syncTickets = function (toBlock) {
|
const syncTickets = function (toBlock) {
|
||||||
console.log("Starting sync process for Ticket Broker events to block " + toBlock);
|
console.log("Starting sync process for Ticket Broker events for blocks " + lastBlockTickets + "->" + toBlock);
|
||||||
isTicketSyncing = true;
|
isTicketSyncing = true;
|
||||||
// Then do a sync from last found until latest known
|
// Then do a sync from last found until latest known
|
||||||
ticketBrokerContract.getPastEvents("allEvents", { fromBlock: lastBlockTickets + 1, toBlock: toBlock }, async (error, events) => {
|
ticketBrokerContract.getPastEvents("allEvents", { fromBlock: lastBlockTickets + 1, toBlock: toBlock }, async (error, events) => {
|
||||||
@ -1650,6 +1703,58 @@ const syncTickets = function (toBlock) {
|
|||||||
isTicketSyncing = false;
|
isTicketSyncing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Syncs rounds database
|
||||||
|
const syncRounds = function (toBlock) {
|
||||||
|
console.log("Starting sync process for Rounds Manager events for blocks " + lastBlockRounds + "->" + toBlock);
|
||||||
|
isRoundSyncing = true;
|
||||||
|
// Then do a sync from last found until latest known
|
||||||
|
roundsManagerContract.getPastEvents("allEvents", { fromBlock: lastBlockRounds + 1, toBlock: toBlock }, async (error, events) => {
|
||||||
|
try {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
let size = events.length;
|
||||||
|
console.log("Parsing " + size + " rounds");
|
||||||
|
if (!size) {
|
||||||
|
if (toBlock == 'latest') {
|
||||||
|
lastBlockRounds = latestBlockInChain;
|
||||||
|
} else {
|
||||||
|
lastBlockRounds = toBlock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.blockNumber > lastBlockRounds) {
|
||||||
|
lastBlockRounds = event.blockNumber;
|
||||||
|
}
|
||||||
|
// Only parse initRound events
|
||||||
|
if (event.event != 'NewRound') {
|
||||||
|
console.log('Skipping Round Event of type ' + event.event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const thisBlock = await getBlock(event.blockNumber);
|
||||||
|
const eventObj = {
|
||||||
|
number: event.returnValues.round,
|
||||||
|
transactionHash: event.transactionHash,
|
||||||
|
blockNumber: thisBlock.number,
|
||||||
|
blockTime: thisBlock.timestamp
|
||||||
|
}
|
||||||
|
// Cache & store
|
||||||
|
if (!CONF_DISABLE_DB) {
|
||||||
|
const dbObj = new Round(eventObj);
|
||||||
|
await dbObj.save();
|
||||||
|
}
|
||||||
|
roundCache.push(eventObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log("FATAL ERROR: ", err);
|
||||||
|
hasError = true;
|
||||||
|
isRoundSyncing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRoundSyncing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieves stuff from DB on first boot
|
// Retrieves stuff from DB on first boot
|
||||||
const initSync = async function () {
|
const initSync = async function () {
|
||||||
@ -1863,6 +1968,9 @@ const initSync = async function () {
|
|||||||
// Get all round info
|
// Get all round info
|
||||||
roundCache = await Round.find({}, {
|
roundCache = await Round.find({}, {
|
||||||
number: 1,
|
number: 1,
|
||||||
|
transactionHash: 1,
|
||||||
|
blockNumber: 1,
|
||||||
|
blockTime: 1,
|
||||||
lengthBlocks: 1,
|
lengthBlocks: 1,
|
||||||
startBlock: 1,
|
startBlock: 1,
|
||||||
endBlock: 1,
|
endBlock: 1,
|
||||||
@ -1876,6 +1984,15 @@ const initSync = async function () {
|
|||||||
newStake: 1,
|
newStake: 1,
|
||||||
_id: 0
|
_id: 0
|
||||||
})
|
})
|
||||||
|
console.log("Retrieved existing rounds of size " + roundCache.length);
|
||||||
|
// Then determine latest block number parsed based on collection
|
||||||
|
for (var idx = 0; idx < roundCache.length; idx++) {
|
||||||
|
const thisRound = roundCache[idx];
|
||||||
|
if (thisRound.blockNumber > lastBlockRounds) {
|
||||||
|
lastBlockRounds = thisRound.blockNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Latest Round block parsed is " + lastBlockRounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cycle = 0;
|
let cycle = 0;
|
||||||
@ -1889,8 +2006,13 @@ const handleSync = async function () {
|
|||||||
cycle++;
|
cycle++;
|
||||||
console.log('Starting new sync cycle #' + cycle);
|
console.log('Starting new sync cycle #' + cycle);
|
||||||
isSyncing = true;
|
isSyncing = true;
|
||||||
// Get latest block in chain
|
// Get latest blocks in chain
|
||||||
const latestBlock = await web3layer2.eth.getBlockNumber();
|
var latestBlock = await web3layer1.eth.getBlockNumber();
|
||||||
|
if (latestBlock > latestL1Block) {
|
||||||
|
latestL1Block = latestBlock;
|
||||||
|
console.log("Latest L1 Eth block changed to " + latestL1Block);
|
||||||
|
}
|
||||||
|
latestBlock = await web3layer2.eth.getBlockNumber();
|
||||||
if (latestBlock > latestBlockInChain) {
|
if (latestBlock > latestBlockInChain) {
|
||||||
latestBlockInChain = latestBlock;
|
latestBlockInChain = latestBlock;
|
||||||
console.log("Latest L2 Eth block changed to " + latestBlockInChain);
|
console.log("Latest L2 Eth block changed to " + latestBlockInChain);
|
||||||
@ -1904,6 +2026,7 @@ const handleSync = async function () {
|
|||||||
}
|
}
|
||||||
console.log("Needs to sync " + (latestBlockInChain - lastBlockEvents) + " blocks for Events sync");
|
console.log("Needs to sync " + (latestBlockInChain - lastBlockEvents) + " blocks for Events sync");
|
||||||
console.log("Needs to sync " + (latestBlockInChain - lastBlockTickets) + " blocks for Tickets sync");
|
console.log("Needs to sync " + (latestBlockInChain - lastBlockTickets) + " blocks for Tickets sync");
|
||||||
|
console.log("Needs to sync " + (latestBlockInChain - lastBlockRounds) + " blocks for Rounds sync");
|
||||||
// Batch requests when sync is large, mark if we are going to reach latestBlockInChain in this round
|
// Batch requests when sync is large, mark if we are going to reach latestBlockInChain in this round
|
||||||
let getFinalTickets = false;
|
let getFinalTickets = false;
|
||||||
let toTickets = 'latest';
|
let toTickets = 'latest';
|
||||||
@ -1919,11 +2042,20 @@ const handleSync = async function () {
|
|||||||
} else {
|
} else {
|
||||||
getFinalEvents = true;
|
getFinalEvents = true;
|
||||||
}
|
}
|
||||||
|
let getFinalRounds = false;
|
||||||
|
let toRounds = 'latest';
|
||||||
|
if (latestBlock - lastBlockRounds > 1000000) {
|
||||||
|
toRounds = lastBlockRounds + 1000000;
|
||||||
|
} else {
|
||||||
|
getFinalRounds = true;
|
||||||
|
}
|
||||||
// Start initial sync for this sync round
|
// Start initial sync for this sync round
|
||||||
syncTickets(toTickets);
|
syncTickets(toTickets);
|
||||||
syncEvents(toEvents);
|
syncEvents(toEvents);
|
||||||
|
syncRounds(toRounds);
|
||||||
// Then loop until we have reached the last known block
|
// Then loop until we have reached the last known block
|
||||||
while (isEventSyncing || isTicketSyncing || !getFinalTickets || !getFinalEvents) {
|
while (isEventSyncing || isTicketSyncing || isRoundSyncing
|
||||||
|
|| !getFinalTickets || !getFinalEvents || !getFinalRounds) {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
throw ("Error while syncing");
|
throw ("Error while syncing");
|
||||||
@ -1952,6 +2084,18 @@ const handleSync = async function () {
|
|||||||
}
|
}
|
||||||
syncTickets(toTickets);
|
syncTickets(toTickets);
|
||||||
}
|
}
|
||||||
|
if (isRoundSyncing) {
|
||||||
|
console.log("Parsed " + lastBlockRounds + " out of " + latestBlockInChain + " blocks for Round sync");
|
||||||
|
} else if (!getFinalRounds) {
|
||||||
|
// Start next batch for tickets
|
||||||
|
toRounds = 'latest';
|
||||||
|
if (latestBlock - lastBlockRounds > 1000000) {
|
||||||
|
toRounds = lastBlockRounds + 1000000;
|
||||||
|
} else {
|
||||||
|
getFinalRounds = true;
|
||||||
|
}
|
||||||
|
syncRounds(toRounds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isSyncing = false;
|
isSyncing = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -1965,6 +2109,7 @@ const handleSync = async function () {
|
|||||||
console.log("latestBlockInChain " + latestBlockInChain);
|
console.log("latestBlockInChain " + latestBlockInChain);
|
||||||
console.log("lastBlockEvents " + lastBlockEvents);
|
console.log("lastBlockEvents " + lastBlockEvents);
|
||||||
console.log("lastBlockTickets " + lastBlockTickets);
|
console.log("lastBlockTickets " + lastBlockTickets);
|
||||||
|
console.log("lastBlockRounds " + lastBlockRounds);
|
||||||
isSyncing = false;
|
isSyncing = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleSync();
|
handleSync();
|
||||||
@ -2305,7 +2450,7 @@ const mutateDynamicStatsFromDB = async function (orchestratorObj) {
|
|||||||
latestCommission: 1,
|
latestCommission: 1,
|
||||||
latestTotalStake: 1
|
latestTotalStake: 1
|
||||||
});
|
});
|
||||||
if (!doc){
|
if (!doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let oldFeeCommission = -1;
|
let oldFeeCommission = -1;
|
||||||
|
25
dumps/rounds.json
Normal file
25
dumps/rounds.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
address: '0xdd6f56DcC28D3F5f27084381fE8Df634985cc39f',
|
||||||
|
blockHash: '0x733753ab3a1ecf3edbbebc075f85827b259295b3a7443d45c633b1b6c3672eef',
|
||||||
|
blockNumber: 8761883,
|
||||||
|
logIndex: 1,
|
||||||
|
removed: false,
|
||||||
|
transactionHash: '0xd8609ae0a52e0bb8a1eda28634e5a804607eb76a156e9e30682dd1ff748a107a',
|
||||||
|
transactionIndex: 0,
|
||||||
|
id: 'log_cbc32efd',
|
||||||
|
returnValues: Result {
|
||||||
|
'0': '2513',
|
||||||
|
'1': '0x2f180b9a2c759abc9c77372f2a339b16191fe782304318e818b0b3ed0706a216',
|
||||||
|
round: '2513',
|
||||||
|
blockHash: '0x2f180b9a2c759abc9c77372f2a339b16191fe782304318e818b0b3ed0706a216'
|
||||||
|
},
|
||||||
|
event: 'NewRound',
|
||||||
|
signature: '0x22f2fc17c5daf07db2379b3a03a8ef20a183f761097a58fce219c8a14619e786',
|
||||||
|
raw: {
|
||||||
|
data: '0x2f180b9a2c759abc9c77372f2a339b16191fe782304318e818b0b3ed0706a216',
|
||||||
|
topics: [
|
||||||
|
'0x22f2fc17c5daf07db2379b3a03a8ef20a183f761097a58fce219c8a14619e786',
|
||||||
|
'0x00000000000000000000000000000000000000000000000000000000000009d1'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ export const SET_ALL_ACTIVATE_EVENTS = "SET_ALL_ACTIVATE_EVENTS";
|
|||||||
export const SET_ALL_UNBOND_EVENTS = "SET_ALL_UNBOND_EVENTS";
|
export const SET_ALL_UNBOND_EVENTS = "SET_ALL_UNBOND_EVENTS";
|
||||||
export const SET_ALL_STAKE_EVENTS = "SET_ALL_STAKE_EVENTS";
|
export const SET_ALL_STAKE_EVENTS = "SET_ALL_STAKE_EVENTS";
|
||||||
export const SET_ALL_ROUNDS = "SET_ALL_ROUNDS";
|
export const SET_ALL_ROUNDS = "SET_ALL_ROUNDS";
|
||||||
export const SET_ADD_ROUNDS = "SET_ADD_ROUNDS";
|
export const SET_ROUND = "SET_ROUND";
|
||||||
|
|
||||||
const setQuotes = message => ({
|
const setQuotes = message => ({
|
||||||
type: RECEIVE_QUOTES, message
|
type: RECEIVE_QUOTES, message
|
||||||
@ -131,8 +131,8 @@ const setAllRounds = message => ({
|
|||||||
type: SET_ALL_ROUNDS, message
|
type: SET_ALL_ROUNDS, message
|
||||||
});
|
});
|
||||||
|
|
||||||
const setAddRound = message => ({
|
const populateRound = message => ({
|
||||||
type: SET_ADD_ROUNDS, message
|
type: SET_ROUND, message
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getQuotes = () => async dispatch => {
|
export const getQuotes = () => async dispatch => {
|
||||||
@ -434,10 +434,10 @@ export const getAllRounds = () => async dispatch => {
|
|||||||
return dispatch(receiveErrors(data));
|
return dispatch(receiveErrors(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRoundAtBlock = (addr) => async dispatch => {
|
export const getRoundInfo = (round) => async dispatch => {
|
||||||
const response = await apiUtil.getRoundAtBlock(addr);
|
const response = await apiUtil.getRoundInfo(round);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return dispatch(setAddRound(data));
|
return dispatch(populateRound(data));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,49 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import {
|
|
||||||
getRoundAtBlock
|
|
||||||
} from "../actions/livepeer";
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
const Block = (obj) => {
|
const Block = (obj) => {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const livepeer = useSelector((state) => state.livepeerstate);
|
|
||||||
const [roundInfo, setRoundInfo] = useState(null);
|
|
||||||
const [hasRefreshed, setRefresh] = useState(false);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// let thisInfo = null;
|
|
||||||
// for (const round of livepeer.rounds) {
|
|
||||||
// if (round.startBlock <= obj.block && round.endBlock >= obj.block) {
|
|
||||||
// thisInfo = round;
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // If it was not cached at all
|
|
||||||
// if (thisInfo == null && !hasRefreshed) {
|
|
||||||
// console.log("Refresh due to non-existing round containing this block");
|
|
||||||
// setRefresh(true);
|
|
||||||
// dispatch(getRoundAtBlock(obj.block));
|
|
||||||
// }
|
|
||||||
// if (thisInfo && thisInfo != roundInfo) {
|
|
||||||
// console.log("Setting block info obj");
|
|
||||||
// setRoundInfo(thisInfo);
|
|
||||||
// }
|
|
||||||
// }, [livepeer.rounds]);
|
|
||||||
|
|
||||||
const thisEpoch = obj.time;
|
const thisEpoch = obj.time;
|
||||||
var dateObj = new Date(0);
|
var dateObj = new Date(0);
|
||||||
dateObj.setUTCSeconds(thisEpoch);
|
dateObj.setUTCSeconds(thisEpoch);
|
||||||
const thisLocalDate = dateObj.toLocaleString();
|
const thisLocalDate = dateObj.toLocaleString();
|
||||||
const thisOffset = (-dateObj.getTimezoneOffset() / 60);
|
const thisOffset = (-dateObj.getTimezoneOffset() / 60);
|
||||||
|
|
||||||
// Get round info
|
|
||||||
let thisRoundInfo;
|
|
||||||
if (roundInfo) {
|
|
||||||
thisRoundInfo = <p style={{ overflowWrap: 'break-word' }}>
|
|
||||||
Round {thisRoundInfo.number}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rowAlignLeft" style={{ margin: 0, marginTop: '1em', width: '100%' }}>
|
<div className="rowAlignLeft" style={{ margin: 0, marginTop: '1em', width: '100%' }}>
|
||||||
<a className="selectOrch" style={{ cursor: 'alias', margin: 0 }} target="_blank" rel="noopener noreferrer" href={obj.url}>
|
<a className="selectOrch" style={{ cursor: 'alias', margin: 0 }} target="_blank" rel="noopener noreferrer" href={obj.url}>
|
||||||
@ -52,7 +15,6 @@ const Block = (obj) => {
|
|||||||
<a className="selectOrch" style={{ cursor: 'alias', margin: 0 }} target="_blank" rel="noopener noreferrer" href={"https://arbiscan.io/block/" + obj.block}>
|
<a className="selectOrch" style={{ cursor: 'alias', margin: 0 }} target="_blank" rel="noopener noreferrer" href={"https://arbiscan.io/block/" + obj.block}>
|
||||||
<h3 style={{ padding: '0.2em', cursor: 'alias' }}>🔗</h3>
|
<h3 style={{ padding: '0.2em', cursor: 'alias' }}>🔗</h3>
|
||||||
</a>
|
</a>
|
||||||
{thisRoundInfo}
|
|
||||||
<span className="rowAlignRight darkText mobileSmallerFont" style={{ margin: 0 }}>
|
<span className="rowAlignRight darkText mobileSmallerFont" style={{ margin: 0 }}>
|
||||||
<p style={{ overflowWrap: 'break-word' }}>
|
<p style={{ overflowWrap: 'break-word' }}>
|
||||||
📅 {thisLocalDate}
|
📅 {thisLocalDate}
|
||||||
|
110
src/components/RoundViewer.js
Normal file
110
src/components/RoundViewer.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
getRoundInfo
|
||||||
|
} from "../actions/livepeer";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Popover } from '@mantine/core';
|
||||||
|
import { getOrchestratorByDelegator } from "../util/livepeer";
|
||||||
|
|
||||||
|
const Round = (obj) => {
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [hasRefreshed, setRefresh] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If it was not cached at all
|
||||||
|
if (obj.round && obj.round.number && !obj.round.endBlock && !hasRefreshed) {
|
||||||
|
console.log("Pulling round info for round " + obj.round.number);
|
||||||
|
setRefresh(true);
|
||||||
|
dispatch(getRoundInfo(obj.round.number));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const thisEpoch = obj.time;
|
||||||
|
var dateObj = new Date(0);
|
||||||
|
dateObj.setUTCSeconds(thisEpoch);
|
||||||
|
const thisLocalDate = dateObj.toLocaleString();
|
||||||
|
const thisOffset = (-dateObj.getTimezoneOffset() / 60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row" style={{ paddingLeft: '1em', paddingRight: '1em' }}>
|
||||||
|
<div className="rowAlignLeft" style={{ margin: 0, marginTop: '1em', width: '100%' }}>
|
||||||
|
<a className="selectOrch" style={{ cursor: 'alias', margin: 0 }} target="_blank" rel="noopener noreferrer" href={"https://arbiscan.io/block/" + obj.round.blockNumber}>
|
||||||
|
<h3 style={{ padding: '0.2em', cursor: 'alias' }}>🔗</h3>
|
||||||
|
</a>
|
||||||
|
<Popover className="strokeSmollLeft" style={{ cursor: 'pointer', marginTop: '0.2em', marginBottom: '0.2em' }}
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => setOpened(false)}
|
||||||
|
target={
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }} onClick={() => setOpened((o) => !o)}>
|
||||||
|
Round {obj.round.number}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
width={260}
|
||||||
|
position="right"
|
||||||
|
withArrow
|
||||||
|
>
|
||||||
|
<div className="strokeSmollLeft">
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
Round {obj.round.number}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
obj.round.mintableTokens ?
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
Has {obj.round.mintableTokens.toFixed(2)} mintable tokens
|
||||||
|
</p>
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
obj.round.volumeEth && obj.round.volumeUsd ?
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
A volume of {obj.round.volumeEth.toFixed(2)} Eth ({obj.round.volumeUsd.toFixed(2)}$)
|
||||||
|
</p>
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
obj.round.totalSupply && obj.round.totalActiveStake ?
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
A total supply of {obj.round.totalSupply.toFixed(2)} LPT, of which {obj.round.totalActiveStake.toFixed(2)} is staked ({(obj.round.participationRate * 100).toFixed(2)}%)
|
||||||
|
</p>
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
obj.round.newStake ?
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
{obj.round.newStake.toFixed(2)} LPT new stake
|
||||||
|
</p>
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
obj.round.movedStake ?
|
||||||
|
<div className="row">
|
||||||
|
<p className="darkText" style={{ overflowWrap: 'break-word' }}>
|
||||||
|
{obj.round.movedStake.toFixed(2)} LPT stake moved around
|
||||||
|
</p>
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Popover>
|
||||||
|
<span className="rowAlignRight darkText mobileSmallerFont" style={{ margin: 0 }}>
|
||||||
|
<p style={{ overflowWrap: 'break-word' }}>
|
||||||
|
📅 {thisLocalDate}
|
||||||
|
</p>
|
||||||
|
{thisOffset != 0 ? <p className='darkTextSmoll' style={{ overflowWrap: 'break-word' }}>
|
||||||
|
({thisOffset > 0 ? "+" : ""}{thisOffset})
|
||||||
|
</p> : null
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Round;
|
@ -4,6 +4,7 @@ import ScrollContainer from 'react-indiana-drag-scroll';
|
|||||||
import { Pagination, Title } from "@mantine/core";
|
import { Pagination, Title } from "@mantine/core";
|
||||||
import Filter from './filterComponent';
|
import Filter from './filterComponent';
|
||||||
import { Dialog, Stack, Button, Group } from '@mantine/core';
|
import { Dialog, Stack, Button, Group } from '@mantine/core';
|
||||||
|
import Round from "./RoundViewer";
|
||||||
|
|
||||||
const thresholdStaking = 0.001;
|
const thresholdStaking = 0.001;
|
||||||
const thresholdFees = 0.00009;
|
const thresholdFees = 0.00009;
|
||||||
@ -71,6 +72,7 @@ const EventViewer = (obj) => {
|
|||||||
let unbondEventsIdx = obj.unbondEvents.length - 1;
|
let unbondEventsIdx = obj.unbondEvents.length - 1;
|
||||||
let transferTicketEventsIdx = obj.transferTicketEvents.length - 1;
|
let transferTicketEventsIdx = obj.transferTicketEvents.length - 1;
|
||||||
let redeemTicketEventsIdx = obj.redeemTicketEvents.length - 1;
|
let redeemTicketEventsIdx = obj.redeemTicketEvents.length - 1;
|
||||||
|
let roundsIdx = obj.rounds.length - 1;
|
||||||
|
|
||||||
if (!filterActivated) {
|
if (!filterActivated) {
|
||||||
filtered += activateEventsIdx + 1;
|
filtered += activateEventsIdx + 1;
|
||||||
@ -119,7 +121,8 @@ const EventViewer = (obj) => {
|
|||||||
redeemTicketEventsIdx >= 0 ||
|
redeemTicketEventsIdx >= 0 ||
|
||||||
activateEventsIdx >= 0 ||
|
activateEventsIdx >= 0 ||
|
||||||
unbondEventsIdx >= 0 ||
|
unbondEventsIdx >= 0 ||
|
||||||
stakeEventsIdx >= 0) {
|
stakeEventsIdx >= 0 ||
|
||||||
|
roundsIdx >= 0) {
|
||||||
|
|
||||||
let latestTime = 0;
|
let latestTime = 0;
|
||||||
let thisEvent;
|
let thisEvent;
|
||||||
@ -214,6 +217,14 @@ const EventViewer = (obj) => {
|
|||||||
latestType = "redeemTicket"
|
latestType = "redeemTicket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (roundsIdx >= 0) {
|
||||||
|
const thisObj = obj.rounds[roundsIdx];
|
||||||
|
if (thisObj.blockTime > latestTime) {
|
||||||
|
latestTime = thisObj.blockTime;
|
||||||
|
thisEvent = thisObj;
|
||||||
|
latestType = "round"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decrement IDX and check filter
|
// Decrement IDX and check filter
|
||||||
if (latestType == "update") {
|
if (latestType == "update") {
|
||||||
@ -412,6 +423,15 @@ const EventViewer = (obj) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (latestType == "round") {
|
||||||
|
roundsIdx--;
|
||||||
|
eventList.push(<Round
|
||||||
|
key={thisEvent.transactionHash + unfiltered + roundsIdx}
|
||||||
|
seed={thisEvent.transactionHash + unfiltered + roundsIdx}
|
||||||
|
round={thisEvent}
|
||||||
|
time={thisEvent.blockTime}
|
||||||
|
/>);
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
console.log("bork");
|
console.log("bork");
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,7 @@ const Livepeer = (obj) => {
|
|||||||
unbondEvents={livepeer.unbondEvents}
|
unbondEvents={livepeer.unbondEvents}
|
||||||
stakeEvents={livepeer.stakeEvents}
|
stakeEvents={livepeer.stakeEvents}
|
||||||
monthlyStats={livepeer.monthlyStats}
|
monthlyStats={livepeer.monthlyStats}
|
||||||
|
rounds={livepeer.rounds}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
getAllClaimEvents, getAllWithdrawStakeEvents, getAllWithdrawFeesEvents,
|
getAllClaimEvents, getAllWithdrawStakeEvents, getAllWithdrawFeesEvents,
|
||||||
getAllTransferTicketEvents, getAllRedeemTicketEvents, getAllActivateEvents,
|
getAllTransferTicketEvents, getAllRedeemTicketEvents, getAllActivateEvents,
|
||||||
getAllUnbondEvents, getAllStakeEvents, getAllCommissions, getAllTotalStakes,
|
getAllUnbondEvents, getAllStakeEvents, getAllCommissions, getAllTotalStakes,
|
||||||
hasAnyRefresh
|
hasAnyRefresh, getAllRounds
|
||||||
} from "../actions/livepeer";
|
} from "../actions/livepeer";
|
||||||
import { login } from "../actions/session";
|
import { login } from "../actions/session";
|
||||||
|
|
||||||
@ -91,6 +91,8 @@ const Startup = (obj) => {
|
|||||||
dispatch(getAllActivateEvents(false));
|
dispatch(getAllActivateEvents(false));
|
||||||
dispatch(getAllUnbondEvents(false));
|
dispatch(getAllUnbondEvents(false));
|
||||||
dispatch(getAllStakeEvents(false));
|
dispatch(getAllStakeEvents(false));
|
||||||
|
// TODO make it part of the hasAnyUpdate check
|
||||||
|
dispatch(getAllRounds());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
SET_ALL_UNBOND_EVENTS,
|
SET_ALL_UNBOND_EVENTS,
|
||||||
SET_ALL_STAKE_EVENTS,
|
SET_ALL_STAKE_EVENTS,
|
||||||
SET_ALL_ROUNDS,
|
SET_ALL_ROUNDS,
|
||||||
SET_ADD_ROUNDS
|
SET_ROUND
|
||||||
} from "../../actions/livepeer";
|
} from "../../actions/livepeer";
|
||||||
|
|
||||||
export default (state = {
|
export default (state = {
|
||||||
@ -137,11 +137,23 @@ export default (state = {
|
|||||||
return { ...state, stakeEvents: message };
|
return { ...state, stakeEvents: message };
|
||||||
case SET_ALL_ROUNDS:
|
case SET_ALL_ROUNDS:
|
||||||
return { ...state, rounds: message };
|
return { ...state, rounds: message };
|
||||||
case SET_ADD_ROUNDS:
|
case SET_ROUND:
|
||||||
|
// Check to see if it is already cached
|
||||||
|
if (state.rounds) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
rounds: [...state.rounds, message]
|
contents: state.rounds.map(
|
||||||
};
|
(content) => {
|
||||||
|
if (content.number == message.number) {
|
||||||
|
return message;
|
||||||
|
} else {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...state };
|
||||||
default:
|
default:
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
@ -273,10 +273,10 @@ export const getAllRounds = () => (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getRoundAtBlock = (blockNumber) => (
|
export const getRoundInfo = (roundNumber) => (
|
||||||
fetch("api/livepeer/getRoundAtBlock", {
|
fetch("api/livepeer/getRoundInfo", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ blockNumber }),
|
body: JSON.stringify({ roundNumber }),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user