parent
99fbbed7d5
commit
063cae46df
|
@ -38,7 +38,8 @@
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"prettier": "prettier --write ."
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
|
|
@ -19,9 +19,9 @@ import {placeOrder} from '../utils/send';
|
||||||
import { floorToDecimal, getDecimalCount } from '../utils/utils';
|
import { floorToDecimal, getDecimalCount } from '../utils/utils';
|
||||||
import FloatingElement from './layout/FloatingElement';
|
import FloatingElement from './layout/FloatingElement';
|
||||||
import WalletConnect from './WalletConnect';
|
import WalletConnect from './WalletConnect';
|
||||||
import {SwapOutlined} from "@ant-design/icons";
|
import { SwapOutlined } from '@ant-design/icons';
|
||||||
import {CustomMarketInfo} from "../utils/types";
|
import { CustomMarketInfo } from '../utils/types';
|
||||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
import Wallet from '@project-serum/sol-wallet-adapter';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
@ -40,41 +40,55 @@ const ConvertButton = styled(Button)`
|
||||||
export default function ConvertForm() {
|
export default function ConvertForm() {
|
||||||
const { connected, wallet } = useWallet();
|
const { connected, wallet } = useWallet();
|
||||||
const { customMarkets } = useMarket();
|
const { customMarkets } = useMarket();
|
||||||
const marketInfos = getMarketInfos(customMarkets)
|
const marketInfos = getMarketInfos(customMarkets);
|
||||||
const { market, setMarketAddress } = useMarket();
|
const { market, setMarketAddress } = useMarket();
|
||||||
|
|
||||||
const [fromToken, setFromToken] = useState<string | undefined>(undefined);
|
const [fromToken, setFromToken] = useState<string | undefined>(undefined);
|
||||||
const [toToken, setToToken] = useState<string | undefined>(undefined);
|
const [toToken, setToToken] = useState<string | undefined>(undefined);
|
||||||
const [size, setSize] = useState<number | undefined>(undefined);
|
const [size, setSize] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const marketInfosbyName = Object.fromEntries(marketInfos.map(market => [market.name, market]));
|
const marketInfosbyName = Object.fromEntries(
|
||||||
|
marketInfos.map((market) => [market.name, market]),
|
||||||
|
);
|
||||||
|
|
||||||
const tokenConvertMap: Map<string, Set<string>> = new Map();
|
const tokenConvertMap: Map<string, Set<string>> = new Map();
|
||||||
Object.keys(marketInfosbyName).forEach((market) => {
|
Object.keys(marketInfosbyName).forEach((market) => {
|
||||||
let [base, quote] = market.split('/');
|
let [base, quote] = market.split('/');
|
||||||
!tokenConvertMap.has(base)
|
!tokenConvertMap.has(base)
|
||||||
? tokenConvertMap.set(base, new Set([quote]))
|
? tokenConvertMap.set(base, new Set([quote]))
|
||||||
: tokenConvertMap.set(base, new Set([...(tokenConvertMap.get(base) || []), quote]));
|
: tokenConvertMap.set(
|
||||||
|
base,
|
||||||
|
new Set([...(tokenConvertMap.get(base) || []), quote]),
|
||||||
|
);
|
||||||
!tokenConvertMap.has(quote)
|
!tokenConvertMap.has(quote)
|
||||||
? tokenConvertMap.set(quote, new Set([base]))
|
? tokenConvertMap.set(quote, new Set([base]))
|
||||||
: tokenConvertMap.set(quote, new Set([...(tokenConvertMap.get(quote) || []), base]));
|
: tokenConvertMap.set(
|
||||||
|
quote,
|
||||||
|
new Set([...(tokenConvertMap.get(quote) || []), base]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const setMarket = (toToken) => {
|
const setMarket = (toToken) => {
|
||||||
const marketInfo = marketInfos.filter(marketInfo => !marketInfo.deprecated).find(marketInfo =>
|
const marketInfo = marketInfos
|
||||||
marketInfo.name === `${fromToken}/${toToken}` || marketInfo.name === `${toToken}/${fromToken}`
|
.filter((marketInfo) => !marketInfo.deprecated)
|
||||||
|
.find(
|
||||||
|
(marketInfo) =>
|
||||||
|
marketInfo.name === `${fromToken}/${toToken}` ||
|
||||||
|
marketInfo.name === `${toToken}/${fromToken}`,
|
||||||
);
|
);
|
||||||
if (!marketInfo) {
|
if (!marketInfo) {
|
||||||
console.warn(`Could not find market info for market names ${fromToken}/${toToken} or ${toToken}/${fromToken}`);
|
console.warn(
|
||||||
|
`Could not find market info for market names ${fromToken}/${toToken} or ${toToken}/${fromToken}`,
|
||||||
|
);
|
||||||
notify({
|
notify({
|
||||||
message: 'Invalid market',
|
message: 'Invalid market',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMarketAddress(marketInfo.address.toBase58())
|
setMarketAddress(marketInfo.address.toBase58());
|
||||||
setToToken(toToken);
|
setToToken(toToken);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingElement style={{ maxWidth: 500 }}>
|
<FloatingElement style={{ maxWidth: 500 }}>
|
||||||
|
@ -148,7 +162,7 @@ function ConvertFormSubmit({
|
||||||
toToken,
|
toToken,
|
||||||
wallet,
|
wallet,
|
||||||
market,
|
market,
|
||||||
customMarkets
|
customMarkets,
|
||||||
}: {
|
}: {
|
||||||
size: number | null | undefined;
|
size: number | null | undefined;
|
||||||
setSize: (newSize: number | undefined) => void;
|
setSize: (newSize: number | undefined) => void;
|
||||||
|
@ -171,7 +185,9 @@ function ConvertFormSubmit({
|
||||||
const isFromTokenBaseOfMarket = (market) => {
|
const isFromTokenBaseOfMarket = (market) => {
|
||||||
const { marketName } = getMarketDetails(market, customMarkets);
|
const { marketName } = getMarketDetails(market, customMarkets);
|
||||||
if (!marketName) {
|
if (!marketName) {
|
||||||
throw Error('Cannot determine if coin is quote or base because marketName is missing');
|
throw Error(
|
||||||
|
'Cannot determine if coin is quote or base because marketName is missing',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const [base] = marketName.split('/');
|
const [base] = marketName.split('/');
|
||||||
return fromToken === base;
|
return fromToken === base;
|
||||||
|
@ -213,7 +229,9 @@ function ConvertFormSubmit({
|
||||||
const sidedOrderbookAccount =
|
const sidedOrderbookAccount =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
side === 'buy' ? market._decoded.asks : market._decoded.bids;
|
side === 'buy' ? market._decoded.asks : market._decoded.bids;
|
||||||
const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount);
|
const orderbookData = await connection.getAccountInfo(
|
||||||
|
sidedOrderbookAccount,
|
||||||
|
);
|
||||||
if (!orderbookData?.data) {
|
if (!orderbookData?.data) {
|
||||||
notify({ message: 'Invalid orderbook data', type: 'error' });
|
notify({ message: 'Invalid orderbook data', type: 'error' });
|
||||||
return;
|
return;
|
||||||
|
@ -232,7 +250,11 @@ function ConvertFormSubmit({
|
||||||
}
|
}
|
||||||
|
|
||||||
const tickSizeDecimals = getDecimalCount(market.tickSize);
|
const tickSizeDecimals = getDecimalCount(market.tickSize);
|
||||||
const parsedPrice = getMarketOrderPrice(decodedOrderbookData, size, tickSizeDecimals);
|
const parsedPrice = getMarketOrderPrice(
|
||||||
|
decodedOrderbookData,
|
||||||
|
size,
|
||||||
|
tickSizeDecimals,
|
||||||
|
);
|
||||||
|
|
||||||
// round size
|
// round size
|
||||||
const sizeDecimalCount = getDecimalCount(market.minOrderSize);
|
const sizeDecimalCount = getDecimalCount(market.minOrderSize);
|
||||||
|
@ -270,7 +292,9 @@ function ConvertFormSubmit({
|
||||||
const sidedOrderbookAccount =
|
const sidedOrderbookAccount =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
side === 'buy' ? market._decoded.asks : market._decoded.bids;
|
side === 'buy' ? market._decoded.asks : market._decoded.bids;
|
||||||
const orderbookData = await connection.getAccountInfo(sidedOrderbookAccount);
|
const orderbookData = await connection.getAccountInfo(
|
||||||
|
sidedOrderbookAccount,
|
||||||
|
);
|
||||||
if (!orderbookData?.data || !market) {
|
if (!orderbookData?.data || !market) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
@ -282,7 +306,11 @@ function ConvertFormSubmit({
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
const tickSizeDecimals = getDecimalCount(market.tickSize);
|
const tickSizeDecimals = getDecimalCount(market.tickSize);
|
||||||
const expectedPrice = getExpectedFillPrice(decodedOrderbookData, size, tickSizeDecimals)
|
const expectedPrice = getExpectedFillPrice(
|
||||||
|
decodedOrderbookData,
|
||||||
|
size,
|
||||||
|
tickSizeDecimals,
|
||||||
|
);
|
||||||
if (side === 'buy') {
|
if (side === 'buy') {
|
||||||
return [expectedPrice.toFixed(6), 1];
|
return [expectedPrice.toFixed(6), 1];
|
||||||
} else {
|
} else {
|
||||||
|
@ -292,21 +320,25 @@ function ConvertFormSubmit({
|
||||||
console.log(`Got error ${e}`);
|
console.log(`Got error ${e}`);
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
|
() => {
|
||||||
getPrice().then(([fromAmount, toAmount]) => {
|
getPrice().then(([fromAmount, toAmount]) => {
|
||||||
setFromAmount(fromAmount || undefined);
|
setFromAmount(fromAmount || undefined);
|
||||||
setToAmount(toAmount || undefined);
|
setToAmount(toAmount || undefined);
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
[market?.address.toBase58(), size]
|
[market?.address.toBase58(), size],
|
||||||
)
|
);
|
||||||
|
|
||||||
const canConvert = market && size && size > 0;
|
const canConvert = market && size && size > 0;
|
||||||
const balance = balances.find(coinBalance => coinBalance.coin === fromToken);
|
const balance = balances.find(
|
||||||
const availableBalance = ((balance?.unsettled || 0.) + (balance?.wallet || 0.)) * 0.99;
|
(coinBalance) => coinBalance.coin === fromToken,
|
||||||
|
);
|
||||||
|
const availableBalance =
|
||||||
|
((balance?.unsettled || 0) + (balance?.wallet || 0)) * 0.99;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, {useState} from "react";
|
import React, { useState } from 'react';
|
||||||
import {Col, Input, Modal, Row} from "antd";
|
import { Col, Input, Modal, Row } from 'antd';
|
||||||
import {EndpointInfo} from "../utils/types";
|
import { EndpointInfo } from '../utils/types';
|
||||||
|
|
||||||
export default function CustomClusterEndpointDialog({
|
export default function CustomClusterEndpointDialog({
|
||||||
visible,
|
visible,
|
||||||
|
@ -22,13 +22,13 @@ export default function CustomClusterEndpointDialog({
|
||||||
name: customEndpointName,
|
name: customEndpointName,
|
||||||
endpoint: fullEndpoint,
|
endpoint: fullEndpoint,
|
||||||
custom: true,
|
custom: true,
|
||||||
}
|
};
|
||||||
onAddCustomEndpoint(params);
|
onAddCustomEndpoint(params);
|
||||||
onDoClose();
|
onDoClose();
|
||||||
};
|
};
|
||||||
const onDoClose = () => {
|
const onDoClose = () => {
|
||||||
setCustomEndpoint('')
|
setCustomEndpoint('');
|
||||||
setCustomEndpointName('')
|
setCustomEndpointName('');
|
||||||
onClose && onClose();
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
const canSubmit = customEndpoint !== '' && customEndpointName !== '';
|
const canSubmit = customEndpoint !== '' && customEndpointName !== '';
|
||||||
|
|
|
@ -2,7 +2,13 @@ import React from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { LinkOutlined } from '@ant-design/icons';
|
import { LinkOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export default function LinkAddress({ title, address }: {title?: undefined | any; address: string;}) {
|
export default function LinkAddress({
|
||||||
|
title,
|
||||||
|
address,
|
||||||
|
}: {
|
||||||
|
title?: undefined | any;
|
||||||
|
address: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{title && <p style={{ color: 'white' }}>{title}</p>}
|
{title && <p style={{ color: 'white' }}>{title}</p>}
|
||||||
|
|
|
@ -16,8 +16,8 @@ import Link from './Link';
|
||||||
import { settleFunds } from '../utils/send';
|
import { settleFunds } from '../utils/send';
|
||||||
import { useSendConnection } from '../utils/connection';
|
import { useSendConnection } from '../utils/connection';
|
||||||
import { notify } from '../utils/notifications';
|
import { notify } from '../utils/notifications';
|
||||||
import {Balances} from "../utils/types";
|
import { Balances } from '../utils/types';
|
||||||
import StandaloneTokenAccountsSelect from "./StandaloneTokenAccountSelect";
|
import StandaloneTokenAccountsSelect from './StandaloneTokenAccountSelect';
|
||||||
|
|
||||||
const RowBox = styled(Row)`
|
const RowBox = styled(Row)`
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
@ -101,23 +101,38 @@ export default function StandaloneBalancesDisplay() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedBalances: [string | undefined, Balances | undefined, string, string | undefined][] = [
|
const formattedBalances: [
|
||||||
[baseCurrency, baseCurrencyBalances, 'base', market?.baseMintAddress.toBase58()],
|
string | undefined,
|
||||||
[quoteCurrency, quoteCurrencyBalances, 'quote', market?.quoteMintAddress.toBase58()],
|
Balances | undefined,
|
||||||
]
|
string,
|
||||||
|
string | undefined,
|
||||||
|
][] = [
|
||||||
|
[
|
||||||
|
baseCurrency,
|
||||||
|
baseCurrencyBalances,
|
||||||
|
'base',
|
||||||
|
market?.baseMintAddress.toBase58(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
quoteCurrency,
|
||||||
|
quoteCurrencyBalances,
|
||||||
|
'quote',
|
||||||
|
market?.quoteMintAddress.toBase58(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
||||||
{formattedBalances.map(([currency, balances, baseOrQuote, mint], index) => (
|
{formattedBalances.map(
|
||||||
|
([currency, balances, baseOrQuote, mint], index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<Divider style={{ borderColor: 'white' }}>{currency}</Divider>
|
<Divider style={{ borderColor: 'white' }}>{currency}</Divider>
|
||||||
{connected && (
|
{connected && (
|
||||||
<RowBox
|
<RowBox align="middle" style={{ paddingBottom: 10 }}>
|
||||||
align="middle"
|
|
||||||
style={{ paddingBottom: 10 }}
|
|
||||||
>
|
|
||||||
<StandaloneTokenAccountsSelect
|
<StandaloneTokenAccountsSelect
|
||||||
accounts={tokenAccounts?.filter(account => account.effectiveMint.toBase58() === mint)}
|
accounts={tokenAccounts?.filter(
|
||||||
|
(account) => account.effectiveMint.toBase58() === mint,
|
||||||
|
)}
|
||||||
mint={mint}
|
mint={mint}
|
||||||
label
|
label
|
||||||
/>
|
/>
|
||||||
|
@ -163,7 +178,8 @@ export default function StandaloneBalancesDisplay() {
|
||||||
wallet
|
wallet
|
||||||
</Tip>
|
</Tip>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
<DepositDialog
|
<DepositDialog
|
||||||
baseOrQuote={baseOrQuote}
|
baseOrQuote={baseOrQuote}
|
||||||
onClose={() => setBaseOrQuote('')}
|
onClose={() => setBaseOrQuote('')}
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {TokenAccount} from "../utils/types";
|
import { TokenAccount } from '../utils/types';
|
||||||
import {useSelectedTokenAccounts} from "../utils/markets";
|
import { useSelectedTokenAccounts } from '../utils/markets';
|
||||||
import {Button, Col, Select, Typography} from "antd";
|
import { Button, Col, Select, Typography } from 'antd';
|
||||||
import { CopyOutlined } from '@ant-design/icons';
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
import {abbreviateAddress} from "../utils/utils";
|
import { abbreviateAddress } from '../utils/utils';
|
||||||
import {notify} from "../utils/notifications";
|
import { notify } from '../utils/notifications';
|
||||||
|
|
||||||
export default function StandaloneTokenAccountsSelect({
|
export default function StandaloneTokenAccountsSelect({
|
||||||
accounts,
|
accounts,
|
||||||
mint,
|
mint,
|
||||||
label,
|
label,
|
||||||
}: {
|
}: {
|
||||||
accounts: TokenAccount[] | null | undefined,
|
accounts: TokenAccount[] | null | undefined;
|
||||||
mint: string | undefined,
|
mint: string | undefined;
|
||||||
label?: boolean
|
label?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [selectedTokenAccounts, setSelectedTokenAccounts] = useSelectedTokenAccounts();
|
const [
|
||||||
|
selectedTokenAccounts,
|
||||||
|
setSelectedTokenAccounts,
|
||||||
|
] = useSelectedTokenAccounts();
|
||||||
|
|
||||||
let selectedValue: string | undefined;
|
let selectedValue: string | undefined;
|
||||||
if (mint && mint in selectedTokenAccounts) {
|
if (mint && mint in selectedTokenAccounts) {
|
||||||
|
@ -32,32 +35,35 @@ export default function StandaloneTokenAccountsSelect({
|
||||||
message: 'Error selecting token account',
|
message: 'Error selecting token account',
|
||||||
description: 'Mint is undefined',
|
description: 'Mint is undefined',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newSelectedTokenAccounts = { ...selectedTokenAccounts };
|
const newSelectedTokenAccounts = { ...selectedTokenAccounts };
|
||||||
newSelectedTokenAccounts[mint] = value;
|
newSelectedTokenAccounts[mint] = value;
|
||||||
setSelectedTokenAccounts(newSelectedTokenAccounts);
|
setSelectedTokenAccounts(newSelectedTokenAccounts);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{label &&
|
{label && <Col span={8}>Token account:</Col>}
|
||||||
<Col span={8}>
|
|
||||||
Token account:
|
|
||||||
</Col>
|
|
||||||
}
|
|
||||||
<Col span={label ? 13 : 21}>
|
<Col span={label ? 13 : 21}>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onSelect={setTokenAccountForCoin}
|
onSelect={setTokenAccountForCoin}
|
||||||
>
|
>
|
||||||
{accounts?.map(account => (
|
{accounts?.map((account) => (
|
||||||
<Select.Option key={account.pubkey.toBase58()} value={account.pubkey.toBase58()}>
|
<Select.Option
|
||||||
<Typography.Text code>{label ? abbreviateAddress(account.pubkey, 8) : account.pubkey.toBase58()}</Typography.Text>
|
key={account.pubkey.toBase58()}
|
||||||
</Select.Option>)
|
value={account.pubkey.toBase58()}
|
||||||
)}
|
>
|
||||||
|
<Typography.Text code>
|
||||||
|
{label
|
||||||
|
? abbreviateAddress(account.pubkey, 8)
|
||||||
|
: account.pubkey.toBase58()}
|
||||||
|
</Typography.Text>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={2} offset={1}>
|
<Col span={2} offset={1}>
|
||||||
|
@ -65,7 +71,9 @@ export default function StandaloneTokenAccountsSelect({
|
||||||
shape="round"
|
shape="round"
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => selectedValue && navigator.clipboard.writeText(selectedValue)}
|
onClick={() =>
|
||||||
|
selectedValue && navigator.clipboard.writeText(selectedValue)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import {InfoCircleOutlined, PlusCircleOutlined, SettingOutlined,} from '@ant-design/icons';
|
import {
|
||||||
|
InfoCircleOutlined,
|
||||||
|
PlusCircleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { Button, Col, Menu, Popover, Row, Select } from 'antd';
|
import { Button, Col, Menu, Popover, Row, Select } from 'antd';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
@ -7,10 +11,10 @@ import styled from 'styled-components';
|
||||||
import { useWallet, WALLET_PROVIDERS } from '../utils/wallet';
|
import { useWallet, WALLET_PROVIDERS } from '../utils/wallet';
|
||||||
import { ENDPOINTS, useConnectionConfig } from '../utils/connection';
|
import { ENDPOINTS, useConnectionConfig } from '../utils/connection';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import CustomClusterEndpointDialog from "./CustomClusterEndpointDialog";
|
import CustomClusterEndpointDialog from './CustomClusterEndpointDialog';
|
||||||
import {EndpointInfo} from "../utils/types";
|
import { EndpointInfo } from '../utils/types';
|
||||||
import {notify} from "../utils/notifications";
|
import { notify } from '../utils/notifications';
|
||||||
import {Connection} from "@solana/web3.js";
|
import { Connection } from '@solana/web3.js';
|
||||||
import WalletConnect from './WalletConnect';
|
import WalletConnect from './WalletConnect';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
|
@ -41,13 +45,19 @@ const EXTERNAL_LINKS = {
|
||||||
'/developer-resources': 'https://serum-academy.com/en/developer-resources/',
|
'/developer-resources': 'https://serum-academy.com/en/developer-resources/',
|
||||||
'/explorer': 'https://explorer.solana.com',
|
'/explorer': 'https://explorer.solana.com',
|
||||||
'/srm-faq': 'https://projectserum.com/srm-faq',
|
'/srm-faq': 'https://projectserum.com/srm-faq',
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function TopBar() {
|
export default function TopBar() {
|
||||||
const { connected, wallet, providerUrl, setProvider } = useWallet();
|
const { connected, wallet, providerUrl, setProvider } = useWallet();
|
||||||
const { endpoint, endpointInfo, setEndpoint, availableEndpoints, setCustomEndpoints } = useConnectionConfig();
|
const {
|
||||||
const [ addEndpointVisible, setAddEndpointVisible ] = useState(false)
|
endpoint,
|
||||||
const [ testingConnection, setTestingConnection] = useState(false)
|
endpointInfo,
|
||||||
|
setEndpoint,
|
||||||
|
availableEndpoints,
|
||||||
|
setCustomEndpoints,
|
||||||
|
} = useConnectionConfig();
|
||||||
|
const [addEndpointVisible, setAddEndpointVisible] = useState(false);
|
||||||
|
const [testingConnection, setTestingConnection] = useState(false);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
@ -73,39 +83,45 @@ export default function TopBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleError = (e) => {
|
const handleError = (e) => {
|
||||||
console.log(`Connection to ${info.endpoint} failed: ${e}`)
|
console.log(`Connection to ${info.endpoint} failed: ${e}`);
|
||||||
notify({
|
notify({
|
||||||
message: `Failed to connect to ${info.endpoint}`,
|
message: `Failed to connect to ${info.endpoint}`,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connection = new Connection(info.endpoint, 'recent');
|
const connection = new Connection(info.endpoint, 'recent');
|
||||||
connection.getEpochInfo().then(result => {
|
connection
|
||||||
|
.getEpochInfo()
|
||||||
|
.then((result) => {
|
||||||
setTestingConnection(true);
|
setTestingConnection(true);
|
||||||
console.log(`testing connection to ${info.endpoint}`);
|
console.log(`testing connection to ${info.endpoint}`);
|
||||||
const newCustomEndpoints = [...availableEndpoints.filter(e => e.custom), info];
|
const newCustomEndpoints = [
|
||||||
|
...availableEndpoints.filter((e) => e.custom),
|
||||||
|
info,
|
||||||
|
];
|
||||||
setEndpoint(info.endpoint);
|
setEndpoint(info.endpoint);
|
||||||
setCustomEndpoints(newCustomEndpoints);
|
setCustomEndpoints(newCustomEndpoints);
|
||||||
}).catch(handleError);
|
})
|
||||||
|
.catch(handleError);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e);
|
handleError(e);
|
||||||
} finally {
|
} finally {
|
||||||
setTestingConnection(false);
|
setTestingConnection(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const endpointInfoCustom = endpointInfo && endpointInfo.custom
|
const endpointInfoCustom = endpointInfo && endpointInfo.custom;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
if (endpointInfoCustom) {
|
if (endpointInfoCustom) {
|
||||||
setEndpoint(ENDPOINTS[0].endpoint)
|
setEndpoint(ENDPOINTS[0].endpoint);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
window.addEventListener("beforeunload", handler)
|
window.addEventListener('beforeunload', handler);
|
||||||
return () => window.removeEventListener("beforeunload", handler)
|
return () => window.removeEventListener('beforeunload', handler);
|
||||||
}, [endpointInfoCustom, setEndpoint])
|
}, [endpointInfoCustom, setEndpoint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -137,34 +153,61 @@ export default function TopBar() {
|
||||||
{connected && <Menu.Item key="/orders">ORDERS</Menu.Item>}
|
{connected && <Menu.Item key="/orders">ORDERS</Menu.Item>}
|
||||||
{connected && <Menu.Item key="/convert">CONVERT</Menu.Item>}
|
{connected && <Menu.Item key="/convert">CONVERT</Menu.Item>}
|
||||||
<Menu.Item key="/list-new-market">ADD MARKET</Menu.Item>
|
<Menu.Item key="/list-new-market">ADD MARKET</Menu.Item>
|
||||||
<Menu.SubMenu title="LEARN" onTitleClick={() => window.open(EXTERNAL_LINKS['/learn'], '_blank')}>
|
<Menu.SubMenu
|
||||||
|
title="LEARN"
|
||||||
|
onTitleClick={() => window.open(EXTERNAL_LINKS['/learn'], '_blank')}
|
||||||
|
>
|
||||||
<Menu.Item key="/add-market">
|
<Menu.Item key="/add-market">
|
||||||
<a href={EXTERNAL_LINKS['/add-market']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/add-market']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Adding a market
|
Adding a market
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/wallet-support">
|
<Menu.Item key="/wallet-support">
|
||||||
<a href={EXTERNAL_LINKS['/wallet-support']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/wallet-support']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Supported wallets
|
Supported wallets
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/dex-list">
|
<Menu.Item key="/dex-list">
|
||||||
<a href={EXTERNAL_LINKS['/dex-list']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/dex-list']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
DEX list
|
DEX list
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/developer-resources">
|
<Menu.Item key="/developer-resources">
|
||||||
<a href={EXTERNAL_LINKS['/developer-resources']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/developer-resources']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Developer resources
|
Developer resources
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/explorer">
|
<Menu.Item key="/explorer">
|
||||||
<a href={EXTERNAL_LINKS['/explorer']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/explorer']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Solana block explorer
|
Solana block explorer
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="/srm-faq">
|
<Menu.Item key="/srm-faq">
|
||||||
<a href={EXTERNAL_LINKS['/srm-faq']} target="_blank" rel="noopener noreferrer">
|
<a
|
||||||
|
href={EXTERNAL_LINKS['/srm-faq']}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
SRM FAQ
|
SRM FAQ
|
||||||
</a>
|
</a>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
import { useSendConnection } from '../utils/connection';
|
import { useSendConnection } from '../utils/connection';
|
||||||
import FloatingElement from './layout/FloatingElement';
|
import FloatingElement from './layout/FloatingElement';
|
||||||
import { placeOrder } from '../utils/send';
|
import { placeOrder } from '../utils/send';
|
||||||
import {SwitchChangeEventHandler} from "antd/es/switch";
|
import { SwitchChangeEventHandler } from 'antd/es/switch';
|
||||||
|
|
||||||
const SellButton = styled(Button)`
|
const SellButton = styled(Button)`
|
||||||
margin: 20px 0px 0px 0px;
|
margin: 20px 0px 0px 0px;
|
||||||
|
@ -42,9 +42,14 @@ const sliderMarks = {
|
||||||
100: '100%',
|
100: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TradeForm({ style, setChangeOrderRef }: {
|
export default function TradeForm({
|
||||||
|
style,
|
||||||
|
setChangeOrderRef,
|
||||||
|
}: {
|
||||||
style?: any;
|
style?: any;
|
||||||
setChangeOrderRef?: (ref: ({ size, price }: {size?: number; price?: number;}) => void) => void;
|
setChangeOrderRef?: (
|
||||||
|
ref: ({ size, price }: { size?: number; price?: number }) => void,
|
||||||
|
) => void;
|
||||||
}) {
|
}) {
|
||||||
const [side, setSide] = useState<'buy' | 'sell'>('buy');
|
const [side, setSide] = useState<'buy' | 'sell'>('buy');
|
||||||
const { baseCurrency, quoteCurrency, market } = useMarket();
|
const { baseCurrency, quoteCurrency, market } = useMarket();
|
||||||
|
@ -65,7 +70,8 @@ export default function TradeForm({ style, setChangeOrderRef }: {
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [sizeFraction, setSizeFraction] = useState(0);
|
const [sizeFraction, setSizeFraction] = useState(0);
|
||||||
|
|
||||||
const availableQuote = openOrdersAccount && market
|
const availableQuote =
|
||||||
|
openOrdersAccount && market
|
||||||
? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree)
|
? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
@ -123,7 +129,13 @@ export default function TradeForm({ style, setChangeOrderRef }: {
|
||||||
setBaseSize(baseSize);
|
setBaseSize(baseSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const doChangeOrder = ({ size, price }: {size?: number; price?: number;}) => {
|
const doChangeOrder = ({
|
||||||
|
size,
|
||||||
|
price,
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
price?: number;
|
||||||
|
}) => {
|
||||||
const formattedSize = size && roundToDecimal(size, sizeDecimalCount);
|
const formattedSize = size && roundToDecimal(size, sizeDecimalCount);
|
||||||
const formattedPrice = price && roundToDecimal(price, priceDecimalCount);
|
const formattedPrice = price && roundToDecimal(price, priceDecimalCount);
|
||||||
formattedSize && onSetBaseSize(formattedSize);
|
formattedSize && onSetBaseSize(formattedSize);
|
||||||
|
@ -131,9 +143,10 @@ export default function TradeForm({ style, setChangeOrderRef }: {
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSizeFraction = () => {
|
const updateSizeFraction = () => {
|
||||||
const rawMaxSize = side === 'buy' ? quoteBalance / (price || markPrice || 1.) : baseBalance;
|
const rawMaxSize =
|
||||||
|
side === 'buy' ? quoteBalance / (price || markPrice || 1) : baseBalance;
|
||||||
const maxSize = floorToDecimal(rawMaxSize, sizeDecimalCount);
|
const maxSize = floorToDecimal(rawMaxSize, sizeDecimalCount);
|
||||||
const sizeFraction = Math.min(((baseSize || 0.) / maxSize) * 100, 100);
|
const sizeFraction = Math.min(((baseSize || 0) / maxSize) * 100, 100);
|
||||||
setSizeFraction(sizeFraction);
|
setSizeFraction(sizeFraction);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -142,13 +155,17 @@ export default function TradeForm({ style, setChangeOrderRef }: {
|
||||||
let formattedMarkPrice: number | string = priceDecimalCount
|
let formattedMarkPrice: number | string = priceDecimalCount
|
||||||
? markPrice.toFixed(priceDecimalCount)
|
? markPrice.toFixed(priceDecimalCount)
|
||||||
: markPrice;
|
: markPrice;
|
||||||
setPrice(typeof formattedMarkPrice === 'number' ? formattedMarkPrice : parseFloat(formattedMarkPrice));
|
setPrice(
|
||||||
|
typeof formattedMarkPrice === 'number'
|
||||||
|
? formattedMarkPrice
|
||||||
|
: parseFloat(formattedMarkPrice),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let newSize;
|
let newSize;
|
||||||
if (side === 'buy') {
|
if (side === 'buy') {
|
||||||
if (price || markPrice) {
|
if (price || markPrice) {
|
||||||
newSize = ((quoteBalance / (price || markPrice || 1.)) * value) / 100;
|
newSize = ((quoteBalance / (price || markPrice || 1)) * value) / 100;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newSize = (baseBalance * value) / 100;
|
newSize = (baseBalance * value) / 100;
|
||||||
|
|
|
@ -32,10 +32,10 @@ export default function PublicTrades({ smallScreen }) {
|
||||||
>
|
>
|
||||||
<Title>Recent Market trades</Title>
|
<Title>Recent Market trades</Title>
|
||||||
<SizeTitle>
|
<SizeTitle>
|
||||||
<Col span={8}>
|
<Col span={8}>Price ({quoteCurrency}) </Col>
|
||||||
Price ({quoteCurrency}){' '}
|
<Col span={8} style={{ textAlign: 'right' }}>
|
||||||
|
Size ({baseCurrency})
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8} style={{ textAlign: 'right' }}>Size ({baseCurrency})</Col>
|
|
||||||
<Col span={8} style={{ textAlign: 'right' }}>
|
<Col span={8} style={{ textAlign: 'right' }}>
|
||||||
Time
|
Time
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -8,14 +8,20 @@ import {useWallet} from '../../utils/wallet';
|
||||||
import { useSendConnection } from '../../utils/connection';
|
import { useSendConnection } from '../../utils/connection';
|
||||||
import { notify } from '../../utils/notifications';
|
import { notify } from '../../utils/notifications';
|
||||||
import { DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import {OrderWithMarketAndMarketName} from "../../utils/types";
|
import { OrderWithMarketAndMarketName } from '../../utils/types';
|
||||||
|
|
||||||
const CancelButton = styled(Button)`
|
const CancelButton = styled(Button)`
|
||||||
color: #f23b69;
|
color: #f23b69;
|
||||||
border: 1px solid #f23b69;
|
border: 1px solid #f23b69;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize, loading, marketFilter } : {
|
export default function OpenOrderTable({
|
||||||
|
openOrders,
|
||||||
|
onCancelSuccess,
|
||||||
|
pageSize,
|
||||||
|
loading,
|
||||||
|
marketFilter,
|
||||||
|
}: {
|
||||||
openOrders: OrderWithMarketAndMarketName[] | null | undefined;
|
openOrders: OrderWithMarketAndMarketName[] | null | undefined;
|
||||||
onCancelSuccess?: () => void;
|
onCancelSuccess?: () => void;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
@ -50,15 +56,17 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
const marketFilters = [
|
const marketFilters = [
|
||||||
...new Set((openOrders || []).map(orderInfos => orderInfos.marketName))
|
...new Set((openOrders || []).map((orderInfos) => orderInfos.marketName)),
|
||||||
].map(marketName => {return {text: marketName, value: marketName}});
|
].map((marketName) => {
|
||||||
|
return { text: marketName, value: marketName };
|
||||||
|
});
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'Market',
|
title: 'Market',
|
||||||
dataIndex: 'marketName',
|
dataIndex: 'marketName',
|
||||||
key: 'marketName',
|
key: 'marketName',
|
||||||
filters: (marketFilter ? marketFilters : undefined),
|
filters: marketFilter ? marketFilters : undefined,
|
||||||
onFilter: (value, record) => record.marketName.indexOf(value) === 0,
|
onFilter: (value, record) => record.marketName.indexOf(value) === 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -75,11 +83,11 @@ export default function OpenOrderTable({ openOrders, onCancelSuccess, pageSize,
|
||||||
),
|
),
|
||||||
sorter: (a, b) => {
|
sorter: (a, b) => {
|
||||||
if (a.side === b.side) {
|
if (a.side === b.side) {
|
||||||
return 0.
|
return 0;
|
||||||
} else if (a.side === 'buy') {
|
} else if (a.side === 'buy') {
|
||||||
return 1.
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
return -1.
|
return -1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSorterTooltip: false,
|
showSorterTooltip: false,
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import DataTable from '../layout/DataTable';
|
import DataTable from '../layout/DataTable';
|
||||||
import {Button, Row} from "antd";
|
import { Button, Row } from 'antd';
|
||||||
import {settleAllFunds} from "../../utils/send";
|
import { settleAllFunds } from '../../utils/send';
|
||||||
import {notify} from "../../utils/notifications";
|
import { notify } from '../../utils/notifications';
|
||||||
import {useConnection} from "../../utils/connection";
|
import { useConnection } from '../../utils/connection';
|
||||||
import {useWallet} from "../../utils/wallet";
|
import { useWallet } from '../../utils/wallet';
|
||||||
import {useAllMarkets, useMarket, useSelectedTokenAccounts, useTokenAccounts} from "../../utils/markets";
|
import {
|
||||||
import StandaloneTokenAccountsSelect from "../StandaloneTokenAccountSelect";
|
useAllMarkets,
|
||||||
|
useMarket,
|
||||||
|
useSelectedTokenAccounts,
|
||||||
|
useTokenAccounts,
|
||||||
|
} from '../../utils/markets';
|
||||||
|
import StandaloneTokenAccountsSelect from '../StandaloneTokenAccountSelect';
|
||||||
|
|
||||||
export default function WalletBalancesTable({
|
export default function WalletBalancesTable({
|
||||||
walletBalances,
|
walletBalances,
|
||||||
|
@ -17,7 +22,7 @@ export default function WalletBalancesTable({
|
||||||
walletBalance: number;
|
walletBalance: number;
|
||||||
openOrdersFree: number;
|
openOrdersFree: number;
|
||||||
openOrdersTotal: number;
|
openOrdersTotal: number;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const { wallet, connected } = useWallet();
|
const { wallet, connected } = useWallet();
|
||||||
|
@ -51,8 +56,8 @@ export default function WalletBalancesTable({
|
||||||
tokenAccounts,
|
tokenAccounts,
|
||||||
selectedTokenAccounts,
|
selectedTokenAccounts,
|
||||||
wallet,
|
wallet,
|
||||||
markets: allMarkets.map(marketInfo => marketInfo.market),
|
markets: allMarkets.map((marketInfo) => marketInfo.market),
|
||||||
})
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notify({
|
notify({
|
||||||
message: 'Error settling funds',
|
message: 'Error settling funds',
|
||||||
|
@ -94,9 +99,11 @@ export default function WalletBalancesTable({
|
||||||
key: 'selectTokenAccount',
|
key: 'selectTokenAccount',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
render: (walletBalance) => (
|
render: (walletBalance) => (
|
||||||
<Row align="middle" style={{width: "430px"}}>
|
<Row align="middle" style={{ width: '430px' }}>
|
||||||
<StandaloneTokenAccountsSelect
|
<StandaloneTokenAccountsSelect
|
||||||
accounts={tokenAccounts?.filter(t => t.effectiveMint.toBase58() === walletBalance.mint)}
|
accounts={tokenAccounts?.filter(
|
||||||
|
(t) => t.effectiveMint.toBase58() === walletBalance.mint,
|
||||||
|
)}
|
||||||
mint={walletBalance.mint}
|
mint={walletBalance.mint}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -111,14 +118,11 @@ export default function WalletBalancesTable({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
{connected &&
|
{connected && (
|
||||||
<Button
|
<Button onClick={onSettleFunds} loading={settlingFunds}>
|
||||||
onClick={onSettleFunds}
|
|
||||||
loading={settlingFunds}
|
|
||||||
>
|
|
||||||
Settle all funds
|
Settle all funds
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import { Button, Popover } from 'antd';
|
import { Button, Popover } from 'antd';
|
||||||
import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import { useWallet } from '../utils/wallet';
|
import { useWallet } from '../utils/wallet';
|
||||||
import LinkAddress from "./LinkAddress";
|
import LinkAddress from './LinkAddress';
|
||||||
|
|
||||||
export default function WalletConnect() {
|
export default function WalletConnect() {
|
||||||
const { connected, wallet } = useWallet();
|
const { connected, wallet } = useWallet();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
declare module '@project-serum/sol-wallet-adapter' {
|
declare module '@project-serum/sol-wallet-adapter' {
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from 'eventemitter3';
|
||||||
import {PublicKey, Transaction} from "@solana/web3.js";
|
import { PublicKey, Transaction } from '@solana/web3.js';
|
||||||
|
|
||||||
export default class Wallet extends EventEmitter {
|
export default class Wallet extends EventEmitter {
|
||||||
constructor(providerUrl: string, network: string)
|
constructor(providerUrl: string, network: string);
|
||||||
publicKey: PublicKey;
|
publicKey: PublicKey;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
autoApprove: boolean;
|
autoApprove: boolean;
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tabs } from 'antd';
|
import { Tabs } from 'antd';
|
||||||
import {useAllOpenOrdersBalances, useWalletBalancesForAllMarkets,} from '../utils/markets';
|
import {
|
||||||
|
useAllOpenOrdersBalances,
|
||||||
|
useWalletBalancesForAllMarkets,
|
||||||
|
} from '../utils/markets';
|
||||||
import FloatingElement from '../components/layout/FloatingElement';
|
import FloatingElement from '../components/layout/FloatingElement';
|
||||||
import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable';
|
import WalletBalancesTable from '../components/UserInfoTable/WalletBalancesTable';
|
||||||
import {useMintToTickers} from "../utils/tokens";
|
import { useMintToTickers } from '../utils/tokens';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
@ -12,28 +15,26 @@ export default function BalancesPage() {
|
||||||
const mintToTickers = useMintToTickers();
|
const mintToTickers = useMintToTickers();
|
||||||
const openOrdersBalances = useAllOpenOrdersBalances();
|
const openOrdersBalances = useAllOpenOrdersBalances();
|
||||||
|
|
||||||
const data = (walletBalances || []).map(balance => {
|
const data = (walletBalances || []).map((balance) => {
|
||||||
const balances = {
|
const balances = {
|
||||||
coin: mintToTickers[balance.mint],
|
coin: mintToTickers[balance.mint],
|
||||||
mint: balance.mint,
|
mint: balance.mint,
|
||||||
walletBalance: balance.balance,
|
walletBalance: balance.balance,
|
||||||
openOrdersFree: 0.,
|
openOrdersFree: 0,
|
||||||
openOrdersTotal: 0.,
|
openOrdersTotal: 0,
|
||||||
}
|
};
|
||||||
for (let openOrdersAccount of (openOrdersBalances[balance.mint] || [])) {
|
for (let openOrdersAccount of openOrdersBalances[balance.mint] || []) {
|
||||||
balances['openOrdersFree'] += openOrdersAccount.free;
|
balances['openOrdersFree'] += openOrdersAccount.free;
|
||||||
balances['openOrdersTotal'] += openOrdersAccount.total;
|
balances['openOrdersTotal'] += openOrdersAccount.total;
|
||||||
}
|
}
|
||||||
return balances
|
return balances;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
||||||
<Tabs defaultActiveKey="walletBalances">
|
<Tabs defaultActiveKey="walletBalances">
|
||||||
<TabPane tab="Wallet Balances" key="walletBalances">
|
<TabPane tab="Wallet Balances" key="walletBalances">
|
||||||
<WalletBalancesTable
|
<WalletBalancesTable walletBalances={data} />
|
||||||
walletBalances={data}
|
|
||||||
/>
|
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</FloatingElement>
|
</FloatingElement>
|
||||||
|
|
|
@ -1,36 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FloatingElement from '../components/layout/FloatingElement';
|
import FloatingElement from '../components/layout/FloatingElement';
|
||||||
import {getMarketInfos, useAllMarkets, useAllOpenOrders, useMarket} from "../utils/markets";
|
import {
|
||||||
import OpenOrderTable from "../components/UserInfoTable/OpenOrderTable";
|
getMarketInfos,
|
||||||
import {Button} from "antd";
|
useAllMarkets,
|
||||||
import {OrderWithMarketAndMarketName} from "../utils/types";
|
useAllOpenOrders,
|
||||||
|
useMarket,
|
||||||
|
} from '../utils/markets';
|
||||||
|
import OpenOrderTable from '../components/UserInfoTable/OpenOrderTable';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { OrderWithMarketAndMarketName } from '../utils/types';
|
||||||
|
|
||||||
export default function OpenOrdersPage() {
|
export default function OpenOrdersPage() {
|
||||||
const { openOrders, loaded, refreshOpenOrders } = useAllOpenOrders();
|
const { openOrders, loaded, refreshOpenOrders } = useAllOpenOrders();
|
||||||
let { customMarkets } = useMarket();
|
let { customMarkets } = useMarket();
|
||||||
let marketInfos = getMarketInfos(customMarkets);
|
let marketInfos = getMarketInfos(customMarkets);
|
||||||
let marketAddressesToNames = Object.fromEntries(marketInfos.map(info => [info.address.toBase58(), info.name]));
|
let marketAddressesToNames = Object.fromEntries(
|
||||||
|
marketInfos.map((info) => [info.address.toBase58(), info.name]),
|
||||||
|
);
|
||||||
let [allMarkets] = useAllMarkets(customMarkets);
|
let [allMarkets] = useAllMarkets(customMarkets);
|
||||||
const marketsByAddress = Object.fromEntries((allMarkets || []).map(
|
const marketsByAddress = Object.fromEntries(
|
||||||
marketInfo => [marketInfo.market.address.toBase58(), marketInfo.market]
|
(allMarkets || []).map((marketInfo) => [
|
||||||
));
|
marketInfo.market.address.toBase58(),
|
||||||
|
marketInfo.market,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const dataSource: OrderWithMarketAndMarketName[] = (openOrders || []).map((orderInfos) =>
|
const dataSource: OrderWithMarketAndMarketName[] = (openOrders || [])
|
||||||
orderInfos.orders.map(order => {
|
.map((orderInfos) =>
|
||||||
|
orderInfos.orders.map((order) => {
|
||||||
return {
|
return {
|
||||||
marketName: marketAddressesToNames[orderInfos.marketAddress],
|
marketName: marketAddressesToNames[orderInfos.marketAddress],
|
||||||
market: marketsByAddress[orderInfos.marketAddress],
|
market: marketsByAddress[orderInfos.marketAddress],
|
||||||
...order
|
...order,
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
).flat();
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
||||||
<Button
|
<Button onClick={refreshOpenOrders} loading={!loaded}>
|
||||||
onClick={refreshOpenOrders}
|
|
||||||
loading={!loaded}
|
|
||||||
>
|
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<OpenOrderTable
|
<OpenOrderTable
|
||||||
|
|
|
@ -55,7 +55,9 @@ export default function TradePage() {
|
||||||
document.title = marketName ? `${marketName} — Serum` : 'Serum';
|
document.title = marketName ? `${marketName} — Serum` : 'Serum';
|
||||||
}, [marketName]);
|
}, [marketName]);
|
||||||
|
|
||||||
const changeOrderRef = useRef<({ size, price }: {size?: number; price?: number;}) => void>();
|
const changeOrderRef = useRef<
|
||||||
|
({ size, price }: { size?: number; price?: number }) => void
|
||||||
|
>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
|
|
@ -5,9 +5,7 @@ export default class BonfidaApi {
|
||||||
|
|
||||||
static async get(path: string) {
|
static async get(path: string) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(this.URL + path);
|
||||||
this.URL + path,
|
|
||||||
);
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
return responseJson.success ? responseJson.data : null;
|
return responseJson.success ? responseJson.data : null;
|
||||||
|
|
|
@ -3,30 +3,31 @@ import {Account, AccountInfo, Connection, PublicKey} from '@solana/web3.js';
|
||||||
import React, { useContext, useEffect, useMemo } from 'react';
|
import React, { useContext, useEffect, useMemo } from 'react';
|
||||||
import { setCache, useAsyncData } from './fetch-loop';
|
import { setCache, useAsyncData } from './fetch-loop';
|
||||||
import tuple from 'immutable-tuple';
|
import tuple from 'immutable-tuple';
|
||||||
import {ConnectionContextValues, EndpointInfo} from "./types";
|
import { ConnectionContextValues, EndpointInfo } from './types';
|
||||||
|
|
||||||
export const ENDPOINTS: EndpointInfo[] = [
|
export const ENDPOINTS: EndpointInfo[] = [
|
||||||
{
|
{
|
||||||
name: 'mainnet-beta',
|
name: 'mainnet-beta',
|
||||||
endpoint: 'https://solana-api.projectserum.com',
|
endpoint: 'https://solana-api.projectserum.com',
|
||||||
custom: false
|
custom: false,
|
||||||
},
|
},
|
||||||
{ name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false },
|
{ name: 'localnet', endpoint: 'http://127.0.0.1:8899', custom: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
const accountListenerCount = new Map();
|
const accountListenerCount = new Map();
|
||||||
|
|
||||||
const ConnectionContext: React.Context<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(null);
|
const ConnectionContext: React.Context<null | ConnectionContextValues> = React.createContext<null | ConnectionContextValues>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
export function ConnectionProvider({ children }) {
|
export function ConnectionProvider({ children }) {
|
||||||
const [endpoint, setEndpoint] = useLocalStorageState<string>(
|
const [endpoint, setEndpoint] = useLocalStorageState<string>(
|
||||||
'connectionEndpts',
|
'connectionEndpts',
|
||||||
ENDPOINTS[0].endpoint,
|
ENDPOINTS[0].endpoint,
|
||||||
);
|
);
|
||||||
const [customEndpoints, setCustomEndpoints] = useLocalStorageState<EndpointInfo[]>(
|
const [customEndpoints, setCustomEndpoints] = useLocalStorageState<
|
||||||
'customConnectionEndpoints',
|
EndpointInfo[]
|
||||||
[]
|
>('customConnectionEndpoints', []);
|
||||||
)
|
|
||||||
const availableEndpoints = ENDPOINTS.concat(customEndpoints);
|
const availableEndpoints = ENDPOINTS.concat(customEndpoints);
|
||||||
|
|
||||||
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
|
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
|
||||||
|
@ -41,12 +42,16 @@ export function ConnectionProvider({ children }) {
|
||||||
// This is a hack to prevent the list from every getting empty
|
// This is a hack to prevent the list from every getting empty
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = connection.onAccountChange(new Account().publicKey, () => {});
|
const id = connection.onAccountChange(new Account().publicKey, () => {});
|
||||||
return () => {connection.removeAccountChangeListener(id)};
|
return () => {
|
||||||
|
connection.removeAccountChangeListener(id);
|
||||||
|
};
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = connection.onSlotChange(() => null);
|
const id = connection.onSlotChange(() => null);
|
||||||
return () => {connection.removeSlotChangeListener(id)};
|
return () => {
|
||||||
|
connection.removeSlotChangeListener(id);
|
||||||
|
};
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -54,17 +59,28 @@ export function ConnectionProvider({ children }) {
|
||||||
new Account().publicKey,
|
new Account().publicKey,
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
return () => {sendConnection.removeAccountChangeListener(id)};
|
return () => {
|
||||||
|
sendConnection.removeAccountChangeListener(id);
|
||||||
|
};
|
||||||
}, [sendConnection]);
|
}, [sendConnection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = sendConnection.onSlotChange(() => null);
|
const id = sendConnection.onSlotChange(() => null);
|
||||||
return () => {sendConnection.removeSlotChangeListener(id)};
|
return () => {
|
||||||
|
sendConnection.removeSlotChangeListener(id);
|
||||||
|
};
|
||||||
}, [sendConnection]);
|
}, [sendConnection]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConnectionContext.Provider
|
<ConnectionContext.Provider
|
||||||
value={{ endpoint, setEndpoint, connection, sendConnection, availableEndpoints, setCustomEndpoints }}
|
value={{
|
||||||
|
endpoint,
|
||||||
|
setEndpoint,
|
||||||
|
connection,
|
||||||
|
sendConnection,
|
||||||
|
availableEndpoints,
|
||||||
|
setCustomEndpoints,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ConnectionContext.Provider>
|
</ConnectionContext.Provider>
|
||||||
|
@ -74,7 +90,7 @@ export function ConnectionProvider({ children }) {
|
||||||
export function useConnection() {
|
export function useConnection() {
|
||||||
const context = useContext(ConnectionContext);
|
const context = useContext(ConnectionContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Missing connection context')
|
throw new Error('Missing connection context');
|
||||||
}
|
}
|
||||||
return context.connection;
|
return context.connection;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +98,7 @@ export function useConnection() {
|
||||||
export function useSendConnection() {
|
export function useSendConnection() {
|
||||||
const context = useContext(ConnectionContext);
|
const context = useContext(ConnectionContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Missing connection context')
|
throw new Error('Missing connection context');
|
||||||
}
|
}
|
||||||
return context.sendConnection;
|
return context.sendConnection;
|
||||||
}
|
}
|
||||||
|
@ -90,18 +106,22 @@ export function useSendConnection() {
|
||||||
export function useConnectionConfig() {
|
export function useConnectionConfig() {
|
||||||
const context = useContext(ConnectionContext);
|
const context = useContext(ConnectionContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Missing connection context')
|
throw new Error('Missing connection context');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
endpoint: context.endpoint,
|
endpoint: context.endpoint,
|
||||||
endpointInfo: context.availableEndpoints.find(info => info.endpoint === context.endpoint),
|
endpointInfo: context.availableEndpoints.find(
|
||||||
|
(info) => info.endpoint === context.endpoint,
|
||||||
|
),
|
||||||
setEndpoint: context.setEndpoint,
|
setEndpoint: context.setEndpoint,
|
||||||
availableEndpoints: context.availableEndpoints,
|
availableEndpoints: context.availableEndpoints,
|
||||||
setCustomEndpoints: context.setCustomEndpoints,
|
setCustomEndpoints: context.setCustomEndpoints,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccountInfo(publicKey: PublicKey | undefined | null): [AccountInfo<Buffer> | null | undefined, boolean] {
|
export function useAccountInfo(
|
||||||
|
publicKey: PublicKey | undefined | null,
|
||||||
|
): [AccountInfo<Buffer> | null | undefined, boolean] {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const cacheKey = tuple(connection, publicKey?.toBase58());
|
const cacheKey = tuple(connection, publicKey?.toBase58());
|
||||||
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(
|
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(
|
||||||
|
|
|
@ -103,7 +103,7 @@ class FetchLoopInternal<T = any> {
|
||||||
try {
|
try {
|
||||||
const data = await this.fn();
|
const data = await this.fn();
|
||||||
if (!this.cacheNullValues && data === null) {
|
if (!this.cacheNullValues && data === null) {
|
||||||
console.log(`Not caching null value for ${this.cacheKey}`)
|
console.log(`Not caching null value for ${this.cacheKey}`);
|
||||||
// cached data has not changed so no need to re-render
|
// cached data has not changed so no need to re-render
|
||||||
this.errors = 0;
|
this.errors = 0;
|
||||||
return data;
|
return data;
|
||||||
|
|
|
@ -1187,8 +1187,8 @@ export function getExpectedFillPrice(
|
||||||
cost: number,
|
cost: number,
|
||||||
tickSizeDecimals?: number,
|
tickSizeDecimals?: number,
|
||||||
) {
|
) {
|
||||||
let spentCost = 0.;
|
let spentCost = 0;
|
||||||
let avgPrice = 0.;
|
let avgPrice = 0;
|
||||||
let price, sizeAtLevel, costAtLevel: number;
|
let price, sizeAtLevel, costAtLevel: number;
|
||||||
for ([price, sizeAtLevel] of orderbook.getL2(1000)) {
|
for ([price, sizeAtLevel] of orderbook.getL2(1000)) {
|
||||||
costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel;
|
costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel;
|
||||||
|
|
|
@ -3,11 +3,18 @@ import { useLocalStorageState } from './utils';
|
||||||
import { useInterval } from './useInterval';
|
import { useInterval } from './useInterval';
|
||||||
import { useConnection } from './connection';
|
import { useConnection } from './connection';
|
||||||
import { useWallet } from './wallet';
|
import { useWallet } from './wallet';
|
||||||
import {useAllMarkets, useTokenAccounts, useMarket, useSelectedTokenAccounts} from './markets';
|
import {
|
||||||
|
useAllMarkets,
|
||||||
|
useTokenAccounts,
|
||||||
|
useMarket,
|
||||||
|
useSelectedTokenAccounts,
|
||||||
|
} from './markets';
|
||||||
import { settleAllFunds } from './send';
|
import { settleAllFunds } from './send';
|
||||||
import {PreferencesContextValues} from "./types";
|
import { PreferencesContextValues } from './types';
|
||||||
|
|
||||||
const PreferencesContext = React.createContext<PreferencesContextValues | null>(null);
|
const PreferencesContext = React.createContext<PreferencesContextValues | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
export function PreferencesProvider({ children }) {
|
export function PreferencesProvider({ children }) {
|
||||||
const [autoSettleEnabled, setAutoSettleEnabled] = useLocalStorageState(
|
const [autoSettleEnabled, setAutoSettleEnabled] = useLocalStorageState(
|
||||||
|
@ -27,7 +34,13 @@ export function PreferencesProvider({ children }) {
|
||||||
const markets = (marketList || []).map((m) => m.market);
|
const markets = (marketList || []).map((m) => m.market);
|
||||||
try {
|
try {
|
||||||
console.log('Auto settling');
|
console.log('Auto settling');
|
||||||
await settleAllFunds({ connection, wallet, tokenAccounts: (tokenAccounts || []), markets, selectedTokenAccounts });
|
await settleAllFunds({
|
||||||
|
connection,
|
||||||
|
wallet,
|
||||||
|
tokenAccounts: tokenAccounts || [],
|
||||||
|
markets,
|
||||||
|
selectedTokenAccounts,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error auto settling funds: ' + e.message);
|
console.log('Error auto settling funds: ' + e.message);
|
||||||
}
|
}
|
||||||
|
@ -51,7 +64,7 @@ export function PreferencesProvider({ children }) {
|
||||||
export function usePreferences() {
|
export function usePreferences() {
|
||||||
const context = useContext(PreferencesContext);
|
const context = useContext(PreferencesContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Missing preferences context')
|
throw new Error('Missing preferences context');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
autoSettleEnabled: context.autoSettleEnabled,
|
autoSettleEnabled: context.autoSettleEnabled,
|
||||||
|
|
|
@ -2,10 +2,14 @@ import { notify } from './notifications';
|
||||||
import { getDecimalCount, sleep } from './utils';
|
import { getDecimalCount, sleep } from './utils';
|
||||||
import { getSelectedTokenAccountForMint } from './markets';
|
import { getSelectedTokenAccountForMint } from './markets';
|
||||||
import {
|
import {
|
||||||
Account, AccountInfo, Connection,
|
Account,
|
||||||
PublicKey, RpcResponseAndContext,
|
AccountInfo,
|
||||||
|
Connection,
|
||||||
|
PublicKey,
|
||||||
|
RpcResponseAndContext,
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
Transaction, TransactionSignature,
|
Transaction,
|
||||||
|
TransactionSignature,
|
||||||
} from '@solana/web3.js';
|
} from '@solana/web3.js';
|
||||||
import { BN } from 'bn.js';
|
import { BN } from 'bn.js';
|
||||||
import {
|
import {
|
||||||
|
@ -15,12 +19,12 @@ import {
|
||||||
TokenInstructions,
|
TokenInstructions,
|
||||||
OpenOrders,
|
OpenOrders,
|
||||||
} from '@project-serum/serum';
|
} from '@project-serum/serum';
|
||||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
import Wallet from '@project-serum/sol-wallet-adapter';
|
||||||
import {SelectedTokenAccounts, TokenAccount} from "./types";
|
import { SelectedTokenAccounts, TokenAccount } from './types';
|
||||||
import {Order} from "@project-serum/serum/lib/market";
|
import { Order } from '@project-serum/serum/lib/market';
|
||||||
import {Buffer} from "buffer";
|
import { Buffer } from 'buffer';
|
||||||
import assert from "assert";
|
import assert from 'assert';
|
||||||
import { struct } from "superstruct";
|
import { struct } from 'superstruct';
|
||||||
|
|
||||||
export async function createTokenAccountTransaction({
|
export async function createTokenAccountTransaction({
|
||||||
connection,
|
connection,
|
||||||
|
@ -110,8 +114,8 @@ export async function settleFunds({
|
||||||
}
|
}
|
||||||
let referrerQuoteWallet: PublicKey | null = null;
|
let referrerQuoteWallet: PublicKey | null = null;
|
||||||
if (market.supportsReferralFees) {
|
if (market.supportsReferralFees) {
|
||||||
const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT')
|
const usdt = TOKEN_MINTS.find(({ name }) => name === 'USDT');
|
||||||
const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC')
|
const usdc = TOKEN_MINTS.find(({ name }) => name === 'USDC');
|
||||||
if (
|
if (
|
||||||
process.env.REACT_APP_USDT_REFERRAL_FEES_ADDRESS &&
|
process.env.REACT_APP_USDT_REFERRAL_FEES_ADDRESS &&
|
||||||
usdt &&
|
usdt &&
|
||||||
|
@ -209,7 +213,8 @@ export async function settleAllFunds({
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const settleTransactions = (await Promise.all(
|
const settleTransactions = (
|
||||||
|
await Promise.all(
|
||||||
openOrdersAccounts.map((openOrdersAccount) => {
|
openOrdersAccounts.map((openOrdersAccount) => {
|
||||||
const market = markets.find((m) =>
|
const market = markets.find((m) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -221,12 +226,16 @@ export async function settleAllFunds({
|
||||||
const selectedBaseTokenAccount = getSelectedTokenAccountForMint(
|
const selectedBaseTokenAccount = getSelectedTokenAccountForMint(
|
||||||
tokenAccounts,
|
tokenAccounts,
|
||||||
baseMint,
|
baseMint,
|
||||||
baseMint && selectedTokenAccounts && selectedTokenAccounts[baseMint.toBase58()]
|
baseMint &&
|
||||||
|
selectedTokenAccounts &&
|
||||||
|
selectedTokenAccounts[baseMint.toBase58()],
|
||||||
)?.pubkey;
|
)?.pubkey;
|
||||||
const selectedQuoteTokenAccount = getSelectedTokenAccountForMint(
|
const selectedQuoteTokenAccount = getSelectedTokenAccountForMint(
|
||||||
tokenAccounts,
|
tokenAccounts,
|
||||||
quoteMint,
|
quoteMint,
|
||||||
quoteMint && selectedTokenAccounts && selectedTokenAccounts[quoteMint.toBase58()]
|
quoteMint &&
|
||||||
|
selectedTokenAccounts &&
|
||||||
|
selectedTokenAccounts[quoteMint.toBase58()],
|
||||||
)?.pubkey;
|
)?.pubkey;
|
||||||
if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) {
|
if (!selectedBaseTokenAccount || !selectedQuoteTokenAccount) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -241,15 +250,24 @@ export async function settleAllFunds({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)).filter((x): x is {signers: [PublicKey | Account]; transaction: Transaction} => !!x);
|
)
|
||||||
|
).filter(
|
||||||
|
(x): x is { signers: [PublicKey | Account]; transaction: Transaction } =>
|
||||||
|
!!x,
|
||||||
|
);
|
||||||
if (!settleTransactions || settleTransactions.length === 0) return;
|
if (!settleTransactions || settleTransactions.length === 0) return;
|
||||||
|
|
||||||
const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction);
|
const transactions = settleTransactions.slice(0, 4).map((t) => t.transaction);
|
||||||
const signers: Array<Account | PublicKey> = [];
|
const signers: Array<Account | PublicKey> = [];
|
||||||
settleTransactions
|
settleTransactions
|
||||||
.reduce((cumulative: Array<Account | PublicKey>, t) => cumulative.concat(t.signers), [])
|
.reduce(
|
||||||
|
(cumulative: Array<Account | PublicKey>, t) =>
|
||||||
|
cumulative.concat(t.signers),
|
||||||
|
[],
|
||||||
|
)
|
||||||
.forEach((signer) => {
|
.forEach((signer) => {
|
||||||
if (!signers.find((s) => {
|
if (
|
||||||
|
!signers.find((s) => {
|
||||||
if (s.constructor.name !== signer.constructor.name) {
|
if (s.constructor.name !== signer.constructor.name) {
|
||||||
return false;
|
return false;
|
||||||
} else if (s.constructor.name === 'PublicKey') {
|
} else if (s.constructor.name === 'PublicKey') {
|
||||||
|
@ -259,7 +277,8 @@ export async function settleAllFunds({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return s.publicKey.equals(signer.publicKey);
|
return s.publicKey.equals(signer.publicKey);
|
||||||
}
|
}
|
||||||
})) {
|
})
|
||||||
|
) {
|
||||||
signers.push(signer);
|
signers.push(signer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -274,11 +293,21 @@ export async function settleAllFunds({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelOrder(params: {market: Market; connection: Connection; wallet: Wallet; order: Order;}) {
|
export async function cancelOrder(params: {
|
||||||
|
market: Market;
|
||||||
|
connection: Connection;
|
||||||
|
wallet: Wallet;
|
||||||
|
order: Order;
|
||||||
|
}) {
|
||||||
return cancelOrders({ ...params, orders: [params.order] });
|
return cancelOrders({ ...params, orders: [params.order] });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelOrders({ market, wallet, connection, orders }: {
|
export async function cancelOrders({
|
||||||
|
market,
|
||||||
|
wallet,
|
||||||
|
connection,
|
||||||
|
orders,
|
||||||
|
}: {
|
||||||
market: Market;
|
market: Market;
|
||||||
wallet: Wallet;
|
wallet: Wallet;
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
|
@ -310,10 +339,10 @@ export async function placeOrder({
|
||||||
baseCurrencyAccount,
|
baseCurrencyAccount,
|
||||||
quoteCurrencyAccount,
|
quoteCurrencyAccount,
|
||||||
}: {
|
}: {
|
||||||
side: "buy" | "sell";
|
side: 'buy' | 'sell';
|
||||||
price: number;
|
price: number;
|
||||||
size: number;
|
size: number;
|
||||||
orderType: "ioc" | "postOnly" | "limit";
|
orderType: 'ioc' | 'postOnly' | 'limit';
|
||||||
market: Market | undefined | null;
|
market: Market | undefined | null;
|
||||||
connection: Connection;
|
connection: Connection;
|
||||||
wallet: Wallet;
|
wallet: Wallet;
|
||||||
|
@ -639,9 +668,12 @@ async function sendSignedTransaction({
|
||||||
const rawTransaction = signedTransaction.serialize();
|
const rawTransaction = signedTransaction.serialize();
|
||||||
const startTime = getUnixTs();
|
const startTime = getUnixTs();
|
||||||
notify({ message: sendingMessage });
|
notify({ message: sendingMessage });
|
||||||
const txid: TransactionSignature = await connection.sendRawTransaction(rawTransaction, {
|
const txid: TransactionSignature = await connection.sendRawTransaction(
|
||||||
|
rawTransaction,
|
||||||
|
{
|
||||||
skipPreflight: true,
|
skipPreflight: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
notify({ message: sentMessage, type: 'success', txid });
|
notify({ message: sentMessage, type: 'success', txid });
|
||||||
|
|
||||||
console.log('Started awaiting confirmation for', txid);
|
console.log('Started awaiting confirmation for', txid);
|
||||||
|
@ -754,17 +786,17 @@ function mergeTransactions(transactions: (Transaction | undefined)[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsonRpcResult(resultDescription: any) {
|
function jsonRpcResult(resultDescription: any) {
|
||||||
const jsonRpcVersion = struct.literal("2.0");
|
const jsonRpcVersion = struct.literal('2.0');
|
||||||
return struct.union([
|
return struct.union([
|
||||||
struct({
|
struct({
|
||||||
jsonrpc: jsonRpcVersion,
|
jsonrpc: jsonRpcVersion,
|
||||||
id: "string",
|
id: 'string',
|
||||||
error: "any",
|
error: 'any',
|
||||||
}),
|
}),
|
||||||
struct({
|
struct({
|
||||||
jsonrpc: jsonRpcVersion,
|
jsonrpc: jsonRpcVersion,
|
||||||
id: "string",
|
id: 'string',
|
||||||
error: "null?",
|
error: 'null?',
|
||||||
result: resultDescription,
|
result: resultDescription,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@ -773,46 +805,43 @@ function jsonRpcResult(resultDescription: any) {
|
||||||
function jsonRpcResultAndContext(resultDescription: any) {
|
function jsonRpcResultAndContext(resultDescription: any) {
|
||||||
return jsonRpcResult({
|
return jsonRpcResult({
|
||||||
context: struct({
|
context: struct({
|
||||||
slot: "number",
|
slot: 'number',
|
||||||
}),
|
}),
|
||||||
value: resultDescription,
|
value: resultDescription,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountInfoResult = struct({
|
const AccountInfoResult = struct({
|
||||||
executable: "boolean",
|
executable: 'boolean',
|
||||||
owner: "string",
|
owner: 'string',
|
||||||
lamports: "number",
|
lamports: 'number',
|
||||||
data: "any",
|
data: 'any',
|
||||||
rentEpoch: "number?",
|
rentEpoch: 'number?',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
|
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
|
||||||
struct.array([struct.union(["null", AccountInfoResult])])
|
struct.array([struct.union(['null', AccountInfoResult])]),
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function getMultipleSolanaAccounts(
|
export async function getMultipleSolanaAccounts(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
publicKeys: PublicKey[]
|
publicKeys: PublicKey[],
|
||||||
): Promise<
|
): Promise<
|
||||||
RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }>
|
RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }>
|
||||||
> {
|
> {
|
||||||
const args = [
|
const args = [publicKeys.map((k) => k.toBase58()), { commitment: 'recent' }];
|
||||||
publicKeys.map((k) => k.toBase58()),
|
|
||||||
{ commitment: "recent" },
|
|
||||||
];
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args);
|
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
|
||||||
const res = GetMultipleAccountsAndContextRpcResult(unsafeRes);
|
const res = GetMultipleAccountsAndContextRpcResult(unsafeRes);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"failed to get info about accounts " +
|
'failed to get info about accounts ' +
|
||||||
publicKeys.map((k) => k.toBase58()).join(", ") +
|
publicKeys.map((k) => k.toBase58()).join(', ') +
|
||||||
": " +
|
': ' +
|
||||||
res.error.message
|
res.error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert(typeof res.result !== "undefined");
|
assert(typeof res.result !== 'undefined');
|
||||||
const accounts: Array<{
|
const accounts: Array<{
|
||||||
executable: any;
|
executable: any;
|
||||||
owner: PublicKey;
|
owner: PublicKey;
|
||||||
|
@ -828,12 +857,12 @@ export async function getMultipleSolanaAccounts(
|
||||||
} | null = null;
|
} | null = null;
|
||||||
if (res.result.value) {
|
if (res.result.value) {
|
||||||
const { executable, owner, lamports, data } = account;
|
const { executable, owner, lamports, data } = account;
|
||||||
assert(data[1] === "base64");
|
assert(data[1] === 'base64');
|
||||||
value = {
|
value = {
|
||||||
executable,
|
executable,
|
||||||
owner: new PublicKey(owner),
|
owner: new PublicKey(owner),
|
||||||
lamports,
|
lamports,
|
||||||
data: Buffer.from(data[0], "base64"),
|
data: Buffer.from(data[0], 'base64'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
accounts.push(value);
|
accounts.push(value);
|
||||||
|
@ -843,7 +872,7 @@ export async function getMultipleSolanaAccounts(
|
||||||
slot: res.result.context.slot,
|
slot: res.result.context.slot,
|
||||||
},
|
},
|
||||||
value: Object.fromEntries(
|
value: Object.fromEntries(
|
||||||
accounts.map((account, i) => [publicKeys[i].toBase58(), account])
|
accounts.map((account, i) => [publicKeys[i].toBase58(), account]),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import * as BufferLayout from 'buffer-layout';
|
||||||
import bs58 from 'bs58';
|
import bs58 from 'bs58';
|
||||||
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
|
||||||
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
|
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
|
||||||
import {TokenAccount} from "./types";
|
import { TokenAccount } from './types';
|
||||||
import {TOKEN_MINTS} from "@project-serum/serum";
|
import { TOKEN_MINTS } from '@project-serum/serum';
|
||||||
import {useAllMarkets, useMarket, useTokenAccounts} from "./markets";
|
import { useAllMarkets, useMarket, useTokenAccounts } from './markets';
|
||||||
import {getMultipleSolanaAccounts} from "./send";
|
import { getMultipleSolanaAccounts } from './send';
|
||||||
import {useConnection} from "./connection";
|
import { useConnection } from './connection';
|
||||||
import {useAsyncData} from "./fetch-loop";
|
import { useAsyncData } from './fetch-loop';
|
||||||
import tuple from 'immutable-tuple';
|
import tuple from 'immutable-tuple';
|
||||||
|
|
||||||
export const ACCOUNT_LAYOUT = BufferLayout.struct([
|
export const ACCOUNT_LAYOUT = BufferLayout.struct([
|
||||||
|
@ -25,7 +25,7 @@ export const MINT_LAYOUT = BufferLayout.struct([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function parseTokenAccountData(
|
export function parseTokenAccountData(
|
||||||
data: Buffer
|
data: Buffer,
|
||||||
): { mint: PublicKey; owner: PublicKey; amount: number } {
|
): { mint: PublicKey; owner: PublicKey; amount: number } {
|
||||||
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
|
let { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
|
||||||
return {
|
return {
|
||||||
|
@ -59,8 +59,9 @@ export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function getOwnedTokenAccounts(
|
export async function getOwnedTokenAccounts(
|
||||||
connection: Connection, publicKey: PublicKey
|
connection: Connection,
|
||||||
): Promise<Array<{publicKey: PublicKey, accountInfo: AccountInfo<Buffer>}>> {
|
publicKey: PublicKey,
|
||||||
|
): Promise<Array<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }>> {
|
||||||
let filters = getOwnedAccountsFilters(publicKey);
|
let filters = getOwnedAccountsFilters(publicKey);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let resp = await connection._rpcRequest('getProgramAccounts', [
|
let resp = await connection._rpcRequest('getProgramAccounts', [
|
||||||
|
@ -107,18 +108,23 @@ export async function getOwnedTokenAccounts(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTokenAccountInfo(connection: Connection, ownerAddress: PublicKey) {
|
export async function getTokenAccountInfo(
|
||||||
|
connection: Connection,
|
||||||
|
ownerAddress: PublicKey,
|
||||||
|
) {
|
||||||
let [splAccounts, account] = await Promise.all([
|
let [splAccounts, account] = await Promise.all([
|
||||||
getOwnedTokenAccounts(connection, ownerAddress),
|
getOwnedTokenAccounts(connection, ownerAddress),
|
||||||
connection.getAccountInfo(ownerAddress),
|
connection.getAccountInfo(ownerAddress),
|
||||||
]);
|
]);
|
||||||
const parsedSplAccounts: TokenAccount[] = splAccounts.map(({ publicKey, accountInfo }) => {
|
const parsedSplAccounts: TokenAccount[] = splAccounts.map(
|
||||||
|
({ publicKey, accountInfo }) => {
|
||||||
return {
|
return {
|
||||||
pubkey: publicKey,
|
pubkey: publicKey,
|
||||||
account: accountInfo,
|
account: accountInfo,
|
||||||
effectiveMint: parseTokenAccountData(accountInfo.data).mint,
|
effectiveMint: parseTokenAccountData(accountInfo.data).mint,
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return parsedSplAccounts.concat({
|
return parsedSplAccounts.concat({
|
||||||
pubkey: ownerAddress,
|
pubkey: ownerAddress,
|
||||||
account,
|
account,
|
||||||
|
@ -126,22 +132,27 @@ export async function getTokenAccountInfo(connection: Connection, ownerAddress:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMintToTickers(): { [mint: string]: string; } {
|
export function useMintToTickers(): { [mint: string]: string } {
|
||||||
const { customMarkets } = useMarket();
|
const { customMarkets } = useMarket();
|
||||||
const [markets] = useAllMarkets(customMarkets);
|
const [markets] = useAllMarkets(customMarkets);
|
||||||
const mintsToTickers = Object.fromEntries(TOKEN_MINTS.map(mint => [mint.address.toBase58(), mint.name]));
|
const mintsToTickers = Object.fromEntries(
|
||||||
for (let market of (markets || [])) {
|
TOKEN_MINTS.map((mint) => [mint.address.toBase58(), mint.name]),
|
||||||
|
);
|
||||||
|
for (let market of markets || []) {
|
||||||
const customMarketInfo = customMarkets.find(
|
const customMarketInfo = customMarkets.find(
|
||||||
customMarket => customMarket.address === market.market.address.toBase58()
|
(customMarket) =>
|
||||||
|
customMarket.address === market.market.address.toBase58(),
|
||||||
);
|
);
|
||||||
if (!(market.market.baseMintAddress.toBase58() in mintsToTickers)) {
|
if (!(market.market.baseMintAddress.toBase58() in mintsToTickers)) {
|
||||||
if (customMarketInfo) {
|
if (customMarketInfo) {
|
||||||
mintsToTickers[market.market.baseMintAddress.toBase58()] = customMarketInfo.baseLabel || `${customMarketInfo.name}_BASE`;
|
mintsToTickers[market.market.baseMintAddress.toBase58()] =
|
||||||
|
customMarketInfo.baseLabel || `${customMarketInfo.name}_BASE`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(market.market.quoteMintAddress.toBase58() in mintsToTickers)) {
|
if (!(market.market.quoteMintAddress.toBase58() in mintsToTickers)) {
|
||||||
if (customMarketInfo) {
|
if (customMarketInfo) {
|
||||||
mintsToTickers[market.market.quoteMintAddress.toBase58()] = customMarketInfo.quoteLabel || `${customMarketInfo.name}_QUOTE`;
|
mintsToTickers[market.market.quoteMintAddress.toBase58()] =
|
||||||
|
customMarketInfo.quoteLabel || `${customMarketInfo.name}_QUOTE`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,30 +162,46 @@ export function useMintToTickers(): { [mint: string]: string; } {
|
||||||
const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
|
const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
|
||||||
|
|
||||||
export function useMintInfos(): [
|
export function useMintInfos(): [
|
||||||
{[mintAddress: string]: {decimals: number; initialized: boolean} | null} | null | undefined,
|
(
|
||||||
boolean
|
| {
|
||||||
|
[mintAddress: string]: {
|
||||||
|
decimals: number;
|
||||||
|
initialized: boolean;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
),
|
||||||
|
boolean,
|
||||||
] {
|
] {
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const { customMarkets } = useMarket();
|
const { customMarkets } = useMarket();
|
||||||
const [tokenAccounts] = useTokenAccounts();
|
const [tokenAccounts] = useTokenAccounts();
|
||||||
const [allMarkets] = useAllMarkets(customMarkets);
|
const [allMarkets] = useAllMarkets(customMarkets);
|
||||||
|
|
||||||
const allMints = (tokenAccounts || []).map(account => account.effectiveMint).concat(
|
const allMints = (tokenAccounts || [])
|
||||||
(allMarkets || []).map(marketInfo => marketInfo.market.baseMintAddress)
|
.map((account) => account.effectiveMint)
|
||||||
).concat(
|
.concat(
|
||||||
(allMarkets || []).map(marketInfo => marketInfo.market.quoteMintAddress)
|
(allMarkets || []).map((marketInfo) => marketInfo.market.baseMintAddress),
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
|
(allMarkets || []).map(
|
||||||
|
(marketInfo) => marketInfo.market.quoteMintAddress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const uniqueMints = [...new Set(allMints.map((mint) => mint.toBase58()))].map(
|
||||||
|
(stringMint) => new PublicKey(stringMint),
|
||||||
);
|
);
|
||||||
const uniqueMints = [...new Set(allMints.map(mint => mint.toBase58()))].map(stringMint => new PublicKey(stringMint))
|
|
||||||
|
|
||||||
const getAllMintInfo = async () => {
|
const getAllMintInfo = async () => {
|
||||||
const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints);
|
const mintInfos = await getMultipleSolanaAccounts(connection, uniqueMints);
|
||||||
return Object.fromEntries(Object.entries(mintInfos.value).map(
|
return Object.fromEntries(
|
||||||
([key, accountInfo]) => [
|
Object.entries(mintInfos.value).map(([key, accountInfo]) => [
|
||||||
key,
|
key,
|
||||||
accountInfo && parseTokenMintData(accountInfo.data)
|
accountInfo && parseTokenMintData(accountInfo.data),
|
||||||
]
|
]),
|
||||||
));
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return useAsyncData(
|
return useAsyncData(
|
||||||
getAllMintInfo,
|
getAllMintInfo,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import BN from "bn.js";
|
import BN from 'bn.js';
|
||||||
|
|
||||||
export function isValidPublicKey(key) {
|
export function isValidPublicKey(key) {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -24,11 +24,17 @@ export const percentFormat = new Intl.NumberFormat(undefined, {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function floorToDecimal(value: number, decimals: number | undefined | null) {
|
export function floorToDecimal(
|
||||||
|
value: number,
|
||||||
|
decimals: number | undefined | null,
|
||||||
|
) {
|
||||||
return decimals ? Math.floor(value * 10 ** decimals) / 10 ** decimals : value;
|
return decimals ? Math.floor(value * 10 ** decimals) / 10 ** decimals : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function roundToDecimal(value: number, decimals: number | undefined | null) {
|
export function roundToDecimal(
|
||||||
|
value: number,
|
||||||
|
decimals: number | undefined | null,
|
||||||
|
) {
|
||||||
return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value;
|
return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +72,7 @@ export function useLocalStorageStringState(
|
||||||
localStorageListeners[key].push(notify);
|
localStorageListeners[key].push(notify);
|
||||||
return () => {
|
return () => {
|
||||||
localStorageListeners[key] = localStorageListeners[key].filter(
|
localStorageListeners[key] = localStorageListeners[key].filter(
|
||||||
listener => listener !== notify,
|
(listener) => listener !== notify,
|
||||||
);
|
);
|
||||||
if (localStorageListeners[key].length === 0) {
|
if (localStorageListeners[key].length === 0) {
|
||||||
delete localStorageListeners[key];
|
delete localStorageListeners[key];
|
||||||
|
@ -75,7 +81,7 @@ export function useLocalStorageStringState(
|
||||||
}, [key]);
|
}, [key]);
|
||||||
|
|
||||||
const setState = useCallback<(newState: string | null) => void>(
|
const setState = useCallback<(newState: string | null) => void>(
|
||||||
newState => {
|
(newState) => {
|
||||||
const changed = state !== newState;
|
const changed = state !== newState;
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
return;
|
return;
|
||||||
|
@ -86,7 +92,9 @@ export function useLocalStorageStringState(
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(key, newState);
|
localStorage.setItem(key, newState);
|
||||||
}
|
}
|
||||||
localStorageListeners[key].forEach(listener => listener(key + '\n' + newState));
|
localStorageListeners[key].forEach((listener) =>
|
||||||
|
listener(key + '\n' + newState),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[state, key],
|
[state, key],
|
||||||
);
|
);
|
||||||
|
@ -94,9 +102,18 @@ export function useLocalStorageStringState(
|
||||||
return [state, setState];
|
return [state, setState];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLocalStorageState<T = any>(key: string, defaultState: T | null = null): [T, (newState: T) => void] {
|
export function useLocalStorageState<T = any>(
|
||||||
let [stringState, setStringState] = useLocalStorageStringState(key, JSON.stringify(defaultState));
|
key: string,
|
||||||
return [stringState && JSON.parse(stringState), newState => setStringState(JSON.stringify(newState))];
|
defaultState: T | null = null,
|
||||||
|
): [T, (newState: T) => void] {
|
||||||
|
let [stringState, setStringState] = useLocalStorageStringState(
|
||||||
|
key,
|
||||||
|
JSON.stringify(defaultState),
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
stringState && JSON.parse(stringState),
|
||||||
|
(newState) => setStringState(JSON.stringify(newState)),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEffectAfterTimeout(effect, timeout) {
|
export function useEffectAfterTimeout(effect, timeout) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Wallet from '@project-serum/sol-wallet-adapter';
|
||||||
import { notify } from './notifications';
|
import { notify } from './notifications';
|
||||||
import { useConnectionConfig } from './connection';
|
import { useConnectionConfig } from './connection';
|
||||||
import { useLocalStorageState } from './utils';
|
import { useLocalStorageState } from './utils';
|
||||||
import {WalletContextValues} from "./types";
|
import { WalletContextValues } from './types';
|
||||||
|
|
||||||
export const WALLET_PROVIDERS = [
|
export const WALLET_PROVIDERS = [
|
||||||
{ name: 'sollet.io', url: 'https://www.sollet.io' },
|
{ name: 'sollet.io', url: 'https://www.sollet.io' },
|
||||||
|
@ -83,7 +83,7 @@ export function WalletProvider({ children }) {
|
||||||
export function useWallet() {
|
export function useWallet() {
|
||||||
const context = useContext(WalletContext);
|
const context = useContext(WalletContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('Missing wallet context')
|
throw new Error('Missing wallet context');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
connected: context.connected,
|
connected: context.connected,
|
||||||
|
|
|
@ -16,17 +16,8 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"lib": [
|
"lib": ["dom", "esnext"]
|
||||||
"dom",
|
|
||||||
"esnext"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src/"],
|
||||||
"./src/"
|
"exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"]
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"./src/**/*.test.js",
|
|
||||||
"node_modules",
|
|
||||||
"**/node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue