request-drawer.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { Code } from "@phosphor-icons/react/dist/ssr/Code";
  2. import { Question } from "@phosphor-icons/react/dist/ssr/Question";
  3. import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
  4. import { Button } from "@pythnetwork/component-library/Button";
  5. import { CopyButton } from "@pythnetwork/component-library/CopyButton";
  6. import { InfoBox } from "@pythnetwork/component-library/InfoBox";
  7. import { Meter } from "@pythnetwork/component-library/Meter";
  8. import { StatCard } from "@pythnetwork/component-library/StatCard";
  9. import { Table } from "@pythnetwork/component-library/Table";
  10. import { Term } from "@pythnetwork/component-library/Term";
  11. import type { OpenDrawerArgs } from "@pythnetwork/component-library/useDrawer";
  12. import type { ComponentProps } from "react";
  13. import { useNumberFormatter } from "react-aria";
  14. import TimeAgo from "react-timeago";
  15. import styles from "./request-drawer.module.scss";
  16. import { getErrorDetails } from "../../errors";
  17. import type {
  18. Request,
  19. CallbackErrorRequest,
  20. FailedRequest,
  21. } from "../../requests";
  22. import { Status } from "../../requests";
  23. import { truncate } from "../../truncate";
  24. import { Account, Transaction } from "../Address";
  25. import { Status as StatusComponent } from "../Status";
  26. import { Timestamp } from "../Timestamp";
  27. import { ChainTag } from "./chain-tag";
  28. export const mkRequestDrawer = (
  29. request: Request,
  30. now: Date,
  31. ): OpenDrawerArgs => ({
  32. title: `Request ${truncate(request.requestTxHash)}`,
  33. headingExtra: <StatusComponent status={request.status} />,
  34. bodyClassName: styles.requestDrawer ?? "",
  35. fill: true,
  36. contents: <RequestDrawerBody request={request} now={now} />,
  37. });
  38. const RequestDrawerBody = ({
  39. request,
  40. now,
  41. }: {
  42. request: Request;
  43. now: Date;
  44. }) => {
  45. const gasFormatter = useNumberFormatter({ maximumFractionDigits: 3 });
  46. return (
  47. <>
  48. <div className={styles.cards}>
  49. <StatCard
  50. nonInteractive
  51. header="Random Number"
  52. small
  53. variant="primary"
  54. stat={
  55. "randomNumber" in request ? (
  56. <CopyButton text={request.randomNumber}>
  57. <code>{truncate(request.randomNumber)}</code>
  58. </CopyButton>
  59. ) : (
  60. <StatusComponent status={request.status} />
  61. )
  62. }
  63. />
  64. <StatCard
  65. nonInteractive
  66. header="Sequence Number"
  67. small
  68. stat={request.sequenceNumber}
  69. />
  70. </div>
  71. {request.status === Status.CallbackError && (
  72. <CallbackErrorInfo request={request} />
  73. )}
  74. {request.status === Status.Failed && (
  75. <FailureInfo header="Reveal failed!" request={request} />
  76. )}
  77. <Table
  78. label="Details"
  79. fill
  80. className={styles.details ?? ""}
  81. columns={[
  82. {
  83. id: "field",
  84. name: "Field",
  85. alignment: "left",
  86. isRowHeader: true,
  87. sticky: true,
  88. },
  89. {
  90. id: "value",
  91. name: "Value",
  92. fill: true,
  93. alignment: "left",
  94. },
  95. ]}
  96. rows={[
  97. {
  98. id: "chain",
  99. field: "Chain",
  100. value: <ChainTag chain={request.chain} />,
  101. },
  102. {
  103. id: "requestTimestamp",
  104. field: "Request Timestamp",
  105. value: <Timestamp timestamp={request.requestTimestamp} now={now} />,
  106. },
  107. ...("callbackTimestamp" in request
  108. ? [
  109. {
  110. id: "callbackTimestamp",
  111. field: "Callback Timestamp",
  112. value: (
  113. <Timestamp
  114. timestamp={request.callbackTimestamp}
  115. now={now}
  116. />
  117. ),
  118. },
  119. {
  120. id: "duration",
  121. field: (
  122. <Term term="Duration">
  123. The amount of time between the request transaction and the
  124. callback transaction.
  125. </Term>
  126. ),
  127. value: (
  128. <TimeAgo
  129. now={() => request.callbackTimestamp.getTime()}
  130. date={request.requestTimestamp}
  131. live={false}
  132. formatter={(value, unit) =>
  133. `${value.toString()} ${unit}${value === 1 ? "" : "s"}`
  134. }
  135. />
  136. ),
  137. },
  138. ]
  139. : []),
  140. {
  141. id: "requestTx",
  142. field: (
  143. <Term term="Request Transaction">
  144. The transaction that requests a new random number from the
  145. Entropy protocol.
  146. </Term>
  147. ),
  148. value: (
  149. <Transaction
  150. chain={request.chain}
  151. value={request.requestTxHash}
  152. />
  153. ),
  154. },
  155. {
  156. id: "sender",
  157. field: "Sender",
  158. value: <Account chain={request.chain} value={request.sender} />,
  159. },
  160. ...(request.status === Status.Complete
  161. ? [
  162. {
  163. id: "callbackTx",
  164. field: (
  165. <Term term="Callback Transaction">
  166. Entropy’s response transaction that returns the random
  167. number to the requester.
  168. </Term>
  169. ),
  170. value: (
  171. <Transaction
  172. chain={request.chain}
  173. value={request.callbackTxHash}
  174. />
  175. ),
  176. },
  177. ]
  178. : []),
  179. {
  180. id: "provider",
  181. field: "Provider",
  182. value: <Account chain={request.chain} value={request.provider} />,
  183. },
  184. {
  185. id: "userContribution",
  186. field: (
  187. <Term term="User Contribution">
  188. User-submitted randomness included in the request.
  189. </Term>
  190. ),
  191. value: (
  192. <CopyButton text={request.userContribution}>
  193. <code>{truncate(request.userContribution)}</code>
  194. </CopyButton>
  195. ),
  196. },
  197. ...("providerContribution" in request
  198. ? [
  199. {
  200. id: "providerContribution",
  201. field: (
  202. <Term term="Provider Contribution">
  203. Provider-submitted randomness used to calculate the random
  204. number.
  205. </Term>
  206. ),
  207. value: (
  208. <CopyButton text={request.providerContribution}>
  209. <code>{truncate(request.providerContribution)}</code>
  210. </CopyButton>
  211. ),
  212. },
  213. ]
  214. : []),
  215. {
  216. id: "gas",
  217. field: "Gas",
  218. value:
  219. "gasUsed" in request ? (
  220. <Meter
  221. label="Gas"
  222. value={request.gasUsed}
  223. maxValue={request.gasLimit}
  224. className={styles.gasMeter ?? ""}
  225. startLabel={`${gasFormatter.format(request.gasUsed)} used`}
  226. endLabel={`${gasFormatter.format(request.gasLimit)} max`}
  227. labelClassName={styles.gasMeterLabel ?? ""}
  228. variant={
  229. request.gasUsed > request.gasLimit ? "error" : "default"
  230. }
  231. />
  232. ) : (
  233. `${gasFormatter.format(request.gasLimit)} max`
  234. ),
  235. },
  236. ].map((data) => ({
  237. id: data.id,
  238. data: {
  239. field: <span className={styles.field}>{data.field}</span>,
  240. value: data.value,
  241. },
  242. }))}
  243. />
  244. </>
  245. );
  246. };
  247. const CallbackErrorInfo = ({ request }: { request: CallbackErrorRequest }) => {
  248. const retryCommand = `cast send ${request.chain.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userContribution} ${request.providerContribution} -r ${request.chain.rpc} --private-key <YOUR_PRIVATE_KEY>`;
  249. return (
  250. <>
  251. <FailureInfo header="Callback failed!" request={request} />
  252. <InfoBox
  253. header="Retry the callback yourself"
  254. icon={<Code />}
  255. className={styles.message}
  256. variant="info"
  257. >
  258. {`If you'd like to execute your callback, you can run the command in your
  259. terminal or connect your wallet to run it here.`}
  260. <div
  261. style={{
  262. display: "flex",
  263. flexFlow: "row nowrap",
  264. justifyContent: "end",
  265. gap: "16px",
  266. marginTop: "16px",
  267. }}
  268. >
  269. <CopyButton text={retryCommand}>Copy Cast Command</CopyButton>
  270. <Button
  271. size="sm"
  272. variant="ghost"
  273. beforeIcon={<Question />}
  274. rounded
  275. hideText
  276. href="https://docs.pyth.network/entropy/debug-callback-failures"
  277. target="_blank"
  278. className={styles.helpButton ?? ""}
  279. >
  280. Help
  281. </Button>
  282. </div>
  283. </InfoBox>
  284. </>
  285. );
  286. };
  287. const FailureInfo = ({
  288. request,
  289. ...props
  290. }: ComponentProps<typeof InfoBox> & {
  291. request: CallbackErrorRequest | FailedRequest;
  292. }) => (
  293. <InfoBox
  294. icon={<Warning />}
  295. className={styles.message}
  296. variant="warning"
  297. {...props}
  298. >
  299. <Button
  300. hideText
  301. beforeIcon={<Question />}
  302. rounded
  303. size="sm"
  304. variant="ghost"
  305. className={styles.helpButton ?? ""}
  306. href={getHelpLink(request)}
  307. target="_blank"
  308. >
  309. Help
  310. </Button>
  311. <div className={styles.failureMessage}>
  312. <FailureMessage request={request} />
  313. </div>
  314. </InfoBox>
  315. );
  316. const getHelpLink = (request: CallbackErrorRequest | FailedRequest) => {
  317. const details = getErrorDetails(request.reason);
  318. if (details === undefined) {
  319. return isGasLimitExceeded(request)
  320. ? "https://docs.pyth.network/entropy/best-practices#limit-gas-usage-on-the-callback"
  321. : "https://docs.pyth.network/entropy/best-practices#handling-callback-failures";
  322. } else {
  323. return details[2];
  324. }
  325. };
  326. const FailureMessage = ({
  327. request,
  328. }: {
  329. request: CallbackErrorRequest | FailedRequest;
  330. }) => {
  331. const details = getErrorDetails(request.reason);
  332. if (details) {
  333. return (
  334. <>
  335. <p>The callback encountered the following error:</p>
  336. <p className={styles.details}>
  337. <b>{details[0]}</b> (<code>{request.reason}</code>): {details[1]}
  338. </p>
  339. </>
  340. );
  341. } else if (isGasLimitExceeded(request)) {
  342. return "The callback used more gas than the set gas limit";
  343. } else {
  344. return (
  345. <>
  346. <b>Error response:</b> {request.reason}
  347. </>
  348. );
  349. }
  350. };
  351. const isGasLimitExceeded = (request: CallbackErrorRequest | FailedRequest) =>
  352. request.status === Status.CallbackError &&
  353. request.reason === "0x" &&
  354. request.gasUsed > request.gasLimit;