Skip to content

Commit 232a388

Browse files
committed
feat(entropy-explorer): implement some feedback items
1 parent 0ce6061 commit 232a388

File tree

19 files changed

+537
-319
lines changed

19 files changed

+537
-319
lines changed

apps/entropy-explorer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"fix:format": "prettier --write .",
1212
"fix:lint:eslint": "eslint --fix .",
1313
"fix:lint:stylelint": "stylelint --fix 'src/**/*.scss'",
14-
"pull:env": "[ $CI ] || VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=prj_TBkf9EyQjQF37gs4Vk0sQKJj97kE vercel env pull",
14+
"pull:env": "[ $CI ] || VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=prj_34F8THr7mZ3eAOQoCLdo8xWj9fdT vercel env pull",
1515
"start:dev": "next dev --port 3006",
1616
"start:prod": "next start --port 3006",
1717
"test:format": "prettier --check .",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@use "@pythnetwork/component-library/theme";
2+
3+
.address {
4+
display: flex;
5+
flex-flow: row nowrap;
6+
gap: theme.spacing(2);
7+
font-size: theme.font-size("sm");
8+
9+
.full {
10+
display: none;
11+
}
12+
13+
&:not([data-always-truncate]) {
14+
@include theme.breakpoint("xl") {
15+
.truncated {
16+
display: none;
17+
}
18+
19+
.full {
20+
display: unset;
21+
}
22+
}
23+
}
24+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CopyButton } from "@pythnetwork/component-library/CopyButton";
2+
import { Link } from "@pythnetwork/component-library/Link";
3+
import { useMemo } from "react";
4+
5+
import styles from "./index.module.scss";
6+
import { EntropyDeployments } from "../../entropy-deployments";
7+
import { truncate } from "../../truncate";
8+
9+
type Props = {
10+
value: string;
11+
chain: keyof typeof EntropyDeployments;
12+
alwaysTruncate?: boolean | undefined;
13+
};
14+
15+
export const Address = ({ value, chain, alwaysTruncate }: Props) => {
16+
const { explorer } = EntropyDeployments[chain];
17+
const truncatedValue = useMemo(() => truncate(value), [value]);
18+
return (
19+
<div
20+
data-always-truncate={alwaysTruncate ? "" : undefined}
21+
className={styles.address}
22+
>
23+
<Link
24+
href={explorer.replace("$ADDRESS", value)}
25+
target="_blank"
26+
rel="noreferrer"
27+
>
28+
<code className={styles.truncated}>{truncatedValue}</code>
29+
<code className={styles.full}>{value}</code>
30+
</Link>
31+
<CopyButton text={value} iconOnly />
32+
</div>
33+
);
34+
};

apps/entropy-explorer/src/components/Home/chain-select.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export const ChainSelect = (
3131
</Suspense>
3232
);
3333

34-
type Deployment = ReturnType<typeof entropyDeploymentsByNetwork>[number];
34+
type Deployment =
35+
| ReturnType<typeof entropyDeploymentsByNetwork>[number]
36+
| { id: "all" };
3537

3638
const ResolvedChainSelect = (
3739
props: ConstrainedOmit<
@@ -49,6 +51,11 @@ const useResolvedProps = () => {
4951
const { chain, setChain } = useQuery();
5052
const chains = useMemo(
5153
() => [
54+
{
55+
name: "ALL",
56+
options: [{ id: "all" as const }],
57+
hideLabel: true,
58+
},
5259
{
5360
name: "MAINNET",
5461
options: entropyDeploymentsByNetwork("mainnet", collator),
@@ -62,30 +69,42 @@ const useResolvedProps = () => {
6269
);
6370

6471
const showChain = useCallback(
65-
(chain: Deployment) => (
66-
<div className={styles.chainSelectItem}>
67-
<ChainIcon id={chain.chainId} />
68-
{chain.name}
69-
</div>
70-
),
72+
(chain: Deployment) =>
73+
chain.id === "all" ? (
74+
"All"
75+
) : (
76+
<div className={styles.chainSelectItem}>
77+
<ChainIcon id={chain.chainId} />
78+
{chain.name}
79+
</div>
80+
),
7181
[],
7282
);
7383

74-
const chainTextValue = useCallback((chain: Deployment) => chain.name, []);
84+
const chainTextValue = useCallback(
85+
(chain: Deployment) => (chain.id === "all" ? "All" : chain.name),
86+
[],
87+
);
88+
// eslint-disable-next-line import/namespace
89+
const viemChain = chain ? viemChains[chain] : undefined;
7590

7691
return {
77-
selectedKey: chain ?? undefined,
92+
selectedKey: chain ?? ("all" as const),
7893
onSelectionChange: setChain,
7994
optionGroups: chains,
8095
show: showChain,
8196
textValue: chainTextValue,
97+
buttonLabel: viemChain?.name ?? "Chain",
98+
...(viemChain && {
99+
icon: () => <ChainIcon id={viemChain.id} />,
100+
}),
82101
};
83102
};
84103

85104
const defaultProps = {
86105
label: "Chain",
87106
hideLabel: true,
88-
defaultButtonLabel: "Select Chain",
107+
defaultButtonLabel: "Chain",
89108
} as const;
90109

91110
const entropyDeploymentsByNetwork = (
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@use "@pythnetwork/component-library/theme";
2+
3+
.requestDrawer {
4+
gap: theme.spacing(8);
5+
6+
.cards {
7+
display: grid;
8+
gap: theme.spacing(4);
9+
grid-template-columns: repeat(2, 1fr);
10+
padding-left: theme.spacing(4);
11+
padding-right: theme.spacing(4);
12+
}
13+
14+
.details {
15+
width: 100%;
16+
overflow: auto;
17+
18+
.field {
19+
@include theme.text("sm", "normal");
20+
21+
color: theme.color("muted");
22+
}
23+
24+
.gasMeter {
25+
margin-right: 5%;
26+
27+
.gasMeterLabel {
28+
@include theme.text("xs", "medium");
29+
}
30+
}
31+
}
32+
33+
.message {
34+
margin-left: theme.spacing(4);
35+
margin-right: theme.spacing(4);
36+
position: relative;
37+
38+
.code {
39+
border-radius: theme.border-radius("lg");
40+
font-size: theme.font-size("sm");
41+
line-height: 125%;
42+
}
43+
44+
.copyButton {
45+
position: absolute;
46+
top: theme.spacing(2);
47+
right: calc(theme.spacing(2) + 0.25em);
48+
}
49+
}
50+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { Code } from "@phosphor-icons/react/dist/ssr/Code";
2+
import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
3+
import { CopyButton } from "@pythnetwork/component-library/CopyButton";
4+
import { InfoBox } from "@pythnetwork/component-library/InfoBox";
5+
import { Meter } from "@pythnetwork/component-library/Meter";
6+
import { StatCard } from "@pythnetwork/component-library/StatCard";
7+
import { Table } from "@pythnetwork/component-library/Table";
8+
import type { OpenDrawerArgs } from "@pythnetwork/component-library/useDrawer";
9+
import { useDateFormatter, useNumberFormatter } from "react-aria";
10+
11+
import styles from "./request-drawer.module.scss";
12+
import { EntropyDeployments } from "../../entropy-deployments";
13+
import type { Request, CompletedRequest } from "../../get-requests";
14+
import { truncate } from "../../truncate";
15+
import { Address } from "../Address";
16+
import { Status } from "../Status";
17+
18+
export const mkRequestDrawer = (request: Request): OpenDrawerArgs => ({
19+
title: `Request ${truncate(request.txHash)}`,
20+
headingExtra: <Status request={request} />,
21+
bodyClassName: styles.requestDrawer ?? "",
22+
fill: true,
23+
contents: <RequestDrawerBody request={request} />,
24+
});
25+
26+
const RequestDrawerBody = ({ request }: { request: Request }) => {
27+
const dateFormatter = useDateFormatter({
28+
dateStyle: "long",
29+
timeStyle: "long",
30+
});
31+
const gasFormatter = useNumberFormatter({ maximumFractionDigits: 3 });
32+
33+
return (
34+
<>
35+
<div className={styles.cards}>
36+
<StatCard
37+
nonInteractive
38+
header="Random Number"
39+
small
40+
variant="primary"
41+
stat={
42+
request.hasCallbackCompleted ? (
43+
<CopyButton text={request.callbackResult.randomNumber}>
44+
<code>{truncate(request.callbackResult.randomNumber)}</code>
45+
</CopyButton>
46+
) : (
47+
<Status request={request} />
48+
)
49+
}
50+
/>
51+
<StatCard
52+
nonInteractive
53+
header="Sequence Number"
54+
small
55+
stat={request.sequenceNumber}
56+
/>
57+
</div>
58+
<Table
59+
label="Details"
60+
fill
61+
className={styles.details ?? ""}
62+
stickyHeader
63+
columns={[
64+
{
65+
id: "field",
66+
name: "Field",
67+
alignment: "left",
68+
isRowHeader: true,
69+
sticky: true,
70+
},
71+
{
72+
id: "value",
73+
name: "Value",
74+
fill: true,
75+
alignment: "left",
76+
},
77+
]}
78+
rows={[
79+
{
80+
field: "Request Timestamp",
81+
value: dateFormatter.format(request.timestamp),
82+
},
83+
...(request.hasCallbackCompleted
84+
? [
85+
{
86+
field: "Callback Timestamp",
87+
value: dateFormatter.format(request.callbackResult.timestamp),
88+
},
89+
]
90+
: []),
91+
{
92+
field: "Request Tx Hash",
93+
value: <Address chain={request.chain} value={request.txHash} />,
94+
},
95+
...(request.hasCallbackCompleted
96+
? [
97+
{
98+
field: "Callback Tx Hash",
99+
value: (
100+
<Address
101+
chain={request.chain}
102+
value={request.callbackResult.txHash}
103+
/>
104+
),
105+
},
106+
]
107+
: []),
108+
{
109+
field: "Caller",
110+
value: <Address chain={request.chain} value={request.caller} />,
111+
},
112+
{
113+
field: "Provider",
114+
value: <Address chain={request.chain} value={request.provider} />,
115+
},
116+
{
117+
field: "Gas",
118+
value: request.hasCallbackCompleted ? (
119+
<Meter
120+
label="Gas"
121+
value={request.callbackResult.gasUsed}
122+
maxValue={request.gasLimit}
123+
className={styles.gasMeter ?? ""}
124+
startLabel={
125+
<>
126+
{gasFormatter.format(request.callbackResult.gasUsed)} used
127+
</>
128+
}
129+
endLabel={<>{gasFormatter.format(request.gasLimit)} max</>}
130+
labelClassName={styles.gasMeterLabel ?? ""}
131+
variant={
132+
request.callbackResult.gasUsed > request.gasLimit
133+
? "error"
134+
: "default"
135+
}
136+
/>
137+
) : (
138+
<>{gasFormatter.format(request.gasLimit)} max</>
139+
),
140+
},
141+
].map((data) => ({
142+
id: data.field,
143+
data: {
144+
field: <span className={styles.field}>{data.field}</span>,
145+
value: data.value,
146+
},
147+
}))}
148+
/>
149+
{request.hasCallbackCompleted && request.callbackResult.failed && (
150+
<CallbackFailedInfo request={request} />
151+
)}
152+
</>
153+
);
154+
};
155+
156+
const CallbackFailedInfo = ({ request }: { request: CompletedRequest }) => {
157+
const deployment = EntropyDeployments[request.chain];
158+
const retryCommand = `cast send ${deployment.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userRandomNumber} ${request.callbackResult.revelationData} -r ${deployment.rpc} --private-key <YOUR_PRIVATE_KEY>`;
159+
160+
return (
161+
<>
162+
<InfoBox
163+
header="Callback failed!"
164+
icon={<Warning />}
165+
className={styles.message}
166+
variant="warning"
167+
>
168+
<CallbackFailureMessage request={request} />
169+
</InfoBox>
170+
<InfoBox
171+
header="To retry this callback, run:"
172+
icon={<Code />}
173+
className={styles.message}
174+
variant="info"
175+
>
176+
<CopyButton className={styles.copyButton ?? ""} text={retryCommand} />
177+
<code className={styles.code}>{retryCommand}</code>
178+
</InfoBox>
179+
</>
180+
);
181+
};
182+
183+
const CallbackFailureMessage = ({ request }: { request: CompletedRequest }) =>
184+
request.callbackResult.returnValue === "" &&
185+
request.callbackResult.gasUsed > request.gasLimit
186+
? "The callback used more gas than the gas limit."
187+
: `An error occurred: ${request.callbackResult.returnValue}`;

0 commit comments

Comments
 (0)