Skip to content

Commit

Permalink
[frenemies] UI tweaks (MystenLabs#8096)
Browse files Browse the repository at this point in the history
  • Loading branch information
damirka authored Feb 6, 2023
1 parent a655a95 commit e79ebc5
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 82 deletions.
2 changes: 2 additions & 0 deletions dapps/frenemies/src/components/Validators/Balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function Balance() {
},
{
enabled: !!currentAccount,
refetchOnWindowFocus: false,
refetchInterval: 60 * 1000,
}
);

Expand Down
2 changes: 1 addition & 1 deletion dapps/frenemies/src/components/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Leaderboard() {
<>
<div className="flex gap-16 mt-3 mb-7">
<Stat variant="leaderboard" label="Highest Score">
{leaderboard.data.topScores[0].score || 0}
{leaderboard.data.topScores[0]?.score || '--'}
</Stat>
{/* <Stat variant="leaderboard" label="Total Score">
420
Expand Down
53 changes: 36 additions & 17 deletions dapps/frenemies/src/components/your-score/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useWalletKit } from "@mysten/wallet-kit";
import { ReactNode } from "react";
import { useScorecard } from "../../network/queries/scorecard";
import { useScorecardHistory } from "../../network/queries/scorecard-history";
import { useSuiSystem } from "../../network/queries/sui-system";
import { Leaderboard, ScorecardUpdatedEvent } from "../../network/types";
import { formatGoal, formatAddress } from "../../utils/format";
import { Logo } from "../Validators/Logo";

interface Props {
data: ScorecardUpdatedEvent[];
Expand All @@ -25,15 +29,23 @@ const Cell = ({
);

export function Table({ data, round, leaderboard }: Props) {
const { currentAccount } = useWalletKit();
const { data: system } = useSuiSystem();
const { data: scorecard } = useScorecard(currentAccount);
const { isLoading } = useScorecardHistory(scorecard?.data.id);
const activeValidators = system?.validators.fields.active_validators || [];
const getValidator = (addr: string) => activeValidators
.find((v) => v.fields.metadata.fields.sui_address.replace('0x', '') == addr)?.fields;
const getValidator = (addr: string) =>
activeValidators.find(
(v) => v.fields.metadata.fields.sui_address.replace("0x", "") == addr
)?.fields;

const dataByRound: { [key: string]: ScorecardUpdatedEvent } = data
.reduce((acc, row) => Object.assign(acc, {
[(row.assignment.epoch - leaderboard.startEpoch).toString()]: row
}), {});
const dataByRound: { [key: string]: ScorecardUpdatedEvent } = data.reduce(
(acc, row) =>
Object.assign(acc, {
[(row.assignment.epoch - leaderboard.startEpoch).toString()]: row,
}),
{}
);

const firstRound = Math.min(...Object.keys(dataByRound).map((e) => +e));
const tableData: (ScorecardUpdatedEvent | null)[] = [];
Expand All @@ -50,27 +62,34 @@ export function Table({ data, round, leaderboard }: Props) {
<Cell as="th">Role</Cell>
<Cell as="th">Assigned Validator</Cell>
<Cell as="th">Objective</Cell>
<Cell as="th">Score</Cell>
<Cell as="th">Points Scored</Cell>
</tr>
</thead>
<tbody>
{[...tableData].reverse().map((evt, round, arr) => {
const currRound = firstRound + arr.length - round - 1;
let totalScore = 0;

if (evt) {
const { goal, validator } = evt.assignment;
totalScore = evt.totalScore;
const validatorMeta = getValidator(validator)?.metadata.fields;
return (
<tr key={currRound.toString()} className="border-t border-white/20">
<tr
key={currRound.toString()}
className="border-t border-white/20"
>
<Cell>{currRound.toString()}</Cell>
<Cell>{formatGoal(goal)}</Cell>
<Cell>{getValidator(validator)?.metadata.fields.name || formatAddress(validator)}</Cell>
<Cell>
<Logo
src={validatorMeta?.image_url as string}
size="sm"
label={validatorMeta?.name as string}
circle
/>
{validatorMeta?.name || formatAddress(validator)}
</Cell>
<Cell>{evt.epochScore !== 0 ? "Achieved" : "Failed"}</Cell>
<Cell>
{evt.epochScore !== 0
? `${evt.totalScore} (+${evt.epochScore})`
: `${evt.totalScore}`}
{(evt.epochScore !== 0 ? "+" : "") + evt.epochScore}
</Cell>
</tr>
);
Expand All @@ -80,8 +99,8 @@ export function Table({ data, round, leaderboard }: Props) {
<Cell>{currRound.toString()}</Cell>
<Cell>--</Cell>
<Cell>--</Cell>
<Cell>Skipped</Cell>
<Cell>{totalScore}</Cell>
<Cell>{isLoading ? "--" : "Skipped"}</Cell>
<Cell>--</Cell>
</tr>
);
}
Expand Down
2 changes: 1 addition & 1 deletion dapps/frenemies/src/components/your-score/YourScore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function YourScore() {
<Stat variant="leaderboard" label="Rank">
{rank === -1 ? "--" : rank + 1}
</Stat>
<Stat variant="leaderboard" label="Score">
<Stat variant="leaderboard" label="Total Score">
{scorecard.data.score}
</Stat>
</div>
Expand Down
50 changes: 29 additions & 21 deletions dapps/frenemies/src/network/queries/epoch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ import { config } from "../../config";
* data on the current epoch and timestamps.
*/
export function useEpoch() {
return useQuery(["epoch"], async (): Promise<{ timestamp: number; prevTimestamp: number; data: SystemEpochInfo } | null> => {
const { data } = await provider.getEvents(
{ MoveEvent: SYSTEM_EPOCH_INFO },
null,
2,
"descending"
);
const [evt, prevEvt] = data;
return useQuery(
["epoch"],
async (): Promise<{
timestamp: number;
prevTimestamp: number;
data: SystemEpochInfo;
} | null> => {
const { data } = await provider.getEvents(
{ MoveEvent: SYSTEM_EPOCH_INFO },
null,
2,
"descending"
);
const [evt, prevEvt] = data;

// should never happen; it's a platform requirement.
if (data.length == 0 || !("moveEvent" in evt.event)) {
return null;
}
// should never happen; it's a platform requirement.
if (data.length == 0 || !("moveEvent" in evt.event)) {
return null;
}

return {
timestamp: evt.timestamp,
prevTimestamp: prevEvt?.timestamp || 0,
data: bcs.de(SYSTEM_EPOCH_INFO, evt.event.moveEvent.bcs, "base64"),
};
}, {
// refetch twice per epoch
refetchInterval: +config.VITE_EPOCH_LEN * 60000 / 2
});
return {
timestamp: evt.timestamp,
prevTimestamp: prevEvt?.timestamp || 0,
data: bcs.de(SYSTEM_EPOCH_INFO, evt.event.moveEvent.bcs, "base64"),
};
},
{
// refetch 4 times per epoch
refetchInterval: (+config.VITE_EPOCH_LEN * 60000) / 4,
}
);
}
43 changes: 27 additions & 16 deletions dapps/frenemies/src/network/queries/scorecard-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,33 @@ import { ScorecardUpdatedEvent, SCORECARD_UPDATED } from "../types";
* @returns
*/
export function useScorecardHistory(scorecardId?: string | null) {
return useQuery(["scorecard-history", scorecardId], async () => {
if (!scorecardId) {
return null;
}
return useQuery(
["scorecard-history", scorecardId],
async () => {
if (!scorecardId) {
return null;
}

// It's very likely to have duplicates in the `txIds`; so we need to
// filter them out and pass a unique set.
const txIds = await provider.getTransactionsForObject(scorecardId);
const txs = await provider.getTransactionWithEffectsBatch(Array.from(new Set(txIds)));
// It's very likely to have duplicates in the `txIds`; so we need to
// filter them out and pass a unique set.
const txIds = await provider.getTransactionsForObject(scorecardId);
const txs = await provider.getTransactionWithEffectsBatch(
Array.from(new Set(txIds))
);

return txs
.reduce((acc: any[], tx) => acc.concat(tx.effects.events || []), [])
.filter((evt) => "moveEvent" in evt && evt.moveEvent.type == SCORECARD_UPDATED)
.map<ScorecardUpdatedEvent>(({ moveEvent }) => bcs.de(SCORECARD_UPDATED, moveEvent.bcs, "base64"));
},
{
enabled: !!scorecardId
});
return txs
.reduce((acc: any[], tx) => acc.concat(tx.effects.events || []), [])
.filter(
(evt) => "moveEvent" in evt && evt.moveEvent.type == SCORECARD_UPDATED
)
.map<ScorecardUpdatedEvent>(({ moveEvent }) =>
bcs.de(SCORECARD_UPDATED, moveEvent.bcs, "base64")
);
},
{
enabled: !!scorecardId,
refetchOnWindowFocus: false,
refetchInterval: 60 * 1000,
}
);
}
1 change: 1 addition & 0 deletions dapps/frenemies/src/network/queries/scorecard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function useScorecard(account?: string | null) {
},
{
enabled: !!account,
refetchInterval: 60 * 1000,
}
);
}
27 changes: 17 additions & 10 deletions dapps/frenemies/src/network/queries/sui-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ export const SUI_SYSTEM_ID: string = normalizeSuiAddress("0x5");
* Read the SuiSystem object.
*/
export function useSuiSystem() {
return useQuery(["object", SUI_SYSTEM_ID], async () => {
const data = await provider.getObject(SUI_SYSTEM_ID);
const systemObject =
data &&
is(data.details, SuiObject) &&
data.details.data.dataType === "moveObject"
? (data.details.data.fields as MoveSuiSystemObjectFields)
: null;
return useQuery(
["object", SUI_SYSTEM_ID],
async () => {
const data = await provider.getObject(SUI_SYSTEM_ID);
const systemObject =
data &&
is(data.details, SuiObject) &&
data.details.data.dataType === "moveObject"
? (data.details.data.fields as MoveSuiSystemObjectFields)
: null;

return systemObject;
});
return systemObject;
},
{
refetchInterval: 60 * 1000,
refetchOnWindowFocus: false,
}
);

// TODO: Fix raw version when there is delegated stake:
// return useRawObject<SuiSystem>(SUI_SYSTEM_ID, SUI_SYSTEM_TYPE);
Expand Down
14 changes: 11 additions & 3 deletions dapps/frenemies/src/network/queries/use-raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import provider from "../provider";
* Generic method to fetch a RawObject from the network.
*/
export function useRawObject<T>(objectId: string, bcsType: string) {
return useQuery([bcsType, objectId], async () => {
return getRawObjectParsedUnsafe<T>(provider, objectId, bcsType);
});
return useQuery(
[bcsType, objectId],
async () => {
return getRawObjectParsedUnsafe<T>(provider, objectId, bcsType);
},
{
// Refetch every 10 seconds:
refetchInterval: 10 * 1000,
}
);
}

/**
Expand Down Expand Up @@ -40,6 +47,7 @@ export function useMyType<T>(type: string, account?: string | null) {
},
{
enabled: !!account,
refetchInterval: 2 * 60 * 1000,
}
);
}
22 changes: 10 additions & 12 deletions dapps/frenemies/src/routes/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { Scoreboard } from "../components/Scoreboard";
import { useEpoch } from "../network/queries/epoch";
import { Goal, LEADERBOARD, Leaderboard } from "../network/types";
import { Goal } from "../network/types";
import { Assignment } from "../components/Assignment";
import { useSuiSystem } from "../network/queries/sui-system";
import { Logo } from "../components/Validators/Logo";
import { useRawObject } from "../network/queries/use-raw";
import { config } from "../config";

/**
* The Home page.
Expand All @@ -26,7 +24,6 @@ export function Home() {
const navigate = useNavigate();
const { data: epoch } = useEpoch();
const { currentAccount } = useWalletKit();
const { data: leaderboard } = useRawObject<Leaderboard>(config.VITE_LEADERBOARD, LEADERBOARD);
const { data: scorecard, isSuccess } = useScorecard(currentAccount);
const { data: system } = useSuiSystem();

Expand Down Expand Up @@ -63,10 +60,15 @@ export function Home() {
return () => clearInterval(interval);
}, [epoch]);

// Whether there's an assignment for the current round (either first one
// or requested for the round via "Play Round X" button).
const hasAssignment = !!scorecard
&& !!epoch
&& scorecard.data.assignment.epoch == epoch.data.epoch;

// Metadata of the currently assigned validator.
const validatorMeta = assignedValidator?.fields.metadata.fields;

return (
<>
<Scoreboard />
Expand All @@ -77,19 +79,15 @@ export function Home() {
</Card>
<Card spacing="sm">
<Stat label="Assigned Validator">
{assignedValidator && hasAssignment ? (
{validatorMeta && hasAssignment ? (
<div className="flex items-center gap-2">
<Logo
src={
assignedValidator.fields.metadata.fields.image_url as string
}
src={validatorMeta.image_url as string}
size="md"
label={
assignedValidator.fields.metadata.fields.name as string
}
label={validatorMeta.name as string}
circle
/>
<div>{assignedValidator.fields.metadata.fields.name}</div>
<div>{validatorMeta.name}</div>
</div>
) : (
"--"
Expand Down
2 changes: 1 addition & 1 deletion dapps/frenemies/src/routes/Setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function Setup() {
["create-scorecard"],
async (username: string) => {
if (!currentAccount) {
throw new Error("No Coins found, please request some from faucet");
throw new Error("No SUI coins found in your wallet. You need SUI to play the Frenemies game");
}

const gasPrice = epoch?.data.referenceGasPrice || 1n;
Expand Down

0 comments on commit e79ebc5

Please sign in to comment.