diff --git a/src/assets/go.svg b/src/assets/go.svg new file mode 100644 index 000000000..2b8db6558 --- /dev/null +++ b/src/assets/go.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/constants/common.ts b/src/constants/common.ts index 2dabffb0c..e5030ceaa 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -18,6 +18,9 @@ export const ONE_MINUTE_SECOND = 60 export const EPOCHS_PER_HALVING = 8760 export const THEORETICAL_EPOCH_TIME = 1000 * 60 * 60 * 4 // 4 hours export const PAGE_SIZE = 10 +export const MIN_DEPOSIT_AMOUNT = 102 +export const MAX_DECIMAL_DIGITS = 8 + export const IS_MAINNET = config.CHAIN_TYPE === 'mainnet' export function getPrimaryColor() { @@ -153,3 +156,6 @@ export const MAINNET_URL = `https://${config.BASE_URL}` export const TESTNET_URL = `https://${ChainName.Testnet}.${config.BASE_URL}` export const TYPE_ID_CODE_HASH = '0x00000000000000000000000000000000000000000000000000545950455f4944' + +export const NERVOS_DAO_RFC_URL = + 'https://www.github.com/nervosnetwork/rfcs/blob/master/rfcs/0023-dao-deposit-withdraw/0023-dao-deposit-withdraw.md' diff --git a/src/locales/en.json b/src/locales/en.json index 175f5b32a..05fde3193 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -569,7 +569,28 @@ "days": "days+", "24hrs_update": "24 Hours Changes(UTC+8:00)", "today_update": "Today Changes(UTC+8:00)", - "deposit_address_tooltip": "Number of addresses with non-zero balance of Nervos DAO" + "deposit_address_tooltip": "Number of addresses with non-zero balance of Nervos DAO", + "deposit_to_dao": "Deposit to Nervos DAO", + "deposit_to_dao_description": "Deposit to receive an equivalent amount of CKB at the same rate as the secondary issuance, keep your assets away from being diluted by the secondary issuance.", + "reward_calculator": "Compensation Calculator", + "nervos_dao_rfc": "Nervos DAO RFC", + "learn_more": "Learn More", + "dao_reward_calculator": "Nervos DAO Compensation Calculator", + "deposit_terms": "Nervos DAO (generally)requires 102 CKBytes for hosting cell itself, and this portion of CKBytes won't be compensated. Please refer to the <0>Nervos DAO RFC for more information on Nervos DAO.", + "you_deposit": "Your Deposit", + "you_can_withdraw": "You may withdraw approximately", + "estimated_rewards": "Make a withdrawal request after {{days}} days:", + "deposit_notice": "30 days work as a circle, if you didn’t make withdrawal request, the compensation will be calculated as compound compensation.", + "estimated_APC": "Estimated APC", + "exclude_inactive_ckb": "Exclude inactive CKB", + "exclude_inactive_ckb_tip": "", + "rewards": "Compensation", + "estimated_rewards_in_years": "Estimated compensation in", + "year": "Year", + "years": "Years", + "day": "Days", + "done": "Done", + "view_apc_trending": "View APC Trending" }, "error": { "maintain": "The tip block number {{tip}}, the current synced block lagging behind by {{lag}} blocks", diff --git a/src/locales/zh.json b/src/locales/zh.json index 7d1e9c936..94e0e3dbb 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -570,7 +570,28 @@ "days": "天+", "24hrs_update": "24 小时变动(UTC+8:00)", "today_update": "今日变动(UTC+8:00)", - "deposit_address_tooltip": "Nervos DAO 锁定余额不为零的地址" + "deposit_address_tooltip": "Nervos DAO 锁定余额不为零的地址", + "deposit_to_dao": "锁定到 Nervos DAO", + "deposit_to_dao_description": "锁定以获得与二次发行利率相同的等额 CKB,避免您的资产被二次发行稀释。", + "reward_calculator": "补贴计算器", + "nervos_dao_rfc": "Nervos DAO RFC", + "learn_more": "了解更多", + "dao_reward_calculator": "Nervos DAO 补贴计算器", + "deposit_terms": "Nervos DAO (通常)需要 102 CKBytes 作为锁定记录的存储,这部分 CKBytes 是无法产生锁定补贴的。
请查看 <0>Nervos DAO RFC 以了解 Nervos DAO 更多信息。", + "you_deposit": "锁定", + "you_can_withdraw": "可领取约", + "estimated_rewards": "{{days}} 天后提出提取申请:", + "deposit_notice": "30 天为一个周期,如果您没有提出提取要求,补贴将按复利计算", + "estimated_APC": "预计年化锁定补贴率", + "exclude_inactive_ckb": "不包含非活动的CKB", + "exclude_inactive_ckb_tip": "", + "rewards": "补贴", + "estimated_rewards_in_years": "预计补贴", + "year": "年", + "years": "年", + "day": "天", + "done": "完成", + "view_apc_trending": "查看 APC 走势" }, "error": { "maintain": "最新区块为 {{tip}}, 当前同步高度落后 {{lag}} 区块", diff --git a/src/pages/NervosDao/DaoBanner/DaoBanner.module.scss b/src/pages/NervosDao/DaoBanner/DaoBanner.module.scss new file mode 100644 index 000000000..7882a94c3 --- /dev/null +++ b/src/pages/NervosDao/DaoBanner/DaoBanner.module.scss @@ -0,0 +1,97 @@ +@import '../../../styles/variables.module'; + +.bannerContainer { + height: 280px; + width: 100%; + border-radius: 4px; + background: #000; + color: #fff; + background-image: url('./banner_bg.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + + .content { + margin-top: 64px; + margin-left: calc((100% - 570px) / 4); + max-width: 570px; + + p { + margin: 0; + } + + .title { + font-size: 24px; + font-weight: 500; + } + + .description { + font-size: 14px; + margin-top: 16px; + } + + .actions { + display: flex; + gap: 16px; + margin-top: 24px; + } + } + + .btn { + background: transparent; + color: #fff; + border: 1px solid #fff; + display: flex; + gap: 4px; + align-items: center; + justify-content: center; + border-radius: 4px; + padding: 8px 12px; + cursor: pointer; + font-size: 14px; + line-height: 14px; + transition: none; + + .icon { + path { + fill: #fff; + } + } + + &:hover { + color: var(--primary-color); + border: 1px solid var(--primary-color); + + .icon { + path { + fill: var(--primary-color); + } + } + } + } +} + +@media (width <= $mobileBreakPoint) { + .bannerContainer { + height: auto; + background-image: url('./banner_bg_mobile.svg'); + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + + .content { + margin-top: 32px; + margin-bottom: 27px; + max-width: 100%; + margin-left: 16px; + + .actions { + flex-direction: column; + + .btn { + width: 160px; + } + } + } + } +} diff --git a/src/pages/NervosDao/DaoBanner/banner_bg.svg b/src/pages/NervosDao/DaoBanner/banner_bg.svg new file mode 100644 index 000000000..4b7834355 --- /dev/null +++ b/src/pages/NervosDao/DaoBanner/banner_bg.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/NervosDao/DaoBanner/banner_bg_mobile.svg b/src/pages/NervosDao/DaoBanner/banner_bg_mobile.svg new file mode 100644 index 000000000..6b2e45f04 --- /dev/null +++ b/src/pages/NervosDao/DaoBanner/banner_bg_mobile.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/NervosDao/DaoBanner/index.tsx b/src/pages/NervosDao/DaoBanner/index.tsx new file mode 100644 index 000000000..255dd8acf --- /dev/null +++ b/src/pages/NervosDao/DaoBanner/index.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ReactComponent as GoIcon } from '../../../assets/go.svg' +import RewardCalcutorModal from '../RewardCalcutorModal' +import { NERVOS_DAO_RFC_URL } from '../../../constants/common' +import styles from './DaoBanner.module.scss' + +const DaoBanner = ({ estimatedApc }: { estimatedApc: string }) => { + const [showRewardCalcutorModal, setShowRewardCalcutorModal] = useState(false) + const { t } = useTranslation() + + return ( +
+
+

{t('nervos_dao.deposit_to_dao')}

+

{t('nervos_dao.deposit_to_dao_description')}

+
+ + + {t('nervos_dao.nervos_dao_rfc')} + + + + {t('nervos_dao.learn_more')} + + +
+
+ + {showRewardCalcutorModal ? ( + setShowRewardCalcutorModal(false)} /> + ) : null} +
+ ) +} + +export default DaoBanner diff --git a/src/pages/NervosDao/RewardCalcutorModal/RewardCalcutorModal.module.scss b/src/pages/NervosDao/RewardCalcutorModal/RewardCalcutorModal.module.scss new file mode 100644 index 000000000..529f205b6 --- /dev/null +++ b/src/pages/NervosDao/RewardCalcutorModal/RewardCalcutorModal.module.scss @@ -0,0 +1,192 @@ +@import '../../../styles/variables.module'; + +.contentWrapper { + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 4px; + width: 80vw; + max-width: 714px; + padding: 23px 40px; + + @media screen and (width <= 750px) { + padding: 16px; + width: calc(100vw - 32px); + } + + p { + margin: 0; + } +} + +.modalTitle { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + color: #333; + font-weight: 600; + font-size: 16px; + line-height: 20px; +} + +.subTitle { + width: 100%; + margin-top: 8px; + font-size: 14px; + line-height: 24px; + font-weight: 400; + color: #333; + + .rfcLink { + color: var(--primary-color); + text-decoration: underline; + } +} + +.divider { + margin-top: 16px; + background: #e5e5e5; + width: 100%; + height: 1px; +} + +.modalContent { + width: 100%; + + h2 { + margin: 16px 0 0; + font-size: 14px; + font-weight: 500; + } + + p { + margin-top: 12px; + font-size: 12px; + color: #666; + } + + .input { + margin-top: 12px; + border: 1px solid #e5e5e5; + height: 38px; + font-size: 14px; + border-radius: 4px; + } + + .notice { + margin-top: 12px; + border-radius: 4px; + padding: 8px 12px; + font-size: 14px; + color: #ffa800; + border: 1px solid #ffdba6; + background: #fffcf2; + } + + .apcHeader { + margin-top: 16px; + display: flex; + justify-content: space-between; + + .apcTitle { + flex: 1; + margin: 0; + + a { + margin-left: 4px; + font-size: 0.75rem; + color: #999; + + &:hover { + color: var(--primary-color); + } + } + } + + div { + display: flex; + align-items: center; + gap: 4px; + } + + p { + margin: 0; + font-size: 14px; + line-height: 22px; + } + } +} + +.closeBtn { + border: none; + background: transparent; + cursor: pointer; + + img { + width: 13px; + height: 13px; + } +} + +.chartWap { + margin-top: 12px; + width: 100%; + height: 296px; + position: relative; + background: #fbfbfb; + padding: 12px; + + .yTitle { + color: #999; + font-size: 12px; + font-weight: 500; + } + + .xTitle { + position: absolute; + bottom: 0; + right: 35px; + color: #999; + font-size: 12px; + font-weight: 500; + } +} + +.years { + input { + outline: none; + border: 1px solid #e5e5e5; + width: 6ch; + padding: 0 4px; + margin: 0 4px; + } +} + +/* stylelint-disable-next-line selector-class-pattern */ +:global(.ant-input[disabled]) { + color: #333 !important; +} + +/* stylelint-disable-next-line selector-class-pattern */ +:global(.ant-input-suffix) { + color: #333 !important; +} + +.doneBtn { + margin-top: 40px; + height: 47px; + background: var(--primary-color); + width: 117px; + border: none; + border-radius: 4px; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.loading { + text-align: center; +} diff --git a/src/pages/NervosDao/RewardCalcutorModal/index.tsx b/src/pages/NervosDao/RewardCalcutorModal/index.tsx new file mode 100644 index 000000000..865d3adc2 --- /dev/null +++ b/src/pages/NervosDao/RewardCalcutorModal/index.tsx @@ -0,0 +1,219 @@ +import { useState, useMemo } from 'react' +import { Link } from 'react-router-dom' +import { Input } from 'antd' +import { Trans, useTranslation } from 'react-i18next' +import BigNumber from 'bignumber.js' +import CommonModal from '../../../components/CommonModal' +import Loading from '../../../components/AwesomeLoadings/Spinner' +import CloseIcon from '../../../assets/modal_close.png' +import { ReactChartCore } from '../../StatisticsChart/common' +import { MIN_DEPOSIT_AMOUNT, NERVOS_DAO_RFC_URL, IS_MAINNET, MAX_DECIMAL_DIGITS } from '../../../constants/common' +import { localeNumberString } from '../../../utils/number' +import styles from './RewardCalcutorModal.module.scss' + +const INIT_DEPOSIT_VALUE = '1000' + +const RewardCalcutorModal = ({ onClose, estimatedApc }: { onClose: () => void; estimatedApc: string }) => { + const { t } = useTranslation() + const [depositValue, setDepositValue] = useState(INIT_DEPOSIT_VALUE) + const [years, setYears] = useState(5) + + const yearReward = useMemo(() => { + const EMPTY = BigNumber(0) + if (!estimatedApc) return EMPTY + if (!depositValue) return EMPTY + const v = BigNumber(depositValue) + + if (v.isNaN() || v.isNegative()) return EMPTY + + const amount = v.minus(MIN_DEPOSIT_AMOUNT) + if (amount.isNegative()) return EMPTY + + const yearReward = amount.multipliedBy(estimatedApc).dividedBy(100) + return yearReward + }, [depositValue, estimatedApc]) + + const monthReward = yearReward.dividedBy(12) + + const handleDepositChange = (e: React.ChangeEvent) => { + e.stopPropagation() + e.preventDefault() + const { value } = e.currentTarget + const v = value.replace(/,/g, '') + if (!v) { + setDepositValue('') + return + } + setDepositValue(v) + } + + const handleYearChange = (e: React.ChangeEvent) => { + e.stopPropagation() + e.preventDefault() + const y = +e.currentTarget.value + if (y < 1) { + return + } + setYears(y) + } + if (!estimatedApc) { + return ( + +
+
+
+

{t('nervos_dao.dao_reward_calculator')}

+ +
+
+ + Nervos DAO RFC + , + ]} + /> +
+
+
+ +
+
+
+ + ) + } + + return ( + +
+
+
+

{t('nervos_dao.dao_reward_calculator')}

+ +
+
+ + Nervos DAO RFC + , + ]} + /> +
+ +
+
+

{t('nervos_dao.you_deposit')}

+ +

{t('nervos_dao.you_can_withdraw')}

+

{t(`nervos_dao.estimated_rewards`, { days: 30 })}

+ +

{t(`nervos_dao.estimated_rewards`, { days: 360 })}

+ +
{t('nervos_dao.deposit_notice')}
+ +
+

+ {t('nervos_dao.estimated_APC')} + + {`(${t('nervos_dao.view_apc_trending')})`} + +

+
+ + +

+ {t('nervos_dao.estimated_rewards_in_years')} + + {t('nervos_dao.years')} +

+
+

{t('nervos_dao.rewards')}

+ i + 1), + axisLabel: { + formatter: (value: string) => + +value > 1 ? `${value} ${t('nervos_dao.years')}` : `${value} ${t('nervos_dao.year')}`, + }, + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: (value: string) => `${value} CKB`, + }, + boundaryGap: ['20%', '20%'], + }, + series: [ + { + data: Array.from({ length: years }, (_, i) => + yearReward + .multipliedBy(BigNumber(1 + +estimatedApc / 100).exponentiatedBy(i + 1)) + .toFixed(8, BigNumber.ROUND_DOWN), + ), + type: 'line', + stack: 'withdrawal', + areaStyle: {}, + label: { + normal: { + show: years <= 5, + position: 'top', + formatter: '{c} CKB', + }, + }, + }, + ], + }} + notMerge + lazyUpdate + style={{ height: '100%', width: '100%' }} + /> +

{t('nervos_dao.years')}

+
+
+ + +
+
+ + ) +} + +export default RewardCalcutorModal diff --git a/src/pages/NervosDao/index.tsx b/src/pages/NervosDao/index.tsx index 0c4be91aa..bac916ef8 100644 --- a/src/pages/NervosDao/index.tsx +++ b/src/pages/NervosDao/index.tsx @@ -8,6 +8,7 @@ import Filter from '../../components/Filter' import DepositorRank from './DepositorRank' import { usePaginationParamsInPage, useSearchParams } from '../../hooks' import DaoOverview from './DaoOverview' +import DaoBanner from './DaoBanner' import SimpleButton from '../../components/SimpleButton' import { QueryResult } from '../../components/QueryResult' import { defaultNervosDaoInfo } from './state' @@ -50,6 +51,7 @@ export const NervosDao = () => { return ( +
@@ -79,7 +81,6 @@ export const NervosDao = () => { }} /> - {tab === 'transactions' ? ( {data => ( diff --git a/src/styles/index.css b/src/styles/index.css index 7c0047a84..2685ca680 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -92,6 +92,16 @@ a { white-space: pre-wrap; } +.ant-input:focus { + border-color: none; +} + +.ant-input-affix-wrapper:hover, +.ant-input-affix-wrapper:focus { + border-color: #e5e5e5 !important; + box-shadow: none; +} + a:hover, .ant-btn-primary:hover, .ant-tabs-tab:hover { diff --git a/src/utils/util.ts b/src/utils/util.ts index cee4b937f..c0defcb90 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -422,6 +422,17 @@ export const hexToBase64 = (hexstring: string) => { return btoa(str) } +export const ckbToShannon = (amount: string = '0') => { + if (Number.isNaN(+amount)) { + return `${amount} ckb` + } + const [integer = '0', decimal = ''] = amount.split('.') + const decimalLength = 10 ** decimal.length + const num = integer + decimal + + return (BigInt(num) * BigInt(1e8 / decimalLength)).toString() +} + export default { copyElementValue, shannonToCkb,