Allow specifying preferred token account
This commit is contained in:
parent
e516603e88
commit
6783fbe8c8
|
@ -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 {
|
||||
|
@ -8,13 +8,16 @@ import {
|
|||
useSelectedBaseCurrencyAccount,
|
||||
useSelectedOpenOrdersAccount,
|
||||
useSelectedQuoteCurrencyAccount,
|
||||
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 {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;
|
||||
|
@ -36,16 +39,50 @@ export default function StandaloneBalancesDisplay() {
|
|||
const balances = useBalances();
|
||||
const openOrdersAccount = useSelectedOpenOrdersAccount(true);
|
||||
const connection = useSendConnection();
|
||||
const { providerUrl, providerName, wallet } = useWallet();
|
||||
const { providerUrl, providerName, wallet, connected } = useWallet();
|
||||
const [baseOrQuote, setBaseOrQuote] = useState('');
|
||||
const baseCurrencyAccount = useSelectedBaseCurrencyAccount();
|
||||
const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount();
|
||||
const [tokenAccounts] = useTokenAccounts();
|
||||
const baseCurrencyBalances =
|
||||
balances && balances.find((b) => b.coin === baseCurrency);
|
||||
const quoteCurrencyBalances =
|
||||
balances && balances.find((b) => b.coin === quoteCurrency);
|
||||
|
||||
async function onSettleFunds() {
|
||||
if (!market) {
|
||||
notify({
|
||||
message: 'Error settling funds',
|
||||
description: 'market is undefined',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!openOrdersAccount) {
|
||||
notify({
|
||||
message: 'Error settling funds',
|
||||
description: 'Open orders account is undefined',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!baseCurrencyAccount) {
|
||||
notify({
|
||||
message: 'Error settling funds',
|
||||
description: 'Open orders account is undefined',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!quoteCurrencyAccount) {
|
||||
notify({
|
||||
message: 'Error settling funds',
|
||||
description: 'Open orders account is undefined',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await settleFunds({
|
||||
market,
|
||||
|
@ -64,14 +101,22 @@ 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()],
|
||||
]
|
||||
|
||||
return (
|
||||
<FloatingElement style={{ flex: 1, paddingTop: 10 }}>
|
||||
{[
|
||||
[baseCurrency, baseCurrencyBalances, 'base'],
|
||||
[quoteCurrency, quoteCurrencyBalances, 'quote'],
|
||||
].map(([currency, balances, baseOrQuote], index) => (
|
||||
{formattedBalances.map(([currency, balances, baseOrQuote, mint], index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Divider style={{ borderColor: 'white' }}>{currency}</Divider>
|
||||
{connected && (
|
||||
<StandaloneTokenAccountsSelect
|
||||
accounts={tokenAccounts?.filter(account => account.effectiveMint.toBase58() === mint)}
|
||||
mint={mint}
|
||||
/>
|
||||
)}
|
||||
<RowBox
|
||||
align="middle"
|
||||
justify="space-between"
|
|
@ -0,0 +1,76 @@
|
|||
import React from 'react';
|
||||
import {TokenAccount} from "../utils/types";
|
||||
import styled from 'styled-components';
|
||||
import {useSelectedTokenAccounts} from "../utils/markets";
|
||||
import {Button, Col, Row, Select, Typography} from "antd";
|
||||
import {CopyOutlined} from '@ant-design/icons';
|
||||
import {abbreviateAddress} from "../utils/utils";
|
||||
import {notify} from "../utils/notifications";
|
||||
|
||||
const RowBox = styled(Row)`
|
||||
padding-bottom: 10px;
|
||||
`;
|
||||
|
||||
export default function StandaloneTokenAccountsSelect({
|
||||
accounts,
|
||||
mint
|
||||
}: {
|
||||
accounts: TokenAccount[] | null | undefined,
|
||||
mint: string | undefined
|
||||
}) {
|
||||
const [selectedTokenAccounts, setSelectedTokenAccounts] = useSelectedTokenAccounts();
|
||||
|
||||
let selectedValue: string | undefined;
|
||||
if (mint && mint in selectedTokenAccounts) {
|
||||
selectedValue = selectedTokenAccounts[mint];
|
||||
} else if (accounts && accounts?.length > 0) {
|
||||
selectedValue = accounts[0].pubkey.toBase58();
|
||||
} else {
|
||||
selectedValue = undefined;
|
||||
}
|
||||
|
||||
const setTokenAccountForCoin = (value) => {
|
||||
if (!mint) {
|
||||
notify({
|
||||
message: 'Error selecting token account',
|
||||
description: 'Mint is undefined',
|
||||
type: 'error',
|
||||
})
|
||||
return;
|
||||
}
|
||||
const newSelectedTokenAccounts = {...selectedTokenAccounts};
|
||||
newSelectedTokenAccounts[mint] = value;
|
||||
setSelectedTokenAccounts(newSelectedTokenAccounts);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RowBox align="middle" >
|
||||
<Col span={8}>
|
||||
Token account:
|
||||
</Col>
|
||||
<Col span={13}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={selectedValue}
|
||||
onSelect={setTokenAccountForCoin}
|
||||
>
|
||||
{accounts?.map(account => (
|
||||
<Select.Option key={account.pubkey.toBase58()} value={account.pubkey.toBase58()}>
|
||||
<Typography.Text code>{abbreviateAddress(account.pubkey, 8)}</Typography.Text>
|
||||
</Select.Option>)
|
||||
)}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={2} offset={1}>
|
||||
<Button
|
||||
shape="round"
|
||||
icon={<CopyOutlined />}
|
||||
size={'small'}
|
||||
onClick={() => selectedValue && navigator.clipboard.writeText(selectedValue)}
|
||||
/>
|
||||
</Col>
|
||||
</RowBox>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -2,8 +2,8 @@ import { Button, Input, Radio, Switch, Slider } from 'antd';
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
useBaseCurrencyBalances,
|
||||
useQuoteCurrencyBalances,
|
||||
useSelectedBaseCurrencyBalances,
|
||||
useSelectedQuoteCurrencyBalances,
|
||||
useMarket,
|
||||
useMarkPrice,
|
||||
useSelectedOpenOrdersAccount,
|
||||
|
@ -20,6 +20,7 @@ import {
|
|||
import { useSendConnection } from '../utils/connection';
|
||||
import FloatingElement from './layout/FloatingElement';
|
||||
import { placeOrder } from '../utils/send';
|
||||
import {SwitchChangeEventHandler} from "antd/es/switch";
|
||||
|
||||
const SellButton = styled(Button)`
|
||||
margin: 20px 0px 0px 0px;
|
||||
|
@ -41,11 +42,14 @@ const sliderMarks = {
|
|||
100: '100%',
|
||||
};
|
||||
|
||||
export default function TradeForm({ style, setChangeOrderRef }) {
|
||||
const [side, setSide] = useState('buy');
|
||||
export default function TradeForm({ style, setChangeOrderRef }: {
|
||||
style?: any;
|
||||
setChangeOrderRef?: (ref: ({ size, price }: {size?: number; price?: number;}) => void) => void;
|
||||
}) {
|
||||
const [side, setSide] = useState<'buy' | 'sell'>('buy');
|
||||
const { baseCurrency, quoteCurrency, market } = useMarket();
|
||||
const baseCurrencyBalances = useBaseCurrencyBalances();
|
||||
const quoteCurrencyBalances = useQuoteCurrencyBalances();
|
||||
const baseCurrencyBalances = useSelectedBaseCurrencyBalances();
|
||||
const quoteCurrencyBalances = useSelectedQuoteCurrencyBalances();
|
||||
const baseCurrencyAccount = useSelectedBaseCurrencyAccount();
|
||||
const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount();
|
||||
const openOrdersAccount = useSelectedOpenOrdersAccount(true);
|
||||
|
@ -55,13 +59,13 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
|
||||
const [postOnly, setPostOnly] = useState(false);
|
||||
const [ioc, setIoc] = useState(false);
|
||||
const [baseSize, setBaseSize] = useState(null);
|
||||
const [quoteSize, setQuoteSize] = useState(null);
|
||||
const [price, setPrice] = useState(null);
|
||||
const [baseSize, setBaseSize] = useState<number | undefined>(undefined);
|
||||
const [quoteSize, setQuoteSize] = useState<number | undefined>(undefined);
|
||||
const [price, setPrice] = useState<number | undefined>(undefined);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [sizeFraction, setSizeFraction] = useState(0);
|
||||
|
||||
const availableQuote = openOrdersAccount
|
||||
const availableQuote = openOrdersAccount && market
|
||||
? market.quoteSplSizeToNumber(openOrdersAccount.quoteTokenFree)
|
||||
: 0;
|
||||
|
||||
|
@ -86,22 +90,40 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [price, baseSize]);
|
||||
|
||||
const onSetBaseSize = (baseSize) => {
|
||||
const onSetBaseSize = (baseSize: number | undefined) => {
|
||||
setBaseSize(baseSize);
|
||||
const rawQuoteSize = baseSize * (price || markPrice);
|
||||
if (!baseSize) {
|
||||
setQuoteSize(undefined);
|
||||
return;
|
||||
}
|
||||
let usePrice = price || markPrice;
|
||||
if (!usePrice) {
|
||||
setQuoteSize(undefined);
|
||||
return;
|
||||
}
|
||||
const rawQuoteSize = baseSize * usePrice;
|
||||
const quoteSize =
|
||||
baseSize && roundToDecimal(rawQuoteSize, sizeDecimalCount);
|
||||
setQuoteSize(quoteSize);
|
||||
};
|
||||
|
||||
const onSetQuoteSize = (quoteSize) => {
|
||||
const onSetQuoteSize = (quoteSize: number | undefined) => {
|
||||
setQuoteSize(quoteSize);
|
||||
const rawBaseSize = quoteSize / price;
|
||||
if (!quoteSize) {
|
||||
setBaseSize(undefined);
|
||||
return;
|
||||
}
|
||||
let usePrice = price || markPrice;
|
||||
if (!usePrice) {
|
||||
setBaseSize(undefined);
|
||||
return;
|
||||
}
|
||||
const rawBaseSize = quoteSize / usePrice;
|
||||
const baseSize = quoteSize && roundToDecimal(rawBaseSize, sizeDecimalCount);
|
||||
setBaseSize(baseSize);
|
||||
};
|
||||
|
||||
const doChangeOrder = ({ size, price }) => {
|
||||
const doChangeOrder = ({ size, price }: {size?: number; price?: number;}) => {
|
||||
const formattedSize = size && roundToDecimal(size, sizeDecimalCount);
|
||||
const formattedPrice = price && roundToDecimal(price, priceDecimalCount);
|
||||
formattedSize && onSetBaseSize(formattedSize);
|
||||
|
@ -109,24 +131,24 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
};
|
||||
|
||||
const updateSizeFraction = () => {
|
||||
const rawMaxSize = side === 'buy' ? quoteBalance / price : baseBalance;
|
||||
const rawMaxSize = side === 'buy' ? quoteBalance / (price || markPrice || 1.) : baseBalance;
|
||||
const maxSize = floorToDecimal(rawMaxSize, sizeDecimalCount);
|
||||
const sizeFraction = Math.min((baseSize / maxSize) * 100, 100);
|
||||
const sizeFraction = Math.min(((baseSize || 0.) / maxSize) * 100, 100);
|
||||
setSizeFraction(sizeFraction);
|
||||
};
|
||||
|
||||
const onSliderChange = (value) => {
|
||||
if (!price && markPrice) {
|
||||
let formattedMarkPrice = priceDecimalCount
|
||||
let formattedMarkPrice: number | string = priceDecimalCount
|
||||
? markPrice.toFixed(priceDecimalCount)
|
||||
: markPrice;
|
||||
setPrice(formattedMarkPrice);
|
||||
setPrice(typeof formattedMarkPrice === 'number' ? formattedMarkPrice : parseFloat(formattedMarkPrice));
|
||||
}
|
||||
|
||||
let newSize;
|
||||
if (side === 'buy') {
|
||||
if (price || markPrice) {
|
||||
newSize = ((quoteBalance / (price || markPrice)) * value) / 100;
|
||||
newSize = ((quoteBalance / (price || markPrice || 1.)) * value) / 100;
|
||||
}
|
||||
} else {
|
||||
newSize = (baseBalance * value) / 100;
|
||||
|
@ -138,13 +160,13 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
onSetBaseSize(formatted);
|
||||
};
|
||||
|
||||
const postOnChange = (checked) => {
|
||||
const postOnChange: SwitchChangeEventHandler = (checked) => {
|
||||
if (checked) {
|
||||
setIoc(false);
|
||||
}
|
||||
setPostOnly(checked);
|
||||
};
|
||||
const iocOnChange = (checked) => {
|
||||
const iocOnChange: SwitchChangeEventHandler = (checked) => {
|
||||
if (checked) {
|
||||
setPostOnly(false);
|
||||
}
|
||||
|
@ -152,15 +174,28 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
};
|
||||
|
||||
async function onSubmit() {
|
||||
const parsedPrice = parseFloat(price);
|
||||
const parsedSize = parseFloat(baseSize);
|
||||
if (!price) {
|
||||
console.warn('Missing price');
|
||||
notify({
|
||||
message: 'Missing price',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} else if (!baseSize) {
|
||||
console.warn('Missing size');
|
||||
notify({
|
||||
message: 'Missing size',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await placeOrder({
|
||||
side,
|
||||
price: parsedPrice,
|
||||
size: parsedSize,
|
||||
price,
|
||||
size: baseSize,
|
||||
orderType: ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit',
|
||||
market,
|
||||
connection: sendConnection,
|
||||
|
@ -168,8 +203,8 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
baseCurrencyAccount: baseCurrencyAccount?.pubkey,
|
||||
quoteCurrencyAccount: quoteCurrencyAccount?.pubkey,
|
||||
});
|
||||
setPrice(null);
|
||||
onSetBaseSize(null);
|
||||
setPrice(undefined);
|
||||
onSetBaseSize(undefined);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
notify({
|
||||
|
@ -228,7 +263,7 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
value={price}
|
||||
type="number"
|
||||
step={market?.tickSize || 1}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
||||
/>
|
||||
<Input.Group compact style={{ paddingBottom: 8 }}>
|
||||
<Input
|
||||
|
@ -240,7 +275,7 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
value={baseSize}
|
||||
type="number"
|
||||
step={market?.minOrderSize || 1}
|
||||
onChange={(e) => onSetBaseSize(e.target.value)}
|
||||
onChange={(e) => onSetBaseSize(parseFloat(e.target.value))}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: 'calc(50% - 30px)', textAlign: 'right' }}
|
||||
|
@ -252,7 +287,7 @@ export default function TradeForm({ style, setChangeOrderRef }) {
|
|||
value={quoteSize}
|
||||
type="number"
|
||||
step={market?.minOrderSize || 1}
|
||||
onChange={(e) => onSetQuoteSize(e.target.value)}
|
||||
onChange={(e) => onSetQuoteSize(parseFloat(e.target.value))}
|
||||
/>
|
||||
</Input.Group>
|
||||
<Slider
|
|
@ -55,7 +55,7 @@ export default function TradePage() {
|
|||
document.title = marketName ? `${marketName} — Serum` : 'Serum';
|
||||
}, [marketName]);
|
||||
|
||||
const changeOrderRef = useRef();
|
||||
const changeOrderRef = useRef<({ size, price }: {size?: number; price?: number;}) => void>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
|
@ -215,7 +215,7 @@ function MarketSelector({
|
|||
listHeight={400}
|
||||
value={selectedMarket}
|
||||
filterOption={(input, option) =>
|
||||
option.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{customMarkets && customMarkets.length > 0 && (
|
||||
|
@ -227,6 +227,7 @@ function MarketSelector({
|
|||
name={name}
|
||||
style={{
|
||||
padding: '10px',
|
||||
// @ts-ignore
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(39, 44, 61)' : null,
|
||||
}}
|
||||
>
|
||||
|
@ -272,6 +273,7 @@ function MarketSelector({
|
|||
name={name}
|
||||
style={{
|
||||
padding: '10px',
|
||||
// @ts-ignore
|
||||
backgroundColor: i % 2 === 0 ? 'rgb(39, 44, 61)' : null,
|
||||
}}
|
||||
>
|
||||
|
@ -301,7 +303,7 @@ const RenderNormal = ({ onChangeOrderRef, onPrice, onSize }) => {
|
|||
return (
|
||||
<Row
|
||||
style={{
|
||||
minHeight: '800px',
|
||||
minHeight: '900px',
|
||||
flexWrap: 'nowrap',
|
||||
}}
|
||||
>
|
||||
|
@ -328,7 +330,7 @@ const RenderSmall = ({ onChangeOrderRef, onPrice, onSize }) => {
|
|||
<>
|
||||
<Row
|
||||
style={{
|
||||
height: '800px',
|
||||
height: '900px',
|
||||
}}
|
||||
>
|
||||
<Col flex="auto" style={{ height: '100%', display: 'flex' }}>
|
|
@ -7,7 +7,7 @@ import {
|
|||
TOKEN_MINTS,
|
||||
TokenInstructions,
|
||||
} from '@project-serum/serum';
|
||||
import {AccountInfo, PublicKey, RpcResponseAndContext, TokenAmount} from '@solana/web3.js';
|
||||
import {PublicKey} from '@solana/web3.js';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import {useLocalStorageState} from './utils';
|
||||
import {refreshCache, useAsyncData} from './fetch-loop';
|
||||
|
@ -26,10 +26,10 @@ import {
|
|||
MarketInfo,
|
||||
OrderWithMarket,
|
||||
OrderWithMarketAndMarketName,
|
||||
SelectedTokenAccounts,
|
||||
TokenAccount,
|
||||
Trade,
|
||||
} from "./types";
|
||||
import {Buffer} from "buffer";
|
||||
|
||||
// Used in debugging, should be false in production
|
||||
const _IGNORE_DEPRECATED = false;
|
||||
|
@ -199,10 +199,10 @@ export function MarketProvider({ children }) {
|
|||
[],
|
||||
);
|
||||
|
||||
const address = new PublicKey(marketAddress);
|
||||
const address = marketAddress && new PublicKey(marketAddress);
|
||||
const connection = useConnection();
|
||||
const marketInfos = getMarketInfos(customMarkets);
|
||||
const marketInfo = marketInfos.find((market) =>
|
||||
const marketInfo = address && marketInfos.find((market) =>
|
||||
market.address.equals(address),
|
||||
);
|
||||
|
||||
|
@ -263,6 +263,13 @@ export function MarketProvider({ children }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function useSelectedTokenAccounts(): [SelectedTokenAccounts, (newSelectedTokenAccounts: SelectedTokenAccounts) => void] {
|
||||
const [selectedTokenAccounts, setSelectedTokenAccounts] = useLocalStorageState<SelectedTokenAccounts>(
|
||||
'selectedTokenAccounts', {}
|
||||
);
|
||||
return [selectedTokenAccounts, setSelectedTokenAccounts]
|
||||
}
|
||||
|
||||
export function useMarket() {
|
||||
const context = useContext(MarketContext);
|
||||
if (!context) {
|
||||
|
@ -398,12 +405,17 @@ export function useTokenAccounts(): [TokenAccount[] | null | undefined, boolean]
|
|||
);
|
||||
}
|
||||
|
||||
export function getSelectedTokenAccountForMint(accounts: TokenAccount[] | undefined | null, mint: PublicKey | undefined) {
|
||||
export function getSelectedTokenAccountForMint(
|
||||
accounts: TokenAccount[] | undefined | null,
|
||||
mint: PublicKey | undefined,
|
||||
selectedPubKey?: string | PublicKey | null,
|
||||
) {
|
||||
if (!accounts || !mint) {
|
||||
return null;
|
||||
}
|
||||
const filtered = accounts.filter(({ effectiveMint }) =>
|
||||
mint.equals(effectiveMint),
|
||||
const filtered = accounts.filter(({ effectiveMint, pubkey }) =>
|
||||
mint.equals(effectiveMint) && (!selectedPubKey ||
|
||||
(typeof selectedPubKey === 'string' ? selectedPubKey : selectedPubKey.toBase58()) === pubkey.toBase58())
|
||||
);
|
||||
return filtered && filtered[0];
|
||||
}
|
||||
|
@ -411,17 +423,29 @@ export function getSelectedTokenAccountForMint(accounts: TokenAccount[] | undefi
|
|||
export function useSelectedQuoteCurrencyAccount() {
|
||||
const [accounts] = useTokenAccounts();
|
||||
const { market } = useMarket();
|
||||
return getSelectedTokenAccountForMint(accounts, market?.quoteMintAddress);
|
||||
const [selectedTokenAccounts] = useSelectedTokenAccounts();
|
||||
const mintAddress = market?.quoteMintAddress;
|
||||
return getSelectedTokenAccountForMint(
|
||||
accounts,
|
||||
mintAddress,
|
||||
mintAddress && selectedTokenAccounts[mintAddress.toBase58()]
|
||||
);
|
||||
}
|
||||
|
||||
export function useSelectedBaseCurrencyAccount() {
|
||||
const [accounts] = useTokenAccounts();
|
||||
const { market } = useMarket();
|
||||
return getSelectedTokenAccountForMint(accounts, market?.baseMintAddress);
|
||||
const [selectedTokenAccounts] = useSelectedTokenAccounts();
|
||||
const mintAddress = market?.baseMintAddress;
|
||||
return getSelectedTokenAccountForMint(
|
||||
accounts,
|
||||
mintAddress,
|
||||
mintAddress && selectedTokenAccounts[mintAddress.toBase58()]
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Update to use websocket
|
||||
export function useQuoteCurrencyBalances() {
|
||||
export function useSelectedQuoteCurrencyBalances() {
|
||||
const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount();
|
||||
const { market } = useMarket();
|
||||
const [accountInfo, loaded] = useAccountInfo(quoteCurrencyAccount?.pubkey);
|
||||
|
@ -437,7 +461,7 @@ export function useQuoteCurrencyBalances() {
|
|||
}
|
||||
|
||||
// TODO: Update to use websocket
|
||||
export function useBaseCurrencyBalances() {
|
||||
export function useSelectedBaseCurrencyBalances() {
|
||||
const baseCurrencyAccount = useSelectedBaseCurrencyAccount();
|
||||
const { market } = useMarket();
|
||||
const [accountInfo, loaded] = useAccountInfo(baseCurrencyAccount?.pubkey);
|
||||
|
@ -639,8 +663,8 @@ export function useOpenOrdersForAllMarkets() {
|
|||
}
|
||||
|
||||
export function useBalances(): Balances[] {
|
||||
const baseCurrencyBalances = useBaseCurrencyBalances();
|
||||
const quoteCurrencyBalances = useQuoteCurrencyBalances();
|
||||
const baseCurrencyBalances = useSelectedBaseCurrencyBalances();
|
||||
const quoteCurrencyBalances = useSelectedQuoteCurrencyBalances();
|
||||
const openOrders = useSelectedOpenOrdersAccount(true);
|
||||
const { baseCurrency, quoteCurrency, market } = useMarket();
|
||||
const baseExists =
|
||||
|
@ -761,16 +785,16 @@ export function useWalletBalancesForAllMarkets() {
|
|||
// );
|
||||
}
|
||||
|
||||
async function getCurrencyBalance(market: Market, connection, wallet, base = true) {
|
||||
const currencyAccounts: { pubkey: PublicKey; account: AccountInfo<Buffer> }[] = base
|
||||
? await market.findBaseTokenAccountsForOwner(connection, wallet.publicKey)
|
||||
: await market.findQuoteTokenAccountsForOwner(connection, wallet.publicKey);
|
||||
const currencyAccount = currencyAccounts && currencyAccounts[0];
|
||||
const tokenAccountBalances: RpcResponseAndContext<TokenAmount> = await connection.getTokenAccountBalance(
|
||||
currencyAccount.pubkey,
|
||||
);
|
||||
return tokenAccountBalances?.value?.uiAmount;
|
||||
}
|
||||
// async function getCurrencyBalance(market: Market, connection, wallet, base = true) {
|
||||
// const currencyAccounts: { pubkey: PublicKey; account: AccountInfo<Buffer> }[] = base
|
||||
// ? await market.findBaseTokenAccountsForOwner(connection, wallet.publicKey)
|
||||
// : await market.findQuoteTokenAccountsForOwner(connection, wallet.publicKey);
|
||||
// const currencyAccount = currencyAccounts && currencyAccounts[0];
|
||||
// const tokenAccountBalances: RpcResponseAndContext<TokenAmount> = await connection.getTokenAccountBalance(
|
||||
// currencyAccount.pubkey,
|
||||
// );
|
||||
// return tokenAccountBalances?.value?.uiAmount;
|
||||
// }
|
||||
|
||||
export function useOpenOrderAccountBalancesForAllMarkets() {
|
||||
return [[], true]
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useLocalStorageState } from './utils';
|
|||
import { useInterval } from './useInterval';
|
||||
import { useConnection } from './connection';
|
||||
import { useWallet } from './wallet';
|
||||
import { useAllMarkets, useTokenAccounts, useMarket } from './markets';
|
||||
import {useAllMarkets, useTokenAccounts, useMarket, useSelectedTokenAccounts} from './markets';
|
||||
import { settleAllFunds } from './send';
|
||||
import {PreferencesContextValues} from "./types";
|
||||
|
||||
|
@ -20,13 +20,14 @@ export function PreferencesProvider({ children }) {
|
|||
const { customMarkets } = useMarket();
|
||||
const marketList = useAllMarkets(customMarkets);
|
||||
const connection = useConnection();
|
||||
const [selectedTokenAccounts] = useSelectedTokenAccounts();
|
||||
|
||||
useInterval(() => {
|
||||
const autoSettle = async () => {
|
||||
const markets = marketList.map((m) => m.market);
|
||||
try {
|
||||
console.log('Auto settling');
|
||||
await settleAllFunds({ connection, wallet, tokenAccounts, markets });
|
||||
await settleAllFunds({ connection, wallet, tokenAccounts: (tokenAccounts || []), markets, selectedTokenAccounts });
|
||||
} catch (e) {
|
||||
console.log('Error auto settling funds: ' + e.message);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
OpenOrders,
|
||||
} from '@project-serum/serum';
|
||||
import Wallet from "@project-serum/sol-wallet-adapter";
|
||||
import {TokenAccount} from "./types";
|
||||
import {SelectedTokenAccounts, TokenAccount} from "./types";
|
||||
import {Order} from "@project-serum/serum/lib/market";
|
||||
|
||||
export async function createTokenAccountTransaction({
|
||||
|
@ -160,6 +160,13 @@ export async function settleAllFunds({
|
|||
wallet,
|
||||
tokenAccounts,
|
||||
markets,
|
||||
selectedTokenAccounts,
|
||||
} : {
|
||||
connection: Connection;
|
||||
wallet: Wallet;
|
||||
tokenAccounts: TokenAccount[];
|
||||
markets: Market[];
|
||||
selectedTokenAccounts?: SelectedTokenAccounts;
|
||||
}) {
|
||||
if (!markets || !wallet || !connection || !tokenAccounts) {
|
||||
return;
|
||||
|
@ -168,6 +175,7 @@ export async function settleAllFunds({
|
|||
const programIds: PublicKey[] = [];
|
||||
markets
|
||||
.reduce((cumulative, m) => {
|
||||
// @ts-ignore
|
||||
cumulative.push(m._programId);
|
||||
return cumulative;
|
||||
}, [])
|
||||
|
@ -201,29 +209,42 @@ 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 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,
|
||||
getSelectedTokenAccountForMint(tokenAccounts, market?.baseMintAddress)
|
||||
?.pubkey,
|
||||
getSelectedTokenAccountForMint(
|
||||
tokenAccounts,
|
||||
market?.quoteMintAddress,
|
||||
)?.pubkey,
|
||||
selectedBaseTokenAccount,
|
||||
selectedQuoteTokenAccount,
|
||||
)
|
||||
);
|
||||
}),
|
||||
)).filter((x) => x);
|
||||
)).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: (Account | PublicKey)[] = [];
|
||||
const signers: Array<Account | PublicKey> = [];
|
||||
settleTransactions
|
||||
.reduce((cumulative, 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) {
|
||||
|
|
|
@ -114,3 +114,10 @@ export interface EndpointInfo {
|
|||
endpoint: string;
|
||||
custom: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* {tokenMint: preferred token account's base58 encoded public key}
|
||||
*/
|
||||
export interface SelectedTokenAccounts {
|
||||
[tokenMint: string]: string;
|
||||
}
|
||||
|
|
|
@ -23,47 +23,68 @@ export const percentFormat = new Intl.NumberFormat(undefined, {
|
|||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export function floorToDecimal(value, decimals) {
|
||||
export function floorToDecimal(value: number, decimals: number | undefined | null) {
|
||||
return decimals ? Math.floor(value * 10 ** decimals) / 10 ** decimals : value;
|
||||
}
|
||||
|
||||
export function roundToDecimal(value, decimals) {
|
||||
export function roundToDecimal(value: number, decimals: number | undefined | null) {
|
||||
return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value;
|
||||
}
|
||||
|
||||
export function getDecimalCount(value) {
|
||||
export function getDecimalCount(value): number {
|
||||
if (!isNaN(value) && Math.floor(value) !== value)
|
||||
return value.toString().split('.')[1].length || 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function useLocalStorageState<T = any>(key: string, defaultState: T | null = null): [T, (newState: T) => void] {
|
||||
const [state, setState] = useState<T>(() => {
|
||||
// NOTE: Not sure if this is ok
|
||||
const storedState = localStorage.getItem(key);
|
||||
if (storedState) {
|
||||
return JSON.parse(storedState);
|
||||
}
|
||||
return defaultState;
|
||||
});
|
||||
const localStorageListeners = {};
|
||||
|
||||
const setLocalStorageState = useCallback<(newState: T) => void>(
|
||||
(newState) => {
|
||||
export function useLocalStorageStringState(
|
||||
key: string,
|
||||
defaultState: string | null = null,
|
||||
): [string | null, (newState: string | null) => void] {
|
||||
const state = localStorage.getItem(key) || defaultState;
|
||||
|
||||
const [, notify] = useState(key + '\n' + state);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorageListeners[key]) {
|
||||
localStorageListeners[key] = [];
|
||||
}
|
||||
localStorageListeners[key].push(notify);
|
||||
return () => {
|
||||
localStorageListeners[key] = localStorageListeners[key].filter(
|
||||
listener => listener !== notify,
|
||||
);
|
||||
if (localStorageListeners[key].length === 0) {
|
||||
delete localStorageListeners[key];
|
||||
}
|
||||
};
|
||||
}, [key]);
|
||||
|
||||
const setState = useCallback<(newState: string | null) => void>(
|
||||
newState => {
|
||||
const changed = state !== newState;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
setState(newState);
|
||||
|
||||
if (newState === null) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
localStorage.setItem(key, JSON.stringify(newState));
|
||||
localStorage.setItem(key, newState);
|
||||
}
|
||||
localStorageListeners[key].forEach(listener => listener(key + '\n' + newState));
|
||||
},
|
||||
[state, key],
|
||||
);
|
||||
|
||||
return [state, setLocalStorageState];
|
||||
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 useEffectAfterTimeout(effect, timeout) {
|
||||
|
@ -82,9 +103,9 @@ export function useListener(emitter, eventName) {
|
|||
}, [emitter, eventName]);
|
||||
}
|
||||
|
||||
export function abbreviateAddress(address) {
|
||||
export function abbreviateAddress(address, size = 4) {
|
||||
const base58 = address.toBase58();
|
||||
return base58.slice(0, 4) + '…' + base58.slice(-4);
|
||||
return base58.slice(0, size) + '…' + base58.slice(-size);
|
||||
}
|
||||
|
||||
export function isEqual(obj1, obj2, keys) {
|
||||
|
|
|
@ -14,10 +14,16 @@ const WalletContext = React.createContext<null | WalletContextValues>(null);
|
|||
export function WalletProvider({ children }) {
|
||||
const { endpoint } = useConnectionConfig();
|
||||
|
||||
const [providerUrl, setProviderUrl] = useLocalStorageState(
|
||||
const [savedProviderUrl, setProviderUrl] = useLocalStorageState(
|
||||
'walletProvider',
|
||||
'https://www.sollet.io',
|
||||
);
|
||||
let providerUrl;
|
||||
if (!savedProviderUrl) {
|
||||
providerUrl = 'https://www.sollet.io';
|
||||
} else {
|
||||
providerUrl = savedProviderUrl;
|
||||
}
|
||||
|
||||
const wallet = useMemo(() => new Wallet(providerUrl, endpoint), [
|
||||
providerUrl,
|
||||
|
|
Loading…
Reference in New Issue