Browse Source

Refactor multiple uses of `useAsync` into a single `usePostRequest` hook

debug-collab-settlement
Thomas Eizinger 3 years ago
parent
commit
622a8ea696
No known key found for this signature in database GPG Key ID: 651AC83A6C6C8B96
  1. 58
      taker-frontend/src/App.tsx
  2. 30
      taker-frontend/src/components/History.tsx
  3. 55
      taker-frontend/src/components/Wallet.tsx
  4. 62
      taker-frontend/src/usePostRequest.ts

58
taker-frontend/src/App.tsx

@ -12,15 +12,12 @@ import {
} from "@chakra-ui/react";
import * as React from "react";
import { useEffect, useState } from "react";
import { useAsync } from "react-async";
import { Route, Routes } from "react-router-dom";
import { useEventSource } from "react-sse-hooks";
import useWebSocket from "react-use-websocket";
import { useBackendMonitor } from "./components/BackendMonitor";
import createErrorToast from "./components/ErrorToast";
import Footer from "./components/Footer";
import History from "./components/History";
import { HttpError } from "./components/HttpError";
import Nav from "./components/NavBar";
import Trade from "./components/Trade";
import {
@ -34,38 +31,10 @@ import {
Order,
StateGroupKey,
WalletInfo,
WithdrawRequest,
} from "./components/Types";
import { Wallet, WalletInfoBar } from "./components/Wallet";
import useLatestEvent from "./useLatestEvent";
async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse> {
let res = await fetch(`/api/calculate/margin`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) {
const resp = await res.json();
throw new HttpError(resp);
}
return res.json();
}
async function postCfdOrderRequest(payload: CfdOrderRequestPayload) {
let res = await fetch(`/api/cfd/order`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) {
const resp = await res.json();
throw new HttpError(resp);
}
}
export async function postWithdraw(payload: WithdrawRequest) {
let res = await fetch(`/api/withdraw`, { method: "POST", body: JSON.stringify(payload) });
if (!res.status.toString().startsWith("2")) {
const resp = await res.json();
throw new HttpError(resp);
}
return res.text();
}
import usePostRequest from "./usePostRequest";
export const App = () => {
const toast = useToast();
@ -104,26 +73,13 @@ export const App = () => {
let effectiveQuantity = userHasEdited ? quantity : (min_quantity?.toString() || "0");
let { run: calculateMargin } = useAsync({
deferFn: async ([payload]: any[]) => {
try {
let res = await getMargin(payload as MarginRequestPayload);
setMargin(res.margin.toString());
} catch (e) {
createErrorToast(toast, e);
}
},
});
let { run: makeNewOrderRequest, isLoading: isCreatingNewOrderRequest } = useAsync({
deferFn: async ([payload]: any[]) => {
try {
await postCfdOrderRequest(payload as CfdOrderRequestPayload);
} catch (e) {
createErrorToast(toast, e);
}
let [calculateMargin] = usePostRequest<MarginRequestPayload, MarginResponse>(
"/api/calculate/margin",
(response) => {
setMargin(response.margin.toString());
},
});
);
let [makeNewOrderRequest, isCreatingNewOrderRequest] = usePostRequest<CfdOrderRequestPayload>("/api/cfd/order");
useEffect(() => {
if (!order) {

30
taker-frontend/src/components/History.tsx

@ -25,12 +25,10 @@ import {
Text,
Tr,
useColorModeValue,
useToast,
VStack,
} from "@chakra-ui/react";
import * as React from "react";
import { useAsync } from "react-async";
import createErrorToast from "./ErrorToast";
import usePostRequest from "../usePostRequest";
import { Cfd, StateGroupKey, StateKey, Tx, TxLabel } from "./Types";
interface HistoryProps {
@ -63,15 +61,7 @@ interface CfdDetailsProps {
cfd: Cfd;
}
async function doPostAction(id: string, action: string) {
await fetch(
`/api/cfd/${id}/${action}`,
{ method: "POST", credentials: "include" },
);
}
const CfdDetails = ({ cfd }: CfdDetailsProps) => {
const toast = useToast();
const initialPrice = `$${cfd.initial_price.toLocaleString()}`;
const quantity = `$${cfd.quantity_usd}`;
const margin = `${Math.round((cfd.margin) * 1_000_000) / 1_000_000}`;
@ -86,16 +76,7 @@ const CfdDetails = ({ cfd }: CfdDetailsProps) => {
const txCet = cfd.details.tx_url_list.find((tx) => tx.label === TxLabel.Cet);
const txSettled = cfd.details.tx_url_list.find((tx) => tx.label === TxLabel.Collaborative);
let { run: postAction, isLoading: isActioning } = useAsync({
deferFn: async ([orderId, action]: any[]) => {
try {
console.log(`Closing: ${orderId} ${action}`);
await doPostAction(orderId, action);
} catch (e) {
createErrorToast(toast, e);
}
},
});
let [settle, isSettling] = usePostRequest(`/api/cfd/${cfd.order_id}/settle`);
const disableCloseButton = cfd.state.getGroup() === StateGroupKey.CLOSED
|| !(cfd.state.key === StateKey.OPEN);
@ -206,11 +187,12 @@ const CfdDetails = ({ cfd }: CfdDetailsProps) => {
<Button
size="sm"
colorScheme="red"
onClick={async () => {
await postAction(cfd.order_id, "settle");
onClick={() => {
console.log(`Settling CFD ${cfd.order_id}`);
settle({});
onClose();
}}
isLoading={isActioning}
isLoading={isSettling}
>
Close
</Button>

55
taker-frontend/src/components/Wallet.tsx

@ -24,13 +24,11 @@ import {
VStack,
} from "@chakra-ui/react";
import * as React from "react";
import { FormEvent, useState } from "react";
import { useAsync } from "react-async";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { postWithdraw } from "../App";
import createErrorToast from "./ErrorToast";
import usePostRequest from "../usePostRequest";
import Timestamp from "./Timestamp";
import { WalletInfo } from "./Types";
import { WalletInfo, WithdrawRequest } from "./Types";
interface WalletProps {
walletInfo: WalletInfo | null;
@ -48,30 +46,17 @@ export default function Wallet(
const [withdrawAmount, setWithdrawAmount] = useState(0);
const [fee, setFee] = useState(1);
const [withdrawAddress, setWithdrawAddress] = useState("");
let { run: runWithdraw, isLoading: isWithdrawing } = useAsync({
deferFn: async ([event]: FormEvent<HTMLFormElement>[]) => {
event.preventDefault();
try {
const url = await postWithdraw({
amount: withdrawAmount,
fee,
address: withdrawAddress,
});
window.open(url, "_blank");
toast({
title: "Withdraw successful",
description: <Link href={url} isExternal>
{url}
</Link>,
status: "info",
duration: 10000,
isClosable: true,
});
} catch (e) {
createErrorToast(toast, e);
}
},
const [runWithdraw, isWithdrawing] = usePostRequest<WithdrawRequest, string>("/api/withdraw", (url) => {
window.open(url, "_blank");
toast({
title: "Withdraw successful",
description: <Link href={url} isExternal>
{url}
</Link>,
status: "info",
duration: 10000,
isClosable: true,
});
});
return (
@ -108,7 +93,17 @@ export default function Wallet(
<Divider marginTop={2} marginBottom={2} />
<VStack padding={2}>
<form onSubmit={runWithdraw}>
<form
onSubmit={(event) => {
event.preventDefault();
runWithdraw({
amount: withdrawAmount,
fee,
address: withdrawAddress,
});
}}
>
<Heading as="h3" size="sm">Withdraw</Heading>
<FormControl id="address">
<FormLabel>Address</FormLabel>

62
taker-frontend/src/usePostRequest.ts

@ -0,0 +1,62 @@
import { useToast } from "@chakra-ui/react";
import { useAsync } from "react-async";
/**
* A React hook for sending a POST request to a certain endpoint.
*
* You can pass a callback (`onSuccess`) to process the response. By default, we extract the HTTP body as JSON
*/
export default function usePostRequest<Req = any, Res = any>(
url: string,
onSuccess: (response: Res) => void = () => {},
): [(req: Req) => void, boolean] {
const toast = useToast();
let { run, isLoading } = useAsync({
deferFn: async ([payload]: any[]) => {
let res = await fetch(url, {
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-type": "application/json",
},
});
if (!res.status.toString().startsWith("2")) {
let problem = await res.json() as Problem;
toast({
title: "Error: " + problem.title,
description: problem.detail,
status: "error",
duration: 10000,
isClosable: true,
});
return;
}
let responseType = res.headers.get("Content-type");
if (responseType && responseType.startsWith("application/json")) {
onSuccess(await res.json() as Res);
return;
}
if (responseType && responseType.startsWith("text/plain")) {
onSuccess(await res.text() as unknown as Res); // `unknown` cast is not ideal because we known that `.text()` gives us string.
return;
}
// if none of the above content types match, pass bytes to the caller
onSuccess(await res.blob() as unknown as Res); // `unknown` cast is not ideal because we known that `.blob()` gives us as blob.
},
});
return [run as (req: Req) => void, isLoading];
}
interface Problem {
title: string;
detail: string;
}
Loading…
Cancel
Save