Run prettier formatter (#38)

* Run prettier

* Add prettier script
This commit is contained in:
Nathaniel Parke 2020-10-22 12:42:24 +08:00 committed by GitHub
parent 99fbbed7d5
commit 063cae46df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 711 additions and 469 deletions

View File

@ -38,7 +38,8 @@
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"test": "craco test", "test": "craco test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"prettier": "prettier --write ."
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

View File

@ -1,7 +1,7 @@
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import {Button, Col, Input, Row, Select, Typography} from 'antd'; import { Button, Col, Input, Row, Select, Typography } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
import {Market, Orderbook} from '@project-serum/serum'; import { Market, Orderbook } from '@project-serum/serum';
import { import {
getExpectedFillPrice, getExpectedFillPrice,
getMarketDetails, getMarketDetails,
@ -12,16 +12,16 @@ import {
useMarket, useMarket,
useTokenAccounts, useTokenAccounts,
} from '../utils/markets'; } from '../utils/markets';
import {notify} from '../utils/notifications'; import { notify } from '../utils/notifications';
import {useWallet} from '../utils/wallet'; import { useWallet } from '../utils/wallet';
import {useConnection, useSendConnection} from '../utils/connection'; import { useConnection, useSendConnection } from '../utils/connection';
import {placeOrder} from '../utils/send'; import { placeOrder } from '../utils/send';
import {floorToDecimal, getDecimalCount} from '../utils/utils'; import { floorToDecimal, getDecimalCount } from '../utils/utils';
import FloatingElement from './layout/FloatingElement'; import FloatingElement from './layout/FloatingElement';
import WalletConnect from './WalletConnect'; import WalletConnect from './WalletConnect';
import {SwapOutlined} from "@ant-design/icons"; import { SwapOutlined } from '@ant-design/icons';
import {CustomMarketInfo} from "../utils/types"; import { CustomMarketInfo } from '../utils/types';
import Wallet from "@project-serum/sol-wallet-adapter"; import Wallet from '@project-serum/sol-wallet-adapter';
const { Option } = Select; const { Option } = Select;
const { Title } = Typography; const { Title } = Typography;
@ -40,41 +40,55 @@ const ConvertButton = styled(Button)`
export default function ConvertForm() { export default function ConvertForm() {
const { connected, wallet } = useWallet(); const { connected, wallet } = useWallet();
const { customMarkets } = useMarket(); const { customMarkets } = useMarket();
const marketInfos = getMarketInfos(customMarkets) const marketInfos = getMarketInfos(customMarkets);
const {market, setMarketAddress} = useMarket(); const { market, setMarketAddress } = useMarket();
const [fromToken, setFromToken] = useState<string | undefined>(undefined); const [fromToken, setFromToken] = useState<string | undefined>(undefined);
const [toToken, setToToken] = useState<string | undefined>(undefined); const [toToken, setToToken] = useState<string | undefined>(undefined);
const [size, setSize] = useState<number | undefined>(undefined); const [size, setSize] = useState<number | undefined>(undefined);
const marketInfosbyName = Object.fromEntries(marketInfos.map(market => [market.name, market])); const marketInfosbyName = Object.fromEntries(
marketInfos.map((market) => [market.name, market]),
);
const tokenConvertMap: Map<string, Set<string>> = new Map(); const tokenConvertMap: Map<string, Set<string>> = new Map();
Object.keys(marketInfosbyName).forEach((market) => { Object.keys(marketInfosbyName).forEach((market) => {
let [base, quote] = market.split('/'); let [base, quote] = market.split('/');
!tokenConvertMap.has(base) !tokenConvertMap.has(base)
? tokenConvertMap.set(base, new Set([quote])) ? 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.has(quote)
? tokenConvertMap.set(quote, new Set([base])) ? 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 setMarket = (toToken) => {
const marketInfo = marketInfos.filter(marketInfo => !marketInfo.deprecated).find(marketInfo => const marketInfo = marketInfos
marketInfo.name === `${fromToken}/${toToken}` || marketInfo.name === `${toToken}/${fromToken}` .filter((marketInfo) => !marketInfo.deprecated)
); .find(
(marketInfo) =>
marketInfo.name === `${fromToken}/${toToken}` ||
marketInfo.name === `${toToken}/${fromToken}`,
);
if (!marketInfo) { 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({ notify({
message: 'Invalid market', message: 'Invalid market',
type: 'error', type: 'error',
}); });
return; return;
} }
setMarketAddress(marketInfo.address.toBase58()) setMarketAddress(marketInfo.address.toBase58());
setToToken(toToken); setToToken(toToken);
} };
return ( return (
<FloatingElement style={{ maxWidth: 500 }}> <FloatingElement style={{ maxWidth: 500 }}>
@ -148,8 +162,8 @@ function ConvertFormSubmit({
toToken, toToken,
wallet, wallet,
market, market,
customMarkets customMarkets,
} : { }: {
size: number | null | undefined; size: number | null | undefined;
setSize: (newSize: number | undefined) => void; setSize: (newSize: number | undefined) => void;
fromToken: string; fromToken: string;
@ -171,7 +185,9 @@ function ConvertFormSubmit({
const isFromTokenBaseOfMarket = (market) => { const isFromTokenBaseOfMarket = (market) => {
const { marketName } = getMarketDetails(market, customMarkets); const { marketName } = getMarketDetails(market, customMarkets);
if (!marketName) { 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('/'); const [base] = marketName.split('/');
return fromToken === base; return fromToken === base;
@ -213,7 +229,9 @@ function ConvertFormSubmit({
const sidedOrderbookAccount = const sidedOrderbookAccount =
// @ts-ignore // @ts-ignore
side === 'buy' ? market._decoded.asks : market._decoded.bids; side === 'buy' ? market._decoded.asks : market._decoded.bids;
const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount); const orderbookData = await connection.getAccountInfo(
sidedOrderbookAccount,
);
if (!orderbookData?.data) { if (!orderbookData?.data) {
notify({ message: 'Invalid orderbook data', type: 'error' }); notify({ message: 'Invalid orderbook data', type: 'error' });
return; return;
@ -231,8 +249,12 @@ function ConvertFormSubmit({
return; return;
} }
const tickSizeDecimals = getDecimalCount(market.tickSize); const tickSizeDecimals = getDecimalCount(market.tickSize);
const parsedPrice = getMarketOrderPrice(decodedOrderbookData, size, tickSizeDecimals); const parsedPrice = getMarketOrderPrice(
decodedOrderbookData,
size,
tickSizeDecimals,
);
// round size // round size
const sizeDecimalCount = getDecimalCount(market.minOrderSize); const sizeDecimalCount = getDecimalCount(market.minOrderSize);
@ -270,19 +292,25 @@ function ConvertFormSubmit({
const sidedOrderbookAccount = const sidedOrderbookAccount =
// @ts-ignore // @ts-ignore
side === 'buy' ? market._decoded.asks : market._decoded.bids; side === 'buy' ? market._decoded.asks : market._decoded.bids;
const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount); const orderbookData = await connection.getAccountInfo(
sidedOrderbookAccount,
);
if (!orderbookData?.data || !market) { if (!orderbookData?.data || !market) {
return [null, null]; return [null, null];
} }
const decodedOrderbookData = Orderbook.decode(market, orderbookData.data); const decodedOrderbookData = Orderbook.decode(market, orderbookData.data);
const [bbo] = const [bbo] =
decodedOrderbookData && decodedOrderbookData &&
decodedOrderbookData.getL2(1).map(([price]) => price); decodedOrderbookData.getL2(1).map(([price]) => price);
if (!bbo || !size) { if (!bbo || !size) {
return [null, null]; return [null, null];
} }
const tickSizeDecimals = getDecimalCount(market.tickSize); const tickSizeDecimals = getDecimalCount(market.tickSize);
const expectedPrice = getExpectedFillPrice(decodedOrderbookData, size, tickSizeDecimals) const expectedPrice = getExpectedFillPrice(
decodedOrderbookData,
size,
tickSizeDecimals,
);
if (side === 'buy') { if (side === 'buy') {
return [expectedPrice.toFixed(6), 1]; return [expectedPrice.toFixed(6), 1];
} else { } else {
@ -292,21 +320,25 @@ function ConvertFormSubmit({
console.log(`Got error ${e}`); console.log(`Got error ${e}`);
return [null, null]; return [null, null];
} }
} };
useEffect(() => { useEffect(
() => {
getPrice().then(([fromAmount, toAmount]) => { getPrice().then(([fromAmount, toAmount]) => {
setFromAmount(fromAmount || undefined); setFromAmount(fromAmount || undefined);
setToAmount(toAmount || undefined); setToAmount(toAmount || undefined);
}) });
}, },
// eslint-disable-next-line // eslint-disable-next-line
[market?.address.toBase58(), size] [market?.address.toBase58(), size],
) );
const canConvert = market && size && size > 0; const canConvert = market && size && size > 0;
const balance = balances.find(coinBalance => coinBalance.coin === fromToken); const balance = balances.find(
const availableBalance = ((balance?.unsettled || 0.) + (balance?.wallet || 0.)) * 0.99; (coinBalance) => coinBalance.coin === fromToken,
);
const availableBalance =
((balance?.unsettled || 0) + (balance?.wallet || 0)) * 0.99;
return ( return (
<React.Fragment> <React.Fragment>
@ -346,12 +378,12 @@ function ConvertFormSubmit({
</Col> </Col>
</Row> </Row>
{canConvert && ( {canConvert && (
<Row align="middle" justify="center"> <Row align="middle" justify="center">
<Col > <Col>
{fromAmount} {fromToken} {fromAmount} {fromToken}
</Col> </Col>
<Col offset={1}> <Col offset={1}>
<SwapOutlined/> <SwapOutlined />
</Col> </Col>
<Col offset={1}> <Col offset={1}>
{toAmount} {toToken} {toAmount} {toToken}

View File

@ -1,20 +1,20 @@
import React, {useState} from "react"; import React, { useState } from 'react';
import {Col, Input, Modal, Row} from "antd"; import { Col, Input, Modal, Row } from 'antd';
import {EndpointInfo} from "../utils/types"; import { EndpointInfo } from '../utils/types';
export default function CustomClusterEndpointDialog({ export default function CustomClusterEndpointDialog({
visible, visible,
testingConnection, testingConnection,
onAddCustomEndpoint, onAddCustomEndpoint,
onClose, onClose,
} : { }: {
visible: boolean; visible: boolean;
testingConnection: boolean; testingConnection: boolean;
onAddCustomEndpoint: (info: EndpointInfo) => void; onAddCustomEndpoint: (info: EndpointInfo) => void;
onClose?: () => void; onClose?: () => void;
}) { }) {
const [ customEndpoint, setCustomEndpoint] = useState(''); const [customEndpoint, setCustomEndpoint] = useState('');
const [ customEndpointName, setCustomEndpointName] = useState(''); const [customEndpointName, setCustomEndpointName] = useState('');
const onSubmit = () => { const onSubmit = () => {
const fullEndpoint = 'https://' + customEndpoint; const fullEndpoint = 'https://' + customEndpoint;
@ -22,13 +22,13 @@ export default function CustomClusterEndpointDialog({
name: customEndpointName, name: customEndpointName,
endpoint: fullEndpoint, endpoint: fullEndpoint,
custom: true, custom: true,
} };
onAddCustomEndpoint(params); onAddCustomEndpoint(params);
onDoClose(); onDoClose();
}; };
const onDoClose = () => { const onDoClose = () => {
setCustomEndpoint('') setCustomEndpoint('');
setCustomEndpointName('') setCustomEndpointName('');
onClose && onClose(); onClose && onClose();
}; };
const canSubmit = customEndpoint !== '' && customEndpointName !== ''; const canSubmit = customEndpoint !== '' && customEndpointName !== '';

View File

@ -2,7 +2,13 @@ import React from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { LinkOutlined } from '@ant-design/icons'; 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 ( return (
<div> <div>
{title && <p style={{ color: 'white' }}>{title}</p>} {title && <p style={{ color: 'white' }}>{title}</p>}

View File

@ -1,5 +1,5 @@
import {Button, Col, Divider, Row} from 'antd'; import { Button, Col, Divider, Row } from 'antd';
import React, {useState} from 'react'; import React, { useState } from 'react';
import FloatingElement from './layout/FloatingElement'; import FloatingElement from './layout/FloatingElement';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@ -11,13 +11,13 @@ import {
useTokenAccounts, useTokenAccounts,
} from '../utils/markets'; } from '../utils/markets';
import DepositDialog from './DepositDialog'; import DepositDialog from './DepositDialog';
import {useWallet} from '../utils/wallet'; import { useWallet } from '../utils/wallet';
import Link from './Link'; import Link from './Link';
import {settleFunds} from '../utils/send'; import { settleFunds } from '../utils/send';
import {useSendConnection} from '../utils/connection'; import { useSendConnection } from '../utils/connection';
import {notify} from '../utils/notifications'; import { notify } from '../utils/notifications';
import {Balances} from "../utils/types"; import { Balances } from '../utils/types';
import StandaloneTokenAccountsSelect from "./StandaloneTokenAccountSelect"; import StandaloneTokenAccountsSelect from './StandaloneTokenAccountSelect';
const RowBox = styled(Row)` const RowBox = styled(Row)`
padding-bottom: 20px; padding-bottom: 20px;
@ -101,69 +101,85 @@ export default function StandaloneBalancesDisplay() {
} }
} }
const formattedBalances: [string | undefined, Balances | undefined, string, string | undefined][] = [ const formattedBalances: [
[baseCurrency, baseCurrencyBalances, 'base', market?.baseMintAddress.toBase58()], string | undefined,
[quoteCurrency, quoteCurrencyBalances, 'quote', market?.quoteMintAddress.toBase58()], Balances | undefined,
] string,
string | undefined,
][] = [
[
baseCurrency,
baseCurrencyBalances,
'base',
market?.baseMintAddress.toBase58(),
],
[
quoteCurrency,
quoteCurrencyBalances,
'quote',
market?.quoteMintAddress.toBase58(),
],
];
return ( return (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}> <FloatingElement style={{ flex: 1, paddingTop: 10 }}>
{formattedBalances.map(([currency, balances, baseOrQuote, mint], index) => ( {formattedBalances.map(
<React.Fragment key={index}> ([currency, balances, baseOrQuote, mint], index) => (
<Divider style={{ borderColor: 'white' }}>{currency}</Divider> <React.Fragment key={index}>
{connected && ( <Divider style={{ borderColor: 'white' }}>{currency}</Divider>
{connected && (
<RowBox align="middle" style={{ paddingBottom: 10 }}>
<StandaloneTokenAccountsSelect
accounts={tokenAccounts?.filter(
(account) => account.effectiveMint.toBase58() === mint,
)}
mint={mint}
label
/>
</RowBox>
)}
<RowBox <RowBox
align="middle" align="middle"
style={{ paddingBottom: 10 }} justify="space-between"
style={{ paddingBottom: 12 }}
> >
<StandaloneTokenAccountsSelect <Col>Wallet balance:</Col>
accounts={tokenAccounts?.filter(account => account.effectiveMint.toBase58() === mint)} <Col>{balances && balances.wallet}</Col>
mint={mint}
label
/>
</RowBox> </RowBox>
)} <RowBox
<RowBox align="middle"
align="middle" justify="space-between"
justify="space-between" style={{ paddingBottom: 12 }}
style={{ paddingBottom: 12 }} >
> <Col>Unsettled balance:</Col>
<Col>Wallet balance:</Col> <Col>{balances && balances.unsettled}</Col>
<Col>{balances && balances.wallet}</Col> </RowBox>
</RowBox> <RowBox align="middle" justify="space-around">
<RowBox <Col style={{ width: 150 }}>
align="middle" <ActionButton
justify="space-between" block
style={{ paddingBottom: 12 }} size="large"
> onClick={() => setBaseOrQuote(baseOrQuote)}
<Col>Unsettled balance:</Col> >
<Col>{balances && balances.unsettled}</Col> Deposit
</RowBox> </ActionButton>
<RowBox align="middle" justify="space-around"> </Col>
<Col style={{ width: 150 }}> <Col style={{ width: 150 }}>
<ActionButton <ActionButton block size="large" onClick={onSettleFunds}>
block Settle
size="large" </ActionButton>
onClick={() => setBaseOrQuote(baseOrQuote)} </Col>
> </RowBox>
Deposit <Tip>
</ActionButton> All deposits go to your{' '}
</Col> <Link external to={providerUrl}>
<Col style={{ width: 150 }}> {providerName}
<ActionButton block size="large" onClick={onSettleFunds}> </Link>{' '}
Settle wallet
</ActionButton> </Tip>
</Col> </React.Fragment>
</RowBox> ),
<Tip> )}
All deposits go to your{' '}
<Link external to={providerUrl}>
{providerName}
</Link>{' '}
wallet
</Tip>
</React.Fragment>
))}
<DepositDialog <DepositDialog
baseOrQuote={baseOrQuote} baseOrQuote={baseOrQuote}
onClose={() => setBaseOrQuote('')} onClose={() => setBaseOrQuote('')}

View File

@ -1,21 +1,24 @@
import React from 'react'; import React from 'react';
import {TokenAccount} from "../utils/types"; import { TokenAccount } from '../utils/types';
import {useSelectedTokenAccounts} from "../utils/markets"; import { useSelectedTokenAccounts } from '../utils/markets';
import {Button, Col, Select, Typography} from "antd"; import { Button, Col, Select, Typography } from 'antd';
import {CopyOutlined} from '@ant-design/icons'; import { CopyOutlined } from '@ant-design/icons';
import {abbreviateAddress} from "../utils/utils"; import { abbreviateAddress } from '../utils/utils';
import {notify} from "../utils/notifications"; import { notify } from '../utils/notifications';
export default function StandaloneTokenAccountsSelect({ export default function StandaloneTokenAccountsSelect({
accounts, accounts,
mint, mint,
label, label,
}: { }: {
accounts: TokenAccount[] | null | undefined, accounts: TokenAccount[] | null | undefined;
mint: string | undefined, mint: string | undefined;
label?: boolean label?: boolean;
}) { }) {
const [selectedTokenAccounts, setSelectedTokenAccounts] = useSelectedTokenAccounts(); const [
selectedTokenAccounts,
setSelectedTokenAccounts,
] = useSelectedTokenAccounts();
let selectedValue: string | undefined; let selectedValue: string | undefined;
if (mint && mint in selectedTokenAccounts) { if (mint && mint in selectedTokenAccounts) {
@ -32,32 +35,35 @@ export default function StandaloneTokenAccountsSelect({
message: 'Error selecting token account', message: 'Error selecting token account',
description: 'Mint is undefined', description: 'Mint is undefined',
type: 'error', type: 'error',
}) });
return; return;
} }
const newSelectedTokenAccounts = {...selectedTokenAccounts}; const newSelectedTokenAccounts = { ...selectedTokenAccounts };
newSelectedTokenAccounts[mint] = value; newSelectedTokenAccounts[mint] = value;
setSelectedTokenAccounts(newSelectedTokenAccounts); setSelectedTokenAccounts(newSelectedTokenAccounts);
} };
return ( return (
<React.Fragment> <React.Fragment>
{label && {label && <Col span={8}>Token account:</Col>}
<Col span={8}>
Token account:
</Col>
}
<Col span={label ? 13 : 21}> <Col span={label ? 13 : 21}>
<Select <Select
style={{ width: '100%' }} style={{ width: '100%' }}
value={selectedValue} value={selectedValue}
onSelect={setTokenAccountForCoin} onSelect={setTokenAccountForCoin}
> >
{accounts?.map(account => ( {accounts?.map((account) => (
<Select.Option key={account.pubkey.toBase58()} value={account.pubkey.toBase58()}> <Select.Option
<Typography.Text code>{label ? abbreviateAddress(account.pubkey, 8) : account.pubkey.toBase58()}</Typography.Text> key={account.pubkey.toBase58()}
</Select.Option>) value={account.pubkey.toBase58()}
)} >
<Typography.Text code>
{label
? abbreviateAddress(account.pubkey, 8)
: account.pubkey.toBase58()}
</Typography.Text>
</Select.Option>
))}
</Select> </Select>
</Col> </Col>
<Col span={2} offset={1}> <Col span={2} offset={1}>
@ -65,7 +71,9 @@ export default function StandaloneTokenAccountsSelect({
shape="round" shape="round"
icon={<CopyOutlined />} icon={<CopyOutlined />}
size={'small'} size={'small'}
onClick={() => selectedValue && navigator.clipboard.writeText(selectedValue)} onClick={() =>
selectedValue && navigator.clipboard.writeText(selectedValue)
}
/> />
</Col> </Col>
</React.Fragment> </React.Fragment>

View File

@ -1,16 +1,20 @@
import {InfoCircleOutlined, PlusCircleOutlined, SettingOutlined,} from '@ant-design/icons'; import {
import {Button, Col, Menu, Popover, Row, Select} from 'antd'; InfoCircleOutlined,
import React, {useCallback, useEffect, useState} from 'react'; PlusCircleOutlined,
import {useHistory, useLocation} from 'react-router-dom'; 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 logo from '../assets/logo.svg';
import styled from 'styled-components'; import styled from 'styled-components';
import {useWallet, WALLET_PROVIDERS} from '../utils/wallet'; import { useWallet, WALLET_PROVIDERS } from '../utils/wallet';
import {ENDPOINTS, useConnectionConfig} from '../utils/connection'; import { ENDPOINTS, useConnectionConfig } from '../utils/connection';
import Settings from './Settings'; import Settings from './Settings';
import CustomClusterEndpointDialog from "./CustomClusterEndpointDialog"; import CustomClusterEndpointDialog from './CustomClusterEndpointDialog';
import {EndpointInfo} from "../utils/types"; import { EndpointInfo } from '../utils/types';
import {notify} from "../utils/notifications"; import { notify } from '../utils/notifications';
import {Connection} from "@solana/web3.js"; import { Connection } from '@solana/web3.js';
import WalletConnect from './WalletConnect'; import WalletConnect from './WalletConnect';
const Wrapper = styled.div` const Wrapper = styled.div`
@ -41,13 +45,19 @@ const EXTERNAL_LINKS = {
'/developer-resources': 'https://serum-academy.com/en/developer-resources/', '/developer-resources': 'https://serum-academy.com/en/developer-resources/',
'/explorer': 'https://explorer.solana.com', '/explorer': 'https://explorer.solana.com',
'/srm-faq': 'https://projectserum.com/srm-faq', '/srm-faq': 'https://projectserum.com/srm-faq',
} };
export default function TopBar() { export default function TopBar() {
const { connected, wallet, providerUrl, setProvider } = useWallet(); const { connected, wallet, providerUrl, setProvider } = useWallet();
const { endpoint, endpointInfo, setEndpoint, availableEndpoints, setCustomEndpoints } = useConnectionConfig(); const {
const [ addEndpointVisible, setAddEndpointVisible ] = useState(false) endpoint,
const [ testingConnection, setTestingConnection] = useState(false) endpointInfo,
setEndpoint,
availableEndpoints,
setCustomEndpoints,
} = useConnectionConfig();
const [addEndpointVisible, setAddEndpointVisible] = useState(false);
const [testingConnection, setTestingConnection] = useState(false);
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
@ -73,39 +83,45 @@ export default function TopBar() {
} }
const handleError = (e) => { const handleError = (e) => {
console.log(`Connection to ${info.endpoint} failed: ${e}`) console.log(`Connection to ${info.endpoint} failed: ${e}`);
notify({ notify({
message: `Failed to connect to ${info.endpoint}`, message: `Failed to connect to ${info.endpoint}`,
type: 'error', type: 'error',
}); });
} };
try { try {
const connection = new Connection(info.endpoint, 'recent'); const connection = new Connection(info.endpoint, 'recent');
connection.getEpochInfo().then(result => { connection
setTestingConnection(true); .getEpochInfo()
console.log(`testing connection to ${info.endpoint}`); .then((result) => {
const newCustomEndpoints = [...availableEndpoints.filter(e => e.custom), info]; setTestingConnection(true);
setEndpoint(info.endpoint); console.log(`testing connection to ${info.endpoint}`);
setCustomEndpoints(newCustomEndpoints); const newCustomEndpoints = [
}).catch(handleError); ...availableEndpoints.filter((e) => e.custom),
info,
];
setEndpoint(info.endpoint);
setCustomEndpoints(newCustomEndpoints);
})
.catch(handleError);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} finally { } finally {
setTestingConnection(false); setTestingConnection(false);
} }
} };
const endpointInfoCustom = endpointInfo && endpointInfo.custom const endpointInfoCustom = endpointInfo && endpointInfo.custom;
useEffect(() => { useEffect(() => {
const handler = () => { const handler = () => {
if (endpointInfoCustom) { if (endpointInfoCustom) {
setEndpoint(ENDPOINTS[0].endpoint) setEndpoint(ENDPOINTS[0].endpoint);
} }
} };
window.addEventListener("beforeunload", handler) window.addEventListener('beforeunload', handler);
return () => window.removeEventListener("beforeunload", handler) return () => window.removeEventListener('beforeunload', handler);
}, [endpointInfoCustom, setEndpoint]) }, [endpointInfoCustom, setEndpoint]);
return ( return (
<> <>
@ -137,34 +153,61 @@ export default function TopBar() {
{connected && <Menu.Item key="/orders">ORDERS</Menu.Item>} {connected && <Menu.Item key="/orders">ORDERS</Menu.Item>}
{connected && <Menu.Item key="/convert">CONVERT</Menu.Item>} {connected && <Menu.Item key="/convert">CONVERT</Menu.Item>}
<Menu.Item key="/list-new-market">ADD MARKET</Menu.Item> <Menu.Item key="/list-new-market">ADD MARKET</Menu.Item>
<Menu.SubMenu title="LEARN" onTitleClick={() => window.open(EXTERNAL_LINKS['/learn'], '_blank')}> <Menu.SubMenu
title="LEARN"
onTitleClick={() => window.open(EXTERNAL_LINKS['/learn'], '_blank')}
>
<Menu.Item key="/add-market"> <Menu.Item key="/add-market">
<a href={EXTERNAL_LINKS['/add-market']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/add-market']}
target="_blank"
rel="noopener noreferrer"
>
Adding a market Adding a market
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item key="/wallet-support"> <Menu.Item key="/wallet-support">
<a href={EXTERNAL_LINKS['/wallet-support']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/wallet-support']}
target="_blank"
rel="noopener noreferrer"
>
Supported wallets Supported wallets
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item key="/dex-list"> <Menu.Item key="/dex-list">
<a href={EXTERNAL_LINKS['/dex-list']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/dex-list']}
target="_blank"
rel="noopener noreferrer"
>
DEX list DEX list
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item key="/developer-resources"> <Menu.Item key="/developer-resources">
<a href={EXTERNAL_LINKS['/developer-resources']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/developer-resources']}
target="_blank"
rel="noopener noreferrer"
>
Developer resources Developer resources
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item key="/explorer"> <Menu.Item key="/explorer">
<a href={EXTERNAL_LINKS['/explorer']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/explorer']}
target="_blank"
rel="noopener noreferrer"
>
Solana block explorer Solana block explorer
</a> </a>
</Menu.Item> </Menu.Item>
<Menu.Item key="/srm-faq"> <Menu.Item key="/srm-faq">
<a href={EXTERNAL_LINKS['/srm-faq']} target="_blank" rel="noopener noreferrer"> <a
href={EXTERNAL_LINKS['/srm-faq']}
target="_blank"
rel="noopener noreferrer"
>
SRM FAQ SRM FAQ
</a> </a>
</Menu.Item> </Menu.Item>
@ -182,7 +225,7 @@ export default function TopBar() {
onClick={() => setAddEndpointVisible(true)} onClick={() => setAddEndpointVisible(true)}
/> />
</Col> </Col>
<Col> <Col>
<Popover <Popover
content={endpoint} content={endpoint}
placement="bottomRight" placement="bottomRight"
@ -232,7 +275,7 @@ export default function TopBar() {
</Select> </Select>
</div> </div>
<div> <div>
<WalletConnect/> <WalletConnect />
</div> </div>
</Wrapper> </Wrapper>
</> </>

View File

@ -20,7 +20,7 @@ import {
import { useSendConnection } from '../utils/connection'; import { useSendConnection } from '../utils/connection';
import FloatingElement from './layout/FloatingElement'; import FloatingElement from './layout/FloatingElement';
import { placeOrder } from '../utils/send'; import { placeOrder } from '../utils/send';
import {SwitchChangeEventHandler} from "antd/es/switch"; import { SwitchChangeEventHandler } from 'antd/es/switch';
const SellButton = styled(Button)` const SellButton = styled(Button)`
margin: 20px 0px 0px 0px; margin: 20px 0px 0px 0px;
@ -42,9 +42,14 @@ const sliderMarks = {
100: '100%', 100: '100%',
}; };
export default function TradeForm({ style, setChangeOrderRef }: { export default function TradeForm({
style,
setChangeOrderRef,
}: {
style?: any; 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 [side, setSide] = useState<'buy' | 'sell'>('buy');
const { baseCurrency, quoteCurrency, market } = useMarket(); const { baseCurrency, quoteCurrency, market } = useMarket();
@ -65,9 +70,10 @@ export default function TradeForm({ style, setChangeOrderRef }: {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [sizeFraction, setSizeFraction] = useState(0); const [sizeFraction, setSizeFraction] = useState(0);
const availableQuote = openOrdersAccount && market const availableQuote =
? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree) openOrdersAccount && market
: 0; ? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree)
: 0;
let quoteBalance = (quoteCurrencyBalances || 0) + (availableQuote || 0); let quoteBalance = (quoteCurrencyBalances || 0) + (availableQuote || 0);
let baseBalance = baseCurrencyBalances || 0; let baseBalance = baseCurrencyBalances || 0;
@ -123,7 +129,13 @@ export default function TradeForm({ style, setChangeOrderRef }: {
setBaseSize(baseSize); 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 formattedSize = size && roundToDecimal(size, sizeDecimalCount);
const formattedPrice = price && roundToDecimal(price, priceDecimalCount); const formattedPrice = price && roundToDecimal(price, priceDecimalCount);
formattedSize && onSetBaseSize(formattedSize); formattedSize && onSetBaseSize(formattedSize);
@ -131,9 +143,10 @@ export default function TradeForm({ style, setChangeOrderRef }: {
}; };
const updateSizeFraction = () => { 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 maxSize = floorToDecimal(rawMaxSize, sizeDecimalCount);
const sizeFraction = Math.min(((baseSize || 0.) / maxSize) * 100, 100); const sizeFraction = Math.min(((baseSize || 0) / maxSize) * 100, 100);
setSizeFraction(sizeFraction); setSizeFraction(sizeFraction);
}; };
@ -142,13 +155,17 @@ export default function TradeForm({ style, setChangeOrderRef }: {
let formattedMarkPrice: number | string = priceDecimalCount let formattedMarkPrice: number | string = priceDecimalCount
? markPrice.toFixed(priceDecimalCount) ? markPrice.toFixed(priceDecimalCount)
: markPrice; : markPrice;
setPrice(typeof formattedMarkPrice === 'number' ? formattedMarkPrice : parseFloat(formattedMarkPrice)); setPrice(
typeof formattedMarkPrice === 'number'
? formattedMarkPrice
: parseFloat(formattedMarkPrice),
);
} }
let newSize; let newSize;
if (side === 'buy') { if (side === 'buy') {
if (price || markPrice) { if (price || markPrice) {
newSize = ((quoteBalance / (price || markPrice || 1.)) * value) / 100; newSize = ((quoteBalance / (price || markPrice || 1)) * value) / 100;
} }
} else { } else {
newSize = (baseBalance * value) / 100; newSize = (baseBalance * value) / 100;

View File

@ -32,10 +32,10 @@ export default function PublicTrades({ smallScreen }) {
> >
<Title>Recent Market trades</Title> <Title>Recent Market trades</Title>
<SizeTitle> <SizeTitle>
<Col span={8}> <Col span={8}>Price ({quoteCurrency}) </Col>
Price ({quoteCurrency}){' '} <Col span={8} style={{ textAlign: 'right' }}>
Size ({baseCurrency})
</Col> </Col>
<Col span={8} style={{ textAlign: 'right' }}>Size ({baseCurrency})</Col>
<Col span={8} style={{ textAlign: 'right' }}> <Col span={8} style={{ textAlign: 'right' }}>
Time Time
</Col> </Col>

View File

@ -1,21 +1,27 @@
import React, {useState} from 'react'; import React, { useState } from 'react';
import DataTable from '../layout/DataTable'; import DataTable from '../layout/DataTable';
import styled from 'styled-components'; import styled from 'styled-components';
import {Button, Col, Row, Tag} from 'antd'; import { Button, Col, Row, Tag } from 'antd';
import {cancelOrder} from '../../utils/send'; import { cancelOrder } from '../../utils/send';
import {useWallet} from '../../utils/wallet'; import { useWallet } from '../../utils/wallet';
import {useSendConnection} from '../../utils/connection'; import { useSendConnection } from '../../utils/connection';
import {notify} from '../../utils/notifications'; import { notify } from '../../utils/notifications';
import {DeleteOutlined} from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import {OrderWithMarketAndMarketName} from "../../utils/types"; import { OrderWithMarketAndMarketName } from '../../utils/types';
const CancelButton = styled(Button)` const CancelButton = styled(Button)`
color: #f23b69; color: #f23b69;
border: 1px solid #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; openOrders: OrderWithMarketAndMarketName[] | null | undefined;
onCancelSuccess?: () => void; onCancelSuccess?: () => void;
pageSize?: number; pageSize?: number;
@ -50,15 +56,17 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize,
} }
const marketFilters = [ const marketFilters = [
...new Set((openOrders || []).map(orderInfos => orderInfos.marketName)) ...new Set((openOrders || []).map((orderInfos) => orderInfos.marketName)),
].map(marketName => {return {text: marketName, value: marketName}}); ].map((marketName) => {
return { text: marketName, value: marketName };
});
const columns = [ const columns = [
{ {
title: 'Market', title: 'Market',
dataIndex: 'marketName', dataIndex: 'marketName',
key: 'marketName', key: 'marketName',
filters: (marketFilter ? marketFilters : undefined), filters: marketFilter ? marketFilters : undefined,
onFilter: (value, record) => record.marketName.indexOf(value) === 0, onFilter: (value, record) => record.marketName.indexOf(value) === 0,
}, },
{ {
@ -75,11 +83,11 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize,
), ),
sorter: (a, b) => { sorter: (a, b) => {
if (a.side === b.side) { if (a.side === b.side) {
return 0. return 0;
} else if (a.side === 'buy') { } else if (a.side === 'buy') {
return 1. return 1;
} else { } else {
return -1. return -1;
} }
}, },
showSorterTooltip: false, showSorterTooltip: false,

View File

@ -1,12 +1,17 @@
import React, {useState} from 'react'; import React, { useState } from 'react';
import DataTable from '../layout/DataTable'; import DataTable from '../layout/DataTable';
import {Button, Row} from "antd"; import { Button, Row } from 'antd';
import {settleAllFunds} from "../../utils/send"; import { settleAllFunds } from '../../utils/send';
import {notify} from "../../utils/notifications"; import { notify } from '../../utils/notifications';
import {useConnection} from "../../utils/connection"; import { useConnection } from '../../utils/connection';
import {useWallet} from "../../utils/wallet"; import { useWallet } from '../../utils/wallet';
import {useAllMarkets, useMarket, useSelectedTokenAccounts, useTokenAccounts} from "../../utils/markets"; import {
import StandaloneTokenAccountsSelect from "../StandaloneTokenAccountSelect"; useAllMarkets,
useMarket,
useSelectedTokenAccounts,
useTokenAccounts,
} from '../../utils/markets';
import StandaloneTokenAccountsSelect from '../StandaloneTokenAccountSelect';
export default function WalletBalancesTable({ export default function WalletBalancesTable({
walletBalances, walletBalances,
@ -17,13 +22,13 @@ export default function WalletBalancesTable({
walletBalance: number; walletBalance: number;
openOrdersFree: number; openOrdersFree: number;
openOrdersTotal: number; openOrdersTotal: number;
}[] }[];
}) { }) {
const connection = useConnection(); const connection = useConnection();
const { wallet, connected } = useWallet(); const { wallet, connected } = useWallet();
const [selectedTokenAccounts] = useSelectedTokenAccounts(); const [selectedTokenAccounts] = useSelectedTokenAccounts();
const [tokenAccounts, tokenAccountsConnected] = useTokenAccounts(); const [tokenAccounts, tokenAccountsConnected] = useTokenAccounts();
const {customMarkets} = useMarket(); const { customMarkets } = useMarket();
const [allMarkets, allMarketsConnected] = useAllMarkets(customMarkets); const [allMarkets, allMarketsConnected] = useAllMarkets(customMarkets);
const [settlingFunds, setSettlingFunds] = useState(false); const [settlingFunds, setSettlingFunds] = useState(false);
@ -51,8 +56,8 @@ export default function WalletBalancesTable({
tokenAccounts, tokenAccounts,
selectedTokenAccounts, selectedTokenAccounts,
wallet, wallet,
markets: allMarkets.map(marketInfo => marketInfo.market), markets: allMarkets.map((marketInfo) => marketInfo.market),
}) });
} catch (e) { } catch (e) {
notify({ notify({
message: 'Error settling funds', message: 'Error settling funds',
@ -94,9 +99,11 @@ export default function WalletBalancesTable({
key: 'selectTokenAccount', key: 'selectTokenAccount',
width: '20%', width: '20%',
render: (walletBalance) => ( render: (walletBalance) => (
<Row align="middle" style={{width: "430px"}}> <Row align="middle" style={{ width: '430px' }}>
<StandaloneTokenAccountsSelect <StandaloneTokenAccountsSelect
accounts={tokenAccounts?.filter(t => t.effectiveMint.toBase58() === walletBalance.mint)} accounts={tokenAccounts?.filter(
(t) => t.effectiveMint.toBase58() === walletBalance.mint,
)}
mint={walletBalance.mint} mint={walletBalance.mint}
/> />
</Row> </Row>
@ -111,14 +118,11 @@ export default function WalletBalancesTable({
columns={columns} columns={columns}
pagination={false} pagination={false}
/> />
{connected && {connected && (
<Button <Button onClick={onSettleFunds} loading={settlingFunds}>
onClick={onSettleFunds}
loading={settlingFunds}
>
Settle all funds Settle all funds
</Button> </Button>
} )}
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import {Button, Popover} from 'antd'; import { Button, Popover } from 'antd';
import {InfoCircleOutlined, UserOutlined} from '@ant-design/icons'; import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons';
import { useWallet } from '../utils/wallet'; import { useWallet } from '../utils/wallet';
import LinkAddress from "./LinkAddress"; import LinkAddress from './LinkAddress';
export default function WalletConnect() { export default function WalletConnect() {
const { connected, wallet } = useWallet(); const { connected, wallet } = useWallet();

22
src/declarations.d.ts vendored
View File

@ -1,14 +1,14 @@
declare module '@project-serum/sol-wallet-adapter' { declare module '@project-serum/sol-wallet-adapter' {
import EventEmitter from "eventemitter3"; import EventEmitter from 'eventemitter3';
import {PublicKey, Transaction} from "@solana/web3.js"; import { PublicKey, Transaction } from '@solana/web3.js';
export default class Wallet extends EventEmitter { export default class Wallet extends EventEmitter {
constructor(providerUrl: string, network: string) constructor(providerUrl: string, network: string);
publicKey: PublicKey; publicKey: PublicKey;
connected: boolean; connected: boolean;
autoApprove: boolean; autoApprove: boolean;
connect: () => Promise<void>; connect: () => Promise<void>;
disconnect: () => void; disconnect: () => void;
signTransaction: (transaction: Transaction) => Promise<Transaction>; signTransaction: (transaction: Transaction) => Promise<Transaction>;
} }
} }

View File

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import {Tabs} from 'antd'; import { Tabs } from 'antd';
import {useAllOpenOrdersBalances, useWalletBalancesForAllMarkets,} from '../utils/markets'; import {
useAllOpenOrdersBalances,
useWalletBalancesForAllMarkets,
} from '../utils/markets';
import FloatingElement from '../components/layout/FloatingElement'; import FloatingElement from '../components/layout/FloatingElement';
import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable'; import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable';
import {useMintToTickers} from "../utils/tokens"; import { useMintToTickers } from '../utils/tokens';
const { TabPane } = Tabs; const { TabPane } = Tabs;
@ -12,28 +15,26 @@ export default function BalancesPage() {
const mintToTickers = useMintToTickers(); const mintToTickers = useMintToTickers();
const openOrdersBalances = useAllOpenOrdersBalances(); const openOrdersBalances = useAllOpenOrdersBalances();
const data = (walletBalances || []).map(balance => { const data = (walletBalances || []).map((balance) => {
const balances = { const balances = {
coin: mintToTickers[balance.mint], coin: mintToTickers[balance.mint],
mint: balance.mint, mint: balance.mint,
walletBalance: balance.balance, walletBalance: balance.balance,
openOrdersFree: 0., openOrdersFree: 0,
openOrdersTotal: 0., openOrdersTotal: 0,
} };
for (let openOrdersAccount of (openOrdersBalances[balance.mint] || [])) { for (let openOrdersAccount of openOrdersBalances[balance.mint] || []) {
balances['openOrdersFree'] += openOrdersAccount.free; balances['openOrdersFree'] += openOrdersAccount.free;
balances['openOrdersTotal'] += openOrdersAccount.total; balances['openOrdersTotal'] += openOrdersAccount.total;
} }
return balances return balances;
}); });
return ( return (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}> <FloatingElement style={{ flex: 1, paddingTop: 10 }}>
<Tabs defaultActiveKey="walletBalances"> <Tabs defaultActiveKey="walletBalances">
<TabPane tab="Wallet Balances" key="walletBalances"> <TabPane tab="Wallet Balances" key="walletBalances">
<WalletBalancesTable <WalletBalancesTable walletBalances={data} />
walletBalances={data}
/>
</TabPane> </TabPane>
</Tabs> </Tabs>
</FloatingElement> </FloatingElement>

View File

@ -1,36 +1,45 @@
import React from 'react'; import React from 'react';
import FloatingElement from '../components/layout/FloatingElement'; import FloatingElement from '../components/layout/FloatingElement';
import {getMarketInfos, useAllMarkets, useAllOpenOrders, useMarket} from "../utils/markets"; import {
import OpenOrderTable from "../components/UserInfoTable/OpenOrderTable"; getMarketInfos,
import {Button} from "antd"; useAllMarkets,
import {OrderWithMarketAndMarketName} from "../utils/types"; useAllOpenOrders,
useMarket,
} from '../utils/markets';
import OpenOrderTable from '../components/UserInfoTable/OpenOrderTable';
import { Button } from 'antd';
import { OrderWithMarketAndMarketName } from '../utils/types';
export default function OpenOrdersPage() { export default function OpenOrdersPage() {
const {openOrders, loaded, refreshOpenOrders} = useAllOpenOrders(); const { openOrders, loaded, refreshOpenOrders } = useAllOpenOrders();
let {customMarkets} = useMarket(); let { customMarkets } = useMarket();
let marketInfos = getMarketInfos(customMarkets); 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); let [allMarkets] = useAllMarkets(customMarkets);
const marketsByAddress = Object.fromEntries((allMarkets || []).map( const marketsByAddress = Object.fromEntries(
marketInfo => [marketInfo.market.address.toBase58(), marketInfo.market] (allMarkets || []).map((marketInfo) => [
)); marketInfo.market.address.toBase58(),
marketInfo.market,
]),
);
const dataSource: OrderWithMarketAndMarketName[] = (openOrders || []).map((orderInfos) => const dataSource: OrderWithMarketAndMarketName[] = (openOrders || [])
orderInfos.orders.map(order => { .map((orderInfos) =>
return { orderInfos.orders.map((order) => {
marketName: marketAddressesToNames[orderInfos.marketAddress], return {
market: marketsByAddress[orderInfos.marketAddress], marketName: marketAddressesToNames[orderInfos.marketAddress],
...order market: marketsByAddress[orderInfos.marketAddress],
}; ...order,
}) };
).flat(); }),
)
.flat();
return ( return (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}> <FloatingElement style={{ flex: 1, paddingTop: 10 }}>
<Button <Button onClick={refreshOpenOrders} loading={!loaded}>
onClick={refreshOpenOrders}
loading={!loaded}
>
Refresh Refresh
</Button> </Button>
<OpenOrderTable <OpenOrderTable

View File

@ -55,7 +55,9 @@ export default function TradePage() {
document.title = marketName ? `${marketName} — Serum` : 'Serum'; document.title = marketName ? `${marketName} — Serum` : 'Serum';
}, [marketName]); }, [marketName]);
const changeOrderRef = useRef<({ size, price }: {size?: number; price?: number;}) => void>(); const changeOrderRef = useRef<
({ size, price }: { size?: number; price?: number }) => void
>();
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {

View File

@ -5,9 +5,7 @@ export default class BonfidaApi {
static async get(path: string) { static async get(path: string) {
try { try {
const response = await fetch( const response = await fetch(this.URL + path);
this.URL + path,
);
if (response.ok) { if (response.ok) {
const responseJson = await response.json(); const responseJson = await response.json();
return responseJson.success ? responseJson.data : null; return responseJson.success ? responseJson.data : null;

View File

@ -1,32 +1,33 @@
import {useLocalStorageState} from './utils'; import { useLocalStorageState } from './utils';
import {Account, AccountInfo, Connection, PublicKey} from '@solana/web3.js'; import { Account, AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import React, {useContext, useEffect, useMemo} from 'react'; import React, { useContext, useEffect, useMemo } from 'react';
import {setCache, useAsyncData} from './fetch-loop'; import { setCache, useAsyncData } from './fetch-loop';
import tuple from 'immutable-tuple'; import tuple from 'immutable-tuple';
import {ConnectionContextValues, EndpointInfo} from "./types"; import { ConnectionContextValues, EndpointInfo } from './types';
export const ENDPOINTS: EndpointInfo[] = [ export const ENDPOINTS: EndpointInfo[] = [
{ {
name: 'mainnet-beta', name: 'mainnet-beta',
endpoint: 'https://solana-api.projectserum.com', endpoint: 'https://solana-api.projectserum.com',
custom: false custom: false,
}, },
{ name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false }, { name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false },
]; ];
const accountListenerCount = new Map(); const accountListenerCount = new Map();
const ConnectionContext: React.Context<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(null); const ConnectionContext: React.Context<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(
null,
);
export function ConnectionProvider({ children }) { export function ConnectionProvider({ children }) {
const [endpoint, setEndpoint] = useLocalStorageState<string>( const [endpoint, setEndpoint] = useLocalStorageState<string>(
'connectionEndpts', 'connectionEndpts',
ENDPOINTS[0].endpoint, ENDPOINTS[0].endpoint,
); );
const [customEndpoints, setCustomEndpoints] = useLocalStorageState<EndpointInfo[]>( const [customEndpoints, setCustomEndpoints] = useLocalStorageState<
'customConnectionEndpoints', EndpointInfo[]
[] >('customConnectionEndpoints', []);
)
const availableEndpoints = ENDPOINTS.concat(customEndpoints); const availableEndpoints = ENDPOINTS.concat(customEndpoints);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [ 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 // This is a hack to prevent the list from every getting empty
useEffect(() => { useEffect(() => {
const id = connection.onAccountChange(new Account().publicKey, () => {}); const id = connection.onAccountChange(new Account().publicKey, () => {});
return () => {connection.removeAccountChangeListener(id)}; return () => {
connection.removeAccountChangeListener(id);
};
}, [connection]); }, [connection]);
useEffect(() => { useEffect(() => {
const id = connection.onSlotChange(() => null); const id = connection.onSlotChange(() => null);
return () => {connection.removeSlotChangeListener(id)}; return () => {
connection.removeSlotChangeListener(id);
};
}, [connection]); }, [connection]);
useEffect(() => { useEffect(() => {
@ -54,17 +59,28 @@ export function ConnectionProvider({ children }) {
new Account().publicKey, new Account().publicKey,
() => {}, () => {},
); );
return () => {sendConnection.removeAccountChangeListener(id)}; return () => {
sendConnection.removeAccountChangeListener(id);
};
}, [sendConnection]); }, [sendConnection]);
useEffect(() => { useEffect(() => {
const id = sendConnection.onSlotChange(() => null); const id = sendConnection.onSlotChange(() => null);
return () => {sendConnection.removeSlotChangeListener(id)}; return () => {
sendConnection.removeSlotChangeListener(id);
};
}, [sendConnection]); }, [sendConnection]);
return ( return (
<ConnectionContext.Provider <ConnectionContext.Provider
value={{ endpoint, setEndpoint, connection, sendConnection, availableEndpoints, setCustomEndpoints }} value={{
endpoint,
setEndpoint,
connection,
sendConnection,
availableEndpoints,
setCustomEndpoints,
}}
> >
{children} {children}
</ConnectionContext.Provider> </ConnectionContext.Provider>
@ -74,7 +90,7 @@ export function ConnectionProvider({ children }) {
export function useConnection() { export function useConnection() {
const context = useContext(ConnectionContext); const context = useContext(ConnectionContext);
if (!context) { if (!context) {
throw new Error('Missing connection context') throw new Error('Missing connection context');
} }
return context.connection; return context.connection;
} }
@ -82,7 +98,7 @@ export function useConnection() {
export function useSendConnection() { export function useSendConnection() {
const context = useContext(ConnectionContext); const context = useContext(ConnectionContext);
if (!context) { if (!context) {
throw new Error('Missing connection context') throw new Error('Missing connection context');
} }
return context.sendConnection; return context.sendConnection;
} }
@ -90,18 +106,22 @@ export function useSendConnection() {
export function useConnectionConfig() { export function useConnectionConfig() {
const context = useContext(ConnectionContext); const context = useContext(ConnectionContext);
if (!context) { if (!context) {
throw new Error('Missing connection context') throw new Error('Missing connection context');
} }
return { return {
endpoint: context.endpoint, endpoint: context.endpoint,
endpointInfo: context.availableEndpoints.find(info => info.endpoint === context.endpoint), endpointInfo: context.availableEndpoints.find(
(info) => info.endpoint === context.endpoint,
),
setEndpoint: context.setEndpoint, setEndpoint: context.setEndpoint,
availableEndpoints: context.availableEndpoints, availableEndpoints: context.availableEndpoints,
setCustomEndpoints: context.setCustomEndpoints, setCustomEndpoints: context.setCustomEndpoints,
}; };
} }
export function useAccountInfo(publicKey: PublicKey | undefined | null): [AccountInfo<Buffer> | null | undefined, boolean] { export function useAccountInfo(
publicKey: PublicKey | undefined | null,
): [AccountInfo<Buffer> | null | undefined, boolean] {
const connection = useConnection(); const connection = useConnection();
const cacheKey = tuple(connection, publicKey?.toBase58()); const cacheKey = tuple(connection, publicKey?.toBase58());
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>( const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(

View File

@ -103,7 +103,7 @@ class FetchLoopInternal<T = any> {
try { try {
const data = await this.fn(); const data = await this.fn();
if (!this.cacheNullValues && data === null) { 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 // cached data has not changed so no need to re-render
this.errors = 0; this.errors = 0;
return data; return data;

View File

@ -1187,8 +1187,8 @@ export function getExpectedFillPrice(
cost: number, cost: number,
tickSizeDecimals?: number, tickSizeDecimals?: number,
) { ) {
let spentCost = 0.; let spentCost = 0;
let avgPrice = 0.; let avgPrice = 0;
let price, sizeAtLevel, costAtLevel: number; let price, sizeAtLevel, costAtLevel: number;
for ([price, sizeAtLevel] of orderbook.getL2(1000)) { for ([price, sizeAtLevel] of orderbook.getL2(1000)) {
costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel; costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel;

View File

@ -3,11 +3,18 @@ import { useLocalStorageState } from './utils';
import { useInterval } from './useInterval'; import { useInterval } from './useInterval';
import { useConnection } from './connection'; import { useConnection } from './connection';
import { useWallet } from './wallet'; import { useWallet } from './wallet';
import {useAllMarkets, useTokenAccounts, useMarket, useSelectedTokenAccounts} from './markets'; import {
useAllMarkets,
useTokenAccounts,
useMarket,
useSelectedTokenAccounts,
} from './markets';
import { settleAllFunds } from './send'; import { settleAllFunds } from './send';
import {PreferencesContextValues} from "./types"; import { PreferencesContextValues } from './types';
const PreferencesContext = React.createContext<PreferencesContextValues | null>(null); const PreferencesContext = React.createContext<PreferencesContextValues | null>(
null,
);
export function PreferencesProvider({ children }) { export function PreferencesProvider({ children }) {
const [autoSettleEnabled, setAutoSettleEnabled] = useLocalStorageState( const [autoSettleEnabled, setAutoSettleEnabled] = useLocalStorageState(
@ -27,7 +34,13 @@ export function PreferencesProvider({ children }) {
const markets = (marketList || []).map((m) => m.market); const markets = (marketList || []).map((m) => m.market);
try { try {
console.log('Auto settling'); console.log('Auto settling');
await settleAllFunds({ connection, wallet, tokenAccounts: (tokenAccounts || []), markets, selectedTokenAccounts }); await settleAllFunds({
connection,
wallet,
tokenAccounts: tokenAccounts || [],
markets,
selectedTokenAccounts,
});
} catch (e) { } catch (e) {
console.log('Error auto settling funds: ' + e.message); console.log('Error auto settling funds: ' + e.message);
} }
@ -51,7 +64,7 @@ export function PreferencesProvider({ children }) {
export function usePreferences() { export function usePreferences() {
const context = useContext(PreferencesContext); const context = useContext(PreferencesContext);
if (!context) { if (!context) {
throw new Error('Missing preferences context') throw new Error('Missing preferences context');
} }
return { return {
autoSettleEnabled: context.autoSettleEnabled, autoSettleEnabled: context.autoSettleEnabled,

View File

@ -2,10 +2,14 @@ import { notify } from './notifications';
import { getDecimalCount, sleep } from './utils'; import { getDecimalCount, sleep } from './utils';
import { getSelectedTokenAccountForMint } from './markets'; import { getSelectedTokenAccountForMint } from './markets';
import { import {
Account, AccountInfo, Connection, Account,
PublicKey, RpcResponseAndContext, AccountInfo,
Connection,
PublicKey,
RpcResponseAndContext,
SystemProgram, SystemProgram,
Transaction, TransactionSignature, Transaction,
TransactionSignature,
} from '@solana/web3.js'; } from '@solana/web3.js';
import { BN } from 'bn.js'; import { BN } from 'bn.js';
import { import {
@ -15,18 +19,18 @@ import {
TokenInstructions, TokenInstructions,
OpenOrders, OpenOrders,
} from '@project-serum/serum'; } from '@project-serum/serum';
import Wallet from "@project-serum/sol-wallet-adapter"; import Wallet from '@project-serum/sol-wallet-adapter';
import {SelectedTokenAccounts, TokenAccount} from "./types"; import { SelectedTokenAccounts, TokenAccount } from './types';
import {Order} from "@project-serum/serum/lib/market"; import { Order } from '@project-serum/serum/lib/market';
import {Buffer} from "buffer"; import { Buffer } from 'buffer';
import assert from "assert"; import assert from 'assert';
import { struct } from "superstruct"; import { struct } from 'superstruct';
export async function createTokenAccountTransaction({ export async function createTokenAccountTransaction({
connection, connection,
wallet, wallet,
mintPublicKey, mintPublicKey,
} : { }: {
connection: Connection; connection: Connection;
wallet: Wallet; wallet: Wallet;
mintPublicKey: PublicKey; mintPublicKey: PublicKey;
@ -64,7 +68,7 @@ export async function settleFunds({
wallet, wallet,
baseCurrencyAccount, baseCurrencyAccount,
quoteCurrencyAccount, quoteCurrencyAccount,
} : { }: {
market: Market; market: Market;
openOrders: OpenOrders; openOrders: OpenOrders;
connection: Connection; connection: Connection;
@ -110,8 +114,8 @@ export async function settleFunds({
} }
let referrerQuoteWallet: PublicKey | null = null; let referrerQuoteWallet: PublicKey | null = null;
if (market.supportsReferralFees) { if (market.supportsReferralFees) {
const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT') const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT');
const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC') const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC');
if ( if (
process.env.REACT_APP_USDT_REFERRAL_FEES_ADDRESS && process.env.REACT_APP_USDT_REFERRAL_FEES_ADDRESS &&
usdt && usdt &&
@ -164,7 +168,7 @@ export async function settleAllFunds({
tokenAccounts, tokenAccounts,
markets, markets,
selectedTokenAccounts, selectedTokenAccounts,
} : { }: {
connection: Connection; connection: Connection;
wallet: Wallet; wallet: Wallet;
tokenAccounts: TokenAccount[]; tokenAccounts: TokenAccount[];
@ -209,57 +213,72 @@ export async function settleAllFunds({
[], [],
); );
const settleTransactions = (await Promise.all( const settleTransactions = (
openOrdersAccounts.map((openOrdersAccount) => { await Promise.all(
const market = markets.find((m) => openOrdersAccounts.map((openOrdersAccount) => {
// @ts-ignore const market = markets.find((m) =>
m._decoded?.ownAddress?.equals(openOrdersAccount.market), // @ts-ignore
); m._decoded?.ownAddress?.equals(openOrdersAccount.market),
const baseMint = market?.baseMintAddress; );
const quoteMint = market?.quoteMintAddress; const baseMint = market?.baseMintAddress;
const quoteMint = market?.quoteMintAddress;
const selectedBaseTokenAccount = getSelectedTokenAccountForMint( const selectedBaseTokenAccount = getSelectedTokenAccountForMint(
tokenAccounts, tokenAccounts,
baseMint, baseMint,
baseMint && selectedTokenAccounts && selectedTokenAccounts[baseMint.toBase58()] baseMint &&
)?.pubkey; selectedTokenAccounts &&
const selectedQuoteTokenAccount = getSelectedTokenAccountForMint( selectedTokenAccounts[baseMint.toBase58()],
tokenAccounts, )?.pubkey;
quoteMint, const selectedQuoteTokenAccount = getSelectedTokenAccountForMint(
quoteMint && selectedTokenAccounts && selectedTokenAccounts[quoteMint.toBase58()] tokenAccounts,
)?.pubkey; quoteMint,
if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) { quoteMint &&
return null; selectedTokenAccounts &&
} selectedTokenAccounts[quoteMint.toBase58()],
return ( )?.pubkey;
market && if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) {
market.makeSettleFundsTransaction( return null;
connection, }
openOrdersAccount, return (
selectedBaseTokenAccount, market &&
selectedQuoteTokenAccount, market.makeSettleFundsTransaction(
) connection,
); openOrdersAccount,
}), selectedBaseTokenAccount,
)).filter((x): x is {signers: [PublicKey | Account]; transaction: Transaction} => !!x); selectedQuoteTokenAccount,
)
);
}),
)
).filter(
(x): x is { signers: [PublicKey | Account]; transaction: Transaction } =>
!!x,
);
if (!settleTransactions || settleTransactions.length === 0) return; if (!settleTransactions || settleTransactions.length === 0) return;
const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction); const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction);
const signers: Array<Account | PublicKey> = []; const signers: Array<Account | PublicKey> = [];
settleTransactions settleTransactions
.reduce((cumulative: Array<Account | PublicKey>, t) => cumulative.concat(t.signers), []) .reduce(
(cumulative: Array<Account | PublicKey>, t) =>
cumulative.concat(t.signers),
[],
)
.forEach((signer) => { .forEach((signer) => {
if (!signers.find((s) => { if (
if (s.constructor.name !== signer.constructor.name) { !signers.find((s) => {
return false; if (s.constructor.name !== signer.constructor.name) {
} else if (s.constructor.name === 'PublicKey') { return false;
// @ts-ignore } else if (s.constructor.name === 'PublicKey') {
return s.equals(signer); // @ts-ignore
} else { return s.equals(signer);
// @ts-ignore } else {
return s.publicKey.equals(signer.publicKey); // @ts-ignore
} return s.publicKey.equals(signer.publicKey);
})) { }
})
) {
signers.push(signer); 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] }); return cancelOrders({ ...params, orders: [params.order] });
} }
export async function cancelOrders({ market, wallet, connection, orders }: { export async function cancelOrders({
market,
wallet,
connection,
orders,
}: {
market: Market; market: Market;
wallet: Wallet; wallet: Wallet;
connection: Connection; connection: Connection;
@ -310,10 +339,10 @@ export async function placeOrder({
baseCurrencyAccount, baseCurrencyAccount,
quoteCurrencyAccount, quoteCurrencyAccount,
}: { }: {
side: "buy" | "sell"; side: 'buy' | 'sell';
price: number; price: number;
size: number; size: number;
orderType: "ioc" | "postOnly" | "limit"; orderType: 'ioc' | 'postOnly' | 'limit';
market: Market | undefined | null; market: Market | undefined | null;
connection: Connection; connection: Connection;
wallet: Wallet; wallet: Wallet;
@ -417,7 +446,7 @@ export async function listMarket({
baseLotSize, baseLotSize,
quoteLotSize, quoteLotSize,
dexProgramId, dexProgramId,
} : { }: {
connection: Connection; connection: Connection;
wallet: Wallet; wallet: Wallet;
baseMint: PublicKey; baseMint: PublicKey;
@ -577,7 +606,7 @@ async function sendTransaction({
sentMessage = 'Transaction sent', sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed', successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT, timeout = DEFAULT_TIMEOUT,
} : { }: {
transaction: Transaction; transaction: Transaction;
wallet: Wallet; wallet: Wallet;
signers?: Array<PublicKey | Account>; signers?: Array<PublicKey | Account>;
@ -608,7 +637,7 @@ async function signTransaction({
wallet, wallet,
signers = [wallet.publicKey], signers = [wallet.publicKey],
connection, connection,
} : { }: {
transaction: Transaction; transaction: Transaction;
wallet: Wallet; wallet: Wallet;
signers: Array<Account | PublicKey>; signers: Array<Account | PublicKey>;
@ -628,7 +657,7 @@ async function sendSignedTransaction({
sentMessage = 'Transaction sent', sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed', successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT, timeout = DEFAULT_TIMEOUT,
} : { }: {
signedTransaction: Transaction; signedTransaction: Transaction;
connection: Connection; connection: Connection;
sendingMessage?: string; sendingMessage?: string;
@ -639,9 +668,12 @@ async function sendSignedTransaction({
const rawTransaction = signedTransaction.serialize(); const rawTransaction = signedTransaction.serialize();
const startTime = getUnixTs(); const startTime = getUnixTs();
notify({ message: sendingMessage }); notify({ message: sendingMessage });
const txid: TransactionSignature = await connection.sendRawTransaction(rawTransaction, { const txid: TransactionSignature = await connection.sendRawTransaction(
skipPreflight: true, rawTransaction,
}); {
skipPreflight: true,
},
);
notify({ message: sentMessage, type: 'success', txid }); notify({ message: sentMessage, type: 'success', txid });
console.log('Started awaiting confirmation for', txid); console.log('Started awaiting confirmation for', txid);
@ -754,17 +786,17 @@ function mergeTransactions(transactions: (Transaction | undefined)[]) {
} }
function jsonRpcResult(resultDescription: any) { function jsonRpcResult(resultDescription: any) {
const jsonRpcVersion = struct.literal("2.0"); const jsonRpcVersion = struct.literal('2.0');
return struct.union([ return struct.union([
struct({ struct({
jsonrpc: jsonRpcVersion, jsonrpc: jsonRpcVersion,
id: "string", id: 'string',
error: "any", error: 'any',
}), }),
struct({ struct({
jsonrpc: jsonRpcVersion, jsonrpc: jsonRpcVersion,
id: "string", id: 'string',
error: "null?", error: 'null?',
result: resultDescription, result: resultDescription,
}), }),
]); ]);
@ -773,46 +805,43 @@ function jsonRpcResult(resultDescription: any) {
function jsonRpcResultAndContext(resultDescription: any) { function jsonRpcResultAndContext(resultDescription: any) {
return jsonRpcResult({ return jsonRpcResult({
context: struct({ context: struct({
slot: "number", slot: 'number',
}), }),
value: resultDescription, value: resultDescription,
}); });
} }
const AccountInfoResult = struct({ const AccountInfoResult = struct({
executable: "boolean", executable: 'boolean',
owner: "string", owner: 'string',
lamports: "number", lamports: 'number',
data: "any", data: 'any',
rentEpoch: "number?", rentEpoch: 'number?',
}); });
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext( export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
struct.array([struct.union(["null", AccountInfoResult])]) struct.array([struct.union(['null', AccountInfoResult])]),
); );
export async function getMultipleSolanaAccounts( export async function getMultipleSolanaAccounts(
connection: Connection, connection: Connection,
publicKeys: PublicKey[] publicKeys: PublicKey[],
): Promise< ): Promise<
RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }> RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }>
> { > {
const args = [ const args = [publicKeys.map((k) => k.toBase58()), { commitment: 'recent' }];
publicKeys.map((k) => k.toBase58()),
{ commitment: "recent" },
];
// @ts-ignore // @ts-ignore
const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args); const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
const res = GetMultipleAccountsAndContextRpcResult(unsafeRes); const res = GetMultipleAccountsAndContextRpcResult(unsafeRes);
if (res.error) { if (res.error) {
throw new Error( throw new Error(
"failed to get info about accounts " + 'failed to get info about accounts ' +
publicKeys.map((k) => k.toBase58()).join(", ") + publicKeys.map((k) => k.toBase58()).join(', ') +
": " + ': ' +
res.error.message res.error.message,
); );
} }
assert(typeof res.result !== "undefined"); assert(typeof res.result !== 'undefined');
const accounts: Array<{ const accounts: Array<{
executable: any; executable: any;
owner: PublicKey; owner: PublicKey;
@ -828,12 +857,12 @@ export async function getMultipleSolanaAccounts(
} | null = null; } | null = null;
if (res.result.value) { if (res.result.value) {
const { executable, owner, lamports, data } = account; const { executable, owner, lamports, data } = account;
assert(data[1] === "base64"); assert(data[1] === 'base64');
value = { value = {
executable, executable,
owner: new PublicKey(owner), owner: new PublicKey(owner),
lamports, lamports,
data: Buffer.from(data[0], "base64"), data: Buffer.from(data[0], 'base64'),
}; };
} }
accounts.push(value); accounts.push(value);
@ -843,7 +872,7 @@ export async function getMultipleSolanaAccounts(
slot: res.result.context.slot, slot: res.result.context.slot,
}, },
value: Object.fromEntries( value: Object.fromEntries(
accounts.map((account, i) => [publicKeys[i].toBase58(), account]) accounts.map((account, i) => [publicKeys[i].toBase58(), account]),
), ),
}; };
} }

View File

@ -1,13 +1,13 @@
import * as BufferLayout from 'buffer-layout'; import * as BufferLayout from 'buffer-layout';
import bs58 from 'bs58'; import bs58 from 'bs58';
import {AccountInfo, Connection, PublicKey} from '@solana/web3.js'; import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import {WRAPPED_SOL_MINT} from '@project-serum/serum/lib/token-instructions'; import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
import {TokenAccount} from "./types"; import { TokenAccount } from './types';
import {TOKEN_MINTS} from "@project-serum/serum"; import { TOKEN_MINTS } from '@project-serum/serum';
import {useAllMarkets, useMarket, useTokenAccounts} from "./markets"; import { useAllMarkets, useMarket, useTokenAccounts } from './markets';
import {getMultipleSolanaAccounts} from "./send"; import { getMultipleSolanaAccounts } from './send';
import {useConnection} from "./connection"; import { useConnection } from './connection';
import {useAsyncData} from "./fetch-loop"; import { useAsyncData } from './fetch-loop';
import tuple from 'immutable-tuple'; import tuple from 'immutable-tuple';
export const ACCOUNT_LAYOUT = BufferLayout.struct([ export const ACCOUNT_LAYOUT = BufferLayout.struct([
@ -25,7 +25,7 @@ export const MINT_LAYOUT = BufferLayout.struct([
]); ]);
export function parseTokenAccountData( export function parseTokenAccountData(
data: Buffer data: Buffer,
): { mint: PublicKey; owner: PublicKey; amount: number } { ): { mint: PublicKey; owner: PublicKey; amount: number } {
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data); let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
return { return {
@ -59,8 +59,9 @@ export const TOKEN_PROGRAM_ID = new PublicKey(
); );
export async function getOwnedTokenAccounts( export async function getOwnedTokenAccounts(
connection: Connection, publicKey: PublicKey connection: Connection,
): Promise<Array<{publicKey: PublicKey, accountInfo: AccountInfo<Buffer>}>> { publicKey: PublicKey,
): Promise<Array<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }>> {
let filters = getOwnedAccountsFilters(publicKey); let filters = getOwnedAccountsFilters(publicKey);
// @ts-ignore // @ts-ignore
let resp = await connection._rpcRequest('getProgramAccounts', [ 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([ let [splAccounts, account] = await Promise.all([
getOwnedTokenAccounts(connection, ownerAddress), getOwnedTokenAccounts(connection, ownerAddress),
connection.getAccountInfo(ownerAddress), connection.getAccountInfo(ownerAddress),
]); ]);
const parsedSplAccounts: TokenAccount[] = splAccounts.map(({ publicKey, accountInfo }) => { const parsedSplAccounts: TokenAccount[] = splAccounts.map(
return { ({ publicKey, accountInfo }) => {
pubkey: publicKey, return {
account: accountInfo, pubkey: publicKey,
effectiveMint: parseTokenAccountData(accountInfo.data).mint, account: accountInfo,
}; effectiveMint: parseTokenAccountData(accountInfo.data).mint,
}); };
},
);
return parsedSplAccounts.concat({ return parsedSplAccounts.concat({
pubkey: ownerAddress, pubkey: ownerAddress,
account, 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 { customMarkets } = useMarket();
const [markets] = useAllMarkets(customMarkets); const [markets] = useAllMarkets(customMarkets);
const mintsToTickers = Object.fromEntries(TOKEN_MINTS.map(mint => [mint.address.toBase58(), mint.name])); const mintsToTickers = Object.fromEntries(
for (let market of (markets || [])) { TOKEN_MINTS.map((mint) => [mint.address.toBase58(), mint.name]),
);
for (let market of markets || []) {
const customMarketInfo = customMarkets.find( 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 (!(market.market.baseMintAddress.toBase58() in mintsToTickers)) {
if (customMarketInfo) { 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 (!(market.market.quoteMintAddress.toBase58() in mintsToTickers)) {
if (customMarketInfo) { 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; const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
export function useMintInfos(): [ 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 connection = useConnection();
const {customMarkets} = useMarket(); const { customMarkets } = useMarket();
const [tokenAccounts] = useTokenAccounts(); const [tokenAccounts] = useTokenAccounts();
const [allMarkets] = useAllMarkets(customMarkets); const [allMarkets] = useAllMarkets(customMarkets);
const allMints = (tokenAccounts || []).map(account => account.effectiveMint).concat( const allMints = (tokenAccounts || [])
(allMarkets || []).map(marketInfo => marketInfo.market.baseMintAddress) .map((account) => account.effectiveMint)
).concat( .concat(
(allMarkets || []).map(marketInfo => marketInfo.market.quoteMintAddress) (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 getAllMintInfo = async () => {
const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints); const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints);
return Object.fromEntries(Object.entries(mintInfos.value).map( return Object.fromEntries(
([key, accountInfo]) => [ Object.entries(mintInfos.value).map(([key, accountInfo]) => [
key, key,
accountInfo && parseTokenMintData(accountInfo.data) accountInfo && parseTokenMintData(accountInfo.data),
] ]),
)); );
} };
return useAsyncData( return useAsyncData(
getAllMintInfo, getAllMintInfo,

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { PublicKey } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
export function isValidPublicKey(key) { export function isValidPublicKey(key) {
if (!key) { if (!key) {
@ -24,11 +24,17 @@ export const percentFormat = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 2, 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; 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; return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value;
} }
@ -66,7 +72,7 @@ export function useLocalStorageStringState(
localStorageListeners[key].push(notify); localStorageListeners[key].push(notify);
return () => { return () => {
localStorageListeners[key] = localStorageListeners[key].filter( localStorageListeners[key] = localStorageListeners[key].filter(
listener => listener !== notify, (listener) => listener !== notify,
); );
if (localStorageListeners[key].length === 0) { if (localStorageListeners[key].length === 0) {
delete localStorageListeners[key]; delete localStorageListeners[key];
@ -75,7 +81,7 @@ export function useLocalStorageStringState(
}, [key]); }, [key]);
const setState = useCallback<(newState: string | null) => void>( const setState = useCallback<(newState: string | null) => void>(
newState => { (newState) => {
const changed = state !== newState; const changed = state !== newState;
if (!changed) { if (!changed) {
return; return;
@ -86,7 +92,9 @@ export function useLocalStorageStringState(
} else { } else {
localStorage.setItem(key, newState); localStorage.setItem(key, newState);
} }
localStorageListeners[key].forEach(listener => listener(key + '\n' + newState)); localStorageListeners[key].forEach((listener) =>
listener(key + '\n' + newState),
);
}, },
[state, key], [state, key],
); );
@ -94,9 +102,18 @@ export function useLocalStorageStringState(
return [state, setState]; return [state, setState];
} }
export function useLocalStorageState<T = any>(key: string, defaultState: T | null = null): [T, (newState: T) => void] { export function useLocalStorageState<T = any>(
let [stringState, setStringState] = useLocalStorageStringState(key, JSON.stringify(defaultState)); key: string,
return [stringState && JSON.parse(stringState), newState => setStringState(JSON.stringify(newState))]; 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) { export function useEffectAfterTimeout(effect, timeout) {

View File

@ -3,7 +3,7 @@ import Wallet from '@project-serum/sol-wallet-adapter';
import { notify } from './notifications'; import { notify } from './notifications';
import { useConnectionConfig } from './connection'; import { useConnectionConfig } from './connection';
import { useLocalStorageState } from './utils'; import { useLocalStorageState } from './utils';
import {WalletContextValues} from "./types"; import { WalletContextValues } from './types';
export const WALLET_PROVIDERS = [ export const WALLET_PROVIDERS = [
{ name: 'sollet.io', url: 'https://www.sollet.io' }, { name: 'sollet.io', url: 'https://www.sollet.io' },
@ -83,7 +83,7 @@ export function WalletProvider({ children }) {
export function useWallet() { export function useWallet() {
const context = useContext(WalletContext); const context = useContext(WalletContext);
if (!context) { if (!context) {
throw new Error('Missing wallet context') throw new Error('Missing wallet context');
} }
return { return {
connected: context.connected, connected: context.connected,

View File

@ -16,17 +16,8 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react", "jsx": "react",
"lib": [ "lib": ["dom", "esnext"]
"dom",
"esnext"
]
}, },
"include": [ "include": ["./src/"],
"./src/" "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"]
],
"exclude": [
"./src/**/*.test.js",
"node_modules",
"**/node_modules"
]
} }