Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swaps deeplink #6178

Merged
merged 15 commits into from
Dec 4, 2024
358 changes: 179 additions & 179 deletions ios/Rainbow.xcodeproj/project.pbxproj
walmat marked this conversation as resolved.
Show resolved Hide resolved

Large diffs are not rendered by default.

65 changes: 50 additions & 15 deletions src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,37 @@ import { deepEqualWorklet } from '@/worklets/comparisons';

const REMOTE_CONFIG = getRemoteConfig();

function getInitialInputValues({
inputAsset,
inputAmount,
outputAsset,
outputAmount,
percentageToSwap,
sliderXPosition,
}: {
inputAsset: ExtendedAnimatedAssetWithColors | null;
inputAmount: string | undefined;
outputAsset: ExtendedAnimatedAssetWithColors | null;
outputAmount: string | undefined;
percentageToSwap: number;
sliderXPosition: number;
}) {
if (inputAsset && inputAmount) {
const inputNativeValue = mulWorklet(inputAmount, inputAsset?.price?.value ?? 0);
return { inputAmount, inputNativeValue, outputAmount: 0, outputNativeValue: 0 };
}
if (outputAsset && outputAmount) {
const outputNativeValue = mulWorklet(outputAmount, outputAsset?.price?.value ?? 0);
return { inputAmount: 0, inputNativeValue: 0, outputAmount, outputNativeValue };
}
walmat marked this conversation as resolved.
Show resolved Hide resolved

const slider = getInputValuesForSliderPositionWorklet({ selectedInputAsset: inputAsset, percentageToSwap, sliderXPosition });
return { inputAmount: slider.inputAmount, inputNativeValue: slider.inputNativeValue, outputAmount: 0, outputNativeValue: 0 };
}

export function useSwapInputsController({
focusedInput,
inputProgress,
initialSelectedInputAsset,
internalSelectedInputAsset,
internalSelectedOutputAsset,
isFetching,
Expand All @@ -63,10 +90,10 @@ export function useSwapInputsController({
quote,
sliderXPosition,
slippage,
initialValues,
}: {
focusedInput: SharedValue<inputKeys>;
inputProgress: SharedValue<number>;
initialSelectedInputAsset: ExtendedAnimatedAssetWithColors | null;
internalSelectedInputAsset: SharedValue<ExtendedAnimatedAssetWithColors | null>;
internalSelectedOutputAsset: SharedValue<ExtendedAnimatedAssetWithColors | null>;
isFetching: SharedValue<boolean>;
Expand All @@ -76,27 +103,30 @@ export function useSwapInputsController({
quote: SharedValue<Quote | CrosschainQuote | QuoteError | null>;
sliderXPosition: SharedValue<number>;
slippage: SharedValue<string>;
initialValues: {
inputAmount?: string;
outputAmount?: string;
inputMethod?: inputMethods;
};
}) {
const percentageToSwap = useDerivedValue(() => {
return Math.round(clamp((sliderXPosition.value - SCRUBBER_WIDTH / SLIDER_WIDTH) / SLIDER_WIDTH, 0, 1) * 100) / 100;
});

const { inputAmount: initialInputAmount, inputNativeValue: initialInputNativeValue } = getInputValuesForSliderPositionWorklet({
selectedInputAsset: initialSelectedInputAsset,
percentageToSwap: percentageToSwap.value,
sliderXPosition: sliderXPosition.value,
});

const { nativeCurrency: currentCurrency } = useAccountSettings();
const setSlippage = swapsStore(state => state.setSlippage);

const inputValues = useSharedValue<inputValuesType>({
inputAmount: initialInputAmount,
inputNativeValue: initialInputNativeValue,
outputAmount: 0,
outputNativeValue: 0,
});
const inputMethod = useSharedValue<inputMethods>('slider');
const inputValues = useSharedValue<inputValuesType>(
getInitialInputValues({
inputAsset: internalSelectedInputAsset.value,
inputAmount: initialValues.inputAmount,
outputAsset: internalSelectedOutputAsset.value,
outputAmount: initialValues.outputAmount,
percentageToSwap: percentageToSwap.value,
sliderXPosition: sliderXPosition.value,
})
);
const inputMethod = useSharedValue<inputMethods>(initialValues.inputMethod || 'slider');

const inputNativePrice = useDerivedValue(() => {
return internalSelectedInputAsset.value?.nativePrice || internalSelectedInputAsset.value?.price?.value || 0;
Expand Down Expand Up @@ -563,6 +593,10 @@ export function useSwapInputsController({
autoStart: false,
});

if (internalSelectedInputAsset.value && internalSelectedOutputAsset.value) {
fetchQuoteAndAssetPrices();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think this is necessary since it should be handled via a useAnimatedReaction.

const onChangedPercentage = useDebouncedCallback(
(percentage: number) => {
lastTypedInput.value = 'inputAmount';
Expand Down Expand Up @@ -657,6 +691,7 @@ export function useSwapInputsController({
assetToSellNetwork: internalSelectedInputAsset.value?.chainId,
}),
(current, previous) => {
if (!previous) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this added?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still curious about this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

const didInputAssetChange = current.assetToSellId !== previous?.assetToSellId;
const didOutputAssetChange = current.assetToBuyId !== previous?.assetToBuyId;

Expand Down
55 changes: 55 additions & 0 deletions src/__swaps__/screens/Swap/navigateToSwaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { GasSpeed } from '@/__swaps__/types/gas';
import { Navigation } from '@/navigation';
import store from '@/redux/store';
import { SwapsState, useSwapsStore } from '@/state/swaps/swapsStore';
import { setSelectedGasSpeed } from './hooks/useSelectedGas';
import { enableActionsOnReadOnlyWallet } from '@/config';
import walletTypes from '@/helpers/walletTypes';
import { watchingAlert } from '@/utils';
import Routes from '@/navigation/routesNames';

export type SwapsParams = Partial<
Pick<SwapsState, 'inputAsset' | 'outputAsset' | 'percentageToSell' | 'flashbots' | 'slippage'> & {
inputAmount: string;
outputAmount: string;
gasSpeed: GasSpeed;
}
>;

const isCurrentWalletReadOnly = () => store.getState().wallets.selected?.type === walletTypes.readOnly;

export async function navigateToSwaps({ gasSpeed, ...params }: SwapsParams) {
if (!enableActionsOnReadOnlyWallet && isCurrentWalletReadOnly()) {
return watchingAlert();
}

const chainId = params.inputAsset?.chainId || params.outputAsset?.chainId || store.getState().settings.chainId;
useSwapsStore.setState(params);

if (gasSpeed && chainId) setSelectedGasSpeed(chainId, gasSpeed);

Navigation.handleAction(Routes.SWAP, params);
}

const getInputMethod = (params: SwapsParams) => {
if (params.percentageToSell) return 'slider';
if (params.inputAsset) return 'inputAmount';
if (params.outputAsset) return 'outputAmount';
return 'inputAmount';
};
export function getSwapsNavigationParams() {
const params = (Navigation.getActiveRoute().params || {}) as SwapsParams;

const inputMethod = getInputMethod(params);
const lastTypedInput = inputMethod === 'slider' ? 'inputAmount' : inputMethod;

const state = useSwapsStore.getState();
return {
inputMethod,
lastTypedInput,
focusedInput: lastTypedInput,
inputAsset: params.inputAsset || state.inputAsset,
outputAsset: params.outputAsset || state.outputAsset,
...params,
} as const;
}
Comment on lines +40 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consumers of this function will think getSwapsNavigationParams().inputAsset and getSwapsNavigationParams().outputAsset are guaranteed.

can you tweak this fn to reflect that they're optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are typed as ParsedSearchAsset | null

41 changes: 31 additions & 10 deletions src/__swaps__/screens/Swap/providers/swap-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useSharedValue,
} from 'react-native-reanimated';

import { equalWorklet, lessThanOrEqualToWorklet, sumWorklet } from '@/__swaps__/safe-math/SafeMath';
import { divWorklet, equalWorklet, lessThanOrEqualToWorklet, mulWorklet, sumWorklet } from '@/__swaps__/safe-math/SafeMath';
import { INITIAL_SLIDER_POSITION, SLIDER_COLLAPSED_HEIGHT, SLIDER_HEIGHT, SLIDER_WIDTH } from '@/__swaps__/screens/Swap/constants';
import { useAnimatedSwapStyles } from '@/__swaps__/screens/Swap/hooks/useAnimatedSwapStyles';
import { useSwapInputsController } from '@/__swaps__/screens/Swap/hooks/useSwapInputsController';
Expand All @@ -26,7 +26,7 @@ import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/scree
import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets';
import { ChainId } from '@/chains/types';
import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap';
import { getDefaultSlippageWorklet, isUnwrapEthWorklet, isWrapEthWorklet, parseAssetAndExtend } from '@/__swaps__/utils/swaps';
import { clamp, getDefaultSlippageWorklet, isUnwrapEthWorklet, isWrapEthWorklet, parseAssetAndExtend } from '@/__swaps__/utils/swaps';
import { analyticsV2 } from '@/analytics';
import { LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities';
import { getFlashbotsProvider, getProvider } from '@/handlers/web3';
Expand Down Expand Up @@ -57,6 +57,7 @@ import { performanceTracking, Screens, TimeToSignOperation } from '@/state/perfo
import { getRemoteConfig } from '@/model/remoteConfig';
import { useConnectedToHardhatStore } from '@/state/connectedToHardhat';
import { chainsNativeAsset, supportedFlashbotsChainIds } from '@/chains';
import { getSwapsNavigationParams } from '../navigateToSwaps';

const swapping = i18n.t(i18n.l.swap.actions.swapping);
const holdToSwap = i18n.t(i18n.l.swap.actions.hold_to_swap);
Expand Down Expand Up @@ -132,28 +133,48 @@ interface SwapProviderProps {
children: ReactNode;
}

const getInitialSliderXPosition = ({
inputAmount,
maxSwappableAmount,
}: {
inputAmount: string | undefined;
maxSwappableAmount: string | undefined;
}) => {
if (inputAmount && maxSwappableAmount) {
return clamp(+mulWorklet(divWorklet(inputAmount, maxSwappableAmount), SLIDER_WIDTH), 0, SLIDER_WIDTH);
walmat marked this conversation as resolved.
Show resolved Hide resolved
}
return SLIDER_WIDTH * swapsStore.getState().percentageToSell;
};

export const SwapProvider = ({ children }: SwapProviderProps) => {
const { nativeCurrency } = useAccountSettings();

const initialValues = getSwapsNavigationParams();

const isFetching = useSharedValue(false);
const isQuoteStale = useSharedValue(0); // TODO: Convert this to a boolean
const isSwapping = useSharedValue(false);

const inputSearchRef = useAnimatedRef<TextInput>();
const outputSearchRef = useAnimatedRef<TextInput>();

const sliderXPosition = useSharedValue(SLIDER_WIDTH * INITIAL_SLIDER_POSITION);
const sliderPressProgress = useSharedValue(SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT);
const lastTypedInput = useSharedValue<inputKeys>(initialValues.lastTypedInput);
const focusedInput = useSharedValue<inputKeys>(initialValues.focusedInput);

const lastTypedInput = useSharedValue<inputKeys>('inputAmount');
const focusedInput = useSharedValue<inputKeys>('inputAmount');

const initialSelectedInputAsset = parseAssetAndExtend({ asset: swapsStore.getState().inputAsset });
const initialSelectedOutputAsset = parseAssetAndExtend({ asset: swapsStore.getState().outputAsset });
const initialSelectedInputAsset = parseAssetAndExtend({ asset: initialValues.inputAsset });
const initialSelectedOutputAsset = parseAssetAndExtend({ asset: initialValues.outputAsset });

const internalSelectedInputAsset = useSharedValue<ExtendedAnimatedAssetWithColors | null>(initialSelectedInputAsset);
const internalSelectedOutputAsset = useSharedValue<ExtendedAnimatedAssetWithColors | null>(initialSelectedOutputAsset);

const sliderXPosition = useSharedValue(
getInitialSliderXPosition({
inputAmount: initialValues.inputAmount,
maxSwappableAmount: initialSelectedInputAsset?.maxSwappableAmount,
})
);
const sliderPressProgress = useSharedValue(SLIDER_COLLAPSED_HEIGHT / SLIDER_HEIGHT);

const selectedOutputChainId = useSharedValue<ChainId>(initialSelectedInputAsset?.chainId || ChainId.mainnet);
const quote = useSharedValue<Quote | CrosschainQuote | QuoteError | null>(null);
const inputProgress = useSharedValue(
Expand All @@ -173,14 +194,14 @@ export const SwapProvider = ({ children }: SwapProviderProps) => {
lastTypedInput,
inputProgress,
outputProgress,
initialSelectedInputAsset,
internalSelectedInputAsset,
internalSelectedOutputAsset,
isFetching,
isQuoteStale,
sliderXPosition,
slippage,
quote,
initialValues,
});

const SwapSettings = useSwapSettings({
Expand Down
4 changes: 2 additions & 2 deletions src/__swaps__/screens/Swap/resources/_selectors/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export function selectUserAssetAddressMapByChainId(assets: ParsedAssetsDictByCha
// selector generators
export function selectUserAssetWithUniqueId(uniqueId: UniqueId) {
return (assets: ParsedAssetsDictByChain) => {
const { chain } = deriveAddressAndChainWithUniqueId(uniqueId);
return assets?.[chain]?.[uniqueId];
const { chainId } = deriveAddressAndChainWithUniqueId(uniqueId);
return assets?.[chainId]?.[uniqueId];
};
}

Expand Down
16 changes: 15 additions & 1 deletion src/__swaps__/screens/Swap/resources/assets/userAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fetchUserAssetsByChain } from './userAssetsByChain';
import { fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets';
import { SUPPORTED_CHAIN_IDS } from '@/chains';
import { useConnectedToHardhatStore } from '@/state/connectedToHardhat';
import store from '@/redux/store';

const addysHttp = new RainbowFetchClient({
baseURL: 'https://addys.p.rainbow.me/v3',
Expand Down Expand Up @@ -70,9 +71,22 @@ type UserAssetsQueryKey = ReturnType<typeof userAssetsQueryKey>;
// Query Function

export const userAssetsFetchQuery = ({ address, currency, testnetMode }: FetchUserAssetsArgs) => {
queryClient.fetchQuery(userAssetsQueryKey({ address, currency, testnetMode }), userAssetsQueryFunction);
return queryClient.fetchQuery(userAssetsQueryKey({ address, currency, testnetMode }), userAssetsQueryFunction);
};

export async function queryUserAssets({
address = store.getState().settings.accountAddress,
currency = store.getState().settings.nativeCurrency,
testnetMode = false,
}: Partial<FetchUserAssetsArgs> = {}) {
const queryKey = userAssetsQueryKey({ address, currency, testnetMode });

const cachedData = queryClient.getQueryData<ParsedAssetsDictByChain>(queryKey);
if (cachedData) return cachedData;

return userAssetsFetchQuery({ address, currency, testnetMode });
}
derHowie marked this conversation as resolved.
Show resolved Hide resolved

export const userAssetsSetQueryDefaults = ({ address, currency, staleTime, testnetMode }: SetUserDefaultsArgs) => {
queryClient.setQueryDefaults(userAssetsQueryKey({ address, currency, testnetMode }), {
staleTime,
Expand Down
12 changes: 12 additions & 0 deletions src/__swaps__/screens/Swap/resources/search/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ export async function fetchTokenSearch(
);
}

export async function queryTokenSearch(
{ chainId, fromChainId, keys, list, threshold, query }: TokenSearchArgs,
config: QueryConfigWithSelect<TokenSearchResult, Error, TokenSearchResult, TokenSearchQueryKey> = {}
) {
const queryKey = tokenSearchQueryKey({ chainId, fromChainId, keys, list, threshold, query });

const cachedData = queryClient.getQueryData<SearchAsset[]>(queryKey);
if (cachedData?.length) return cachedData;

return await queryClient.fetchQuery(queryKey, tokenSearchQueryFunction, config);
}

// ///////////////////////////////////////////////
// Query Hook

Expand Down
4 changes: 2 additions & 2 deletions src/__swaps__/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export function truncateAddress(address?: AddressOrEth) {
export function deriveAddressAndChainWithUniqueId(uniqueId: UniqueId) {
const fragments = uniqueId.split('_');
const address = fragments[0] as Address;
const chain = parseInt(fragments[1], 10) as ChainId;
const chainId = parseInt(fragments[1], 10) as ChainId;
return {
address,
chain,
chainId,
};
}
4 changes: 2 additions & 2 deletions src/__swaps__/utils/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export const parseSearchAsset = ({
searchAsset,
userAsset,
}: {
assetWithPrice?: ParsedAsset;
assetWithPrice?: Partial<ParsedAsset>;
searchAsset: ParsedSearchAsset | SearchAsset;
userAsset?: ParsedUserAsset;
}): ParsedSearchAsset => ({
Expand All @@ -296,7 +296,7 @@ export const parseSearchAsset = ({
amount: '0',
display: '0.00',
},
price: assetWithPrice?.native.price || userAsset?.native?.price,
price: assetWithPrice?.native?.price || userAsset?.native?.price,
},
price: assetWithPrice?.price || userAsset?.price,
balance: userAsset?.balance || { amount: '0', display: '0.00' },
Expand Down
Loading
Loading