Allow specifying preferred token account

This commit is contained in:
Nathaniel Parke 2020-10-13 10:21:34 +08:00
parent e516603e88
commit 6783fbe8c8
10 changed files with 339 additions and 101 deletions

View File

@ -1,5 +1,5 @@
import { Button, Col, Divider, Row } from 'antd';
import React, { useState } from 'react';
import {Button, Col, Divider, Row} from 'antd';
import React, {useState} from 'react';
import FloatingElement from './layout/FloatingElement';
import styled from 'styled-components';
import {
@ -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"

View File

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

View File

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

View File

@ -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' }}>

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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) {

View File

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