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 (
+
+ );
+};
+
+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 &&
} {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 ;
};
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 && (
+
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+
+
{DenomsUtils.getNormalDenom(currentPool.nativeDenom).toUpperCase()}
+
+
+
+
+
+
+
+
+
+
+
+ {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.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');
+ }}
+ >
+
+ {I18n.t('deposit.seeOnMintscan')}
+
+
+
+
{
+ navigate(NavigationConstants.DROPS_MY_DEPOSITS);
+ }}
+ >
+
+ {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 (
+
+
+
+
+
{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 (
+
+
+
+
+
+
+ {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 })}
+
+
+
+
+
+
+
+ {DenomsUtils.getNormalDenom(deposits && deposits[0] && deposits[0].amount ? deposits[0]?.amount.denom : '').toUpperCase()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 ? (
+
+
+
+
{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) => (
+
+
+
+
+
+
+ {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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
+
+
{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 (
-
- );
-};
-
-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"