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

feat: useTransfers hook [OTE-853] #1137

Merged
merged 11 commits into from
Oct 18, 2024
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ import { useBreakpoints } from './hooks/useBreakpoints';
import { useCommandMenu } from './hooks/useCommandMenu';
import { useComplianceState } from './hooks/useComplianceState';
import { useInitializePage } from './hooks/useInitializePage';
import { usePrefetchedQueries } from './hooks/usePrefetchedQueries';
import { useShouldShowFooter } from './hooks/useShouldShowFooter';
import { useTokenConfigs } from './hooks/useTokenConfigs';
import { testFlags } from './lib/testFlags';
@@ -73,6 +74,7 @@ const Content = () => {
useInitializePage();
useAnalytics();
useCommandMenu();
usePrefetchedQueries();

const dispatch = useAppDispatch();
const { isTablet, isNotTablet } = useBreakpoints();
38 changes: 38 additions & 0 deletions src/constants/__test__/cctp.spec.ts
Original file line number Diff line number Diff line change
@@ -5,8 +5,10 @@ import {
getMapOfHighestFeeTokensByChainId,
getMapOfLowestFeeTokensByChainId,
getMapOfLowestFeeTokensByDenom,
isTokenCctp,
} from '@/constants/cctp';

import cctpTokens from '../../../public/configs/cctp.json';
import { TransferType } from '../abacus';

describe('getLowestFeeChainNames', () => {
@@ -252,3 +254,39 @@ describe('getMapOfHighestFeeTokensByChainId', () => {
});
});
});

describe('isTokenCctp', () => {
const getTestAssetWithDenom = (denom: string) => ({
denom,
chainID: '1',
originDenom: denom,
originChainID: '1',
trace: 'test-trace',
isCW20: false,
isEVM: true,
isSVM: false,
symbol: 'test-symbol',
name: 'test-name',
logoURI: 'test-logoURI',
decimals: 10,
tokenContract: 'test-contract',
description: 'test-description',
coingeckoID: 'test-coingeckoID',
recommendedSymbol: 'test-recommendedSymbol',
});
it('returns true for cctp token', () => {
const denom = cctpTokens[0].tokenAddress;
const asset = getTestAssetWithDenom(denom);
expect(isTokenCctp(asset)).toBe(true);
});
it('returns true for cctp token case insensitive', () => {
const denom = cctpTokens[0].tokenAddress.toLowerCase();
const asset = getTestAssetWithDenom(denom);
expect(isTokenCctp(asset)).toBe(true);
});
it('returns false for non-cctp tokens', () => {
const denom = 'non-cctp-denom';
const asset = getTestAssetWithDenom(denom);
expect(isTokenCctp(asset)).toBe(false);
});
});
1 change: 1 addition & 0 deletions src/constants/abacus.ts
Original file line number Diff line number Diff line change
@@ -89,6 +89,7 @@ export const PerpetualMarketType = Abacus.exchange.dydx.abacus.output.PerpetualM

// ------ Configs ------ //
export const StatsigConfig = Abacus.exchange.dydx.abacus.state.manager.StatsigConfig;
export const AutoSweepConfig = Abacus.exchange.dydx.abacus.state.manager.AutoSweepConfig;
export type Configs = Abacus.exchange.dydx.abacus.output.Configs;
export type FeeDiscount = Abacus.exchange.dydx.abacus.output.FeeDiscount;
export type FeeTier = Abacus.exchange.dydx.abacus.output.FeeTier;
20 changes: 16 additions & 4 deletions src/constants/cctp.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Asset } from '@skip-go/client';

import cctpTokens from '../../public/configs/cctp.json';
import { TransferType, TransferTypeType } from './abacus';

@@ -57,12 +59,13 @@ export const getMapOfLowestFeeTokensByChainId = (type: NullableTransferType) =>
export const getMapOfHighestFeeTokensByChainId = (type: NullableTransferType) =>
getMapOfChainsByChainId(getHighestFeeChains(type));

export const cctpTokensByDenom = cctpTokens.reduce(
export const cctpTokensByDenomLowerCased = cctpTokens.reduce(
(acc, token) => {
if (!acc[token.tokenAddress]) {
acc[token.tokenAddress] = [];
const lowerCasedAddress = token.tokenAddress.toLowerCase();
if (!acc[lowerCasedAddress]) {
acc[lowerCasedAddress] = [];
}
acc[token.tokenAddress].push(token);
acc[lowerCasedAddress].push(token);
return acc;
},
{} as Record<string, CctpTokenInfo[]>
@@ -78,3 +81,12 @@ export const cctpTokensByChainId = cctpTokens.reduce(
},
{} as Record<string, CctpTokenInfo[]>
);

export const isTokenCctp = (token: Asset | undefined) => {
return isDenomCctp(token?.denom);
};

const isDenomCctp = (denom: string | undefined) => {
if (!denom) return false;
return Boolean(cctpTokensByDenomLowerCased[denom.toLowerCase()]);
};
4 changes: 4 additions & 0 deletions src/constants/graz.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,10 @@ export const getNeutronChainId = () => {
return isMainnet ? CosmosChainId.Neutron : CosmosChainId.NeutronTestnet;
};

export const getSolanaChainId = () => {
return isMainnet ? 'solana' : 'solana-devnet';
};

const osmosisChainId = getOsmosisChainId();
const nobleChainId = getNobleChainId();
const neutronChainId = getNeutronChainId();
1 change: 1 addition & 0 deletions src/constants/notifications.ts
Original file line number Diff line number Diff line change
@@ -207,6 +207,7 @@ export enum TransferNotificationTypes {
Deposit = 'deposit',
}

// TODO: fix typo
export type TransferNotifcation = {
id?: string;
txHash: string;
113 changes: 113 additions & 0 deletions src/constants/transfers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Asset } from '@skip-go/client';

import { WalletNetworkType } from '@/constants/wallets';

import { isNativeDenom } from '@/lib/assetUtils';

import { isTokenCctp } from './cctp';

export enum TransferType {
Deposit = 'DEPOSIT',
Withdraw = 'WITHDRAW',
}

export type NetworkType = 'evm' | 'svm' | 'cosmos';

// TODO [onboarding-rewrite]: followup with skip about making logoUri an optional property
const DUMMY_LOGO_URI = 'dummy-logo-uri';

// TODO [onboarding-rewrite]: maybe unhardcode this. we can retrieve this via the skipclient and filter ourselves
// but we don't have to? may be worth hardcoding to reduce an extra network call since this isn't going to change much
// and we should have decent visibility into when this changes. TBD
export const UNISWAP_VENUES = [
{
name: 'ethereum-uniswap',
chainID: '1',
},
{
name: 'polygon-uniswap',
chainID: '137',
},
{
name: 'optimism-uniswap',
chainID: '10',
},
{
name: 'arbitrum-uniswap',
chainID: '42161',
},
{
name: 'base-uniswap',
chainID: '8453',
},
{
name: 'avalanche-uniswap',
chainID: '43114',
},
{
name: 'binance-uniswap',
chainID: '56',
},
{
name: 'celo-uniswap',
chainID: '42220',
},
{
name: 'blast-uniswap',
chainID: '81457',
},
].map((x) => ({ ...x, logoUri: DUMMY_LOGO_URI }));

// TODO [onboarding-rewrite]: maybe unhardcode this. same as above.
export const COSMOS_SWAP_VENUES = [
{
name: 'osmosis-poolmanager',
chainID: 'osmosis-1',
logoUri: DUMMY_LOGO_URI,
},
{
name: 'neutron-astroport',
chainID: 'neutron-1',
logoUri: DUMMY_LOGO_URI,
},
];

export const SWAP_VENUES = [...UNISWAP_VENUES, ...COSMOS_SWAP_VENUES];

export const getNetworkTypeFromWalletNetworkType = (
walletNetworkType?: WalletNetworkType
): NetworkType => {
if (walletNetworkType === WalletNetworkType.Evm) {
return 'evm';
}
if (walletNetworkType === WalletNetworkType.Solana) {
return 'svm';
}
if (walletNetworkType === WalletNetworkType.Cosmos) {
return 'cosmos';
}
return 'evm';
};

export const getDefaultTokenDenomFromAssets = (assets: Asset[]): string => {
const cctpToken = assets.find((asset) => {
return isTokenCctp(asset);
});
const nativeChainToken = assets.find((asset) => {
return isNativeDenom(asset.denom);
});
const uusdcToken = assets.find((asset) => {
return asset.denom === 'uusdc' || asset.originDenom === 'uusdc';
});
// If not cctp or native chain token, default to the first item in the list
const defaultTokenDenom =
cctpToken?.denom ?? nativeChainToken?.denom ?? uusdcToken?.denom ?? assets[0]?.denom;
return defaultTokenDenom;
};

export const getDefaultChainIDFromNetworkType = (networkType: NetworkType): string | undefined => {
if (networkType === 'evm') return '1';
if (networkType === 'svm') return 'solana';
if (networkType === 'cosmos') return 'noble-1';
return undefined;
};
20 changes: 15 additions & 5 deletions src/hooks/transfers/skipClient.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createContext, useContext, useMemo } from 'react';

import {
MsgWithdrawFromSubaccount,
TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT,
} from '@dydxprotocol/v4-client-js';
import { SkipClient } from '@skip-go/client';

import { getNeutronChainId, getNobleChainId, getOsmosisChainId } from '@/constants/graz';
@@ -14,7 +18,7 @@ import { useDydxClient } from '../useDydxClient';
import { useEndpointsConfig } from '../useEndpointsConfig';

type SkipContextType = ReturnType<typeof useSkipClientContext>;
const SkipContext = createContext<SkipContextType | undefined>(undefined);
const SkipContext = createContext<SkipContextType>({} as SkipContextType);
SkipContext.displayName = 'skipClient';

export const SkipProvider = ({ ...props }) => (
@@ -28,9 +32,12 @@ const useSkipClientContext = () => {
useEndpointsConfig();
const { compositeClient } = useDydxClient();
const selectedDydxChainId = useAppSelector(getSelectedDydxChainId);
const skipClient = useMemo(
() =>
new SkipClient({
// reactQuery only accepts serializable objects/values, so we return a string id
// so any useQuery that uses the skipClient can use that id as a query key
// to ensure it has the most up-to-date skipClient
const { skipClient, skipClientId } = useMemo(
() => ({
skipClient: new SkipClient({
endpointOptions: {
getRpcEndpointForChain: async (chainId: string) => {
if (chainId === getNobleChainId()) return nobleValidator;
@@ -44,7 +51,10 @@ const useSkipClientContext = () => {
throw new Error(`Error: no rpc endpoint found for chainId: ${chainId}`);
},
},
registryTypes: [[TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT, MsgWithdrawFromSubaccount]],
}),
skipClientId: crypto.randomUUID(),
Copy link
Contributor

Choose a reason for hiding this comment

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

👀 what is this clientId supposed to represent? can you leave a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ooo good point! will do :)

}),
[
compositeClient?.network.validatorConfig.restEndpoint,
neutronValidator,
@@ -55,5 +65,5 @@ const useSkipClientContext = () => {
validators,
]
);
return { skipClient };
return { skipClient, skipClientId };
};
Loading

Unchanged files with check annotations Beta

const navigation = globalThis.navigation;
if (!navigation) {
globalThis.history?.back();

Check warning on line 21 in src/components/BackButton.tsx

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
// @ts-ignore
} else if (navigation.canGoBack) {
navigation.back();
${({ action }) => action && buttonActionVariants[action]}
${({ action, state }) =>
state &&

Check warning on line 168 in src/components/Button.tsx

GitHub Actions / lint

Unnecessary conditional, value is always truthy
css`
// Ordered from lowest to highest priority (ie. Disabled should overwrite Active and Loading states)
${state[ButtonState.Loading] && buttonStateVariants(action)[ButtonState.Loading]}
--details-grid-numColumns: 2;
--details-item-vertical-padding: ;
${({ layout }) => layout && detailsLayoutVariants[layout]}

Check warning on line 224 in src/components/Details.tsx

GitHub Actions / lint

Unnecessary conditional, value is always truthy
`;
const $Item = styled.div<{
justifyItems?: 'start' | 'end';
withOverflow?: boolean;
}>`
${({ layout }) => layout && itemLayoutVariants[layout]}

Check warning on line 232 in src/components/Details.tsx

GitHub Actions / lint

Unnecessary conditional, value is always truthy
${({ justifyItems }) =>
justifyItems === 'end' &&
`}
${({ layout, withOverflow }) =>
layout &&

Check warning on line 244 in src/components/Details.tsx

GitHub Actions / lint

Unnecessary conditional, value is always truthy
withOverflow &&
{
column: css`
<$Item
disabled={!item.onSelect}
$highlightColor={item.highlightColor}
onSelect={item?.onSelect}

Check warning on line 82 in src/components/DropdownMenu.tsx

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
>
{item.icon}
{item.label}
<$FormInputContainer className={className} isValidationAttached={validationConfig?.attached}>
<$InputContainer hasLabel={!!label} hasSlotRight={!!slotRight}>
{label ? (
<$WithLabel label={label} inputID={id} disabled={otherProps?.disabled}>

Check warning on line 35 in src/components/FormInput.tsx

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
<Input ref={ref} id={id} {...otherProps} />
</$WithLabel>
) : (
if (isTablet && !canAccountTrade) {
dispatch(openDialog(DialogTypes.Onboarding()));
}
}, []);

Check warning on line 27 in src/components/GuardedMobileRoute.tsx

GitHub Actions / lint

React Hook useEffect has missing dependencies: 'canAccountTrade', 'dispatch', and 'isTablet'. Either include them or remove the dependency array
useEffect(() => {
const dialogClosed =
navigate('/');
}
prevActiveDialog.current = activeDialog;
}, [activeDialog, canAccountTrade, isTablet]);

Check warning on line 38 in src/components/GuardedMobileRoute.tsx

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'navigate'. Either include it or remove the dependency array
return <Outlet />;
};
? undefined
: Number(newFormattedValue.replace(',', '.'));
onInput?.({ value: newValue, floatValue, formattedValue: newFormattedValue, ...e });

Check warning on line 178 in src/components/Input.tsx

GitHub Actions / lint

Unnecessary optional chain on a non-nullish value
}}
// Native
disabled={disabled}