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",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"prettier": "prettier --write ."
},
"eslintConfig": {
"extends": "react-app"

View File

@ -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<string | undefined>(undefined);
const [toToken, setToToken] = useState<string | 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();
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 (
<FloatingElement style={{ maxWidth: 500 }}>
@ -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 (
<React.Fragment>
@ -346,12 +378,12 @@ function ConvertFormSubmit({
</Col>
</Row>
{canConvert && (
<Row align="middle" justify="center">
<Col >
<Row align="middle" justify="center">
<Col>
{fromAmount} {fromToken}
</Col>
<Col offset={1}>
<SwapOutlined/>
<SwapOutlined />
</Col>
<Col offset={1}>
{toAmount} {toToken}

View File

@ -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 !== '';

View File

@ -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 (
<div>
{title && <p style={{ color: 'white' }}>{title}</p>}

View File

@ -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 (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
{formattedBalances.map(([currency, balances, baseOrQuote, mint], index) => (
<React.Fragment key={index}>
<Divider style={{ borderColor: 'white' }}>{currency}</Divider>
{connected && (
{formattedBalances.map(
([currency, balances, baseOrQuote, mint], index) => (
<React.Fragment key={index}>
<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
align="middle"
style={{ paddingBottom: 10 }}
justify="space-between"
style={{ paddingBottom: 12 }}
>
<StandaloneTokenAccountsSelect
accounts={tokenAccounts?.filter(account => account.effectiveMint.toBase58() === mint)}
mint={mint}
label
/>
<Col>Wallet balance:</Col>
<Col>{balances && balances.wallet}</Col>
</RowBox>
)}
<RowBox
align="middle"
justify="space-between"
style={{ paddingBottom: 12 }}
>
<Col>Wallet balance:</Col>
<Col>{balances && balances.wallet}</Col>
</RowBox>
<RowBox
align="middle"
justify="space-between"
style={{ paddingBottom: 12 }}
>
<Col>Unsettled balance:</Col>
<Col>{balances && balances.unsettled}</Col>
</RowBox>
<RowBox align="middle" justify="space-around">
<Col style={{ width: 150 }}>
<ActionButton
block
size="large"
onClick={() => setBaseOrQuote(baseOrQuote)}
>
Deposit
</ActionButton>
</Col>
<Col style={{ width: 150 }}>
<ActionButton block size="large" onClick={onSettleFunds}>
Settle
</ActionButton>
</Col>
</RowBox>
<Tip>
All deposits go to your{' '}
<Link external to={providerUrl}>
{providerName}
</Link>{' '}
wallet
</Tip>
</React.Fragment>
))}
<RowBox
align="middle"
justify="space-between"
style={{ paddingBottom: 12 }}
>
<Col>Unsettled balance:</Col>
<Col>{balances && balances.unsettled}</Col>
</RowBox>
<RowBox align="middle" justify="space-around">
<Col style={{ width: 150 }}>
<ActionButton
block
size="large"
onClick={() => setBaseOrQuote(baseOrQuote)}
>
Deposit
</ActionButton>
</Col>
<Col style={{ width: 150 }}>
<ActionButton block size="large" onClick={onSettleFunds}>
Settle
</ActionButton>
</Col>
</RowBox>
<Tip>
All deposits go to your{' '}
<Link external to={providerUrl}>
{providerName}
</Link>{' '}
wallet
</Tip>
</React.Fragment>
),
)}
<DepositDialog
baseOrQuote={baseOrQuote}
onClose={() => setBaseOrQuote('')}

View File

@ -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 (
<React.Fragment>
{label &&
<Col span={8}>
Token account:
</Col>
}
{label && <Col span={8}>Token account:</Col>}
<Col span={label ? 13 : 21}>
<Select
style={{ width: '100%' }}
value={selectedValue}
onSelect={setTokenAccountForCoin}
>
{accounts?.map(account => (
<Select.Option key={account.pubkey.toBase58()} value={account.pubkey.toBase58()}>
<Typography.Text code>{label ? abbreviateAddress(account.pubkey, 8) : account.pubkey.toBase58()}</Typography.Text>
</Select.Option>)
)}
{accounts?.map((account) => (
<Select.Option
key={account.pubkey.toBase58()}
value={account.pubkey.toBase58()}
>
<Typography.Text code>
{label
? abbreviateAddress(account.pubkey, 8)
: account.pubkey.toBase58()}
</Typography.Text>
</Select.Option>
))}
</Select>
</Col>
<Col span={2} offset={1}>
@ -65,7 +71,9 @@ export default function StandaloneTokenAccountsSelect({
shape="round"
icon={<CopyOutlined />}
size={'small'}
onClick={() => selectedValue && navigator.clipboard.writeText(selectedValue)}
onClick={() =>
selectedValue && navigator.clipboard.writeText(selectedValue)
}
/>
</Col>
</React.Fragment>

View File

@ -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 && <Menu.Item key="/orders">ORDERS</Menu.Item>}
{connected && <Menu.Item key="/convert">CONVERT</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">
<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
</a>
</Menu.Item>
<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
</a>
</Menu.Item>
<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
</a>
</Menu.Item>
<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
</a>
</Menu.Item>
<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
</a>
</Menu.Item>
<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
</a>
</Menu.Item>
@ -182,7 +225,7 @@ export default function TopBar() {
onClick={() => setAddEndpointVisible(true)}
/>
</Col>
<Col>
<Col>
<Popover
content={endpoint}
placement="bottomRight"
@ -232,7 +275,7 @@ export default function TopBar() {
</Select>
</div>
<div>
<WalletConnect/>
<WalletConnect />
</div>
</Wrapper>
</>

View File

@ -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;

View File

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

View File

@ -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,

View File

@ -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) => (
<Row align="middle" style={{width: "430px"}}>
<Row align="middle" style={{ width: '430px' }}>
<StandaloneTokenAccountsSelect
accounts={tokenAccounts?.filter(t => t.effectiveMint.toBase58() === walletBalance.mint)}
accounts={tokenAccounts?.filter(
(t) => t.effectiveMint.toBase58() === walletBalance.mint,
)}
mint={walletBalance.mint}
/>
</Row>
@ -111,14 +118,11 @@ export default function WalletBalancesTable({
columns={columns}
pagination={false}
/>
{connected &&
<Button
onClick={onSettleFunds}
loading={settlingFunds}
>
{connected && (
<Button onClick={onSettleFunds} loading={settlingFunds}>
Settle all funds
</Button>
}
)}
</React.Fragment>
);
}

View File

@ -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();

22
src/declarations.d.ts vendored
View File

@ -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<void>;
disconnect: () => void;
signTransaction: (transaction: Transaction) => Promise<Transaction>;
}
export default class Wallet extends EventEmitter {
constructor(providerUrl: string, network: string);
publicKey: PublicKey;
connected: boolean;
autoApprove: boolean;
connect: () => Promise<void>;
disconnect: () => void;
signTransaction: (transaction: Transaction) => Promise<Transaction>;
}
}

View File

@ -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 (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
<Tabs defaultActiveKey="walletBalances">
<TabPane tab="Wallet Balances" key="walletBalances">
<WalletBalancesTable
walletBalances={data}
/>
<WalletBalancesTable walletBalances={data} />
</TabPane>
</Tabs>
</FloatingElement>

View File

@ -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 (
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
<Button
onClick={refreshOpenOrders}
loading={!loaded}
>
<Button onClick={refreshOpenOrders} loading={!loaded}>
Refresh
</Button>
<OpenOrderTable

View File

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

View File

@ -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;

View File

@ -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<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(null);
const ConnectionContext: React.Context<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(
null,
);
export function ConnectionProvider({ children }) {
const [endpoint, setEndpoint] = useLocalStorageState<string>(
'connectionEndpts',
ENDPOINTS[0].endpoint,
);
const [customEndpoints, setCustomEndpoints] = useLocalStorageState<EndpointInfo[]>(
'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 (
<ConnectionContext.Provider
value={{ endpoint, setEndpoint, connection, sendConnection, availableEndpoints, setCustomEndpoints }}
value={{
endpoint,
setEndpoint,
connection,
sendConnection,
availableEndpoints,
setCustomEndpoints,
}}
>
{children}
</ConnectionContext.Provider>
@ -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<Buffer> | null | undefined, boolean] {
export function useAccountInfo(
publicKey: PublicKey | undefined | null,
): [AccountInfo<Buffer> | null | undefined, boolean] {
const connection = useConnection();
const cacheKey = tuple(connection, publicKey?.toBase58());
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(

View File

@ -103,7 +103,7 @@ class FetchLoopInternal<T = any> {
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;

View File

@ -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;

View File

@ -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<PreferencesContextValues | null>(null);
const PreferencesContext = React.createContext<PreferencesContextValues | null>(
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,

View File

@ -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<Account | PublicKey> = [];
settleTransactions
.reduce((cumulative: Array<Account | PublicKey>, t) => cumulative.concat(t.signers), [])
.reduce(
(cumulative: Array<Account | PublicKey>, 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<PublicKey | Account>;
@ -608,7 +637,7 @@ async function signTransaction({
wallet,
signers = [wallet.publicKey],
connection,
} : {
}: {
transaction: Transaction;
wallet: Wallet;
signers: Array<Account | PublicKey>;
@ -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<Buffer> | 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]),
),
};
}

View File

@ -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<Array<{publicKey: PublicKey, accountInfo: AccountInfo<Buffer>}>> {
connection: Connection,
publicKey: PublicKey,
): Promise<Array<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }>> {
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,

View File

@ -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<T = any>(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<T = any>(
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) {

View File

@ -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,

View File

@ -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"]
}