request-drawer.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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 { useNumberFormatter } from "react-aria";
  13. import TimeAgo from "react-timeago";
  14. import styles from "./request-drawer.module.scss";
  15. import { EntropyDeployments } from "../../entropy-deployments";
  16. import { getErrorDetails } from "../../errors";
  17. import type { Request, CallbackErrorRequest } from "../../requests";
  18. import { Status } from "../../requests";
  19. import { truncate } from "../../truncate";
  20. import { Address } from "../Address";
  21. import { Status as StatusComponent } from "../Status";
  22. import { Timestamp } from "../Timestamp";
  23. export const mkRequestDrawer = (request: Request): OpenDrawerArgs => ({
  24. title: `Request ${truncate(request.requestTxHash)}`,
  25. headingExtra: <StatusComponent prefix="CALLBACK " status={request.status} />,
  26. bodyClassName: styles.requestDrawer ?? "",
  27. fill: true,
  28. contents: <RequestDrawerBody request={request} />,
  29. });
  30. const RequestDrawerBody = ({ request }: { request: Request }) => {
  31. const gasFormatter = useNumberFormatter({ maximumFractionDigits: 3 });
  32. return (
  33. <>
  34. <div className={styles.cards}>
  35. <StatCard
  36. nonInteractive
  37. header="Random Number"
  38. small
  39. variant="primary"
  40. stat={
  41. request.status === Status.Pending ? (
  42. <StatusComponent prefix="CALLBACK " status={Status.Pending} />
  43. ) : (
  44. <CopyButton text={request.randomNumber}>
  45. <code>{truncate(request.randomNumber)}</code>
  46. </CopyButton>
  47. )
  48. }
  49. />
  50. <StatCard
  51. nonInteractive
  52. header="Sequence Number"
  53. small
  54. stat={request.sequenceNumber}
  55. />
  56. </div>
  57. {request.status === Status.CallbackError && (
  58. <CallbackFailedInfo request={request} />
  59. )}
  60. <Table
  61. label="Details"
  62. fill
  63. className={styles.details ?? ""}
  64. stickyHeader
  65. columns={[
  66. {
  67. id: "field",
  68. name: "Field",
  69. alignment: "left",
  70. isRowHeader: true,
  71. sticky: true,
  72. },
  73. {
  74. id: "value",
  75. name: "Value",
  76. fill: true,
  77. alignment: "left",
  78. },
  79. ]}
  80. rows={[
  81. {
  82. id: "requestTimestamp",
  83. field: "Request Timestamp",
  84. value: <Timestamp timestamp={request.requestTimestamp} />,
  85. },
  86. ...(request.status === Status.Pending
  87. ? []
  88. : [
  89. {
  90. id: "callbackTimestamp",
  91. field: "Callback Timestamp",
  92. value: <Timestamp timestamp={request.callbackTimestamp} />,
  93. },
  94. {
  95. id: "duration",
  96. field: (
  97. <Term term="Duration">
  98. The amount of time between the request transaction and the
  99. callback transaction.
  100. </Term>
  101. ),
  102. value: (
  103. <TimeAgo
  104. now={() => request.callbackTimestamp.getTime()}
  105. date={request.requestTimestamp}
  106. live={false}
  107. formatter={(value, unit) =>
  108. `${value.toString()} ${unit}${value === 1 ? "" : "s"}`
  109. }
  110. />
  111. ),
  112. },
  113. ]),
  114. {
  115. id: "requestTx",
  116. field: (
  117. <Term term="Request Transaction">
  118. The transaction that requests a new random number from the
  119. Entropy protocol.
  120. </Term>
  121. ),
  122. value: (
  123. <Address chain={request.chain} value={request.requestTxHash} />
  124. ),
  125. },
  126. {
  127. id: "sender",
  128. field: "Sender",
  129. value: <Address chain={request.chain} value={request.sender} />,
  130. },
  131. ...(request.status === Status.Pending
  132. ? []
  133. : [
  134. {
  135. id: "callbackTx",
  136. field: (
  137. <Term term="Callback Transaction">
  138. Entropy’s response transaction that returns the random
  139. number to the requester.
  140. </Term>
  141. ),
  142. value: (
  143. <Address
  144. chain={request.chain}
  145. value={request.callbackTxHash}
  146. />
  147. ),
  148. },
  149. ]),
  150. {
  151. id: "provider",
  152. field: "Provider",
  153. value: <Address chain={request.chain} value={request.provider} />,
  154. },
  155. {
  156. id: "userContribution",
  157. field: (
  158. <Term term="User Contribution">
  159. User-submitted randomness included in the request.
  160. </Term>
  161. ),
  162. value: (
  163. <CopyButton text={request.userRandomNumber}>
  164. <code>{truncate(request.userRandomNumber)}</code>
  165. </CopyButton>
  166. ),
  167. },
  168. {
  169. id: "providerContribution",
  170. field: (
  171. <Term term="Provider Contribution">
  172. Provider-submitted randomness used to calculate the random
  173. number.
  174. </Term>
  175. ),
  176. value: (
  177. <CopyButton text={request.userRandomNumber}>
  178. <code>{truncate(request.userRandomNumber)}</code>
  179. </CopyButton>
  180. ),
  181. },
  182. {
  183. id: "gas",
  184. field: "Gas",
  185. value:
  186. request.status === Status.Pending ? (
  187. `${gasFormatter.format(request.gasLimit)} max`
  188. ) : (
  189. <Meter
  190. label="Gas"
  191. value={request.gasUsed}
  192. maxValue={request.gasLimit}
  193. className={styles.gasMeter ?? ""}
  194. startLabel={`${gasFormatter.format(request.gasUsed)} used`}
  195. endLabel={`${gasFormatter.format(request.gasLimit)} max`}
  196. labelClassName={styles.gasMeterLabel ?? ""}
  197. variant={
  198. request.gasUsed > request.gasLimit ? "error" : "default"
  199. }
  200. />
  201. ),
  202. },
  203. ].map((data) => ({
  204. id: data.id,
  205. data: {
  206. field: <span className={styles.field}>{data.field}</span>,
  207. value: data.value,
  208. },
  209. }))}
  210. />
  211. </>
  212. );
  213. };
  214. const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
  215. const deployment = EntropyDeployments[request.chain];
  216. const retryCommand = `cast send ${deployment.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userRandomNumber} ${request.randomNumber} -r ${deployment.rpc} --private-key <YOUR_PRIVATE_KEY>`;
  217. return (
  218. <>
  219. <InfoBox
  220. header="Callback failed!"
  221. icon={<Warning />}
  222. className={styles.message}
  223. variant="warning"
  224. >
  225. <CallbackFailureMessage request={request} />
  226. </InfoBox>
  227. <InfoBox
  228. header="Retry the callback yourself"
  229. icon={<Code />}
  230. className={styles.message}
  231. variant="info"
  232. >
  233. {`If you'd like to execute your callback, you can run the command in your
  234. terminal or connect your wallet to run it here.`}
  235. <div
  236. style={{
  237. display: "flex",
  238. flexFlow: "row nowrap",
  239. justifyContent: "end",
  240. gap: "16px",
  241. marginTop: "16px",
  242. }}
  243. >
  244. <CopyButton text={retryCommand}>Copy Forge Command</CopyButton>
  245. <Button size="sm" variant="outline">
  246. Connect Wallet
  247. </Button>
  248. <Button
  249. size="sm"
  250. variant="ghost"
  251. beforeIcon={Question}
  252. rounded
  253. hideText
  254. href="https://docs.pyth.network/entropy/debug-callback-failures"
  255. target="_blank"
  256. >
  257. Help
  258. </Button>
  259. </div>
  260. </InfoBox>
  261. </>
  262. );
  263. };
  264. const CallbackFailureMessage = ({
  265. request,
  266. }: {
  267. request: CallbackErrorRequest;
  268. }) => {
  269. if (request.returnValue === "" && request.gasUsed > request.gasLimit) {
  270. return "The callback used more gas than the gas limit.";
  271. } else {
  272. const details = getErrorDetails(request.returnValue);
  273. return details ? (
  274. <>
  275. <p>The callback encountered the following error:</p>
  276. <p className={styles.details}>
  277. <b>{details[0]}</b> (<code>{request.returnValue}</code>): {details[1]}
  278. </p>
  279. </>
  280. ) : (
  281. <>
  282. The callback encountered an unknown error:{" "}
  283. <code>{request.returnValue}</code>
  284. </>
  285. );
  286. }
  287. };