Batch settle (#80)

This commit is contained in:
Nathaniel Parke 2021-05-06 16:27:25 +08:00 committed by GitHub
parent 0f5f04faf7
commit a285181eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 131 additions and 112 deletions

View File

@ -1,5 +1,5 @@
import { Button, Col, Divider, Popover, Row } from 'antd';
import React, { useState } from 'react';
import {Button, Col, Divider, Popover, Row} from 'antd';
import React, {useState} from 'react';
import FloatingElement from './layout/FloatingElement';
import styled from 'styled-components';
import {
@ -11,15 +11,17 @@ import {
useTokenAccounts,
} from '../utils/markets';
import DepositDialog from './DepositDialog';
import { useWallet } from '../utils/wallet';
import {useWallet} from '../utils/wallet';
import Link from './Link';
import { settleFunds } from '../utils/send';
import { useSendConnection } from '../utils/connection';
import { notify } from '../utils/notifications';
import { Balances } from '../utils/types';
import {settleFunds} from '../utils/send';
import {useSendConnection} from '../utils/connection';
import {notify} from '../utils/notifications';
import {Balances} from '../utils/types';
import StandaloneTokenAccountsSelect from './StandaloneTokenAccountSelect';
import LinkAddress from './LinkAddress';
import { InfoCircleOutlined } from '@ant-design/icons';
import {InfoCircleOutlined} from '@ant-design/icons';
import {useInterval} from "../utils/useInterval";
import {useLocalStorageState} from "../utils/utils";
const RowBox = styled(Row)`
padding-bottom: 20px;
@ -50,6 +52,11 @@ export default function StandaloneBalancesDisplay() {
balances && balances.find((b) => b.coin === baseCurrency);
const quoteCurrencyBalances =
balances && balances.find((b) => b.coin === quoteCurrency);
const [autoSettleEnabled] = useLocalStorageState(
'autoSettleEnabled',
true,
);
const [lastSettledAt, setLastSettledAt] = useState<number>(0);
async function onSettleFunds() {
if (!wallet) {
@ -112,6 +119,42 @@ export default function StandaloneBalancesDisplay() {
}
}
useInterval(() => {
const autoSettle = async () => {
if (!wallet || !market || !openOrdersAccount || !baseCurrencyAccount || !quoteCurrencyAccount || !autoSettleEnabled) {
return;
}
if (!baseCurrencyBalances?.unsettled && !quoteCurrencyBalances?.unsettled) {
return;
}
if (Date.now() - lastSettledAt < 15000) {
return;
}
try {
console.log('Settling funds...');
setLastSettledAt(Date.now());
await settleFunds({
market,
openOrders: openOrdersAccount,
connection,
wallet,
baseCurrencyAccount,
quoteCurrencyAccount,
});
} catch (e) {
console.log('Error auto settling funds: ' + e.message);
return;
}
console.log('Finished settling funds.');
};
(
connected &&
wallet?.autoApprove &&
autoSettleEnabled &&
autoSettle()
);
}, 1000);
const formattedBalances: [
string | undefined,
Balances | undefined,

View File

@ -14,14 +14,13 @@ import {
} from '../utils/markets';
import {useWallet} from '../utils/wallet';
import {notify} from '../utils/notifications';
import {floorToDecimal, getDecimalCount, roundToDecimal, useLocalStorageState,} from '../utils/utils';
import {floorToDecimal, getDecimalCount, roundToDecimal,} from '../utils/utils';
import {useSendConnection} from '../utils/connection';
import FloatingElement from './layout/FloatingElement';
import {getUnixTs, placeOrder, settleFunds} from '../utils/send';
import {getUnixTs, placeOrder} from '../utils/send';
import {SwitchChangeEventHandler} from 'antd/es/switch';
import {refreshCache} from '../utils/fetch-loop';
import tuple from 'immutable-tuple';
import {useInterval} from "../utils/useInterval";
const SellButton = styled(Button)`
margin: 20px 0px 0px 0px;
@ -74,10 +73,6 @@ export default function TradeForm({
const [price, setPrice] = useState<number | undefined>(undefined);
const [submitting, setSubmitting] = useState(false);
const [sizeFraction, setSizeFraction] = useState(0);
const [autoSettleEnabled] = useLocalStorageState(
'autoSettleEnabled',
true,
);
const availableQuote =
openOrdersAccount && market
@ -133,33 +128,6 @@ export default function TradeForm({
return () => clearInterval(id);
}, [market, sendConnection, wallet, publicKey]);
useInterval(() => {
const autoSettle = async () => {
if (!wallet || !market || !openOrdersAccount || !baseCurrencyAccount || !quoteCurrencyAccount) {
return;
}
try {
// settle funds into selected token wallets
await settleFunds({
market,
openOrders: openOrdersAccount,
connection: sendConnection,
wallet,
baseCurrencyAccount,
quoteCurrencyAccount
});
} catch (e) {
console.log('Error auto settling funds: ' + e.message);
}
};
(
connected &&
wallet?.autoApprove &&
autoSettleEnabled &&
autoSettle()
);
}, 10000);
const onSetBaseSize = (baseSize: number | undefined) => {
setBaseSize(baseSize);
if (!baseSize) {

View File

@ -1,8 +1,8 @@
import {Market, MARKETS, OpenOrders, Orderbook, TOKEN_MINTS, TokenInstructions,} from '@project-serum/serum';
import {Connection, PublicKey} from '@solana/web3.js';
import {PublicKey} from '@solana/web3.js';
import React, {useContext, useEffect, useState} from 'react';
import {divideBnToNumber, floorToDecimal, getTokenMultiplierFromDecimals, sleep, useLocalStorageState,} from './utils';
import {getCache, refreshCache, setCache, useAsyncData} from './fetch-loop';
import {refreshCache, useAsyncData} from './fetch-loop';
import {useAccountData, useAccountInfo, useConnection} from './connection';
import {useWallet} from './wallet';
import tuple from 'immutable-tuple';
@ -422,34 +422,6 @@ export function useOpenOrdersAccounts(fast = false) {
);
}
// todo: refresh cache after some time?
export async function getCachedMarket(connection: Connection, address: PublicKey, programId: PublicKey) {
let market;
const cacheKey = tuple('getCachedMarket', 'market', address.toString(), connection);
if (!getCache(cacheKey)) {
market = await Market.load(connection, address, {}, programId)
setCache(cacheKey, market)
} else {
market = getCache(cacheKey);
}
return market;
}
export async function getCachedOpenOrderAccounts(connection: Connection, market: Market, owner: PublicKey) {
let accounts;
const cacheKey = tuple('getCachedOpenOrderAccounts', market.address.toString(), owner.toString(), connection);
if (!getCache(cacheKey)) {
accounts = await market.findOpenOrdersAccountsForOwner(
connection,
owner,
);
setCache(cacheKey, accounts);
} else {
accounts = getCache(cacheKey);
}
return accounts;
}
export function useSelectedOpenOrdersAccount(fast = false) {
const [accounts] = useOpenOrdersAccounts(fast);
if (!accounts) {

View File

@ -1,19 +1,15 @@
import React, {useContext} from 'react';
import React, {useContext, useState} from 'react';
import {sleep, useLocalStorageState} from './utils';
import {useInterval} from './useInterval';
import {useConnection} from './connection';
import {useWallet} from './wallet';
import {
getCachedMarket,
getCachedOpenOrderAccounts,
getSelectedTokenAccountForMint,
useCurrentlyAutoSettling,
useMarketInfos,
useTokenAccounts,
} from './markets';
import {settleFunds} from './send';
import {settleAllFunds} from './send';
import {PreferencesContextValues} from './types';
import {getAssociatedTokenAddress} from "@project-serum/associated-token";
import {Market} from "@project-serum/serum";
const PreferencesContext = React.createContext<PreferencesContextValues | null>(
null,
@ -28,7 +24,11 @@ export function PreferencesProvider({ children }) {
const [tokenAccounts] = useTokenAccounts();
const { connected, wallet } = useWallet();
const marketInfoList = useMarketInfos();
const [currentlyAutoSettling, setCurrentlyAutoSettling] = useCurrentlyAutoSettling();
const [currentlyFetchingMarkets, setCurrentlyFetchingMarkets] = useState<boolean>(false);
const [markets, setMarkets] = useState<Map<string, Market>>(new Map())
const addToMarketsMap = (marketId, market) => {
setMarkets(prev => new Map(prev).set(marketId, market));
}
const connection = useConnection();
useInterval(() => {
@ -36,42 +36,58 @@ export function PreferencesProvider({ children }) {
if (!wallet) {
return;
}
setCurrentlyAutoSettling(true);
for (const marketInfo of marketInfoList) {
try {
console.log(`Autosettling ${marketInfo.name} ${marketInfo.address.toString()}`);
const market = await getCachedMarket(connection, marketInfo.address, marketInfo.programId);
const openOrderAccounts = await getCachedOpenOrderAccounts(connection, market, wallet.publicKey);
// settle funds into selected token wallets
const [baseAssocTokenAddress, quoteAssocTokenAddress] = await Promise.all([
getAssociatedTokenAddress(wallet.publicKey, market.baseMintAddress),
getAssociatedTokenAddress(wallet.publicKey, market.quoteMintAddress)
]);
const baseCurrencyAccount = getSelectedTokenAccountForMint(
tokenAccounts, market.baseMintAddress, baseAssocTokenAddress);
const quoteCurrencyAccount = getSelectedTokenAccountForMint(
tokenAccounts, market.quoteMintAddress, quoteAssocTokenAddress);
const openOrders = openOrderAccounts.find(oo => oo.market.equals(marketInfo.address));
if (baseCurrencyAccount && quoteCurrencyAccount && openOrders) {
await settleFunds({
market, openOrders, connection, wallet, baseCurrencyAccount, quoteCurrencyAccount
});
await sleep(1000);
}
} catch (e) {
console.log('Error auto settling funds: ' + e.message);
}
try {
console.log('Settling funds...');
await settleAllFunds({
connection,
wallet,
tokenAccounts: tokenAccounts || [],
markets: [...markets.values()],
});
} catch (e) {
console.log('Error auto settling funds: ' + e.message);
return;
}
setCurrentlyAutoSettling(false);
console.log('Finished settling funds.');
};
(
connected &&
wallet?.autoApprove &&
autoSettleEnabled &&
!currentlyAutoSettling &&
autoSettle()
);
}, 60000);
}, 20000);
// warms up the market and open orders cache for auto-settlement
useInterval(() => {
const fetchMarkets = async () => {
if (!wallet) {
// only need these markets for auto-settlement, so don't fetch unless we are connected.
return;
}
setCurrentlyFetchingMarkets(true);
for (const marketInfo of marketInfoList) {
if (markets.has(marketInfo.address.toString())) {
continue;
}
try {
const market = await Market.load(connection, marketInfo.address, {}, marketInfo.programId)
addToMarketsMap(marketInfo.address.toString(), market);
await sleep(1000);
} catch (e) {
console.log('Error fetching market: ' + e.message);
}
}
setCurrentlyFetchingMarkets(false);
}
(
connected &&
wallet?.autoApprove &&
autoSettleEnabled &&
!currentlyFetchingMarkets &&
fetchMarkets()
);
}, 60000)
return (
<PreferencesContext.Provider

View File

@ -72,6 +72,7 @@ export async function settleFunds({
wallet,
baseCurrencyAccount,
quoteCurrencyAccount,
sendNotification = true,
}: {
market: Market;
openOrders: OpenOrders;
@ -79,6 +80,7 @@ export async function settleFunds({
wallet: WalletAdapter;
baseCurrencyAccount: TokenAccount;
quoteCurrencyAccount: TokenAccount;
sendNotification?: boolean,
}): Promise<string | undefined> {
if (
!market ||
@ -87,7 +89,9 @@ export async function settleFunds({
!openOrders ||
(!baseCurrencyAccount && !quoteCurrencyAccount)
) {
notify({ message: 'Not connected' });
if (sendNotification) {
notify({message: 'Not connected'});
}
return;
}
@ -163,6 +167,7 @@ export async function settleFunds({
wallet,
connection,
sendingMessage: 'Settling funds...',
sendNotification
});
}
@ -224,6 +229,10 @@ export async function settleAllFunds({
// @ts-ignore
m._decoded?.ownAddress?.equals(openOrdersAccount.market),
);
if (openOrdersAccount.baseTokenFree.isZero() && openOrdersAccount.quoteTokenFree.isZero()) {
// nothing to settle for this market.
return null;
}
const baseMint = market?.baseMintAddress;
const quoteMint = market?.quoteMintAddress;
@ -635,6 +644,7 @@ export async function sendTransaction({
sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT,
sendNotification = true,
}: {
transaction: Transaction;
wallet: WalletAdapter;
@ -644,6 +654,7 @@ export async function sendTransaction({
sentMessage?: string;
successMessage?: string;
timeout?: number;
sendNotification?: boolean
}) {
const signedTransaction = await signTransaction({
transaction,
@ -658,6 +669,7 @@ export async function sendTransaction({
sentMessage,
successMessage,
timeout,
sendNotification
});
}
@ -717,6 +729,7 @@ export async function sendSignedTransaction({
sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT,
sendNotification = true,
}: {
signedTransaction: Transaction;
connection: Connection;
@ -724,17 +737,22 @@ export async function sendSignedTransaction({
sentMessage?: string;
successMessage?: string;
timeout?: number;
sendNotification?: boolean
}): Promise<string> {
const rawTransaction = signedTransaction.serialize();
const startTime = getUnixTs();
notify({ message: sendingMessage });
if (sendNotification){
notify({ message: sendingMessage });
}
const txid: TransactionSignature = await connection.sendRawTransaction(
rawTransaction,
{
skipPreflight: true,
},
);
notify({ message: sentMessage, type: 'success', txid });
if (sendNotification) {
notify({ message: sentMessage, type: 'success', txid });
}
console.log('Started awaiting confirmation for', txid);
@ -783,7 +801,9 @@ export async function sendSignedTransaction({
} finally {
done = true;
}
notify({ message: successMessage, type: 'success', txid });
if (sendNotification) {
notify({ message: successMessage, type: 'success', txid });
}
console.log('Latency', txid, getUnixTs() - startTime);
return txid;