From 8e0117c92666e0d8738beb5ca2089b4693d56de2 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:18:16 +1100 Subject: [PATCH 01/23] feat: migrate tradeInputSlice to higher-order-slice pattern --- .../common/createTradeInputBaseSlice.ts | 109 ++++++++++++++++ .../slices/tradeInputSlice/tradeInputSlice.ts | 121 ++---------------- 2 files changed, 118 insertions(+), 112 deletions(-) create mode 100644 src/state/slices/common/createTradeInputBaseSlice.ts diff --git a/src/state/slices/common/createTradeInputBaseSlice.ts b/src/state/slices/common/createTradeInputBaseSlice.ts new file mode 100644 index 00000000000..b3d9e1e96a4 --- /dev/null +++ b/src/state/slices/common/createTradeInputBaseSlice.ts @@ -0,0 +1,109 @@ +import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' +import type { AccountId } from '@shapeshiftoss/caip' +import type { Asset } from '@shapeshiftoss/types' +import { bnOrZero } from 'lib/bignumber/bignumber' + +export interface TradeInputBaseState { + buyAsset: Asset + sellAsset: Asset + sellAssetAccountId: AccountId | undefined + buyAssetAccountId: AccountId | undefined + sellAmountCryptoPrecision: string + isInputtingFiatSellAmount: boolean + manualReceiveAddress: string | undefined + manualReceiveAddressIsValidating: boolean + manualReceiveAddressIsEditing: boolean + manualReceiveAddressIsValid: boolean | undefined +} + +export function createTradeInputBaseSlice< + S extends Record, + T extends TradeInputBaseState, +>({ + name, + initialState, + extraReducers = {} as S, +}: { + name: string + initialState: T + extraReducers?: S +}) { + return createSlice({ + name, + initialState, + reducers: { + clear: () => initialState, + setBuyAsset: (state, action: PayloadAction) => { + const asset = action.payload + if (asset.assetId === state.buyAsset.assetId) return + + if (asset.assetId === state.sellAsset.assetId) { + state.sellAsset = state.buyAsset + state.sellAmountCryptoPrecision = '0' + } + + if (asset.chainId !== state.buyAsset.chainId) { + state.buyAssetAccountId = undefined + } + + state.manualReceiveAddress = undefined + state.buyAsset = asset + }, + setSellAsset: (state, action: PayloadAction) => { + const asset = action.payload + if (asset.assetId === state.sellAsset.assetId) return + + if (asset.assetId === state.buyAsset.assetId) { + state.buyAsset = state.sellAsset + } + + state.sellAmountCryptoPrecision = '0' + + if (asset.chainId !== state.sellAsset.chainId) { + state.sellAssetAccountId = undefined + } + + state.manualReceiveAddress = undefined + state.sellAsset = action.payload + }, + setSellAssetAccountId: (state, action: PayloadAction) => { + state.sellAssetAccountId = action.payload + }, + setBuyAssetAccountId: (state, action: PayloadAction) => { + state.buyAssetAccountId = action.payload + }, + setSellAmountCryptoPrecision: (state, action: PayloadAction) => { + state.sellAmountCryptoPrecision = bnOrZero(action.payload).toString() + }, + switchAssets: state => { + const buyAsset = state.sellAsset + state.sellAsset = state.buyAsset + state.buyAsset = buyAsset + state.sellAmountCryptoPrecision = '0' + + const sellAssetAccountId = state.sellAssetAccountId + state.sellAssetAccountId = state.buyAssetAccountId + state.buyAssetAccountId = sellAssetAccountId + + state.manualReceiveAddress = undefined + }, + setManualReceiveAddress: (state, action: PayloadAction) => { + state.manualReceiveAddress = action.payload + }, + setManualReceiveAddressIsValidating: (state, action: PayloadAction) => { + state.manualReceiveAddressIsValidating = action.payload + }, + setManualReceiveAddressIsEditing: (state, action: PayloadAction) => { + state.manualReceiveAddressIsEditing = action.payload + }, + setManualReceiveAddressIsValid: (state, action: PayloadAction) => { + state.manualReceiveAddressIsValid = action.payload + }, + setIsInputtingFiatSellAmount: (state, action: PayloadAction) => { + state.isInputtingFiatSellAmount = action.payload + }, + ...extraReducers, + }, + }) +} diff --git a/src/state/slices/tradeInputSlice/tradeInputSlice.ts b/src/state/slices/tradeInputSlice/tradeInputSlice.ts index ec961819ffb..43de02d7f33 100644 --- a/src/state/slices/tradeInputSlice/tradeInputSlice.ts +++ b/src/state/slices/tradeInputSlice/tradeInputSlice.ts @@ -1,28 +1,15 @@ import type { PayloadAction } from '@reduxjs/toolkit' -import { createSlice } from '@reduxjs/toolkit' -import type { AccountId } from '@shapeshiftoss/caip' import { ethAssetId, foxAssetId } from '@shapeshiftoss/caip' -import type { Asset } from '@shapeshiftoss/types' import { localAssetData } from 'lib/asset-service' -import { bnOrZero } from 'lib/bignumber/bignumber' import { defaultAsset } from '../assetsSlice/assetsSlice' +import type { TradeInputBaseState } from '../common/createTradeInputBaseSlice' +import { createTradeInputBaseSlice } from '../common/createTradeInputBaseSlice' -export type TradeInputState = { - buyAsset: Asset - sellAsset: Asset - sellAssetAccountId: AccountId | undefined - buyAssetAccountId: AccountId | undefined - sellAmountCryptoPrecision: string - isInputtingFiatSellAmount: boolean - manualReceiveAddress: string | undefined - manualReceiveAddressIsValidating: boolean - manualReceiveAddressIsEditing: boolean - manualReceiveAddressIsValid: boolean | undefined +interface TradeInputState extends TradeInputBaseState { slippagePreferencePercentage: string | undefined } -// Define the initial state: const initialState: TradeInputState = { buyAsset: localAssetData[foxAssetId] ?? defaultAsset, sellAsset: localAssetData[ethAssetId] ?? defaultAsset, @@ -37,105 +24,15 @@ const initialState: TradeInputState = { slippagePreferencePercentage: undefined, } -// Create the slice: -export const tradeInput = createSlice({ +export const tradeInput = createTradeInputBaseSlice({ name: 'tradeInput', initialState, - reducers: { - clear: () => initialState, - setBuyAsset: (state, action: PayloadAction) => { - const asset = action.payload - - // Prevent doodling state when no change is made - if (asset.assetId === state.buyAsset.assetId) { - return - } - - // Handle the user selecting the same asset for both buy and sell - const isSameAsSellAsset = asset.assetId === state.sellAsset.assetId - if (isSameAsSellAsset) { - state.sellAsset = state.buyAsset - // clear the sell amount when switching assets - state.sellAmountCryptoPrecision = '0' - } - - // Reset the buyAssetAccountId if the chain has changed - if (asset.chainId !== state.buyAsset.chainId) { - state.buyAssetAccountId = undefined - } - - // Reset the manual receive address - state.manualReceiveAddress = undefined - - state.buyAsset = asset - }, - setSellAsset: (state, action: PayloadAction) => { - const asset = action.payload - - // Prevent doodling state when no change is made - if (asset.assetId === state.sellAsset.assetId) { - return - } - - // Handle the user selecting the same asset for both buy and sell - const isSameAsBuyAsset = asset.assetId === state.buyAsset.assetId - if (isSameAsBuyAsset) state.buyAsset = state.sellAsset - - // clear the sell amount - state.sellAmountCryptoPrecision = '0' - - // Reset the sellAssetAccountId if the chain has changed - if (asset.chainId !== state.sellAsset.chainId) { - state.sellAssetAccountId = undefined - } - - // Reset the manual receive address - state.manualReceiveAddress = undefined - - state.sellAsset = action.payload - }, - setSellAssetAccountId: (state, action: PayloadAction) => { - state.sellAssetAccountId = action.payload - }, - setBuyAssetAccountId: (state, action: PayloadAction) => { - state.buyAssetAccountId = action.payload - }, - setSellAmountCryptoPrecision: (state, action: PayloadAction) => { - // dedupe 0, 0., 0.0, 0.00 etc - state.sellAmountCryptoPrecision = bnOrZero(action.payload).toString() - }, - switchAssets: state => { - // Switch the assets - const buyAsset = state.sellAsset - state.sellAsset = state.buyAsset - state.buyAsset = buyAsset - state.sellAmountCryptoPrecision = '0' - - // Switch the account IDs - const sellAssetAccountId = state.sellAssetAccountId - state.sellAssetAccountId = state.buyAssetAccountId - state.buyAssetAccountId = sellAssetAccountId - - // Reset the manual receive address - state.manualReceiveAddress = undefined - }, - setManualReceiveAddress: (state, action: PayloadAction) => { - state.manualReceiveAddress = action.payload - }, - setManualReceiveAddressIsValidating: (state, action: PayloadAction) => { - state.manualReceiveAddressIsValidating = action.payload - }, - setManualReceiveAddressIsEditing: (state, action: PayloadAction) => { - state.manualReceiveAddressIsEditing = action.payload - }, - setManualReceiveAddressIsValid(state, action: PayloadAction) { - state.manualReceiveAddressIsValid = action.payload - }, - setSlippagePreferencePercentage: (state, action: PayloadAction) => { + extraReducers: { + setSlippagePreferencePercentage: ( + state: TradeInputState, + action: PayloadAction, + ) => { state.slippagePreferencePercentage = action.payload }, - setIsInputtingFiatSellAmount: (state, action: PayloadAction) => { - state.isInputtingFiatSellAmount = action.payload - }, }, }) From f19057e9d3f7cb0c8000cfe6c19daec7de2bd434 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:34:21 +1100 Subject: [PATCH 02/23] feat: move common input selectors into a selector factory --- .../createTradeInputBaseSelectors.ts | 85 +++++++++++++++++++ .../createTradeInputBaseSlice.ts | 7 ++ src/state/slices/tradeInputSlice/selectors.ts | 80 ++++------------- .../slices/tradeInputSlice/tradeInputSlice.ts | 18 +--- 4 files changed, 113 insertions(+), 77 deletions(-) create mode 100644 src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts rename src/state/slices/common/{ => tradeInputBase}/createTradeInputBaseSlice.ts (93%) diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts new file mode 100644 index 00000000000..522a5a3bb23 --- /dev/null +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts @@ -0,0 +1,85 @@ +import { createSelector } from '@reduxjs/toolkit' +import { bn } from '@shapeshiftoss/utils' +import type { ReduxState } from 'state/reducer' +import { createDeepEqualOutputSelector } from 'state/selector-utils' + +import { selectMarketDataUsd, selectUserCurrencyToUsdRate } from '../../marketDataSlice/selectors' +import type { TradeInputBaseState } from './createTradeInputBaseSlice' + +export const createTradeInputBaseSelectors = (sliceName: keyof ReduxState) => { + // Base selector to get the slice + const selectBaseSlice = (state: ReduxState) => state[sliceName] as TradeInputBaseState + + // Create reusable selectors + const selectInputBuyAsset = createDeepEqualOutputSelector( + selectBaseSlice, + tradeInput => tradeInput.buyAsset, + ) + + const selectInputSellAsset = createDeepEqualOutputSelector( + selectBaseSlice, + tradeInput => tradeInput.sellAsset, + ) + + const selectInputSellAssetUsdRate = createSelector( + selectInputSellAsset, + selectMarketDataUsd, + (sellAsset, marketDataUsd) => { + if (sellAsset === undefined) return + return marketDataUsd[sellAsset.assetId]?.price + }, + ) + + const selectInputBuyAssetUsdRate = createSelector( + selectInputBuyAsset, + selectMarketDataUsd, + (buyAsset, marketDataUsd) => { + if (buyAsset === undefined) return + return marketDataUsd[buyAsset.assetId]?.price + }, + ) + + const selectInputSellAssetUserCurrencyRate = createSelector( + selectInputSellAssetUsdRate, + selectUserCurrencyToUsdRate, + (sellAssetUsdRate, userCurrencyToUsdRate) => { + if (sellAssetUsdRate === undefined) return + return bn(sellAssetUsdRate).times(userCurrencyToUsdRate).toString() + }, + ) + + const selectInputBuyAssetUserCurrencyRate = createSelector( + selectInputBuyAssetUsdRate, + selectUserCurrencyToUsdRate, + (buyAssetUsdRate, userCurrencyToUsdRate) => { + if (buyAssetUsdRate === undefined) return + return bn(buyAssetUsdRate).times(userCurrencyToUsdRate).toString() + }, + ) + + const selectUserSlippagePercentage = createSelector( + selectBaseSlice, + tradeInput => tradeInput.slippagePreferencePercentage, + ) + + // User input comes in as an actual percentage e.g 1 for 1%, so we need to convert it to a decimal e.g 0.01 for 1% + const selectUserSlippagePercentageDecimal = createSelector( + selectUserSlippagePercentage, + slippagePercentage => { + if (!slippagePercentage) return + return bn(slippagePercentage).div(100).toString() + }, + ) + + return { + selectBaseSlice, + selectInputBuyAsset, + selectInputSellAsset, + selectInputSellAssetUsdRate, + selectInputBuyAssetUsdRate, + selectInputSellAssetUserCurrencyRate, + selectInputBuyAssetUserCurrencyRate, + selectUserSlippagePercentage, + selectUserSlippagePercentageDecimal, + } +} diff --git a/src/state/slices/common/createTradeInputBaseSlice.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts similarity index 93% rename from src/state/slices/common/createTradeInputBaseSlice.ts rename to src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts index b3d9e1e96a4..5303d3e55d2 100644 --- a/src/state/slices/common/createTradeInputBaseSlice.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts @@ -15,6 +15,7 @@ export interface TradeInputBaseState { manualReceiveAddressIsValidating: boolean manualReceiveAddressIsEditing: boolean manualReceiveAddressIsValid: boolean | undefined + slippagePreferencePercentage: string | undefined } export function createTradeInputBaseSlice< @@ -103,6 +104,12 @@ export function createTradeInputBaseSlice< setIsInputtingFiatSellAmount: (state, action: PayloadAction) => { state.isInputtingFiatSellAmount = action.payload }, + setSlippagePreferencePercentage: ( + state: TradeInputBaseState, + action: PayloadAction, + ) => { + state.slippagePreferencePercentage = action.payload + }, ...extraReducers, }, }) diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index beea3be516b..681a4acb594 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -7,11 +7,11 @@ import type { ApiQuote } from 'state/apis/swapper/types' import type { ReduxState } from 'state/reducer' import { createDeepEqualOutputSelector } from 'state/selector-utils' +import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' import { selectEnabledWalletAccountIds, selectPortfolioCryptoBalanceBaseUnitByFilter, } from '../common-selectors' -import { selectMarketDataUsd, selectUserCurrencyToUsdRate } from '../marketDataSlice/selectors' import { selectAccountIdByAccountNumberAndChainId, selectPortfolioAccountMetadata, @@ -24,67 +24,21 @@ import { import { getActiveQuoteMetaOrDefault, sortTradeQuotes } from '../tradeQuoteSlice/helpers' import type { ActiveQuoteMeta } from '../tradeQuoteSlice/types' -const selectTradeInput = (state: ReduxState) => state.tradeInput - -export const selectInputBuyAsset = createDeepEqualOutputSelector( - selectTradeInput, - tradeInput => tradeInput.buyAsset, -) - -export const selectInputSellAsset = createDeepEqualOutputSelector( - selectTradeInput, - tradeInput => tradeInput.sellAsset, -) - -export const selectInputSellAssetUsdRate = createSelector( - selectInputSellAsset, - selectMarketDataUsd, - (sellAsset, marketDataUsd) => { - if (sellAsset === undefined) return - return marketDataUsd[sellAsset.assetId]?.price - }, -) - -export const selectInputBuyAssetUsdRate = createSelector( +export const { + selectBaseSlice, selectInputBuyAsset, - selectMarketDataUsd, - (buyAsset, marketDataUsd) => { - if (buyAsset === undefined) return - return marketDataUsd[buyAsset.assetId]?.price - }, -) - -export const selectInputSellAssetUserCurrencyRate = createSelector( + selectInputSellAsset, selectInputSellAssetUsdRate, - selectUserCurrencyToUsdRate, - (sellAssetUsdRate, userCurrencyToUsdRate) => { - if (sellAssetUsdRate === undefined) return - return bn(sellAssetUsdRate).times(userCurrencyToUsdRate).toString() - }, -) - -export const selectInputBuyAssetUserCurrencyRate = createSelector( selectInputBuyAssetUsdRate, - selectUserCurrencyToUsdRate, - (buyAssetUsdRate, userCurrencyToUsdRate) => { - if (buyAssetUsdRate === undefined) return - return bn(buyAssetUsdRate).times(userCurrencyToUsdRate).toString() - }, -) - -export const selectUserSlippagePercentage: Selector = - createSelector(selectTradeInput, tradeInput => tradeInput.slippagePreferencePercentage) - -// User input comes in as an actual percentage e.g 1 for 1%, so we need to convert it to a decimal e.g 0.01 for 1% -export const selectUserSlippagePercentageDecimal: Selector = - createSelector(selectUserSlippagePercentage, slippagePercentage => { - if (!slippagePercentage) return - return bn(slippagePercentage).div(100).toString() - }) + selectInputSellAssetUserCurrencyRate, + selectInputBuyAssetUserCurrencyRate, + selectUserSlippagePercentage, + selectUserSlippagePercentageDecimal, +} = createTradeInputBaseSelectors('tradeInput') // selects the account ID we're selling from for the first hop export const selectFirstHopSellAccountId = createSelector( - selectTradeInput, + selectBaseSlice, selectInputSellAsset, selectPortfolioAssetAccountBalancesSortedUserCurrency, selectEnabledWalletAccountIds, @@ -105,7 +59,7 @@ export const selectFirstHopSellAccountId = createSelector( // selects the account ID we're buying into for the last hop export const selectLastHopBuyAccountId = createSelector( - selectTradeInput, + selectBaseSlice, selectInputBuyAsset, selectEnabledWalletAccountIds, selectAccountIdByAccountNumberAndChainId, @@ -145,7 +99,7 @@ export const selectLastHopBuyAccountId = createSelector( ) export const selectInputSellAmountCryptoPrecision = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.sellAmountCryptoPrecision, ) @@ -157,22 +111,22 @@ export const selectInputSellAmountCryptoBaseUnit = createSelector( ) export const selectManualReceiveAddress = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.manualReceiveAddress, ) export const selectManualReceiveAddressIsValidating = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.manualReceiveAddressIsValidating, ) export const selectManualReceiveAddressIsEditing = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.manualReceiveAddressIsEditing, ) export const selectManualReceiveAddressIsValid = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.manualReceiveAddressIsValid, ) @@ -204,7 +158,7 @@ export const selectSellAssetBalanceCryptoBaseUnit = createSelector( ) export const selectIsInputtingFiatSellAmount = createSelector( - selectTradeInput, + selectBaseSlice, tradeInput => tradeInput.isInputtingFiatSellAmount, ) diff --git a/src/state/slices/tradeInputSlice/tradeInputSlice.ts b/src/state/slices/tradeInputSlice/tradeInputSlice.ts index 43de02d7f33..08a90864d85 100644 --- a/src/state/slices/tradeInputSlice/tradeInputSlice.ts +++ b/src/state/slices/tradeInputSlice/tradeInputSlice.ts @@ -1,14 +1,11 @@ -import type { PayloadAction } from '@reduxjs/toolkit' import { ethAssetId, foxAssetId } from '@shapeshiftoss/caip' import { localAssetData } from 'lib/asset-service' import { defaultAsset } from '../assetsSlice/assetsSlice' -import type { TradeInputBaseState } from '../common/createTradeInputBaseSlice' -import { createTradeInputBaseSlice } from '../common/createTradeInputBaseSlice' +import type { TradeInputBaseState } from '../common/tradeInputBase/createTradeInputBaseSlice' +import { createTradeInputBaseSlice } from '../common/tradeInputBase/createTradeInputBaseSlice' -interface TradeInputState extends TradeInputBaseState { - slippagePreferencePercentage: string | undefined -} +type TradeInputState = TradeInputBaseState const initialState: TradeInputState = { buyAsset: localAssetData[foxAssetId] ?? defaultAsset, @@ -27,12 +24,5 @@ const initialState: TradeInputState = { export const tradeInput = createTradeInputBaseSlice({ name: 'tradeInput', initialState, - extraReducers: { - setSlippagePreferencePercentage: ( - state: TradeInputState, - action: PayloadAction, - ) => { - state.slippagePreferencePercentage = action.payload - }, - }, + extraReducers: {}, }) From 7c03183fef20807f338b2dbab9844d823c9171e6 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:42:04 +1100 Subject: [PATCH 03/23] chore: add some code comments --- .../tradeInputBase/createTradeInputBaseSelectors.ts | 10 ++++++++++ .../tradeInputBase/createTradeInputBaseSlice.ts | 12 ++++++++++++ src/state/slices/tradeInputSlice/selectors.ts | 2 ++ src/state/slices/tradeInputSlice/tradeInputSlice.ts | 4 +++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts index 522a5a3bb23..6d6fbd10c52 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts @@ -6,6 +6,16 @@ import { createDeepEqualOutputSelector } from 'state/selector-utils' import { selectMarketDataUsd, selectUserCurrencyToUsdRate } from '../../marketDataSlice/selectors' import type { TradeInputBaseState } from './createTradeInputBaseSlice' +/** + * Creates a set of reusable Redux selectors for trade input functionality. This is a higher-order + * selector factory that generates selectors for any slice that handles trade inputs. It provides + * selectors for common trade-related data like buy/sell assets, exchange rates, and slippage + * preferences. This allows multiple features (like trading and limit orders) to reuse the same + * selector logic while maintaining their own independent state. + * + * @param sliceName - The name of the Redux slice to create selectors for + * @returns An object containing all the generated selectors + */ export const createTradeInputBaseSelectors = (sliceName: keyof ReduxState) => { // Base selector to get the slice const selectBaseSlice = (state: ReduxState) => state[sliceName] as TradeInputBaseState diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts index 5303d3e55d2..f3794dcf8fb 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts @@ -18,6 +18,18 @@ export interface TradeInputBaseState { slippagePreferencePercentage: string | undefined } +/** + * Creates a reusable Redux slice for trade input functionality. This is a higher-order slice + * factory that generates a slice with common trade-related reducers. It provides base functionality + * for managing trade state like buy/sell assets, account IDs, amounts, and slippage preferences. + * This allows multiple features (like trading and limit orders) to reuse the same reducer logic + * while maintaining their own independent state. + * + * @param name - The name of the Redux slice + * @param initialState - The initial state extending the base trade input state + * @param extraReducers - Additional reducers specific to the implementing slice + * @returns A configured Redux slice with all the base trade input reducers + */ export function createTradeInputBaseSlice< S extends Record, T extends TradeInputBaseState, diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index 681a4acb594..a3e789a107b 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -24,6 +24,8 @@ import { import { getActiveQuoteMetaOrDefault, sortTradeQuotes } from '../tradeQuoteSlice/helpers' import type { ActiveQuoteMeta } from '../tradeQuoteSlice/types' +// Shared selectors from the base trade input slice that handle common functionality like input +// assets, rates, and slippage preferences export const { selectBaseSlice, selectInputBuyAsset, diff --git a/src/state/slices/tradeInputSlice/tradeInputSlice.ts b/src/state/slices/tradeInputSlice/tradeInputSlice.ts index 08a90864d85..e5644f30cc3 100644 --- a/src/state/slices/tradeInputSlice/tradeInputSlice.ts +++ b/src/state/slices/tradeInputSlice/tradeInputSlice.ts @@ -24,5 +24,7 @@ const initialState: TradeInputState = { export const tradeInput = createTradeInputBaseSlice({ name: 'tradeInput', initialState, - extraReducers: {}, + extraReducers: { + // Add any reducers specific to tradeInput slice here that aren't shared with other slices + }, }) From 33a46414a0f46d44d28be7b0a7c1d2a95f5aee2e Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:52:06 +1100 Subject: [PATCH 04/23] feat: added limitOrderInputSlice --- src/state/reducer.ts | 3 + .../limitOrderInputSlice.ts | 30 ++++ .../slices/limitOrderInputSlice/selectors.ts | 164 ++++++++++++++++++ src/state/slices/tradeInputSlice/selectors.ts | 22 +-- src/state/store.ts | 1 + src/test/mocks/store.ts | 13 ++ 6 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts create mode 100644 src/state/slices/limitOrderInputSlice/selectors.ts diff --git a/src/state/reducer.ts b/src/state/reducer.ts index ff9bbcd72ca..ffb270d8372 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -26,6 +26,7 @@ import { } from './migrations' import type { AssetsState } from './slices/assetsSlice/assetsSlice' import { assetApi, assets } from './slices/assetsSlice/assetsSlice' +import { limitOrderInput } from './slices/limitOrderInputSlice/limitOrderInputSlice' import type { LocalWalletState } from './slices/localWalletSlice/localWalletSlice' import { localWalletSlice } from './slices/localWalletSlice/localWalletSlice' import { marketApi, marketData } from './slices/marketDataSlice/marketDataSlice' @@ -50,6 +51,7 @@ export const slices = { opportunities, nft, tradeInput, + limitOrderInput, tradeQuoteSlice, snapshot, localWalletSlice, @@ -124,6 +126,7 @@ export const sliceReducers = { portfolio: persistReducer(portfolioPersistConfig, portfolio.reducer), preferences: persistReducer(preferencesPersistConfig, preferences.reducer), tradeInput: tradeInput.reducer, + limitOrderInput: limitOrderInput.reducer, opportunities: persistReducer( opportunitiesPersistConfig, opportunities.reducer, diff --git a/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts new file mode 100644 index 00000000000..af5a9f47583 --- /dev/null +++ b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts @@ -0,0 +1,30 @@ +import { foxAssetId, usdcAssetId } from '@shapeshiftoss/caip' +import { localAssetData } from 'lib/asset-service' + +import { defaultAsset } from '../assetsSlice/assetsSlice' +import type { TradeInputBaseState } from '../common/tradeInputBase/createTradeInputBaseSlice' +import { createTradeInputBaseSlice } from '../common/tradeInputBase/createTradeInputBaseSlice' + +type LimitOrderInputState = TradeInputBaseState + +const initialState: LimitOrderInputState = { + buyAsset: localAssetData[foxAssetId] ?? defaultAsset, + sellAsset: localAssetData[usdcAssetId] ?? defaultAsset, + sellAssetAccountId: undefined, + buyAssetAccountId: undefined, + sellAmountCryptoPrecision: '0', + isInputtingFiatSellAmount: false, + manualReceiveAddress: undefined, + manualReceiveAddressIsValidating: false, + manualReceiveAddressIsValid: undefined, + manualReceiveAddressIsEditing: false, + slippagePreferencePercentage: undefined, +} + +export const limitOrderInput = createTradeInputBaseSlice({ + name: 'limitOrderInput', + initialState, + extraReducers: { + // Add any reducers specific to limitOrderInput slice here that aren't shared with other slices + }, +}) diff --git a/src/state/slices/limitOrderInputSlice/selectors.ts b/src/state/slices/limitOrderInputSlice/selectors.ts new file mode 100644 index 00000000000..19a50bc2953 --- /dev/null +++ b/src/state/slices/limitOrderInputSlice/selectors.ts @@ -0,0 +1,164 @@ +import { createSelector } from '@reduxjs/toolkit' +import { bn, bnOrZero } from 'lib/bignumber/bignumber' +import { toBaseUnit } from 'lib/math' +import type { ReduxState } from 'state/reducer' + +import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' +import { + selectEnabledWalletAccountIds, + selectPortfolioCryptoBalanceBaseUnitByFilter, +} from '../common-selectors' +import { + selectAccountIdByAccountNumberAndChainId, + selectPortfolioAccountMetadata, + selectPortfolioAssetAccountBalancesSortedUserCurrency, +} from '../portfolioSlice/selectors' +import { + getFirstAccountIdByChainId, + getHighestUserCurrencyBalanceAccountByAssetId, +} from '../portfolioSlice/utils' + +// Shared selectors from the base trade input slice that handle common functionality like input +// assets, rates, and slippage preferences +export const { + selectBaseSlice, + selectInputBuyAsset, + selectInputSellAsset, + selectInputSellAssetUsdRate, + selectInputBuyAssetUsdRate, + selectInputSellAssetUserCurrencyRate, + selectInputBuyAssetUserCurrencyRate, + selectUserSlippagePercentage, + selectUserSlippagePercentageDecimal, +} = createTradeInputBaseSelectors('limitOrderInput') + +// selects the account ID we're selling from for the first hop +export const selectFirstHopSellAccountId = createSelector( + selectBaseSlice, + selectInputSellAsset, + selectPortfolioAssetAccountBalancesSortedUserCurrency, + selectEnabledWalletAccountIds, + (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { + // return the users selection if it exists + if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId + + const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( + accountIdAssetValues, + sellAsset.assetId, + ) + const firstSellAssetAccountId = getFirstAccountIdByChainId(accountIds, sellAsset.chainId) + + // otherwise return a sane default + return highestFiatBalanceSellAccountId ?? firstSellAssetAccountId + }, +) + +// selects the account ID we're buying into for the last hop +export const selectLastHopBuyAccountId = createSelector( + selectBaseSlice, + selectInputBuyAsset, + selectEnabledWalletAccountIds, + selectAccountIdByAccountNumberAndChainId, + selectFirstHopSellAccountId, + selectPortfolioAccountMetadata, + ( + baseSlice, + buyAsset, + accountIds, + accountIdByAccountNumberAndChainId, + firstHopSellAccountId, + accountMetadata, + ) => { + // return the users selection if it exists + if (baseSlice.buyAssetAccountId) { + return baseSlice.buyAssetAccountId + } + + // maybe convert the account id to an account number + const maybeMatchingBuyAccountNumber = firstHopSellAccountId + ? accountMetadata[firstHopSellAccountId]?.bip44Params.accountNumber + : undefined + + // maybe convert account number to account id on the buy asset chain + const maybeMatchingBuyAccountId = maybeMatchingBuyAccountNumber + ? accountIdByAccountNumberAndChainId[maybeMatchingBuyAccountNumber]?.[buyAsset.chainId] + : undefined + + // an AccountId was found matching the sell asset's account number and chainId, return it + if (maybeMatchingBuyAccountId) { + return maybeMatchingBuyAccountId + } + + // otherwise return a sane default + return getFirstAccountIdByChainId(accountIds, buyAsset.chainId) + }, +) + +export const selectInputSellAmountCryptoPrecision = createSelector( + selectBaseSlice, + baseSlice => baseSlice.sellAmountCryptoPrecision, +) + +export const selectInputSellAmountCryptoBaseUnit = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAsset, + (sellAmountCryptoPrecision, sellAsset) => + toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision), +) + +export const selectManualReceiveAddress = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddress, +) + +export const selectManualReceiveAddressIsValidating = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsValidating, +) + +export const selectManualReceiveAddressIsEditing = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsEditing, +) + +export const selectManualReceiveAddressIsValid = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsValid, +) + +export const selectInputSellAmountUsd = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAssetUsdRate, + (sellAmountCryptoPrecision, sellAssetUsdRate) => { + if (!sellAssetUsdRate) return + return bn(sellAmountCryptoPrecision).times(sellAssetUsdRate).toFixed() + }, +) + +export const selectInputSellAmountUserCurrency = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAssetUserCurrencyRate, + (sellAmountCryptoPrecision, sellAssetUserCurrencyRate) => { + if (!sellAssetUserCurrencyRate) return + return bn(sellAmountCryptoPrecision).times(sellAssetUserCurrencyRate).toFixed() + }, +) + +export const selectSellAssetBalanceCryptoBaseUnit = createSelector( + (state: ReduxState) => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { + accountId: selectFirstHopSellAccountId(state), + assetId: selectInputSellAsset(state).assetId, + }), + sellAssetBalanceCryptoBaseUnit => sellAssetBalanceCryptoBaseUnit, +) + +export const selectIsInputtingFiatSellAmount = createSelector( + selectBaseSlice, + baseSlice => baseSlice.isInputtingFiatSellAmount, +) + +export const selectHasUserEnteredAmount = createSelector( + selectInputSellAmountCryptoPrecision, + sellAmountCryptoPrecision => bnOrZero(sellAmountCryptoPrecision).gt(0), +) diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index a3e789a107b..7a0a4d7faf0 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -44,9 +44,9 @@ export const selectFirstHopSellAccountId = createSelector( selectInputSellAsset, selectPortfolioAssetAccountBalancesSortedUserCurrency, selectEnabledWalletAccountIds, - (tradeInput, sellAsset, accountIdAssetValues, accountIds) => { + (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { // return the users selection if it exists - if (tradeInput.sellAssetAccountId) return tradeInput.sellAssetAccountId + if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( accountIdAssetValues, @@ -68,7 +68,7 @@ export const selectLastHopBuyAccountId = createSelector( selectFirstHopSellAccountId, selectPortfolioAccountMetadata, ( - tradeInput, + baseSlice, buyAsset, accountIds, accountIdByAccountNumberAndChainId, @@ -76,8 +76,8 @@ export const selectLastHopBuyAccountId = createSelector( accountMetadata, ) => { // return the users selection if it exists - if (tradeInput.buyAssetAccountId) { - return tradeInput.buyAssetAccountId + if (baseSlice.buyAssetAccountId) { + return baseSlice.buyAssetAccountId } // maybe convert the account id to an account number @@ -102,7 +102,7 @@ export const selectLastHopBuyAccountId = createSelector( export const selectInputSellAmountCryptoPrecision = createSelector( selectBaseSlice, - tradeInput => tradeInput.sellAmountCryptoPrecision, + baseSlice => baseSlice.sellAmountCryptoPrecision, ) export const selectInputSellAmountCryptoBaseUnit = createSelector( @@ -114,22 +114,22 @@ export const selectInputSellAmountCryptoBaseUnit = createSelector( export const selectManualReceiveAddress = createSelector( selectBaseSlice, - tradeInput => tradeInput.manualReceiveAddress, + baseSlice => baseSlice.manualReceiveAddress, ) export const selectManualReceiveAddressIsValidating = createSelector( selectBaseSlice, - tradeInput => tradeInput.manualReceiveAddressIsValidating, + baseSlice => baseSlice.manualReceiveAddressIsValidating, ) export const selectManualReceiveAddressIsEditing = createSelector( selectBaseSlice, - tradeInput => tradeInput.manualReceiveAddressIsEditing, + baseSlice => baseSlice.manualReceiveAddressIsEditing, ) export const selectManualReceiveAddressIsValid = createSelector( selectBaseSlice, - tradeInput => tradeInput.manualReceiveAddressIsValid, + baseSlice => baseSlice.manualReceiveAddressIsValid, ) export const selectInputSellAmountUsd = createSelector( @@ -161,7 +161,7 @@ export const selectSellAssetBalanceCryptoBaseUnit = createSelector( export const selectIsInputtingFiatSellAmount = createSelector( selectBaseSlice, - tradeInput => tradeInput.isInputtingFiatSellAmount, + baseSlice => baseSlice.isInputtingFiatSellAmount, ) export const selectHasUserEnteredAmount = createSelector( diff --git a/src/state/store.ts b/src/state/store.ts index f85e1a3189a..958ba578cc9 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -53,6 +53,7 @@ export const clearState = () => { store.dispatch(slices.opportunities.actions.clear()) store.dispatch(slices.tradeInput.actions.clear()) store.dispatch(slices.localWalletSlice.actions.clear()) + store.dispatch(slices.limitOrderInput.actions.clear()) store.dispatch(apiSlices.assetApi.util.resetApiState()) store.dispatch(apiSlices.marketApi.util.resetApiState()) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index c17a5ab431d..913162591cb 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -237,6 +237,19 @@ export const mockStore: ReduxState = { manualReceiveAddressIsValid: undefined, slippagePreferencePercentage: undefined, }, + limitOrderInput: { + buyAsset: defaultAsset, + sellAsset: defaultAsset, + sellAssetAccountId: undefined, + buyAssetAccountId: undefined, + sellAmountCryptoPrecision: '0', + isInputtingFiatSellAmount: false, + manualReceiveAddress: undefined, + manualReceiveAddressIsValidating: false, + manualReceiveAddressIsEditing: false, + manualReceiveAddressIsValid: undefined, + slippagePreferencePercentage: undefined, + }, tradeQuoteSlice: { activeQuoteMeta: undefined, confirmedQuote: undefined, From 2d77e62fc2d0913d46c2b36cd0505b60d01989ce Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:08:03 +1100 Subject: [PATCH 05/23] feat: migrate the rest of the shared selectors --- .../createTradeInputBaseSelectors.ts | 159 ++++++++++++++++- .../slices/limitOrderInputSlice/selectors.ts | 163 ++--------------- src/state/slices/tradeInputSlice/selectors.ts | 164 +++--------------- 3 files changed, 191 insertions(+), 295 deletions(-) diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts index 6d6fbd10c52..83cc6fb1f55 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts @@ -1,9 +1,22 @@ import { createSelector } from '@reduxjs/toolkit' -import { bn } from '@shapeshiftoss/utils' +import { bn, bnOrZero, toBaseUnit } from '@shapeshiftoss/utils' import type { ReduxState } from 'state/reducer' import { createDeepEqualOutputSelector } from 'state/selector-utils' +import { + selectEnabledWalletAccountIds, + selectPortfolioCryptoBalanceBaseUnitByFilter, +} from '../../common-selectors' import { selectMarketDataUsd, selectUserCurrencyToUsdRate } from '../../marketDataSlice/selectors' +import { + selectAccountIdByAccountNumberAndChainId, + selectPortfolioAccountMetadata, + selectPortfolioAssetAccountBalancesSortedUserCurrency, +} from '../../portfolioSlice/selectors' +import { + getFirstAccountIdByChainId, + getHighestUserCurrencyBalanceAccountByAssetId, +} from '../../portfolioSlice/utils' import type { TradeInputBaseState } from './createTradeInputBaseSlice' /** @@ -81,6 +94,137 @@ export const createTradeInputBaseSelectors = (sliceName: keyof ReduxState) => { }, ) + // selects the account ID we're selling from + const selectSellAccountId = createSelector( + selectBaseSlice, + selectInputSellAsset, + selectPortfolioAssetAccountBalancesSortedUserCurrency, + selectEnabledWalletAccountIds, + (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { + // return the users selection if it exists + if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId + + const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( + accountIdAssetValues, + sellAsset.assetId, + ) + const firstSellAssetAccountId = getFirstAccountIdByChainId(accountIds, sellAsset.chainId) + + // otherwise return a sane default + return highestFiatBalanceSellAccountId ?? firstSellAssetAccountId + }, + ) + + // selects the account ID we're buying into + const selectBuyAccountId = createSelector( + selectBaseSlice, + selectInputBuyAsset, + selectEnabledWalletAccountIds, + selectAccountIdByAccountNumberAndChainId, + selectSellAccountId, + selectPortfolioAccountMetadata, + ( + baseSlice, + buyAsset, + accountIds, + accountIdByAccountNumberAndChainId, + firstHopSellAccountId, + accountMetadata, + ) => { + // return the users selection if it exists + if (baseSlice.buyAssetAccountId) { + return baseSlice.buyAssetAccountId + } + + // maybe convert the account id to an account number + const maybeMatchingBuyAccountNumber = firstHopSellAccountId + ? accountMetadata[firstHopSellAccountId]?.bip44Params.accountNumber + : undefined + + // maybe convert account number to account id on the buy asset chain + const maybeMatchingBuyAccountId = maybeMatchingBuyAccountNumber + ? accountIdByAccountNumberAndChainId[maybeMatchingBuyAccountNumber]?.[buyAsset.chainId] + : undefined + + // an AccountId was found matching the sell asset's account number and chainId, return it + if (maybeMatchingBuyAccountId) { + return maybeMatchingBuyAccountId + } + + // otherwise return a sane default + return getFirstAccountIdByChainId(accountIds, buyAsset.chainId) + }, + ) + + const selectInputSellAmountCryptoPrecision = createSelector( + selectBaseSlice, + baseSlice => baseSlice.sellAmountCryptoPrecision, + ) + + const selectInputSellAmountCryptoBaseUnit = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAsset, + (sellAmountCryptoPrecision, sellAsset) => + toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision), + ) + + const selectManualReceiveAddress = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddress, + ) + + const selectManualReceiveAddressIsValidating = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsValidating, + ) + + const selectManualReceiveAddressIsEditing = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsEditing, + ) + + const selectManualReceiveAddressIsValid = createSelector( + selectBaseSlice, + baseSlice => baseSlice.manualReceiveAddressIsValid, + ) + + const selectInputSellAmountUsd = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAssetUsdRate, + (sellAmountCryptoPrecision, sellAssetUsdRate) => { + if (!sellAssetUsdRate) return + return bn(sellAmountCryptoPrecision).times(sellAssetUsdRate).toFixed() + }, + ) + + const selectInputSellAmountUserCurrency = createSelector( + selectInputSellAmountCryptoPrecision, + selectInputSellAssetUserCurrencyRate, + (sellAmountCryptoPrecision, sellAssetUserCurrencyRate) => { + if (!sellAssetUserCurrencyRate) return + return bn(sellAmountCryptoPrecision).times(sellAssetUserCurrencyRate).toFixed() + }, + ) + + const selectSellAssetBalanceCryptoBaseUnit = createSelector( + (state: ReduxState) => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, { + accountId: selectSellAccountId(state), + assetId: selectInputSellAsset(state).assetId, + }), + sellAssetBalanceCryptoBaseUnit => sellAssetBalanceCryptoBaseUnit, + ) + + const selectIsInputtingFiatSellAmount = createSelector( + selectBaseSlice, + baseSlice => baseSlice.isInputtingFiatSellAmount, + ) + + const selectHasUserEnteredAmount = createSelector( + selectInputSellAmountCryptoPrecision, + sellAmountCryptoPrecision => bnOrZero(sellAmountCryptoPrecision).gt(0), + ) + return { selectBaseSlice, selectInputBuyAsset, @@ -91,5 +235,18 @@ export const createTradeInputBaseSelectors = (sliceName: keyof ReduxState) => { selectInputBuyAssetUserCurrencyRate, selectUserSlippagePercentage, selectUserSlippagePercentageDecimal, + selectSellAccountId, + selectBuyAccountId, + selectInputSellAmountCryptoBaseUnit, + selectManualReceiveAddress, + selectManualReceiveAddressIsValidating, + selectManualReceiveAddressIsEditing, + selectManualReceiveAddressIsValid, + selectInputSellAmountUsd, + selectInputSellAmountUserCurrency, + selectSellAssetBalanceCryptoBaseUnit, + selectIsInputtingFiatSellAmount, + selectHasUserEnteredAmount, + selectInputSellAmountCryptoPrecision, } } diff --git a/src/state/slices/limitOrderInputSlice/selectors.ts b/src/state/slices/limitOrderInputSlice/selectors.ts index 19a50bc2953..15128a4893c 100644 --- a/src/state/slices/limitOrderInputSlice/selectors.ts +++ b/src/state/slices/limitOrderInputSlice/selectors.ts @@ -1,27 +1,8 @@ -import { createSelector } from '@reduxjs/toolkit' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { toBaseUnit } from 'lib/math' -import type { ReduxState } from 'state/reducer' - import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' -import { - selectEnabledWalletAccountIds, - selectPortfolioCryptoBalanceBaseUnitByFilter, -} from '../common-selectors' -import { - selectAccountIdByAccountNumberAndChainId, - selectPortfolioAccountMetadata, - selectPortfolioAssetAccountBalancesSortedUserCurrency, -} from '../portfolioSlice/selectors' -import { - getFirstAccountIdByChainId, - getHighestUserCurrencyBalanceAccountByAssetId, -} from '../portfolioSlice/utils' // Shared selectors from the base trade input slice that handle common functionality like input // assets, rates, and slippage preferences export const { - selectBaseSlice, selectInputBuyAsset, selectInputSellAsset, selectInputSellAssetUsdRate, @@ -30,135 +11,17 @@ export const { selectInputBuyAssetUserCurrencyRate, selectUserSlippagePercentage, selectUserSlippagePercentageDecimal, -} = createTradeInputBaseSelectors('limitOrderInput') - -// selects the account ID we're selling from for the first hop -export const selectFirstHopSellAccountId = createSelector( - selectBaseSlice, - selectInputSellAsset, - selectPortfolioAssetAccountBalancesSortedUserCurrency, - selectEnabledWalletAccountIds, - (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { - // return the users selection if it exists - if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId - - const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( - accountIdAssetValues, - sellAsset.assetId, - ) - const firstSellAssetAccountId = getFirstAccountIdByChainId(accountIds, sellAsset.chainId) - - // otherwise return a sane default - return highestFiatBalanceSellAccountId ?? firstSellAssetAccountId - }, -) - -// selects the account ID we're buying into for the last hop -export const selectLastHopBuyAccountId = createSelector( - selectBaseSlice, - selectInputBuyAsset, - selectEnabledWalletAccountIds, - selectAccountIdByAccountNumberAndChainId, - selectFirstHopSellAccountId, - selectPortfolioAccountMetadata, - ( - baseSlice, - buyAsset, - accountIds, - accountIdByAccountNumberAndChainId, - firstHopSellAccountId, - accountMetadata, - ) => { - // return the users selection if it exists - if (baseSlice.buyAssetAccountId) { - return baseSlice.buyAssetAccountId - } - - // maybe convert the account id to an account number - const maybeMatchingBuyAccountNumber = firstHopSellAccountId - ? accountMetadata[firstHopSellAccountId]?.bip44Params.accountNumber - : undefined - - // maybe convert account number to account id on the buy asset chain - const maybeMatchingBuyAccountId = maybeMatchingBuyAccountNumber - ? accountIdByAccountNumberAndChainId[maybeMatchingBuyAccountNumber]?.[buyAsset.chainId] - : undefined - - // an AccountId was found matching the sell asset's account number and chainId, return it - if (maybeMatchingBuyAccountId) { - return maybeMatchingBuyAccountId - } - - // otherwise return a sane default - return getFirstAccountIdByChainId(accountIds, buyAsset.chainId) - }, -) - -export const selectInputSellAmountCryptoPrecision = createSelector( - selectBaseSlice, - baseSlice => baseSlice.sellAmountCryptoPrecision, -) - -export const selectInputSellAmountCryptoBaseUnit = createSelector( - selectInputSellAmountCryptoPrecision, - selectInputSellAsset, - (sellAmountCryptoPrecision, sellAsset) => - toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision), -) - -export const selectManualReceiveAddress = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddress, -) - -export const selectManualReceiveAddressIsValidating = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValidating, -) - -export const selectManualReceiveAddressIsEditing = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsEditing, -) - -export const selectManualReceiveAddressIsValid = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValid, -) - -export const selectInputSellAmountUsd = createSelector( + selectSellAccountId, + selectBuyAccountId, + selectInputSellAmountCryptoBaseUnit, + selectManualReceiveAddress, + selectManualReceiveAddressIsValidating, + selectManualReceiveAddressIsEditing, + selectManualReceiveAddressIsValid, + selectInputSellAmountUsd, + selectInputSellAmountUserCurrency, + selectSellAssetBalanceCryptoBaseUnit, + selectIsInputtingFiatSellAmount, + selectHasUserEnteredAmount, selectInputSellAmountCryptoPrecision, - selectInputSellAssetUsdRate, - (sellAmountCryptoPrecision, sellAssetUsdRate) => { - if (!sellAssetUsdRate) return - return bn(sellAmountCryptoPrecision).times(sellAssetUsdRate).toFixed() - }, -) - -export const selectInputSellAmountUserCurrency = createSelector( - selectInputSellAmountCryptoPrecision, - selectInputSellAssetUserCurrencyRate, - (sellAmountCryptoPrecision, sellAssetUserCurrencyRate) => { - if (!sellAssetUserCurrencyRate) return - return bn(sellAmountCryptoPrecision).times(sellAssetUserCurrencyRate).toFixed() - }, -) - -export const selectSellAssetBalanceCryptoBaseUnit = createSelector( - (state: ReduxState) => - selectPortfolioCryptoBalanceBaseUnitByFilter(state, { - accountId: selectFirstHopSellAccountId(state), - assetId: selectInputSellAsset(state).assetId, - }), - sellAssetBalanceCryptoBaseUnit => sellAssetBalanceCryptoBaseUnit, -) - -export const selectIsInputtingFiatSellAmount = createSelector( - selectBaseSlice, - baseSlice => baseSlice.isInputtingFiatSellAmount, -) - -export const selectHasUserEnteredAmount = createSelector( - selectInputSellAmountCryptoPrecision, - sellAmountCryptoPrecision => bnOrZero(sellAmountCryptoPrecision).gt(0), -) +} = createTradeInputBaseSelectors('limitOrderInput') diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index 7a0a4d7faf0..a82495ae5b8 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -1,26 +1,12 @@ import { createSelector } from '@reduxjs/toolkit' import { isExecutableTradeStep, type SwapperName, type TradeQuote } from '@shapeshiftoss/swapper' import type { Selector } from 'react-redux' -import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { toBaseUnit } from 'lib/math' import type { ApiQuote } from 'state/apis/swapper/types' import type { ReduxState } from 'state/reducer' import { createDeepEqualOutputSelector } from 'state/selector-utils' import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' -import { - selectEnabledWalletAccountIds, - selectPortfolioCryptoBalanceBaseUnitByFilter, -} from '../common-selectors' -import { - selectAccountIdByAccountNumberAndChainId, - selectPortfolioAccountMetadata, - selectPortfolioAssetAccountBalancesSortedUserCurrency, -} from '../portfolioSlice/selectors' -import { - getFirstAccountIdByChainId, - getHighestUserCurrencyBalanceAccountByAssetId, -} from '../portfolioSlice/utils' +import { selectAccountIdByAccountNumberAndChainId } from '../portfolioSlice/selectors' import { getActiveQuoteMetaOrDefault, sortTradeQuotes } from '../tradeQuoteSlice/helpers' import type { ActiveQuoteMeta } from '../tradeQuoteSlice/types' @@ -36,138 +22,28 @@ export const { selectInputBuyAssetUserCurrencyRate, selectUserSlippagePercentage, selectUserSlippagePercentageDecimal, -} = createTradeInputBaseSelectors('tradeInput') - -// selects the account ID we're selling from for the first hop -export const selectFirstHopSellAccountId = createSelector( - selectBaseSlice, - selectInputSellAsset, - selectPortfolioAssetAccountBalancesSortedUserCurrency, - selectEnabledWalletAccountIds, - (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { - // return the users selection if it exists - if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId - - const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( - accountIdAssetValues, - sellAsset.assetId, - ) - const firstSellAssetAccountId = getFirstAccountIdByChainId(accountIds, sellAsset.chainId) - - // otherwise return a sane default - return highestFiatBalanceSellAccountId ?? firstSellAssetAccountId - }, -) - -// selects the account ID we're buying into for the last hop -export const selectLastHopBuyAccountId = createSelector( - selectBaseSlice, - selectInputBuyAsset, - selectEnabledWalletAccountIds, - selectAccountIdByAccountNumberAndChainId, - selectFirstHopSellAccountId, - selectPortfolioAccountMetadata, - ( - baseSlice, - buyAsset, - accountIds, - accountIdByAccountNumberAndChainId, - firstHopSellAccountId, - accountMetadata, - ) => { - // return the users selection if it exists - if (baseSlice.buyAssetAccountId) { - return baseSlice.buyAssetAccountId - } - - // maybe convert the account id to an account number - const maybeMatchingBuyAccountNumber = firstHopSellAccountId - ? accountMetadata[firstHopSellAccountId]?.bip44Params.accountNumber - : undefined - - // maybe convert account number to account id on the buy asset chain - const maybeMatchingBuyAccountId = maybeMatchingBuyAccountNumber - ? accountIdByAccountNumberAndChainId[maybeMatchingBuyAccountNumber]?.[buyAsset.chainId] - : undefined - - // an AccountId was found matching the sell asset's account number and chainId, return it - if (maybeMatchingBuyAccountId) { - return maybeMatchingBuyAccountId - } - - // otherwise return a sane default - return getFirstAccountIdByChainId(accountIds, buyAsset.chainId) - }, -) - -export const selectInputSellAmountCryptoPrecision = createSelector( - selectBaseSlice, - baseSlice => baseSlice.sellAmountCryptoPrecision, -) - -export const selectInputSellAmountCryptoBaseUnit = createSelector( - selectInputSellAmountCryptoPrecision, - selectInputSellAsset, - (sellAmountCryptoPrecision, sellAsset) => - toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision), -) - -export const selectManualReceiveAddress = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddress, -) - -export const selectManualReceiveAddressIsValidating = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValidating, -) - -export const selectManualReceiveAddressIsEditing = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsEditing, -) - -export const selectManualReceiveAddressIsValid = createSelector( - selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValid, -) - -export const selectInputSellAmountUsd = createSelector( + selectInputSellAmountCryptoBaseUnit, + selectManualReceiveAddress, + selectManualReceiveAddressIsValidating, + selectManualReceiveAddressIsEditing, + selectManualReceiveAddressIsValid, + selectInputSellAmountUsd, + selectInputSellAmountUserCurrency, + selectSellAssetBalanceCryptoBaseUnit, + selectIsInputtingFiatSellAmount, + selectHasUserEnteredAmount, selectInputSellAmountCryptoPrecision, - selectInputSellAssetUsdRate, - (sellAmountCryptoPrecision, sellAssetUsdRate) => { - if (!sellAssetUsdRate) return - return bn(sellAmountCryptoPrecision).times(sellAssetUsdRate).toFixed() - }, -) - -export const selectInputSellAmountUserCurrency = createSelector( - selectInputSellAmountCryptoPrecision, - selectInputSellAssetUserCurrencyRate, - (sellAmountCryptoPrecision, sellAssetUserCurrencyRate) => { - if (!sellAssetUserCurrencyRate) return - return bn(sellAmountCryptoPrecision).times(sellAssetUserCurrencyRate).toFixed() - }, -) - -export const selectSellAssetBalanceCryptoBaseUnit = createSelector( - (state: ReduxState) => - selectPortfolioCryptoBalanceBaseUnitByFilter(state, { - accountId: selectFirstHopSellAccountId(state), - assetId: selectInputSellAsset(state).assetId, - }), - sellAssetBalanceCryptoBaseUnit => sellAssetBalanceCryptoBaseUnit, -) + // We don't want to export some of the selectors so we can give them more specific names + ...privateSelectors +} = createTradeInputBaseSelectors('tradeInput') -export const selectIsInputtingFiatSellAmount = createSelector( - selectBaseSlice, - baseSlice => baseSlice.isInputtingFiatSellAmount, -) +// We rename this to include the specific hop to avoid confusion in multi-hop contexts +// Selects the account ID we're selling from for the first hop +export const selectFirstHopSellAccountId = privateSelectors.selectSellAccountId -export const selectHasUserEnteredAmount = createSelector( - selectInputSellAmountCryptoPrecision, - sellAmountCryptoPrecision => bnOrZero(sellAmountCryptoPrecision).gt(0), -) +// We rename this to include the specific hop to avoid confusion in multi-hop contexts +// Selects the account ID we're buying into for the last hop +export const selectLastHopBuyAccountId = privateSelectors.selectBuyAccountId // All the below selectors are re-declared from tradeQuoteSlice/selectors to avoid circular deps // and allow selectSecondHopSellAccountId to keep a pwetty API From 3eafc9c00ff2e993156db7ebf24a7d76fb1aed04 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:14:48 +1100 Subject: [PATCH 06/23] feat: wire in redux user slippage for limit orders --- .../LimitOrder/components/LimitOrderInput.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 52916d80f70..dd35aa68ce3 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -20,6 +20,8 @@ import type { ParameterModel } from 'lib/fees/parameters/types' import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' import { defaultAsset } from 'state/slices/assetsSlice/assetsSlice' +import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' +import { selectUserSlippagePercentage } from 'state/slices/limitOrderInputSlice/selectors' import { selectFirstAccountIdByChainId, selectIsAnyAccountMetadataLoadedForChainId, @@ -33,7 +35,7 @@ import { selectIsTradeQuoteRequestAborted, selectShouldShowTradeQuoteOrAwaitInput, } from 'state/slices/tradeQuoteSlice/selectors' -import { useAppSelector } from 'state/store' +import { useAppDispatch, useAppSelector } from 'state/store' import { SharedSlippagePopover } from '../../SharedTradeInput/SharedSlippagePopover' import { SharedTradeInput } from '../../SharedTradeInput/SharedTradeInput' @@ -63,12 +65,13 @@ export const LimitOrderInput = ({ dispatch: walletDispatch, state: { isConnected, isDemoWallet }, } = useWallet() + const dispatch = useAppDispatch() const history = useHistory() const { handleSubmit } = useFormContext() const { showErrorToast } = useErrorHandler() - const [userSlippagePercentage, setUserSlippagePercentage] = useState() + const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) const [sellAsset, setSellAsset] = useState(localAssetData[usdcAssetId] ?? defaultAsset) const [buyAsset, setBuyAsset] = useState(localAssetData[foxAssetId] ?? defaultAsset) const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0') @@ -297,16 +300,23 @@ export const LimitOrderInput = ({ shouldShowTradeQuoteOrAwaitInput, ]) + const handleSetUserSlippagePercentage = useCallback( + (slippagePercentage: string | undefined) => { + dispatch(limitOrderInput.actions.setSlippagePreferencePercentage(slippagePercentage)) + }, + [dispatch], + ) + const headerRightContent = useMemo(() => { return ( ) - }, [defaultSlippagePercentage, userSlippagePercentage]) + }, [defaultSlippagePercentage, handleSetUserSlippagePercentage, userSlippagePercentage]) const bodyContent = useMemo(() => { return ( From 5dedd59fe0ce3ab5e26a60ce76a0b3e4abc2ddea Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:17:20 +1100 Subject: [PATCH 07/23] feat: wire in redux sell asset for limit orders --- .../LimitOrder/components/LimitOrderInput.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index dd35aa68ce3..e07bd1b3a73 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,6 +1,6 @@ import { Divider, Stack } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' -import { foxAssetId, fromAccountId, usdcAssetId } from '@shapeshiftoss/caip' +import { foxAssetId, fromAccountId } from '@shapeshiftoss/caip' import { SwapperName } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils' @@ -21,7 +21,10 @@ import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' import { defaultAsset } from 'state/slices/assetsSlice/assetsSlice' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' -import { selectUserSlippagePercentage } from 'state/slices/limitOrderInputSlice/selectors' +import { + selectInputSellAsset, + selectUserSlippagePercentage, +} from 'state/slices/limitOrderInputSlice/selectors' import { selectFirstAccountIdByChainId, selectIsAnyAccountMetadataLoadedForChainId, @@ -72,7 +75,7 @@ export const LimitOrderInput = ({ const { showErrorToast } = useErrorHandler() const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) - const [sellAsset, setSellAsset] = useState(localAssetData[usdcAssetId] ?? defaultAsset) + const sellAsset = useAppSelector(selectInputSellAsset) const [buyAsset, setBuyAsset] = useState(localAssetData[foxAssetId] ?? defaultAsset) const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0') @@ -140,20 +143,14 @@ export const LimitOrderInput = ({ }, []) const handleSwitchAssets = useCallback(() => { - setSellAsset(buyAsset) - setBuyAsset(sellAsset) - setSellAmountCryptoPrecision('0') - }, [buyAsset, sellAsset]) + dispatch(limitOrderInput.actions.switchAssets()) + }, [dispatch]) const handleSetSellAsset = useCallback( (newSellAsset: Asset) => { - if (newSellAsset === sellAsset) { - handleSwitchAssets() - return - } - setSellAsset(newSellAsset) + dispatch(limitOrderInput.actions.setSellAsset(newSellAsset)) }, - [handleSwitchAssets, sellAsset], + [dispatch], ) const handleSetBuyAsset = useCallback( From 1f5015098ab609167d55bcb76f17d302fb17d98b Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:18:31 +1100 Subject: [PATCH 08/23] feat: wire in redux buy asset for limit orders --- .../LimitOrder/components/LimitOrderInput.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index e07bd1b3a73..6be0b8f99a0 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,6 +1,6 @@ import { Divider, Stack } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' -import { foxAssetId, fromAccountId } from '@shapeshiftoss/caip' +import { fromAccountId } from '@shapeshiftoss/caip' import { SwapperName } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils' @@ -14,12 +14,10 @@ import { TradeInputTab } from 'components/MultiHopTrade/types' import { WalletActions } from 'context/WalletProvider/actions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' -import { localAssetData } from 'lib/asset-service' import { calculateFees } from 'lib/fees/model' import type { ParameterModel } from 'lib/fees/parameters/types' import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' -import { defaultAsset } from 'state/slices/assetsSlice/assetsSlice' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectInputSellAsset, @@ -27,6 +25,7 @@ import { } from 'state/slices/limitOrderInputSlice/selectors' import { selectFirstAccountIdByChainId, + selectInputBuyAsset, selectIsAnyAccountMetadataLoadedForChainId, selectMarketDataByAssetIdUserCurrency, selectUsdRateByAssetId, @@ -76,7 +75,7 @@ export const LimitOrderInput = ({ const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) const sellAsset = useAppSelector(selectInputSellAsset) - const [buyAsset, setBuyAsset] = useState(localAssetData[foxAssetId] ?? defaultAsset) + const buyAsset = useAppSelector(selectInputBuyAsset) const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0') const defaultAccountId = useAppSelector(state => @@ -155,13 +154,9 @@ export const LimitOrderInput = ({ const handleSetBuyAsset = useCallback( (newBuyAsset: Asset) => { - if (newBuyAsset === buyAsset) { - handleSwitchAssets() - return - } - setBuyAsset(newBuyAsset) + dispatch(limitOrderInput.actions.setBuyAsset(newBuyAsset)) }, - [buyAsset, handleSwitchAssets], + [dispatch], ) const handleConnect = useCallback(() => { From bb7474da5baebf350ae992c52cdfd93c2ae6bee5 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:23:11 +1100 Subject: [PATCH 09/23] fix: use hardcoded cowswap for default limit order slippage --- .../LimitOrder/components/LimitOrderInput.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 6be0b8f99a0..034ad12db28 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,7 +1,7 @@ import { Divider, Stack } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' import { fromAccountId } from '@shapeshiftoss/caip' -import { SwapperName } from '@shapeshiftoss/swapper' +import { getDefaultSlippageDecimalPercentageForSwapper, SwapperName } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils' import type { FormEvent } from 'react' @@ -33,7 +33,6 @@ import { } from 'state/slices/selectors' import { selectCalculatedFees, - selectDefaultSlippagePercentage, selectIsTradeQuoteRequestAborted, selectShouldShowTradeQuoteOrAwaitInput, } from 'state/slices/tradeQuoteSlice/selectors' @@ -76,14 +75,18 @@ export const LimitOrderInput = ({ const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) + + // TODO: Move to redux slice const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0') const defaultAccountId = useAppSelector(state => selectFirstAccountIdByChainId(state, sellAsset.chainId), ) - const defaultSlippagePercentage = useAppSelector(state => - selectDefaultSlippagePercentage(state, SwapperName.CowSwap), - ) + const defaultSlippagePercentage = useMemo(() => { + return bn(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap)) + .times(100) + .toString() + }, []) const [buyAccountId, setBuyAccountId] = useState(defaultAccountId) const [sellAccountId, setSellAccountId] = useState(defaultAccountId) From 3f25226eab8e0bcd50f9821184f82c442cdeee4e Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:30:04 +1100 Subject: [PATCH 10/23] feat: migrate marketPriceBuyAsset to redux --- .../LimitOrder/components/LimitOrderInput.tsx | 19 +++++++++++++------ .../createTradeInputBaseSelectors.ts | 6 ++++-- .../limitOrderInputSlice.ts | 8 ++++++-- .../slices/limitOrderInputSlice/selectors.ts | 13 ++++++++++++- src/state/slices/tradeInputSlice/selectors.ts | 7 ++++--- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 034ad12db28..0df5d35ddab 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -21,6 +21,7 @@ import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectInputSellAsset, + selectLimitPriceBuyAsset, selectUserSlippagePercentage, } from 'state/slices/limitOrderInputSlice/selectors' import { @@ -75,9 +76,7 @@ export const LimitOrderInput = ({ const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) - - // TODO: Move to redux slice - const [limitPriceBuyAsset, setLimitPriceBuyAsset] = useState('0') + const limitPriceBuyAsset = useAppSelector(selectLimitPriceBuyAsset) const defaultAccountId = useAppSelector(state => selectFirstAccountIdByChainId(state, sellAsset.chainId), @@ -271,8 +270,15 @@ export const LimitOrderInput = ({ // TODO: If we introduce polling of quotes, we will need to add logic inside `LimitOrderConfig` to // not reset the user's config unless the asset pair changes. useEffect(() => { - setLimitPriceBuyAsset(marketPriceBuyAsset) - }, [marketPriceBuyAsset]) + dispatch(limitOrderInput.actions.setLimitPriceBuyAsset(marketPriceBuyAsset)) + }, [dispatch, marketPriceBuyAsset]) + + const handleSetLimitPriceBuyAsset = useCallback( + (newMarketPriceBuyAsset: string) => { + dispatch(limitOrderInput.actions.setLimitPriceBuyAsset(newMarketPriceBuyAsset)) + }, + [dispatch], + ) const isLoading = useMemo(() => { return ( @@ -344,7 +350,7 @@ export const LimitOrderInput = ({ isLoading={isLoading} marketPriceBuyAsset={marketPriceBuyAsset} limitPriceBuyAsset={limitPriceBuyAsset} - setLimitPriceBuyAsset={setLimitPriceBuyAsset} + setLimitPriceBuyAsset={handleSetLimitPriceBuyAsset} /> @@ -361,6 +367,7 @@ export const LimitOrderInput = ({ sellAmountUserCurrency, sellAsset, handleSetBuyAsset, + handleSetLimitPriceBuyAsset, handleSetSellAsset, handleSwitchAssets, ]) diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts index 83cc6fb1f55..23bbd05a2a2 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts @@ -29,9 +29,11 @@ import type { TradeInputBaseState } from './createTradeInputBaseSlice' * @param sliceName - The name of the Redux slice to create selectors for * @returns An object containing all the generated selectors */ -export const createTradeInputBaseSelectors = (sliceName: keyof ReduxState) => { +export const createTradeInputBaseSelectors = ( + sliceName: keyof ReduxState, +) => { // Base selector to get the slice - const selectBaseSlice = (state: ReduxState) => state[sliceName] as TradeInputBaseState + const selectBaseSlice = (state: ReduxState) => state[sliceName] as T // Create reusable selectors const selectInputBuyAsset = createDeepEqualOutputSelector( diff --git a/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts index af5a9f47583..09fad8c0975 100644 --- a/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts +++ b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts @@ -1,3 +1,4 @@ +import type { PayloadAction } from '@reduxjs/toolkit' import { foxAssetId, usdcAssetId } from '@shapeshiftoss/caip' import { localAssetData } from 'lib/asset-service' @@ -5,7 +6,7 @@ import { defaultAsset } from '../assetsSlice/assetsSlice' import type { TradeInputBaseState } from '../common/tradeInputBase/createTradeInputBaseSlice' import { createTradeInputBaseSlice } from '../common/tradeInputBase/createTradeInputBaseSlice' -type LimitOrderInputState = TradeInputBaseState +export type LimitOrderInputState = { limitPriceBuyAsset: string } & TradeInputBaseState const initialState: LimitOrderInputState = { buyAsset: localAssetData[foxAssetId] ?? defaultAsset, @@ -19,12 +20,15 @@ const initialState: LimitOrderInputState = { manualReceiveAddressIsValid: undefined, manualReceiveAddressIsEditing: false, slippagePreferencePercentage: undefined, + limitPriceBuyAsset: '0', } export const limitOrderInput = createTradeInputBaseSlice({ name: 'limitOrderInput', initialState, extraReducers: { - // Add any reducers specific to limitOrderInput slice here that aren't shared with other slices + setLimitPriceBuyAsset: (state: LimitOrderInputState, action: PayloadAction) => { + state.limitPriceBuyAsset = action.payload + }, }, }) diff --git a/src/state/slices/limitOrderInputSlice/selectors.ts b/src/state/slices/limitOrderInputSlice/selectors.ts index 15128a4893c..8d57d58ba6f 100644 --- a/src/state/slices/limitOrderInputSlice/selectors.ts +++ b/src/state/slices/limitOrderInputSlice/selectors.ts @@ -1,4 +1,7 @@ +import { createSelector } from 'reselect' + import { createTradeInputBaseSelectors } from '../common/tradeInputBase/createTradeInputBaseSelectors' +import type { LimitOrderInputState } from './limitOrderInputSlice' // Shared selectors from the base trade input slice that handle common functionality like input // assets, rates, and slippage preferences @@ -24,4 +27,12 @@ export const { selectIsInputtingFiatSellAmount, selectHasUserEnteredAmount, selectInputSellAmountCryptoPrecision, -} = createTradeInputBaseSelectors('limitOrderInput') + ...privateSelectors +} = createTradeInputBaseSelectors('limitOrderInput') + +const { selectBaseSlice } = privateSelectors + +export const selectLimitPriceBuyAsset = createSelector( + selectBaseSlice, + tradeInput => tradeInput.limitPriceBuyAsset, +) diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index a82495ae5b8..035a97b7f63 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -13,7 +13,6 @@ import type { ActiveQuoteMeta } from '../tradeQuoteSlice/types' // Shared selectors from the base trade input slice that handle common functionality like input // assets, rates, and slippage preferences export const { - selectBaseSlice, selectInputBuyAsset, selectInputSellAsset, selectInputSellAssetUsdRate, @@ -37,13 +36,15 @@ export const { ...privateSelectors } = createTradeInputBaseSelectors('tradeInput') +const { selectSellAccountId, selectBuyAccountId } = privateSelectors + // We rename this to include the specific hop to avoid confusion in multi-hop contexts // Selects the account ID we're selling from for the first hop -export const selectFirstHopSellAccountId = privateSelectors.selectSellAccountId +export const selectFirstHopSellAccountId = selectSellAccountId // We rename this to include the specific hop to avoid confusion in multi-hop contexts // Selects the account ID we're buying into for the last hop -export const selectLastHopBuyAccountId = privateSelectors.selectBuyAccountId +export const selectLastHopBuyAccountId = selectBuyAccountId // All the below selectors are re-declared from tradeQuoteSlice/selectors to avoid circular deps // and allow selectSecondHopSellAccountId to keep a pwetty API From fc76235ba4bdba7b6f181e2c5ce0fecb486903af Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:38:41 +1100 Subject: [PATCH 11/23] feat: migrate limit order account IDs to redux --- .../LimitOrder/components/LimitOrderInput.tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 0df5d35ddab..1da0a9b18f5 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,5 +1,6 @@ import { Divider, Stack } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' +import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' import { getDefaultSlippageDecimalPercentageForSwapper, SwapperName } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' @@ -20,12 +21,13 @@ import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { + selectBuyAccountId, selectInputSellAsset, selectLimitPriceBuyAsset, + selectSellAccountId, selectUserSlippagePercentage, } from 'state/slices/limitOrderInputSlice/selectors' import { - selectFirstAccountIdByChainId, selectInputBuyAsset, selectIsAnyAccountMetadataLoadedForChainId, selectMarketDataByAssetIdUserCurrency, @@ -77,19 +79,15 @@ export const LimitOrderInput = ({ const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) const limitPriceBuyAsset = useAppSelector(selectLimitPriceBuyAsset) + const sellAccountId = useAppSelector(selectSellAccountId) + const buyAccountId = useAppSelector(selectBuyAccountId) - const defaultAccountId = useAppSelector(state => - selectFirstAccountIdByChainId(state, sellAsset.chainId), - ) const defaultSlippagePercentage = useMemo(() => { return bn(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap)) .times(100) .toString() }, []) - const [buyAccountId, setBuyAccountId] = useState(defaultAccountId) - const [sellAccountId, setSellAccountId] = useState(defaultAccountId) - const { isRecipientAddressEntryActive, renderedRecipientAddress, recipientAddress } = useLimitOrderRecipientAddress({ buyAsset, @@ -196,6 +194,20 @@ export const LimitOrderInput = ({ [handleFormSubmit], ) + const handleSetSellAccountId = useCallback( + (newSellAccountId: AccountId) => { + dispatch(limitOrderInput.actions.setSellAssetAccountId(newSellAccountId)) + }, + [dispatch], + ) + + const handleSetBuyAccountId = useCallback( + (newBuyAccountId: AccountId) => { + dispatch(limitOrderInput.actions.setBuyAssetAccountId(newBuyAccountId)) + }, + [dispatch], + ) + const sellAmountCryptoBaseUnit = useMemo(() => { return toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision) }, [sellAmountCryptoPrecision, sellAsset.precision]) @@ -333,14 +345,14 @@ export const LimitOrderInput = ({ onChangeIsInputtingFiatSellAmount={setIsInputtingFiatSellAmount} onChangeSellAmountCryptoPrecision={setSellAmountCryptoPrecision} setSellAsset={handleSetSellAsset} - setSellAccountId={setSellAccountId} + setSellAccountId={handleSetSellAccountId} > @@ -366,8 +378,10 @@ export const LimitOrderInput = ({ sellAmountCryptoPrecision, sellAmountUserCurrency, sellAsset, + handleSetBuyAccountId, handleSetBuyAsset, handleSetLimitPriceBuyAsset, + handleSetSellAccountId, handleSetSellAsset, handleSwitchAssets, ]) From 3539519ec79eb38bbfaf54ea92745b7fe10e564d Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 08:44:55 +1100 Subject: [PATCH 12/23] feat: migrate limit order input amounts to redux --- .../LimitOrder/components/LimitOrderInput.tsx | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 1da0a9b18f5..42166597492 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -22,6 +22,8 @@ import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectBuyAccountId, + selectInputSellAmountUsd, + selectInputSellAmountUserCurrency, selectInputSellAsset, selectLimitPriceBuyAsset, selectSellAccountId, @@ -30,7 +32,6 @@ import { import { selectInputBuyAsset, selectIsAnyAccountMetadataLoadedForChainId, - selectMarketDataByAssetIdUserCurrency, selectUsdRateByAssetId, selectUserCurrencyToUsdRate, } from 'state/slices/selectors' @@ -120,21 +121,8 @@ export const LimitOrderInput = ({ [isSnapshotApiQueriesPending, votingPower], ) - const sellAssetMarketDataUserCurrency = useAppSelector(state => - selectMarketDataByAssetIdUserCurrency(state, sellAsset.assetId), - ) - - const sellAmountUserCurrency = useMemo(() => { - return bnOrZero(sellAmountCryptoPrecision) - .times(sellAssetMarketDataUserCurrency.price) - .toFixed() - }, [sellAssetMarketDataUserCurrency.price, sellAmountCryptoPrecision]) - - const sellAmountUsd = useMemo(() => { - return bnOrZero(sellAmountCryptoPrecision) - .times(sellAssetUsdRate ?? '0') - .toFixed() - }, [sellAmountCryptoPrecision, sellAssetUsdRate]) + const inputSellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) + const inputSellAmountUsd = useAppSelector(selectInputSellAmountUsd) const warningAcknowledgementMessage = useMemo(() => { // TODO: Implement me @@ -338,7 +326,7 @@ export const LimitOrderInput = ({ isInputtingFiatSellAmount={isInputtingFiatSellAmount} isLoading={isLoading} sellAmountCryptoPrecision={sellAmountCryptoPrecision} - sellAmountUserCurrency={sellAmountUserCurrency} + sellAmountUserCurrency={inputSellAmountUserCurrency} sellAsset={sellAsset} sellAccountId={sellAccountId} handleSwitchAssets={handleSwitchAssets} @@ -376,7 +364,7 @@ export const LimitOrderInput = ({ marketPriceBuyAsset, sellAccountId, sellAmountCryptoPrecision, - sellAmountUserCurrency, + inputSellAmountUserCurrency, sellAsset, handleSetBuyAccountId, handleSetBuyAsset, @@ -387,7 +375,7 @@ export const LimitOrderInput = ({ ]) const { feeUsd } = useAppSelector(state => - selectCalculatedFees(state, { feeModel: 'SWAPPER', inputAmountUsd: sellAmountUsd }), + selectCalculatedFees(state, { feeModel: 'SWAPPER', inputAmountUsd: inputSellAmountUsd }), ) const affiliateFeeAfterDiscountUserCurrency = useMemo(() => { @@ -401,7 +389,7 @@ export const LimitOrderInput = ({ affiliateFeeAfterDiscountUserCurrency={affiliateFeeAfterDiscountUserCurrency} buyAsset={buyAsset} hasUserEnteredAmount={hasUserEnteredAmount} - inputAmountUsd={sellAmountUsd} + inputAmountUsd={inputSellAmountUsd} isError={Boolean(error)} isLoading={isLoading} quoteStatusTranslation={'limitOrder.previewOrder'} @@ -422,7 +410,7 @@ export const LimitOrderInput = ({ affiliateFeeAfterDiscountUserCurrency, buyAsset, hasUserEnteredAmount, - sellAmountUsd, + inputSellAmountUsd, error, isLoading, limitPriceBuyAsset, From f4597d992a69de98c7b37d35b57f8d60694f9c88 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:03:14 +1100 Subject: [PATCH 13/23] chore: move selectCalculatedFees into into snapshot selectors --- src/components/FeeExplainer/FeeExplainer.tsx | 15 +++++++-- src/components/FeeModal/FeeBreakdown.tsx | 2 +- .../LimitOrder/components/LimitOrderInput.tsx | 26 ++++++++++++--- .../components/FeeStep.tsx | 7 ++-- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 9 +++++- src/lib/fees/model.test.ts | 19 ++++++++++- src/lib/fees/model.ts | 13 +++++--- .../AddLiquidity/AddLiquidityInput.tsx | 9 +++++- src/state/apis/snapshot/selectors.ts | 31 ++++++++++++++++++ src/state/slices/tradeQuoteSlice/selectors.ts | 32 +------------------ src/test/mocks/store.ts | 1 + 11 files changed, 112 insertions(+), 52 deletions(-) diff --git a/src/components/FeeExplainer/FeeExplainer.tsx b/src/components/FeeExplainer/FeeExplainer.tsx index 238021de507..9b4b16dd339 100644 --- a/src/components/FeeExplainer/FeeExplainer.tsx +++ b/src/components/FeeExplainer/FeeExplainer.tsx @@ -26,7 +26,10 @@ import { calculateFees } from 'lib/fees/model' import { FEE_CURVE_PARAMETERS, FEE_MODEL_TO_FEATURE_NAME } from 'lib/fees/parameters' import type { ParameterModel } from 'lib/fees/parameters/types' import { isSome } from 'lib/utils' -import { selectVotingPower } from 'state/apis/snapshot/selectors' +import { + selectIsSnapshotApiQueriesRejected, + selectVotingPower, +} from 'state/apis/snapshot/selectors' import { useAppSelector } from 'state/store' import { CHART_TRADE_SIZE_MAX_USD } from './common' @@ -135,6 +138,8 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) return handleDebounce.cancel }, [foxHolding]) + const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) + const data = useMemo(() => { return tradeSizeData .map(trade => { @@ -142,21 +147,23 @@ const FeeChart: React.FC = ({ foxHolding, tradeSize, feeModel }) tradeAmountUsd: bn(trade), foxHeld: bn(debouncedFoxHolding), feeModel, + isSnapshotApiQueriesRejected, }).feeBpsFloat.toNumber() return { x: trade, y: feeBps } }) .filter(isSome) - }, [debouncedFoxHolding, feeModel]) + }, [debouncedFoxHolding, feeModel, isSnapshotApiQueriesRejected]) const currentPoint = useMemo(() => { const feeBps = calculateFees({ tradeAmountUsd: bn(tradeSize), foxHeld: bn(debouncedFoxHolding), feeModel, + isSnapshotApiQueriesRejected, }).feeBpsFloat.toNumber() return [{ x: tradeSize, y: feeBps }] - }, [tradeSize, debouncedFoxHolding, feeModel]) + }, [tradeSize, debouncedFoxHolding, feeModel, isSnapshotApiQueriesRejected]) const tickLabelProps = useCallback( () => ({ fill: textColor, fontSize: 12, fontWeight: 'medium' }), @@ -263,11 +270,13 @@ type FeeOutputProps = { } export const FeeOutput: React.FC = ({ tradeSizeUSD, foxHolding, feeModel }) => { + const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) const { feeUsd, feeBps, foxDiscountPercent, feeUsdBeforeDiscount, feeBpsBeforeDiscount } = calculateFees({ tradeAmountUsd: bn(tradeSizeUSD), foxHeld: bn(foxHolding), feeModel, + isSnapshotApiQueriesRejected, }) const basedOnFeeTranslation: TextPropTypes['translation'] = useMemo( diff --git a/src/components/FeeModal/FeeBreakdown.tsx b/src/components/FeeModal/FeeBreakdown.tsx index e3cea2b3054..b554b013d26 100644 --- a/src/components/FeeModal/FeeBreakdown.tsx +++ b/src/components/FeeModal/FeeBreakdown.tsx @@ -6,7 +6,7 @@ import { RawText } from 'components/Text' import { BigNumber } from 'lib/bignumber/bignumber' import { FEE_MODEL_TO_FEATURE_NAME } from 'lib/fees/parameters' import type { ParameterModel } from 'lib/fees/parameters/types' -import { selectCalculatedFees } from 'state/slices/tradeQuoteSlice/selectors' +import { selectCalculatedFees } from 'state/apis/snapshot/selectors' import { useAppSelector } from 'state/store' const divider = diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 42166597492..91c31db1e6d 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -18,7 +18,12 @@ import { useWallet } from 'hooks/useWallet/useWallet' import { calculateFees } from 'lib/fees/model' import type { ParameterModel } from 'lib/fees/parameters/types' import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' -import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { + selectCalculatedFees, + selectIsSnapshotApiQueriesPending, + selectIsSnapshotApiQueriesRejected, + selectVotingPower, +} from 'state/apis/snapshot/selectors' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectBuyAccountId, @@ -36,7 +41,6 @@ import { selectUserCurrencyToUsdRate, } from 'state/slices/selectors' import { - selectCalculatedFees, selectIsTradeQuoteRequestAborted, selectShouldShowTradeQuoteOrAwaitInput, } from 'state/slices/tradeQuoteSlice/selectors' @@ -206,6 +210,8 @@ export const LimitOrderInput = ({ return fromAccountId(sellAccountId).account as Address }, [sellAccountId]) + const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) + const affiliateBps = useMemo(() => { const tradeAmountUsd = bnOrZero(sellAssetUsdRate).times(sellAmountCryptoPrecision) @@ -214,10 +220,17 @@ export const LimitOrderInput = ({ foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) return feeBps.toFixed(0) - }, [sellAmountCryptoPrecision, sellAssetUsdRate, thorVotingPower, votingPower]) + }, [ + isSnapshotApiQueriesRejected, + sellAmountCryptoPrecision, + sellAssetUsdRate, + thorVotingPower, + votingPower, + ]) const limitOrderQuoteParams = useMemo(() => { // Return skipToken if any required params are missing @@ -374,10 +387,13 @@ export const LimitOrderInput = ({ handleSwitchAssets, ]) - const { feeUsd } = useAppSelector(state => - selectCalculatedFees(state, { feeModel: 'SWAPPER', inputAmountUsd: inputSellAmountUsd }), + const feeParams = useMemo( + () => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }), + [inputSellAmountUsd], ) + const { feeUsd } = useAppSelector(state => selectCalculatedFees(state, feeParams)) + const affiliateFeeAfterDiscountUserCurrency = useMemo(() => { return bn(feeUsd).times(userCurrencyRate).toFixed(2, BigNumber.ROUND_HALF_UP) }, [feeUsd, userCurrencyRate]) diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx index a21b612eaa9..bd87abd0570 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx @@ -8,12 +8,9 @@ import { RawText } from 'components/Text' import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' import { bnOrZero } from 'lib/bignumber/bignumber' import { THORSWAP_MAXIMUM_YEAR_TRESHOLD, THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' -import { selectThorVotingPower } from 'state/apis/snapshot/selectors' +import { selectCalculatedFees, selectThorVotingPower } from 'state/apis/snapshot/selectors' import { selectInputSellAmountUsd } from 'state/slices/selectors' -import { - selectActiveQuoteAffiliateBps, - selectCalculatedFees, -} from 'state/slices/tradeQuoteSlice/selectors' +import { selectActiveQuoteAffiliateBps } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' import { StepperStep } from './StepperStep' diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index ce3f4dea4d1..c7c075c08d7 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -21,7 +21,11 @@ import type { ParameterModel } from 'lib/fees/parameters/types' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isSome } from 'lib/utils' -import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { + selectIsSnapshotApiQueriesPending, + selectIsSnapshotApiQueriesRejected, + selectVotingPower, +} from 'state/apis/snapshot/selectors' import { swapperApi } from 'state/apis/swapper/swapperApi' import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' import { @@ -126,6 +130,7 @@ export const useGetTradeQuotes = () => { const hasFocus = useHasFocus() const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) + const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) const { manualReceiveAddress, walletReceiveAddress } = useTradeReceiveAddress() const receiveAddress = manualReceiveAddress ?? walletReceiveAddress const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) @@ -224,6 +229,7 @@ export const useGetTradeQuotes = () => { foxHeld: bnOrZero(votingPower), thorHeld: bnOrZero(thorVotingPower), feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) const potentialAffiliateBps = feeBpsBeforeDiscount.toFixed(0) @@ -272,6 +278,7 @@ export const useGetTradeQuotes = () => { isVotingPowerLoading, isBuyAssetChainSupported, quoteOrRate, + isSnapshotApiQueriesRejected, ]) const getTradeQuoteArgs = useCallback( diff --git a/src/lib/fees/model.test.ts b/src/lib/fees/model.test.ts index 95b19b226b5..7d913ec9ccd 100644 --- a/src/lib/fees/model.test.ts +++ b/src/lib/fees/model.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { bn } from 'lib/bignumber/bignumber' +import { selectIsSnapshotApiQueriesRejected } from 'state/apis/snapshot/selectors' +import { store } from 'state/store' import { calculateFees } from './model' import { swapperParameters } from './parameters/swapper' @@ -30,10 +32,12 @@ describe('calculateFees', () => { it('should return 0 bps for < no fee threshold', () => { const tradeAmountUsd = bn(FEE_CURVE_NO_FEE_THRESHOLD_USD).minus(1) const foxHeld = bn(0) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(0) }) @@ -41,10 +45,12 @@ describe('calculateFees', () => { it('should return FEE_CURVE_MAX_FEE_BPS - 1 for === no fee threshold', () => { const tradeAmountUsd = bn(FEE_CURVE_NO_FEE_THRESHOLD_USD) const foxHeld = bn(0) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(FEE_CURVE_MAX_FEE_BPS - 1) }) @@ -52,10 +58,12 @@ describe('calculateFees', () => { it('should return FEE_CURVE_MAX_FEE_BPS - 1 for slightly above no fee threshold', () => { const tradeAmountUsd = bn(FEE_CURVE_NO_FEE_THRESHOLD_USD + 0.01) const foxHeld = bn(0) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(FEE_CURVE_MAX_FEE_BPS - 1) }) @@ -63,10 +71,12 @@ describe('calculateFees', () => { it('should return close to min bps for huge amounts', () => { const tradeAmountUsd = bn(1_000_000) const foxHeld = bn(0) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(FEE_CURVE_MIN_FEE_BPS) }) @@ -74,10 +84,12 @@ describe('calculateFees', () => { it('should return close to midpoint for midpoint', () => { const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(0) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(35) }) @@ -85,10 +97,12 @@ describe('calculateFees', () => { it('should discount fees by 50% holding at midpoint holding half max fox discount limit', () => { const tradeAmountUsd = bn(FEE_CURVE_MIDPOINT_USD) const foxHeld = bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD / 2) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(17) expect(foxDiscountPercent).toEqual(bn(50)) @@ -97,10 +111,12 @@ describe('calculateFees', () => { it('should discount fees 100% holding max fox discount limit', () => { const tradeAmountUsd = bn(Infinity) const foxHeld = bn(FEE_CURVE_FOX_MAX_DISCOUNT_THRESHOLD) + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(0) expect(foxDiscountPercent).toEqual(bn(100)) @@ -111,11 +127,12 @@ describe('calculateFees', () => { mocks.selectIsSnapshotApiQueriesRejected.mockReturnValueOnce(true) const foxHeld = bn(0) - + const isSnapshotApiQueriesRejected = selectIsSnapshotApiQueriesRejected(store.getState()) const { feeBps, foxDiscountPercent } = calculateFees({ tradeAmountUsd, foxHeld, feeModel: 'SWAPPER', + isSnapshotApiQueriesRejected, }) expect(feeBps.toNumber()).toEqual(FEE_CURVE_MAX_FEE_BPS) expect(foxDiscountPercent).toEqual(bn(0)) diff --git a/src/lib/fees/model.ts b/src/lib/fees/model.ts index 926489107d5..f7dd288a820 100644 --- a/src/lib/fees/model.ts +++ b/src/lib/fees/model.ts @@ -1,8 +1,6 @@ import BigNumber from 'bignumber.js' import { getConfig } from 'config' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { selectIsSnapshotApiQueriesRejected } from 'state/apis/snapshot/selectors' -import { store } from 'state/store' import { FEE_CURVE_PARAMETERS } from './parameters' import type { ParameterModel } from './parameters/types' @@ -15,6 +13,7 @@ type CalculateFeeBpsArgs = { foxHeld: BigNumber thorHeld?: BigNumber feeModel: ParameterModel + isSnapshotApiQueriesRejected: boolean } /** @@ -39,7 +38,13 @@ export type CalculateFeeBpsReturn = { } type CalculateFeeBps = (args: CalculateFeeBpsArgs) => CalculateFeeBpsReturn -export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeModel, thorHeld }) => { +export const calculateFees: CalculateFeeBps = ({ + tradeAmountUsd, + foxHeld, + feeModel, + thorHeld, + isSnapshotApiQueriesRejected, +}) => { const { FEE_CURVE_NO_FEE_THRESHOLD_USD, FEE_CURVE_MAX_FEE_BPS, @@ -64,7 +69,7 @@ export const calculateFees: CalculateFeeBps = ({ tradeAmountUsd, foxHeld, feeMod new Date().getUTCFullYear() < THORSWAP_MAXIMUM_YEAR_TRESHOLD // failure to fetch fox discount results in free trades. - const isFallbackFees = selectIsSnapshotApiQueriesRejected(store.getState()) + const isFallbackFees = isSnapshotApiQueriesRejected // the fox discount before any other logic is applied const foxBaseDiscountPercent = (() => { diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 891ae1cbe22..87f36bcb35d 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -84,7 +84,11 @@ import { useUserLpData } from 'pages/ThorChainLP/queries/hooks/useUserLpData' import { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' import type { Opportunity } from 'pages/ThorChainLP/utils' import { fromOpportunityId, toOpportunityId } from 'pages/ThorChainLP/utils' -import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { + selectIsSnapshotApiQueriesPending, + selectIsSnapshotApiQueriesRejected, + selectVotingPower, +} from 'state/apis/snapshot/selectors' import { snapshotApi } from 'state/apis/snapshot/snapshot' import { selectAccountIdsByAssetId, @@ -167,6 +171,7 @@ export const AddLiquidityInput: React.FC = ({ const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) + const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) const { isSnapInstalled } = useIsSnapInstalled() const isVotingPowerLoading = useMemo( () => isSnapshotApiQueriesPending, @@ -1015,6 +1020,7 @@ export const AddLiquidityInput: React.FC = ({ tradeAmountUsd: bn(totalAmountUsd), foxHeld: bnOrZero(votingPower), feeModel: 'THORCHAIN_LP', + isSnapshotApiQueriesRejected, }) setConfirmedQuote({ @@ -1056,6 +1062,7 @@ export const AddLiquidityInput: React.FC = ({ totalGasFeeFiatUserCurrency, userCurrencyToUsdRate, votingPower, + isSnapshotApiQueriesRejected, ]) const percentOptions = useMemo(() => [], []) diff --git a/src/state/apis/snapshot/selectors.ts b/src/state/apis/snapshot/selectors.ts index ff0a43ffdee..3619f556975 100644 --- a/src/state/apis/snapshot/selectors.ts +++ b/src/state/apis/snapshot/selectors.ts @@ -1,6 +1,12 @@ import { QueryStatus } from '@reduxjs/toolkit/dist/query' import { ethChainId } from '@shapeshiftoss/caip' +import { bnOrZero } from '@shapeshiftoss/utils' +import createCachedSelector from 're-reselect' +import type { Selector } from 'reselect' import { createSelector } from 'reselect' +import type { CalculateFeeBpsReturn } from 'lib/fees/model' +import { calculateFees } from 'lib/fees/model' +import type { ParameterModel } from 'lib/fees/parameters/types' import type { ReduxState } from 'state/reducer' import { selectFeeModelParamFromFilter } from 'state/selectors' import { selectAccountIdsByChainId } from 'state/slices/portfolioSlice/selectors' @@ -36,3 +42,28 @@ export const selectThorVotingPower = (state: ReduxState) => state.snapshot.votingPowerByModel['THORSWAP'] export const selectProposals = (state: ReduxState) => state.snapshot.proposals + +type AffiliateFeesProps = { + feeModel: ParameterModel + inputAmountUsd: string | undefined +} + +export const selectCalculatedFees: Selector = + createCachedSelector( + (_state: ReduxState, { feeModel }: AffiliateFeesProps) => feeModel, + (_state: ReduxState, { inputAmountUsd }: AffiliateFeesProps) => inputAmountUsd, + selectVotingPower, + selectThorVotingPower, + selectIsSnapshotApiQueriesRejected, + (feeModel, inputAmountUsd, votingPower, thorVotingPower, isSnapshotApiQueriesRejected) => { + const fees: CalculateFeeBpsReturn = calculateFees({ + tradeAmountUsd: bnOrZero(inputAmountUsd), + foxHeld: bnOrZero(votingPower), + thorHeld: bnOrZero(thorVotingPower), + feeModel, + isSnapshotApiQueriesRejected, + }) + + return fees + }, + )((_state, { feeModel, inputAmountUsd }) => `${feeModel}-${inputAmountUsd}`) diff --git a/src/state/slices/tradeQuoteSlice/selectors.ts b/src/state/slices/tradeQuoteSlice/selectors.ts index db34ad921e2..ad7307f7068 100644 --- a/src/state/slices/tradeQuoteSlice/selectors.ts +++ b/src/state/slices/tradeQuoteSlice/selectors.ts @@ -8,14 +8,10 @@ import { } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import { identity } from 'lodash' -import createCachedSelector from 're-reselect' import type { Selector } from 'reselect' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import type { CalculateFeeBpsReturn } from 'lib/fees/model' -import { calculateFees } from 'lib/fees/model' -import type { ParameterModel } from 'lib/fees/parameters/types' import { fromBaseUnit } from 'lib/math' -import { selectThorVotingPower, selectVotingPower } from 'state/apis/snapshot/selectors' +import { selectCalculatedFees } from 'state/apis/snapshot/selectors' import { validateQuoteRequest } from 'state/apis/swapper/helpers/validateQuoteRequest' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' import type { ApiQuote, ErrorWithMeta, TradeQuoteError } from 'state/apis/swapper/types' @@ -550,32 +546,6 @@ export const selectActiveQuoteAffiliateBps: Selector = - createCachedSelector( - (_state: ReduxState, { feeModel }: AffiliateFeesProps) => feeModel, - (_state: ReduxState, { inputAmountUsd }: AffiliateFeesProps) => inputAmountUsd, - selectVotingPower, - selectThorVotingPower, - - (feeModel, inputAmountUsd, votingPower, thorVotingPower) => { - const fees: CalculateFeeBpsReturn = calculateFees({ - tradeAmountUsd: bnOrZero(inputAmountUsd), - foxHeld: bnOrZero(votingPower), - thorHeld: bnOrZero(thorVotingPower), - feeModel, - }) - - return fees - }, - )((_state, { feeModel, inputAmountUsd }) => `${feeModel}-${inputAmountUsd}`) - export const selectTradeQuoteAffiliateFeeAfterDiscountUsd = createSelector( (state: ReduxState) => selectCalculatedFees(state, { diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 913162591cb..998a4f2fa8f 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -249,6 +249,7 @@ export const mockStore: ReduxState = { manualReceiveAddressIsEditing: false, manualReceiveAddressIsValid: undefined, slippagePreferencePercentage: undefined, + limitPriceBuyAsset: '0', }, tradeQuoteSlice: { activeQuoteMeta: undefined, From ca39e07dc96b18fcf513c35da12f43d3faed6513 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:35:52 +1100 Subject: [PATCH 14/23] chore: cleanup voting power selectors --- .../LimitOrder/components/LimitOrderInput.tsx | 71 ++++--------------- .../components/TradeInput/TradeInput.tsx | 11 +-- .../useGetTradeQuotes/useGetTradeQuotes.tsx | 8 +-- .../AddLiquidity/AddLiquidityInput.tsx | 8 +-- src/state/apis/snapshot/selectors.ts | 22 ++++++ 5 files changed, 43 insertions(+), 77 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 91c31db1e6d..3fe4075ddae 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -15,15 +15,8 @@ import { TradeInputTab } from 'components/MultiHopTrade/types' import { WalletActions } from 'context/WalletProvider/actions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' -import { calculateFees } from 'lib/fees/model' -import type { ParameterModel } from 'lib/fees/parameters/types' import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' -import { - selectCalculatedFees, - selectIsSnapshotApiQueriesPending, - selectIsSnapshotApiQueriesRejected, - selectVotingPower, -} from 'state/apis/snapshot/selectors' +import { selectCalculatedFees, selectIsVotingPowerLoading } from 'state/apis/snapshot/selectors' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectBuyAccountId, @@ -37,7 +30,6 @@ import { import { selectInputBuyAsset, selectIsAnyAccountMetadataLoadedForChainId, - selectUsdRateByAssetId, selectUserCurrencyToUsdRate, } from 'state/slices/selectors' import { @@ -56,9 +48,6 @@ import { CollapsibleLimitOrderList } from './CollapsibleLimitOrderList' import { LimitOrderBuyAsset } from './LimitOrderBuyAsset' import { LimitOrderConfig } from './LimitOrderConfig' -const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } -const thorVotingPowerParams: { feeModel: ParameterModel } = { feeModel: 'THORSWAP' } - type LimitOrderInputProps = { tradeInputRef: React.MutableRefObject isCompact?: boolean @@ -86,6 +75,15 @@ export const LimitOrderInput = ({ const limitPriceBuyAsset = useAppSelector(selectLimitPriceBuyAsset) const sellAccountId = useAppSelector(selectSellAccountId) const buyAccountId = useAppSelector(selectBuyAccountId) + const inputSellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) + const inputSellAmountUsd = useAppSelector(selectInputSellAmountUsd) + + const feeParams = useMemo( + () => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }), + [inputSellAmountUsd], + ) + + const { feeUsd, feeBps } = useAppSelector(state => selectCalculatedFees(state, feeParams)) const defaultSlippagePercentage = useMemo(() => { return bn(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap)) @@ -105,12 +103,8 @@ export const LimitOrderInput = ({ const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) const [sellAmountCryptoPrecision, setSellAmountCryptoPrecision] = useState('0') const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) const hasUserEnteredAmount = true - const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) - const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) - const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId)) const userCurrencyRate = useAppSelector(selectUserCurrencyToUsdRate) const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( () => ({ chainId: sellAsset.chainId }), @@ -120,13 +114,7 @@ export const LimitOrderInput = ({ selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), ) - const isVotingPowerLoading = useMemo( - () => isSnapshotApiQueriesPending && votingPower === undefined, - [isSnapshotApiQueriesPending, votingPower], - ) - - const inputSellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) - const inputSellAmountUsd = useAppSelector(selectInputSellAmountUsd) + const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) const warningAcknowledgementMessage = useMemo(() => { // TODO: Implement me @@ -210,28 +198,6 @@ export const LimitOrderInput = ({ return fromAccountId(sellAccountId).account as Address }, [sellAccountId]) - const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) - - const affiliateBps = useMemo(() => { - const tradeAmountUsd = bnOrZero(sellAssetUsdRate).times(sellAmountCryptoPrecision) - - const { feeBps } = calculateFees({ - tradeAmountUsd, - foxHeld: bnOrZero(votingPower), - thorHeld: bnOrZero(thorVotingPower), - feeModel: 'SWAPPER', - isSnapshotApiQueriesRejected, - }) - - return feeBps.toFixed(0) - }, [ - isSnapshotApiQueriesRejected, - sellAmountCryptoPrecision, - sellAssetUsdRate, - thorVotingPower, - votingPower, - ]) - const limitOrderQuoteParams = useMemo(() => { // Return skipToken if any required params are missing if (bnOrZero(sellAmountCryptoBaseUnit).isZero()) { @@ -245,7 +211,7 @@ export const LimitOrderInput = ({ slippageTolerancePercentageDecimal: bn(userSlippagePercentage ?? defaultSlippagePercentage) .div(100) .toString(), - affiliateBps, + affiliateBps: feeBps.toFixed(0), sellAccountAddress, sellAmountCryptoBaseUnit, recipientAddress, @@ -257,7 +223,7 @@ export const LimitOrderInput = ({ buyAsset.assetId, userSlippagePercentage, defaultSlippagePercentage, - affiliateBps, + feeBps, sellAccountAddress, recipientAddress, ]) @@ -387,13 +353,6 @@ export const LimitOrderInput = ({ handleSwitchAssets, ]) - const feeParams = useMemo( - () => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }), - [inputSellAmountUsd], - ) - - const { feeUsd } = useAppSelector(state => selectCalculatedFees(state, feeParams)) - const affiliateFeeAfterDiscountUserCurrency = useMemo(() => { return bn(feeUsd).times(userCurrencyRate).toFixed(2, BigNumber.ROUND_HALF_UP) }, [feeUsd, userCurrencyRate]) @@ -401,7 +360,7 @@ export const LimitOrderInput = ({ const footerContent = useMemo(() => { return ( ) }, [ - affiliateBps, + feeBps, affiliateFeeAfterDiscountUserCurrency, buyAsset, hasUserEnteredAmount, diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index d0dc343961a..02714eb9a68 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -23,12 +23,11 @@ import { WalletActions } from 'context/WalletProvider/actions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useModal } from 'hooks/useModal/useModal' import { useWallet } from 'hooks/useWallet/useWallet' -import type { ParameterModel } from 'lib/fees/parameters/types' import { fromBaseUnit } from 'lib/math' import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isKeplrHDWallet } from 'lib/utils' -import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { selectIsVotingPowerLoading } from 'state/apis/snapshot/selectors' import { selectHasUserEnteredAmount, selectInputBuyAsset, @@ -60,7 +59,6 @@ import { ConfirmSummary } from './components/ConfirmSummary' import { TradeSettingsMenu } from './components/TradeSettingsMenu' import { useTradeReceiveAddress } from './hooks/useTradeReceiveAddress' -const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } const emptyPercentOptions: number[] = [] const formControlProps = { borderRadius: 0, @@ -104,13 +102,11 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) const isInputtingFiatSellAmount = useAppSelector(selectIsInputtingFiatSellAmount) const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) const tradeQuoteStep = useAppSelector(selectFirstHop) const isUnsafeQuote = useAppSelector(selectIsUnsafeActiveQuote) - const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) const activeQuote = useAppSelector(selectActiveQuote) @@ -133,10 +129,7 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const isKeplr = useMemo(() => !!wallet && isKeplrHDWallet(wallet), [wallet]) - const isVotingPowerLoading = useMemo( - () => isSnapshotApiQueriesPending && votingPower === undefined, - [isSnapshotApiQueriesPending, votingPower], - ) + const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) const isLoading = useMemo( () => diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index c7c075c08d7..f5ee1000227 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -22,8 +22,8 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isSome } from 'lib/utils' import { - selectIsSnapshotApiQueriesPending, selectIsSnapshotApiQueriesRejected, + selectIsVotingPowerLoading, selectVotingPower, } from 'state/apis/snapshot/selectors' import { swapperApi } from 'state/apis/swapper/swapperApi' @@ -165,13 +165,9 @@ export const useGetTradeQuotes = () => { const sellAssetUsdRate = useAppSelector(state => selectUsdRateByAssetId(state, sellAsset.assetId)) - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) const thorVotingPower = useAppSelector(state => selectVotingPower(state, thorVotingPowerParams)) - const isVotingPowerLoading = useMemo( - () => isSnapshotApiQueriesPending && votingPower === undefined, - [isSnapshotApiQueriesPending, votingPower], - ) + const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const isBuyAssetChainSupported = walletSupportsBuyAssetChain diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 87f36bcb35d..3a92ea5471e 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -85,8 +85,8 @@ import { getThorchainLpPosition } from 'pages/ThorChainLP/queries/queries' import type { Opportunity } from 'pages/ThorChainLP/utils' import { fromOpportunityId, toOpportunityId } from 'pages/ThorChainLP/utils' import { - selectIsSnapshotApiQueriesPending, selectIsSnapshotApiQueriesRejected, + selectIsVotingPowerLoading, selectVotingPower, } from 'state/apis/snapshot/selectors' import { snapshotApi } from 'state/apis/snapshot/snapshot' @@ -170,13 +170,9 @@ export const AddLiquidityInput: React.FC = ({ const accountIdsByChainId = useAppSelector(selectAccountIdsByChainId) const userCurrencyToUsdRate = useAppSelector(selectUserCurrencyToUsdRate) const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) const isSnapshotApiQueriesRejected = useAppSelector(selectIsSnapshotApiQueriesRejected) const { isSnapInstalled } = useIsSnapInstalled() - const isVotingPowerLoading = useMemo( - () => isSnapshotApiQueriesPending, - [isSnapshotApiQueriesPending], - ) + const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) const [showFeeModal, toggleShowFeeModal] = useState(false) const [poolAsset, setPoolAsset] = useState() diff --git a/src/state/apis/snapshot/selectors.ts b/src/state/apis/snapshot/selectors.ts index 3619f556975..1be612c23b7 100644 --- a/src/state/apis/snapshot/selectors.ts +++ b/src/state/apis/snapshot/selectors.ts @@ -7,6 +7,7 @@ import { createSelector } from 'reselect' import type { CalculateFeeBpsReturn } from 'lib/fees/model' import { calculateFees } from 'lib/fees/model' import type { ParameterModel } from 'lib/fees/parameters/types' +import { isSome } from 'lib/utils' import type { ReduxState } from 'state/reducer' import { selectFeeModelParamFromFilter } from 'state/selectors' import { selectAccountIdsByChainId } from 'state/slices/portfolioSlice/selectors' @@ -24,6 +25,7 @@ export const selectIsSnapshotApiQueriesRejected = createSelector( ) export const selectVotingPowerByModel = (state: ReduxState) => state.snapshot.votingPowerByModel + export const selectVotingPower = createSelector( selectVotingPowerByModel, selectFeeModelParamFromFilter, @@ -38,6 +40,13 @@ export const selectVotingPower = createSelector( return votingPowerByModel[feeModel!] }, ) + +export const selectThorchainLpVotingPower = (state: ReduxState) => + state.snapshot.votingPowerByModel['THORCHAIN_LP'] + +export const selectSwapperVotingPower = (state: ReduxState) => + state.snapshot.votingPowerByModel['SWAPPER'] + export const selectThorVotingPower = (state: ReduxState) => state.snapshot.votingPowerByModel['THORSWAP'] @@ -67,3 +76,16 @@ export const selectCalculatedFees: Selector = return fees }, )((_state, { feeModel, inputAmountUsd }) => `${feeModel}-${inputAmountUsd}`) + +export const selectIsVotingPowerLoading = createSelector( + selectIsSnapshotApiQueriesPending, + selectThorVotingPower, + selectSwapperVotingPower, + selectThorchainLpVotingPower, + (isSnapshotApiQueriesPending, thorVotingPower, swapperVotingPower, thorchainLpVotingPower) => { + return ( + isSnapshotApiQueriesPending && + ![thorVotingPower, swapperVotingPower, thorchainLpVotingPower].every(isSome) + ) + }, +) From 2e22e89ebfce4be8833759059bf1bdf236bb1833 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:56:45 +1100 Subject: [PATCH 15/23] feat: migrate limit order useLimitOrderRecipientAddress to redux --- .../hooks/useLimitOrderRecipientAddress.tsx | 78 +++++++++++++------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx index 8b441bb3951..16738869373 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx @@ -1,8 +1,16 @@ import type { Asset } from '@shapeshiftoss/types' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import type { Address } from 'viem' import { useIsManualReceiveAddressRequired } from 'components/MultiHopTrade/hooks/useIsManualReceiveAddressRequired' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' +import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' +import { + selectManualReceiveAddress, + selectManualReceiveAddressIsEditing, + selectManualReceiveAddressIsValid, + selectManualReceiveAddressIsValidating, +} from 'state/slices/limitOrderInputSlice/selectors' +import { useAppDispatch, useAppSelector } from 'state/store' import { SharedRecipientAddress } from '../../SharedTradeInput/SharedRecipientAddress' @@ -17,12 +25,13 @@ export const useLimitOrderRecipientAddress = ({ buyAccountId, sellAccountId, }: UseLimitOrderRecipientAddressProps) => { - const [manualReceiveAddress, setManualReceiveAddress] = useState(undefined) - const [isManualReceiveAddressValid, setIsManualReceiveAddressValid] = useState< - boolean | undefined - >(undefined) - const [isManualReceiveAddressEditing, setIsManualReceiveAddressEditing] = useState(false) - const [isManualReceiveAddressValidating, setIsManualReceiveAddressValidating] = useState(false) + const dispatch = useAppDispatch() + + const manualReceiveAddress = useAppSelector(selectManualReceiveAddress) + const isManualReceiveAddressValid = useAppSelector(selectManualReceiveAddressIsValid) + const isManualReceiveAddressEditing = useAppSelector(selectManualReceiveAddressIsEditing) + const isManualReceiveAddressValidating = useAppSelector(selectManualReceiveAddressIsValidating) + const { walletReceiveAddress, isLoading: isWalletReceiveAddressLoading } = useReceiveAddress({ sellAccountId, buyAccountId, @@ -30,30 +39,47 @@ export const useLimitOrderRecipientAddress = ({ }) const handleManualReceiveAddressError = useCallback(() => { - setManualReceiveAddress(undefined) - }, []) + dispatch(limitOrderInput.actions.setManualReceiveAddress(undefined)) + }, [dispatch]) const handleEditManualReceiveAddress = useCallback(() => { - setIsManualReceiveAddressEditing(true) - }, []) + dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(true)) + }, [dispatch]) const handleCancelManualReceiveAddress = useCallback(() => { - setIsManualReceiveAddressEditing(false) + dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(false)) // Reset form value and valid state on cancel so the valid check doesn't wrongly evaluate to false after bailing out of editing an invalid address - setIsManualReceiveAddressValid(undefined) - }, []) + dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(undefined)) + }, [dispatch]) const handleResetManualReceiveAddress = useCallback(() => { // Reset the manual receive address in store - setManualReceiveAddress(undefined) + dispatch(limitOrderInput.actions.setManualReceiveAddress(undefined)) // Reset the valid state in store - setIsManualReceiveAddressValid(undefined) - }, []) + dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(undefined)) + }, [dispatch]) + + const handleSubmitManualReceiveAddress = useCallback( + (address: string) => { + dispatch(limitOrderInput.actions.setManualReceiveAddress(address)) + dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(false)) + }, + [dispatch], + ) + + const handleIsManualReceiveAddressValidatingChange = useCallback( + (isValidating: boolean) => { + dispatch(limitOrderInput.actions.setManualReceiveAddressIsValidating(isValidating)) + }, + [dispatch], + ) - const handleSubmitManualReceiveAddress = useCallback((address: string) => { - setManualReceiveAddress(address) - setIsManualReceiveAddressEditing(false) - }, []) + const handleIsManualReceiveAddressValidChange = useCallback( + (isValid: boolean) => { + dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(isValid)) + }, + [dispatch], + ) const isManualReceiveAddressRequired = useIsManualReceiveAddressRequired({ shouldForceManualAddressEntry: false, @@ -88,22 +114,24 @@ export const useLimitOrderRecipientAddress = ({ onCancel={handleCancelManualReceiveAddress} onEdit={handleEditManualReceiveAddress} onError={handleManualReceiveAddressError} - onIsValidatingChange={setIsManualReceiveAddressValidating} - onIsValidChange={setIsManualReceiveAddressValid} + onIsValidatingChange={handleIsManualReceiveAddressValidatingChange} + onIsValidChange={handleIsManualReceiveAddressValidChange} onReset={handleResetManualReceiveAddress} onSubmit={handleSubmitManualReceiveAddress} /> ) }, [ buyAsset, + isWalletReceiveAddressLoading, manualReceiveAddress, + walletReceiveAddress, handleCancelManualReceiveAddress, handleEditManualReceiveAddress, handleManualReceiveAddressError, + handleIsManualReceiveAddressValidatingChange, + handleIsManualReceiveAddressValidChange, handleResetManualReceiveAddress, handleSubmitManualReceiveAddress, - walletReceiveAddress, - isWalletReceiveAddressLoading, ]) return { From e30f46a26bfae90a8e767f691cb5562aef350a01 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:03:07 +1100 Subject: [PATCH 16/23] chore: cleanup slippage percentages in limit order input --- .../LimitOrder/components/LimitOrderInput.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 3fe4075ddae..b30b624e748 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -26,6 +26,7 @@ import { selectLimitPriceBuyAsset, selectSellAccountId, selectUserSlippagePercentage, + selectUserSlippagePercentageDecimal, } from 'state/slices/limitOrderInputSlice/selectors' import { selectInputBuyAsset, @@ -69,6 +70,7 @@ export const LimitOrderInput = ({ const { handleSubmit } = useFormContext() const { showErrorToast } = useErrorHandler() + const userSlippagePercentageDecimal = useAppSelector(selectUserSlippagePercentageDecimal) const userSlippagePercentage = useAppSelector(selectUserSlippagePercentage) const sellAsset = useAppSelector(selectInputSellAsset) const buyAsset = useAppSelector(selectInputBuyAsset) @@ -85,11 +87,12 @@ export const LimitOrderInput = ({ const { feeUsd, feeBps } = useAppSelector(state => selectCalculatedFees(state, feeParams)) - const defaultSlippagePercentage = useMemo(() => { - return bn(getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap)) - .times(100) - .toString() + const defaultSlippagePercentageDecimal = useMemo(() => { + return getDefaultSlippageDecimalPercentageForSwapper(SwapperName.CowSwap) }, []) + const defaultSlippagePercentage = useMemo(() => { + return bn(defaultSlippagePercentageDecimal).times(100).toString() + }, [defaultSlippagePercentageDecimal]) const { isRecipientAddressEntryActive, renderedRecipientAddress, recipientAddress } = useLimitOrderRecipientAddress({ @@ -208,9 +211,8 @@ export const LimitOrderInput = ({ sellAssetId: sellAsset.assetId, buyAssetId: buyAsset.assetId, chainId: sellAsset.chainId, - slippageTolerancePercentageDecimal: bn(userSlippagePercentage ?? defaultSlippagePercentage) - .div(100) - .toString(), + slippageTolerancePercentageDecimal: + userSlippagePercentageDecimal ?? defaultSlippagePercentageDecimal, affiliateBps: feeBps.toFixed(0), sellAccountAddress, sellAmountCryptoBaseUnit, @@ -221,8 +223,8 @@ export const LimitOrderInput = ({ sellAsset.assetId, sellAsset.chainId, buyAsset.assetId, - userSlippagePercentage, - defaultSlippagePercentage, + userSlippagePercentageDecimal, + defaultSlippagePercentageDecimal, feeBps, sellAccountAddress, recipientAddress, From 9e5f647f8188803443cb23aedeb9ac5cf5c4bc17 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:12:22 +1100 Subject: [PATCH 17/23] feat: migrate remaining limit order input handlers to redux --- .../LimitOrder/components/LimitOrderInput.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index b30b624e748..f77be0e7b6c 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -20,9 +20,12 @@ import { selectCalculatedFees, selectIsVotingPowerLoading } from 'state/apis/sna import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectBuyAccountId, + selectHasUserEnteredAmount, + selectInputSellAmountCryptoPrecision, selectInputSellAmountUsd, selectInputSellAmountUserCurrency, selectInputSellAsset, + selectIsInputtingFiatSellAmount, selectLimitPriceBuyAsset, selectSellAccountId, selectUserSlippagePercentage, @@ -79,6 +82,13 @@ export const LimitOrderInput = ({ const buyAccountId = useAppSelector(selectBuyAccountId) const inputSellAmountUserCurrency = useAppSelector(selectInputSellAmountUserCurrency) const inputSellAmountUsd = useAppSelector(selectInputSellAmountUsd) + const isInputtingFiatSellAmount = useAppSelector(selectIsInputtingFiatSellAmount) + const sellAmountCryptoPrecision = useAppSelector(selectInputSellAmountCryptoPrecision) + const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) + const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) + const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) + const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) + const userCurrencyRate = useAppSelector(selectUserCurrencyToUsdRate) const feeParams = useMemo( () => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }), @@ -101,14 +111,9 @@ export const LimitOrderInput = ({ sellAccountId, }) - const [isInputtingFiatSellAmount, setIsInputtingFiatSellAmount] = useState(false) const [isConfirmationLoading, setIsConfirmationLoading] = useState(false) const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) - const [sellAmountCryptoPrecision, setSellAmountCryptoPrecision] = useState('0') - const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) - const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) - const hasUserEnteredAmount = true - const userCurrencyRate = useAppSelector(selectUserCurrencyToUsdRate) + const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( () => ({ chainId: sellAsset.chainId }), [sellAsset.chainId], @@ -117,8 +122,6 @@ export const LimitOrderInput = ({ selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), ) - const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) - const warningAcknowledgementMessage = useMemo(() => { // TODO: Implement me return '' @@ -289,6 +292,20 @@ export const LimitOrderInput = ({ [dispatch], ) + const handleSetIsInputtingFiatSellAmount = useCallback( + (isInputtingFiatSellAmount: boolean) => { + dispatch(limitOrderInput.actions.setIsInputtingFiatSellAmount(isInputtingFiatSellAmount)) + }, + [dispatch], + ) + + const handleSetSellAmountCryptoPrecision = useCallback( + (sellAmountCryptoPrecision: string) => { + dispatch(limitOrderInput.actions.setSellAmountCryptoPrecision(sellAmountCryptoPrecision)) + }, + [dispatch], + ) + const headerRightContent = useMemo(() => { return ( @@ -339,18 +356,20 @@ export const LimitOrderInput = ({ }, [ buyAccountId, buyAsset, + inputSellAmountUserCurrency, isInputtingFiatSellAmount, isLoading, limitPriceBuyAsset, marketPriceBuyAsset, sellAccountId, sellAmountCryptoPrecision, - inputSellAmountUserCurrency, sellAsset, + handleSetIsInputtingFiatSellAmount, handleSetBuyAccountId, handleSetBuyAsset, handleSetLimitPriceBuyAsset, handleSetSellAccountId, + handleSetSellAmountCryptoPrecision, handleSetSellAsset, handleSwitchAssets, ]) From 4d87276f002c8c02ab24b1105385fbb69c10dacd Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:35:38 +1100 Subject: [PATCH 18/23] feat: introduced useActions hook for max cleanliness --- .../LimitOrder/components/LimitOrderInput.tsx | 118 +++++------------- src/hooks/useActions.tsx | 20 +++ 2 files changed, 54 insertions(+), 84 deletions(-) create mode 100644 src/hooks/useActions.tsx diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index f77be0e7b6c..7b8330f523c 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,9 +1,7 @@ import { Divider, Stack } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' -import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId } from '@shapeshiftoss/caip' import { getDefaultSlippageDecimalPercentageForSwapper, SwapperName } from '@shapeshiftoss/swapper' -import type { Asset } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero, fromBaseUnit, toBaseUnit } from '@shapeshiftoss/utils' import type { FormEvent } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -13,6 +11,7 @@ import type { Address } from 'viem' import { WarningAcknowledgement } from 'components/Acknowledgement/Acknowledgement' import { TradeInputTab } from 'components/MultiHopTrade/types' import { WalletActions } from 'context/WalletProvider/actions' +import { useActions } from 'hooks/useActions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' import { useQuoteLimitOrderQuery } from 'state/apis/limit-orders/limitOrderApi' @@ -40,7 +39,7 @@ import { selectIsTradeQuoteRequestAborted, selectShouldShowTradeQuoteOrAwaitInput, } from 'state/slices/tradeQuoteSlice/selectors' -import { useAppDispatch, useAppSelector } from 'state/store' +import { useAppSelector } from 'state/store' import { SharedSlippagePopover } from '../../SharedTradeInput/SharedSlippagePopover' import { SharedTradeInput } from '../../SharedTradeInput/SharedTradeInput' @@ -67,7 +66,6 @@ export const LimitOrderInput = ({ dispatch: walletDispatch, state: { isConnected, isDemoWallet }, } = useWallet() - const dispatch = useAppDispatch() const history = useHistory() const { handleSubmit } = useFormContext() @@ -90,6 +88,18 @@ export const LimitOrderInput = ({ const isVotingPowerLoading = useAppSelector(selectIsVotingPowerLoading) const userCurrencyRate = useAppSelector(selectUserCurrencyToUsdRate) + const { + switchAssets, + setSellAsset, + setBuyAsset, + setSellAssetAccountId, + setBuyAssetAccountId, + setLimitPriceBuyAsset, + setSlippagePreferencePercentage, + setIsInputtingFiatSellAmount, + setSellAmountCryptoPrecision, + } = useActions(limitOrderInput.actions) + const feeParams = useMemo( () => ({ feeModel: 'SWAPPER' as const, inputAmountUsd: inputSellAmountUsd }), [inputSellAmountUsd], @@ -127,24 +137,6 @@ export const LimitOrderInput = ({ return '' }, []) - const handleSwitchAssets = useCallback(() => { - dispatch(limitOrderInput.actions.switchAssets()) - }, [dispatch]) - - const handleSetSellAsset = useCallback( - (newSellAsset: Asset) => { - dispatch(limitOrderInput.actions.setSellAsset(newSellAsset)) - }, - [dispatch], - ) - - const handleSetBuyAsset = useCallback( - (newBuyAsset: Asset) => { - dispatch(limitOrderInput.actions.setBuyAsset(newBuyAsset)) - }, - [dispatch], - ) - const handleConnect = useCallback(() => { walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) }, [walletDispatch]) @@ -180,20 +172,6 @@ export const LimitOrderInput = ({ [handleFormSubmit], ) - const handleSetSellAccountId = useCallback( - (newSellAccountId: AccountId) => { - dispatch(limitOrderInput.actions.setSellAssetAccountId(newSellAccountId)) - }, - [dispatch], - ) - - const handleSetBuyAccountId = useCallback( - (newBuyAccountId: AccountId) => { - dispatch(limitOrderInput.actions.setBuyAssetAccountId(newBuyAccountId)) - }, - [dispatch], - ) - const sellAmountCryptoBaseUnit = useMemo(() => { return toBaseUnit(sellAmountCryptoPrecision, sellAsset.precision) }, [sellAmountCryptoPrecision, sellAsset.precision]) @@ -254,15 +232,8 @@ export const LimitOrderInput = ({ // TODO: If we introduce polling of quotes, we will need to add logic inside `LimitOrderConfig` to // not reset the user's config unless the asset pair changes. useEffect(() => { - dispatch(limitOrderInput.actions.setLimitPriceBuyAsset(marketPriceBuyAsset)) - }, [dispatch, marketPriceBuyAsset]) - - const handleSetLimitPriceBuyAsset = useCallback( - (newMarketPriceBuyAsset: string) => { - dispatch(limitOrderInput.actions.setLimitPriceBuyAsset(newMarketPriceBuyAsset)) - }, - [dispatch], - ) + setLimitPriceBuyAsset(marketPriceBuyAsset) + }, [setLimitPriceBuyAsset, marketPriceBuyAsset]) const isLoading = useMemo(() => { return ( @@ -285,37 +256,16 @@ export const LimitOrderInput = ({ shouldShowTradeQuoteOrAwaitInput, ]) - const handleSetUserSlippagePercentage = useCallback( - (slippagePercentage: string | undefined) => { - dispatch(limitOrderInput.actions.setSlippagePreferencePercentage(slippagePercentage)) - }, - [dispatch], - ) - - const handleSetIsInputtingFiatSellAmount = useCallback( - (isInputtingFiatSellAmount: boolean) => { - dispatch(limitOrderInput.actions.setIsInputtingFiatSellAmount(isInputtingFiatSellAmount)) - }, - [dispatch], - ) - - const handleSetSellAmountCryptoPrecision = useCallback( - (sellAmountCryptoPrecision: string) => { - dispatch(limitOrderInput.actions.setSellAmountCryptoPrecision(sellAmountCryptoPrecision)) - }, - [dispatch], - ) - const headerRightContent = useMemo(() => { return ( ) - }, [defaultSlippagePercentage, handleSetUserSlippagePercentage, userSlippagePercentage]) + }, [defaultSlippagePercentage, setSlippagePreferencePercentage, userSlippagePercentage]) const bodyContent = useMemo(() => { return ( @@ -327,19 +277,19 @@ export const LimitOrderInput = ({ sellAmountUserCurrency={inputSellAmountUserCurrency} sellAsset={sellAsset} sellAccountId={sellAccountId} - handleSwitchAssets={handleSwitchAssets} - onChangeIsInputtingFiatSellAmount={handleSetIsInputtingFiatSellAmount} - onChangeSellAmountCryptoPrecision={handleSetSellAmountCryptoPrecision} - setSellAsset={handleSetSellAsset} - setSellAccountId={handleSetSellAccountId} + handleSwitchAssets={switchAssets} + onChangeIsInputtingFiatSellAmount={setIsInputtingFiatSellAmount} + onChangeSellAmountCryptoPrecision={setSellAmountCryptoPrecision} + setSellAsset={setSellAsset} + setSellAccountId={setSellAssetAccountId} > @@ -364,14 +314,14 @@ export const LimitOrderInput = ({ sellAccountId, sellAmountCryptoPrecision, sellAsset, - handleSetIsInputtingFiatSellAmount, - handleSetBuyAccountId, - handleSetBuyAsset, - handleSetLimitPriceBuyAsset, - handleSetSellAccountId, - handleSetSellAmountCryptoPrecision, - handleSetSellAsset, - handleSwitchAssets, + setBuyAsset, + setBuyAssetAccountId, + setIsInputtingFiatSellAmount, + setLimitPriceBuyAsset, + setSellAmountCryptoPrecision, + setSellAsset, + setSellAssetAccountId, + switchAssets, ]) const affiliateFeeAfterDiscountUserCurrency = useMemo(() => { diff --git a/src/hooks/useActions.tsx b/src/hooks/useActions.tsx new file mode 100644 index 00000000000..e3832abc8a3 --- /dev/null +++ b/src/hooks/useActions.tsx @@ -0,0 +1,20 @@ +import type { ActionCreatorsMapObject } from '@reduxjs/toolkit' +import { bindActionCreators } from '@reduxjs/toolkit' +import { useMemo } from 'react' +import { useDispatch } from 'react-redux' + +// A hook that binds Redux action creators to dispatch, allowing them to be called directly without +// manually calling `dispatch` and memoizing with `useCallback`. Returns an object with the same +// shape as the input actions, but with each action creator wrapped to automatically dispatch its +// action. +export function useActions( + actions: T, +): { + [P in keyof T]: T[P] extends (...args: any[]) => any + ? (...args: Parameters) => ReturnType + : T[P] +} { + const dispatch = useDispatch() + + return useMemo(() => bindActionCreators(actions, dispatch), [actions, dispatch]) +} From 2e7b941f1f43e256e2843f989815d51223ff51f8 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:45:28 +1100 Subject: [PATCH 19/23] chore: unify naming of trade input actions and selectors --- .../LimitOrder/components/LimitOrderInput.tsx | 12 +++--- .../hooks/useLimitOrderRecipientAddress.tsx | 26 ++++++------ .../TradeInput/components/ConfirmSummary.tsx | 12 +++--- .../components/RecipientAddress.tsx | 14 +++---- .../MultiHopTrade/hooks/useAccountIds.tsx | 4 +- src/context/AppProvider/AppContext.tsx | 4 +- .../createTradeInputBaseSelectors.ts | 24 +++++------ .../createTradeInputBaseSlice.ts | 40 +++++++++---------- .../limitOrderInputSlice.ts | 10 ++--- .../slices/limitOrderInputSlice/selectors.ts | 6 +-- src/state/slices/tradeInputSlice/selectors.ts | 6 +-- .../slices/tradeInputSlice/tradeInputSlice.ts | 10 ++--- src/test/mocks/store.ts | 20 +++++----- 13 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 7b8330f523c..41712842526 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -92,8 +92,8 @@ export const LimitOrderInput = ({ switchAssets, setSellAsset, setBuyAsset, - setSellAssetAccountId, - setBuyAssetAccountId, + setSellAccountId, + setBuyAccountId, setLimitPriceBuyAsset, setSlippagePreferencePercentage, setIsInputtingFiatSellAmount, @@ -281,14 +281,14 @@ export const LimitOrderInput = ({ onChangeIsInputtingFiatSellAmount={setIsInputtingFiatSellAmount} onChangeSellAmountCryptoPrecision={setSellAmountCryptoPrecision} setSellAsset={setSellAsset} - setSellAccountId={setSellAssetAccountId} + setSellAccountId={setSellAccountId} > @@ -315,12 +315,12 @@ export const LimitOrderInput = ({ sellAmountCryptoPrecision, sellAsset, setBuyAsset, - setBuyAssetAccountId, + setBuyAccountId, setIsInputtingFiatSellAmount, setLimitPriceBuyAsset, setSellAmountCryptoPrecision, setSellAsset, - setSellAssetAccountId, + setSellAccountId, switchAssets, ]) diff --git a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx index 16738869373..ac16b045c3f 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx @@ -5,10 +5,10 @@ import { useIsManualReceiveAddressRequired } from 'components/MultiHopTrade/hook import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { + selectIsManualReceiveAddressEditing, + selectIsManualReceiveAddressValid, + selectIsManualReceiveAddressValidating, selectManualReceiveAddress, - selectManualReceiveAddressIsEditing, - selectManualReceiveAddressIsValid, - selectManualReceiveAddressIsValidating, } from 'state/slices/limitOrderInputSlice/selectors' import { useAppDispatch, useAppSelector } from 'state/store' @@ -28,9 +28,9 @@ export const useLimitOrderRecipientAddress = ({ const dispatch = useAppDispatch() const manualReceiveAddress = useAppSelector(selectManualReceiveAddress) - const isManualReceiveAddressValid = useAppSelector(selectManualReceiveAddressIsValid) - const isManualReceiveAddressEditing = useAppSelector(selectManualReceiveAddressIsEditing) - const isManualReceiveAddressValidating = useAppSelector(selectManualReceiveAddressIsValidating) + const isManualReceiveAddressValid = useAppSelector(selectIsManualReceiveAddressValid) + const isManualReceiveAddressEditing = useAppSelector(selectIsManualReceiveAddressEditing) + const isManualReceiveAddressValidating = useAppSelector(selectIsManualReceiveAddressValidating) const { walletReceiveAddress, isLoading: isWalletReceiveAddressLoading } = useReceiveAddress({ sellAccountId, @@ -43,40 +43,40 @@ export const useLimitOrderRecipientAddress = ({ }, [dispatch]) const handleEditManualReceiveAddress = useCallback(() => { - dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(true)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(true)) }, [dispatch]) const handleCancelManualReceiveAddress = useCallback(() => { - dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(false)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(false)) // Reset form value and valid state on cancel so the valid check doesn't wrongly evaluate to false after bailing out of editing an invalid address - dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(undefined)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(undefined)) }, [dispatch]) const handleResetManualReceiveAddress = useCallback(() => { // Reset the manual receive address in store dispatch(limitOrderInput.actions.setManualReceiveAddress(undefined)) // Reset the valid state in store - dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(undefined)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(undefined)) }, [dispatch]) const handleSubmitManualReceiveAddress = useCallback( (address: string) => { dispatch(limitOrderInput.actions.setManualReceiveAddress(address)) - dispatch(limitOrderInput.actions.setManualReceiveAddressIsEditing(false)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(false)) }, [dispatch], ) const handleIsManualReceiveAddressValidatingChange = useCallback( (isValidating: boolean) => { - dispatch(limitOrderInput.actions.setManualReceiveAddressIsValidating(isValidating)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressValidating(isValidating)) }, [dispatch], ) const handleIsManualReceiveAddressValidChange = useCallback( (isValid: boolean) => { - dispatch(limitOrderInput.actions.setManualReceiveAddressIsValid(isValid)) + dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(isValid)) }, [dispatch], ) diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx index 7a3820b0c35..a5c0a0a48d7 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx @@ -26,9 +26,9 @@ import { selectInputBuyAsset, selectInputSellAmountUsd, selectInputSellAsset, - selectManualReceiveAddressIsEditing, - selectManualReceiveAddressIsValid, - selectManualReceiveAddressIsValidating, + selectIsManualReceiveAddressEditing, + selectIsManualReceiveAddressValid, + selectIsManualReceiveAddressValidating, } from 'state/slices/selectors' import { selectActiveQuote, @@ -83,9 +83,9 @@ export const ConfirmSummary = ({ const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) const totalNetworkFeeFiatPrecision = useAppSelector(selectTotalNetworkFeeUserCurrencyPrecision) - const isManualReceiveAddressValidating = useAppSelector(selectManualReceiveAddressIsValidating) - const isManualReceiveAddressEditing = useAppSelector(selectManualReceiveAddressIsEditing) - const isManualReceiveAddressValid = useAppSelector(selectManualReceiveAddressIsValid) + const isManualReceiveAddressValidating = useAppSelector(selectIsManualReceiveAddressValidating) + const isManualReceiveAddressEditing = useAppSelector(selectIsManualReceiveAddressEditing) + const isManualReceiveAddressValid = useAppSelector(selectIsManualReceiveAddressValid) const slippagePercentageDecimal = useAppSelector(selectTradeSlippagePercentageDecimal) const totalProtocolFees = useAppSelector(selectTotalProtocolFeeByAsset) const activeQuoteErrors = useAppSelector(selectActiveQuoteErrors) diff --git a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx index d604e21a0eb..3f1b69b06ed 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx @@ -29,14 +29,14 @@ export const RecipientAddress = ({ const handleIsValidatingChange = useCallback( (isValidating: boolean) => { - dispatch(tradeInput.actions.setManualReceiveAddressIsValidating(isValidating)) + dispatch(tradeInput.actions.setIsManualReceiveAddressValidating(isValidating)) }, [dispatch], ) const handleIsValidChange = useCallback( (isValid: boolean) => { - dispatch(tradeInput.actions.setManualReceiveAddressIsValid(isValid)) + dispatch(tradeInput.actions.setIsManualReceiveAddressValid(isValid)) }, [dispatch], ) @@ -46,26 +46,26 @@ export const RecipientAddress = ({ }, [dispatch]) const handleEdit = useCallback(() => { - dispatch(tradeInput.actions.setManualReceiveAddressIsEditing(true)) + dispatch(tradeInput.actions.setIsManualReceiveAddressEditing(true)) }, [dispatch]) const handleCancel = useCallback(() => { - dispatch(tradeInput.actions.setManualReceiveAddressIsEditing(false)) + dispatch(tradeInput.actions.setIsManualReceiveAddressEditing(false)) // Reset form value and valid state on cancel so the valid check doesn't wrongly evaluate to false after bailing out of editing an invalid address - dispatch(tradeInput.actions.setManualReceiveAddressIsValid(undefined)) + dispatch(tradeInput.actions.setIsManualReceiveAddressValid(undefined)) }, [dispatch]) const handleReset = useCallback(() => { // Reset the manual receive address in store dispatch(tradeInput.actions.setManualReceiveAddress(undefined)) // Reset the valid state in store - dispatch(tradeInput.actions.setManualReceiveAddressIsValid(undefined)) + dispatch(tradeInput.actions.setIsManualReceiveAddressValid(undefined)) }, [dispatch]) const handleSubmit = useCallback( (address: string) => { dispatch(tradeInput.actions.setManualReceiveAddress(address)) - dispatch(tradeInput.actions.setManualReceiveAddressIsEditing(false)) + dispatch(tradeInput.actions.setIsManualReceiveAddressEditing(false)) }, [dispatch], ) diff --git a/src/components/MultiHopTrade/hooks/useAccountIds.tsx b/src/components/MultiHopTrade/hooks/useAccountIds.tsx index 0cf6d4a9b30..afd154b6fce 100644 --- a/src/components/MultiHopTrade/hooks/useAccountIds.tsx +++ b/src/components/MultiHopTrade/hooks/useAccountIds.tsx @@ -20,14 +20,14 @@ export const useAccountIds = (): { const setSellAssetAccountId = useCallback( (accountId: AccountId | undefined) => { - dispatch(tradeInput.actions.setSellAssetAccountId(accountId)) + dispatch(tradeInput.actions.setSellAccountId(accountId)) }, [dispatch], ) const setBuyAssetAccountId = useCallback( (accountId: AccountId | undefined) => { - dispatch(tradeInput.actions.setBuyAssetAccountId(accountId)) + dispatch(tradeInput.actions.setBuyAccountId(accountId)) }, [dispatch], ) diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index 9b40b940e34..2d12f813c32 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -147,8 +147,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { if (!prevWalletId) return if (walletId === prevWalletId) return - dispatch(tradeInput.actions.setSellAssetAccountId(undefined)) - dispatch(tradeInput.actions.setBuyAssetAccountId(undefined)) + dispatch(tradeInput.actions.setSellAccountId(undefined)) + dispatch(tradeInput.actions.setBuyAccountId(undefined)) }, [dispatch, prevWalletId, walletId]) const marketDataPollingInterval = 60 * 15 * 1000 // refetch data every 15 minutes diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts index 23bbd05a2a2..54393ecfcea 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSelectors.ts @@ -104,7 +104,7 @@ export const createTradeInputBaseSelectors = ( selectEnabledWalletAccountIds, (baseSlice, sellAsset, accountIdAssetValues, accountIds) => { // return the users selection if it exists - if (baseSlice.sellAssetAccountId) return baseSlice.sellAssetAccountId + if (baseSlice.sellAccountId) return baseSlice.sellAccountId const highestFiatBalanceSellAccountId = getHighestUserCurrencyBalanceAccountByAssetId( accountIdAssetValues, @@ -134,8 +134,8 @@ export const createTradeInputBaseSelectors = ( accountMetadata, ) => { // return the users selection if it exists - if (baseSlice.buyAssetAccountId) { - return baseSlice.buyAssetAccountId + if (baseSlice.buyAccountId) { + return baseSlice.buyAccountId } // maybe convert the account id to an account number @@ -175,19 +175,19 @@ export const createTradeInputBaseSelectors = ( baseSlice => baseSlice.manualReceiveAddress, ) - const selectManualReceiveAddressIsValidating = createSelector( + const selectIsManualReceiveAddressValidating = createSelector( selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValidating, + baseSlice => baseSlice.isManualReceiveAddressValidating, ) - const selectManualReceiveAddressIsEditing = createSelector( + const selectIsManualReceiveAddressEditing = createSelector( selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsEditing, + baseSlice => baseSlice.isManualReceiveAddressEditing, ) - const selectManualReceiveAddressIsValid = createSelector( + const selectIsManualReceiveAddressValid = createSelector( selectBaseSlice, - baseSlice => baseSlice.manualReceiveAddressIsValid, + baseSlice => baseSlice.isManualReceiveAddressValid, ) const selectInputSellAmountUsd = createSelector( @@ -241,9 +241,9 @@ export const createTradeInputBaseSelectors = ( selectBuyAccountId, selectInputSellAmountCryptoBaseUnit, selectManualReceiveAddress, - selectManualReceiveAddressIsValidating, - selectManualReceiveAddressIsEditing, - selectManualReceiveAddressIsValid, + selectIsManualReceiveAddressValidating, + selectIsManualReceiveAddressEditing, + selectIsManualReceiveAddressValid, selectInputSellAmountUsd, selectInputSellAmountUserCurrency, selectSellAssetBalanceCryptoBaseUnit, diff --git a/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts index f3794dcf8fb..ed2d5b376e9 100644 --- a/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts +++ b/src/state/slices/common/tradeInputBase/createTradeInputBaseSlice.ts @@ -7,14 +7,14 @@ import { bnOrZero } from 'lib/bignumber/bignumber' export interface TradeInputBaseState { buyAsset: Asset sellAsset: Asset - sellAssetAccountId: AccountId | undefined - buyAssetAccountId: AccountId | undefined + sellAccountId: AccountId | undefined + buyAccountId: AccountId | undefined sellAmountCryptoPrecision: string isInputtingFiatSellAmount: boolean manualReceiveAddress: string | undefined - manualReceiveAddressIsValidating: boolean - manualReceiveAddressIsEditing: boolean - manualReceiveAddressIsValid: boolean | undefined + isManualReceiveAddressValidating: boolean + isManualReceiveAddressEditing: boolean + isManualReceiveAddressValid: boolean | undefined slippagePreferencePercentage: string | undefined } @@ -57,7 +57,7 @@ export function createTradeInputBaseSlice< } if (asset.chainId !== state.buyAsset.chainId) { - state.buyAssetAccountId = undefined + state.buyAccountId = undefined } state.manualReceiveAddress = undefined @@ -74,17 +74,17 @@ export function createTradeInputBaseSlice< state.sellAmountCryptoPrecision = '0' if (asset.chainId !== state.sellAsset.chainId) { - state.sellAssetAccountId = undefined + state.sellAccountId = undefined } state.manualReceiveAddress = undefined state.sellAsset = action.payload }, - setSellAssetAccountId: (state, action: PayloadAction) => { - state.sellAssetAccountId = action.payload + setSellAccountId: (state, action: PayloadAction) => { + state.sellAccountId = action.payload }, - setBuyAssetAccountId: (state, action: PayloadAction) => { - state.buyAssetAccountId = action.payload + setBuyAccountId: (state, action: PayloadAction) => { + state.buyAccountId = action.payload }, setSellAmountCryptoPrecision: (state, action: PayloadAction) => { state.sellAmountCryptoPrecision = bnOrZero(action.payload).toString() @@ -95,23 +95,23 @@ export function createTradeInputBaseSlice< state.buyAsset = buyAsset state.sellAmountCryptoPrecision = '0' - const sellAssetAccountId = state.sellAssetAccountId - state.sellAssetAccountId = state.buyAssetAccountId - state.buyAssetAccountId = sellAssetAccountId + const sellAssetAccountId = state.sellAccountId + state.sellAccountId = state.buyAccountId + state.buyAccountId = sellAssetAccountId state.manualReceiveAddress = undefined }, setManualReceiveAddress: (state, action: PayloadAction) => { state.manualReceiveAddress = action.payload }, - setManualReceiveAddressIsValidating: (state, action: PayloadAction) => { - state.manualReceiveAddressIsValidating = action.payload + setIsManualReceiveAddressValidating: (state, action: PayloadAction) => { + state.isManualReceiveAddressValidating = action.payload }, - setManualReceiveAddressIsEditing: (state, action: PayloadAction) => { - state.manualReceiveAddressIsEditing = action.payload + setIsManualReceiveAddressEditing: (state, action: PayloadAction) => { + state.isManualReceiveAddressEditing = action.payload }, - setManualReceiveAddressIsValid: (state, action: PayloadAction) => { - state.manualReceiveAddressIsValid = action.payload + setIsManualReceiveAddressValid: (state, action: PayloadAction) => { + state.isManualReceiveAddressValid = action.payload }, setIsInputtingFiatSellAmount: (state, action: PayloadAction) => { state.isInputtingFiatSellAmount = action.payload diff --git a/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts index 09fad8c0975..f1fc215c0be 100644 --- a/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts +++ b/src/state/slices/limitOrderInputSlice/limitOrderInputSlice.ts @@ -11,14 +11,14 @@ export type LimitOrderInputState = { limitPriceBuyAsset: string } & TradeInputBa const initialState: LimitOrderInputState = { buyAsset: localAssetData[foxAssetId] ?? defaultAsset, sellAsset: localAssetData[usdcAssetId] ?? defaultAsset, - sellAssetAccountId: undefined, - buyAssetAccountId: undefined, + sellAccountId: undefined, + buyAccountId: undefined, sellAmountCryptoPrecision: '0', isInputtingFiatSellAmount: false, manualReceiveAddress: undefined, - manualReceiveAddressIsValidating: false, - manualReceiveAddressIsValid: undefined, - manualReceiveAddressIsEditing: false, + isManualReceiveAddressValidating: false, + isManualReceiveAddressValid: undefined, + isManualReceiveAddressEditing: false, slippagePreferencePercentage: undefined, limitPriceBuyAsset: '0', } diff --git a/src/state/slices/limitOrderInputSlice/selectors.ts b/src/state/slices/limitOrderInputSlice/selectors.ts index 8d57d58ba6f..7a6cfaac5a5 100644 --- a/src/state/slices/limitOrderInputSlice/selectors.ts +++ b/src/state/slices/limitOrderInputSlice/selectors.ts @@ -18,9 +18,9 @@ export const { selectBuyAccountId, selectInputSellAmountCryptoBaseUnit, selectManualReceiveAddress, - selectManualReceiveAddressIsValidating, - selectManualReceiveAddressIsEditing, - selectManualReceiveAddressIsValid, + selectIsManualReceiveAddressValidating, + selectIsManualReceiveAddressEditing, + selectIsManualReceiveAddressValid, selectInputSellAmountUsd, selectInputSellAmountUserCurrency, selectSellAssetBalanceCryptoBaseUnit, diff --git a/src/state/slices/tradeInputSlice/selectors.ts b/src/state/slices/tradeInputSlice/selectors.ts index 035a97b7f63..ee1799149d3 100644 --- a/src/state/slices/tradeInputSlice/selectors.ts +++ b/src/state/slices/tradeInputSlice/selectors.ts @@ -23,9 +23,9 @@ export const { selectUserSlippagePercentageDecimal, selectInputSellAmountCryptoBaseUnit, selectManualReceiveAddress, - selectManualReceiveAddressIsValidating, - selectManualReceiveAddressIsEditing, - selectManualReceiveAddressIsValid, + selectIsManualReceiveAddressValidating, + selectIsManualReceiveAddressEditing, + selectIsManualReceiveAddressValid, selectInputSellAmountUsd, selectInputSellAmountUserCurrency, selectSellAssetBalanceCryptoBaseUnit, diff --git a/src/state/slices/tradeInputSlice/tradeInputSlice.ts b/src/state/slices/tradeInputSlice/tradeInputSlice.ts index e5644f30cc3..7ffba80da50 100644 --- a/src/state/slices/tradeInputSlice/tradeInputSlice.ts +++ b/src/state/slices/tradeInputSlice/tradeInputSlice.ts @@ -10,14 +10,14 @@ type TradeInputState = TradeInputBaseState const initialState: TradeInputState = { buyAsset: localAssetData[foxAssetId] ?? defaultAsset, sellAsset: localAssetData[ethAssetId] ?? defaultAsset, - sellAssetAccountId: undefined, - buyAssetAccountId: undefined, + sellAccountId: undefined, + buyAccountId: undefined, sellAmountCryptoPrecision: '0', isInputtingFiatSellAmount: false, manualReceiveAddress: undefined, - manualReceiveAddressIsValidating: false, - manualReceiveAddressIsValid: undefined, - manualReceiveAddressIsEditing: false, + isManualReceiveAddressValidating: false, + isManualReceiveAddressValid: undefined, + isManualReceiveAddressEditing: false, slippagePreferencePercentage: undefined, } diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 998a4f2fa8f..03615e05285 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -227,27 +227,27 @@ export const mockStore: ReduxState = { tradeInput: { buyAsset: defaultAsset, sellAsset: defaultAsset, - sellAssetAccountId: undefined, - buyAssetAccountId: undefined, + sellAccountId: undefined, + buyAccountId: undefined, sellAmountCryptoPrecision: '0', isInputtingFiatSellAmount: false, manualReceiveAddress: undefined, - manualReceiveAddressIsValidating: false, - manualReceiveAddressIsEditing: false, - manualReceiveAddressIsValid: undefined, + isManualReceiveAddressValidating: false, + isManualReceiveAddressEditing: false, + isManualReceiveAddressValid: undefined, slippagePreferencePercentage: undefined, }, limitOrderInput: { buyAsset: defaultAsset, sellAsset: defaultAsset, - sellAssetAccountId: undefined, - buyAssetAccountId: undefined, + sellAccountId: undefined, + buyAccountId: undefined, sellAmountCryptoPrecision: '0', isInputtingFiatSellAmount: false, manualReceiveAddress: undefined, - manualReceiveAddressIsValidating: false, - manualReceiveAddressIsEditing: false, - manualReceiveAddressIsValid: undefined, + isManualReceiveAddressValidating: false, + isManualReceiveAddressEditing: false, + isManualReceiveAddressValid: undefined, slippagePreferencePercentage: undefined, limitPriceBuyAsset: '0', }, From ae28f8e913826b50c3708fe52bf76efa49172e48 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:14:21 +1100 Subject: [PATCH 20/23] fix: free trades are free --- .../SharedTradeInputFooter/components/ReceiveSummary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/components/ReceiveSummary.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/components/ReceiveSummary.tsx index 194c1d46e0f..d049b3b795a 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/components/ReceiveSummary.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputFooter/components/ReceiveSummary.tsx @@ -94,8 +94,7 @@ export const ReceiveSummary: FC = memo( _hover={!isThorFreeTrade ? shapeShiftFeeModalRowHover : undefined} > - {!!affiliateFeeAfterDiscountUserCurrency && - affiliateFeeAfterDiscountUserCurrency !== '0' ? ( + {!bnOrZero(affiliateFeeAfterDiscountUserCurrency).isZero() ? ( <> From 09c981df2216e6493bbc8ab4a7119140fff52566 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:27:42 +1100 Subject: [PATCH 21/23] fix: reset market rate when switching assets --- .../LimitOrder/components/LimitOrderInput.tsx | 12 +++++++----- .../SharedTradeInput/SharedTradeInputBody.tsx | 6 +++--- .../components/TradeInput/TradeInput.tsx | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 41712842526..f7568180a3b 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -222,11 +222,13 @@ export const LimitOrderInput = ({ } = useQuoteLimitOrderQuery(limitOrderQuoteParams) const marketPriceBuyAsset = useMemo(() => { - if (!data) return '0' + // RTK query returns stale data when `skipToken` is used, so we need to handle that case here. + if (!data || limitOrderQuoteParams === skipToken) return '0' + return bnOrZero(fromBaseUnit(data.quote.buyAmount, buyAsset.precision)) .div(fromBaseUnit(data.quote.sellAmount, sellAsset.precision)) .toFixed() - }, [buyAsset.precision, data, sellAsset.precision]) + }, [buyAsset.precision, data, sellAsset.precision, limitOrderQuoteParams]) // Reset the limit price when the market price changes. // TODO: If we introduce polling of quotes, we will need to add logic inside `LimitOrderConfig` to @@ -277,7 +279,7 @@ export const LimitOrderInput = ({ sellAmountUserCurrency={inputSellAmountUserCurrency} sellAsset={sellAsset} sellAccountId={sellAccountId} - handleSwitchAssets={switchAssets} + onSwitchAssets={switchAssets} onChangeIsInputtingFiatSellAmount={setIsInputtingFiatSellAmount} onChangeSellAmountCryptoPrecision={setSellAmountCryptoPrecision} setSellAsset={setSellAsset} @@ -314,13 +316,13 @@ export const LimitOrderInput = ({ sellAccountId, sellAmountCryptoPrecision, sellAsset, - setBuyAsset, setBuyAccountId, + setBuyAsset, setIsInputtingFiatSellAmount, setLimitPriceBuyAsset, + setSellAccountId, setSellAmountCryptoPrecision, setSellAsset, - setSellAccountId, switchAssets, ]) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx index 4010750617a..1218feda615 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx @@ -37,7 +37,7 @@ type SharedTradeInputBodyProps = { sellAmountUserCurrency: string | undefined sellAsset: Asset sellAccountId: AccountId | undefined - handleSwitchAssets: () => void + onSwitchAssets: () => void onChangeIsInputtingFiatSellAmount: (isInputtingFiatSellAmount: boolean) => void onChangeSellAmountCryptoPrecision: (sellAmountCryptoPrecision: string) => void setSellAsset: (asset: Asset) => void @@ -53,7 +53,7 @@ export const SharedTradeInputBody = ({ sellAmountUserCurrency, sellAsset, sellAccountId, - handleSwitchAssets, + onSwitchAssets, onChangeIsInputtingFiatSellAmount, onChangeSellAmountCryptoPrecision, setSellAsset, @@ -163,7 +163,7 @@ export const SharedTradeInputBody = ({ justifyContent='center' > Date: Thu, 14 Nov 2024 10:44:05 +1100 Subject: [PATCH 22/23] fix: import from wrong slice, prevent it from happening again --- .../LimitOrder/components/LimitOrderInput.tsx | 2 +- .../components/SharedApprovalDescription.tsx | 2 +- .../MultiHopTradeConfirm/components/FeeStep.tsx | 2 +- .../hooks/useIsApprovalInitiallyNeeded.tsx | 5 ++++- .../MultiHopTrade/components/TradeInput/TradeInput.tsx | 4 ++-- .../TradeInput/components/ConfirmSummary.tsx | 4 ++-- .../TradeInput/components/RecipientAddress.tsx | 2 +- .../TradeInput/components/TradeQuotes/TradeQuote.tsx | 10 ++++++---- .../TradeInput/components/TradeQuotes/TradeQuotes.tsx | 2 +- .../TradeInput/hooks/useTradeReceiveAddress.tsx | 2 +- .../components/VerifyAddresses/VerifyAddresses.tsx | 6 ++++-- src/components/MultiHopTrade/hooks/useAccountIds.tsx | 5 ++++- .../hooks/useGetTradeQuotes/useGetTradeQuotes.tsx | 8 +++++--- src/lib/tradeExecution.ts | 2 +- src/state/apis/swapper/helpers/validateTradeQuote.ts | 6 ++++-- src/state/slices/selectors.ts | 1 - 16 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index f7568180a3b..21ab84442e3 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -20,6 +20,7 @@ import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInp import { selectBuyAccountId, selectHasUserEnteredAmount, + selectInputBuyAsset, selectInputSellAmountCryptoPrecision, selectInputSellAmountUsd, selectInputSellAmountUserCurrency, @@ -31,7 +32,6 @@ import { selectUserSlippagePercentageDecimal, } from 'state/slices/limitOrderInputSlice/selectors' import { - selectInputBuyAsset, selectIsAnyAccountMetadataLoadedForChainId, selectUserCurrencyToUsdRate, } from 'state/slices/selectors' diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/components/SharedApprovalDescription.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/components/SharedApprovalDescription.tsx index a3d556090be..4ff97c180a6 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/components/SharedApprovalDescription.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/ApprovalStep/components/SharedApprovalDescription.tsx @@ -5,7 +5,7 @@ import { MiddleEllipsis } from 'components/MiddleEllipsis/MiddleEllipsis' import { Text } from 'components/Text' import { useSafeTxQuery } from 'hooks/queries/useSafeTx' import { getTxLink } from 'lib/getTxLink' -import { selectFirstHopSellAccountId } from 'state/slices/selectors' +import { selectFirstHopSellAccountId } from 'state/slices/tradeInputSlice/selectors' import { useAppSelector } from 'state/store' export type TxLineProps = { diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx index bd87abd0570..731c521faad 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/components/FeeStep.tsx @@ -9,7 +9,7 @@ import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' import { bnOrZero } from 'lib/bignumber/bignumber' import { THORSWAP_MAXIMUM_YEAR_TRESHOLD, THORSWAP_UNIT_THRESHOLD } from 'lib/fees/model' import { selectCalculatedFees, selectThorVotingPower } from 'state/apis/snapshot/selectors' -import { selectInputSellAmountUsd } from 'state/slices/selectors' +import { selectInputSellAmountUsd } from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuoteAffiliateBps } from 'state/slices/tradeQuoteSlice/selectors' import { useAppSelector } from 'state/store' diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx index 60c7daf5ea5..73060c92677 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/hooks/useIsApprovalInitiallyNeeded.tsx @@ -4,7 +4,10 @@ import { type TradeQuoteStep } from '@shapeshiftoss/swapper' import { useEffect, useMemo, useState } from 'react' import { useIsAllowanceApprovalRequired } from 'hooks/queries/useIsAllowanceApprovalRequired' import { useIsAllowanceResetRequired } from 'hooks/queries/useIsAllowanceResetRequired' -import { selectFirstHopSellAccountId, selectSecondHopSellAccountId } from 'state/slices/selectors' +import { + selectFirstHopSellAccountId, + selectSecondHopSellAccountId, +} from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuote, selectFirstHop, diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index aaf1d7d019c..26a4bb7a48a 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -28,15 +28,15 @@ import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' import { MixPanelEvent } from 'lib/mixpanel/types' import { isKeplrHDWallet } from 'lib/utils' import { selectIsVotingPowerLoading } from 'state/apis/snapshot/selectors' +import { selectIsAnyAccountMetadataLoadedForChainId } from 'state/slices/selectors' import { selectHasUserEnteredAmount, selectInputBuyAsset, selectInputSellAmountCryptoPrecision, selectInputSellAmountUserCurrency, selectInputSellAsset, - selectIsAnyAccountMetadataLoadedForChainId, selectIsInputtingFiatSellAmount, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { selectActiveQuote, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx index a5c0a0a48d7..9622b311184 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/ConfirmSummary.tsx @@ -20,8 +20,8 @@ import { useIsSmartContractAddress } from 'hooks/useIsSmartContractAddress/useIs import { useWallet } from 'hooks/useWallet/useWallet' import { isToken } from 'lib/utils' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' +import { selectFeeAssetById } from 'state/slices/selectors' import { - selectFeeAssetById, selectHasUserEnteredAmount, selectInputBuyAsset, selectInputSellAmountUsd, @@ -29,7 +29,7 @@ import { selectIsManualReceiveAddressEditing, selectIsManualReceiveAddressValid, selectIsManualReceiveAddressValidating, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuote, selectActiveQuoteAffiliateBps, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx index 3f1b69b06ed..9031c59ab7c 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/RecipientAddress.tsx @@ -1,5 +1,5 @@ import { useCallback } from 'react' -import { selectInputBuyAsset } from 'state/slices/selectors' +import { selectInputBuyAsset } from 'state/slices/tradeInputSlice/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { useAppDispatch, useAppSelector } from 'state/store' diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx index eb5d2407087..3928b941992 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuote.tsx @@ -19,14 +19,16 @@ import { type ApiQuote, TradeQuoteValidationError } from 'state/apis/swapper/typ import { selectFeeAssetByChainId, selectFeeAssetById, - selectInputBuyAsset, - selectInputSellAmountCryptoPrecision, - selectInputSellAsset, selectIsAssetWithoutMarketData, selectMarketDataByAssetIdUserCurrency, selectMarketDataByFilter, - selectUserSlippagePercentageDecimal, } from 'state/slices/selectors' +import { + selectInputBuyAsset, + selectInputSellAmountCryptoPrecision, + selectInputSellAsset, + selectUserSlippagePercentageDecimal, +} from 'state/slices/tradeInputSlice/selectors' import { getBuyAmountAfterFeesCryptoPrecision, getTotalNetworkFeeUserCurrencyPrecision, diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuotes.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuotes.tsx index fc399afd549..fab5d4c1a7c 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuotes.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeQuotes/TradeQuotes.tsx @@ -21,7 +21,7 @@ import { Text } from 'components/Text' import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' import type { ApiQuote } from 'state/apis/swapper/types' import { TradeQuoteValidationError } from 'state/apis/swapper/types' -import { selectInputBuyAsset, selectInputSellAsset } from 'state/slices/selectors' +import { selectInputBuyAsset, selectInputSellAsset } from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuoteMetaOrDefault, selectBuyAmountAfterFeesCryptoPrecision, diff --git a/src/components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress.tsx b/src/components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress.tsx index b39edb4d4e4..1af06f57a0f 100644 --- a/src/components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/hooks/useTradeReceiveAddress.tsx @@ -4,7 +4,7 @@ import { selectInputBuyAsset, selectLastHopBuyAccountId, selectManualReceiveAddress, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { useAppSelector } from 'state/store' export const useTradeReceiveAddress = () => { diff --git a/src/components/MultiHopTrade/components/VerifyAddresses/VerifyAddresses.tsx b/src/components/MultiHopTrade/components/VerifyAddresses/VerifyAddresses.tsx index ec8d6ae81d3..c52de7115ad 100644 --- a/src/components/MultiHopTrade/components/VerifyAddresses/VerifyAddresses.tsx +++ b/src/components/MultiHopTrade/components/VerifyAddresses/VerifyAddresses.tsx @@ -30,11 +30,13 @@ import { useWallet } from 'hooks/useWallet/useWallet' import { walletSupportsChain } from 'hooks/useWalletSupportsChain/useWalletSupportsChain' import { selectAccountIdsByChainIdFilter, + selectPortfolioAccountMetadataByAccountId, +} from 'state/slices/selectors' +import { selectInputBuyAsset, selectInputSellAsset, selectManualReceiveAddress, - selectPortfolioAccountMetadataByAccountId, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { useAppSelector } from 'state/store' import { WithBackButton } from '../WithBackButton' diff --git a/src/components/MultiHopTrade/hooks/useAccountIds.tsx b/src/components/MultiHopTrade/hooks/useAccountIds.tsx index afd154b6fce..57d025698df 100644 --- a/src/components/MultiHopTrade/hooks/useAccountIds.tsx +++ b/src/components/MultiHopTrade/hooks/useAccountIds.tsx @@ -1,6 +1,9 @@ import type { AccountId } from '@shapeshiftoss/caip' import { useCallback, useMemo } from 'react' -import { selectFirstHopSellAccountId, selectLastHopBuyAccountId } from 'state/slices/selectors' +import { + selectFirstHopSellAccountId, + selectLastHopBuyAccountId, +} from 'state/slices/tradeInputSlice/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { useAppDispatch, useAppSelector } from 'state/store' diff --git a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx index f5ee1000227..ce100ba5655 100644 --- a/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx +++ b/src/components/MultiHopTrade/hooks/useGetTradeQuotes/useGetTradeQuotes.tsx @@ -28,6 +28,10 @@ import { } from 'state/apis/snapshot/selectors' import { swapperApi } from 'state/apis/swapper/swapperApi' import type { ApiQuote, TradeQuoteError } from 'state/apis/swapper/types' +import { + selectPortfolioAccountMetadataByAccountId, + selectUsdRateByAssetId, +} from 'state/slices/selectors' import { selectFirstHopSellAccountId, selectInputBuyAsset, @@ -35,10 +39,8 @@ import { selectInputSellAmountUsd, selectInputSellAsset, selectLastHopBuyAccountId, - selectPortfolioAccountMetadataByAccountId, - selectUsdRateByAssetId, selectUserSlippagePercentageDecimal, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { selectActiveQuoteMetaOrDefault, selectIsAnyTradeQuoteLoading, diff --git a/src/lib/tradeExecution.ts b/src/lib/tradeExecution.ts index 71b591e87da..5456361305a 100644 --- a/src/lib/tradeExecution.ts +++ b/src/lib/tradeExecution.ts @@ -24,7 +24,7 @@ import { getConfig } from 'config' import EventEmitter from 'events' import { fetchIsSmartContractAddressQuery } from 'hooks/useIsSmartContractAddress/useIsSmartContractAddress' import { poll } from 'lib/poll/poll' -import { selectFirstHopSellAccountId } from 'state/slices/selectors' +import { selectFirstHopSellAccountId } from 'state/slices/tradeInputSlice/selectors' import { store } from 'state/store' import { assertGetCosmosSdkChainAdapter } from './utils/cosmosSdk' diff --git a/src/state/apis/swapper/helpers/validateTradeQuote.ts b/src/state/apis/swapper/helpers/validateTradeQuote.ts index bffe379239d..7a58f2a6c4f 100644 --- a/src/state/apis/swapper/helpers/validateTradeQuote.ts +++ b/src/state/apis/swapper/helpers/validateTradeQuote.ts @@ -21,11 +21,13 @@ import { import { selectAssets, selectFeeAssetById, + selectPortfolioAccountIdByNumberByChainId, +} from 'state/slices/selectors' +import { selectFirstHopSellAccountId, selectInputSellAmountCryptoPrecision, - selectPortfolioAccountIdByNumberByChainId, selectSecondHopSellAccountId, -} from 'state/slices/selectors' +} from 'state/slices/tradeInputSlice/selectors' import { getTotalProtocolFeeByAssetForStep } from 'state/slices/tradeQuoteSlice/helpers' import type { ErrorWithMeta } from '../types' diff --git a/src/state/slices/selectors.ts b/src/state/slices/selectors.ts index aadebf24da4..be35dcd9e9e 100644 --- a/src/state/slices/selectors.ts +++ b/src/state/slices/selectors.ts @@ -13,7 +13,6 @@ export * from './portfolioSlice/selectors' export * from './preferencesSlice/selectors' export * from './txHistorySlice/selectors' export * from './opportunitiesSlice/selectors' -export * from './tradeInputSlice/selectors' /** * some selectors span multiple business logic domains, e.g. portfolio and opportunities From ebc7b6208130c9ce61fa1516dc6ac5d8420422c5 Mon Sep 17 00:00:00 2001 From: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Date: Sat, 16 Nov 2024 08:48:20 +1100 Subject: [PATCH 23/23] chore: useActions for the useLimitOrderRecipientAddress hook --- .../hooks/useLimitOrderRecipientAddress.tsx | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx index ac16b045c3f..5d2d6d23368 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrderRecipientAddress.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react' import type { Address } from 'viem' import { useIsManualReceiveAddressRequired } from 'components/MultiHopTrade/hooks/useIsManualReceiveAddressRequired' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' +import { useActions } from 'hooks/useActions' import { limitOrderInput } from 'state/slices/limitOrderInputSlice/limitOrderInputSlice' import { selectIsManualReceiveAddressEditing, @@ -10,7 +11,7 @@ import { selectIsManualReceiveAddressValidating, selectManualReceiveAddress, } from 'state/slices/limitOrderInputSlice/selectors' -import { useAppDispatch, useAppSelector } from 'state/store' +import { useAppSelector } from 'state/store' import { SharedRecipientAddress } from '../../SharedTradeInput/SharedRecipientAddress' @@ -25,8 +26,6 @@ export const useLimitOrderRecipientAddress = ({ buyAccountId, sellAccountId, }: UseLimitOrderRecipientAddressProps) => { - const dispatch = useAppDispatch() - const manualReceiveAddress = useAppSelector(selectManualReceiveAddress) const isManualReceiveAddressValid = useAppSelector(selectIsManualReceiveAddressValid) const isManualReceiveAddressEditing = useAppSelector(selectIsManualReceiveAddressEditing) @@ -38,47 +37,40 @@ export const useLimitOrderRecipientAddress = ({ buyAsset, }) + const { + setManualReceiveAddress, + setIsManualReceiveAddressEditing, + setIsManualReceiveAddressValid, + setIsManualReceiveAddressValidating, + } = useActions(limitOrderInput.actions) + const handleManualReceiveAddressError = useCallback(() => { - dispatch(limitOrderInput.actions.setManualReceiveAddress(undefined)) - }, [dispatch]) + setManualReceiveAddress(undefined) + }, [setManualReceiveAddress]) const handleEditManualReceiveAddress = useCallback(() => { - dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(true)) - }, [dispatch]) + setIsManualReceiveAddressEditing(true) + }, [setIsManualReceiveAddressEditing]) const handleCancelManualReceiveAddress = useCallback(() => { - dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(false)) + setIsManualReceiveAddressEditing(false) // Reset form value and valid state on cancel so the valid check doesn't wrongly evaluate to false after bailing out of editing an invalid address - dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(undefined)) - }, [dispatch]) + setIsManualReceiveAddressValid(undefined) + }, [setIsManualReceiveAddressEditing, setIsManualReceiveAddressValid]) const handleResetManualReceiveAddress = useCallback(() => { // Reset the manual receive address in store - dispatch(limitOrderInput.actions.setManualReceiveAddress(undefined)) + setManualReceiveAddress(undefined) // Reset the valid state in store - dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(undefined)) - }, [dispatch]) + setIsManualReceiveAddressValid(undefined) + }, [setIsManualReceiveAddressValid, setManualReceiveAddress]) const handleSubmitManualReceiveAddress = useCallback( (address: string) => { - dispatch(limitOrderInput.actions.setManualReceiveAddress(address)) - dispatch(limitOrderInput.actions.setIsManualReceiveAddressEditing(false)) - }, - [dispatch], - ) - - const handleIsManualReceiveAddressValidatingChange = useCallback( - (isValidating: boolean) => { - dispatch(limitOrderInput.actions.setIsManualReceiveAddressValidating(isValidating)) - }, - [dispatch], - ) - - const handleIsManualReceiveAddressValidChange = useCallback( - (isValid: boolean) => { - dispatch(limitOrderInput.actions.setIsManualReceiveAddressValid(isValid)) + setManualReceiveAddress(address) + setIsManualReceiveAddressEditing(false) }, - [dispatch], + [setIsManualReceiveAddressEditing, setManualReceiveAddress], ) const isManualReceiveAddressRequired = useIsManualReceiveAddressRequired({ @@ -114,8 +106,8 @@ export const useLimitOrderRecipientAddress = ({ onCancel={handleCancelManualReceiveAddress} onEdit={handleEditManualReceiveAddress} onError={handleManualReceiveAddressError} - onIsValidatingChange={handleIsManualReceiveAddressValidatingChange} - onIsValidChange={handleIsManualReceiveAddressValidChange} + onIsValidatingChange={setIsManualReceiveAddressValidating} + onIsValidChange={setIsManualReceiveAddressValid} onReset={handleResetManualReceiveAddress} onSubmit={handleSubmitManualReceiveAddress} /> @@ -128,8 +120,8 @@ export const useLimitOrderRecipientAddress = ({ handleCancelManualReceiveAddress, handleEditManualReceiveAddress, handleManualReceiveAddressError, - handleIsManualReceiveAddressValidatingChange, - handleIsManualReceiveAddressValidChange, + setIsManualReceiveAddressValidating, + setIsManualReceiveAddressValid, handleResetManualReceiveAddress, handleSubmitManualReceiveAddress, ])