From 8ec490c042b22882e929f636464cbe2dc934fbd5 Mon Sep 17 00:00:00 2001 From: tjs Date: Wed, 3 Aug 2022 17:46:37 -0400 Subject: [PATCH] extract jupiter logic into hook --- components/modals/DepositModal.tsx | 41 +++++---- components/shared/DepositTokenItem.tsx | 4 +- components/shared/DepositTokenList.tsx | 4 +- components/swap/JupiterRoutes.tsx | 1 - components/swap/LeverageSlider.tsx | 2 +- components/swap/Swap.tsx | 105 +++++------------------ components/swap/useJupiter.ts | 114 +++++++++++++++++++++++++ package.json | 2 +- public/locales/en/common.json | 1 + public/locales/es/common.json | 1 + public/locales/zh/common.json | 1 + public/locales/zh_tw/common.json | 1 + store/state.ts | 11 ++- utils/numbers.ts | 2 +- utils/tokens.ts | 2 + 15 files changed, 176 insertions(+), 116 deletions(-) create mode 100644 components/swap/useJupiter.ts diff --git a/components/modals/DepositModal.tsx b/components/modals/DepositModal.tsx index d4a16d91..abb95cd5 100644 --- a/components/modals/DepositModal.tsx +++ b/components/modals/DepositModal.tsx @@ -7,7 +7,7 @@ import React, { ChangeEvent, useCallback, useMemo, useState } from 'react' import mangoStore from '../../store/state' import { ModalProps } from '../../types/modal' import { notify } from '../../utils/notifications' -import { formatFixedDecimals } from '../../utils/numbers' +import { floorToDecimal, formatFixedDecimals } from '../../utils/numbers' import { TokenAccount } from '../../utils/tokens' import ButtonGroup from '../forms/ButtonGroup' import Input from '../forms/Input' @@ -28,17 +28,22 @@ type ModalCombinedProps = DepositModalProps & ModalProps const walletBalanceForToken = ( walletTokens: TokenAccount[], token: string -): number => { +): { maxAmount: number; maxDecimals: number } => { const group = mangoStore.getState().group const bank = group?.banksMap.get(token) - if (!bank) return 0 - const tokenMint = bank?.mint - const walletToken = tokenMint - ? walletTokens.find((t) => t.mint.toString() === tokenMint.toString()) - : null + let walletToken + if (bank) { + const tokenMint = bank?.mint + walletToken = tokenMint + ? walletTokens.find((t) => t.mint.toString() === tokenMint.toString()) + : null + } - return walletToken ? walletToken.uiAmount : 0 + return { + maxAmount: walletToken ? walletToken.uiAmount : 0, + maxDecimals: bank?.mintDecimals || 6, + } } function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) { @@ -57,15 +62,18 @@ function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) { }, [walletTokens, selectedToken]) const setMax = useCallback(() => { - setInputAmount(tokenMax.toString()) + setInputAmount(tokenMax.maxAmount.toString()) }, [tokenMax]) const handleSizePercentage = useCallback( (percentage: string) => { setSizePercentage(percentage) - const max = tokenMax - const amount = (Number(percentage) / 100) * max + let amount = (Number(percentage) / 100) * tokenMax.maxAmount + if (percentage !== '100') { + amount = floorToDecimal(amount, tokenMax.maxDecimals) + } + setInputAmount(amount.toString()) }, [tokenMax] @@ -124,10 +132,7 @@ function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) {

{t('select-token')}

- +

{t('deposit')}

@@ -138,7 +143,7 @@ function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) { {t('wallet-balance')} - {formatFixedDecimals(tokenMax)} + {formatFixedDecimals(tokenMax.maxAmount)}
@@ -183,7 +188,7 @@ function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) { />
-
+ {/*

{t('health-impact')}

+12%

@@ -205,7 +210,7 @@ function DepositModal({ isOpen, onClose, token }: ModalCombinedProps) {

{t('collateral-value')}

$800.00

-
+
*/} ) } diff --git a/components/shared/DepositTokenList.tsx b/components/shared/DepositTokenList.tsx index 6ac3068c..27648edc 100644 --- a/components/shared/DepositTokenList.tsx +++ b/components/shared/DepositTokenList.tsx @@ -18,11 +18,11 @@ const DepositTokenList = ({ onSelect }: { onSelect: (x: any) => void }) => {

{t('rate')}

-
+ {/*

{t('collateral-multiplier')}

-
+
*/}
{banks.map((bank, index) => ( diff --git a/components/swap/JupiterRoutes.tsx b/components/swap/JupiterRoutes.tsx index 846194f1..b0691ad5 100644 --- a/components/swap/JupiterRoutes.tsx +++ b/components/swap/JupiterRoutes.tsx @@ -19,7 +19,6 @@ type JupiterRoutesProps = { slippage: number submitting: boolean handleSwap: (x: TransactionInstruction[]) => void - setAmountOut: (x?: number) => void onClose: () => void jupiter: Jupiter | undefined routes: RouteInfo[] | undefined diff --git a/components/swap/LeverageSlider.tsx b/components/swap/LeverageSlider.tsx index 78eb7048..084fab60 100644 --- a/components/swap/LeverageSlider.tsx +++ b/components/swap/LeverageSlider.tsx @@ -24,7 +24,7 @@ const LeverageSlider = ({ if (inputToken && outputToken) { max = toUiDecimals( mangoAccount - .getMaxSourceForTokenSwap(group, inputToken, outputToken, 1) + .getMaxSourceForTokenSwap(group, inputToken, outputToken, 0.9) .toNumber() ) } else { diff --git a/components/swap/Swap.tsx b/components/swap/Swap.tsx index a18d7bf1..1a58ec9b 100644 --- a/components/swap/Swap.tsx +++ b/components/swap/Swap.tsx @@ -1,26 +1,31 @@ import { useState, ChangeEvent, useCallback, useEffect, useMemo } from 'react' import { TransactionInstruction } from '@solana/web3.js' import { ArrowDownIcon } from '@heroicons/react/solid' -import mangoStore, { CLUSTER } from '../../store/state' -import { Jupiter, RouteInfo } from '@jup-ag/core' +import mangoStore from '../../store/state' +import { RouteInfo } from '@jup-ag/core' import { Token } from '../../types/jupiter' import ContentBox from '../shared/ContentBox' import { notify } from '../../utils/notifications' import JupiterRoutes from './JupiterRoutes' import TokenSelect from '../TokenSelect' import useDebounce from '../shared/useDebounce' -import { formatFixedDecimals, numberFormat } from '../../utils/numbers' +import { + floorToDecimal, + formatFixedDecimals, + numberFormat, +} from '../../utils/numbers' import LeverageSlider from './LeverageSlider' import Input from '../forms/Input' import { useTranslation } from 'next-i18next' import SelectToken from './SelectToken' import { Transition } from '@headlessui/react' import Switch from '../forms/Switch' -import Button, { IconButton, LinkButton } from '../shared/Button' +import Button, { LinkButton } from '../shared/Button' import ButtonGroup from '../forms/ButtonGroup' import { toUiDecimals } from '@blockworks-foundation/mango-v4' import Loading from '../shared/Loading' import { EnterBottomExitBottom } from '../shared/Transitions' +import useJupiter from './useJupiter' const getBestRoute = (routesInfos: RouteInfo[]) => { return routesInfos[0] @@ -40,14 +45,14 @@ const MaxWalletBalance = ({ const group = mangoStore.getState().group const bank = group?.banksMap.get(inputToken) - if (!group || !bank || !mangoAccount) return 0 + if (!group || !bank || !mangoAccount) return 0.0 const balance = mangoAccount.getUi(bank) - return balance + return floorToDecimal(balance, bank.mintDecimals) }, [inputToken, mangoAccount]) const setMaxInputAmount = () => { - setAmountIn(tokenInMax.toString()) + setAmountIn(tokenInMax) } return ( @@ -62,12 +67,8 @@ const MaxWalletBalance = ({ const Swap = () => { const { t } = useTranslation('common') - const [jupiter, setJupiter] = useState() const [selectedRoute, setSelectedRoute] = useState() - const [outputTokenInfo, setOutputTokenInfo] = useState() - const [routes, setRoutes] = useState() const [amountIn, setAmountIn] = useState('') - const [amountOut, setAmountOut] = useState() const [submitting, setSubmitting] = useState(false) const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0) const [showTokenSelect, setShowTokenSelect] = useState('') @@ -83,80 +84,17 @@ const Swap = () => { const connected = mangoStore((s) => s.connected) const debouncedAmountIn = useDebounce(amountIn, 400) - useEffect(() => { - const connection = mangoStore.getState().connection - const loadJupiter = async () => { - const jupiter = await Jupiter.load({ - connection, - cluster: CLUSTER, - // platformFeeAndAccounts: NO_PLATFORM_FEE, - routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds - }) - setJupiter(jupiter) - } - try { - loadJupiter() - } catch (e) { - console.warn(e) - } - }, []) + const { amountOut, jupiter, outputTokenInfo, routes } = useJupiter({ + inputTokenSymbol: inputToken, + outputTokenSymbol: outputToken, + inputAmount: Number(debouncedAmountIn), + slippage, + }) useEffect(() => { - const group = mangoStore.getState().group - if (!group) return - const tokens = mangoStore.getState().jupiterTokens - - const loadRoutes = async () => { - const inputBank = group!.banksMap.get(inputToken) - const outputBank = group!.banksMap.get(outputToken) - if (!inputBank || !outputBank) return - if (!debouncedAmountIn) { - setAmountOut(undefined) - setSelectedRoute(undefined) - } else { - try { - const computedRoutes = await jupiter - ?.computeRoutes({ - inputMint: inputBank.mint, // Mint address of the input token - outputMint: outputBank.mint, // Mint address of the output token - inputAmount: - Number(debouncedAmountIn) * 10 ** inputBank.mintDecimals, // raw input amount of tokens - slippage, // The slippage in % terms - filterTopNResult: 10, - onlyDirectRoutes: true, - }) - .catch((e) => { - console.log('Error loading Jupiter:', e) - return - }) - const tokenOut = tokens.find( - (t: any) => t.address === outputBank.mint.toString() - ) - setOutputTokenInfo(tokenOut) - const routesInfosWithoutRaydium = computedRoutes?.routesInfos.filter( - (r) => { - if (r.marketInfos.length > 1) { - for (const mkt of r.marketInfos) { - if (mkt.amm.label === 'Raydium') return false - } - } - return true - } - ) - if (routesInfosWithoutRaydium?.length) { - setRoutes(routesInfosWithoutRaydium) - const bestRoute = getBestRoute(computedRoutes!.routesInfos) - setSelectedRoute(bestRoute) - setAmountOut(toUiDecimals(bestRoute.outAmount, tokenOut?.decimals)) - } - } catch (e) { - console.warn(e) - } - } - } - - loadRoutes() - }, [inputToken, outputToken, jupiter, slippage, debouncedAmountIn]) + console.log('setting selected route') + setSelectedRoute(routes[0]) + }, [routes]) const handleAmountInChange = useCallback( (e: ChangeEvent) => { @@ -274,7 +212,6 @@ const Swap = () => { slippage={slippage} handleSwap={handleSwap} submitting={submitting} - setAmountOut={setAmountOut} outputTokenInfo={outputTokenInfo} jupiter={jupiter} routes={routes} diff --git a/components/swap/useJupiter.ts b/components/swap/useJupiter.ts new file mode 100644 index 00000000..fa859062 --- /dev/null +++ b/components/swap/useJupiter.ts @@ -0,0 +1,114 @@ +import { toUiDecimals } from '@blockworks-foundation/mango-v4' +import { Jupiter, RouteInfo } from '@jup-ag/core' +import { useEffect, useState } from 'react' +import mangoStore, { CLUSTER } from '../../store/state' +import { Token } from '../../types/jupiter' + +type useJupiterPropTypes = { + inputTokenSymbol: string + outputTokenSymbol: string + inputAmount: number + slippage: number +} + +type RouteParams = { + routes: RouteInfo[] + outputTokenInfo: Token | undefined + amountOut: number +} + +const defaultComputedInfo = { + routes: [], + outputTokenInfo: undefined, + amountOut: 0, +} + +const useJupiter = ({ + inputTokenSymbol, + outputTokenSymbol, + inputAmount, + slippage, +}: useJupiterPropTypes) => { + const [jupiter, setJupiter] = useState() + const [computedInfo, setComputedInfo] = + useState(defaultComputedInfo) + + useEffect(() => { + const connection = mangoStore.getState().connection + const loadJupiter = async () => { + const jupiter = await Jupiter.load({ + connection, + cluster: CLUSTER, + // platformFeeAndAccounts: NO_PLATFORM_FEE, + routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds + }) + setJupiter(jupiter) + } + try { + loadJupiter() + } catch (e) { + console.warn(e) + } + }, []) + + useEffect(() => { + const group = mangoStore.getState().group + if (!group) return + const tokens = mangoStore.getState().jupiterTokens + + const loadRoutes = async () => { + const inputBank = group.banksMap.get(inputTokenSymbol) + const outputBank = group.banksMap.get(outputTokenSymbol) + if (!inputBank || !outputBank) return + if (!inputAmount) { + setComputedInfo(defaultComputedInfo) + } else { + try { + const computedRoutes = await jupiter + ?.computeRoutes({ + inputMint: inputBank.mint, // Mint address of the input token + outputMint: outputBank.mint, // Mint address of the output token + inputAmount: inputAmount * 10 ** inputBank.mintDecimals, // raw input amount of tokens + slippage, // The slippage in % terms + filterTopNResult: 10, + onlyDirectRoutes: true, + }) + .catch((e) => { + console.error('Error computing Jupiter routes:', e) + return + }) + const tokenOut = tokens.find( + (t: any) => t.address === outputBank.mint.toString() + ) + const routesInfosWithoutRaydium = computedRoutes?.routesInfos.filter( + (r) => { + if (r.marketInfos.length > 1) { + for (const mkt of r.marketInfos) { + if (mkt.amm.label === 'Raydium') return false + } + } + return true + } + ) + if (routesInfosWithoutRaydium?.length) { + const bestRoute = routesInfosWithoutRaydium[0] + + setComputedInfo({ + routes: routesInfosWithoutRaydium, + outputTokenInfo: tokenOut, + amountOut: toUiDecimals(bestRoute.outAmount, tokenOut?.decimals), + }) + } + } catch (e) { + console.warn(e) + } + } + } + + loadRoutes() + }, [inputTokenSymbol, outputTokenSymbol, jupiter, slippage, inputAmount]) + + return { jupiter, ...computedInfo } +} + +export default useJupiter diff --git a/package.json b/package.json index 2c1a2b0c..430eb9eb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "prepare": "husky install" }, "dependencies": { - "@blockworks-foundation/mango-v4": "git+https://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:x-oauth-basic@github.com/blockworks-foundation/mango-v4.git", + "@blockworks-foundation/mango-v4": "git+https://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:x-oauth-basic@github.com/blockworks-foundation/mango-v4.git#main", "@headlessui/react": "^1.6.6", "@heroicons/react": "^1.0.6", "@jup-ag/core": "^1.0.0-beta.27", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 925aa6d2..c58e1a8e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -65,6 +65,7 @@ "trade": "Trade", "update": "Update", "wallet-balance": "Wallet Balance", + "wallet-disconnected": "Disconnected from wallet", "withdraw": "Withdraw", "withdrawal-value": "Withdrawal Value" } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index a878d3de..918ea16d 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -65,6 +65,7 @@ "trade": "Trade", "update": "Update", "wallet-balance": "Wallet Balance", + "wallet-disconnected": "Billetera desconectada", "withdraw": "Withdraw", "withdrawal-value": "Withdrawal Value" } diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 9c8452de..b5ce0176 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -65,6 +65,7 @@ "trade": "Trade", "update": "Update", "wallet-balance": "Wallet Balance", + "wallet-disconnected": "断开钱包连结", "withdraw": "Withdraw", "withdrawal-value": "Withdrawal Value" } diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index f7a633c6..8b084e7d 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -65,6 +65,7 @@ "trade": "Trade", "update": "Update", "wallet-balance": "Wallet Balance", + "wallet-disconnected": "斷開錢包連結", "withdraw": "Withdraw", "withdrawal-value": "Withdrawal Value" } diff --git a/store/state.ts b/store/state.ts index bdef861b..d102641b 100644 --- a/store/state.ts +++ b/store/state.ts @@ -23,9 +23,7 @@ import { Token } from '../types/jupiter' import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp' import { TOKEN_LIST_URL } from '@jup-ag/core' -const DEVNET_GROUP = new PublicKey( - 'A9XhGqUUjV992cD36qWDY8wDiZnGuCaUWtSE3NGXjDCb' -) +const GROUP = new PublicKey('A9XhGqUUjV992cD36qWDY8wDiZnGuCaUWtSE3NGXjDCb') export const connection = new web3.Connection( 'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88', @@ -173,7 +171,8 @@ const mangoStore = create( try { const set = get().set const client = get().client - const group = await client.getGroup(DEVNET_GROUP) + const group = await client.getGroup(GROUP) + const markets = await client.serum3GetMarkets( group, group.banksMap.get('BTC')?.tokenIndex, @@ -189,8 +188,8 @@ const mangoStore = create( } }, fetchMangoAccount: async (wallet) => { + const set = get().set try { - const set = get().set const group = get().group if (!group) throw new Error('Group not loaded') @@ -334,7 +333,7 @@ const mangoStore = create( try { const set = get().set const client = get().client - const group = await client.getGroup(DEVNET_GROUP) + const group = await client.getGroup(GROUP) set((state) => { state.group = group diff --git a/utils/numbers.ts b/utils/numbers.ts index 0f1690da..9ab6e393 100644 --- a/utils/numbers.ts +++ b/utils/numbers.ts @@ -18,7 +18,7 @@ export const numberFormat = new Intl.NumberFormat('en', { maximumSignificantDigits: 7, }) -const floorToDecimal = (value: number, decimals: number) => { +export const floorToDecimal = (value: number, decimals: number) => { return Math.floor(value * 10 ** decimals) / 10 ** decimals } diff --git a/utils/tokens.ts b/utils/tokens.ts index 707e6f57..b83617de 100644 --- a/utils/tokens.ts +++ b/utils/tokens.ts @@ -34,6 +34,8 @@ export async function getTokenAccountsByOwnerWithWrappedSol( // fetch data const [solResp, tokenResp] = await Promise.all([solReq, tokenReq]) + console.log(tokenResp.value) + // parse token accounts const tokenAccounts = tokenResp.value.map((t) => { return {