diff --git a/.eslintignore b/.eslintignore index 9792363..3e4ba80 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ config-overrides.js build dist vite.config.ts -.eslintrc.js \ No newline at end of file +.eslintrc.js +storybook-static \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 487ed1d..5b173cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "is-primitive": "^3.0.1", "lodash": "^4.17.19", "node-polyfill-webpack-plugin": "^1.1.2", - "notistack": "^1.0.0", + "notistack": "^3.0.1", "patch-package": "^6.4.7", "rc-scrollbars": "^1.1.3", "react": "^16.13.1", @@ -107,6 +107,7 @@ "@types/storybook__addon-knobs": "^5.2.1", "@types/storybook__react": "^5.2.1", "@types/yup": "^0.29.3", + "cross-env": "^7.0.3", "dotenv-webpack": "^7.0.3", "eslint-config-standard-with-typescript": "^21.0.1", "prettier": "^2.1.0", @@ -18379,6 +18380,24 @@ "sha.js": "^2.4.8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-fetch": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", @@ -28946,21 +28965,38 @@ } }, "node_modules/notistack": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/notistack/-/notistack-1.0.10.tgz", - "integrity": "sha512-z0y4jJaVtOoH3kc3GtNUlhNTY+5LE04QDeLVujX3VPhhzg67zw055mZjrBF+nzpv3V9aiPNph1EgRU4+t8kQTQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", "dependencies": { "clsx": "^1.1.0", - "hoist-non-react-statics": "^3.3.0" + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/notistack" }, "peerDependencies": { - "@material-ui/core": "^4.0.0", - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/notistack/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true + }, + "node_modules/notistack/node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" } }, "node_modules/npm-run-path": { diff --git a/package.json b/package.json index cab6b40..917ef90 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "is-primitive": "^3.0.1", "lodash": "^4.17.19", "node-polyfill-webpack-plugin": "^1.1.2", - "notistack": "^1.0.0", + "notistack": "^3.0.1", "patch-package": "^6.4.7", "rc-scrollbars": "^1.1.3", "react": "^16.13.1", @@ -76,8 +76,8 @@ "preview": "vite preview", "lint": "eslint --ext .js,.ts,.tsx .", "fix": "eslint --fix --fix-type suggestion --ext .js,.ts,.tsx .", - "storybook": "start-storybook -p 9009 -c .storybook watch-css -s public", - "build-storybook": "build-storybook -s public", + "storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 9009 -c .storybook watch-css -s public --no-manager-cache", + "build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook -s public ", "build": "vite build", "update-mainnet-list": "node ./src/fetchMainnetList.js" }, @@ -129,6 +129,7 @@ "@types/storybook__addon-knobs": "^5.2.1", "@types/storybook__react": "^5.2.1", "@types/yup": "^0.29.3", + "cross-env": "^7.0.3", "dotenv-webpack": "^7.0.3", "eslint-config-standard-with-typescript": "^21.0.1", "prettier": "^2.1.0", diff --git a/src/components/HeaderButton/FaucetButton.tsx b/src/components/HeaderButton/FaucetButton.tsx index bf21675..cd5ec07 100644 --- a/src/components/HeaderButton/FaucetButton.tsx +++ b/src/components/HeaderButton/FaucetButton.tsx @@ -1,7 +1,6 @@ import Faucet from '@components/Modals/Faucet/Faucet' import { blurContent, unblurContent } from '@consts/uiUtils' import { Button } from '@material-ui/core' -import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown' import React from 'react' import useStyles from './style' diff --git a/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.stories.tsx b/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.stories.tsx new file mode 100644 index 0000000..9408762 --- /dev/null +++ b/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.stories.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { storiesOf } from '@storybook/react' +import LoadingSnackbar from './LoadingSnackbar' + +storiesOf('newUi/loadingSnackbar', module).add('default', () => { + return ( + + }} + id='id' + persist={false} + style={{}} + variant='pending' + /> + ) +}) diff --git a/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.tsx b/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.tsx new file mode 100644 index 0000000..60d2b2d --- /dev/null +++ b/src/components/Snackbar/LoadingSnackbar/LoadingSnackbar.tsx @@ -0,0 +1,39 @@ +import React, { useCallback } from 'react' +import icons from '@static/icons' +import { Box, Grid } from '@material-ui/core' +import { + StyledCircularProgress, + StyledCloseButton, + StyledContainer, + StyledSnackbarContent, + StyledTitle +} from './style' +import { CustomContentProps, useSnackbar } from 'notistack' + +const LoadingSnackbar = React.forwardRef( + ({ id, message }, ref) => { + const { closeSnackbar } = useSnackbar() + + const handleDismiss = useCallback(() => { + closeSnackbar(id) + }, [id, closeSnackbar]) + + return ( + + + + + + + {message} + + + + + + + ) + } +) + +export default LoadingSnackbar diff --git a/src/components/Snackbar/LoadingSnackbar/style.ts b/src/components/Snackbar/LoadingSnackbar/style.ts new file mode 100644 index 0000000..05d9e83 --- /dev/null +++ b/src/components/Snackbar/LoadingSnackbar/style.ts @@ -0,0 +1,75 @@ +import { colors, typography } from '@static/theme' +import { CircularProgress, styled, Typography } from '@material-ui/core' +import { SnackbarContent } from 'notistack' + +export const StyledSnackbarContent = styled(SnackbarContent)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + backgroundColor: colors.invariant.component, + borderStyle: 'solid', + borderWidth: 1, + borderColor: colors.invariant.component, + borderRadius: 10, + maxWidth: 330, + width: 330, + padding: '7px 16px', + minWidth: 100, + ...typography.body2, + + boxShadow: + '0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)', + + '& .MuiCircularProgress-colorPrimary': { + color: colors.invariant.textGrey + }, + + [theme.breakpoints.down('xs')]: { + maxWidth: 'none', + width: 'auto' + } +})) + +export const StyledContainer = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: 'auto', + '& div > div': { + margin: 0 + } +}) + +export const StyledTitle = styled(Typography)({ + marginLeft: 8, + color: colors.invariant.text, + ...typography.body2 +}) + +export const StyledCloseButton = styled('button')({ + backgroundColor: 'transparent', + border: 'none', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + height: 30, + width: 'fit-content', + cursor: 'pointer', + margin: 0, + '&:hover': { + '& img': { + transition: '.2s all ease-in', + transform: 'scale(1.2)' + } + } +}) + +export const StyledCircularProgress = styled(CircularProgress)({ + color: colors.invariant.textGrey, + display: 'flex', + alignItems: 'center', + + '& SVG': { + width: 13, + height: 13 + } +}) diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index a04013f..2da9c0e 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -1,27 +1,47 @@ import React from 'react' import { SnackbarProvider } from 'notistack' -import useStyles from './style' +import { StyledMaterialDesignContent } from './style' +import { Color } from '@material-ui/lab/Alert' +import LoadingSnackbar from './LoadingSnackbar/LoadingSnackbar' +import { useMediaQuery } from '@material-ui/core' +import { theme } from '@static/theme' + +type ExtraVariants = 'pending' + +export type SnackbarVariant = Color | ExtraVariants + +declare module 'notistack' { + interface VariantOverrides { + pending: true + } +} interface ISnackbarProps { children: JSX.Element[] maxSnack: number } + export const Snackbar: React.FC = ({ children, maxSnack }) => { - const classes = useStyles() + const isExSmall = useMediaQuery(theme.breakpoints.down('xs')) return ( + dense + maxSnack={isExSmall ? 5 : maxSnack} + anchorOrigin={ + isExSmall + ? { vertical: 'top', horizontal: 'left' } + : { vertical: 'bottom', horizontal: 'left' } + } + Components={{ + success: StyledMaterialDesignContent, + error: StyledMaterialDesignContent, + info: StyledMaterialDesignContent, + warning: StyledMaterialDesignContent, + pending: LoadingSnackbar + }}> {children} - ) } export default Snackbar diff --git a/src/components/Snackbar/style.ts b/src/components/Snackbar/style.ts index 5b61fa3..46a2cbf 100644 --- a/src/components/Snackbar/style.ts +++ b/src/components/Snackbar/style.ts @@ -1,19 +1,26 @@ -import { makeStyles, Theme } from '@material-ui/core/styles' import { colors, typography } from '@static/theme' +import { styled } from '@material-ui/core' +import { MaterialDesignContent } from 'notistack' -const useStyles = makeStyles((theme: Theme) => ({ - success: { +export const StyledMaterialDesignContent = styled(MaterialDesignContent)(({ theme }) => ({ + '&.notistack-MuiContent-success': { backgroundColor: colors.invariant.component, borderStyle: 'solid', borderWidth: 1, borderColor: colors.invariant.component, borderRadius: 10, ...typography.body2, - maxWidth: 350, - padding: '6px 16px', - maxHeight: 64, + maxWidth: 330, + width: 330, + padding: '4px 16px', minWidth: 100, + + '& > div:first-child': { + flex: 1 + }, + '& SVG': { + fontSize: '16px !important', color: colors.invariant.green, marginTop: -2, [theme.breakpoints.down('xs')]: { @@ -21,24 +28,28 @@ const useStyles = makeStyles((theme: Theme) => ({ } }, [theme.breakpoints.down('xs')]: { - ...typography.body3, - maxWidth: 255, - maxHeight: 32, - padding: '0px 8px 5px 4px' + maxWidth: 'none', + width: 'auto' } }, - error: { + '&.notistack-MuiContent-error': { backgroundColor: colors.invariant.component, borderStyle: 'solid', borderWidth: 1, borderColor: colors.invariant.component, borderRadius: 10, - padding: 10, ...typography.body2, - maxWidth: 450, - maxHeight: 64, + maxWidth: 330, + width: 330, + padding: '4px 16px', minWidth: 100, + + '& > div:first-child': { + flex: 1 + }, + '& SVG': { + fontSize: '16px !important', color: colors.invariant.Error, marginTop: -2, [theme.breakpoints.down('xs')]: { @@ -46,24 +57,28 @@ const useStyles = makeStyles((theme: Theme) => ({ } }, [theme.breakpoints.down('xs')]: { - ...typography.body3, - maxWidth: 255, - maxHeight: 32, - padding: '0px 8px 5px 4px' + maxWidth: 'none', + width: 'auto' } }, - info: { + '&.notistack-MuiContent-info': { backgroundColor: colors.invariant.component, borderStyle: 'solid', borderWidth: 1, borderColor: colors.invariant.component, borderRadius: 10, - padding: 10, ...typography.body2, - maxWidth: 350, - maxHeight: 64, + maxWidth: 330, + width: 330, + padding: '6px 16px', minWidth: 100, + + '& > div:first-child': { + flex: 1 + }, + '& SVG': { + fontSize: '16px !important', color: colors.invariant.textGrey, marginTop: -2, [theme.breakpoints.down('xs')]: { @@ -71,24 +86,28 @@ const useStyles = makeStyles((theme: Theme) => ({ } }, [theme.breakpoints.down('xs')]: { - ...typography.body3, - maxWidth: 255, - maxHeight: 32, - padding: '0px 8px 5px 4px' + maxWidth: 'none', + width: 'auto' } }, - warning: { + '&.notistack-MuiContent-warning': { backgroundColor: colors.invariant.component, borderStyle: 'solid', borderWidth: 1, borderColor: colors.invariant.component, borderRadius: 10, - padding: 10, ...typography.body2, - maxWidth: 350, - maxHeight: 64, + maxWidth: 330, + width: 330, + padding: '6px 16px', minWidth: 100, + + '& > div:first-child': { + flex: 1 + }, + '& SVG': { + fontSize: '16px !important', color: colors.invariant.warning, marginTop: -2, [theme.breakpoints.down('xs')]: { @@ -96,12 +115,8 @@ const useStyles = makeStyles((theme: Theme) => ({ } }, [theme.breakpoints.down('xs')]: { - ...typography.body3, - maxWidth: 255, - maxHeight: 32, - padding: '0px 8px 5px 4px' + maxWidth: 'none', + width: 'auto' } } })) - -export default useStyles diff --git a/src/containers/Notifier/Notifier.tsx b/src/containers/Notifier/Notifier.tsx index 8f4bc1f..7bf7b72 100644 --- a/src/containers/Notifier/Notifier.tsx +++ b/src/containers/Notifier/Notifier.tsx @@ -7,7 +7,6 @@ import { snackbars } from '@selectors/snackbars' import { network } from '@selectors/solanaConnection' import useStyles from './style' import { getExplorer } from '@consts/utils' -import { Network } from '@invariant-labs/sdk-eclipse' import { NetworkType } from '@consts/static' let displayed: string[] = [] diff --git a/src/containers/Notifier/style.ts b/src/containers/Notifier/style.ts index 9680a56..ae451af 100644 --- a/src/containers/Notifier/style.ts +++ b/src/containers/Notifier/style.ts @@ -15,6 +15,8 @@ const useStyles = makeStyles((theme: Theme) => ({ transition: '0.2s all cubic-bezier(0.25, 0.46, 0.45, 0.94)', backfaceVisibility: 'hidden', fontSmoothing: 'subpixel-antialiased', + padding: '0 4px', + '&:hover': { transform: 'scale(1.15) translateY(0px)' }, @@ -48,6 +50,13 @@ const useStyles = makeStyles((theme: Theme) => ({ detailsWrapper: { display: 'flex', alignItems: 'center' + }, + pendingContainer: { + display: 'flex', + flexDirection: 'column' + }, + pendingMessage: { + width: 100 } })) diff --git a/src/static/icons.ts b/src/static/icons.ts index d88c745..795202d 100644 --- a/src/static/icons.ts +++ b/src/static/icons.ts @@ -29,6 +29,7 @@ import activeIcon from './svg/active.svg' import inactiveIcon from './svg/inactive.svg' import allIcon from './svg/all.svg' import docsIcon from './svg/docsCircle.svg' +import closeSmallIcon from './svg/closeSmall.svg' const icons: { [key: string]: string } = { USDT: USDIcon, @@ -62,6 +63,7 @@ const icons: { [key: string]: string } = { activeIcon: activeIcon, inactiveIcon: inactiveIcon, allIcon: allIcon, + closeSmallIcon: closeSmallIcon, docsIcon } diff --git a/src/static/svg/closeSmall.svg b/src/static/svg/closeSmall.svg new file mode 100644 index 0000000..6daa98a --- /dev/null +++ b/src/static/svg/closeSmall.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/store/consts/static.ts b/src/store/consts/static.ts index 0ec5b77..4e9ff61 100644 --- a/src/store/consts/static.ts +++ b/src/store/consts/static.ts @@ -2,6 +2,7 @@ import { FEE_TIERS } from '@invariant-labs/sdk-eclipse/lib/utils' import { BN } from '@project-serum/anchor' import { PublicKey } from '@solana/web3.js' import { TokenPriceData } from './utils' +import { ISnackbar } from '@reducers/snackbars' declare global { interface Window { @@ -247,3 +248,9 @@ export const ALL_FEE_TIERS_DATA = FEE_TIERS.map((tier, index) => ({ export { DEFAULT_PUBLICKEY, EclipseNetworks, MAX_U64, NetworkType } export const POSITIONS_PER_PAGE = 5 + +export const SIGNING_SNACKBAR_CONFIG: Omit = { + message: 'Signing transactions', + variant: 'pending', + persist: true +} diff --git a/src/store/consts/utils.ts b/src/store/consts/utils.ts index 954b216..f935597 100644 --- a/src/store/consts/utils.ts +++ b/src/store/consts/utils.ts @@ -1119,3 +1119,5 @@ export const getExplorer = (networkType: NetworkType) => { return 'https://explorer.dev.eclipsenetwork.xyz/' } } + +export const createLoaderKey = () => (new Date().getMilliseconds() + Math.random()).toString() diff --git a/src/store/reducers/snackbars.ts b/src/store/reducers/snackbars.ts index 1c35fd6..5de5213 100644 --- a/src/store/reducers/snackbars.ts +++ b/src/store/reducers/snackbars.ts @@ -1,11 +1,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { PayloadType } from './types' -import { Color } from '@material-ui/lab/Alert' +import { SnackbarVariant } from '@components/Snackbar/Snackbar' +import { createLoaderKey } from '@consts/utils' export interface ISnackbar { message: string key?: string - variant: Color + variant: SnackbarVariant open: boolean action?: (key: number) => JSX.Element persist?: boolean @@ -27,7 +28,7 @@ const snackbarsSlice = createSlice({ reducers: { add(state, action: PayloadAction>) { state.snackbars.push({ - key: (new Date().getMilliseconds() + Math.random()).toString(), + key: action.payload.key ? action.payload.key : createLoaderKey(), ...action.payload, open: true }) diff --git a/src/store/sagas/positions.ts b/src/store/sagas/positions.ts index 9026e7f..f8fbd16 100644 --- a/src/store/sagas/positions.ts +++ b/src/store/sagas/positions.ts @@ -15,6 +15,7 @@ import { poolsArraySortedByFees, tokens } from '@selectors/pools' import { Pair } from '@invariant-labs/sdk-eclipse' import { createLiquidityPlot, + createLoaderKey, createPlaceholderLiquidityPlot, getPositionsAddressesFromRange } from '@consts/utils' @@ -27,11 +28,12 @@ import { // PublicKey } from '@solana/web3.js' import { NATIVE_MINT, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { WRAPPED_ETH_ADDRESS } from '@consts/static' +import { SIGNING_SNACKBAR_CONFIG, WRAPPED_ETH_ADDRESS } from '@consts/static' import { positionsWithPoolsData, singlePositionData } from '@selectors/positions' import { GuardPredicate } from '@redux-saga/types' // import { createClaimAllPositionRewardsTx } from './farms' import { network, rpcAddress } from '@selectors/solanaConnection' +import { closeSnackbar } from 'notistack' // import { actions as farmsActions } from '@reducers/farms' // import { stakesForPosition } from '@selectors/farms' // import { getStakerProgram } from '@web3/programs/staker' @@ -47,7 +49,18 @@ function* handleInitPositionAndPoolWithETH(action: PayloadAction): Ge return yield* call(handleInitPositionAndPoolWithETH, action) } + const loaderCreatePosition = createLoaderKey() + const loaderSigningTx = createLoaderKey() try { + yield put( + snackbarsActions.add({ + message: 'Creating position', + variant: 'pending', + persist: true, + key: loaderCreatePosition + }) + ) + const connection = yield* call(getConnection) const wallet = yield* call(getWallet) const networkType = yield* select(network) @@ -368,8 +408,13 @@ function* handleInitPositionWithETH(action: PayloadAction): Ge combinedTransaction.recentBlockhash = blockhash.blockhash combinedTransaction.feePayer = wallet.publicKey + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], combinedTransaction) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + signedTx.partialSign(wrappedEthAccount) if (poolSigners.length) { @@ -383,6 +428,9 @@ function* handleInitPositionWithETH(action: PayloadAction): Ge if (!txId.length) { yield put(actions.setInitPositionSuccess(false)) + closeSnackbar(loaderCreatePosition) + yield put(snackbarsActions.remove(loaderCreatePosition)) + return yield put( snackbarsActions.add({ message: 'Position adding failed. Please try again.', @@ -405,11 +453,19 @@ function* handleInitPositionWithETH(action: PayloadAction): Ge } yield put(actions.setInitPositionSuccess(true)) + + closeSnackbar(loaderCreatePosition) + yield put(snackbarsActions.remove(loaderCreatePosition)) } catch (error) { console.log(error) yield put(actions.setInitPositionSuccess(false)) + closeSnackbar(loaderCreatePosition) + yield put(snackbarsActions.remove(loaderCreatePosition)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to send. Please try again.', @@ -421,6 +477,9 @@ function* handleInitPositionWithETH(action: PayloadAction): Ge } export function* handleInitPosition(action: PayloadAction): Generator { + const loaderCreatePosition = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { const allTokens = yield* select(tokens) @@ -433,6 +492,15 @@ export function* handleInitPosition(action: PayloadAction): Ge return yield* call(handleInitPositionWithETH, action) } + yield put( + snackbarsActions.add({ + message: 'Creating position', + variant: 'pending', + persist: true, + key: loaderCreatePosition + }) + ) + const connection = yield* call(getConnection) const wallet = yield* call(getWallet) const networkType = yield* select(network) @@ -502,8 +570,14 @@ export function* handleInitPosition(action: PayloadAction): Ge const blockhash = yield* call([connection, connection.getRecentBlockhash]) tx.recentBlockhash = blockhash.blockhash tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + if (poolSigners.length) { signedTx.partialSign(...poolSigners) } @@ -535,11 +609,19 @@ export function* handleInitPosition(action: PayloadAction): Ge yield put(actions.getPositionsList()) } + + closeSnackbar(loaderCreatePosition) + yield put(snackbarsActions.remove(loaderCreatePosition)) } catch (error) { console.log(error) yield put(actions.setInitPositionSuccess(false)) + closeSnackbar(loaderCreatePosition) + yield put(snackbarsActions.remove(loaderCreatePosition)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to send. Please try again.', @@ -650,7 +732,19 @@ export function* handleGetPositionsList() { } export function* handleClaimFeeWithETH(positionIndex: number) { + const loaderClaimFee = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { + yield put( + snackbarsActions.add({ + message: 'Claiming fee', + variant: 'pending', + persist: true, + key: loaderClaimFee + }) + ) + const connection = yield* call(getConnection) const networkType = yield* select(network) const rpc = yield* select(rpcAddress) @@ -726,7 +820,14 @@ export function* handleClaimFeeWithETH(positionIndex: number) { const blockhash = yield* call([connection, connection.getRecentBlockhash]) tx.recentBlockhash = blockhash.blockhash tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + signedTx.partialSign(wrappedEthAccount) const txid = yield* call(sendAndConfirmRawTransaction, connection, signedTx.serialize(), { @@ -754,8 +855,17 @@ export function* handleClaimFeeWithETH(positionIndex: number) { } yield put(actions.getSinglePosition(positionIndex)) + + closeSnackbar(loaderClaimFee) + yield put(snackbarsActions.remove(loaderClaimFee)) } catch (error) { console.log(error) + + closeSnackbar(loaderClaimFee) + yield put(snackbarsActions.remove(loaderClaimFee)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to claim fee. Please try again.', @@ -767,6 +877,9 @@ export function* handleClaimFeeWithETH(positionIndex: number) { } export function* handleClaimFee(action: PayloadAction) { + const loaderClaimFee = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { const allTokens = yield* select(tokens) const allPositionsData = yield* select(positionsWithPoolsData) @@ -779,6 +892,15 @@ export function* handleClaimFee(action: PayloadAction) { return yield* call(handleClaimFeeWithETH, action.payload) } + yield put( + snackbarsActions.add({ + message: 'Claiming fee', + variant: 'pending', + persist: true, + key: loaderClaimFee + }) + ) + const connection = yield* call(getConnection) const networkType = yield* select(network) const rpc = yield* select(rpcAddress) @@ -819,8 +941,14 @@ export function* handleClaimFee(action: PayloadAction) { const blockhash = yield* call([connection, connection.getRecentBlockhash]) tx.recentBlockhash = blockhash.blockhash tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + const txid = yield* call(sendAndConfirmRawTransaction, connection, signedTx.serialize(), { skipPreflight: false }) @@ -846,8 +974,17 @@ export function* handleClaimFee(action: PayloadAction) { } yield put(actions.getSinglePosition(action.payload)) + + closeSnackbar(loaderClaimFee) + yield put(snackbarsActions.remove(loaderClaimFee)) } catch (error) { console.log(error) + + closeSnackbar(loaderClaimFee) + yield put(snackbarsActions.remove(loaderClaimFee)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to claim fee. Please try again.', @@ -859,7 +996,19 @@ export function* handleClaimFee(action: PayloadAction) { } export function* handleClosePositionWithETH(data: ClosePositionData) { + const loaderClosePosition = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { + yield put( + snackbarsActions.add({ + message: 'Closing position', + variant: 'pending', + persist: true, + key: loaderClosePosition + }) + ) + const connection = yield* call(getConnection) const networkType = yield* select(network) const rpc = yield* select(rpcAddress) @@ -951,7 +1100,14 @@ export function* handleClosePositionWithETH(data: ClosePositionData) { const blockhash = yield* call([connection, connection.getRecentBlockhash]) tx.recentBlockhash = blockhash.blockhash tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + signedTx.partialSign(wrappedEthAccount) const txid = yield* call(sendAndConfirmRawTransaction, connection, signedTx.serialize(), { @@ -984,8 +1140,17 @@ export function* handleClosePositionWithETH(data: ClosePositionData) { // yield* put(farmsActions.getUserStakes()) data.onSuccess() + + closeSnackbar(loaderClosePosition) + yield put(snackbarsActions.remove(loaderClosePosition)) } catch (error) { console.log(error) + + closeSnackbar(loaderClosePosition) + yield put(snackbarsActions.remove(loaderClosePosition)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to close position. Please try again.', @@ -1005,6 +1170,9 @@ export function* handleClosePositionWithETH(data: ClosePositionData) { // } export function* handleClosePosition(action: PayloadAction) { + const loaderClosePosition = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { const allTokens = yield* select(tokens) const allPositionsData = yield* select(positionsWithPoolsData) @@ -1017,6 +1185,15 @@ export function* handleClosePosition(action: PayloadAction) { return yield* call(handleClosePositionWithETH, action.payload) } + yield put( + snackbarsActions.add({ + message: 'Closing position', + variant: 'pending', + persist: true, + key: loaderClosePosition + }) + ) + const connection = yield* call(getConnection) const networkType = yield* select(network) const rpc = yield* select(rpcAddress) @@ -1074,8 +1251,14 @@ export function* handleClosePosition(action: PayloadAction) { const blockhash = yield* call([connection, connection.getRecentBlockhash]) tx.recentBlockhash = blockhash.blockhash tx.feePayer = wallet.publicKey + + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], tx) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + const txid = yield* call(sendAndConfirmRawTransaction, connection, signedTx.serialize(), { skipPreflight: false }) @@ -1106,8 +1289,17 @@ export function* handleClosePosition(action: PayloadAction) { // yield* put(farmsActions.getUserStakes()) action.payload.onSuccess() + + closeSnackbar(loaderClosePosition) + yield put(snackbarsActions.remove(loaderClosePosition)) } catch (error) { console.log(error) + + closeSnackbar(loaderClosePosition) + yield put(snackbarsActions.remove(loaderClosePosition)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to close position. Please try again.', diff --git a/src/store/sagas/swap.ts b/src/store/sagas/swap.ts index 657fb52..10373d2 100644 --- a/src/store/sagas/swap.ts +++ b/src/store/sagas/swap.ts @@ -10,10 +10,15 @@ import { Pair } from '@invariant-labs/sdk-eclipse' import { getConnection } from './connection' import { Keypair, sendAndConfirmRawTransaction, SystemProgram, Transaction } from '@solana/web3.js' import { NATIVE_MINT, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { WRAPPED_ETH_ADDRESS } from '@consts/static' +import { SIGNING_SNACKBAR_CONFIG, WRAPPED_ETH_ADDRESS } from '@consts/static' import { network, rpcAddress } from '@selectors/solanaConnection' +import { createLoaderKey } from '@consts/utils' +import { closeSnackbar } from 'notistack' export function* handleSwapWithETH(): Generator { + const loaderSwappingTokens = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { const allTokens = yield* select(tokens) const allPools = yield* select(poolsArraySortedByFees) @@ -44,6 +49,15 @@ export function* handleSwapWithETH(): Generator { return } + yield put( + snackbarsActions.add({ + message: 'Swapping tokens', + variant: 'pending', + persist: true, + key: loaderSwappingTokens + }) + ) + const isXtoY = tokenFrom.equals(swapPool.tokenX) const wrappedEthAccount = Keypair.generate() @@ -129,11 +143,16 @@ export function* handleSwapWithETH(): Generator { unwrapTx.recentBlockhash = unwrapBlockhash.blockhash unwrapTx.feePayer = wallet.publicKey + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const [initialSignedTx, swapSignedTx, unwrapSignedTx] = yield* call( [wallet, wallet.signAllTransactions], [initialTx, swapTx, unwrapTx] ) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + initialSignedTx.partialSign(wrappedEthAccount) const initialTxid = yield* call( @@ -148,6 +167,9 @@ export function* handleSwapWithETH(): Generator { if (!initialTxid.length) { yield put(swapActions.setSwapSuccess(false)) + closeSnackbar(loaderSwappingTokens) + yield put(snackbarsActions.remove(loaderSwappingTokens)) + return yield put( snackbarsActions.add({ message: 'ETH wrapping failed. Please try again.', @@ -220,11 +242,19 @@ export function* handleSwapWithETH(): Generator { }) ) } + + closeSnackbar(loaderSwappingTokens) + yield put(snackbarsActions.remove(loaderSwappingTokens)) } catch (error) { console.log(error) yield put(swapActions.setSwapSuccess(false)) + closeSnackbar(loaderSwappingTokens) + yield put(snackbarsActions.remove(loaderSwappingTokens)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: @@ -237,6 +267,9 @@ export function* handleSwapWithETH(): Generator { } export function* handleSwap(): Generator { + const loaderSwappingTokens = createLoaderKey() + const loaderSigningTx = createLoaderKey() + try { const allTokens = yield* select(tokens) const allPools = yield* select(poolsArraySortedByFees) @@ -273,6 +306,15 @@ export function* handleSwap(): Generator { return } + yield put( + snackbarsActions.add({ + message: 'Swapping tokens', + variant: 'pending', + persist: true, + key: loaderSwappingTokens + }) + ) + const isXtoY = tokenFrom.equals(swapPool.tokenX) let fromAddress = tokensAccounts[tokenFrom.toString()] @@ -306,7 +348,13 @@ export function* handleSwap(): Generator { swapTx.recentBlockhash = blockhash.blockhash swapTx.feePayer = wallet.publicKey + yield put(snackbarsActions.add({ ...SIGNING_SNACKBAR_CONFIG, key: loaderSigningTx })) + const signedTx = yield* call([wallet, wallet.signTransaction], swapTx) + + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + const txid = yield* call(sendAndConfirmRawTransaction, connection, signedTx.serialize(), { skipPreflight: false }) @@ -332,11 +380,19 @@ export function* handleSwap(): Generator { }) ) } + + closeSnackbar(loaderSwappingTokens) + yield put(snackbarsActions.remove(loaderSwappingTokens)) } catch (error) { console.log(error) yield put(swapActions.setSwapSuccess(false)) + closeSnackbar(loaderSwappingTokens) + yield put(snackbarsActions.remove(loaderSwappingTokens)) + closeSnackbar(loaderSigningTx) + yield put(snackbarsActions.remove(loaderSigningTx)) + yield put( snackbarsActions.add({ message: 'Failed to send. Please try again.', diff --git a/src/store/sagas/wallet.ts b/src/store/sagas/wallet.ts index 82f9c48..99bd439 100644 --- a/src/store/sagas/wallet.ts +++ b/src/store/sagas/wallet.ts @@ -33,6 +33,8 @@ import airdropAdmin from '@consts/airdropAdmin' import { network } from '@selectors/solanaConnection' import { tokens } from '@selectors/pools' import { actions as poolsActions } from '@reducers/pools' +import { createLoaderKey } from '@consts/utils' +import { closeSnackbar } from 'notistack' // import { actions as farmsActions } from '@reducers/farms' // import { actions as bondsActions } from '@reducers/bonds' @@ -112,6 +114,16 @@ export function* handleAirdrop(): Generator { return } + const loaderKey = createLoaderKey() + yield put( + snackbarsActions.add({ + message: 'Airdrop in progress', + variant: 'pending', + persist: true, + key: loaderKey + }) + ) + const connection = yield* call(getConnection) const networkType = yield* select(network) const wallet = yield* call(getWallet) @@ -125,7 +137,7 @@ export function* handleAirdrop(): Generator { airdropTokens[networkType], airdropQuantities[networkType] ) - + yield put( snackbarsActions.add({ message: 'You will soon receive airdrop of tokens', @@ -149,6 +161,9 @@ export function* handleAirdrop(): Generator { }) ) } + + closeSnackbar(loaderKey) + yield put(snackbarsActions.remove(loaderKey)) } export function* setEmptyAccounts(collateralsAddresses: PublicKey[]): Generator { diff --git a/src/store/selectors/solanaWallet.ts b/src/store/selectors/solanaWallet.ts index c909177..4d67983 100644 --- a/src/store/selectors/solanaWallet.ts +++ b/src/store/selectors/solanaWallet.ts @@ -7,11 +7,9 @@ import { tokens } from './pools' import { WRAPPED_ETH_ADDRESS, WETH_POOL_INIT_LAMPORTS, - WETH_POSITION_INIT_LAMPORTS, NetworkType, WETH_POOL_INIT_LAMPORTS_TEST } from '@consts/static' -import { Network } from '@invariant-labs/sdk-eclipse' const store = (s: AnyProps) => s[solanaWalletSliceName] as ISolanaWallet