diff --git a/package.json b/package.json index 582c2e46..b55e28d3 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "lottie-web": "^5.10.2", "normalize.css": "^8.0.1", "numeral": "^2.0.6", + "papaparse": "^5.4.1", "path-browserify": "^1.0.1", "querystring-es3": "^0.2.1", "react": "^18.2.0", @@ -51,6 +52,7 @@ "react-burger-menu": "^3.0.9", "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", "react-i18next": "^12.2.0", "react-infinite-scroller": "^1.2.6", "react-loading-skeleton": "^3.1.1", @@ -114,6 +116,7 @@ "@types/canvas-confetti": "^1.6.0", "@types/crypto-js": "^4.2.1", "@types/numeral": "^2.0.2", + "@types/papaparse": "^5.3.7", "@types/react-burger-menu": "^2.8.3", "@types/react-infinite-scroller": "^1.2.3", "@types/use-persisted-state": "^0.3.2", diff --git a/public/deposit_drop_template.csv b/public/deposit_drop_template.csv new file mode 100644 index 00000000..202e285f --- /dev/null +++ b/public/deposit_drop_template.csv @@ -0,0 +1,2 @@ +amount;winner_address +0;lum…. \ No newline at end of file diff --git a/src/assets/images/cosmonaut_flying.svg b/src/assets/images/cosmonaut_flying.svg new file mode 100644 index 00000000..675a4e36 --- /dev/null +++ b/src/assets/images/cosmonaut_flying.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/download.svg b/src/assets/images/download.svg new file mode 100644 index 00000000..cf6d0304 --- /dev/null +++ b/src/assets/images/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 4610ad7b..759e6153 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -8,6 +8,7 @@ import coinsStacked2 from './images/coins_stacked_2.svg'; import coinsStackedPurple from './images/coins_stacked_purple.svg'; import cosmonautCoin from './images/cosmonaut_coin.png'; import cosmostation from './images/cosmostation.svg'; +import cosmonautFlying from './images/cosmonaut_flying.svg'; import claim from './images/claim.svg'; import clock from './images/clock.svg'; import deposit from './images/deposit.svg'; @@ -19,6 +20,7 @@ import dollarIcon from './images/dollar_icon.svg'; import dollarWhite from './images/dollar_white.svg'; import downArrow from './images/down_arrow.svg'; import faucet from './images/faucet.svg'; +import download from './images/download.svg'; import info from './images/info.svg'; import infoWhite from './images/info_white.svg'; import keplr from './images/keplr.svg'; @@ -75,6 +77,7 @@ const Assets = { coinsStacked, coinsStacked2, cosmonautCoin, + cosmonautFlying, cosmostation, claim, clock, @@ -87,6 +90,7 @@ const Assets = { dollarWhite, downArrow, faucet, + download, info, infoWhite, keplr, diff --git a/src/components/AmountInput/AmountInput.scss b/src/components/AmountInput/AmountInput.scss index 88951811..1176b5ad 100644 --- a/src/components/AmountInput/AmountInput.scss +++ b/src/components/AmountInput/AmountInput.scss @@ -50,7 +50,7 @@ input[type='number'] { font-size: 22px; font-weight: 400; line-height: 26px; - letter-spacing: 0em; + letter-spacing: 0; text-align: left; background-color: transparent; color: var(--color-black); diff --git a/src/components/BestPrizeCard/BestPrizeCard.scss b/src/components/BestPrizeCard/BestPrizeCard.scss index 254fc9bd..3bdabcae 100644 --- a/src/components/BestPrizeCard/BestPrizeCard.scss +++ b/src/components/BestPrizeCard/BestPrizeCard.scss @@ -185,10 +185,10 @@ .orbit { position: absolute; - top: 0px; - left: 0px; - bottom: 0px; - right: 0px; + top: 0; + left: 0; + bottom: 0; + right: 0; width: 100%; height: 100%; } diff --git a/src/components/DepositIbcTransfer/DepositIbcTransfer.tsx b/src/components/DepositIbcTransfer/DepositIbcTransfer.tsx new file mode 100644 index 00000000..dff315e8 --- /dev/null +++ b/src/components/DepositIbcTransfer/DepositIbcTransfer.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { Coin, MICRO_LUM_DENOM } from '@lum-network/sdk-javascript'; +import { FormikProps } from 'formik'; + +import Assets from 'assets'; +import { AmountInput, AssetsSelect, Button, Card, Tooltip } from 'components'; +import { OtherWalletModel, PoolModel } from 'models'; +import { RootState } from 'redux/store'; +import { I18n, NumbersUtils, DenomsUtils, WalletUtils, PoolsUtils } from 'utils'; + +interface Props { + currentPool: PoolModel; + balances: Coin[]; + price: number; + pools: PoolModel[]; + title: string; + subtitle: string; + nonEmptyWallets: OtherWalletModel[]; + form: FormikProps<{ amount: string }>; + onTransfer?: (amount: string) => void; + disabled?: boolean; +} + +const DepositIbcTransfer = (props: Props) => { + const { currentPool, balances, disabled, price, pools, form, nonEmptyWallets, title, subtitle, onTransfer } = props; + + const navigate = useNavigate(); + + const isLoading = useSelector((state: RootState) => state.loading.effects.wallet.ibcTransfer); + const prizeStrat = currentPool.prizeStrategy; + + let avgPrize = 0; + + if (prizeStrat) { + let avgPrizesDrawn = 0; + for (const prizeBatch of prizeStrat.prizeBatches) { + avgPrizesDrawn += (Number(currentPool.estimatedPrizeToWin?.amount || '0') * (Number(prizeBatch.poolPercent) / 100)) / Number(prizeBatch.quantity); + } + + avgPrize = avgPrizesDrawn / prizeStrat.prizeBatches.length / prizeStrat.prizeBatches.length; + } + + return ( +
+
+
+
+
+
+
+ 0 ? balances[0].amount : '0')), + denom: DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase(), + })} + onMax={() => { + const amount = WalletUtils.getMaxAmount(currentPool.nativeDenom, balances, currentPool.internalInfos?.fees); + form.setFieldValue('amount', amount); + }} + inputProps={{ + type: 'number', + min: 0, + max: balances.length > 0 ? balances[0].amount : '0', + step: 'any', + lang: 'en', + disabled, + placeholder: (100 / price).toFixed(6), + ...form.getFieldProps('amount'), + onChange: (e) => { + const inputAmount = Number(e.target.value); + const maxAmount = Number(WalletUtils.getMaxAmount(currentPool.nativeDenom, balances, currentPool.internalInfos?.fees)); + + if (Number.isNaN(inputAmount) || inputAmount < 0) { + e.target.value = '0'; + } else if (inputAmount > maxAmount) { + e.target.value = maxAmount > 0 ? maxAmount.toString() : '0'; + } + + form.handleChange(e); + }, + }} + price={price} + error={form.touched.amount ? form.errors.amount : ''} + /> +
+
+ {pools.filter((p) => p.nativeDenom !== MICRO_LUM_DENOM).length > 1 && ( + ((result, { balances }) => { + if (balances.length > 0) { + result.push({ + amount: balances[0].amount, + denom: balances[0].denom, + }); + } + return result; + }, [])} + value={currentPool.nativeDenom} + onChange={(value) => { + navigate(`/pools/${DenomsUtils.getNormalDenom(value)}`, { replace: true }); + }} + options={nonEmptyWallets.map((wallet) => ({ + label: DenomsUtils.getNormalDenom(wallet.balances[0].denom), + value: wallet.balances[0].denom, + }))} + /> + )} + +
+
+ {I18n.t('deposit.chancesHint.winning.title')} + + info + + +
+
{NumbersUtils.float2ratio(PoolsUtils.getWinningChances(form.values.amount ? Number(form.values.amount) : 100 / price, currentPool))}
+
+
+
+ {I18n.t('deposit.chancesHint.averagePrize.title')} + + info + + +
+
+ {avgPrize.toFixed(2)} {DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase()} +
+
+
+ +
+
+
+ ); +}; + +export default DepositIbcTransfer; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 467912ad..ffa8fbd9 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -27,6 +27,7 @@ const Header = ({ logoutModalRef }: { logoutModalRef: RefObject } const prizes = useSelector((state: RootState) => state.wallet.lumWallet?.prizes); const timeline = useRef(); const [isLanding, setIsLanding] = useState(false); + const [isDrops, setIsDrops] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const headerRef = useRef(null); @@ -36,6 +37,8 @@ const Header = ({ logoutModalRef }: { logoutModalRef: RefObject } useEffect(() => { setIsLanding(window.location.pathname === NavigationConstants.LANDING); + + setIsDrops(window.location.pathname.startsWith(NavigationConstants.DROPS)); }, [location.pathname]); useEffect(() => { @@ -216,6 +219,47 @@ const Header = ({ logoutModalRef }: { logoutModalRef: RefObject } ); } + if (isDrops) { + return ( +
    +
  • + `navlink ${isActive ? 'active' : ''}`}> + {I18n.t('pools.title')} + +
  • + {address && ( +
  • + `navlink position-relative ${isActive ? 'active' : ''}`}> + {I18n.t('depositDrops.myDeposits.title')} + +
  • + )} + {inBurgerMenu ? : null} +
  • +
    + + {address ? ( + + ) : null} +
    +
  • + {!inBurgerMenu ? : null} +
+ ); + } + const prizesPendingLength = (prizes && prizes.filter((prize) => prize.state === PrizesConstants.PrizeState.PENDING).length) || 0; return ( diff --git a/src/components/Leaderboard/Leaderboard.scss b/src/components/Leaderboard/Leaderboard.scss index eb85a0da..5876b35f 100644 --- a/src/components/Leaderboard/Leaderboard.scss +++ b/src/components/Leaderboard/Leaderboard.scss @@ -23,7 +23,7 @@ padding: 40px; &.with-anim { - padding-bottom: 0px; + padding-bottom: 0; } @include media-breakpoint-down(md) { @@ -59,7 +59,7 @@ } &.white-bg { - box-shadow: 0px 4px 23px 0px #f1edff; + box-shadow: 0 4px 23px 0 #f1edff; } &.flat { diff --git a/src/pages/Deposit/components/Modals/IbcTransfer/IbcTransfer.tsx b/src/components/Modals/IbcTransfer/IbcTransfer.tsx similarity index 100% rename from src/pages/Deposit/components/Modals/IbcTransfer/IbcTransfer.tsx rename to src/components/Modals/IbcTransfer/IbcTransfer.tsx diff --git a/src/pages/Deposit/components/Modals/QuitDeposit/QuitDeposit.tsx b/src/components/Modals/QuitDeposit/QuitDeposit.tsx similarity index 100% rename from src/pages/Deposit/components/Modals/QuitDeposit/QuitDeposit.tsx rename to src/components/Modals/QuitDeposit/QuitDeposit.tsx diff --git a/src/components/PoolSelect/PoolSelect.tsx b/src/components/PoolSelect/PoolSelect.tsx index 5433446d..c89247ed 100644 --- a/src/components/PoolSelect/PoolSelect.tsx +++ b/src/components/PoolSelect/PoolSelect.tsx @@ -39,7 +39,7 @@ const PoolOption = (
- {assetIcon && } {props.data.label} + {assetIcon && menu} {props.data.label}
diff --git a/src/components/PurpleBackgroundImage/PurpleBackgroundImage.tsx b/src/components/PurpleBackgroundImage/PurpleBackgroundImage.tsx index 3f178013..e5c94804 100644 --- a/src/components/PurpleBackgroundImage/PurpleBackgroundImage.tsx +++ b/src/components/PurpleBackgroundImage/PurpleBackgroundImage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import './PurpleBackgroundImage.scss'; const PurpleBackgroundImage = (props: React.ImgHTMLAttributes) => { - return ; + return purple; }; export default PurpleBackgroundImage; diff --git a/src/components/Steps/Steps.scss b/src/components/Steps/Steps.scss index 5aaa2b2e..c5df98dc 100644 --- a/src/components/Steps/Steps.scss +++ b/src/components/Steps/Steps.scss @@ -44,12 +44,12 @@ translate: -1.5px -1.5px; transform: rotateY(180deg); } - + .index-default-border { background: conic-gradient(var(--color-light-grey) 100%, 0, transparent); } - - .index-border { + + .index-border { background: conic-gradient(var(--border-color) var(--border-progress), 0, transparent); } @@ -75,11 +75,11 @@ &.active { --border-color: var(--color-purple); - + .index-container { color: var(--color-primary); } - + .title { opacity: 1; } diff --git a/src/components/TransactionBatchProgress/TransactionBatchProgress.scss b/src/components/TransactionBatchProgress/TransactionBatchProgress.scss index 01b39ad3..212ff67b 100644 --- a/src/components/TransactionBatchProgress/TransactionBatchProgress.scss +++ b/src/components/TransactionBatchProgress/TransactionBatchProgress.scss @@ -20,4 +20,4 @@ transition: width 0.3s ease-out; } -} \ No newline at end of file +} diff --git a/src/components/index.ts b/src/components/index.ts index be2b7d34..d8c88e42 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -19,9 +19,12 @@ export { default as PoolSelect } from './PoolSelect/PoolSelect'; export { default as Tooltip } from './Tooltip/Tooltip'; export { default as BigWinnerCard } from './BigWinnerCard/BigWinnerCard'; export { default as Pagination } from './Pagination/Pagination'; +export { default as IbcTransferModal } from './Modals/IbcTransfer/IbcTransfer'; +export { default as QuitDepositModal } from './Modals/QuitDeposit/QuitDeposit'; +export { default as TransactionBatchProgress } from './TransactionBatchProgress/TransactionBatchProgress'; +export { default as DepositIbcTransfer } from './DepositIbcTransfer/DepositIbcTransfer'; export { default as Leaderboard } from './Leaderboard/Leaderboard'; export { default as PurpleBackgroundImage } from './PurpleBackgroundImage/PurpleBackgroundImage'; -export { default as TransactionBatchProgress } from './TransactionBatchProgress/TransactionBatchProgress'; export { default as Tag } from './Tag/Tag'; export * from './ToastContent/ToastContent'; diff --git a/src/constant/navigation.ts b/src/constant/navigation.ts index 76e1f99a..646927ba 100644 --- a/src/constant/navigation.ts +++ b/src/constant/navigation.ts @@ -8,7 +8,11 @@ export const POOLS = '/pools'; export const POOL_DETAILS = '/pools/details'; export const MY_SAVINGS = '/my-savings'; export const WINNERS = '/winners'; +export const DROPS = '/drops'; +export const DROPS_POOLS = '/drops/pools'; +export const DROPS_MY_DEPOSITS = '/drops/my-deposits'; export const DOCUMENTATION = 'https://docs.cosmosmillions.com'; +export const DOCUMENTATION_DROPS = 'https://docs.cosmosmillions.com/cosmos-millions/deposits-and-withdrawals#deposit-drop'; export const FAQ = '#faq'; export const DISCORD = 'https://discord.com/invite/PWHUMdwQ5r'; export const TWITTER = 'https://twitter.com/CosmosMillions'; diff --git a/src/drops/components/CsvFileInput/CsvFileInput.scss b/src/drops/components/CsvFileInput/CsvFileInput.scss new file mode 100644 index 00000000..f546f2c5 --- /dev/null +++ b/src/drops/components/CsvFileInput/CsvFileInput.scss @@ -0,0 +1,42 @@ +@import 'src/styles/main'; + +.file-input-container { + border: 1px solid var(--color-primary-light); + + &.drag-entered { + opacity: 0.8; + } + + &.drag-rejected { + border-color: var(--color-failure); + + .sublabel { + color: var(--color-failure); + font-size: 13px; + font-weight: 300; + } + } + + .label { + text-align: center; + color: var(--color-primary); + font-size: 12px; + line-height: normal; + font-weight: 400; + } + + .sublabel { + text-align: center; + color: var(--color-grey); + line-height: normal; + font-size: 12px; + font-weight: 400; + } + + .icon-container { + width: 38px; + height: 38px; + border-radius: 7px; + background-color: var(--color-white); + } +} diff --git a/src/drops/components/CsvFileInput/CsvFileInput.tsx b/src/drops/components/CsvFileInput/CsvFileInput.tsx new file mode 100644 index 00000000..c0e18ff5 --- /dev/null +++ b/src/drops/components/CsvFileInput/CsvFileInput.tsx @@ -0,0 +1,148 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDropzone, ErrorCode } from 'react-dropzone'; +import Papa from 'papaparse'; +import { LUM_DENOM, MICRO_LUM_DENOM, convertUnit } from '@lum-network/sdk-javascript'; + +import Assets from 'assets'; +import { Card } from 'components'; +import { WalletUtils } from 'utils'; + +import './CsvFileInput.scss'; + +const isValidRow = (info: unknown): info is [string, string] => { + return !!(Array.isArray(info) && info[0] && info[1]); +}; + +interface CsvFileInputProps { + onValidCsv: (depositDrops: { amount: string; winnerAddress: string }[]) => void; + onInvalidCsv: (error: string) => void; + minDepositAmount?: number; + className?: string; + disabled: boolean; + limit: number; +} + +const CsvFileInput = (props: CsvFileInputProps): JSX.Element => { + const { className, minDepositAmount, disabled, limit, onValidCsv, onInvalidCsv } = props; + const { t } = useTranslation(); + + const [innerLabel, setInnerLabel] = useState(t('depositDrops.depositFlow.fileInputLabel.pending')); + const [innerSubLabel, setInnerSubLabel] = useState(t('depositDrops.depositFlow.fileInputSubLabel.pending')); + const [status, setStatus] = useState<'accepted' | 'rejected' | null>(null); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept: { + 'text/csv': ['.csv'], + }, + maxFiles: 1, + maxSize: 20000000, + multiple: false, + disabled, + onDropAccepted: (files) => { + Papa.parse(files[0], { + skipEmptyLines: true, + complete: (results) => { + const drops: { + amount: string; + winnerAddress: string; + }[] = []; + let error = ''; + + const data = results.data; + data.splice(0, 1); + + for (let i = 0; i < data.length; i++) { + const r = data[i]; + + if (!isValidRow(r)) { + error = t('depositDrops.depositFlow.fileInputSubLabel.invalidRow', { row: i + 1 }); + break; + } + + const amount = r[0]; + const winnerAddress = r[1]; + + const amountToNumber = Number(amount); + + if (Number.isNaN(amountToNumber)) { + error = t('depositDrops.depositFlow.fileInputSubLabel.invalidAmount', { row: i + 1 }); + break; + } + + if (amountToNumber < (minDepositAmount || 0)) { + error = t('depositDrops.depositFlow.fileInputSubLabel.lessThanMinDeposit', { row: i + 1 }); + break; + } + + if (!WalletUtils.isAddressValid(winnerAddress, 'lum')) { + error = t('depositDrops.depositFlow.fileInputSubLabel.invalidAddress', { row: i + 1 }); + break; + } + + drops.push({ + amount: convertUnit({ amount: amount, denom: LUM_DENOM }, MICRO_LUM_DENOM), + winnerAddress, + }); + } + + if (error) { + setStatus('rejected'); + setInnerLabel(t('depositDrops.depositFlow.fileInputLabel.pending')); + setInnerSubLabel(error); + onInvalidCsv(error); + } else { + setStatus('accepted'); + setInnerLabel(t('depositDrops.depositFlow.fileInputLabel.success')); + setInnerSubLabel(t('depositDrops.depositFlow.fileInputSubLabel.success', { walletCount: drops.length, batchCount: Math.ceil(drops.length / limit) })); + onValidCsv([...drops]); + } + }, + }); + }, + onDropRejected: (fileRejections) => { + let error = ''; + + switch (fileRejections[0].errors[0].code) { + case ErrorCode.TooManyFiles: { + error = t('depositDrops.depositFlow.fileInputSubLabel.tooManyFileError'); + break; + } + case ErrorCode.FileInvalidType: { + error = t('depositDrops.depositFlow.fileInputSubLabel.fileTypeError'); + break; + } + case ErrorCode.FileTooLarge: { + error = t('depositDrops.depositFlow.fileInputSubLabel.fileTooBigError'); + break; + } + default: { + error = t('depositDrops.depositFlow.fileInputSubLabel.invalidFile'); + break; + } + } + + setStatus('rejected'); + setInnerLabel(t('depositDrops.depositFlow.fileInputLabel.pending')); + setInnerSubLabel(error); + onInvalidCsv(error); + }, + }); + + return ( +
+ +
+ +
+ +
+ + +
+
+
+ ); +}; + +export default CsvFileInput; diff --git a/src/drops/components/DepositDropSteps/DepositDropSteps.scss b/src/drops/components/DepositDropSteps/DepositDropSteps.scss new file mode 100644 index 00000000..098d8497 --- /dev/null +++ b/src/drops/components/DepositDropSteps/DepositDropSteps.scss @@ -0,0 +1,69 @@ +@import 'src/styles/main'; + +#depositDropFlow { + .add-icon, + .remove-icon { + & circle { + fill: var(--color-primary); + } + + & path { + stroke: var(--color-white); + } + } + + .deposit-drop-input-type { + background-color: transparent; + border-right: none; + border-left: none; + border-top: none; + border-bottom: 2px solid var(--color-primary); + color: var(--color-primary); + opacity: 0.2; + transition: opacity 0.15s ease-in-out; + + &.active, + &:hover { + opacity: 1; + } + } + + .deposit-drop-winner-card { + &:not(:first-child) { + margin-bottom: 1rem; + } + + .deposit-drop-address-input, + .deposit-drop-amount-input { + background-color: var(--color-white); + border: none; + border-radius: 8px; + &.error { + border: 1px solid var(--color-failure); + } + &:focus { + outline: none; + } + } + + .deposit-drop-address-input { + color: var(--color-primary); + font-size: 14px; + padding: 15px 12px; + } + + .deposit-drop-amount-input { + color: var(--color-black); + font-size: 18px; + padding: 12px 12px; + } + + label { + color: var(--color-primary); + } + } + + p.error-message { + color: var(--color-failure); + } +} diff --git a/src/drops/components/DepositDropSteps/DepositDropSteps.tsx b/src/drops/components/DepositDropSteps/DepositDropSteps.tsx new file mode 100644 index 00000000..a90aef90 --- /dev/null +++ b/src/drops/components/DepositDropSteps/DepositDropSteps.tsx @@ -0,0 +1,523 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { FormikProps } from 'formik'; +import numeral from 'numeral'; +import { Coin, LUM_DENOM, MICRO_LUM_DENOM, convertUnit } from '@lum-network/sdk-javascript'; +import { DepositState } from '@lum-network/sdk-javascript/build/codegen/lum/network/millions/deposit'; + +import Assets from 'assets'; +import { Button, Card, TransactionBatchProgress, SmallerDecimal, Tooltip, DepositIbcTransfer } from 'components'; +import { NavigationConstants } from 'constant'; +import { LumWalletModel, OtherWalletModel, PoolModel } from 'models'; +import { DenomsUtils, I18n, NumbersUtils, WalletUtils } from 'utils'; +import { RootState } from 'redux/store'; + +import CsvFileInput from '../CsvFileInput/CsvFileInput'; + +import './DepositDropSteps.scss'; + +interface StepProps { + currentPool: PoolModel; + balances: Coin[]; + price: number; + pools: PoolModel[]; + title: string; + subtitle?: string; + disabled: boolean; +} + +interface Props { + currentPool: PoolModel; + pools: PoolModel[]; + currentStep: number; + steps: { + title: string; + subtitle: string; + cardTitle?: string; + cardSubtitle?: string; + }[]; + otherWallets: { + [denom: string]: OtherWalletModel | undefined; + }; + onNextStep: () => void; + onDepositDrop: (pool: PoolModel, deposits: { amount: string; winnerAddress: string }[], onDepositCallback: (batchNum: number) => void, startIndex: number) => Promise; + onFinishDeposit: (callback: () => void) => void; + onTwitterShare: () => void; + lumWallet: LumWalletModel | null; + transferForm: FormikProps<{ amount: string }>; + price: number; + limit: number; +} + +type TxInfos = { + depositorAddress: string; + amount: string; + denom: string; + tvl: string; + poolId: string; + numOfWallets: number; +}; + +type ManualInput = { + winnerAddress: string; + amount: string; + errors: Record; +}; + +type DepositDrop = { + winnerAddress: string; + amount: string; +}; + +const DepositDropStep = ( + props: StepProps & { + onDepositDrop: (pool: PoolModel, deposits: { amount: string; winnerAddress: string }[], onDepositCallback: (batchNum: number) => void, startIndex: number) => Promise; + limit: number; + }, +) => { + const { currentPool, balances, title, disabled, limit, onDepositDrop } = props; + + const [inputType, setInputType] = useState<'csv' | 'manual'>('csv'); + + const [manualInputs, setManualInputs] = useState([]); + const [csvError, setCsvError] = useState(''); + + const [batch, setBatch] = useState(0); + const [batchTotal, setBatchTotal] = useState(1); + const [totalDepositAmount, setTotalDepositAmount] = useState(0); + + const [depositDrops, setDepositDrops] = useState([]); + + const isLoading = useSelector((state: RootState) => state.loading.effects.wallet.depositDrop); + + const validateInputs = (inputs: { amount: string; winnerAddress: string; errors: Record }[]) => { + let isValid = true; + + for (const input of inputs) { + const amountToNumber = Number(input.amount); + const minDeposit = NumbersUtils.convertUnitNumber(currentPool.minDepositAmount); + + if (Object.keys(input.errors).length > 0 || !WalletUtils.isAddressValid(input.winnerAddress) || Number.isNaN(amountToNumber) || amountToNumber < minDeposit) { + isValid = false; + } + } + + return isValid; + }; + + const onInvalidCsv = useCallback((error: string) => { + setCsvError(error); + }, []); + + const onValidCsv = useCallback((depositDrops: { amount: string; winnerAddress: string }[]) => { + setDepositDrops([...depositDrops]); + setTotalDepositAmount(depositDrops.reduce((acc, drop) => NumbersUtils.convertUnitNumber(drop.amount) + acc, 0)); + setCsvError(''); + }, []); + + const onAddressInputChange = (text: string, index: number) => { + const newInputs = [...manualInputs]; + const input = newInputs[index]; + + if (!input) { + return; + } + + input.winnerAddress = text; + + if (!WalletUtils.isAddressValid(text)) { + input.errors = { + winnerAddress: I18n.t('errors.generic.invalid', { field: 'lum address' }), + }; + } else { + delete input.errors.winnerAddress; + } + + setManualInputs([...newInputs]); + }; + + const onAmountInputChange = (text: string, index: number) => { + const newInputs = [...manualInputs]; + const input = newInputs[index]; + + if (!input) { + return; + } + + input.amount = text; + + const amountToNumber = Number(text); + const maxAmount = NumbersUtils.convertUnitNumber(balances.find((bal) => bal.denom === currentPool.nativeDenom)?.amount || '0'); + const minDeposit = NumbersUtils.convertUnitNumber(currentPool.minDepositAmount); + + if (Number.isNaN(amountToNumber)) { + input.errors = { + amount: I18n.t('errors.generic.invalid', { field: 'amount' }), + }; + } else if (amountToNumber < minDeposit) { + input.errors = { + amount: I18n.t('errors.deposit.lessThanMinDeposit', { minDeposit, denom: DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase() }), + }; + } else if (!Number.isNaN(maxAmount) && amountToNumber > maxAmount) { + input.errors = { + amount: I18n.t('errors.deposit.greaterThanBalance'), + }; + } else { + delete input.errors.amount; + } + + setManualInputs([...newInputs]); + + setTimeout(() => { + if (Object.keys(input.errors).length === 0) { + setTotalDepositAmount(manualInputs.reduce((acc, drop) => Number(drop.amount) + acc, 0)); + } + }, 500); + }; + + useEffect(() => { + setDepositDrops([]); + setTotalDepositAmount(0); + setCsvError(''); + setBatch(0); + setManualInputs( + inputType === 'manual' + ? [ + { + winnerAddress: '', + amount: '', + errors: {}, + }, + ] + : [], + ); + }, [inputType]); + + return ( +
+
+
+
+
+
+ + +
+
+ {inputType === 'csv' ? ( + <> + + + + ) : ( + <> + {manualInputs.map((input, index) => { + return ( + +
+ + onAddressInputChange(event.target.value, index)} + /> + {input.errors.winnerAddress ?

{input.errors.winnerAddress}

: null} +
+
+ + 0 ? NumbersUtils.convertUnitNumber(balances.find((bal) => bal.denom === currentPool.nativeDenom)?.amount || '0') : '0'} + value={manualInputs[index].amount || ''} + onChange={(event) => onAmountInputChange(event.target.value, index)} + /> + {input.errors.amount ?

{input.errors.amount}

: null} +
+ +
+ ); + })} + {manualInputs.length < 5 && ( + + )} + + )} +
+
+ + +
+ +
+ denom + {DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase()} +
+
+ +
+
+
+ + + info + + + {I18n.t('deposit.feesWarning')} + + {batchTotal > 1 && } + +
+ ); +}; + +const ShareStep = ({ txInfos, price, title, subtitle, onTwitterShare }: { txInfos: TxInfos; title: string; subtitle: string; price: number; onTwitterShare: () => void }) => { + const navigate = useNavigate(); + + return ( +
+
+
+
+
+
+
+
+ {txInfos.denom} +
+
+ {txInfos.amount} {DenomsUtils.getNormalDenom(txInfos.denom).toUpperCase()} +
+ + {numeral(txInfos.amount).multiply(price).format('$0,0[.]00')} - {I18n.t('pools.poolId', { poolId: txInfos.poolId })} - {txInfos.numOfWallets} Wallets + +
+
+
{I18n.t('mySavings.depositStates', { returnObjects: true })[DepositState.DEPOSIT_STATE_SUCCESS]}
+
+
+
+ { + window.open(`${NavigationConstants.MINTSCAN}/address/${txInfos.depositorAddress}`, '_blank'); + }} + > + Mintscan + {I18n.t('deposit.seeOnMintscan')} + +
+
+ { + navigate(NavigationConstants.DROPS_MY_DEPOSITS); + }} + > + My Deposit Drops + {I18n.t('depositDrops.depositFlow.shareStepCard.goToMyDrops')} + +
+
+ +
+
+ ); +}; + +const DepositDropSteps = (props: Props) => { + const { currentStep, steps, otherWallets, price, pools, currentPool, limit, onNextStep, onDepositDrop, onFinishDeposit, onTwitterShare, transferForm, lumWallet } = props; + const [txInfos, setTxInfos] = useState(null); + const [otherWallet, setOtherWallet] = useState(otherWallets[DenomsUtils.getNormalDenom(currentPool.nativeDenom)]); + const [nonEmptyWallets, setNonEmptyWallets] = useState( + Object.values(otherWallets).filter((otherWallet): otherWallet is OtherWalletModel => !!(otherWallet && otherWallet.balances.length > 0 && Number(otherWallet.balances[0].amount) > 0)), + ); + + useEffect(() => { + setOtherWallet(otherWallets[DenomsUtils.getNormalDenom(currentPool.nativeDenom)]); + setNonEmptyWallets( + Object.values(otherWallets).filter((otherWallet): otherWallet is OtherWalletModel => !!(otherWallet && otherWallet.balances.length > 0 && Number(otherWallet.balances[0].amount) > 0)), + ); + }, [otherWallets, currentPool]); + + return ( +
+
+ {currentStep === 0 && currentPool.nativeDenom !== MICRO_LUM_DENOM && ( + + )} + {((currentStep === 1 && currentPool.nativeDenom !== MICRO_LUM_DENOM) || (currentStep === 0 && currentPool.nativeDenom === MICRO_LUM_DENOM)) && ( + { + const totalDepositAmount = deposits.reduce((acc, deposit) => acc + NumbersUtils.convertUnitNumber(deposit.amount), 0); + const res = await onDepositDrop(pool, deposits, callback, startIndex); + + if (res) { + onFinishDeposit(onNextStep); + setTxInfos({ + amount: numeral(totalDepositAmount).format('0,0[.]00'), + denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase(), + tvl: numeral(NumbersUtils.convertUnitNumber(pool.tvlAmount) + totalDepositAmount).format('0,0'), + poolId: pool.poolId.toString(), + numOfWallets: deposits.length, + depositorAddress: lumWallet?.address || '', + }); + } + + return res; + }} + currentPool={currentPool} + pools={pools} + price={price} + limit={limit} + /> + )} + {currentStep === steps.length && txInfos && ( + + )} +
+
+ ); +}; + +export default DepositDropSteps; diff --git a/src/drops/components/DepositDropsCard/DepositDropsCard.scss b/src/drops/components/DepositDropsCard/DepositDropsCard.scss new file mode 100644 index 00000000..6df5a3e0 --- /dev/null +++ b/src/drops/components/DepositDropsCard/DepositDropsCard.scss @@ -0,0 +1,19 @@ +@import 'src/styles/main'; + +.drops-card { + .drops-description { + a, a:hover, a:focus, a:active { + color: var(--color-primary); + } + } + + .cta-drops { + width: 270px; + } + + .cosmonaut-flying { + @include media-breakpoint-down(xl) { + margin-top: -50px; + } + } +} diff --git a/src/drops/components/DepositDropsCard/DepositDropsCard.tsx b/src/drops/components/DepositDropsCard/DepositDropsCard.tsx new file mode 100644 index 00000000..5160d42b --- /dev/null +++ b/src/drops/components/DepositDropsCard/DepositDropsCard.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Button, Card } from 'components'; +import { I18n, WalletProvidersUtils } from 'utils'; +import Assets from 'assets'; +import { NavigationConstants } from 'constant'; +import { useSelector } from 'react-redux'; +import { RootState } from 'redux/store'; + +import './DepositDropsCard.scss'; + +interface IProps { + cta: string; + link: string; + className?: string; +} + +const DepositDropsCard = ({ className, cta, link }: IProps): JSX.Element => { + const lumWallet = useSelector((state: RootState) => state.wallet.lumWallet); + + return ( + +
+ cosmonaut flying +
+

{I18n.t('depositDrops.card.title')}

+

+

+ +
+
+ ); +}; + +export default DepositDropsCard; diff --git a/src/drops/components/index.ts b/src/drops/components/index.ts new file mode 100644 index 00000000..a7e71195 --- /dev/null +++ b/src/drops/components/index.ts @@ -0,0 +1,2 @@ +export { default as DepositDropsCard } from './DepositDropsCard/DepositDropsCard'; +export { default as DepositDropSteps } from './DepositDropSteps/DepositDropSteps'; diff --git a/src/drops/pages/MyDeposits/MyDeposits.scss b/src/drops/pages/MyDeposits/MyDeposits.scss new file mode 100644 index 00000000..fafb2327 --- /dev/null +++ b/src/drops/pages/MyDeposits/MyDeposits.scss @@ -0,0 +1,72 @@ +@import 'src/styles/main'; + +.drops-my-deposits { + .drops-card { + background-color: var(--color-primary-light); + border-radius: 8px; + &:not(:last-child) { + margin-bottom: 24px; + } + + .deposit-drop-title { + font-size: 18px; + color: var(--color-purple); + font-weight: 400; + } + + .deposit-drop-date { + color: var(--color-black); + font-size: 12px; + font-style: normal; + font-weight: 300; + line-height: normal; + text-transform: uppercase; + } + + .deposit-drop-amount { + font-size: 18px; + font-weight: 400; + color: var(--color-black); + } + + .deposit-drop-amount-denom { + font-size: 18px; + font-weight: 400; + color: var(--color-purple); + text-transform: uppercase; + line-height: 18px; + } + + .deposit-drop-usd-amount { + font-weight: 300; + font-size: 12px; + color: var(--color-dark-grey); + } + + .wallets-count { + color: var(--color-black); + } + } + + .deposit-drop-state { + width: fit-content; + padding: 7px 14px; + + box-shadow: 0 4px 23px #f1edff; + + background: #8c8c8c; + color: white; + + font-size: 12px; + &.success { + background: var(--color-success); + } + + &.failure { + background: var(--color-failure); + } + &.pending { + font-size: 10px; + } + } +} diff --git a/src/drops/pages/MyDeposits/MyDeposits.tsx b/src/drops/pages/MyDeposits/MyDeposits.tsx new file mode 100644 index 00000000..03ae79b3 --- /dev/null +++ b/src/drops/pages/MyDeposits/MyDeposits.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { useSelector } from 'react-redux'; +import dayjs from 'dayjs'; +import numeral from 'numeral'; + +import Assets from 'assets'; + +import { Button, Card, Modal, SmallerDecimal } from 'components'; +import { NavigationConstants } from 'constant'; +import { DepositDropsCard } from 'drops/components'; +import { DenomsUtils, I18n, NumbersUtils } from 'utils'; +import { RootState } from 'redux/store'; +import { AggregatedDepositModel, DepositModel } from 'models'; + +import DropsDetailsModal from './components/Modals/DropsDetailsModal/DropsDetailsModal'; +import EditDepositModal from './components/Modals/EditDepositModal/EditDepositModal'; +import CancelDropModal from './components/Modals/CancelDropModal/CancelDropModal'; + +import './MyDeposits.scss'; + +const MyDeposits = () => { + const [selectedDeposit, setSelectedDeposit] = useState(null); + const [selectedDepositDrop, setSelectedDepositDrop] = useState(null); + const dropsDetailsModalRef = useRef>(null); + + const { lumWallet, depositDrops, prices } = useSelector((state: RootState) => ({ + lumWallet: state.wallet.lumWallet, + depositDrops: state.wallet.lumWallet?.depositDrops, + prices: state.stats.prices, + })); + + useEffect(() => { + const handler = () => { + setSelectedDeposit(null); + }; + + const cancelDropModal = document.getElementById('cancelDropModal'); + const editDepositModal = document.getElementById('editDepositModal'); + + if (cancelDropModal) { + cancelDropModal.addEventListener('hidden.bs.modal', handler); + } + + if (editDepositModal) { + editDepositModal.addEventListener('hidden.bs.modal', handler); + } + + return () => { + if (cancelDropModal) { + cancelDropModal.removeEventListener('hidden.bs.modal', handler); + } + + if (editDepositModal) { + editDepositModal.removeEventListener('hidden.bs.modal', handler); + } + }; + }, []); + + const onAction = (deposit: DepositModel) => { + setSelectedDeposit(deposit); + }; + + const renderDepositDrop = (drop: AggregatedDepositModel, index: number) => { + const usdPrice = NumbersUtils.convertUnitNumber(drop.amount?.amount || '0') * prices[DenomsUtils.getNormalDenom(drop.amount?.denom || '')] || 0; + + return ( +
+
+
+
+ deposit drop +
+ {I18n.t('depositDrops.myDeposits.depositDrops', { count: drop.deposits.length })} + {dayjs(drop.createdAt).format('ll')} +
+
+
+
+
+
+ + + +   + {DenomsUtils.getNormalDenom(drop.amount?.denom || '')} +
+ {numeral(usdPrice).format('$0,0.00')} +
+
+
+ {drop.deposits.length} {I18n.t('depositDrops.myDeposits.wallet', { count: drop.deposits.length })} +
+
+
+ {I18n.t('depositDrops.myDeposits.activeSince', { count: dayjs().diff(dayjs(drop.createdAt), 'day') })} +
+
+
+ +
+
+
+ ); + }; + + return ( +
+

{I18n.t('depositDrops.myDeposits.title')}

+ {depositDrops && depositDrops.length ? {depositDrops.map((drop, index) => renderDepositDrop(drop, index))} : null} + + + + +
+ ); +}; + +export default MyDeposits; diff --git a/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.scss b/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.scss new file mode 100644 index 00000000..666480b0 --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.scss @@ -0,0 +1,20 @@ +@import 'src/styles/main'; + +#cancelDropModal { + .card-step-subtitle { + a { + color: var(--color-grey); + } + } + .warnings { + p { + color: var(--color-failure); + font-size: 16px; + } + + .app-card { + background-color: var(--color-failure-light); + font-size: 14px; + } + } +} diff --git a/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.tsx b/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.tsx new file mode 100644 index 00000000..dc125fde --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/CancelDropModal/CancelDropModal.tsx @@ -0,0 +1,150 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import Assets from 'assets'; +import { Button, Card, Modal, TransactionBatchProgress, SmallerDecimal, Steps, Tooltip } from 'components'; +import { DenomsUtils, I18n, NumbersUtils, WalletUtils } from 'utils'; +import { Dispatch, RootState } from 'redux/store'; +import { DepositModel } from 'models'; + +import './CancelDropModal.scss'; + +const CancelDropModal = ({ deposits, limit }: { deposits?: DepositModel[]; limit: number }) => { + const [currentStep, setCurrentStep] = useState(1); + const [batch, setBatch] = useState(0); + const [batchTotal, setBatchTotal] = useState(1); + + const modalRef = useRef>(null); + + const isLoading = useSelector((state: RootState) => state.loading.effects.wallet.cancelDrop); + const pool = useSelector((state: RootState) => state.pools.pools.find((p) => (deposits && deposits.length > 0 ? p.poolId === deposits[0]?.poolId : undefined))); + + const dispatch = useDispatch(); + const steps = I18n.t('depositDrops.cancelDropModal.steps', { + returnObjects: true, + provider: WalletUtils.getAutoconnectProvider(), + }); + + const depositsTotalAmount = deposits ? deposits.reduce((acc, deposit) => NumbersUtils.convertUnitNumber(deposit.amount?.amount || '0') + acc, 0) : 0; + + useEffect(() => { + const handler = () => { + setCurrentStep(1); + }; + + const cancelDropModal = document.getElementById('cancelDropModal'); + + if (cancelDropModal) { + cancelDropModal.addEventListener('hidden.bs.modal', handler); + } + + return () => { + if (cancelDropModal) { + cancelDropModal.removeEventListener('hidden.bs.modal', handler); + } + }; + }, []); + + const onCancelDrop = async () => { + if (!pool || !deposits || deposits.length === 0) { + return; + } + + const batchCount = Math.ceil(deposits.length / limit); + + setBatchTotal(batchCount); + + const res = await dispatch.wallet.cancelDrop({ + deposits, + pool, + onCancelCallback: (index) => setBatch(index), + startIndex: batch, + batchCount, + limit, + }); + + if (!res) { + setCurrentStep(1); + } else { + setCurrentStep(currentStep + 1); + if (modalRef.current) { + modalRef.current.hide(); + } + } + }; + + return ( + +
+
+

{I18n.t('depositDrops.cancelDropModal.title')}

+ +
+
+ +
+
+
+
+
+
+
+

{I18n.t('mySavings.leavePoolModal.warnings.title')}

+ + {I18n.t('mySavings.leavePoolModal.warnings.draws')} + + + {I18n.t('mySavings.leavePoolModal.warnings.cancel')} + + + {I18n.t('mySavings.leavePoolModal.warnings.waiting', { unbondingTime: pool?.internalInfos?.unbondingTime || 21 })} + +
+
+ +
+ denom + + {DenomsUtils.getNormalDenom(deposits && deposits[0] && deposits[0].amount ? deposits[0]?.amount.denom : '').toUpperCase()} + +
+
+ +
+
+
+
+ + + info + + + {I18n.t('deposit.feesWarning')} + + {batch > 0 && batchTotal > 1 && } + +
+
+
+ +
+
+ + ); +}; + +export default CancelDropModal; diff --git a/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.scss b/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.scss new file mode 100644 index 00000000..112d3692 --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.scss @@ -0,0 +1,94 @@ +@import 'src/styles/main'; + +#dropsDetailsModal { + h4 { + color: var(--color-black); + } + + .next-pool-info { + color: var(--color-black); + font-weight: 400; + } + + .date { + font-size: 12px; + font-weight: 300; + color: var(--color-muted); + } + + .redelegated-prizes-card { + .prize-remaining-amount { + color: var(--color-primary); + } + } + + .index-container { + background-color: var(--color-primary-light); + padding: 0 8px; + color: var(--color-primary); + border-radius: 7px; + font-size: 14px; + width: max-content; + height: 32px; + } + + .draw-winners-table-container { + .draw-winners-table { + .table tbody tr td { + padding: 25px 32px; + + .tx-icon-container { + img { + width: 32px; + height: 32px; + object-fit: contain; + } + } + + .winner-address { + color: var(--color-black); + font-size: 14px; + font-weight: 400; + } + + .tx-amount { + .amount { + color: var(--color-primary); + } + + .usd-price { + color: var(--color-black); + } + } + + .usd-price { + font-size: 12px; + color: var(--color-black); + font-weight: 300; + } + } + } + + .pagination { + .page-item .page-link { + background-color: var(--color-background) !important; + border-color: var(--color-background) !important; + } + .page-item.active:not(.without-border) .page-link { + border-color: var(--color-black) !important; + } + } + + .cancel-icon { + path { + fill: var(--color-primary); + } + } + + .edit-icon { + path { + fill: var(--color-background); + } + } + } +} diff --git a/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.tsx b/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.tsx new file mode 100644 index 00000000..f2d22f7b --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/DropsDetailsModal/DropsDetailsModal.tsx @@ -0,0 +1,156 @@ +/* eslint-disable max-len */ + +import React, { useEffect, useState } from 'react'; +import dayjs from 'dayjs'; +import numeral from 'numeral'; + +import Assets from 'assets'; +import { Modal, SmallerDecimal, Table, Button } from 'components'; +import { ModalHandlers } from 'components/Modal/Modal'; +import { DenomsUtils, I18n, NumbersUtils, StringsUtils } from 'utils'; +import { AggregatedDepositModel, DepositModel } from 'models'; + +import './DropsDetailsModal.scss'; + +interface Props { + drops: AggregatedDepositModel | null; + poolDenom: string; + prices: { [key: string]: number }; + modalRef: React.RefObject; + onEdit: (deposit: DepositModel) => void; + onCancel?: (deposit: DepositModel) => void; +} + +const DropsDetails = ({ drops, poolDenom, prices, modalRef, onCancel, onEdit }: Props) => { + const [dropsPage, setDropsPage] = useState(1); + + useEffect(() => { + const handler = () => { + setDropsPage(1); + }; + + const dropsDetailsModal = document.getElementById('dropsDetailsModal'); + + if (dropsDetailsModal) { + dropsDetailsModal.addEventListener('hidden.bs.modal', handler); + } + + return () => { + if (dropsDetailsModal) { + dropsDetailsModal.removeEventListener('hidden.bs.modal', handler); + } + }; + }, []); + + return ( + + {drops ? ( +
+
+ {poolDenom} +

{I18n.t('depositDrops.card.title')}

+
+
+
+
+

{I18n.t('common.pool')}

+
#{drops.poolId?.toString()}
+
+
+

{I18n.t('common.deposit')}

+
#{drops.depositId?.toString()}
+
+
+
{dayjs(drops.createdAt).format('ll')}
+
+
+ setDropsPage(page)} + pagination={ + drops.deposits.length > 5 + ? { + pagesTotal: Math.ceil(drops.deposits.length / 5), + hasNextPage: dropsPage < Math.ceil(drops.deposits.length / 5), + hasPreviousPage: dropsPage > 1, + page: dropsPage, + } + : undefined + } + > + {drops.deposits.slice((dropsPage - 1) * 5, dropsPage * 5).map((drop, index) => ( + + + + + + ))} +
+
+
+ drops +
+ {StringsUtils.trunc(drop.winnerAddress || '')} +
+
+
+
{numeral(NumbersUtils.convertUnitNumber(drop.amount?.amount || 0) * (prices[poolDenom] || 0)).format('$0,0[.]00')}
+ + {poolDenom.toUpperCase()} + +
+
+
+ + +
+
+ +
+
+ ) : null} +
+ ); +}; + +export default DropsDetails; diff --git a/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.scss b/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.scss new file mode 100644 index 00000000..52aa4f2d --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.scss @@ -0,0 +1,35 @@ +@import 'src/styles/main'; + +#editDepositModal { + .edit-address-input, + .edit-amount-input { + background-color: var(--color-white); + border: none; + border-radius: 8px; + &.error { + border: 1px solid var(--color-failure); + } + &:focus { + outline: none; + } + } + + .edit-address-input { + color: var(--color-primary); + font-size: 14px; + padding: 15px 12px; + } + + .edit-amount-input { + font-size: 18px; + padding: 12px 12px; + } + + label { + color: var(--color-primary); + } + + p.error-message { + color: var(--color-failure); + } +} diff --git a/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.tsx b/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.tsx new file mode 100644 index 00000000..e3bbe217 --- /dev/null +++ b/src/drops/pages/MyDeposits/components/Modals/EditDepositModal/EditDepositModal.tsx @@ -0,0 +1,149 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; + +import Assets from 'assets'; + +import { Button, Card, Modal, Steps, Tooltip } from 'components'; +import { DepositModel } from 'models'; +import { RootState, Dispatch } from 'redux/store'; +import { I18n, NumbersUtils, WalletUtils } from 'utils'; + +import './EditDepositModal.scss'; + +const EditDepositModal = ({ deposit }: { deposit: DepositModel | null }) => { + const [currentStep, setCurrentStep] = useState(0); + const [address, setAddress] = useState(deposit ? deposit.winnerAddress : ''); + const [addressError, setAddressError] = useState(''); + const modalRef = useRef>(null); + + const isLoading = useSelector((state: RootState) => state.loading.effects.wallet.editDrop); + const pool = useSelector((state: RootState) => state.pools.pools.find((p) => (deposit ? p.poolId === deposit.poolId : undefined))); + + const dispatch = useDispatch(); + const steps = I18n.t('depositDrops.editDropModal.steps', { + returnObjects: true, + provider: WalletUtils.getAutoconnectProvider(), + }); + + useEffect(() => { + const handler = () => { + setCurrentStep(0); + }; + + const editDepositModal = document.getElementById('editDepositModal'); + + if (editDepositModal) { + editDepositModal.addEventListener('hidden.bs.modal', handler); + } + + return () => { + if (editDepositModal) { + editDepositModal.removeEventListener('hidden.bs.modal', handler); + } + }; + }, []); + + useEffect(() => { + if (deposit) { + setAddress(deposit.winnerAddress); + } + }, [deposit]); + + const onAddressChange = (newAddress: string) => { + setAddress(newAddress); + + if (newAddress && !WalletUtils.isAddressValid(newAddress)) { + setAddressError(I18n.t('errors.generic.invalid', { field: 'lum address' })); + } else if (newAddress === deposit?.winnerAddress) { + setAddressError(I18n.t('depositDrops.editDropModal.sameAddressError')); + } else { + setAddressError(''); + } + }; + + const onEditDeposit = async () => { + if (!deposit || !pool || !address || address === deposit?.winnerAddress) { + return null; + } + + setCurrentStep(currentStep + 1); + + const res = await dispatch.wallet.editDrop({ + pool, + deposit, + newWinnerAddress: address, + }); + + if (!res || (res && res.error)) { + setCurrentStep(0); + } else { + setCurrentStep(currentStep + 1); + if (modalRef.current) { + modalRef.current.hide(); + } + } + }; + + return ( + +
+
+

{I18n.t('depositDrops.editDropModal.title')}

+ +
+
+ +
+
+
+
+
+
+
+ +
+ + onAddressChange(event.target.value)} + /> + {addressError ?

{addressError}

: null} +
+
+ + +
+
+
+
+ + + info + + + {I18n.t('deposit.feesWarning')} + + +
+
+
+ +
+
+ + ); +}; + +export default EditDepositModal; diff --git a/src/drops/pages/Pools/Pools.tsx b/src/drops/pages/Pools/Pools.tsx new file mode 100644 index 00000000..c20a447b --- /dev/null +++ b/src/drops/pages/Pools/Pools.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { NavigationConstants } from 'constant'; +import { DenomsUtils, I18n, NumbersUtils } from 'utils'; +import Assets from 'assets'; +import { DepositDropsCard } from 'drops/components'; +import { useSelector } from 'react-redux'; +import { RootState } from 'redux/store'; +import PoolCard from 'pages/Pools/components/PoolCard'; +import PoolCardPlaceholder from 'pages/Pools/components/PoolCardPlaceholder'; + +const placeholderNames = ['Mad Scientist ?', 'Lucky Star ?']; + +interface IProps {} + +const Pools = ({}: IProps): JSX.Element => { + const pools = useSelector((state: RootState) => state.pools.pools); + + const poolsPlaceholders = []; + + if (pools.length < 3) { + poolsPlaceholders.push(...new Array(3 - pools.length).fill({})); + } + + return ( +
+
+ Gift +

{I18n.t('depositDrops.pools.title')}

+
+ +
+ {pools.map((pool, index) => ( +
+ +
+ ))} + {poolsPlaceholders.map((_, index) => ( +
+ +
+ ))} +
+
+ ); +}; + +export default Pools; diff --git a/src/drops/pages/index.ts b/src/drops/pages/index.ts new file mode 100644 index 00000000..cd862b5f --- /dev/null +++ b/src/drops/pages/index.ts @@ -0,0 +1,2 @@ +export { default as DropsPoolsPage } from './Pools/Pools'; +export { default as DropsMyDepositsPage } from './MyDeposits/MyDeposits'; diff --git a/src/layout/MainLayout/MainLayout.tsx b/src/layout/MainLayout/MainLayout.tsx index 6abcde22..ae35e838 100644 --- a/src/layout/MainLayout/MainLayout.tsx +++ b/src/layout/MainLayout/MainLayout.tsx @@ -83,6 +83,9 @@ const MainLayout = () => { if (wallet && (location.pathname === NavigationConstants.HOME || location.pathname === NavigationConstants.POOLS || location.pathname === NavigationConstants.MY_SAVINGS)) { dispatch.wallet.reloadWalletInfos({ address: wallet.address, force: false }); } + if (wallet && location.pathname.includes(NavigationConstants.DROPS)) { + dispatch.wallet.reloadWalletInfos({ address: wallet.address, drops: true, force: false }); + } } }, [visibilityState, location.pathname]); diff --git a/src/locales/en.ts b/src/locales/en.ts index a487e451..8e8da6e4 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -69,7 +69,7 @@ export default { }, deposit: { lessThanZero: 'Amount must be greater than 0', - lessThanMinDeposit: 'Amount must be equal to or greather than {{ minDeposit }} {{ denom }}', + lessThanMinDeposit: 'Amount must be equal to or greater than {{ minDeposit }} {{ denom }}', greaterThanBalance: 'Amount must be less than available balance', fees: 'Not enough LUM to pay fees', generic: 'Failed to deposit to {{ denom }} pool', @@ -93,6 +93,10 @@ export default { claimPrize: 'Successfully claimed prizes', claimAndCompound: 'Successfully compounded prizes', logOut: 'You have been logged out.', + multiDeposit: 'Successfully made {{ count }} deposits', + cancelDrop: 'Successfully cancelled your deposit drop', + cancelDropMulti: 'Successfully cancelled your deposit drops', + editDrop: 'Successfully edited your deposit drop', withdrawalRetry: 'Successfully retried withdrawal #{{ withdrawalId }} to pool #{{ poolId }}', }, pending: { @@ -103,6 +107,10 @@ export default { claimPrizeBatch: 'Claiming prizes batch {{ count }}/{{ total }}...', claimAndCompound: 'Compounding prizes...', withdrawalRetry: 'Retrying withdrawal #{{ withdrawalId }} to pool #{{ poolId }}', + multiDeposit: 'Depositing batch {{ index }}/{{ count }}...', + cancelDrop: 'Cancelling deposit drops...', + cancelDropMulti: 'Cancelling deposit drops batch {{ index }}/{{ count }}...', + editDrop: 'Editing your deposit drop...', }, landing: { howItWorks: 'How it Works', @@ -582,4 +590,118 @@ export default { depositBtn: 'Deposit {{ amount }} {{ denom }} to take his place', newRanking: 'You new ranking will be displayed in a few minutes', }, + depositDrops: { + myDeposits: { + title: 'My Deposit Drops', + depositDrops_one: 'Deposit Drop', + depositDrops_other: 'Deposit Drops', + wallet_one: 'Wallet', + wallet_other: 'Wallets', + activeSince_zero: 'Active since today', + activeSince_one: 'Active since {{ count }} day', + activeSince_other: 'Active since {{ count }} days', + seeAll: 'See all', + }, + pools: { + title: 'Deposit Drops Tool', + ctaText: 'Deposit drop', + }, + card: { + title: 'Deposit Drops', + description: + 'By making a deposit drop, you\'re boosting the winning chances of the recipient address, all while maintaining control over your deposit.
Read the docs', + ctaFromPools: 'My Deposit drops', + ctaFromDeposits: 'New Deposit drop', + }, + depositFlow: { + csv: 'CSV', + manual: 'Manual', + downloadTemplate: 'Download our CSV template', + addWinner: 'Add Another Winner', + removeWinner: 'Delete This Winner', + cta: 'Deposit Drop', + winnerAddress: 'Winner Address', + amount: 'Amount', + steps: [ + { + title: 'Transfer {{ denom }} to Lum Network', + subtitle: 'Transfer your {{ denom }} from {{ chainName }} to Lum Network', + }, + { + title: 'Make your Deposit Drop to the {{ denom }} Pool', + subtitle: "Boost someone 's day!", + cardTitle: 'Deposit Drop into {{ denom }} Pool', + }, + ], + fileInputLabel: { + pending: 'Click or drag & drop to upload', + success: 'Your CSV has been uploaded', + }, + depositLabel: 'Total amount of {{ denom }} to deposit in the Pool', + batch: 'Transactions batch {{ count }}/{{ total }}', + batchTooltip: '', + fileInputSubLabel: { + pending: 'Only CSV file of 20 Mb max', + tooManyFileError: 'Too many files uploaded, we support only one file at a time.', + fileTooBigError: 'File is too large, we support only 20 Mb CSV files', + fileTypeError: 'Only CSV files are supported, try another file', + invalidFile: 'This CSV file is invalid. Please use our template below.', + invalidRow: 'The row {{ row }} of your CSV file is invalid. Please use our template below.', + invalidAddress: 'The address provided at row {{ row }} in your CSV is invalid, please check that everything is valid before processing', + invalidAmount: 'The amount provided at row {{ row }} is invalid, please check your CSV entries', + lessThanMinDeposit: 'The amount provided at row {{ row }} is less than the minimum deposit amount, please check your CSV entries', + success: '$t(depositDrops.depositFlow.fileInputSubLabel.uniqueWallets, { "count": {{walletCount}} }) will receive a deposit drop.\nYou will have to sign $t(depositDrops.depositFlow.fileInputSubLabel.transactions, { "count": {{batchCount}} })', + uniqueWallets_one: '{{ count }} unique wallet', + uniqueWallets_other: '{{ count }} unique wallets', + transactions_one: '{{ count }} transaction', + transactions_other: '{{ count }} transactions', + }, + manualInputsErrors: { + greaterThanAvailable: 'Total deposits amount is greater than your available balance', + }, + infoCards: { + deposit: { + content: 'Tip:\nYour deposit operates on a no-loss principle and you can withdraw it whenever you like.', + howItWorks: 'See how it works', + }, + }, + shareTwitterContent: 'Boosted a Cosmonaut @cosmosmillions savings with a deposit drop! 🎁 🏆 🚀\n\nJoin us and help skyrocket our pool of {{ tvl }} {{ denom }} and stand a chance to win hundreds of prizes every week! 🧑‍🚀 #Cosmos https://cosmosmillions.com' , + shareStepCard: { + goToMyDrops: 'Go to\nMy Deposit Drops' + } + }, + cancelDropModal: { + title: 'Is it time to end the boosted chances, Cosmonaut?', + steps: [ + { + title: 'Choose the pool you want to leave', + subtitle: 'Redeem your savings', + }, + { + title: 'Select the deposit drop to redeem', + subtitle: 'Select the savings you want to redeem and accept the transaction on your {{ provider }} wallet', + cardTitle: 'Cancel Deposit Drop', + cardSubtitle: 'Redeem your savings is submitted to an unbonding period', + }, + ], + cta: 'Cancel Deposit Drop', + }, + editDropModal: { + cta: 'Edit Deposit', + title: 'Need to change the lucky winner for your Deposit Drop ?', + sameAddressError: 'You cannot edit your deposit with the same winner address', + steps: [ + { + title: 'Edit your Deposit Drop', + subtitle: 'Enter the address of the lucky winner to profit from your deposit', + cardTitle: '', + cardSubtitle: '' + }, + { + title: 'Confirm the transaction on {{ provider }}', + subtitle: 'Update the winner address of your Deposit Drop\nand accept the transaction on your {{ provider }} wallet', + } + ] + } + }, }; diff --git a/src/models/wallets.ts b/src/models/wallets.ts index 16dea777..c608d4a1 100644 --- a/src/models/wallets.ts +++ b/src/models/wallets.ts @@ -15,6 +15,7 @@ export interface LumWalletModel { deposits: AggregatedDepositModel[]; prizes: PrizeModel[]; totalPrizesWon: { [denom: string]: number }; + depositDrops: AggregatedDepositModel[]; isLedger: boolean; } diff --git a/src/navigation/index.tsx b/src/navigation/index.tsx index 5dd371ed..9a8b4d09 100644 --- a/src/navigation/index.tsx +++ b/src/navigation/index.tsx @@ -3,6 +3,7 @@ import { createBrowserRouter, createRoutesFromElements, Route, Location } from ' import { Firebase } from 'utils'; import { FirebaseConstants, NavigationConstants } from 'constant'; import { HomePage, MySavingsPage, PoolsPage, DepositPage, LandingPage, Error404, Winners, PoolDetailsPage } from 'pages'; +import { DropsMyDepositsPage, DropsPoolsPage } from 'drops/pages'; import { MainLayout } from 'layout'; export const RouteListener = ({ location }: { location: Location }): JSX.Element | null => { @@ -19,12 +20,19 @@ export const router = createBrowserRouter( } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> } /> + + } /> + } /> + } /> + } /> + } /> + } /> , ), diff --git a/src/pages/Deposit/Deposit.scss b/src/pages/Deposit/Deposit.scss index da6505aa..c2943041 100644 --- a/src/pages/Deposit/Deposit.scss +++ b/src/pages/Deposit/Deposit.scss @@ -66,6 +66,10 @@ @include media-breakpoint-down(lg) { margin-right: 0; } + + a { + color: var(--color-primary); + } } } @@ -92,7 +96,6 @@ line-height: 16px; margin-bottom: 0.5rem; color: var(--color-black); - color: var(--color-black); } .amount-container { diff --git a/src/pages/Deposit/Deposit.tsx b/src/pages/Deposit/Deposit.tsx index 6c51ba01..8ab96f9f 100644 --- a/src/pages/Deposit/Deposit.tsx +++ b/src/pages/Deposit/Deposit.tsx @@ -9,29 +9,28 @@ import { CustomEase } from 'gsap/CustomEase'; import { Tabs, LiquidityModal, ThemeDefinition, WalletClientContext, defaultBlurs, defaultBorderRadii } from '@leapwallet/elements'; import { Modal as BootstrapModal } from 'bootstrap'; +import Assets from 'assets'; import { LUM_DENOM } from '@lum-network/sdk-javascript'; import cosmonautWithRocket from 'assets/lotties/cosmonaut_with_rocket.json'; -import Assets from 'assets'; -import { Button, Card, Lottie, Modal, PurpleBackgroundImage, Steps } from 'components'; +import { Button, Card, Lottie, Modal, PurpleBackgroundImage, Steps, IbcTransferModal, QuitDepositModal } from 'components'; import { FirebaseConstants, NavigationConstants, WalletProvider } from 'constant'; import { useColorScheme, usePrevious, useVisibilityState } from 'hooks'; import { PoolModel } from 'models'; import { DenomsUtils, Firebase, I18n, NumbersUtils, WalletUtils, WalletProvidersUtils, ToastUtils } from 'utils'; import { confettis } from 'utils/confetti'; import { RootState, Dispatch } from 'redux/store'; +import DepositDropSteps from 'drops/components/DepositDropSteps/DepositDropSteps'; import DepositSteps from './components/DepositSteps/DepositSteps'; -import QuitDepositModal from './components/Modals/QuitDeposit/QuitDeposit'; -import IbcTransferModal from './components/Modals/IbcTransfer/IbcTransfer'; import Error404 from '../404/404'; import './Deposit.scss'; const GSAP_DEFAULT_CONFIG = { ease: CustomEase.create('custom', 'M0,0 C0.092,0.834 0.26,1 1,1 ') }; -const Deposit = () => { +const Deposit = ({ isDrop }: { isDrop: boolean }) => { const { poolId, denom } = useParams(); const location = useLocation(); @@ -47,7 +46,11 @@ const Deposit = () => { const existsInLumBalances = lumWallet?.balances?.find((balance) => DenomsUtils.getNormalDenom(balance.denom) === denom); const [currentStep, setCurrentStep] = useState( - existsInLumBalances && denom !== LUM_DENOM && NumbersUtils.convertUnitNumber(existsInLumBalances.amount) > NumbersUtils.convertUnitNumber(pool?.minDepositAmount || '0') ? 1 : 0, + existsInLumBalances && + denom !== LUM_DENOM && + ((!isDrop && NumbersUtils.convertUnitNumber(existsInLumBalances.amount) > NumbersUtils.convertUnitNumber(pool?.minDepositAmount || '0')) || isDrop) + ? 1 + : 0, ); const [shareState, setShareState] = useState<('sharing' | 'shared') | null>(null); const [ibcModalPrevAmount, setIbcModalPrevAmount] = useState('0'); @@ -212,7 +215,7 @@ const Deposit = () => { const blocker = useBlocker(transferForm.dirty && currentStep < 2); const visibilityState = useVisibilityState(); - const steps = I18n.t('deposit.steps', { + const steps = I18n.t(isDrop ? 'depositDrops.depositFlow.steps' : 'deposit.steps', { returnObjects: true, denom: DenomsUtils.getNormalDenom(denom || '').toUpperCase(), chainName: pool?.internalInfos?.chainName || 'Native Chain', @@ -329,7 +332,6 @@ const Deposit = () => { const step2Timeline = () => { const tl = gsap.timeline(GSAP_DEFAULT_CONFIG); const ctaST = new SplitText('#depositFlow .step-2 .deposit-cta .deposit-cta-text', { type: 'words' }); - const cardStepSubtitleST = new SplitText('#depositFlow .step-2 .card-step-subtitle', { type: 'lines' }); tl.from( '#depositFlow .step-2 .card-step-title', @@ -338,34 +340,64 @@ const Deposit = () => { y: 50, }, '<0.1', - ) - .from( - cardStepSubtitleST.lines, + ); + + if (isDrop) { + tl.from( + '#depositFlow .step-2 .input-type-container', { opacity: 0, y: 50, - stagger: 0.1, }, '<0.1', ) - .from( - '#depositFlow .step-2 .deposit-warning', + .from( + '#depositFlow .step-2 .csv-file-input', + { + opacity: 0, + y: 50, + }, + '<0.1', + ) + .from( + '#depositFlow .step-2 .download-btn-container', + { + opacity: 0, + y: 50, + }, + '<0.1', + ); + } else { + const cardStepSubtitleST = new SplitText('#depositFlow .step-2 .card-step-subtitle', { type: 'lines' }); + + tl.from( + cardStepSubtitleST.lines, { opacity: 0, y: 50, + stagger: 0.1, }, '<0.1', - ) - .from( - '#depositFlow .step-2 .step2-input-container', + ).from( + '#depositFlow .step-2 .deposit-warning', { opacity: 0, y: 50, }, '<0.1', ); + } - if (pools.length > 1) { + tl.from( + '#depositFlow .step-2 .step2-input-container', + { + opacity: 0, + y: 50, + }, + '<0.1', + ); + + if (!isDrop && pools.length > 1) { tl.from( '#depositFlow .step-2 .custom-select', { @@ -657,6 +689,31 @@ const Deposit = () => { return await dispatch.wallet.depositToPool({ pool: poolToDeposit, amount: depositAmount }); }; + const onDepositDrop = async (pool: PoolModel, deposits: { amount: string; winnerAddress: string }[], onDepositCallback: (batchNum: number) => void, startIndex: number) => { + const maxAmount = Number(WalletUtils.getMaxAmount(pool.nativeDenom, lumWallet?.balances || [])); + const depositAmountNumber = deposits.reduce((acc, deposit) => acc + NumbersUtils.convertUnitNumber(deposit.amount), 0); + + if (depositAmountNumber > maxAmount) { + const prev = depositAmountNumber.toFixed(6); + const next = (depositAmountNumber - maxAmount).toFixed(6); + + transferForm.setFieldValue('amount', next); + setIbcModalPrevAmount(prev); + setIbcModalDepositAmount(next); + + if (ibcModalRef.current) { + ibcModalRef.current.show(); + } + + return null; + } + + const LIMIT = lumWallet?.isLedger ? 3 : 6; + const batchCount = Math.ceil(deposits.length / LIMIT); + + return await dispatch.wallet.depositDrop({ pool, deposits, onDepositCallback, startIndex, batchCount, limit: LIMIT }); + }; + useEffect(() => { if (blocker.state === 'blocked') { if (!transferForm.dirty) { @@ -866,6 +923,7 @@ const Deposit = () => { Firebase.logEvent(FirebaseConstants.ANALYTICS_EVENTS.DEPOSIT_FLOW, { step: currentStep, denom: denom, + is_drop: isDrop, }); }, [currentStep]); @@ -886,6 +944,38 @@ const Deposit = () => { const isShareStep = currentStep >= steps.length; const showSwapCard = otherWallet && process.env.NODE_ENV !== 'test'; + const depositComponentCommonProps = { + transferForm, + onNextStep: startTransition, + onFinishDeposit: (callback: () => void) => { + const tl = gsap.timeline({ + ...GSAP_DEFAULT_CONFIG, + onComplete: () => { + callback(); + gsap.set('#depositFlow .deposit-flow-container', { + opacity: 1, + delay: 0.2, + }); + }, + }); + + tl.to('#depositFlow .deposit-flow-container', { + opacity: 0, + y: -50, + }).set('#depositFlow .deposit-flow-container', { + y: 0, + }); + }, + onTwitterShare: () => setShareState('sharing'), + currentStep, + steps, + pools: pools.filter((pool) => pool.nativeDenom === 'u' + denom), + currentPool: pool, + price: prices?.[denom || ''] || 0, + lumWallet, + otherWallets, + }; + return ( <>
@@ -955,47 +1045,11 @@ const Deposit = () => { )}
- { - const tl = gsap.timeline({ - ...GSAP_DEFAULT_CONFIG, - onComplete: () => { - callback(); - gsap.set('#depositFlow .deposit-flow-container', { - opacity: 1, - delay: 0.2, - }); - }, - }); - - tl.to('#depositFlow .deposit-flow-container', { - opacity: 0, - y: -50, - }).set('#depositFlow .deposit-flow-container', { - y: 0, - }); - }} - onPrevStep={(prev, next) => { - transferForm.setFieldValue('amount', next); - setIbcModalPrevAmount(prev); - setIbcModalDepositAmount(next); - if (ibcModalRef.current) { - ibcModalRef.current.show(); - } - }} - onTwitterShare={() => setShareState('sharing')} - currentStep={currentStep} - steps={steps} - pools={pools.filter((pool) => pool.nativeDenom === 'u' + denom)} - currentPool={pool} - price={prices?.[denom || ''] || 0} - lumWallet={lumWallet} - otherWallets={otherWallets} - amountFromLocationState={amountToDeposit} - /> + {isDrop ? ( + + ) : ( + + )} {isShareStep && ( void; - onPrevStep: (prevAmount: string, nextAmount: string) => void; onDeposit: (poolToDeposit: PoolModel, depositAmount: string) => Promise<{ hash: string; error: string | null | undefined } | null>; onFinishDeposit: (callback: () => void) => void; onTwitterShare: () => void; @@ -58,151 +57,11 @@ type TxInfos = { poolId: string; }; -const DepositStep1 = ( - props: StepProps & { - nonEmptyWallets: OtherWalletModel[]; - form: FormikProps<{ amount: string }>; - onTransfer: (amount: string) => void; - }, -) => { - const { currentPool, balances, price, pools, form, nonEmptyWallets, title, subtitle, disabled, onTransfer } = props; - - const navigate = useNavigate(); - - const isLoading = useSelector((state: RootState) => state.loading.effects.wallet.ibcTransfer); - const prizeStrat = currentPool.prizeStrategy; - - let avgPrize = 0; - - if (prizeStrat) { - let avgPrizesDrawn = 0; - for (const prizeBatch of prizeStrat.prizeBatches) { - avgPrizesDrawn += (Number(currentPool.estimatedPrizeToWin?.amount || '0') * (Number(prizeBatch.poolPercent) / 100)) / Number(prizeBatch.quantity); - } - - avgPrize = avgPrizesDrawn / prizeStrat.prizeBatches.length / prizeStrat.prizeBatches.length; - } - - return ( -
-
-
-
-
-
-
- 0 ? balances[0].amount : '0')), - denom: DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase(), - })} - onMax={() => { - const amount = WalletUtils.getMaxAmount(currentPool.nativeDenom, balances, currentPool.internalInfos?.fees); - form.setFieldValue('amount', amount); - }} - inputProps={{ - type: 'number', - min: 0, - max: balances.length > 0 ? balances[0].amount : '0', - step: 'any', - lang: 'en', - placeholder: (100 / price).toFixed(6), - disabled, - ...form.getFieldProps('amount'), - onChange: (e) => { - const inputAmount = Number(e.target.value); - const maxAmount = Number(WalletUtils.getMaxAmount(currentPool.nativeDenom, balances, currentPool.internalInfos?.fees)); - - if (Number.isNaN(inputAmount) || inputAmount < 0) { - e.target.value = '0'; - } else if (inputAmount > maxAmount) { - e.target.value = maxAmount > 0 ? maxAmount.toString() : '0'; - } - - form.handleChange(e); - }, - }} - price={price} - error={form.touched.amount ? form.errors.amount : ''} - /> -
-
- {pools.filter((p) => p.nativeDenom !== MICRO_LUM_DENOM).length > 1 && ( - ((result, { balances }) => { - if (balances.length > 0) { - result.push({ - amount: balances[0].amount, - denom: balances[0].denom, - }); - } - return result; - }, [])} - value={currentPool.nativeDenom} - onChange={(value) => { - navigate(`/pools/${DenomsUtils.getNormalDenom(value)}`, { replace: true }); - }} - options={nonEmptyWallets.map((wallet) => ({ - label: DenomsUtils.getNormalDenom(wallet.balances[0].denom), - value: wallet.balances[0].denom, - }))} - /> - )} - -
-
- {I18n.t('deposit.chancesHint.winning.title')} - - info - - -
-
{NumbersUtils.float2ratio(PoolsUtils.getWinningChances(form.values.amount ? Number(form.values.amount) : 100 / price, currentPool))}
-
-
-
- {I18n.t('deposit.chancesHint.averagePrize.title')} - - info - - -
-
- {avgPrize.toFixed(2)} {DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase()} -
-
-
- -
-
-
- ); -}; - -const DepositStep2 = ( +const DepositStep = ( props: StepProps & { amount: string; onDeposit: (poolToDeposit: PoolModel, depositAmount: string) => Promise; initialAmount?: string; - onPrevStep: (prevAmount: string, nextAmount: string) => void; }, ) => { const { pools, currentPool, price, balances, amount, initialAmount, title, subtitle, disabled, onDeposit } = props; @@ -352,7 +211,7 @@ const DepositStep2 = ( ); }; -const DepositStep3 = ({ txInfos, price, title, subtitle, onTwitterShare }: { txInfos: TxInfos; title: string; subtitle: string; price: number; onTwitterShare: () => void }) => { +const ShareStep = ({ txInfos, price, title, subtitle, onTwitterShare }: { txInfos: TxInfos; title: string; subtitle: string; price: number; onTwitterShare: () => void }) => { const navigate = useNavigate(); return ( @@ -410,7 +269,7 @@ const DepositStep3 = ({ txInfos, price, title, subtitle, onTwitterShare }: { txI window.open( `${NavigationConstants.TWEET_URL}?text=${encodeURI( I18n.t('deposit.shareTwitterContent', txInfos ? { amount: txInfos.amount, denom: txInfos.denom, tvl: txInfos.tvl + ' ' + txInfos.denom } : {}), - )}`, + ).replaceAll('#', '%23')}`, '_blank', ); onTwitterShare(); @@ -426,7 +285,7 @@ const DepositStep3 = ({ txInfos, price, title, subtitle, onTwitterShare }: { txI }; const DepositSteps = (props: Props) => { - const { currentStep, steps, otherWallets, price, pools, currentPool, amountFromLocationState, onNextStep, onPrevStep, onDeposit, onFinishDeposit, onTwitterShare, transferForm, lumWallet } = props; + const { currentStep, steps, otherWallets, price, pools, currentPool, amountFromLocationState, onNextStep, onDeposit, onFinishDeposit, onTwitterShare, transferForm, lumWallet } = props; const [amount, setAmount] = useState(''); const [txInfos, setTxInfos] = useState(null); const [otherWallet, setOtherWallet] = useState(otherWallets[DenomsUtils.getNormalDenom(currentPool.nativeDenom)]); @@ -453,7 +312,7 @@ const DepositSteps = (props: Props) => {
{currentStep === 0 && currentPool.nativeDenom !== MICRO_LUM_DENOM && ( - { /> )} {((currentStep === 1 && currentPool.nativeDenom !== MICRO_LUM_DENOM) || (currentStep === 0 && currentPool.nativeDenom === MICRO_LUM_DENOM)) && ( - { currentPool={currentPool} pools={pools} price={price} - onPrevStep={onPrevStep} /> )} {currentStep === steps.length && txInfos && ( - + )}
diff --git a/src/pages/MySavings/MySavings.tsx b/src/pages/MySavings/MySavings.tsx index 2878e501..1bd6e03d 100644 --- a/src/pages/MySavings/MySavings.tsx +++ b/src/pages/MySavings/MySavings.tsx @@ -32,7 +32,7 @@ const MySavings = () => { useSelector((state: RootState) => ({ lumWallet: state.wallet.lumWallet, otherWallets: state.wallet.otherWallets, - balances: state.wallet.lumWallet?.balances, + balances: state.wallet.lumWallet?.balances.filter((balance) => state.pools.pools.find((pool) => pool.nativeDenom === balance.denom) || balance.denom === MICRO_LUM_DENOM), activities: state.wallet.lumWallet?.activities, deposits: state.wallet.lumWallet?.deposits, prizes: state.wallet.lumWallet?.prizes, diff --git a/src/pages/MySavings/components/Modals/Claim/Claim.tsx b/src/pages/MySavings/components/Modals/Claim/Claim.tsx index f70d14e3..03d87b76 100644 --- a/src/pages/MySavings/components/Modals/Claim/Claim.tsx +++ b/src/pages/MySavings/components/Modals/Claim/Claim.tsx @@ -97,7 +97,7 @@ const ShareClaim = ({ infos, prices, modalRef, onTwitterShare }: { infos: ShareI denom: infos.amount[0].denom, tvl: infos.tvl + ' ' + infos.amount[0].denom, }), - )}`, + ).replaceAll('#', '%23')}`, '_blank', ); onTwitterShare(); diff --git a/src/pages/Pools/components/PoolCard.tsx b/src/pages/Pools/components/PoolCard.tsx index 3416bb89..ce7d4bd7 100644 --- a/src/pages/Pools/components/PoolCard.tsx +++ b/src/pages/Pools/components/PoolCard.tsx @@ -16,9 +16,11 @@ interface IProps { estimatedPrize?: number; drawEndAt: Date; apy: number; + ctaText?: string; + ctaLink?: string; } -const PoolCard = ({ denom, tvl, poolId, estimatedPrize, drawEndAt, apy }: IProps) => { +const PoolCard = ({ denom, tvl, poolId, estimatedPrize, drawEndAt, apy, ctaText, ctaLink }: IProps) => { const prices = useSelector((state: RootState) => state.stats?.prices); const loadingAdditionalInfo = useSelector((state: RootState) => state.loading.effects.pools.getPoolsAdditionalInfo); const lumWallet = useSelector((state: RootState) => state.wallet.lumWallet); @@ -94,13 +96,13 @@ const PoolCard = ({ denom, tvl, poolId, estimatedPrize, drawEndAt, apy }: IProps 'data-bs-toggle': 'modal', } : { - to: `${NavigationConstants.POOLS}/${denom}/${poolId}`, + to: ctaLink ? ctaLink : `${NavigationConstants.POOLS}/${denom}/${poolId}`, })} forcePurple className='deposit-cta w-100 mt-3' onClick={() => Firebase.logEvent(FirebaseConstants.ANALYTICS_EVENTS.DEPOSIT_CLICK, { denom, pool_id: poolId })} > - {I18n.t('pools.cta')} + {ctaText ? ctaText : I18n.t('pools.cta')}
diff --git a/src/redux/models/wallet.ts b/src/redux/models/wallet.ts index 792c9576..666475da 100644 --- a/src/redux/models/wallet.ts +++ b/src/redux/models/wallet.ts @@ -1,12 +1,11 @@ import axios from 'axios'; -import { Coin } from '@keplr-wallet/types'; -import { LUM_DENOM, MICRO_LUM_DENOM, LUM_EXPONENT, LumBech32Prefixes, convertUnit } from '@lum-network/sdk-javascript'; +import { Coin, LUM_DENOM, MICRO_LUM_DENOM, LUM_EXPONENT, LumBech32Prefixes, convertUnit } from '@lum-network/sdk-javascript'; import { createModel } from '@rematch/core'; import { LumApi } from 'api'; -import { DenomsConstants, LUM_COINGECKO_ID, LUM_WALLET_LINK, WalletProvider, FirebaseConstants, ApiConstants, PrizesConstants } from 'constant'; -import { LumWalletModel, OtherWalletModel, PoolModel, PrizeModel, TransactionModel, AggregatedDepositModel, LeaderboardItemModel } from 'models'; +import { DenomsConstants, LUM_COINGECKO_ID, LUM_WALLET_LINK, WalletProvider, FirebaseConstants, ApiConstants, PrizesConstants, NavigationConstants } from 'constant'; +import { LumWalletModel, OtherWalletModel, PoolModel, PrizeModel, TransactionModel, AggregatedDepositModel, LeaderboardItemModel, DepositModel } from 'models'; import { ToastUtils, I18n, LumClient, DenomsUtils, WalletClient, WalletUtils, NumbersUtils, Firebase, WalletProvidersUtils } from 'utils'; import { getMillionsDevnetKeplrConfig } from 'utils/devnet'; @@ -35,6 +34,7 @@ interface SetWalletDataPayload { pagesTotal: number; }; deposits?: AggregatedDepositModel[]; + depositDrops?: AggregatedDepositModel[]; prizes?: PrizeModel[]; totalPrizesWon?: { [denom: string]: number }; } @@ -68,6 +68,18 @@ interface RetryDepositPayload { depositId: bigint; } +interface DepositDropPayload { + pool: PoolModel; + deposits: { + amount: string; + winnerAddress?: string; + }[]; + startIndex: number; + batchCount: number; + limit: number; + onDepositCallback?: (batchNum: number) => void; +} + interface LeavePoolPayload { poolId: bigint; denom: string; @@ -80,6 +92,15 @@ interface LeavePoolRetryPayload { withdrawalId: bigint; } +interface CancelDropPayload { + pool: PoolModel; + deposits: DepositModel[]; + startIndex: number; + batchCount: number; + limit: number; + onCancelCallback?: (batchNum: number) => void; +} + interface WalletState { lumWallet: LumWalletModel | null; otherWallets: { @@ -115,6 +136,7 @@ export const wallet = createModel()({ pagesTotal: 1, }, deposits: [], + depositDrops: [], prizes: [], totalPrizesWon: {}, isLedger: !!payload.isLedger, @@ -130,6 +152,7 @@ export const wallet = createModel()({ balances: payload.balances || state.lumWallet.balances, activities: payload.activities || state.lumWallet.activities, deposits: payload.deposits || state.lumWallet.deposits, + depositDrops: payload.depositDrops || state.lumWallet.depositDrops, prizes: payload.prizes || state.lumWallet.prizes, totalPrizesWon: payload.totalPrizesWon || state.lumWallet.totalPrizesWon, }, @@ -274,7 +297,12 @@ export const wallet = createModel()({ WalletUtils.storeAutoconnectKey(provider); - await dispatch.wallet.reloadWalletInfos({ address, force: true, init: true }); + if (location.pathname.includes(NavigationConstants.DROPS)) { + await dispatch.wallet.reloadWalletInfos({ address, force: true, init: true, drops: true }); + } else { + await dispatch.wallet.reloadWalletInfos({ address, force: true, init: true, drops: false }); + } + if (!silent) ToastUtils.showSuccessToast({ content: I18n.t('success.wallet') }); Firebase.signInAnonymous().finally(() => null); @@ -330,13 +358,17 @@ export const wallet = createModel()({ console.warn((e as Error).message); } }, - async reloadWalletInfos({ address, force = true, init = false }: { address: string; force?: boolean; init?: boolean }, state) { + async reloadWalletInfos({ address, force = true, init = false, drops = false }: { address: string; force?: boolean; init?: boolean; drops?: boolean }, state) { if (!force && Date.now() - state.wallet.autoReloadTimestamp < 1000 * 60 * 3) { return; } dispatch.wallet.setAutoReloadTimestamp(Date.now()); + if (!drops) { + await dispatch.stats.fetchStats(); + } + if (!init) { await dispatch.pools.fetchPools(null); await dispatch.pools.getPoolsAdditionalInfo(null); @@ -344,9 +376,14 @@ export const wallet = createModel()({ await dispatch.wallet.getLumWalletBalances(null); } - await dispatch.wallet.fetchPrizes(address); await dispatch.wallet.getActivities({ address, reset: true }); - await dispatch.wallet.getDepositsAndWithdrawals(address); + + if (!drops) { + await dispatch.wallet.fetchPrizes(address); + await dispatch.wallet.getDepositsAndWithdrawals(address); + } else { + await dispatch.wallet.getDepositsAndWithdrawalsDrops(address); + } }, async reloadOtherWalletInfo(payload: { address: string }, state) { const { address } = payload; @@ -917,5 +954,177 @@ export const wallet = createModel()({ return null; } }, + + // DROPS + async getDepositsAndWithdrawalsDrops(address: string) { + try { + const res = await LumClient.getDepositsAndWithdrawalsDrops(address); + + if (res) { + dispatch.wallet.setLumWalletData({ depositDrops: res }); + } + } catch (e) { + console.warn(e); + } + }, + async depositDrop(payload: DepositDropPayload, state) { + const { lumWallet } = state.wallet; + const { pool, startIndex, deposits, onDepositCallback, batchCount, limit } = payload; + + let lastBatch = startIndex; + + const toastId = ToastUtils.showLoadingToast({ + content: I18n.t(batchCount === 1 ? 'pending.deposit' : 'pending.multiDeposit', { index: 1, count: batchCount, denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase() }), + }); + + try { + if (!lumWallet) { + throw new Error(I18n.t('errors.client.noWalletConnected')); + } + + let result = null; + + for (let i = startIndex; i < batchCount; i++) { + lastBatch = i; + + if (i > 0) { + ToastUtils.updateToastContent(toastId, { content: I18n.t('pending.multiDeposit', { index: i + 1, count: batchCount }) }); + } + + const toDeposit = deposits.slice(i * limit, (i + 1) * limit).map((d) => ({ ...d, pool })); + + try { + result = await LumClient.multiDeposit(lumWallet, toDeposit); + } catch (e) { + const error = e as Error; + if (isRealError(error)) { + throw error; + } + } + + if (!result || (result && result.error)) { + throw new Error(result?.error || undefined); + } else { + onDepositCallback?.(i + 1); + } + } + + ToastUtils.updateLoadingToast(toastId, 'success', { + content: I18n.t(batchCount === 1 ? 'success.deposit' : 'success.multiDeposit', { + count: batchCount, + denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase(), + amount: deposits.reduce((acc, deposit) => acc + NumbersUtils.convertUnitNumber(deposit.amount), 0), + }), + }); + + dispatch.wallet.reloadWalletInfos({ address: lumWallet.address, force: true, drops: true }); + return true; + } catch (e) { + onDepositCallback?.(lastBatch); + ToastUtils.updateLoadingToast(toastId, 'error', { + content: (e as Error).message || I18n.t('errors.deposit.generic', { denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase() }), + }); + return null; + } + }, + async cancelDrop(payload: CancelDropPayload, state) { + const { lumWallet } = state.wallet; + const { pool, startIndex, deposits, batchCount, limit, onCancelCallback } = payload; + + let lastBatch = startIndex; + + const toastId = ToastUtils.showLoadingToast({ + content: I18n.t(batchCount === 1 ? 'pending.cancelDrop' : 'pending.cancelDropMulti', { + index: 1, + count: batchCount, + denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase(), + }), + }); + + try { + if (!lumWallet) { + throw new Error(I18n.t('errors.client.noWalletConnected')); + } + + let result = null; + + for (let i = startIndex; i < batchCount; i++) { + lastBatch = i; + + if (i > 0) { + ToastUtils.updateToastContent(toastId, { content: I18n.t('pending.cancelDropMulti', { index: i + 1, count: batchCount }) }); + } + + const toCancel = deposits.slice(i * limit, (i + 1) * limit); + + try { + result = await LumClient.cancelDepositDrop(lumWallet, toCancel); + } catch (e) { + const error = e as Error; + if (isRealError(error)) { + throw error; + } + } + + if (!result || (result && result.error)) { + throw new Error(result?.error || undefined); + } else { + onCancelCallback?.(i + 1); + } + } + + ToastUtils.updateLoadingToast(toastId, 'success', { + content: I18n.t(batchCount === 1 ? 'success.cancelDrop' : 'success.cancelDropMulti', { count: batchCount, denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase() }), + }); + + dispatch.wallet.reloadWalletInfos({ address: lumWallet.address, force: true, drops: true }); + return true; + } catch (e) { + onCancelCallback?.(lastBatch); + ToastUtils.updateLoadingToast(toastId, 'error', { + content: (e as Error).message || I18n.t('errors.deposit.generic', { denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase() }), + }); + return null; + } + }, + async editDrop(payload: { pool: PoolModel; deposit: DepositModel; newWinnerAddress: string }, state): Promise<{ hash: string; error: string | null | undefined } | null> { + const { lumWallet } = state.wallet; + const { pool, deposit, newWinnerAddress } = payload; + + const toastId = ToastUtils.showLoadingToast({ + content: I18n.t('pending.editDrop'), + }); + + try { + if (!lumWallet) { + throw new Error(I18n.t('errors.client.noWalletConnected')); + } + + let result = null; + + try { + result = await LumClient.editDeposit(lumWallet, deposit, newWinnerAddress); + } catch (e) { + const error = e as Error; + if (isRealError(error)) { + throw error; + } + } + + if (!result || (result && result.error)) { + throw new Error(result?.error || undefined); + } + + ToastUtils.updateLoadingToast(toastId, 'success', { content: I18n.t('success.editDrop') }); + + dispatch.wallet.reloadWalletInfos({ address: lumWallet.address, force: true, drops: true }); + return result; + } catch (e) { + ToastUtils.updateLoadingToast(toastId, 'error', { + content: (e as Error).message || I18n.t('errors.deposit.generic', { denom: DenomsUtils.getNormalDenom(pool.nativeDenom).toUpperCase() }), + }); + return null; + } + }, }), }); diff --git a/src/styles/_main.scss b/src/styles/_main.scss index a10d7c0e..fe079079 100644 --- a/src/styles/_main.scss +++ b/src/styles/_main.scss @@ -16,6 +16,19 @@ $utilities: map-merge( ) ); +$utilities: map-merge( + $utilities, + ( + 'width': + map-merge( + map-get($utilities, 'width'), + ( + responsive: true, + ) + ), + ) +); + @import '~bootstrap/scss/utilities/api'; html body { @@ -115,7 +128,7 @@ input[type='checkbox'] { position: relative; &:before { - content: ""; + content: ''; display: block; position: absolute; width: 20px; @@ -128,7 +141,7 @@ input[type='checkbox'] { } &:checked:before { - content: ""; + content: ''; display: block; position: absolute; width: 20px; @@ -140,7 +153,7 @@ input[type='checkbox'] { } &:checked:after { - content: ""; + content: ''; display: block; width: 6px; height: 10px; diff --git a/src/tests/app.test.tsx b/src/tests/app.test.tsx index 00ed8a66..e6741382 100644 --- a/src/tests/app.test.tsx +++ b/src/tests/app.test.tsx @@ -171,11 +171,11 @@ describe('App', () => { [ { path: '/pools/:denom', - element: , + element: , }, { path: '/pools/:denom/:poolId', - element: , + element: , }, ], { diff --git a/src/utils/lumClient.ts b/src/utils/lumClient.ts index bed5e703..1812301b 100644 --- a/src/utils/lumClient.ts +++ b/src/utils/lumClient.ts @@ -16,7 +16,7 @@ import { PoolsUtils } from 'utils'; import { formatTxs } from './txs'; import { getDenomFromIbc } from './denoms'; -const { deposit, claimPrize, withdrawDeposit, withdrawDepositRetry, depositRetry } = lum.network.millions.MessageComposer.withTypeUrl; +const { deposit, claimPrize, withdrawDeposit, withdrawDepositRetry, depositRetry, depositEdit } = lum.network.millions.MessageComposer.withTypeUrl; class LumClient { private static instance: LumClient | null = null; @@ -382,7 +382,7 @@ class LumClient { }; }; - multiDeposit = async (wallet: LumWalletModel, toDeposit: { pool: PoolModel; amount: string }[]) => { + multiDeposit = async (wallet: LumWalletModel, toDeposit: { pool: PoolModel; amount: string; winnerAddress?: string }[]) => { if (this.signingClient === null) { return null; } @@ -395,7 +395,7 @@ class LumClient { deposit({ poolId: d.pool.poolId, depositorAddress: wallet.address, - winnerAddress: wallet.address, + winnerAddress: d.winnerAddress || wallet.address, isSponsor: false, amount: { amount: d.amount, @@ -500,6 +500,111 @@ class LumClient { error: broadcastResult.code !== 0 ? broadcastResult.rawLog : null, }; }; + + // DROPS + getDepositsAndWithdrawalsDrops = async (depositorAddress: string): Promise => { + if (this.lumQueryClient === null) { + return null; + } + + let pageDeposits: Uint8Array | undefined = undefined; + const deposits: DepositModel[] = []; + + while (true) { + const resDeposits: QueryDepositsResponse = await this.lumQueryClient.lum.network.millions.accountDeposits({ + depositorAddress, + pagination: pageDeposits ? ({ key: pageDeposits } as PageRequest) : undefined, + }); + + deposits.push(...resDeposits.deposits); + + // If we have pagination key, we just patch it, and it will process in the next loop + if (resDeposits.pagination && resDeposits.pagination.nextKey && resDeposits.pagination.nextKey.length) { + pageDeposits = resDeposits.pagination.nextKey; + } else { + break; + } + } + + const aggregatedDeposits = await PoolsUtils.reduceDepositDropsByPoolIdAndDays(deposits); + + aggregatedDeposits.sort((a, b) => { + const aHeight = a.createdAtHeight ? Number(a.createdAtHeight) : 0; + const bHeight = b.createdAtHeight ? Number(b.createdAtHeight) : 0; + + return bHeight - aHeight; + }); + + return aggregatedDeposits; + }; + + cancelDepositDrop = async (wallet: LumWalletModel, deposits: DepositModel[]) => { + if (this.signingClient === null) { + return null; + } + + // Build transaction message + const messages = []; + + for (const deposit of deposits) { + messages.push( + withdrawDeposit({ + poolId: deposit.poolId, + depositId: deposit.depositId, + depositorAddress: wallet.address, + toAddress: wallet.address, + }), + ); + } + + // Define fees + const gasEstimated = await this.signingClient.simulate(wallet.address, messages, ''); + const fee = { + amount: coins('25000', MICRO_LUM_DENOM), + gas: new IntPretty(new Dec(gasEstimated).mul(new Dec(1.3))).maxDecimals(0).locale(false).toString(), + }; + + const broadcastResult = await this.signingClient.signAndBroadcast(wallet.address, messages, fee); + + // Verify the transaction was successfully broadcasted and made it into a block + assertIsDeliverTxSuccess(broadcastResult); + + return { + hash: broadcastResult.transactionHash, + error: broadcastResult.code !== 0 ? broadcastResult.rawLog : null, + }; + }; + + editDeposit = async (wallet: LumWalletModel, depositToEdit: DepositModel, newWinnerAddress: string) => { + if (this.signingClient === null) { + return null; + } + + // Build transaction message + const message = depositEdit({ + poolId: depositToEdit.poolId, + depositId: depositToEdit.depositId, + depositorAddress: wallet.address, + winnerAddress: newWinnerAddress, + }); + + // Define fees + const gasEstimated = await this.signingClient.simulate(wallet.address, [message], ''); + const fee = { + amount: coins('25000', MICRO_LUM_DENOM), + gas: new IntPretty(new Dec(gasEstimated).mul(new Dec(1.3))).maxDecimals(0).locale(false).toString(), + }; + + const broadcastResult = await this.signingClient.signAndBroadcast(wallet.address, [message], fee); + + // Verify the transaction was successfully broadcasted and made it into a block + assertIsDeliverTxSuccess(broadcastResult); + + return { + hash: broadcastResult.transactionHash, + error: broadcastResult.code !== 0 ? broadcastResult.rawLog : null, + }; + }; } export default LumClient.Instance; diff --git a/src/utils/pools.ts b/src/utils/pools.ts index 7f4c2a63..a4eed8e9 100644 --- a/src/utils/pools.ts +++ b/src/utils/pools.ts @@ -6,6 +6,7 @@ import { Prize } from '@lum-network/sdk-javascript/build/codegen/lum/network/mil import { biggerCoin, convertUnitNumber } from './numbers'; import { AggregatedDepositModel, DepositModel, PoolModel } from 'models'; import { getDenomFromIbc, getNormalDenom } from './denoms'; +import dayjs from 'dayjs'; export const getBestPrize = (prizes: Prize[], prices: { [key: string]: number }) => { if (prizes.length === 0) { @@ -121,3 +122,71 @@ export const getPoolByPoolId = (pools: PoolModel[], poolId: string) => { return pools.find((p) => p.poolId.toString() === poolId); }; + +// DROPS +export const reduceDepositDropsByPoolIdAndDays = async (deposits: Partial[]) => { + const aggregatedDeposits: AggregatedDepositModel[] = []; + + for (const deposit of deposits) { + const poolId = deposit.poolId; + + if (poolId === undefined || deposit.createdAt === undefined) { + continue; + } + + const createdAt = dayjs(deposit.createdAt).format('YYYY-MM-DD'); + + if (deposit.depositorAddress === deposit.winnerAddress) { + continue; + } + + const existingDeposit = aggregatedDeposits.find((d) => d.poolId?.toString() === poolId.toString() && dayjs(d.createdAt).format('YYYY-MM-DD') === createdAt); + + if ( + existingDeposit && + (deposit.state === DepositState.DEPOSIT_STATE_SUCCESS || deposit.state === DepositState.DEPOSIT_STATE_IBC_TRANSFER || deposit.state === DepositState.DEPOSIT_STATE_ICA_DELEGATE) + ) { + existingDeposit.deposits.push({ + ...deposit, + amount: deposit.amount + ? { + denom: await getDenomFromIbc(deposit.amount.denom), + amount: deposit.amount?.amount || '0', + } + : undefined, + }); + + const depositAmounts = existingDeposit.deposits.map((d) => Number(d.amount?.amount) || 0); + const totalDepositAmount = depositAmounts.reduce((a, b) => a + b, 0); + + existingDeposit.amount = { + ...existingDeposit.amount, + denom: await getDenomFromIbc(existingDeposit.amount?.denom || ''), + amount: totalDepositAmount.toString(), + }; + } else { + aggregatedDeposits.push({ + ...deposit, + amount: deposit.amount + ? { + ...deposit.amount, + denom: await getDenomFromIbc(deposit.amount.denom), + } + : undefined, + deposits: [ + { + ...deposit, + amount: deposit.amount + ? { + ...deposit.amount, + denom: await getDenomFromIbc(deposit.amount.denom), + } + : undefined, + }, + ], + }); + } + } + + return aggregatedDeposits; +}; diff --git a/src/utils/time.ts b/src/utils/time.ts index b2b779ee..422fa7b6 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,6 +1,9 @@ import dayjs from 'dayjs'; import dayjsUTC from 'dayjs/plugin/utc'; import dayjsDuration from 'dayjs/plugin/duration'; +import dayjsLocalizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(dayjsUTC); dayjs.extend(dayjsDuration); +dayjs.extend(dayjsLocalizedFormat); +dayjs().format('L LT'); diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index eeead0dc..7ebfeae4 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -1,12 +1,10 @@ -import { Coin, MICRO_LUM_DENOM } from '@lum-network/sdk-javascript'; +import { Coin, LumBech32Prefixes, fromBech32 } from '@lum-network/sdk-javascript'; import { getNormalDenom } from './denoms'; import { convertUnitNumber } from './numbers'; import { AggregatedDepositModel } from 'models'; import { AUTOCONNECT_STORAGE_KEY, WalletProvider } from 'constant'; import { DepositState } from '@lum-network/sdk-javascript/build/codegen/lum/network/millions/deposit'; -type Fee = { amount: { amount: string; denom: string }[]; gas: string }; - export const getTotalBalance = (balances: Coin[], prices: { [denom: string]: number }) => { let totalBalance = 0; let missingPrice = false; @@ -85,16 +83,13 @@ export const getMaxAmount = (denom?: string, balances?: Coin[], feesAmount?: num return '0'; }; -export const buildTxFee = (fee: string, gas: string, denom = MICRO_LUM_DENOM): Fee => { - return { - amount: [ - { - amount: fee, - denom, - }, - ], - gas, - }; +export const isAddressValid = (address: string, prefix: string | undefined = LumBech32Prefixes.ACC_ADDR): boolean => { + try { + const decoded = fromBech32(address); + return (!prefix || prefix === decoded.prefix) && decoded.data.length === 20; + } catch (err) { + return false; + } }; export const updatedBalances = (currentBalance?: Coin[], newBalance?: Coin[]) => { diff --git a/src/utils/walletProviders.ts b/src/utils/walletProviders.ts index 18b8257b..e0459509 100644 --- a/src/utils/walletProviders.ts +++ b/src/utils/walletProviders.ts @@ -140,7 +140,6 @@ export const suggestChain = async (walletProvider: WalletProvider, chainInfo: Ch export const requestCosmostationAccount = async (chainId: string) => { const provider = await cosmos(); - const account = await provider.requestAccount(chainId); - return account; + return await provider.requestAccount(chainId); }; diff --git a/yarn.lock b/yarn.lock index e4b437a5..f64f0da6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5724,6 +5724,13 @@ resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.2.tgz#8ea2c4f4e64c0cc948ad7da375f6f827778a7912" integrity sha512-A8F30k2gYJ/6e07spSCPpkuZu79LCnkPTvqmIWQzNGcrzwFKpVOydG41lNt5wZXjSI149qjyzC2L1+F2PD/NUA== +"@types/papaparse@^5.3.7": + version "5.3.11" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.11.tgz#73c0bcc884709da9d30af926e487a76ce21a05f9" + integrity sha512-ISil0lMkpRDrBTKRPnUgVb5IqxWwj19gWBrX/ROk3pbkkslBN3URa713r/BSfAUj+w9gTPg3S3f45aMToVfh1w== + dependencies: + "@types/node" "*" + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -7077,6 +7084,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +attr-accept@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^10.4.13: version "10.4.13" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8" @@ -9886,6 +9898,13 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== + dependencies: + tslib "^2.4.0" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -12861,6 +12880,11 @@ pako@^2.0.2: resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +papaparse@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127" + integrity sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw== + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -14078,6 +14102,15 @@ react-dom@^16.14.0: prop-types "^15.6.2" scheduler "^0.19.1" +react-dropzone@^14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.6.0" + prop-types "^15.8.1" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"