parent
99fbbed7d5
commit
063cae46df
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 !== '';
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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('')}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue