diff --git a/frontend/src/components/Liquidity/FeeSwitch/FeeSwitch.tsx b/frontend/src/components/Liquidity/FeeSwitch/FeeSwitch.tsx index 6abe18c..b02dbd4 100644 --- a/frontend/src/components/Liquidity/FeeSwitch/FeeSwitch.tsx +++ b/frontend/src/components/Liquidity/FeeSwitch/FeeSwitch.tsx @@ -55,10 +55,8 @@ export const FeeSwitch: React.FC = ({ singleTabClasses.root, index === bestTierIndex ? singleTabClasses.best : undefined ), - selected: singleTabClasses.selected, - disabled: singleTabClasses.disabled + selected: singleTabClasses.selected }} - disabled={index !== bestTierIndex} /> ))} diff --git a/frontend/src/components/Liquidity/FeeSwitch/style.ts b/frontend/src/components/Liquidity/FeeSwitch/style.ts index 8cc99b5..49d4998 100644 --- a/frontend/src/components/Liquidity/FeeSwitch/style.ts +++ b/frontend/src/components/Liquidity/FeeSwitch/style.ts @@ -95,10 +95,6 @@ export const useSingleTabStyles = makeStyles()(() => { '&:hover': { color: colors.white.main } - }, - disabled: { - ...typography.heading4, - color: '#3a466b' + ' !important' } } }) diff --git a/frontend/src/components/Liquidity/Liquidity.tsx b/frontend/src/components/Liquidity/Liquidity.tsx index f8f79b6..b9d20c0 100644 --- a/frontend/src/components/Liquidity/Liquidity.tsx +++ b/frontend/src/components/Liquidity/Liquidity.tsx @@ -3,7 +3,7 @@ import { SwapToken } from '@store/selectors/wallet' import React, { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import useStyles from './style' -import { FormatNumberThreshold, TokenPriceData } from '@store/consts/types' +import { TokenPriceData } from '@store/consts/types' import { TooltipHover } from '@components/TooltipHover/TooltipHover' import { INoConnected, NoConnected } from '@components/NoConnected/NoConnected' import { ProgressState } from '@components/AnimatedButton/AnimatedButton' @@ -37,7 +37,7 @@ export interface ILiquidity { midPrice: any setMidPrice: (mid: any) => void addLiquidityHandler: (tokenXDeposit: BN, tokenYDeposit: BN) => void - removeLiquidityHandler: (xAmount: number, yAmount: FormatNumberThreshold) => void + removeLiquidityHandler: () => void onChangePositionTokens: ( tokenAIndex: number | null, tokenBindex: number | null, @@ -81,7 +81,7 @@ export const Liquidity: React.FC = ({ tokens, // setMidPrice, addLiquidityHandler, - // removeLiquidityHandler, + removeLiquidityHandler, onChangePositionTokens, calcAmount, feeTiers, @@ -93,7 +93,7 @@ export const Liquidity: React.FC = ({ tickSpacing, // isWaitingForNewPool, poolIndex, - // bestTiers, + bestTiers, // canCreateNewPool, handleAddToken, commonTokens, @@ -182,19 +182,16 @@ export const Liquidity: React.FC = ({ return trimLeadingZeros(printBN(result, tokens[printIndex].decimals)) } - // const bestTierIndex = - // tokenAIndex === null || tokenBIndex === null - // ? undefined - // : (bestTiers.find( - // tier => - // (tier.tokenX.equals(tokens[tokenAIndex].assetAddress) && - // tier.tokenY.equals(tokens[tokenBIndex].assetAddress)) || - // (tier.tokenX.equals(tokens[tokenBIndex].assetAddress) && - // tier.tokenY.equals(tokens[tokenAIndex].assetAddress)) - // )?.bestTierIndex ?? undefined) - - // Temporary set best tier index as only available - const bestTierIndex = 2 + const bestTierIndex = + tokenAIndex === null || tokenBIndex === null + ? undefined + : (bestTiers.find( + tier => + (tier.tokenX.equals(tokens[tokenAIndex].assetAddress) && + tier.tokenY.equals(tokens[tokenBIndex].assetAddress)) || + (tier.tokenX.equals(tokens[tokenBIndex].assetAddress) && + tier.tokenY.equals(tokens[tokenAIndex].assetAddress)) + )?.bestTierIndex ?? undefined) const updatePath = ( index1: number | null, @@ -285,11 +282,6 @@ export const Liquidity: React.FC = ({ return '0' }, [tokenADeposit, tokenBDeposit, poolIndex]) - useEffect(() => { - if (bestTierIndex) { - setPositionTokens(tokenAIndex, tokenBIndex, bestTierIndex, true) - } - }, [bestTierIndex]) return ( {showNoConnected && } @@ -464,9 +456,7 @@ export const Liquidity: React.FC = ({ ) : ( { - //TODO - }} + onRemoveLiquidity={removeLiquidityHandler} LPTokenInputState={{ value: LPTokenDeposit, setValue: value => { diff --git a/frontend/src/containers/LiquidityWrapper/LiquidityWrapper.tsx b/frontend/src/containers/LiquidityWrapper/LiquidityWrapper.tsx index e75c602..4c6a1a7 100644 --- a/frontend/src/containers/LiquidityWrapper/LiquidityWrapper.tsx +++ b/frontend/src/containers/LiquidityWrapper/LiquidityWrapper.tsx @@ -218,8 +218,6 @@ export const LiquidityWrapper: React.FC = ({ } }, [allLpPools]) - console.log(lpPoolIndex) - useEffect(() => { isMountedRef.current = true return () => { @@ -252,7 +250,20 @@ export const LiquidityWrapper: React.FC = ({ ) } }} - removeLiquidityHandler={() => {}} + removeLiquidityHandler={() => { + if (tokenAIndex !== null && tokenBIndex !== null) { + dispatch( + poolsActions.burn({ + pair: new Pair( + tokens[tokenAIndex].address, + tokens[tokenBIndex].address, + FEE_TIERS[feeIndex] + ), + liquidityDelta: new BN(10000) + }) + ) + } + }} onChangePositionTokens={(tokenA, tokenB, feeTierIndex) => { setTokenAIndex(tokenA) setTokenBIndex(tokenB) diff --git a/frontend/src/store/consts/static.ts b/frontend/src/store/consts/static.ts index 1348545..1f6d023 100644 --- a/frontend/src/store/consts/static.ts +++ b/frontend/src/store/consts/static.ts @@ -2,6 +2,7 @@ import { BN } from '@project-serum/anchor' import { PublicKey } from '@solana/web3.js' import { FormatNumberThreshold, PrefixConfig } from './types' import { FEE_TIERS } from '@invariant-labs/sdk-eclipse/lib/utils' +import { ISnackbar } from '@store/reducers/snackbars' export interface FeeTier { fee: BN tickSpacing?: number @@ -277,3 +278,9 @@ export const addressTickerMap: { [key: string]: string } = { USDC: '5gFSyxjNsuQsZKn9g5L9Ky3cSUvJ6YXqWVuPzmSi8Trx', ETH: 'So11111111111111111111111111111111111111112' } + +export const SIGNING_SNACKBAR_CONFIG: Omit = { + message: 'Signing transactions', + variant: 'pending', + persist: true +} diff --git a/frontend/src/store/reducers/pools.ts b/frontend/src/store/reducers/pools.ts index 1936c94..3689a4a 100644 --- a/frontend/src/store/reducers/pools.ts +++ b/frontend/src/store/reducers/pools.ts @@ -98,6 +98,11 @@ export interface MintData { lpPoolExists: boolean } +export interface BurnData { + pair: Pair + liquidityDelta: BN +} + export const poolsSliceName = 'pools' const poolsSlice = createSlice({ name: poolsSliceName, @@ -151,6 +156,9 @@ const poolsSlice = createSlice({ }, mint(state, _action: PayloadAction) { return state + }, + burn(state, _action: PayloadAction) { + return state } } }) diff --git a/frontend/src/store/sagas/pools.ts b/frontend/src/store/sagas/pools.ts index dbd361e..92a4f4f 100644 --- a/frontend/src/store/sagas/pools.ts +++ b/frontend/src/store/sagas/pools.ts @@ -1,4 +1,4 @@ -import { actions, MintData, PairTokens, PoolWithAddress } from '@store/reducers/pools' +import { actions, BurnData, MintData, PairTokens, PoolWithAddress } from '@store/reducers/pools' import { network, rpcAddress } from '@store/selectors/connection' import { all, call, put, select, spawn, takeLatest } from 'typed-redux-saga' import { PayloadAction } from '@reduxjs/toolkit' @@ -7,11 +7,15 @@ import { getPools } from '@store/consts/utils' import { Pair } from '@invariant-labs/sdk-eclipse' import { FEE_TIERS, getMaxTick, getMinTick } from '@invariant-labs/sdk-eclipse/lib/utils' import { getProtocolProgram } from '@web3/programs/protocol' -import { getWallet } from './wallet' import { AnchorProvider, BN } from '@project-serum/anchor' +import { BlockheightBasedTransactionConfirmationStrategy, Transaction } from '@solana/web3.js' import { getAssociatedTokenAddress } from '@solana/spl-token' import { getConnection } from './connection' -import { BlockheightBasedTransactionConfirmationStrategy, Transaction } from '@solana/web3.js' +import { getWallet } from './wallet' +import { createLoaderKey } from '@utils/utils' +import { actions as snackbarsActions } from '@store/reducers/snackbars' +import { SIGNING_SNACKBAR_CONFIG } from '@store/consts/static' +import { closeSnackbar } from 'notistack' export interface iTick { index: iTick[] @@ -76,113 +80,260 @@ export function* fetchLpPoolData(action: PayloadAction) { } export function* handleMint(action: PayloadAction) { - const { pair, lpPoolExists } = action.payload + const loaderHandleMint = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { + yield put( + snackbarsActions.add({ + message: 'Adding liquidity...', + variant: 'pending', + persist: true, + key: loaderHandleMint + }) + ) + const { pair, lpPoolExists } = action.payload - const networkType = yield* select(network) - const rpc = yield* select(rpcAddress) - const wallet = yield* call(getWallet) - const connection = yield* call(getConnection) + const networkType = yield* select(network) + const rpc = yield* select(rpcAddress) + const wallet = yield* call(getWallet) + const connection = yield* call(getConnection) - const protocolProgram = yield* call(getProtocolProgram, networkType, rpc) - const marketProgram = yield* call(getMarketProgram, networkType, rpc) + const protocolProgram = yield* call(getProtocolProgram, networkType, rpc) + const marketProgram = yield* call(getMarketProgram, networkType, rpc) - const tx = new Transaction() + const tx = new Transaction() - if (lpPoolExists) { - const initLpPoolIx = yield* call([protocolProgram, protocolProgram.initLpPoolIx], { - pair - }) - tx.add(initLpPoolIx) - - const lowerTickIndex = getMinTick(pair.feeTier.tickSpacing ?? 0) - try { - yield* call([marketProgram, marketProgram.getTick], pair, lowerTickIndex) - } catch (e) { - const createLowerTickIx = yield* call([marketProgram, marketProgram.createTickInstruction], { - pair, - index: getMinTick(pair.feeTier.tickSpacing ?? 0) + if (lpPoolExists) { + const initLpPoolIx = yield* call([protocolProgram, protocolProgram.initLpPoolIx], { + pair }) - tx.add(createLowerTickIx) + tx.add(initLpPoolIx) + + const lowerTickIndex = getMinTick(pair.feeTier.tickSpacing ?? 0) + try { + yield* call([marketProgram, marketProgram.getTick], pair, lowerTickIndex) + } catch (e) { + const createLowerTickIx = yield* call( + [marketProgram, marketProgram.createTickInstruction], + { + pair, + index: getMinTick(pair.feeTier.tickSpacing ?? 0) + } + ) + tx.add(createLowerTickIx) + } + + const upperTickIndex = getMaxTick(pair.feeTier.tickSpacing ?? 0) + try { + yield* call([marketProgram, marketProgram.getTick], pair, upperTickIndex) + } catch (e) { + const createLowerTickIx = yield* call( + [marketProgram, marketProgram.createTickInstruction], + { + pair, + index: getMinTick(pair.feeTier.tickSpacing ?? 0) + } + ) + tx.add(createLowerTickIx) + } } - const upperTickIndex = getMaxTick(pair.feeTier.tickSpacing ?? 0) - try { - yield* call([marketProgram, marketProgram.getTick], pair, upperTickIndex) - } catch (e) { - const createLowerTickIx = yield* call([marketProgram, marketProgram.createTickInstruction], { - pair, - index: getMinTick(pair.feeTier.tickSpacing ?? 0) - }) - tx.add(createLowerTickIx) + const { address: stateAddress } = yield* call([marketProgram, marketProgram.getStateAddress]) + const { positionListAddress } = yield* call( + [marketProgram, marketProgram.getPositionListAddress], + protocolProgram.programAuthority + ) + const { positionAddress } = yield* call( + [marketProgram, marketProgram.getPositionAddress], + protocolProgram.programAuthority, + 0 + ) + const { positionAddress: lastPositionAddress } = yield* call( + [marketProgram, marketProgram.getPositionAddress], + protocolProgram.programAuthority, + 0 + ) + const { tickAddress: lowerTickAddress } = yield* call( + [marketProgram, marketProgram.getTickAddress], + pair, + getMinTick(pair.feeTier.tickSpacing ?? 0) + ) + const { tickAddress: upperTickAddress } = yield* call( + [marketProgram, marketProgram.getTickAddress], + pair, + getMaxTick(pair.feeTier.tickSpacing ?? 0) + ) + const pool = yield* call([marketProgram, marketProgram.getPool], pair) + const accountXAddress = yield* call(getAssociatedTokenAddress, pool.tokenX, wallet.publicKey) + const accountYAddress = yield* call(getAssociatedTokenAddress, pool.tokenY, wallet.publicKey) + const { programAuthority } = yield* call([marketProgram, marketProgram.getProgramAuthority]) + + const mintIx = yield* call([protocolProgram, protocolProgram.mintLpTokenIx], { + pair, + index: 0, + liquidityDelta: new BN(10000), + invProgram: marketProgram.program.programId, + invState: stateAddress, + position: positionAddress, + lastPosition: lastPositionAddress, + positionList: positionListAddress, + lowerTick: lowerTickAddress, + upperTick: upperTickAddress, + tickmap: pool.tickmap, + accountX: accountXAddress, + accountY: accountYAddress, + invReserveX: pool.tokenXReserve, + invReserveY: pool.tokenYReserve, + invProgramAuthority: programAuthority + }) + tx.add(mintIx) + + const blockhash = yield* call([connection, connection.getLatestBlockhash]) + tx.recentBlockhash = blockhash.blockhash + tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + + const signature = yield* call( + [connection, connection.sendRawTransaction], + signedTx.serialize(), + AnchorProvider.defaultOptions() + ) + + const confirmStrategy: BlockheightBasedTransactionConfirmationStrategy = { + blockhash: blockhash.blockhash, + lastValidBlockHeight: blockhash.lastValidBlockHeight, + signature } + yield* call([connection, connection.confirmTransaction], confirmStrategy) + + closeSnackbar(loaderHandleMint) + yield put(snackbarsActions.remove(loaderHandleMint)) + } catch (error) { + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + closeSnackbar(loaderHandleMint) + yield put(snackbarsActions.remove(loaderHandleMint)) + + console.error('Error minting LP tokens:', error) } +} - const { address: stateAddress } = yield* call([marketProgram, marketProgram.getStateAddress]) - const { positionListAddress } = yield* call( - [marketProgram, marketProgram.getPositionListAddress], - protocolProgram.programAuthority - ) - const { positionAddress } = yield* call( - [marketProgram, marketProgram.getPositionAddress], - protocolProgram.programAuthority, - 0 - ) - const { positionAddress: lastPositionAddress } = yield* call( - [marketProgram, marketProgram.getPositionAddress], - protocolProgram.programAuthority, - 0 - ) - const { tickAddress: lowerTickAddress } = yield* call( - [marketProgram, marketProgram.getTickAddress], - pair, - getMinTick(pair.feeTier.tickSpacing ?? 0) - ) - const { tickAddress: upperTickAddress } = yield* call( - [marketProgram, marketProgram.getTickAddress], - pair, - getMaxTick(pair.feeTier.tickSpacing ?? 0) - ) - const pool = yield* call([marketProgram, marketProgram.getPool], pair) - const accountXAddress = yield* call(getAssociatedTokenAddress, pool.tokenX, wallet.publicKey) - const accountYAddress = yield* call(getAssociatedTokenAddress, pool.tokenY, wallet.publicKey) - const { programAuthority } = yield* call([marketProgram, marketProgram.getProgramAuthority]) - - const mintIx = yield* call([protocolProgram, protocolProgram.mintLpTokenIx], { - pair, - index: 0, - liquidityDelta: new BN(10000), - invProgram: marketProgram.program.programId, - invState: stateAddress, - position: positionAddress, - lastPosition: lastPositionAddress, - positionList: positionListAddress, - lowerTick: lowerTickAddress, - upperTick: upperTickAddress, - tickmap: pool.tickmap, - accountX: accountXAddress, - accountY: accountYAddress, - invReserveX: pool.tokenXReserve, - invReserveY: pool.tokenYReserve, - invProgramAuthority: programAuthority - }) - tx.add(mintIx) - - const blockhash = yield* call([connection, connection.getLatestBlockhash]) - tx.recentBlockhash = blockhash.blockhash - tx.feePayer = wallet.publicKey - const signedTx = yield* call([wallet, wallet.signTransaction], tx) - const signature = yield* call( - [connection, connection.sendRawTransaction], - signedTx.serialize(), - AnchorProvider.defaultOptions() - ) +export function* handleBurn(action: PayloadAction) { + const loaderHandleBurn = createLoaderKey() + const loaderSigningTx = createLoaderKey() + + try { + yield put( + snackbarsActions.add({ + message: 'Removing liquidity...', + variant: 'pending', + persist: true, + key: loaderHandleBurn + }) + ) + + const { liquidityDelta, pair } = action.payload + + const networkType = yield* select(network) + const rpc = yield* select(rpcAddress) + const wallet = yield* call(getWallet) + const connection = yield* call(getConnection) + + const protocolProgram = yield* call(getProtocolProgram, networkType, rpc) + const marketProgram = yield* call(getMarketProgram, networkType, rpc) + + const tx = new Transaction() + + const { address: stateAddress } = yield* call([marketProgram, marketProgram.getStateAddress]) + const { positionListAddress } = yield* call( + [marketProgram, marketProgram.getPositionListAddress], + protocolProgram.programAuthority + ) + const { positionAddress } = yield* call( + [marketProgram, marketProgram.getPositionAddress], + protocolProgram.programAuthority, + 0 + ) + const { positionAddress: lastPositionAddress } = yield* call( + [marketProgram, marketProgram.getPositionAddress], + protocolProgram.programAuthority, + 0 + ) + const { tickAddress: lowerTickAddress } = yield* call( + [marketProgram, marketProgram.getTickAddress], + pair, + getMinTick(pair.feeTier.tickSpacing ?? 0) + ) + const { tickAddress: upperTickAddress } = yield* call( + [marketProgram, marketProgram.getTickAddress], + pair, + getMaxTick(pair.feeTier.tickSpacing ?? 0) + ) + const pool = yield* call([marketProgram, marketProgram.getPool], pair) + const accountXAddress = yield* call(getAssociatedTokenAddress, pool.tokenX, wallet.publicKey) + const accountYAddress = yield* call(getAssociatedTokenAddress, pool.tokenY, wallet.publicKey) + const { programAuthority } = yield* call([marketProgram, marketProgram.getProgramAuthority]) + + const burnIx = yield* call([protocolProgram, protocolProgram.burnLpTokenIx], { + pair, + index: 0, + liquidityDelta, + invProgram: marketProgram.program.programId, + invState: stateAddress, + position: positionAddress, + lastPosition: lastPositionAddress, + positionList: positionListAddress, + lowerTick: lowerTickAddress, + upperTick: upperTickAddress, + tickmap: pool.tickmap, + accountX: accountXAddress, + accountY: accountYAddress, + invReserveX: pool.tokenXReserve, + invReserveY: pool.tokenYReserve, + invProgramAuthority: programAuthority + }) + tx.add(burnIx) + + const blockhash = yield* call([connection, connection.getLatestBlockhash]) + tx.recentBlockhash = blockhash.blockhash + tx.feePayer = wallet.publicKey - const confirmStrategy: BlockheightBasedTransactionConfirmationStrategy = { - blockhash: blockhash.blockhash, - lastValidBlockHeight: blockhash.lastValidBlockHeight, - signature + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + + const signature = yield* call( + [connection, connection.sendRawTransaction], + signedTx.serialize(), + AnchorProvider.defaultOptions() + ) + + const confirmStrategy: BlockheightBasedTransactionConfirmationStrategy = { + blockhash: blockhash.blockhash, + lastValidBlockHeight: blockhash.lastValidBlockHeight, + signature + } + yield* call([connection, connection.confirmTransaction], confirmStrategy) + + closeSnackbar(loaderHandleBurn) + yield put(snackbarsActions.remove(loaderHandleBurn)) + } catch (error) { + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + closeSnackbar(loaderHandleBurn) + yield put(snackbarsActions.remove(loaderHandleBurn)) + + console.error('Error burning LP tokens:', error) } - yield* call([connection, connection.confirmTransaction], confirmStrategy) } export function* getPoolDataHandler(): Generator { @@ -201,10 +352,18 @@ export function* mintHandler(): Generator { yield* takeLatest(actions.mint, handleMint) } +export function* burnHandler(): Generator { + yield* takeLatest(actions.burn, handleBurn) +} + export function* poolsSaga(): Generator { yield all( - [getPoolDataHandler, getAllPoolsForPairDataHandler, getLpPoolDataHandler, mintHandler].map( - spawn - ) + [ + getPoolDataHandler, + getAllPoolsForPairDataHandler, + getLpPoolDataHandler, + mintHandler, + burnHandler + ].map(spawn) ) }