Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VITE_CASPERDASH_BASE_API_URL=https://testnet-api.casperdash.io
VITE_NETWORK_NAME=casper-test
VITE_CASPERDASH_BASE_API_URL=https://api.casperdash.io
VITE_NETWORK_NAME=casper
VITE_CSPR_LIVE_URL=https://testnet.cspr.live
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"chakra-react-select": "^4.6.0",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"framer-motion": "^10.2.4",
"framer-motion": "^10.12.12",
"fuse.js": "^6.6.2",
"i18next": "^22.4.11",
"identicon.js": "^2.3.3",
Expand Down
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const App = () => {
QueryKeysEnum.MY_TOKENS,
QueryKeysEnum.ACCOUNT,
QueryKeysEnum.ACCOUNT_BALANCES,
QueryKeysEnum.SWAP_SETTINGS,
].includes(_.first(queryKey) as unknown as QueryKeysEnum);
},
},
Expand Down
47 changes: 47 additions & 0 deletions src/components/Inputs/InputNumberField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NumberInput, NumberInputField } from '@chakra-ui/react';
import { Control, Controller } from 'react-hook-form';

type Props = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: Control<any, any>;
name: string;
onChange?: (value: number) => void;
min?: number;
max?: number;
};

const InputNumberField = ({
min,
max,
name,
control,
onChange,
...restProps
}: Props) => {
return (
<Controller
name={name}
control={control}
render={({ field: { onChange: onChangeForm, value, onBlur } }) => {
return (
<NumberInput
{...restProps}
value={value}
min={min}
max={max}
onChange={(val: string) => {
const valNumber = parseFloat(val) || 0;
onChangeForm(valNumber);
onChange?.(valNumber);
}}
onBlur={onBlur}
>
<NumberInputField />
</NumberInput>
);
}}
></Controller>
);
};

export default InputNumberField;
10 changes: 1 addition & 9 deletions src/components/Inputs/RadioButton/RadioButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,7 @@ export const RadioButtonGroup = ({
const group = getRootProps();

return (
<Flex
flexWrap="wrap"
maxW={{ base: 'xs', md: 'lg' }}
justifyContent="center"
gap="4"
{...group}
{...restProps}
__css={styles.radioButtons}
>
<Flex {...group} {...restProps} __css={styles.radioButtons}>
{radios.map((child: ReactElement, index: number) => {
const radioProps = getRadioProps({ value: child.props.value });
return cloneElement(child, {
Expand Down
59 changes: 59 additions & 0 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ReactNode } from 'react';

import {
Modal as ModalChakra,
ModalOverlay,
ModalHeader,
ModalBody,
ModalCloseButton,
Heading,
ModalFooter,
ModalContent,
} from '@chakra-ui/react';

type ModalProps = {
isOpen: boolean;
onClose: () => void;
title?: string | null;
children: ReactNode;
header?: ReactNode;
footer?: ReactNode;
};

const Modal = ({
isOpen,
onClose,
title,
children,
header,
footer,
}: ModalProps) => {
return (
<>
<ModalChakra isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent borderRadius="2xl" m={16} w={{ base: 'sm', md: 'xl' }}>
<ModalHeader>
{title && (
<Heading
variant={{
base: 'sm',
md: 'xl',
}}
textAlign={'center'}
>
{title}
</Heading>
)}
{header}
</ModalHeader>
<ModalCloseButton />
<ModalBody>{children}</ModalBody>
<ModalFooter>{footer}</ModalFooter>
</ModalContent>
</ModalChakra>
</>
);
};

export default Modal;
25 changes: 25 additions & 0 deletions src/components/Surface/CircleWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Box, BoxProps } from '@chakra-ui/react';

type CircleWrapperProps = {
size?: number | string;
} & BoxProps;

const CircleWrapper = ({
size = 8,
children,
...restProps
}: CircleWrapperProps) => {
return (
<Box
backgroundColor={'light'}
w={size}
h={size}
borderRadius={'full'}
{...restProps}
>
{children}
</Box>
);
};

export default CircleWrapper;
7 changes: 7 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ export const Config = {
VITE_CASPERDASH_BASE_API_URL || 'https://api.casperdash.io',
networkName: VITE_NETWORK_NAME || 'casper',
csprLiveUrl: 'https://testnet.cspr.live',
friendlyMarketUrl: 'https://api.friendly.market/api/v1',
tokenListUrl:
'https://raw.githubusercontent.com/FriendlyMarket/token-list/main/tokenlist.json',
friendlyMarketModuleBytesUrl:
'https://s3.ap-southeast-1.amazonaws.com/assets.casperdash.io/sc-resources/gistfile1.txt',
swapContractHash:
'fa64806972777d6263dea1f0e5a908620ffd19113df57ebd9ea4aa4e23de6090',
};
1 change: 1 addition & 0 deletions src/enums/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum PathEnum {
NFT = '/nfts',
STAKING = '/staking',
SEND = '/send',
SWAP = '/swap',
// New wallet.
NEW_WALLET = '/create',
NEW_PASSWORD = '/create/new-password',
Expand Down
13 changes: 12 additions & 1 deletion src/enums/queryKeys.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export enum QueryKeysEnum {
LOCKED = 'locked',
ACCOUNT_BALANCES = 'account_balances',
RECOVERY_PHRASE = 'recovery_phrase',
MY_TOKENS = 'tokens',
TOKENS = 'tokens',
MY_TOKENS = 'my_tokens',
TOKEN = 'token',
CURRENT_ACCOUNT = 'current_account',
ASSETS = 'assets',
Expand All @@ -20,4 +21,14 @@ export enum QueryKeysEnum {
PRICE_HISTORIES = 'price_histories',
NFTS = 'nfts',
PRIVATE_KEY_WITH_UID = 'private_key_with_uid',
// Swap
SWAP_TOKENS = 'swap_tokens',
SWAP_TOKEN_BALANCE = 'swap_token_balance',
SWAP_SETTINGS = 'swap_settings',
SWAP_AMM_PAIR = 'swap_amm_pair',
// Coingecko
COINGECKO_COIN_MARKET_DATA = 'coingecko_coin_market_data',

// Utils
UTILS_VALIDATE_SWAP = 'utils_validate_swap',
}
4 changes: 4 additions & 0 deletions src/enums/tokenTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum TokenTypesEnum {
NATIVE = 'Native',
ERC20 = 'ERC20',
}
44 changes: 44 additions & 0 deletions src/hooks/queries/useGetCoinMarketData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import _get from 'lodash-es/get';

import { QueryKeysEnum } from '@/enums/queryKeys.enum';
import { getCoinPrice } from '@/services/coingecko/coin/prices';

type Data = {
marketCap: number;
price: number;
priceChange: number;
volume: number;
};

export const useGetCoinMarketData = (
id?: string,
options: Omit<
UseQueryOptions<
Data,
unknown,
Data,
[QueryKeysEnum.COINGECKO_COIN_MARKET_DATA, string | undefined]
>,
'queryKey' | 'queryFn'
> = {}
) => {
return useQuery(
[QueryKeysEnum.COINGECKO_COIN_MARKET_DATA, id],
async (): Promise<Data> => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = await getCoinPrice({ id: id! });

return {
marketCap: _get(result, 'market_data.market_cap.usd', 0),
price: _get(result, 'market_data.current_price.usd', 0),
priceChange: _get(result, 'market_data.price_change_percentage_24h', 0),
volume: _get(result, 'market_data.total_volume.usd', 0),
};
},
{
...options,
enabled: !!id,
}
);
};
8 changes: 3 additions & 5 deletions src/hooks/queries/useGetToken.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';

import useDebounce from '../helpers/useDebounce';
import { QueryKeysEnum } from '@/enums/queryKeys.enum';
import { getToken, GetTokenResponse } from '@/services/casperdash/token';

Expand All @@ -20,13 +19,12 @@ export const useGetToken = (
'queryKey' | 'queryFn'
>
) => {
const tokenAddressDebounced = useDebounce<string>(tokenAddress, 300);
return useQuery(
[QueryKeysEnum.TOKEN, tokenAddressDebounced],
() => getToken({ tokenAddress: tokenAddressDebounced }),
[QueryKeysEnum.TOKEN, tokenAddress],
() => getToken({ tokenAddress }),
{
...options,
enabled: !!tokenAddressDebounced,
enabled: !!tokenAddress,
}
);
};
54 changes: 54 additions & 0 deletions src/hooks/queries/useGetTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import * as _ from 'lodash-es';

import { QueryKeysEnum } from '@/enums/queryKeys.enum';
import { getTokensInfo, TokenInfo } from '@/services/casperdash/token';
import { Token } from '@/typings/token';
import { hexToNumber } from '@/utils/currency';

type UseGetTokensParams = {
tokenAddresses: string[];
publicKey?: string;
};

export const useGetTokens = (
{ tokenAddresses, publicKey }: UseGetTokensParams,
options?: Omit<
UseQueryOptions<
Token[],
unknown,
Token[],
[QueryKeysEnum.TOKENS, string | undefined, string[]]
>,
'queryKey' | 'queryFn'
>
) => {
return useQuery(
[QueryKeysEnum.TOKENS, publicKey, tokenAddresses],
async () => {
if (!publicKey) {
return [];
}
const tokensInfo = await getTokensInfo({
publicKey,
tokenAddress: tokenAddresses,
});

return tokensInfo.map((tokenInfo: TokenInfo) => {
const balanceHex = _.get(tokenInfo, 'balance.hex', '0');
const decimalsHex = _.get(tokenInfo, 'decimals.hex', '0');
return {
name: tokenInfo.name,
tokenAddress: tokenInfo.address,
symbol: tokenInfo.symbol,
balance: hexToNumber(balanceHex, decimalsHex),
decimals: parseInt(decimalsHex, 16),
};
});
},
{
...options,
enabled: !!publicKey,
}
);
};
45 changes: 45 additions & 0 deletions src/hooks/useFuse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ChangeEvent, useCallback, useMemo, useState } from 'react';

import Fuse from 'fuse.js';
import { debounce } from 'lodash-es';

interface Options<T> extends Fuse.IFuseOptions<T> {
limit?: number;
}

export const useFuse = <T = unknown>(list: T[], options: Options<T>) => {
// defining our query state in there directly
const [query, updateQuery] = useState('');

// removing custom options from Fuse options object
// NOTE: `limit` is actually a `fuse.search` option, but we merge all options for convenience
const { limit = 1000, ...fuseOptions } = options;

// let's memoize the fuse instance for performances
const fuse = useMemo(() => new Fuse(list, fuseOptions), [list, fuseOptions]);

// memoize results whenever the query or options change
const hits = useMemo(
// if query is empty and `matchAllOnEmptyQuery` is `true` then return all list
// NOTE: we remap the results to match the return structure of `fuse.search()`
() => fuse.search(query, { limit }),
[fuse, limit, query]
);

// debounce updateQuery and rename it `setQuery` so it's transparent
const setQuery = useMemo(() => debounce(updateQuery, 100), []);

// pass a handling helper to speed up implementation
const onSearch = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value.trim()),
[setQuery]
);

// still returning `setQuery` for custom handler implementations
return {
hits,
onSearch,
query,
setQuery,
};
};
Loading