diff --git a/package.json b/package.json index bf7e7c5..4492db1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "start": "craco start", "build": "craco build", "test": "craco test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "prettier": "prettier --write ." }, "eslintConfig": { "extends": "react-app" diff --git a/src/components/ConvertForm.tsx b/src/components/ConvertForm.tsx index 65dd800..10a7588 100644 --- a/src/components/ConvertForm.tsx +++ b/src/components/ConvertForm.tsx @@ -1,7 +1,7 @@ -import React, {useEffect, useState} from 'react'; -import {Button, Col, Input, Row, Select, Typography} from 'antd'; +import React, { useEffect, useState } from 'react'; +import { Button, Col, Input, Row, Select, Typography } from 'antd'; import styled from 'styled-components'; -import {Market, Orderbook} from '@project-serum/serum'; +import { Market, Orderbook } from '@project-serum/serum'; import { getExpectedFillPrice, getMarketDetails, @@ -12,16 +12,16 @@ import { useMarket, useTokenAccounts, } from '../utils/markets'; -import {notify} from '../utils/notifications'; -import {useWallet} from '../utils/wallet'; -import {useConnection, useSendConnection} from '../utils/connection'; -import {placeOrder} from '../utils/send'; -import {floorToDecimal, getDecimalCount} from '../utils/utils'; +import { notify } from '../utils/notifications'; +import { useWallet } from '../utils/wallet'; +import { useConnection, useSendConnection } from '../utils/connection'; +import { placeOrder } from '../utils/send'; +import { floorToDecimal, getDecimalCount } from '../utils/utils'; import FloatingElement from './layout/FloatingElement'; import WalletConnect from './WalletConnect'; -import {SwapOutlined} from "@ant-design/icons"; -import {CustomMarketInfo} from "../utils/types"; -import Wallet from "@project-serum/sol-wallet-adapter"; +import { SwapOutlined } from '@ant-design/icons'; +import { CustomMarketInfo } from '../utils/types'; +import Wallet from '@project-serum/sol-wallet-adapter'; const { Option } = Select; const { Title } = Typography; @@ -40,41 +40,55 @@ const ConvertButton = styled(Button)` export default function ConvertForm() { const { connected, wallet } = useWallet(); const { customMarkets } = useMarket(); - const marketInfos = getMarketInfos(customMarkets) - const {market, setMarketAddress} = useMarket(); + const marketInfos = getMarketInfos(customMarkets); + const { market, setMarketAddress } = useMarket(); const [fromToken, setFromToken] = useState(undefined); const [toToken, setToToken] = useState(undefined); const [size, setSize] = useState(undefined); - const marketInfosbyName = Object.fromEntries(marketInfos.map(market => [market.name, market])); + const marketInfosbyName = Object.fromEntries( + marketInfos.map((market) => [market.name, market]), + ); const tokenConvertMap: Map> = new Map(); Object.keys(marketInfosbyName).forEach((market) => { let [base, quote] = market.split('/'); !tokenConvertMap.has(base) ? tokenConvertMap.set(base, new Set([quote])) - : tokenConvertMap.set(base, new Set([...(tokenConvertMap.get(base) || []), quote])); + : tokenConvertMap.set( + base, + new Set([...(tokenConvertMap.get(base) || []), quote]), + ); !tokenConvertMap.has(quote) ? tokenConvertMap.set(quote, new Set([base])) - : tokenConvertMap.set(quote, new Set([...(tokenConvertMap.get(quote) || []), base])); + : tokenConvertMap.set( + quote, + new Set([...(tokenConvertMap.get(quote) || []), base]), + ); }); const setMarket = (toToken) => { - const marketInfo = marketInfos.filter(marketInfo => !marketInfo.deprecated).find(marketInfo => - marketInfo.name === `${fromToken}/${toToken}` || marketInfo.name === `${toToken}/${fromToken}` - ); + const marketInfo = marketInfos + .filter((marketInfo) => !marketInfo.deprecated) + .find( + (marketInfo) => + marketInfo.name === `${fromToken}/${toToken}` || + marketInfo.name === `${toToken}/${fromToken}`, + ); if (!marketInfo) { - console.warn(`Could not find market info for market names ${fromToken}/${toToken} or ${toToken}/${fromToken}`); + console.warn( + `Could not find market info for market names ${fromToken}/${toToken} or ${toToken}/${fromToken}`, + ); notify({ message: 'Invalid market', type: 'error', }); return; } - setMarketAddress(marketInfo.address.toBase58()) + setMarketAddress(marketInfo.address.toBase58()); setToToken(toToken); - } + }; return ( @@ -148,8 +162,8 @@ function ConvertFormSubmit({ toToken, wallet, market, - customMarkets -} : { + customMarkets, +}: { size: number | null | undefined; setSize: (newSize: number | undefined) => void; fromToken: string; @@ -171,7 +185,9 @@ function ConvertFormSubmit({ const isFromTokenBaseOfMarket = (market) => { const { marketName } = getMarketDetails(market, customMarkets); if (!marketName) { - throw Error('Cannot determine if coin is quote or base because marketName is missing'); + throw Error( + 'Cannot determine if coin is quote or base because marketName is missing', + ); } const [base] = marketName.split('/'); return fromToken === base; @@ -213,7 +229,9 @@ function ConvertFormSubmit({ const sidedOrderbookAccount = // @ts-ignore side === 'buy' ? market._decoded.asks : market._decoded.bids; - const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount); + const orderbookData = await connection.getAccountInfo( + sidedOrderbookAccount, + ); if (!orderbookData?.data) { notify({ message: 'Invalid orderbook data', type: 'error' }); return; @@ -231,8 +249,12 @@ function ConvertFormSubmit({ return; } - const tickSizeDecimals = getDecimalCount(market.tickSize); - const parsedPrice = getMarketOrderPrice(decodedOrderbookData, size, tickSizeDecimals); + const tickSizeDecimals = getDecimalCount(market.tickSize); + const parsedPrice = getMarketOrderPrice( + decodedOrderbookData, + size, + tickSizeDecimals, + ); // round size const sizeDecimalCount = getDecimalCount(market.minOrderSize); @@ -270,19 +292,25 @@ function ConvertFormSubmit({ const sidedOrderbookAccount = // @ts-ignore side === 'buy' ? market._decoded.asks : market._decoded.bids; - const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount); + const orderbookData = await connection.getAccountInfo( + sidedOrderbookAccount, + ); if (!orderbookData?.data || !market) { return [null, null]; } const decodedOrderbookData = Orderbook.decode(market, orderbookData.data); const [bbo] = - decodedOrderbookData && - decodedOrderbookData.getL2(1).map(([price]) => price); + decodedOrderbookData && + decodedOrderbookData.getL2(1).map(([price]) => price); if (!bbo || !size) { return [null, null]; } - const tickSizeDecimals = getDecimalCount(market.tickSize); - const expectedPrice = getExpectedFillPrice(decodedOrderbookData, size, tickSizeDecimals) + const tickSizeDecimals = getDecimalCount(market.tickSize); + const expectedPrice = getExpectedFillPrice( + decodedOrderbookData, + size, + tickSizeDecimals, + ); if (side === 'buy') { return [expectedPrice.toFixed(6), 1]; } else { @@ -292,21 +320,25 @@ function ConvertFormSubmit({ console.log(`Got error ${e}`); return [null, null]; } - } + }; - useEffect(() => { + useEffect( + () => { getPrice().then(([fromAmount, toAmount]) => { setFromAmount(fromAmount || undefined); setToAmount(toAmount || undefined); - }) + }); }, // eslint-disable-next-line - [market?.address.toBase58(), size] - ) + [market?.address.toBase58(), size], + ); const canConvert = market && size && size > 0; - const balance = balances.find(coinBalance => coinBalance.coin === fromToken); - const availableBalance = ((balance?.unsettled || 0.) + (balance?.wallet || 0.)) * 0.99; + const balance = balances.find( + (coinBalance) => coinBalance.coin === fromToken, + ); + const availableBalance = + ((balance?.unsettled || 0) + (balance?.wallet || 0)) * 0.99; return ( @@ -346,12 +378,12 @@ function ConvertFormSubmit({ {canConvert && ( - - + + {fromAmount} {fromToken} - + {toAmount} {toToken} diff --git a/src/components/CustomClusterEndpointDialog.tsx b/src/components/CustomClusterEndpointDialog.tsx index 54c3430..32552b0 100644 --- a/src/components/CustomClusterEndpointDialog.tsx +++ b/src/components/CustomClusterEndpointDialog.tsx @@ -1,20 +1,20 @@ -import React, {useState} from "react"; -import {Col, Input, Modal, Row} from "antd"; -import {EndpointInfo} from "../utils/types"; +import React, { useState } from 'react'; +import { Col, Input, Modal, Row } from 'antd'; +import { EndpointInfo } from '../utils/types'; export default function CustomClusterEndpointDialog({ visible, testingConnection, onAddCustomEndpoint, onClose, -} : { +}: { visible: boolean; testingConnection: boolean; onAddCustomEndpoint: (info: EndpointInfo) => void; onClose?: () => void; }) { - const [ customEndpoint, setCustomEndpoint] = useState(''); - const [ customEndpointName, setCustomEndpointName] = useState(''); + const [customEndpoint, setCustomEndpoint] = useState(''); + const [customEndpointName, setCustomEndpointName] = useState(''); const onSubmit = () => { const fullEndpoint = 'https://' + customEndpoint; @@ -22,13 +22,13 @@ export default function CustomClusterEndpointDialog({ name: customEndpointName, endpoint: fullEndpoint, custom: true, - } + }; onAddCustomEndpoint(params); onDoClose(); }; const onDoClose = () => { - setCustomEndpoint('') - setCustomEndpointName('') + setCustomEndpoint(''); + setCustomEndpointName(''); onClose && onClose(); }; const canSubmit = customEndpoint !== '' && customEndpointName !== ''; diff --git a/src/components/LinkAddress.tsx b/src/components/LinkAddress.tsx index a114233..2fafc12 100644 --- a/src/components/LinkAddress.tsx +++ b/src/components/LinkAddress.tsx @@ -2,7 +2,13 @@ import React from 'react'; import { Button } from 'antd'; import { LinkOutlined } from '@ant-design/icons'; -export default function LinkAddress({ title, address }: {title?: undefined | any; address: string;}) { +export default function LinkAddress({ + title, + address, +}: { + title?: undefined | any; + address: string; +}) { return (
{title &&

{title}

} diff --git a/src/components/StandaloneBalancesDisplay.tsx b/src/components/StandaloneBalancesDisplay.tsx index f2c2b6a..cd5e173 100644 --- a/src/components/StandaloneBalancesDisplay.tsx +++ b/src/components/StandaloneBalancesDisplay.tsx @@ -1,5 +1,5 @@ -import {Button, Col, Divider, Row} from 'antd'; -import React, {useState} from 'react'; +import { Button, Col, Divider, Row } from 'antd'; +import React, { useState } from 'react'; import FloatingElement from './layout/FloatingElement'; import styled from 'styled-components'; import { @@ -11,13 +11,13 @@ import { useTokenAccounts, } from '../utils/markets'; import DepositDialog from './DepositDialog'; -import {useWallet} from '../utils/wallet'; +import { useWallet } from '../utils/wallet'; import Link from './Link'; -import {settleFunds} from '../utils/send'; -import {useSendConnection} from '../utils/connection'; -import {notify} from '../utils/notifications'; -import {Balances} from "../utils/types"; -import StandaloneTokenAccountsSelect from "./StandaloneTokenAccountSelect"; +import { settleFunds } from '../utils/send'; +import { useSendConnection } from '../utils/connection'; +import { notify } from '../utils/notifications'; +import { Balances } from '../utils/types'; +import StandaloneTokenAccountsSelect from './StandaloneTokenAccountSelect'; const RowBox = styled(Row)` padding-bottom: 20px; @@ -101,69 +101,85 @@ export default function StandaloneBalancesDisplay() { } } - const formattedBalances: [string | undefined, Balances | undefined, string, string | undefined][] = [ - [baseCurrency, baseCurrencyBalances, 'base', market?.baseMintAddress.toBase58()], - [quoteCurrency, quoteCurrencyBalances, 'quote', market?.quoteMintAddress.toBase58()], - ] + const formattedBalances: [ + string | undefined, + Balances | undefined, + string, + string | undefined, + ][] = [ + [ + baseCurrency, + baseCurrencyBalances, + 'base', + market?.baseMintAddress.toBase58(), + ], + [ + quoteCurrency, + quoteCurrencyBalances, + 'quote', + market?.quoteMintAddress.toBase58(), + ], + ]; return ( - {formattedBalances.map(([currency, balances, baseOrQuote, mint], index) => ( - - {currency} - {connected && ( + {formattedBalances.map( + ([currency, balances, baseOrQuote, mint], index) => ( + + {currency} + {connected && ( + + account.effectiveMint.toBase58() === mint, + )} + mint={mint} + label + /> + + )} - account.effectiveMint.toBase58() === mint)} - mint={mint} - label - /> + Wallet balance: + {balances && balances.wallet} - )} - - Wallet balance: - {balances && balances.wallet} - - - Unsettled balance: - {balances && balances.unsettled} - - - - setBaseOrQuote(baseOrQuote)} - > - Deposit - - - - - Settle - - - - - All deposits go to your{' '} - - {providerName} - {' '} - wallet - - - ))} + + Unsettled balance: + {balances && balances.unsettled} + + + + setBaseOrQuote(baseOrQuote)} + > + Deposit + + + + + Settle + + + + + All deposits go to your{' '} + + {providerName} + {' '} + wallet + + + ), + )} setBaseOrQuote('')} diff --git a/src/components/StandaloneTokenAccountSelect.tsx b/src/components/StandaloneTokenAccountSelect.tsx index 4c0b0f3..253db43 100644 --- a/src/components/StandaloneTokenAccountSelect.tsx +++ b/src/components/StandaloneTokenAccountSelect.tsx @@ -1,21 +1,24 @@ import React from 'react'; -import {TokenAccount} from "../utils/types"; -import {useSelectedTokenAccounts} from "../utils/markets"; -import {Button, Col, Select, Typography} from "antd"; -import {CopyOutlined} from '@ant-design/icons'; -import {abbreviateAddress} from "../utils/utils"; -import {notify} from "../utils/notifications"; +import { TokenAccount } from '../utils/types'; +import { useSelectedTokenAccounts } from '../utils/markets'; +import { Button, Col, Select, Typography } from 'antd'; +import { CopyOutlined } from '@ant-design/icons'; +import { abbreviateAddress } from '../utils/utils'; +import { notify } from '../utils/notifications'; export default function StandaloneTokenAccountsSelect({ accounts, mint, label, }: { - accounts: TokenAccount[] | null | undefined, - mint: string | undefined, - label?: boolean + accounts: TokenAccount[] | null | undefined; + mint: string | undefined; + label?: boolean; }) { - const [selectedTokenAccounts, setSelectedTokenAccounts] = useSelectedTokenAccounts(); + const [ + selectedTokenAccounts, + setSelectedTokenAccounts, + ] = useSelectedTokenAccounts(); let selectedValue: string | undefined; if (mint && mint in selectedTokenAccounts) { @@ -32,32 +35,35 @@ export default function StandaloneTokenAccountsSelect({ message: 'Error selecting token account', description: 'Mint is undefined', type: 'error', - }) + }); return; } - const newSelectedTokenAccounts = {...selectedTokenAccounts}; + const newSelectedTokenAccounts = { ...selectedTokenAccounts }; newSelectedTokenAccounts[mint] = value; setSelectedTokenAccounts(newSelectedTokenAccounts); - } + }; return ( - {label && - - Token account: - - } + {label && Token account:} @@ -65,7 +71,9 @@ export default function StandaloneTokenAccountsSelect({ shape="round" icon={} size={'small'} - onClick={() => selectedValue && navigator.clipboard.writeText(selectedValue)} + onClick={() => + selectedValue && navigator.clipboard.writeText(selectedValue) + } /> diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 45603f0..8da047f 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,16 +1,20 @@ -import {InfoCircleOutlined, PlusCircleOutlined, SettingOutlined,} from '@ant-design/icons'; -import {Button, Col, Menu, Popover, Row, Select} from 'antd'; -import React, {useCallback, useEffect, useState} from 'react'; -import {useHistory, useLocation} from 'react-router-dom'; +import { + InfoCircleOutlined, + PlusCircleOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { Button, Col, Menu, Popover, Row, Select } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import logo from '../assets/logo.svg'; import styled from 'styled-components'; -import {useWallet, WALLET_PROVIDERS} from '../utils/wallet'; -import {ENDPOINTS, useConnectionConfig} from '../utils/connection'; +import { useWallet, WALLET_PROVIDERS } from '../utils/wallet'; +import { ENDPOINTS, useConnectionConfig } from '../utils/connection'; import Settings from './Settings'; -import CustomClusterEndpointDialog from "./CustomClusterEndpointDialog"; -import {EndpointInfo} from "../utils/types"; -import {notify} from "../utils/notifications"; -import {Connection} from "@solana/web3.js"; +import CustomClusterEndpointDialog from './CustomClusterEndpointDialog'; +import { EndpointInfo } from '../utils/types'; +import { notify } from '../utils/notifications'; +import { Connection } from '@solana/web3.js'; import WalletConnect from './WalletConnect'; const Wrapper = styled.div` @@ -41,13 +45,19 @@ const EXTERNAL_LINKS = { '/developer-resources': 'https://serum-academy.com/en/developer-resources/', '/explorer': 'https://explorer.solana.com', '/srm-faq': 'https://projectserum.com/srm-faq', -} +}; export default function TopBar() { const { connected, wallet, providerUrl, setProvider } = useWallet(); - const { endpoint, endpointInfo, setEndpoint, availableEndpoints, setCustomEndpoints } = useConnectionConfig(); - const [ addEndpointVisible, setAddEndpointVisible ] = useState(false) - const [ testingConnection, setTestingConnection] = useState(false) + const { + endpoint, + endpointInfo, + setEndpoint, + availableEndpoints, + setCustomEndpoints, + } = useConnectionConfig(); + const [addEndpointVisible, setAddEndpointVisible] = useState(false); + const [testingConnection, setTestingConnection] = useState(false); const location = useLocation(); const history = useHistory(); @@ -73,39 +83,45 @@ export default function TopBar() { } const handleError = (e) => { - console.log(`Connection to ${info.endpoint} failed: ${e}`) + console.log(`Connection to ${info.endpoint} failed: ${e}`); notify({ message: `Failed to connect to ${info.endpoint}`, type: 'error', }); - } + }; try { const connection = new Connection(info.endpoint, 'recent'); - connection.getEpochInfo().then(result => { - setTestingConnection(true); - console.log(`testing connection to ${info.endpoint}`); - const newCustomEndpoints = [...availableEndpoints.filter(e => e.custom), info]; - setEndpoint(info.endpoint); - setCustomEndpoints(newCustomEndpoints); - }).catch(handleError); + connection + .getEpochInfo() + .then((result) => { + setTestingConnection(true); + console.log(`testing connection to ${info.endpoint}`); + const newCustomEndpoints = [ + ...availableEndpoints.filter((e) => e.custom), + info, + ]; + setEndpoint(info.endpoint); + setCustomEndpoints(newCustomEndpoints); + }) + .catch(handleError); } catch (e) { handleError(e); } finally { setTestingConnection(false); } - } + }; - const endpointInfoCustom = endpointInfo && endpointInfo.custom + const endpointInfoCustom = endpointInfo && endpointInfo.custom; useEffect(() => { const handler = () => { if (endpointInfoCustom) { - setEndpoint(ENDPOINTS[0].endpoint) + setEndpoint(ENDPOINTS[0].endpoint); } - } - window.addEventListener("beforeunload", handler) - return () => window.removeEventListener("beforeunload", handler) - }, [endpointInfoCustom, setEndpoint]) + }; + window.addEventListener('beforeunload', handler); + return () => window.removeEventListener('beforeunload', handler); + }, [endpointInfoCustom, setEndpoint]); return ( <> @@ -137,34 +153,61 @@ export default function TopBar() { {connected && ORDERS} {connected && CONVERT} ADD MARKET - window.open(EXTERNAL_LINKS['/learn'], '_blank')}> + window.open(EXTERNAL_LINKS['/learn'], '_blank')} + > - + Adding a market - + Supported wallets - + DEX list - + Developer resources - + Solana block explorer - + SRM FAQ @@ -182,7 +225,7 @@ export default function TopBar() { onClick={() => setAddEndpointVisible(true)} /> - +
- +
diff --git a/src/components/TradeForm.tsx b/src/components/TradeForm.tsx index ec6aff1..7ff9f79 100644 --- a/src/components/TradeForm.tsx +++ b/src/components/TradeForm.tsx @@ -20,7 +20,7 @@ import { import { useSendConnection } from '../utils/connection'; import FloatingElement from './layout/FloatingElement'; import { placeOrder } from '../utils/send'; -import {SwitchChangeEventHandler} from "antd/es/switch"; +import { SwitchChangeEventHandler } from 'antd/es/switch'; const SellButton = styled(Button)` margin: 20px 0px 0px 0px; @@ -42,9 +42,14 @@ const sliderMarks = { 100: '100%', }; -export default function TradeForm({ style, setChangeOrderRef }: { +export default function TradeForm({ + style, + setChangeOrderRef, +}: { style?: any; - setChangeOrderRef?: (ref: ({ size, price }: {size?: number; price?: number;}) => void) => void; + setChangeOrderRef?: ( + ref: ({ size, price }: { size?: number; price?: number }) => void, + ) => void; }) { const [side, setSide] = useState<'buy' | 'sell'>('buy'); const { baseCurrency, quoteCurrency, market } = useMarket(); @@ -65,9 +70,10 @@ export default function TradeForm({ style, setChangeOrderRef }: { const [submitting, setSubmitting] = useState(false); const [sizeFraction, setSizeFraction] = useState(0); - const availableQuote = openOrdersAccount && market - ? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree) - : 0; + const availableQuote = + openOrdersAccount && market + ? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree) + : 0; let quoteBalance = (quoteCurrencyBalances || 0) + (availableQuote || 0); let baseBalance = baseCurrencyBalances || 0; @@ -123,7 +129,13 @@ export default function TradeForm({ style, setChangeOrderRef }: { setBaseSize(baseSize); }; - const doChangeOrder = ({ size, price }: {size?: number; price?: number;}) => { + const doChangeOrder = ({ + size, + price, + }: { + size?: number; + price?: number; + }) => { const formattedSize = size && roundToDecimal(size, sizeDecimalCount); const formattedPrice = price && roundToDecimal(price, priceDecimalCount); formattedSize && onSetBaseSize(formattedSize); @@ -131,9 +143,10 @@ export default function TradeForm({ style, setChangeOrderRef }: { }; const updateSizeFraction = () => { - const rawMaxSize = side === 'buy' ? quoteBalance / (price || markPrice || 1.) : baseBalance; + const rawMaxSize = + side === 'buy' ? quoteBalance / (price || markPrice || 1) : baseBalance; const maxSize = floorToDecimal(rawMaxSize, sizeDecimalCount); - const sizeFraction = Math.min(((baseSize || 0.) / maxSize) * 100, 100); + const sizeFraction = Math.min(((baseSize || 0) / maxSize) * 100, 100); setSizeFraction(sizeFraction); }; @@ -142,13 +155,17 @@ export default function TradeForm({ style, setChangeOrderRef }: { let formattedMarkPrice: number | string = priceDecimalCount ? markPrice.toFixed(priceDecimalCount) : markPrice; - setPrice(typeof formattedMarkPrice === 'number' ? formattedMarkPrice : parseFloat(formattedMarkPrice)); + setPrice( + typeof formattedMarkPrice === 'number' + ? formattedMarkPrice + : parseFloat(formattedMarkPrice), + ); } let newSize; if (side === 'buy') { if (price || markPrice) { - newSize = ((quoteBalance / (price || markPrice || 1.)) * value) / 100; + newSize = ((quoteBalance / (price || markPrice || 1)) * value) / 100; } } else { newSize = (baseBalance * value) / 100; diff --git a/src/components/TradesTable.tsx b/src/components/TradesTable.tsx index 14eabb8..0a79e51 100644 --- a/src/components/TradesTable.tsx +++ b/src/components/TradesTable.tsx @@ -32,10 +32,10 @@ export default function PublicTrades({ smallScreen }) { > Recent Market trades - - Price ({quoteCurrency}){' '} + Price ({quoteCurrency}) + + Size ({baseCurrency}) - Size ({baseCurrency}) Time diff --git a/src/components/UserInfoTable/OpenOrderTable.tsx b/src/components/UserInfoTable/OpenOrderTable.tsx index fa06427..3845d54 100644 --- a/src/components/UserInfoTable/OpenOrderTable.tsx +++ b/src/components/UserInfoTable/OpenOrderTable.tsx @@ -1,21 +1,27 @@ -import React, {useState} from 'react'; +import React, { useState } from 'react'; import DataTable from '../layout/DataTable'; import styled from 'styled-components'; -import {Button, Col, Row, Tag} from 'antd'; -import {cancelOrder} from '../../utils/send'; -import {useWallet} from '../../utils/wallet'; -import {useSendConnection} from '../../utils/connection'; -import {notify} from '../../utils/notifications'; -import {DeleteOutlined} from '@ant-design/icons'; -import {OrderWithMarketAndMarketName} from "../../utils/types"; +import { Button, Col, Row, Tag } from 'antd'; +import { cancelOrder } from '../../utils/send'; +import { useWallet } from '../../utils/wallet'; +import { useSendConnection } from '../../utils/connection'; +import { notify } from '../../utils/notifications'; +import { DeleteOutlined } from '@ant-design/icons'; +import { OrderWithMarketAndMarketName } from '../../utils/types'; const CancelButton = styled(Button)` color: #f23b69; border: 1px solid #f23b69; `; -export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize, loading, marketFilter } : { +export default function OpenOrderTable({ + openOrders, + onCancelSuccess, + pageSize, + loading, + marketFilter, +}: { openOrders: OrderWithMarketAndMarketName[] | null | undefined; onCancelSuccess?: () => void; pageSize?: number; @@ -50,15 +56,17 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize, } const marketFilters = [ - ...new Set((openOrders || []).map(orderInfos => orderInfos.marketName)) - ].map(marketName => {return {text: marketName, value: marketName}}); + ...new Set((openOrders || []).map((orderInfos) => orderInfos.marketName)), + ].map((marketName) => { + return { text: marketName, value: marketName }; + }); const columns = [ { title: 'Market', dataIndex: 'marketName', key: 'marketName', - filters: (marketFilter ? marketFilters : undefined), + filters: marketFilter ? marketFilters : undefined, onFilter: (value, record) => record.marketName.indexOf(value) === 0, }, { @@ -75,11 +83,11 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize, ), sorter: (a, b) => { if (a.side === b.side) { - return 0. + return 0; } else if (a.side === 'buy') { - return 1. + return 1; } else { - return -1. + return -1; } }, showSorterTooltip: false, diff --git a/src/components/UserInfoTable/WalletBalancesTable.tsx b/src/components/UserInfoTable/WalletBalancesTable.tsx index b3a34a0..3139cae 100644 --- a/src/components/UserInfoTable/WalletBalancesTable.tsx +++ b/src/components/UserInfoTable/WalletBalancesTable.tsx @@ -1,12 +1,17 @@ -import React, {useState} from 'react'; +import React, { useState } from 'react'; import DataTable from '../layout/DataTable'; -import {Button, Row} from "antd"; -import {settleAllFunds} from "../../utils/send"; -import {notify} from "../../utils/notifications"; -import {useConnection} from "../../utils/connection"; -import {useWallet} from "../../utils/wallet"; -import {useAllMarkets, useMarket, useSelectedTokenAccounts, useTokenAccounts} from "../../utils/markets"; -import StandaloneTokenAccountsSelect from "../StandaloneTokenAccountSelect"; +import { Button, Row } from 'antd'; +import { settleAllFunds } from '../../utils/send'; +import { notify } from '../../utils/notifications'; +import { useConnection } from '../../utils/connection'; +import { useWallet } from '../../utils/wallet'; +import { + useAllMarkets, + useMarket, + useSelectedTokenAccounts, + useTokenAccounts, +} from '../../utils/markets'; +import StandaloneTokenAccountsSelect from '../StandaloneTokenAccountSelect'; export default function WalletBalancesTable({ walletBalances, @@ -17,13 +22,13 @@ export default function WalletBalancesTable({ walletBalance: number; openOrdersFree: number; openOrdersTotal: number; - }[] + }[]; }) { const connection = useConnection(); const { wallet, connected } = useWallet(); const [selectedTokenAccounts] = useSelectedTokenAccounts(); const [tokenAccounts, tokenAccountsConnected] = useTokenAccounts(); - const {customMarkets} = useMarket(); + const { customMarkets } = useMarket(); const [allMarkets, allMarketsConnected] = useAllMarkets(customMarkets); const [settlingFunds, setSettlingFunds] = useState(false); @@ -51,8 +56,8 @@ export default function WalletBalancesTable({ tokenAccounts, selectedTokenAccounts, wallet, - markets: allMarkets.map(marketInfo => marketInfo.market), - }) + markets: allMarkets.map((marketInfo) => marketInfo.market), + }); } catch (e) { notify({ message: 'Error settling funds', @@ -94,9 +99,11 @@ export default function WalletBalancesTable({ key: 'selectTokenAccount', width: '20%', render: (walletBalance) => ( - + t.effectiveMint.toBase58() === walletBalance.mint)} + accounts={tokenAccounts?.filter( + (t) => t.effectiveMint.toBase58() === walletBalance.mint, + )} mint={walletBalance.mint} /> @@ -111,14 +118,11 @@ export default function WalletBalancesTable({ columns={columns} pagination={false} /> - {connected && - - } + )}
); } diff --git a/src/components/WalletConnect.tsx b/src/components/WalletConnect.tsx index 13108be..5c4e2dc 100644 --- a/src/components/WalletConnect.tsx +++ b/src/components/WalletConnect.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import {Button, Popover} from 'antd'; -import {InfoCircleOutlined, UserOutlined} from '@ant-design/icons'; +import { Button, Popover } from 'antd'; +import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons'; import { useWallet } from '../utils/wallet'; -import LinkAddress from "./LinkAddress"; +import LinkAddress from './LinkAddress'; export default function WalletConnect() { const { connected, wallet } = useWallet(); diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 86f21d9..66bc668 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1,14 +1,14 @@ declare module '@project-serum/sol-wallet-adapter' { - import EventEmitter from "eventemitter3"; - import {PublicKey, Transaction} from "@solana/web3.js"; + import EventEmitter from 'eventemitter3'; + import { PublicKey, Transaction } from '@solana/web3.js'; - export default class Wallet extends EventEmitter { - constructor(providerUrl: string, network: string) - publicKey: PublicKey; - connected: boolean; - autoApprove: boolean; - connect: () => Promise; - disconnect: () => void; - signTransaction: (transaction: Transaction) => Promise; - } + export default class Wallet extends EventEmitter { + constructor(providerUrl: string, network: string); + publicKey: PublicKey; + connected: boolean; + autoApprove: boolean; + connect: () => Promise; + disconnect: () => void; + signTransaction: (transaction: Transaction) => Promise; + } } diff --git a/src/pages/BalancesPage.tsx b/src/pages/BalancesPage.tsx index ac896b6..18877e2 100644 --- a/src/pages/BalancesPage.tsx +++ b/src/pages/BalancesPage.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import {Tabs} from 'antd'; -import {useAllOpenOrdersBalances, useWalletBalancesForAllMarkets,} from '../utils/markets'; +import { Tabs } from 'antd'; +import { + useAllOpenOrdersBalances, + useWalletBalancesForAllMarkets, +} from '../utils/markets'; import FloatingElement from '../components/layout/FloatingElement'; import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable'; -import {useMintToTickers} from "../utils/tokens"; +import { useMintToTickers } from '../utils/tokens'; const { TabPane } = Tabs; @@ -12,28 +15,26 @@ export default function BalancesPage() { const mintToTickers = useMintToTickers(); const openOrdersBalances = useAllOpenOrdersBalances(); - const data = (walletBalances || []).map(balance => { + const data = (walletBalances || []).map((balance) => { const balances = { coin: mintToTickers[balance.mint], mint: balance.mint, walletBalance: balance.balance, - openOrdersFree: 0., - openOrdersTotal: 0., - } - for (let openOrdersAccount of (openOrdersBalances[balance.mint] || [])) { + openOrdersFree: 0, + openOrdersTotal: 0, + }; + for (let openOrdersAccount of openOrdersBalances[balance.mint] || []) { balances['openOrdersFree'] += openOrdersAccount.free; balances['openOrdersTotal'] += openOrdersAccount.total; } - return balances + return balances; }); return ( - + diff --git a/src/pages/OpenOrdersPage.tsx b/src/pages/OpenOrdersPage.tsx index d4059a2..2078a07 100644 --- a/src/pages/OpenOrdersPage.tsx +++ b/src/pages/OpenOrdersPage.tsx @@ -1,36 +1,45 @@ import React from 'react'; import FloatingElement from '../components/layout/FloatingElement'; -import {getMarketInfos, useAllMarkets, useAllOpenOrders, useMarket} from "../utils/markets"; -import OpenOrderTable from "../components/UserInfoTable/OpenOrderTable"; -import {Button} from "antd"; -import {OrderWithMarketAndMarketName} from "../utils/types"; +import { + getMarketInfos, + useAllMarkets, + useAllOpenOrders, + useMarket, +} from '../utils/markets'; +import OpenOrderTable from '../components/UserInfoTable/OpenOrderTable'; +import { Button } from 'antd'; +import { OrderWithMarketAndMarketName } from '../utils/types'; export default function OpenOrdersPage() { - const {openOrders, loaded, refreshOpenOrders} = useAllOpenOrders(); - let {customMarkets} = useMarket(); + const { openOrders, loaded, refreshOpenOrders } = useAllOpenOrders(); + let { customMarkets } = useMarket(); let marketInfos = getMarketInfos(customMarkets); - let marketAddressesToNames = Object.fromEntries(marketInfos.map(info => [info.address.toBase58(), info.name])); + let marketAddressesToNames = Object.fromEntries( + marketInfos.map((info) => [info.address.toBase58(), info.name]), + ); let [allMarkets] = useAllMarkets(customMarkets); - const marketsByAddress = Object.fromEntries((allMarkets || []).map( - marketInfo => [marketInfo.market.address.toBase58(), marketInfo.market] - )); + const marketsByAddress = Object.fromEntries( + (allMarkets || []).map((marketInfo) => [ + marketInfo.market.address.toBase58(), + marketInfo.market, + ]), + ); - const dataSource: OrderWithMarketAndMarketName[] = (openOrders || []).map((orderInfos) => - orderInfos.orders.map(order => { - return { - marketName: marketAddressesToNames[orderInfos.marketAddress], - market: marketsByAddress[orderInfos.marketAddress], - ...order - }; - }) - ).flat(); + const dataSource: OrderWithMarketAndMarketName[] = (openOrders || []) + .map((orderInfos) => + orderInfos.orders.map((order) => { + return { + marketName: marketAddressesToNames[orderInfos.marketAddress], + market: marketsByAddress[orderInfos.marketAddress], + ...order, + }; + }), + ) + .flat(); return ( - void>(); + const changeOrderRef = useRef< + ({ size, price }: { size?: number; price?: number }) => void + >(); useEffect(() => { const handleResize = () => { diff --git a/src/utils/bonfidaConnector.tsx b/src/utils/bonfidaConnector.tsx index d5a9ff5..deb35fa 100644 --- a/src/utils/bonfidaConnector.tsx +++ b/src/utils/bonfidaConnector.tsx @@ -5,9 +5,7 @@ export default class BonfidaApi { static async get(path: string) { try { - const response = await fetch( - this.URL + path, - ); + const response = await fetch(this.URL + path); if (response.ok) { const responseJson = await response.json(); return responseJson.success ? responseJson.data : null; diff --git a/src/utils/connection.tsx b/src/utils/connection.tsx index c185cd3..d1db9e2 100644 --- a/src/utils/connection.tsx +++ b/src/utils/connection.tsx @@ -1,32 +1,33 @@ -import {useLocalStorageState} from './utils'; -import {Account, AccountInfo, Connection, PublicKey} from '@solana/web3.js'; -import React, {useContext, useEffect, useMemo} from 'react'; -import {setCache, useAsyncData} from './fetch-loop'; +import { useLocalStorageState } from './utils'; +import { Account, AccountInfo, Connection, PublicKey } from '@solana/web3.js'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { setCache, useAsyncData } from './fetch-loop'; import tuple from 'immutable-tuple'; -import {ConnectionContextValues, EndpointInfo} from "./types"; +import { ConnectionContextValues, EndpointInfo } from './types'; export const ENDPOINTS: EndpointInfo[] = [ { name: 'mainnet-beta', endpoint: 'https://solana-api.projectserum.com', - custom: false + custom: false, }, { name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false }, ]; const accountListenerCount = new Map(); -const ConnectionContext: React.Context = React.createContext(null); +const ConnectionContext: React.Context = React.createContext( + null, +); export function ConnectionProvider({ children }) { const [endpoint, setEndpoint] = useLocalStorageState( 'connectionEndpts', ENDPOINTS[0].endpoint, ); - const [customEndpoints, setCustomEndpoints] = useLocalStorageState( - 'customConnectionEndpoints', - [] - ) + const [customEndpoints, setCustomEndpoints] = useLocalStorageState< + EndpointInfo[] + >('customConnectionEndpoints', []); const availableEndpoints = ENDPOINTS.concat(customEndpoints); const connection = useMemo(() => new Connection(endpoint, 'recent'), [ @@ -41,12 +42,16 @@ export function ConnectionProvider({ children }) { // This is a hack to prevent the list from every getting empty useEffect(() => { const id = connection.onAccountChange(new Account().publicKey, () => {}); - return () => {connection.removeAccountChangeListener(id)}; + return () => { + connection.removeAccountChangeListener(id); + }; }, [connection]); useEffect(() => { const id = connection.onSlotChange(() => null); - return () => {connection.removeSlotChangeListener(id)}; + return () => { + connection.removeSlotChangeListener(id); + }; }, [connection]); useEffect(() => { @@ -54,17 +59,28 @@ export function ConnectionProvider({ children }) { new Account().publicKey, () => {}, ); - return () => {sendConnection.removeAccountChangeListener(id)}; + return () => { + sendConnection.removeAccountChangeListener(id); + }; }, [sendConnection]); useEffect(() => { const id = sendConnection.onSlotChange(() => null); - return () => {sendConnection.removeSlotChangeListener(id)}; + return () => { + sendConnection.removeSlotChangeListener(id); + }; }, [sendConnection]); return ( {children} @@ -74,7 +90,7 @@ export function ConnectionProvider({ children }) { export function useConnection() { const context = useContext(ConnectionContext); if (!context) { - throw new Error('Missing connection context') + throw new Error('Missing connection context'); } return context.connection; } @@ -82,7 +98,7 @@ export function useConnection() { export function useSendConnection() { const context = useContext(ConnectionContext); if (!context) { - throw new Error('Missing connection context') + throw new Error('Missing connection context'); } return context.sendConnection; } @@ -90,18 +106,22 @@ export function useSendConnection() { export function useConnectionConfig() { const context = useContext(ConnectionContext); if (!context) { - throw new Error('Missing connection context') + throw new Error('Missing connection context'); } return { endpoint: context.endpoint, - endpointInfo: context.availableEndpoints.find(info => info.endpoint === context.endpoint), + endpointInfo: context.availableEndpoints.find( + (info) => info.endpoint === context.endpoint, + ), setEndpoint: context.setEndpoint, availableEndpoints: context.availableEndpoints, setCustomEndpoints: context.setCustomEndpoints, }; } -export function useAccountInfo(publicKey: PublicKey | undefined | null): [AccountInfo | null | undefined, boolean] { +export function useAccountInfo( + publicKey: PublicKey | undefined | null, +): [AccountInfo | null | undefined, boolean] { const connection = useConnection(); const cacheKey = tuple(connection, publicKey?.toBase58()); const [accountInfo, loaded] = useAsyncData | null>( diff --git a/src/utils/fetch-loop.tsx b/src/utils/fetch-loop.tsx index 7b4623d..de8d5f4 100644 --- a/src/utils/fetch-loop.tsx +++ b/src/utils/fetch-loop.tsx @@ -103,7 +103,7 @@ class FetchLoopInternal { try { const data = await this.fn(); if (!this.cacheNullValues && data === null) { - console.log(`Not caching null value for ${this.cacheKey}`) + console.log(`Not caching null value for ${this.cacheKey}`); // cached data has not changed so no need to re-render this.errors = 0; return data; diff --git a/src/utils/markets.tsx b/src/utils/markets.tsx index 6e02e5d..239cb39 100644 --- a/src/utils/markets.tsx +++ b/src/utils/markets.tsx @@ -1187,8 +1187,8 @@ export function getExpectedFillPrice( cost: number, tickSizeDecimals?: number, ) { - let spentCost = 0.; - let avgPrice = 0.; + let spentCost = 0; + let avgPrice = 0; let price, sizeAtLevel, costAtLevel: number; for ([price, sizeAtLevel] of orderbook.getL2(1000)) { costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel; diff --git a/src/utils/preferences.tsx b/src/utils/preferences.tsx index 5e20993..1150176 100644 --- a/src/utils/preferences.tsx +++ b/src/utils/preferences.tsx @@ -3,11 +3,18 @@ import { useLocalStorageState } from './utils'; import { useInterval } from './useInterval'; import { useConnection } from './connection'; import { useWallet } from './wallet'; -import {useAllMarkets, useTokenAccounts, useMarket, useSelectedTokenAccounts} from './markets'; +import { + useAllMarkets, + useTokenAccounts, + useMarket, + useSelectedTokenAccounts, +} from './markets'; import { settleAllFunds } from './send'; -import {PreferencesContextValues} from "./types"; +import { PreferencesContextValues } from './types'; -const PreferencesContext = React.createContext(null); +const PreferencesContext = React.createContext( + null, +); export function PreferencesProvider({ children }) { const [autoSettleEnabled, setAutoSettleEnabled] = useLocalStorageState( @@ -27,7 +34,13 @@ export function PreferencesProvider({ children }) { const markets = (marketList || []).map((m) => m.market); try { console.log('Auto settling'); - await settleAllFunds({ connection, wallet, tokenAccounts: (tokenAccounts || []), markets, selectedTokenAccounts }); + await settleAllFunds({ + connection, + wallet, + tokenAccounts: tokenAccounts || [], + markets, + selectedTokenAccounts, + }); } catch (e) { console.log('Error auto settling funds: ' + e.message); } @@ -51,7 +64,7 @@ export function PreferencesProvider({ children }) { export function usePreferences() { const context = useContext(PreferencesContext); if (!context) { - throw new Error('Missing preferences context') + throw new Error('Missing preferences context'); } return { autoSettleEnabled: context.autoSettleEnabled, diff --git a/src/utils/send.tsx b/src/utils/send.tsx index 62595ff..8f451f1 100644 --- a/src/utils/send.tsx +++ b/src/utils/send.tsx @@ -2,10 +2,14 @@ import { notify } from './notifications'; import { getDecimalCount, sleep } from './utils'; import { getSelectedTokenAccountForMint } from './markets'; import { - Account, AccountInfo, Connection, - PublicKey, RpcResponseAndContext, + Account, + AccountInfo, + Connection, + PublicKey, + RpcResponseAndContext, SystemProgram, - Transaction, TransactionSignature, + Transaction, + TransactionSignature, } from '@solana/web3.js'; import { BN } from 'bn.js'; import { @@ -15,18 +19,18 @@ import { TokenInstructions, OpenOrders, } from '@project-serum/serum'; -import Wallet from "@project-serum/sol-wallet-adapter"; -import {SelectedTokenAccounts, TokenAccount} from "./types"; -import {Order} from "@project-serum/serum/lib/market"; -import {Buffer} from "buffer"; -import assert from "assert"; -import { struct } from "superstruct"; +import Wallet from '@project-serum/sol-wallet-adapter'; +import { SelectedTokenAccounts, TokenAccount } from './types'; +import { Order } from '@project-serum/serum/lib/market'; +import { Buffer } from 'buffer'; +import assert from 'assert'; +import { struct } from 'superstruct'; export async function createTokenAccountTransaction({ connection, wallet, mintPublicKey, -} : { +}: { connection: Connection; wallet: Wallet; mintPublicKey: PublicKey; @@ -64,7 +68,7 @@ export async function settleFunds({ wallet, baseCurrencyAccount, quoteCurrencyAccount, -} : { +}: { market: Market; openOrders: OpenOrders; connection: Connection; @@ -110,8 +114,8 @@ export async function settleFunds({ } let referrerQuoteWallet: PublicKey | null = null; if (market.supportsReferralFees) { - const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT') - const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC') + const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT'); + const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC'); if ( process.env.REACT_APP_USDT_REFERRAL_FEES_ADDRESS && usdt && @@ -164,7 +168,7 @@ export async function settleAllFunds({ tokenAccounts, markets, selectedTokenAccounts, -} : { +}: { connection: Connection; wallet: Wallet; tokenAccounts: TokenAccount[]; @@ -209,57 +213,72 @@ export async function settleAllFunds({ [], ); - const settleTransactions = (await Promise.all( - openOrdersAccounts.map((openOrdersAccount) => { - const market = markets.find((m) => - // @ts-ignore - m._decoded?.ownAddress?.equals(openOrdersAccount.market), - ); - const baseMint = market?.baseMintAddress; - const quoteMint = market?.quoteMintAddress; + const settleTransactions = ( + await Promise.all( + openOrdersAccounts.map((openOrdersAccount) => { + const market = markets.find((m) => + // @ts-ignore + m._decoded?.ownAddress?.equals(openOrdersAccount.market), + ); + const baseMint = market?.baseMintAddress; + const quoteMint = market?.quoteMintAddress; - const selectedBaseTokenAccount = getSelectedTokenAccountForMint( - tokenAccounts, - baseMint, - baseMint && selectedTokenAccounts && selectedTokenAccounts[baseMint.toBase58()] - )?.pubkey; - const selectedQuoteTokenAccount = getSelectedTokenAccountForMint( - tokenAccounts, - quoteMint, - quoteMint && selectedTokenAccounts && selectedTokenAccounts[quoteMint.toBase58()] - )?.pubkey; - if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) { - return null; - } - return ( - market && - market.makeSettleFundsTransaction( - connection, - openOrdersAccount, - selectedBaseTokenAccount, - selectedQuoteTokenAccount, - ) - ); - }), - )).filter((x): x is {signers: [PublicKey | Account]; transaction: Transaction} => !!x); + const selectedBaseTokenAccount = getSelectedTokenAccountForMint( + tokenAccounts, + baseMint, + baseMint && + selectedTokenAccounts && + selectedTokenAccounts[baseMint.toBase58()], + )?.pubkey; + const selectedQuoteTokenAccount = getSelectedTokenAccountForMint( + tokenAccounts, + quoteMint, + quoteMint && + selectedTokenAccounts && + selectedTokenAccounts[quoteMint.toBase58()], + )?.pubkey; + if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) { + return null; + } + return ( + market && + market.makeSettleFundsTransaction( + connection, + openOrdersAccount, + selectedBaseTokenAccount, + selectedQuoteTokenAccount, + ) + ); + }), + ) + ).filter( + (x): x is { signers: [PublicKey | Account]; transaction: Transaction } => + !!x, + ); if (!settleTransactions || settleTransactions.length === 0) return; const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction); const signers: Array = []; settleTransactions - .reduce((cumulative: Array, t) => cumulative.concat(t.signers), []) + .reduce( + (cumulative: Array, t) => + cumulative.concat(t.signers), + [], + ) .forEach((signer) => { - if (!signers.find((s) => { - if (s.constructor.name !== signer.constructor.name) { - return false; - } else if (s.constructor.name === 'PublicKey') { - // @ts-ignore - return s.equals(signer); - } else { - // @ts-ignore - return s.publicKey.equals(signer.publicKey); - } - })) { + if ( + !signers.find((s) => { + if (s.constructor.name !== signer.constructor.name) { + return false; + } else if (s.constructor.name === 'PublicKey') { + // @ts-ignore + return s.equals(signer); + } else { + // @ts-ignore + return s.publicKey.equals(signer.publicKey); + } + }) + ) { signers.push(signer); } }); @@ -274,11 +293,21 @@ export async function settleAllFunds({ }); } -export async function cancelOrder(params: {market: Market; connection: Connection; wallet: Wallet; order: Order;}) { +export async function cancelOrder(params: { + market: Market; + connection: Connection; + wallet: Wallet; + order: Order; +}) { return cancelOrders({ ...params, orders: [params.order] }); } -export async function cancelOrders({ market, wallet, connection, orders }: { +export async function cancelOrders({ + market, + wallet, + connection, + orders, +}: { market: Market; wallet: Wallet; connection: Connection; @@ -310,10 +339,10 @@ export async function placeOrder({ baseCurrencyAccount, quoteCurrencyAccount, }: { - side: "buy" | "sell"; + side: 'buy' | 'sell'; price: number; size: number; - orderType: "ioc" | "postOnly" | "limit"; + orderType: 'ioc' | 'postOnly' | 'limit'; market: Market | undefined | null; connection: Connection; wallet: Wallet; @@ -417,7 +446,7 @@ export async function listMarket({ baseLotSize, quoteLotSize, dexProgramId, -} : { +}: { connection: Connection; wallet: Wallet; baseMint: PublicKey; @@ -577,7 +606,7 @@ async function sendTransaction({ sentMessage = 'Transaction sent', successMessage = 'Transaction confirmed', timeout = DEFAULT_TIMEOUT, -} : { +}: { transaction: Transaction; wallet: Wallet; signers?: Array; @@ -608,7 +637,7 @@ async function signTransaction({ wallet, signers = [wallet.publicKey], connection, -} : { +}: { transaction: Transaction; wallet: Wallet; signers: Array; @@ -628,7 +657,7 @@ async function sendSignedTransaction({ sentMessage = 'Transaction sent', successMessage = 'Transaction confirmed', timeout = DEFAULT_TIMEOUT, -} : { +}: { signedTransaction: Transaction; connection: Connection; sendingMessage?: string; @@ -639,9 +668,12 @@ async function sendSignedTransaction({ const rawTransaction = signedTransaction.serialize(); const startTime = getUnixTs(); notify({ message: sendingMessage }); - const txid: TransactionSignature = await connection.sendRawTransaction(rawTransaction, { - skipPreflight: true, - }); + const txid: TransactionSignature = await connection.sendRawTransaction( + rawTransaction, + { + skipPreflight: true, + }, + ); notify({ message: sentMessage, type: 'success', txid }); console.log('Started awaiting confirmation for', txid); @@ -754,17 +786,17 @@ function mergeTransactions(transactions: (Transaction | undefined)[]) { } function jsonRpcResult(resultDescription: any) { - const jsonRpcVersion = struct.literal("2.0"); + const jsonRpcVersion = struct.literal('2.0'); return struct.union([ struct({ jsonrpc: jsonRpcVersion, - id: "string", - error: "any", + id: 'string', + error: 'any', }), struct({ jsonrpc: jsonRpcVersion, - id: "string", - error: "null?", + id: 'string', + error: 'null?', result: resultDescription, }), ]); @@ -773,46 +805,43 @@ function jsonRpcResult(resultDescription: any) { function jsonRpcResultAndContext(resultDescription: any) { return jsonRpcResult({ context: struct({ - slot: "number", + slot: 'number', }), value: resultDescription, }); } const AccountInfoResult = struct({ - executable: "boolean", - owner: "string", - lamports: "number", - data: "any", - rentEpoch: "number?", + executable: 'boolean', + owner: 'string', + lamports: 'number', + data: 'any', + rentEpoch: 'number?', }); export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext( - struct.array([struct.union(["null", AccountInfoResult])]) + struct.array([struct.union(['null', AccountInfoResult])]), ); export async function getMultipleSolanaAccounts( connection: Connection, - publicKeys: PublicKey[] + publicKeys: PublicKey[], ): Promise< RpcResponseAndContext<{ [key: string]: AccountInfo | null }> > { - const args = [ - publicKeys.map((k) => k.toBase58()), - { commitment: "recent" }, - ]; + const args = [publicKeys.map((k) => k.toBase58()), { commitment: 'recent' }]; // @ts-ignore - const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args); + const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args); const res = GetMultipleAccountsAndContextRpcResult(unsafeRes); if (res.error) { throw new Error( - "failed to get info about accounts " + - publicKeys.map((k) => k.toBase58()).join(", ") + - ": " + - res.error.message + 'failed to get info about accounts ' + + publicKeys.map((k) => k.toBase58()).join(', ') + + ': ' + + res.error.message, ); } - assert(typeof res.result !== "undefined"); + assert(typeof res.result !== 'undefined'); const accounts: Array<{ executable: any; owner: PublicKey; @@ -828,12 +857,12 @@ export async function getMultipleSolanaAccounts( } | null = null; if (res.result.value) { const { executable, owner, lamports, data } = account; - assert(data[1] === "base64"); + assert(data[1] === 'base64'); value = { executable, owner: new PublicKey(owner), lamports, - data: Buffer.from(data[0], "base64"), + data: Buffer.from(data[0], 'base64'), }; } accounts.push(value); @@ -843,7 +872,7 @@ export async function getMultipleSolanaAccounts( slot: res.result.context.slot, }, value: Object.fromEntries( - accounts.map((account, i) => [publicKeys[i].toBase58(), account]) + accounts.map((account, i) => [publicKeys[i].toBase58(), account]), ), }; } diff --git a/src/utils/tokens.tsx b/src/utils/tokens.tsx index 6ff7f9a..f0088d9 100644 --- a/src/utils/tokens.tsx +++ b/src/utils/tokens.tsx @@ -1,13 +1,13 @@ import * as BufferLayout from 'buffer-layout'; import bs58 from 'bs58'; -import {AccountInfo, Connection, PublicKey} from '@solana/web3.js'; -import {WRAPPED_SOL_MINT} from '@project-serum/serum/lib/token-instructions'; -import {TokenAccount} from "./types"; -import {TOKEN_MINTS} from "@project-serum/serum"; -import {useAllMarkets, useMarket, useTokenAccounts} from "./markets"; -import {getMultipleSolanaAccounts} from "./send"; -import {useConnection} from "./connection"; -import {useAsyncData} from "./fetch-loop"; +import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'; +import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; +import { TokenAccount } from './types'; +import { TOKEN_MINTS } from '@project-serum/serum'; +import { useAllMarkets, useMarket, useTokenAccounts } from './markets'; +import { getMultipleSolanaAccounts } from './send'; +import { useConnection } from './connection'; +import { useAsyncData } from './fetch-loop'; import tuple from 'immutable-tuple'; export const ACCOUNT_LAYOUT = BufferLayout.struct([ @@ -25,7 +25,7 @@ export const MINT_LAYOUT = BufferLayout.struct([ ]); export function parseTokenAccountData( - data: Buffer + data: Buffer, ): { mint: PublicKey; owner: PublicKey; amount: number } { let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); return { @@ -59,8 +59,9 @@ export const TOKEN_PROGRAM_ID = new PublicKey( ); export async function getOwnedTokenAccounts( - connection: Connection, publicKey: PublicKey -): Promise}>> { + connection: Connection, + publicKey: PublicKey, +): Promise }>> { let filters = getOwnedAccountsFilters(publicKey); // @ts-ignore let resp = await connection._rpcRequest('getProgramAccounts', [ @@ -107,18 +108,23 @@ export async function getOwnedTokenAccounts( }); } -export async function getTokenAccountInfo(connection: Connection, ownerAddress: PublicKey) { +export async function getTokenAccountInfo( + connection: Connection, + ownerAddress: PublicKey, +) { let [splAccounts, account] = await Promise.all([ getOwnedTokenAccounts(connection, ownerAddress), connection.getAccountInfo(ownerAddress), ]); - const parsedSplAccounts: TokenAccount[] = splAccounts.map(({ publicKey, accountInfo }) => { - return { - pubkey: publicKey, - account: accountInfo, - effectiveMint: parseTokenAccountData(accountInfo.data).mint, - }; - }); + const parsedSplAccounts: TokenAccount[] = splAccounts.map( + ({ publicKey, accountInfo }) => { + return { + pubkey: publicKey, + account: accountInfo, + effectiveMint: parseTokenAccountData(accountInfo.data).mint, + }; + }, + ); return parsedSplAccounts.concat({ pubkey: ownerAddress, account, @@ -126,22 +132,27 @@ export async function getTokenAccountInfo(connection: Connection, ownerAddress: }); } -export function useMintToTickers(): { [mint: string]: string; } { +export function useMintToTickers(): { [mint: string]: string } { const { customMarkets } = useMarket(); const [markets] = useAllMarkets(customMarkets); - const mintsToTickers = Object.fromEntries(TOKEN_MINTS.map(mint => [mint.address.toBase58(), mint.name])); - for (let market of (markets || [])) { + const mintsToTickers = Object.fromEntries( + TOKEN_MINTS.map((mint) => [mint.address.toBase58(), mint.name]), + ); + for (let market of markets || []) { const customMarketInfo = customMarkets.find( - customMarket => customMarket.address === market.market.address.toBase58() + (customMarket) => + customMarket.address === market.market.address.toBase58(), ); if (!(market.market.baseMintAddress.toBase58() in mintsToTickers)) { if (customMarketInfo) { - mintsToTickers[market.market.baseMintAddress.toBase58()] = customMarketInfo.baseLabel || `${customMarketInfo.name}_BASE`; + mintsToTickers[market.market.baseMintAddress.toBase58()] = + customMarketInfo.baseLabel || `${customMarketInfo.name}_BASE`; } } if (!(market.market.quoteMintAddress.toBase58() in mintsToTickers)) { if (customMarketInfo) { - mintsToTickers[market.market.quoteMintAddress.toBase58()] = customMarketInfo.quoteLabel || `${customMarketInfo.name}_QUOTE`; + mintsToTickers[market.market.quoteMintAddress.toBase58()] = + customMarketInfo.quoteLabel || `${customMarketInfo.name}_QUOTE`; } } } @@ -151,30 +162,46 @@ export function useMintToTickers(): { [mint: string]: string; } { const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000; export function useMintInfos(): [ - {[mintAddress: string]: {decimals: number; initialized: boolean} | null} | null | undefined, - boolean + ( + | { + [mintAddress: string]: { + decimals: number; + initialized: boolean; + } | null; + } + | null + | undefined + ), + boolean, ] { const connection = useConnection(); - const {customMarkets} = useMarket(); + const { customMarkets } = useMarket(); const [tokenAccounts] = useTokenAccounts(); const [allMarkets] = useAllMarkets(customMarkets); - const allMints = (tokenAccounts || []).map(account => account.effectiveMint).concat( - (allMarkets || []).map(marketInfo => marketInfo.market.baseMintAddress) - ).concat( - (allMarkets || []).map(marketInfo => marketInfo.market.quoteMintAddress) + const allMints = (tokenAccounts || []) + .map((account) => account.effectiveMint) + .concat( + (allMarkets || []).map((marketInfo) => marketInfo.market.baseMintAddress), + ) + .concat( + (allMarkets || []).map( + (marketInfo) => marketInfo.market.quoteMintAddress, + ), + ); + const uniqueMints = [...new Set(allMints.map((mint) => mint.toBase58()))].map( + (stringMint) => new PublicKey(stringMint), ); - const uniqueMints = [...new Set(allMints.map(mint => mint.toBase58()))].map(stringMint => new PublicKey(stringMint)) const getAllMintInfo = async () => { const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints); - return Object.fromEntries(Object.entries(mintInfos.value).map( - ([key, accountInfo]) => [ + return Object.fromEntries( + Object.entries(mintInfos.value).map(([key, accountInfo]) => [ key, - accountInfo && parseTokenMintData(accountInfo.data) - ] - )); - } + accountInfo && parseTokenMintData(accountInfo.data), + ]), + ); + }; return useAsyncData( getAllMintInfo, diff --git a/src/utils/utils.tsx b/src/utils/utils.tsx index 7b4df82..439dd6c 100644 --- a/src/utils/utils.tsx +++ b/src/utils/utils.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { PublicKey } from '@solana/web3.js'; -import BN from "bn.js"; +import BN from 'bn.js'; export function isValidPublicKey(key) { if (!key) { @@ -24,11 +24,17 @@ export const percentFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, }); -export function floorToDecimal(value: number, decimals: number | undefined | null) { +export function floorToDecimal( + value: number, + decimals: number | undefined | null, +) { return decimals ? Math.floor(value * 10 ** decimals) / 10 ** decimals : value; } -export function roundToDecimal(value: number, decimals: number | undefined | null) { +export function roundToDecimal( + value: number, + decimals: number | undefined | null, +) { return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value; } @@ -66,7 +72,7 @@ export function useLocalStorageStringState( localStorageListeners[key].push(notify); return () => { localStorageListeners[key] = localStorageListeners[key].filter( - listener => listener !== notify, + (listener) => listener !== notify, ); if (localStorageListeners[key].length === 0) { delete localStorageListeners[key]; @@ -75,7 +81,7 @@ export function useLocalStorageStringState( }, [key]); const setState = useCallback<(newState: string | null) => void>( - newState => { + (newState) => { const changed = state !== newState; if (!changed) { return; @@ -86,7 +92,9 @@ export function useLocalStorageStringState( } else { localStorage.setItem(key, newState); } - localStorageListeners[key].forEach(listener => listener(key + '\n' + newState)); + localStorageListeners[key].forEach((listener) => + listener(key + '\n' + newState), + ); }, [state, key], ); @@ -94,9 +102,18 @@ export function useLocalStorageStringState( return [state, setState]; } -export function useLocalStorageState(key: string, defaultState: T | null = null): [T, (newState: T) => void] { - let [stringState, setStringState] = useLocalStorageStringState(key, JSON.stringify(defaultState)); - return [stringState && JSON.parse(stringState), newState => setStringState(JSON.stringify(newState))]; +export function useLocalStorageState( + key: string, + defaultState: T | null = null, +): [T, (newState: T) => void] { + let [stringState, setStringState] = useLocalStorageStringState( + key, + JSON.stringify(defaultState), + ); + return [ + stringState && JSON.parse(stringState), + (newState) => setStringState(JSON.stringify(newState)), + ]; } export function useEffectAfterTimeout(effect, timeout) { diff --git a/src/utils/wallet.tsx b/src/utils/wallet.tsx index 0c492e2..40de681 100644 --- a/src/utils/wallet.tsx +++ b/src/utils/wallet.tsx @@ -3,7 +3,7 @@ import Wallet from '@project-serum/sol-wallet-adapter'; import { notify } from './notifications'; import { useConnectionConfig } from './connection'; import { useLocalStorageState } from './utils'; -import {WalletContextValues} from "./types"; +import { WalletContextValues } from './types'; export const WALLET_PROVIDERS = [ { name: 'sollet.io', url: 'https://www.sollet.io' }, @@ -83,7 +83,7 @@ export function WalletProvider({ children }) { export function useWallet() { const context = useContext(WalletContext); if (!context) { - throw new Error('Missing wallet context') + throw new Error('Missing wallet context'); } return { connected: context.connected, diff --git a/tsconfig.json b/tsconfig.json index dfced42..17d35ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,17 +16,8 @@ "isolatedModules": true, "noEmit": true, "jsx": "react", - "lib": [ - "dom", - "esnext" - ] + "lib": ["dom", "esnext"] }, - "include": [ - "./src/" - ], - "exclude": [ - "./src/**/*.test.js", - "node_modules", - "**/node_modules" - ] + "include": ["./src/"], + "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] }