Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,5 @@ |
|||||
|
node_modules |
||||
|
.DS_Store |
||||
|
dist-ssr |
||||
|
*.local |
||||
|
dist/ |
@ -0,0 +1,13 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<link rel="icon" type="image/svg+xml" href="/src/logo.svg" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<title>Itchy Stats</title> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div id="root"></div> |
||||
|
<script type="module" src="/src/main.tsx"></script> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,67 @@ |
|||||
|
{ |
||||
|
"name": "itchysats", |
||||
|
"version": "0.1.0", |
||||
|
"scripts": { |
||||
|
"dev": "vite", |
||||
|
"build": "vite build", |
||||
|
"serve": "vite preview", |
||||
|
"eslint": "eslint src/**/*.{ts,tsx}", |
||||
|
"tsc": "tsc" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@chakra-ui/icons": "^1.0.16", |
||||
|
"@chakra-ui/react": "^1.6.10", |
||||
|
"@emotion/react": "^11", |
||||
|
"@emotion/styled": "^11", |
||||
|
"react": "^17.0.2", |
||||
|
"react-dom": "^17.0.2", |
||||
|
"@testing-library/jest-dom": "^5.9.0", |
||||
|
"@testing-library/react": "^10.2.1", |
||||
|
"@testing-library/user-event": "^12.0.2", |
||||
|
"@types/jest": "^25.0.0", |
||||
|
"@types/node": "^12.0.0", |
||||
|
"@types/react": "^17.0.34", |
||||
|
"@types/react-dom": "^17.0.11", |
||||
|
"@types/react-router-dom": "5.3.1", |
||||
|
"framer-motion": "^4", |
||||
|
"react-async": "^10.0.1", |
||||
|
"react-icons": "^3.0.0", |
||||
|
"react-refresh": "^0.10.0", |
||||
|
"react-router-dom": "5.3.0", |
||||
|
"react-scripts": "4.0.3", |
||||
|
"react-sse-hooks": "^1.0.5", |
||||
|
"web-vitals": "^0.2.2", |
||||
|
"@vitejs/plugin-react": "^1.0.0", |
||||
|
"typescript": "^4.4.4", |
||||
|
"vite": "^2.6.13" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/eslint": "^7", |
||||
|
"@types/react": "^17.0.34", |
||||
|
"@types/react-dom": "^17.0.11", |
||||
|
"typescript": "^4.4.4", |
||||
|
"vite": "^2.6.13" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"parser": "@typescript-eslint/parser", |
||||
|
"plugins": [ |
||||
|
"@typescript-eslint" |
||||
|
], |
||||
|
"parserOptions": { |
||||
|
"project": "./tsconfig.json" |
||||
|
}, |
||||
|
"extends": [ |
||||
|
"react-app", |
||||
|
"react-app/jest" |
||||
|
], |
||||
|
"rules": { |
||||
|
"@typescript-eslint/no-floating-promises": "error", |
||||
|
"@typescript-eslint/promise-function-async": "error", |
||||
|
"require-await": "off", |
||||
|
"@typescript-eslint/require-await": "error", |
||||
|
"@typescript-eslint/no-use-before-define": "off", |
||||
|
"@typescript-eslint/no-unused-vars": "error", |
||||
|
"react-hooks/exhaustive-deps": "error" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,168 @@ |
|||||
|
import { Box, StackDivider, useToast, VStack } from "@chakra-ui/react"; |
||||
|
import * as React from "react"; |
||||
|
import { useEffect, useState } from "react"; |
||||
|
import { useAsync } from "react-async"; |
||||
|
import { Route, Switch } from "react-router-dom"; |
||||
|
import { useEventSource } from "react-sse-hooks"; |
||||
|
import History from "./components/History"; |
||||
|
import Nav from "./components/NavBar"; |
||||
|
import Trade from "./components/Trade"; |
||||
|
import { |
||||
|
Cfd, |
||||
|
CfdOrderRequestPayload, |
||||
|
intoCfd, |
||||
|
intoOrder, |
||||
|
MarginRequestPayload, |
||||
|
MarginResponse, |
||||
|
Order, |
||||
|
StateGroupKey, |
||||
|
WalletInfo, |
||||
|
} from "./components/Types"; |
||||
|
import { Wallet } from "./components/Wallet"; |
||||
|
import useLatestEvent from "./Hooks"; |
||||
|
|
||||
|
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")) { |
||||
|
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText); |
||||
|
} |
||||
|
|
||||
|
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")) { |
||||
|
console.log(`Error${JSON.stringify(res)}`); |
||||
|
throw new Error("failed to create new CFD order request: " + res.status + ", " + res.statusText); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const App = () => { |
||||
|
const toast = useToast(); |
||||
|
|
||||
|
let source = useEventSource({ source: "/api/feed" }); |
||||
|
const walletInfo = useLatestEvent<WalletInfo>(source, "wallet"); |
||||
|
const order = useLatestEvent<Order>(source, "order", intoOrder); |
||||
|
const cfdsOrUndefined = useLatestEvent<Cfd[]>(source, "cfds", intoCfd); |
||||
|
let cfds = cfdsOrUndefined ? cfdsOrUndefined! : []; |
||||
|
cfds.sort((a, b) => a.order_id.localeCompare(b.order_id)); |
||||
|
|
||||
|
let [quantity, setQuantity] = useState("0"); |
||||
|
let [margin, setMargin] = useState("0"); |
||||
|
let [userHasEdited, setUserHasEdited] = useState(false); |
||||
|
|
||||
|
const { price, min_quantity, max_quantity, leverage, liquidation_price: liquidationPrice } = order || {}; |
||||
|
|
||||
|
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) { |
||||
|
const description = typeof e === "string" ? e : JSON.stringify(e); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Error", |
||||
|
description, |
||||
|
status: "error", |
||||
|
duration: 9000, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
let { run: makeNewOrderRequest, isLoading: isCreatingNewOrderRequest } = useAsync({ |
||||
|
deferFn: async ([payload]: any[]) => { |
||||
|
try { |
||||
|
await postCfdOrderRequest(payload as CfdOrderRequestPayload); |
||||
|
} catch (e) { |
||||
|
console.error(`Error received: ${JSON.stringify(e)}`); |
||||
|
const description = typeof e === "string" ? e : JSON.stringify(e); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Error", |
||||
|
description, |
||||
|
status: "error", |
||||
|
duration: 9000, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!order) { |
||||
|
return; |
||||
|
} |
||||
|
let quantity = effectiveQuantity ? Number.parseFloat(effectiveQuantity) : 0; |
||||
|
let payload: MarginRequestPayload = { |
||||
|
leverage: order.leverage, |
||||
|
price: order.price, |
||||
|
quantity, |
||||
|
}; |
||||
|
calculateMargin(payload); |
||||
|
}, // Eslint demands us to include `calculateMargin` in the list of dependencies.
|
||||
|
// We don't want that as we will end up in an endless loop. It is safe to ignore `calculateMargin` because
|
||||
|
// nothing in `calculateMargin` depends on outside values, i.e. is guaranteed to be stable.
|
||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
[margin, effectiveQuantity, order]); |
||||
|
|
||||
|
const format = (val: any) => `$` + val; |
||||
|
const parse = (val: any) => val.replace(/^\$/, ""); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Nav walletInfo={walletInfo} /> |
||||
|
<Box textAlign="center" padding={3}> |
||||
|
<Switch> |
||||
|
<Route path="/"> |
||||
|
<VStack divider={<StackDivider borderColor="gray.500" />} spacing={4}> |
||||
|
<Trade |
||||
|
order_id={order?.id} |
||||
|
quantity={format(effectiveQuantity)} |
||||
|
max_quantity={max_quantity || 0} |
||||
|
min_quantity={min_quantity || 0} |
||||
|
price={price} |
||||
|
margin={margin} |
||||
|
leverage={leverage} |
||||
|
liquidationPrice={liquidationPrice} |
||||
|
onQuantityChange={(valueString: string) => { |
||||
|
setUserHasEdited(true); |
||||
|
setQuantity(parse(valueString)); |
||||
|
if (!order) { |
||||
|
return; |
||||
|
} |
||||
|
let quantity = valueString ? Number.parseFloat(valueString) : 0; |
||||
|
let payload: MarginRequestPayload = { |
||||
|
leverage: order.leverage, |
||||
|
price: order.price, |
||||
|
quantity, |
||||
|
}; |
||||
|
calculateMargin(payload); |
||||
|
}} |
||||
|
onLongSubmit={makeNewOrderRequest} |
||||
|
isSubmitting={isCreatingNewOrderRequest} |
||||
|
/> |
||||
|
<History |
||||
|
cfds={cfds.filter((cfd) => cfd.state.getGroup() !== StateGroupKey.CLOSED)} |
||||
|
title={"Open Positions"} |
||||
|
/> |
||||
|
<History |
||||
|
cfds={cfds.filter((cfd) => cfd.state.getGroup() === StateGroupKey.CLOSED)} |
||||
|
title={"Closed Positions"} |
||||
|
/> |
||||
|
</VStack> |
||||
|
</Route> |
||||
|
<Route path="/wallet"> |
||||
|
<Wallet walletInfo={walletInfo} /> |
||||
|
</Route> |
||||
|
</Switch> |
||||
|
</Box> |
||||
|
</> |
||||
|
); |
||||
|
}; |
@ -0,0 +1,27 @@ |
|||||
|
import { useState } from "react"; |
||||
|
import { useEventSourceListener } from "react-sse-hooks"; |
||||
|
|
||||
|
export default function useLatestEvent<T>( |
||||
|
source: EventSource, |
||||
|
event_name: string, |
||||
|
mapping: (key: string, value: any) => any = (key, value) => value, |
||||
|
): T | null { |
||||
|
const [state, setState] = useState<T | null>(null); |
||||
|
|
||||
|
useEventSourceListener<T | null>( |
||||
|
{ |
||||
|
source: source, |
||||
|
startOnInit: true, |
||||
|
event: { |
||||
|
name: event_name, |
||||
|
listener: ({ event }) => { |
||||
|
// @ts-ignore - yes, there is a data field on event
|
||||
|
setState(JSON.parse(event.data, mapping)); |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
[source], |
||||
|
); |
||||
|
|
||||
|
return state; |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
import { IconButton, IconButtonProps, useColorMode, useColorModeValue } from "@chakra-ui/react"; |
||||
|
import * as React from "react"; |
||||
|
import { FaMoon, FaSun } from "react-icons/fa"; |
||||
|
|
||||
|
type ColorModeSwitcherProps = Omit<IconButtonProps, "aria-label">; |
||||
|
|
||||
|
export const ColorModeSwitcher: React.FC<ColorModeSwitcherProps> = (props) => { |
||||
|
const { toggleColorMode } = useColorMode(); |
||||
|
const text = useColorModeValue("dark", "light"); |
||||
|
const SwitchIcon = useColorModeValue(FaMoon, FaSun); |
||||
|
|
||||
|
return ( |
||||
|
<IconButton |
||||
|
size="md" |
||||
|
fontSize="lg" |
||||
|
variant="ghost" |
||||
|
color="current" |
||||
|
marginLeft="2" |
||||
|
onClick={toggleColorMode} |
||||
|
icon={<SwitchIcon />} |
||||
|
aria-label={`Switch to ${text} mode`} |
||||
|
{...props} |
||||
|
/> |
||||
|
); |
||||
|
}; |
@ -0,0 +1,264 @@ |
|||||
|
import { ExternalLinkIcon, Icon } from "@chakra-ui/icons"; |
||||
|
import { |
||||
|
Badge, |
||||
|
Box, |
||||
|
Button, |
||||
|
Center, |
||||
|
Checkbox, |
||||
|
Divider, |
||||
|
GridItem, |
||||
|
Heading, |
||||
|
HStack, |
||||
|
Link, |
||||
|
Popover, |
||||
|
PopoverArrow, |
||||
|
PopoverBody, |
||||
|
PopoverCloseButton, |
||||
|
PopoverContent, |
||||
|
PopoverFooter, |
||||
|
PopoverHeader, |
||||
|
PopoverTrigger, |
||||
|
SimpleGrid, |
||||
|
Spinner, |
||||
|
Table, |
||||
|
Tbody, |
||||
|
Td, |
||||
|
Text, |
||||
|
Tr, |
||||
|
useColorModeValue, |
||||
|
useToast, |
||||
|
VStack, |
||||
|
} from "@chakra-ui/react"; |
||||
|
import * as React from "react"; |
||||
|
import { useAsync } from "react-async"; |
||||
|
import { Cfd, StateGroupKey, StateKey, Tx, TxLabel } from "./Types"; |
||||
|
|
||||
|
interface HistoryProps { |
||||
|
cfds: Cfd[]; |
||||
|
title: string; |
||||
|
} |
||||
|
|
||||
|
const History = ({ cfds, title }: HistoryProps) => { |
||||
|
return ( |
||||
|
<VStack spacing={3}> |
||||
|
<Heading size={"lg"} padding={2}>{title}</Heading> |
||||
|
<SimpleGrid |
||||
|
columns={{ sm: 2, md: 4 }} |
||||
|
gap={4} |
||||
|
> |
||||
|
{cfds.map((cfd) => { |
||||
|
return (<GridItem rowSpan={1} colSpan={2} key={cfd.order_id}> |
||||
|
<CfdDetails cfd={cfd} /> |
||||
|
</GridItem>); |
||||
|
})} |
||||
|
</SimpleGrid> |
||||
|
</VStack> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default History; |
||||
|
|
||||
|
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}`; |
||||
|
const liquidationPrice = `$${cfd.liquidation_price}`; |
||||
|
const pAndL = Math.round((cfd.profit_btc) * 1_000_000) / 1_000_000; |
||||
|
const expiry = cfd.expiry_timestamp; |
||||
|
const profit = Math.round((cfd.margin + cfd.profit_btc) * 1_000_000) / 1_000_000; |
||||
|
|
||||
|
const txLock = cfd.details.tx_url_list.find((tx) => tx.label === TxLabel.Lock); |
||||
|
const txCommit = cfd.details.tx_url_list.find((tx) => tx.label === TxLabel.Commit); |
||||
|
const txRefund = cfd.details.tx_url_list.find((tx) => tx.label === TxLabel.Refund); |
||||
|
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) { |
||||
|
const description = typeof e === "string" ? e : JSON.stringify(e); |
||||
|
|
||||
|
toast({ |
||||
|
title: "Error", |
||||
|
description, |
||||
|
status: "error", |
||||
|
duration: 9000, |
||||
|
isClosable: true, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const disableCloseButton = cfd.state.getGroup() === StateGroupKey.CLOSED |
||||
|
|| [StateKey.OPEN_COMMITTED, StateKey.OUTGOING_SETTLEMENT_PROPOSAL, StateKey.PENDING_CLOSE].includes( |
||||
|
cfd.state.key, |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<HStack bg={useColorModeValue("gray.100", "gray.700")} rounded={5}> |
||||
|
<Center rounded={5} h={"100%"}> |
||||
|
<Table variant="striped" colorScheme="gray" size="sm"> |
||||
|
<Tbody> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Quantity</Text></Td> |
||||
|
<Td>{quantity}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Opening price</Text></Td> |
||||
|
<Td>{initialPrice}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Liquidation</Text></Td> |
||||
|
<Td>{liquidationPrice}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Margin</Text></Td> |
||||
|
<Td>{margin}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Unrealized P/L</Text></Td> |
||||
|
<Td>{pAndL.toString()}</Td> |
||||
|
</Tr> |
||||
|
</Tbody> |
||||
|
</Table> |
||||
|
</Center> |
||||
|
<VStack> |
||||
|
<Badge colorScheme={cfd.state.getColorScheme()}>{cfd.state.getLabel()}</Badge> |
||||
|
<HStack w={"95%"}> |
||||
|
<VStack> |
||||
|
<TxIcon tx={txLock} /> |
||||
|
<Text>Lock</Text> |
||||
|
</VStack> |
||||
|
{txSettled |
||||
|
? <> |
||||
|
<Divider variant={txSettled ? "solid" : "dashed"} /> |
||||
|
<VStack> |
||||
|
<TxIcon tx={txSettled} /> |
||||
|
<Text>Payout</Text> |
||||
|
</VStack> |
||||
|
</> |
||||
|
: <> |
||||
|
<Divider variant={txCommit ? "solid" : "dashed"} /> |
||||
|
<VStack> |
||||
|
<TxIcon tx={txCommit} /> |
||||
|
<Text>Commit</Text> |
||||
|
</VStack> |
||||
|
|
||||
|
{txRefund |
||||
|
? <> |
||||
|
<Divider variant={txRefund ? "solid" : "dashed"} /> |
||||
|
<VStack> |
||||
|
<TxIcon tx={txRefund} /> |
||||
|
<Text>Refund</Text> |
||||
|
</VStack> |
||||
|
</> |
||||
|
: <> |
||||
|
<Divider variant={txCet ? "solid" : "dashed"} /> |
||||
|
<VStack> |
||||
|
<TxIcon tx={txCet} /> |
||||
|
<Text>Payout</Text> |
||||
|
</VStack> |
||||
|
</>} |
||||
|
</>} |
||||
|
</HStack> |
||||
|
<HStack> |
||||
|
<Box w={"45%"}> |
||||
|
<Text fontSize={"sm"} align={"left"}> |
||||
|
At the current rate you would receive <b>₿ {profit}</b> |
||||
|
</Text> |
||||
|
</Box> |
||||
|
<Box w={"45%"}> |
||||
|
<Popover |
||||
|
placement="bottom" |
||||
|
closeOnBlur={true} |
||||
|
> |
||||
|
{({ onClose }) => (<> |
||||
|
<PopoverTrigger> |
||||
|
<Button colorScheme={"blue"} disabled={disableCloseButton}>Close</Button> |
||||
|
</PopoverTrigger> |
||||
|
<PopoverContent color="white" bg="blue.800" borderColor="blue.800"> |
||||
|
<PopoverHeader pt={4} fontWeight="bold" border="0"> |
||||
|
Close your position |
||||
|
</PopoverHeader> |
||||
|
<PopoverArrow /> |
||||
|
<PopoverCloseButton /> |
||||
|
<PopoverBody> |
||||
|
<Text> |
||||
|
This will force-close your position if your counterparty cannot be reached. |
||||
|
The exchange rate at {expiry} |
||||
|
will determine your profit/losses. It is likely that the rate will change |
||||
|
until then. |
||||
|
</Text> |
||||
|
</PopoverBody> |
||||
|
<PopoverFooter |
||||
|
border="0" |
||||
|
d="flex" |
||||
|
alignItems="center" |
||||
|
justifyContent="space-between" |
||||
|
pb={4} |
||||
|
> |
||||
|
<Button |
||||
|
size="sm" |
||||
|
colorScheme="red" |
||||
|
onClick={async () => { |
||||
|
await postAction(cfd.order_id, "settle"); |
||||
|
onClose(); |
||||
|
}} |
||||
|
isLoading={isActioning} |
||||
|
> |
||||
|
Close |
||||
|
</Button> |
||||
|
<Checkbox defaultIsChecked>Don't show this again</Checkbox> |
||||
|
</PopoverFooter> |
||||
|
</PopoverContent> |
||||
|
</>)} |
||||
|
</Popover> |
||||
|
</Box> |
||||
|
</HStack> |
||||
|
</VStack> |
||||
|
</HStack> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const CircleIcon = (props: any) => ( |
||||
|
<Icon viewBox="0 0 200 200" {...props}> |
||||
|
<path |
||||
|
stroke="currentColor" |
||||
|
fill="transparent" |
||||
|
d="M 100, 100 m -75, 0 a 75,75 0 1,0 150,0 a 75,75 0 1,0 -150,0" |
||||
|
/> |
||||
|
</Icon> |
||||
|
); |
||||
|
|
||||
|
interface TxIconProps { |
||||
|
tx?: Tx; |
||||
|
} |
||||
|
|
||||
|
const TxIcon = ({ tx }: TxIconProps) => { |
||||
|
const iconColor = useColorModeValue("white.200", "white.600"); |
||||
|
|
||||
|
if (!tx) { |
||||
|
return (<CircleIcon boxSize={5} color={iconColor} />); |
||||
|
} else if (tx && !tx.url) { |
||||
|
return (<Spinner mx="2px" color={iconColor} speed="1.65s" />); |
||||
|
} else { |
||||
|
return (<Link href={tx.url!} isExternal> |
||||
|
<ExternalLinkIcon mx="2px" color={iconColor} /> |
||||
|
</Link>); |
||||
|
} |
||||
|
}; |
@ -0,0 +1,44 @@ |
|||||
|
import { Box, BoxProps } from "@chakra-ui/layout"; |
||||
|
import { Center, Image, Text, VStack } from "@chakra-ui/react"; |
||||
|
import { motion } from "framer-motion"; |
||||
|
import React from "react"; |
||||
|
import { useHistory } from "react-router-dom"; |
||||
|
import logo from "../images/logo.svg"; |
||||
|
|
||||
|
const MotionBox = motion<BoxProps>(Box); |
||||
|
|
||||
|
export const Logo = () => { |
||||
|
const history = useHistory(); |
||||
|
|
||||
|
function handleClick() { |
||||
|
history.push("/trade"); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Center> |
||||
|
<VStack> |
||||
|
<MotionBox |
||||
|
height="200px" |
||||
|
width="200px" |
||||
|
whileHover={{ scale: 1.2 }} |
||||
|
whileTap={{ scale: 0.9 }} |
||||
|
animate={{ |
||||
|
scale: [1, 1.5, 1.5, 1, 1], |
||||
|
rotate: [0, 360], |
||||
|
}} |
||||
|
onClick={handleClick} |
||||
|
// @ts-ignore: lint is complaining but should be fine :)
|
||||
|
transition={{ |
||||
|
repeat: 999, |
||||
|
repeatType: "loop", |
||||
|
duration: 2, |
||||
|
}} |
||||
|
> |
||||
|
<Image src={logo} width="200px" /> |
||||
|
</MotionBox> |
||||
|
|
||||
|
<Text>Click to enter</Text> |
||||
|
</VStack> |
||||
|
</Center> |
||||
|
); |
||||
|
}; |
@ -0,0 +1,78 @@ |
|||||
|
import { HamburgerIcon, MoonIcon, SunIcon } from "@chakra-ui/icons"; |
||||
|
import { |
||||
|
Box, |
||||
|
Button, |
||||
|
Flex, |
||||
|
Image, |
||||
|
Menu, |
||||
|
MenuButton, |
||||
|
MenuItem, |
||||
|
MenuList, |
||||
|
Stack, |
||||
|
useColorMode, |
||||
|
useColorModeValue, |
||||
|
} from "@chakra-ui/react"; |
||||
|
import * as React from "react"; |
||||
|
import { useHistory } from "react-router-dom"; |
||||
|
import logoBlack from "../images/logo_nav_bar_black.svg"; |
||||
|
import logoWhite from "../images/logo_nav_bar_white.svg"; |
||||
|
import { WalletInfo } from "./Types"; |
||||
|
import { WalletNavBar } from "./Wallet"; |
||||
|
|
||||
|
interface NavProps { |
||||
|
walletInfo: WalletInfo | null; |
||||
|
} |
||||
|
|
||||
|
export default function Nav({ walletInfo }: NavProps) { |
||||
|
const history = useHistory(); |
||||
|
|
||||
|
const { toggleColorMode } = useColorMode(); |
||||
|
const navBarLog = useColorModeValue( |
||||
|
<Image src={logoBlack} w="128px" />, |
||||
|
<Image src={logoWhite} w="128px" />, |
||||
|
); |
||||
|
|
||||
|
const toggleIcon = useColorModeValue( |
||||
|
<MoonIcon />, |
||||
|
<SunIcon />, |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<Box bg={useColorModeValue("gray.100", "gray.900")} px={4}> |
||||
|
<Flex h={16} alignItems={"center"} justifyContent={"space-between"}> |
||||
|
<Menu> |
||||
|
<MenuButton |
||||
|
as={Button} |
||||
|
variant={"link"} |
||||
|
cursor={"pointer"} |
||||
|
minW={0} |
||||
|
> |
||||
|
<HamburgerIcon w={"32px"} /> |
||||
|
</MenuButton> |
||||
|
<MenuList alignItems={"center"}> |
||||
|
<MenuItem onClick={() => history.push("/trade")}>Home</MenuItem> |
||||
|
<MenuItem onClick={() => history.push("/wallet")}>Wallet</MenuItem> |
||||
|
<MenuItem>Settings</MenuItem> |
||||
|
</MenuList> |
||||
|
</Menu> |
||||
|
|
||||
|
<WalletNavBar walletInfo={walletInfo} /> |
||||
|
|
||||
|
<Flex alignItems={"center"}> |
||||
|
<Stack direction={"row"} spacing={7}> |
||||
|
<Button onClick={toggleColorMode} bg={"transparent"}> |
||||
|
{toggleIcon} |
||||
|
</Button> |
||||
|
<Box> |
||||
|
<Button bg={"transparent"} onClick={() => history.push("/trade")}> |
||||
|
{navBarLog} |
||||
|
</Button> |
||||
|
</Box> |
||||
|
</Stack> |
||||
|
</Flex> |
||||
|
</Flex> |
||||
|
</Box> |
||||
|
</> |
||||
|
); |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
import { Text } from "@chakra-ui/react"; |
||||
|
import React from "react"; |
||||
|
import { unixTimestampToDate } from "./Types"; |
||||
|
|
||||
|
interface Props { |
||||
|
timestamp: number; |
||||
|
} |
||||
|
|
||||
|
export default function Timestamp( |
||||
|
{ |
||||
|
timestamp, |
||||
|
}: Props, |
||||
|
) { |
||||
|
return ( |
||||
|
<Text> |
||||
|
{unixTimestampToDate(timestamp).toLocaleDateString("en-US", { |
||||
|
year: "numeric", |
||||
|
month: "numeric", |
||||
|
day: "numeric", |
||||
|
hour: "2-digit", |
||||
|
minute: "2-digit", |
||||
|
second: "2-digit", |
||||
|
})} |
||||
|
</Text> |
||||
|
); |
||||
|
} |
@ -0,0 +1,294 @@ |
|||||
|
import { BoxProps } from "@chakra-ui/layout"; |
||||
|
import { |
||||
|
Box, |
||||
|
Button, |
||||
|
ButtonGroup, |
||||
|
Center, |
||||
|
Checkbox, |
||||
|
Circle, |
||||
|
FormControl, |
||||
|
FormHelperText, |
||||
|
FormLabel, |
||||
|
Grid, |
||||
|
GridItem, |
||||
|
HStack, |
||||
|
Modal, |
||||
|
ModalBody, |
||||
|
ModalCloseButton, |
||||
|
ModalContent, |
||||
|
ModalFooter, |
||||
|
ModalHeader, |
||||
|
ModalOverlay, |
||||
|
NumberDecrementStepper, |
||||
|
NumberIncrementStepper, |
||||
|
NumberInput, |
||||
|
NumberInputField, |
||||
|
NumberInputStepper, |
||||
|
Skeleton, |
||||
|
Slider, |
||||
|
SliderFilledTrack, |
||||
|
SliderThumb, |
||||
|
SliderTrack, |
||||
|
Table, |
||||
|
TableCaption, |
||||
|
Tbody, |
||||
|
Td, |
||||
|
Text, |
||||
|
Tr, |
||||
|
useColorModeValue, |
||||
|
useDisclosure, |
||||
|
VStack, |
||||
|
} from "@chakra-ui/react"; |
||||
|
import { motion } from "framer-motion"; |
||||
|
import * as React from "react"; |
||||
|
import { useAsync } from "react-async"; |
||||
|
import { CfdOrderRequestPayload } from "./Types"; |
||||
|
|
||||
|
const MotionBox = motion<BoxProps>(Box); |
||||
|
|
||||
|
interface TradeProps { |
||||
|
order_id?: string; |
||||
|
min_quantity: number; |
||||
|
max_quantity: number; |
||||
|
price?: number; |
||||
|
margin?: string; |
||||
|
leverage?: number; |
||||
|
quantity: string; |
||||
|
liquidationPrice?: number; |
||||
|
isSubmitting: boolean; |
||||
|
onQuantityChange: any; |
||||
|
onLongSubmit: (payload: CfdOrderRequestPayload) => void; |
||||
|
} |
||||
|
|
||||
|
const Trade = ( |
||||
|
{ |
||||
|
min_quantity, |
||||
|
max_quantity, |
||||
|
price: priceAsNumber, |
||||
|
quantity, |
||||
|
onQuantityChange, |
||||
|
margin: marginAsNumber, |
||||
|
leverage, |
||||
|
liquidationPrice: liquidationPriceAsNumber, |
||||
|
onLongSubmit, |
||||
|
order_id, |
||||
|
}: TradeProps, |
||||
|
) => { |
||||
|
let outerCircleBg = useColorModeValue("gray.100", "gray.700"); |
||||
|
let innerCircleBg = useColorModeValue("gray.200", "gray.600"); |
||||
|
|
||||
|
const price = `$${priceAsNumber?.toLocaleString() || "0.0"}`; |
||||
|
const liquidationPrice = `$${liquidationPriceAsNumber?.toLocaleString() || "0.0"}`; |
||||
|
const margin = `₿${marginAsNumber?.toLocaleString() || "0.0"}`; |
||||
|
|
||||
|
const { isOpen, onOpen, onClose } = useDisclosure(); |
||||
|
|
||||
|
let { run: goLong, isLoading: isSubmitting } = useAsync({ |
||||
|
deferFn: async () => { |
||||
|
const quantityAsNumber = quantity.replace("$", ""); |
||||
|
|
||||
|
let payload: CfdOrderRequestPayload = { |
||||
|
order_id: order_id!, |
||||
|
quantity: Number.parseFloat(quantityAsNumber), |
||||
|
}; |
||||
|
await onLongSubmit(payload); |
||||
|
onClose(); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<Center> |
||||
|
<Grid |
||||
|
templateRows="repeat(1, 1fr)" |
||||
|
templateColumns="repeat(1, 1fr)" |
||||
|
gap={4} |
||||
|
> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Center> |
||||
|
<MotionBox |
||||
|
variants={{ |
||||
|
pulse: { |
||||
|
scale: [1, 1.05, 1], |
||||
|
}, |
||||
|
}} |
||||
|
// @ts-ignore: lint is complaining but should be fine :)
|
||||
|
transition={{ |
||||
|
// type: "spring",
|
||||
|
ease: "linear", |
||||
|
duration: 2, |
||||
|
repeat: Infinity, |
||||
|
}} |
||||
|
animate={"pulse"} |
||||
|
> |
||||
|
<Circle size="256px" bg={outerCircleBg}> |
||||
|
<Circle size="180px" bg={innerCircleBg}> |
||||
|
<MotionBox> |
||||
|
<Skeleton isLoaded={!!price}> |
||||
|
<Text fontSize={"4xl"} as="b">{price}</Text> |
||||
|
</Skeleton> |
||||
|
</MotionBox> |
||||
|
</Circle> |
||||
|
</Circle> |
||||
|
</MotionBox> |
||||
|
</Center> |
||||
|
</GridItem> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Quantity min={min_quantity} max={max_quantity} quantity={quantity} onChange={onQuantityChange} /> |
||||
|
</GridItem> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Leverage leverage={leverage} /> |
||||
|
</GridItem> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Margin margin={margin} /> |
||||
|
</GridItem> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Liquidation value={liquidationPrice} /> |
||||
|
</GridItem> |
||||
|
<GridItem colSpan={1}> |
||||
|
<Center> |
||||
|
<ButtonGroup variant="solid" padding="3" spacing="6"> |
||||
|
<Button colorScheme="red" size="lg" disabled h={16}> |
||||
|
<VStack> |
||||
|
<Text as="b">Short</Text> |
||||
|
<Text fontSize={"sm"}>{quantity.replace("$", "")}@{price}</Text> |
||||
|
</VStack> |
||||
|
</Button> |
||||
|
<Button colorScheme="green" size="lg" onClick={onOpen} h={16}> |
||||
|
<VStack> |
||||
|
<Text as="b">Long</Text> |
||||
|
<Text fontSize={"sm"}>{quantity.replace("$", "")}@{price}</Text> |
||||
|
</VStack> |
||||
|
</Button> |
||||
|
|
||||
|
<Modal isOpen={isOpen} onClose={onClose}> |
||||
|
<ModalOverlay /> |
||||
|
<ModalContent> |
||||
|
<ModalHeader>Market buy <b>{quantity}</b> of BTC/USD @ <b>{price}</b></ModalHeader> |
||||
|
<ModalCloseButton /> |
||||
|
<ModalBody> |
||||
|
<Table variant="striped" colorScheme="gray" size="sm"> |
||||
|
<TableCaption> |
||||
|
By submitting {margin} will be locked on-chain in a contract. |
||||
|
</TableCaption> |
||||
|
<Tbody> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Margin</Text></Td> |
||||
|
<Td>{margin}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Leverage</Text></Td> |
||||
|
<Td>{leverage}</Td> |
||||
|
</Tr> |
||||
|
<Tr> |
||||
|
<Td><Text as={"b"}>Liquidation Price</Text></Td> |
||||
|
<Td>{liquidationPrice}</Td> |
||||
|
</Tr> |
||||
|
</Tbody> |
||||
|
</Table> |
||||
|
</ModalBody> |
||||
|
|
||||
|
<ModalFooter> |
||||
|
<HStack> |
||||
|
<Button colorScheme="teal" isLoading={isSubmitting} onClick={goLong}> |
||||
|
Confirm |
||||
|
</Button> |
||||
|
<Checkbox defaultIsChecked>Always show this dialog</Checkbox> |
||||
|
</HStack> |
||||
|
</ModalFooter> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</ButtonGroup> |
||||
|
</Center> |
||||
|
</GridItem> |
||||
|
</Grid> |
||||
|
</Center> |
||||
|
); |
||||
|
}; |
||||
|
export default Trade; |
||||
|
|
||||
|
interface QuantityProps { |
||||
|
min: number; |
||||
|
max: number; |
||||
|
quantity: string; |
||||
|
onChange: any; |
||||
|
} |
||||
|
|
||||
|
function Quantity({ min, max, onChange, quantity }: QuantityProps) { |
||||
|
return ( |
||||
|
<FormControl id="quantity"> |
||||
|
<FormLabel>Quantity</FormLabel> |
||||
|
<NumberInput |
||||
|
min={min} |
||||
|
max={max} |
||||
|
default={min} |
||||
|
onChange={onChange} |
||||
|
value={quantity} |
||||
|
> |
||||
|
<NumberInputField /> |
||||
|
<NumberInputStepper> |
||||
|
<NumberIncrementStepper /> |
||||
|
<NumberDecrementStepper /> |
||||
|
</NumberInputStepper> |
||||
|
</NumberInput> |
||||
|
<FormHelperText>How much do you want to buy or sell?</FormHelperText> |
||||
|
</FormControl> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface LeverageProps { |
||||
|
leverage?: number; |
||||
|
} |
||||
|
|
||||
|
function Leverage({ leverage }: LeverageProps) { |
||||
|
return ( |
||||
|
<FormControl id="leverage"> |
||||
|
<FormLabel>Leverage (fixed to 2 atm)</FormLabel> |
||||
|
<Slider isReadOnly defaultValue={leverage} min={1} max={5} step={1}> |
||||
|
<SliderTrack> |
||||
|
<Box position="relative" right={10} /> |
||||
|
<SliderFilledTrack /> |
||||
|
</SliderTrack> |
||||
|
<SliderThumb boxSize={6}> |
||||
|
<Text color="black">{leverage}</Text> |
||||
|
</SliderThumb> |
||||
|
</Slider> |
||||
|
<FormHelperText> |
||||
|
How much do you want to leverage your position? |
||||
|
</FormHelperText> |
||||
|
</FormControl> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface MarginProps { |
||||
|
margin?: string; |
||||
|
} |
||||
|
|
||||
|
function Margin({ margin }: MarginProps) { |
||||
|
return ( |
||||
|
<VStack> |
||||
|
<HStack> |
||||
|
<Text as={"b"}>Required margin:</Text> |
||||
|
<Text>{margin}</Text> |
||||
|
</HStack> |
||||
|
<Text fontSize={"sm"} color={"darkgrey"}>The collateral you will need to provide</Text> |
||||
|
</VStack> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface LiquidationProps { |
||||
|
value?: string; |
||||
|
} |
||||
|
|
||||
|
function Liquidation({ value }: LiquidationProps) { |
||||
|
return ( |
||||
|
<VStack> |
||||
|
<HStack> |
||||
|
<Text as={"b"}>Liquidation price:</Text> |
||||
|
<Text>{value || "0.0"}</Text> |
||||
|
</HStack> |
||||
|
<Text fontSize={"sm"} color={"darkgrey"}> |
||||
|
You will lose your collateral if the price drops below this value |
||||
|
</Text> |
||||
|
</VStack> |
||||
|
); |
||||
|
} |
@ -0,0 +1,267 @@ |
|||||
|
export interface WalletInfo { |
||||
|
balance: number; |
||||
|
address: string; |
||||
|
last_updated_at: number; |
||||
|
} |
||||
|
|
||||
|
export function unixTimestampToDate(unixTimestamp: number): Date { |
||||
|
return new Date(unixTimestamp * 1000); |
||||
|
} |
||||
|
|
||||
|
export interface Order { |
||||
|
id: string; |
||||
|
trading_pair: string; |
||||
|
position: Position; |
||||
|
price: number; |
||||
|
min_quantity: number; |
||||
|
max_quantity: number; |
||||
|
leverage: number; |
||||
|
liquidation_price: number; |
||||
|
creation_timestamp: number; |
||||
|
settlement_time_interval_in_secs: number; |
||||
|
} |
||||
|
|
||||
|
export class Position { |
||||
|
constructor(public key: PositionKey) {} |
||||
|
|
||||
|
public getColorScheme(): string { |
||||
|
switch (this.key) { |
||||
|
case PositionKey.LONG: |
||||
|
return "green"; |
||||
|
case PositionKey.SHORT: |
||||
|
return "blue"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
enum PositionKey { |
||||
|
LONG = "Long", |
||||
|
SHORT = "Short", |
||||
|
} |
||||
|
|
||||
|
export interface Cfd { |
||||
|
order_id: string; |
||||
|
initial_price: number; |
||||
|
|
||||
|
leverage: number; |
||||
|
trading_pair: string; |
||||
|
position: Position; |
||||
|
liquidation_price: number; |
||||
|
|
||||
|
quantity_usd: number; |
||||
|
|
||||
|
margin: number; |
||||
|
|
||||
|
profit_btc: number; |
||||
|
profit_in_percent: number; |
||||
|
|
||||
|
state: State; |
||||
|
state_transition_timestamp: number; |
||||
|
details: CfdDetails; |
||||
|
expiry_timestamp: number; |
||||
|
} |
||||
|
|
||||
|
export interface CfdDetails { |
||||
|
tx_url_list: Tx[]; |
||||
|
payout?: number; |
||||
|
} |
||||
|
|
||||
|
export interface Tx { |
||||
|
label: TxLabel; |
||||
|
url: string; |
||||
|
} |
||||
|
|
||||
|
export enum TxLabel { |
||||
|
Lock = "Lock", |
||||
|
Commit = "Commit", |
||||
|
Cet = "Cet", |
||||
|
Refund = "Refund", |
||||
|
Collaborative = "Collaborative", |
||||
|
} |
||||
|
|
||||
|
export class State { |
||||
|
constructor(public key: StateKey) {} |
||||
|
|
||||
|
public getLabel(): string { |
||||
|
switch (this.key) { |
||||
|
case StateKey.INCOMING_ORDER_REQUEST: |
||||
|
return "Order Requested"; |
||||
|
case StateKey.OUTGOING_ORDER_REQUEST: |
||||
|
return "Order Requested"; |
||||
|
case StateKey.ACCEPTED: |
||||
|
return "Accepted"; |
||||
|
case StateKey.REJECTED: |
||||
|
return "Rejected"; |
||||
|
case StateKey.CONTRACT_SETUP: |
||||
|
return "Contract Setup"; |
||||
|
case StateKey.PENDING_OPEN: |
||||
|
return "Pending Open"; |
||||
|
case StateKey.OPEN: |
||||
|
return "Open"; |
||||
|
case StateKey.PENDING_COMMIT: |
||||
|
return "Pending Commit"; |
||||
|
case StateKey.PENDING_CLOSE: |
||||
|
return "Pending Close"; |
||||
|
case StateKey.OPEN_COMMITTED: |
||||
|
return "Open (commit-tx published)"; |
||||
|
case StateKey.INCOMING_SETTLEMENT_PROPOSAL: |
||||
|
return "Settlement Proposed"; |
||||
|
case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: |
||||
|
return "Settlement Proposed"; |
||||
|
case StateKey.INCOMING_ROLL_OVER_PROPOSAL: |
||||
|
return "Rollover Proposed"; |
||||
|
case StateKey.OUTGOING_ROLL_OVER_PROPOSAL: |
||||
|
return "Rollover Proposed"; |
||||
|
case StateKey.MUST_REFUND: |
||||
|
return "Refunding"; |
||||
|
case StateKey.REFUNDED: |
||||
|
return "Refunded"; |
||||
|
case StateKey.SETUP_FAILED: |
||||
|
return "Setup Failed"; |
||||
|
case StateKey.PENDING_CET: |
||||
|
return "Pending CET"; |
||||
|
case StateKey.CLOSED: |
||||
|
return "Closed"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getColorScheme(): string { |
||||
|
const default_color = "gray"; |
||||
|
const green = "green"; |
||||
|
const red = "red"; |
||||
|
const orange = "orange"; |
||||
|
|
||||
|
switch (this.key) { |
||||
|
case StateKey.ACCEPTED: |
||||
|
case StateKey.OPEN: |
||||
|
return green; |
||||
|
|
||||
|
case StateKey.REJECTED: |
||||
|
return red; |
||||
|
|
||||
|
case StateKey.PENDING_COMMIT: |
||||
|
case StateKey.OPEN_COMMITTED: |
||||
|
case StateKey.MUST_REFUND: |
||||
|
case StateKey.PENDING_CET: |
||||
|
case StateKey.PENDING_CLOSE: |
||||
|
return orange; |
||||
|
|
||||
|
case StateKey.OUTGOING_ORDER_REQUEST: |
||||
|
case StateKey.INCOMING_ORDER_REQUEST: |
||||
|
case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: |
||||
|
case StateKey.INCOMING_SETTLEMENT_PROPOSAL: |
||||
|
case StateKey.INCOMING_ROLL_OVER_PROPOSAL: |
||||
|
case StateKey.OUTGOING_ROLL_OVER_PROPOSAL: |
||||
|
case StateKey.CONTRACT_SETUP: |
||||
|
case StateKey.PENDING_OPEN: |
||||
|
case StateKey.REFUNDED: |
||||
|
case StateKey.SETUP_FAILED: |
||||
|
case StateKey.CLOSED: |
||||
|
return default_color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getGroup(): StateGroupKey { |
||||
|
switch (this.key) { |
||||
|
case StateKey.INCOMING_ORDER_REQUEST: |
||||
|
return StateGroupKey.PENDING_ORDER; |
||||
|
|
||||
|
case StateKey.OUTGOING_ORDER_REQUEST: |
||||
|
case StateKey.ACCEPTED: |
||||
|
case StateKey.CONTRACT_SETUP: |
||||
|
return StateGroupKey.OPENING; |
||||
|
|
||||
|
case StateKey.PENDING_OPEN: |
||||
|
case StateKey.OPEN: |
||||
|
case StateKey.PENDING_COMMIT: |
||||
|
case StateKey.OPEN_COMMITTED: |
||||
|
case StateKey.MUST_REFUND: |
||||
|
case StateKey.OUTGOING_SETTLEMENT_PROPOSAL: |
||||
|
case StateKey.OUTGOING_ROLL_OVER_PROPOSAL: |
||||
|
case StateKey.PENDING_CET: |
||||
|
case StateKey.PENDING_CLOSE: |
||||
|
return StateGroupKey.OPEN; |
||||
|
|
||||
|
case StateKey.INCOMING_SETTLEMENT_PROPOSAL: |
||||
|
return StateGroupKey.PENDING_SETTLEMENT; |
||||
|
|
||||
|
case StateKey.INCOMING_ROLL_OVER_PROPOSAL: |
||||
|
return StateGroupKey.PENDING_ROLL_OVER; |
||||
|
|
||||
|
case StateKey.REJECTED: |
||||
|
case StateKey.REFUNDED: |
||||
|
case StateKey.SETUP_FAILED: |
||||
|
case StateKey.CLOSED: |
||||
|
return StateGroupKey.CLOSED; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const enum StateKey { |
||||
|
OUTGOING_ORDER_REQUEST = "OutgoingOrderRequest", |
||||
|
INCOMING_ORDER_REQUEST = "IncomingOrderRequest", |
||||
|
ACCEPTED = "Accepted", |
||||
|
REJECTED = "Rejected", |
||||
|
CONTRACT_SETUP = "ContractSetup", |
||||
|
PENDING_OPEN = "PendingOpen", |
||||
|
OPEN = "Open", |
||||
|
PENDING_CLOSE = "PendingClose", |
||||
|
PENDING_COMMIT = "PendingCommit", |
||||
|
PENDING_CET = "PendingCet", |
||||
|
OPEN_COMMITTED = "OpenCommitted", |
||||
|
OUTGOING_SETTLEMENT_PROPOSAL = "OutgoingSettlementProposal", |
||||
|
INCOMING_SETTLEMENT_PROPOSAL = "IncomingSettlementProposal", |
||||
|
OUTGOING_ROLL_OVER_PROPOSAL = "OutgoingRollOverProposal", |
||||
|
INCOMING_ROLL_OVER_PROPOSAL = "IncomingRollOverProposal", |
||||
|
MUST_REFUND = "MustRefund", |
||||
|
REFUNDED = "Refunded", |
||||
|
SETUP_FAILED = "SetupFailed", |
||||
|
CLOSED = "Closed", |
||||
|
} |
||||
|
|
||||
|
export enum StateGroupKey { |
||||
|
/// A CFD which is still being set up (not on chain yet)
|
||||
|
OPENING = "Opening", |
||||
|
PENDING_ORDER = "Pending Order", |
||||
|
/// A CFD that is an ongoing open position (on chain)
|
||||
|
OPEN = "Open", |
||||
|
PENDING_SETTLEMENT = "Pending Settlement", |
||||
|
PENDING_ROLL_OVER = "Pending Roll Over", |
||||
|
/// A CFD that has been successfully or not-successfully terminated
|
||||
|
CLOSED = "Closed", |
||||
|
} |
||||
|
|
||||
|
export interface MarginRequestPayload { |
||||
|
price: number; |
||||
|
quantity: number; |
||||
|
leverage: number; |
||||
|
} |
||||
|
|
||||
|
export interface MarginResponse { |
||||
|
margin: number; |
||||
|
} |
||||
|
|
||||
|
export interface CfdOrderRequestPayload { |
||||
|
order_id: string; |
||||
|
quantity: number; |
||||
|
} |
||||
|
|
||||
|
export function intoOrder(key: string, value: any): any { |
||||
|
switch (key) { |
||||
|
case "position": |
||||
|
return new Position(value); |
||||
|
default: |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function intoCfd(key: string, value: any): any { |
||||
|
switch (key) { |
||||
|
case "position": |
||||
|
return new Position(value); |
||||
|
case "state": |
||||
|
return new State(value); |
||||
|
default: |
||||
|
return value; |
||||
|
} |
||||
|
} |
@ -0,0 +1,67 @@ |
|||||
|
import { CheckIcon, CopyIcon } from "@chakra-ui/icons"; |
||||
|
import { Box, Center, Divider, HStack, IconButton, Skeleton, Text, useClipboard } from "@chakra-ui/react"; |
||||
|
import * as React from "react"; |
||||
|
import Timestamp from "./Timestamp"; |
||||
|
import { WalletInfo } from "./Types"; |
||||
|
|
||||
|
interface WalletProps { |
||||
|
walletInfo: WalletInfo | null; |
||||
|
} |
||||
|
|
||||
|
export default function Wallet( |
||||
|
{ |
||||
|
walletInfo, |
||||
|
}: WalletProps, |
||||
|
) { |
||||
|
const { hasCopied, onCopy } = useClipboard(walletInfo ? walletInfo.address : ""); |
||||
|
const { balance, address, last_updated_at } = walletInfo || {}; |
||||
|
|
||||
|
return ( |
||||
|
<Center> |
||||
|
<Box shadow={"md"} marginBottom={5} padding={5} boxSize={"sm"}> |
||||
|
<Center><Text fontWeight={"bold"}>Your wallet</Text></Center> |
||||
|
<HStack> |
||||
|
<Text align={"left"}>Balance:</Text> |
||||
|
<Skeleton isLoaded={balance != null}> |
||||
|
<Text>{balance} BTC</Text> |
||||
|
</Skeleton> |
||||
|
</HStack> |
||||
|
<Divider marginTop={2} marginBottom={2} /> |
||||
|
<Skeleton isLoaded={address != null}> |
||||
|
<HStack> |
||||
|
<Text isTruncated>{address}</Text> |
||||
|
<IconButton |
||||
|
aria-label="Copy to clipboard" |
||||
|
icon={hasCopied ? <CheckIcon /> : <CopyIcon />} |
||||
|
onClick={onCopy} |
||||
|
/> |
||||
|
</HStack> |
||||
|
</Skeleton> |
||||
|
<Divider marginTop={2} marginBottom={2} /> |
||||
|
<HStack> |
||||
|
<Text align={"left"}>Updated:</Text> |
||||
|
<Skeleton isLoaded={last_updated_at != null}> |
||||
|
<Timestamp timestamp={last_updated_at!} /> |
||||
|
</Skeleton> |
||||
|
</HStack> |
||||
|
</Box> |
||||
|
</Center> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const WalletNavBar = ({ |
||||
|
walletInfo, |
||||
|
}: WalletProps) => { |
||||
|
const { balance } = walletInfo || {}; |
||||
|
|
||||
|
return ( |
||||
|
<HStack> |
||||
|
<Text align={"left"} as="b">On-chain balance:</Text> |
||||
|
<Skeleton isLoaded={balance != null}> |
||||
|
<Text>{balance} BTC</Text> |
||||
|
</Skeleton> |
||||
|
</HStack> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export { Wallet, WalletNavBar }; |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,13 @@ |
|||||
|
body { |
||||
|
margin: 0; |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
||||
|
sans-serif; |
||||
|
-webkit-font-smoothing: antialiased; |
||||
|
-moz-osx-font-smoothing: grayscale; |
||||
|
} |
||||
|
|
||||
|
code { |
||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
||||
|
monospace; |
||||
|
} |
After Width: | Height: | Size: 8.8 KiB |
@ -0,0 +1,21 @@ |
|||||
|
import { ChakraProvider } from "@chakra-ui/react"; |
||||
|
import React from "react"; |
||||
|
import ReactDOM from "react-dom"; |
||||
|
import { BrowserRouter } from "react-router-dom"; |
||||
|
import { EventSourceProvider } from "react-sse-hooks"; |
||||
|
import { App } from "./App"; |
||||
|
import "./index.css"; |
||||
|
import theme from "./theme"; |
||||
|
|
||||
|
ReactDOM.render( |
||||
|
<React.StrictMode> |
||||
|
<ChakraProvider theme={theme}> |
||||
|
<EventSourceProvider> |
||||
|
<BrowserRouter> |
||||
|
<App /> |
||||
|
</BrowserRouter> |
||||
|
</EventSourceProvider> |
||||
|
</ChakraProvider> |
||||
|
</React.StrictMode>, |
||||
|
document.getElementById("root"), |
||||
|
); |
@ -0,0 +1,10 @@ |
|||||
|
import { extendTheme, ThemeConfig } from "@chakra-ui/react"; |
||||
|
|
||||
|
let themeConfig: ThemeConfig = { |
||||
|
initialColorMode: "dark", |
||||
|
useSystemColorMode: false, |
||||
|
}; |
||||
|
|
||||
|
const theme = extendTheme({ themeConfig }); |
||||
|
|
||||
|
export default theme; |
@ -0,0 +1 @@ |
|||||
|
/// <reference types="vite/client" />
|
@ -0,0 +1,20 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"target": "ESNext", |
||||
|
"useDefineForClassFields": true, |
||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"], |
||||
|
"allowJs": false, |
||||
|
"esModuleInterop": false, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"strict": true, |
||||
|
"forceConsistentCasingInFileNames": true, |
||||
|
"module": "ESNext", |
||||
|
"moduleResolution": "Node", |
||||
|
"resolveJsonModule": true, |
||||
|
"isolatedModules": true, |
||||
|
"noEmit": true, |
||||
|
"jsx": "react", |
||||
|
"skipLibCheck": false |
||||
|
}, |
||||
|
"include": ["src"] |
||||
|
} |
@ -0,0 +1,19 @@ |
|||||
|
import react from "@vitejs/plugin-react"; |
||||
|
import { resolve } from "path"; |
||||
|
import { defineConfig } from "vite"; |
||||
|
|
||||
|
// https://vitejs.dev/config/
|
||||
|
export default defineConfig({ |
||||
|
plugins: [react()], |
||||
|
build: { |
||||
|
rollupOptions: { |
||||
|
input: resolve(__dirname, `index.html`), |
||||
|
}, |
||||
|
outDir: `dist/taker`, |
||||
|
}, |
||||
|
server: { |
||||
|
proxy: { |
||||
|
"/api": "http://localhost:8000", |
||||
|
}, |
||||
|
}, |
||||
|
}); |