diff --git a/lerna.json b/lerna.json index 56eecf45f7..110180cdb8 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.24.1", + "version": "0.24.2", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index f9a65aa48b..845830554b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.24.1", + "version": "0.24.2", "private": true, "author": { "name": "Nervos Core Dev", diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index 2247d22eda..9353a6280d 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.24.1", + "version": "0.24.2", "private": true, "author": { "name": "Nervos Core Dev", @@ -53,7 +53,7 @@ "qr.js": "0.0.0", "react": "16.9.0", "react-dom": "16.9.0", - "react-i18next": "10.12.2", + "react-i18next": "11.0.1", "react-router-dom": "5.0.1", "react-scripts": "3.2.0", "styled-components": "5.0.0-beta.0" diff --git a/packages/neuron-ui/src/components/CustomRows/DAORecordRow.tsx b/packages/neuron-ui/src/components/CustomRows/DAORecordRow.tsx new file mode 100644 index 0000000000..640ce7a0ad --- /dev/null +++ b/packages/neuron-ui/src/components/CustomRows/DAORecordRow.tsx @@ -0,0 +1,143 @@ +import React, { useEffect, useState } from 'react' +import { DefaultButton } from 'office-ui-fabric-react' +import { useTranslation } from 'react-i18next' +import { ckbCore, getBlockByNumber } from 'services/chain' +import calculateAPY from 'utils/calculateAPY' +import { shannonToCKBFormatter, uniformTimeFormatter, localNumberFormatter } from 'utils/formatters' +import calculateClaimEpochNumber from 'utils/calculateClaimEpochNumber' +import { epochParser } from 'utils/parsers' + +import * as styles from './daoRecordRow.module.scss' + +const DAORecord = ({ + daoData, + blockNumber, + blockHash, + outPoint: { txHash, index }, + tipBlockNumber, + tipBlockHash, + capacity, + actionLabel, + onClick, + timestamp, + depositOutPoint, + epoch, +}: State.NervosDAORecord & { + actionLabel: string + onClick: any + tipBlockNumber: string + tipBlockHash: string + epoch: string +}) => { + const [t] = useTranslation() + const [withdrawValue, setWithdrawValue] = useState('') + const [depositEpoch, setDepositEpoch] = useState('') + + useEffect(() => { + const withdrawBlockHash = depositOutPoint ? blockHash : tipBlockHash + if (!withdrawBlockHash) { + return + } + const formattedDepositOutPoint = depositOutPoint + ? { + txHash: depositOutPoint.txHash, + index: BigInt(depositOutPoint.index), + } + : { + txHash, + index: BigInt(index), + } + ;(ckbCore.rpc as any) + .calculateDaoMaximumWithdraw(formattedDepositOutPoint, withdrawBlockHash) + .then((res: string) => { + setWithdrawValue(BigInt(res).toString()) + }) + .catch((err: Error) => { + console.error(err) + }) + }, [txHash, index, tipBlockHash, depositOutPoint, blockHash]) + + useEffect(() => { + if (!depositOutPoint) { + return + } + const depositBlockNumber = ckbCore.utils.bytesToHex(ckbCore.utils.hexToBytes(daoData).reverse()) + getBlockByNumber(BigInt(depositBlockNumber)) + .then(b => { + setDepositEpoch(b.header.epoch) + }) + .catch((err: Error) => { + console.error(err) + }) + }, [daoData, depositOutPoint]) + + const interest = BigInt(withdrawValue) - BigInt(capacity) + + let ready = false + let metaInfo = 'Ready' + if (!depositOutPoint) { + const duration = BigInt(tipBlockNumber) - BigInt(blockNumber) + metaInfo = t('nervos-dao.interest-accumulated', { + blockNumber: localNumberFormatter(duration >= BigInt(0) ? duration : 0), + }) + } else { + const depositEpochInfo = epochParser(depositEpoch) + const currentEpochInfo = epochParser(epoch) + const targetEpochNumber = calculateClaimEpochNumber(depositEpochInfo, currentEpochInfo) + if (targetEpochNumber < currentEpochInfo.number + BigInt(1) && targetEpochNumber >= currentEpochInfo.number) { + metaInfo = 'Ready' + ready = true + } else { + const epochs = targetEpochNumber - currentEpochInfo.number - BigInt(1) + metaInfo = t('nervos-dao.blocks-left', { + epochs: localNumberFormatter(epochs), + blocks: localNumberFormatter(currentEpochInfo.length - currentEpochInfo.index), + days: localNumberFormatter(epochs / BigInt(6)), + }) + } + } + + return ( +
+
+
{interest >= BigInt(0) ? `${shannonToCKBFormatter(interest.toString()).toString()} CKB` : ''}
+
{`${shannonToCKBFormatter(capacity)} CKB`}
+
+ +
+
+
+ + {`APY: ~${calculateAPY( + interest >= BigInt(0) ? interest.toString() : '0', + capacity, + `${Date.now() - +timestamp}` + )}%`} + + {uniformTimeFormatter(+timestamp)} + {metaInfo} +
+
+ ) +} + +DAORecord.displayName = 'DAORecord' + +export default DAORecord diff --git a/packages/neuron-ui/src/components/CustomRows/daoRecordRow.module.scss b/packages/neuron-ui/src/components/CustomRows/daoRecordRow.module.scss new file mode 100644 index 0000000000..7a2bede64c --- /dev/null +++ b/packages/neuron-ui/src/components/CustomRows/daoRecordRow.module.scss @@ -0,0 +1,35 @@ +.daoRecord { + display: flex; + flex-direction: column; + border: 1px solid #000; + border-radius: 5px; + margin: 10px 0; + padding: 5px 15px; + + .primaryInfo, + .secondaryInfo { + display: flex; + justify-content: space-between; + + &>div, + &>span { + flex: 1; + text-align: center; + + &:first-child { + text-align: left; + } + + &:last-child { + text-align: right; + } + } + + } + + .secondaryInfo { + font-size: 12px; + color: #666; + } + +} diff --git a/packages/neuron-ui/src/components/NervosDAO/DepositDialog.tsx b/packages/neuron-ui/src/components/NervosDAO/DepositDialog.tsx new file mode 100644 index 0000000000..43e1d8a8b6 --- /dev/null +++ b/packages/neuron-ui/src/components/NervosDAO/DepositDialog.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { + Stack, + Dialog, + TextField, + Slider, + Text, + DefaultButton, + PrimaryButton, + DialogType, + DialogFooter, + Spinner, + SpinnerSize, +} from 'office-ui-fabric-react' +import { useTranslation } from 'react-i18next' +import { SHANNON_CKB_RATIO } from 'utils/const' + +const DepositDialog = ({ + show, + value, + fee, + balance, + onChange, + onSlide, + onSubmit, + onDismiss, + isDepositing, + errorMessage, +}: any) => { + const [t] = useTranslation() + const maxValue = +(BigInt(balance) / BigInt(SHANNON_CKB_RATIO)).toString() + + if (!show) { + return null + } + + return ( + + ) +} + +DepositDialog.displayName = 'DepositDialog' + +export default DepositDialog diff --git a/packages/neuron-ui/src/components/NervosDAO/WithdrawDialog.tsx b/packages/neuron-ui/src/components/NervosDAO/WithdrawDialog.tsx new file mode 100644 index 0000000000..77c77b2760 --- /dev/null +++ b/packages/neuron-ui/src/components/NervosDAO/WithdrawDialog.tsx @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from 'react' +import { Dialog, DialogFooter, DefaultButton, PrimaryButton, DialogType } from 'office-ui-fabric-react' +import { useTranslation } from 'react-i18next' +import { shannonToCKBFormatter, localNumberFormatter } from 'utils/formatters' +import { ckbCore } from 'services/chain' +import calculateTargetEpochNumber from 'utils/calculateClaimEpochNumber' +import { epochParser } from 'utils/parsers' + +const WithdrawDialog = ({ + onDismiss, + onSubmit, + record, + tipBlockHash, + currentEpoch, +}: { + onDismiss: any + onSubmit: any + record: State.NervosDAORecord + tipBlockHash: string + currentEpoch: string +}) => { + const [t] = useTranslation() + const [depositEpoch, setDepositEpoch] = useState('') + const [withdrawValue, setWithdrawValue] = useState('') + useEffect(() => { + if (!record) { + return + } + ckbCore.rpc + .getBlock(record.blockHash) + .then(b => { + setDepositEpoch(b.header.epoch) + }) + .catch((err: Error) => { + console.error(err) + }) + }, [record]) + useEffect(() => { + if (!record || !tipBlockHash) { + return + } + + ;(ckbCore.rpc as any) + .calculateDaoMaximumWithdraw( + { + txHash: record.outPoint.txHash, + index: `0x${BigInt(record.outPoint.index).toString(16)}`, + }, + tipBlockHash + ) + .then((res: string) => { + setWithdrawValue(res) + }) + .catch((err: Error) => { + console.error(err) + }) + }, [record, tipBlockHash]) + + const depositEpochInfo = epochParser(depositEpoch) + const currentEpochInfo = epochParser(currentEpoch) + const targetEpochNumber = calculateTargetEpochNumber(depositEpochInfo, currentEpochInfo) + const epochs = targetEpochNumber - currentEpochInfo.number - BigInt(1) + const message = t('nervos-dao.notice-wait-time', { + epochs: localNumberFormatter(epochs), + blocks: localNumberFormatter(currentEpochInfo.length - currentEpochInfo.index), + days: localNumberFormatter(epochs / BigInt(6)), + }) + return ( + + ) +} + +WithdrawDialog.displayName = 'WithdrawDialog' + +export default WithdrawDialog diff --git a/packages/neuron-ui/src/components/NervosDAO/index.tsx b/packages/neuron-ui/src/components/NervosDAO/index.tsx new file mode 100644 index 0000000000..738fcec07a --- /dev/null +++ b/packages/neuron-ui/src/components/NervosDAO/index.tsx @@ -0,0 +1,294 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react' +import { RouteComponentProps } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Stack, Text, DefaultButton, Icon, TooltipHost, Spinner } from 'office-ui-fabric-react' + +import appState from 'states/initStates/app' +import { AppActions, StateWithDispatch } from 'states/stateProvider/reducer' +import { updateNervosDaoData, clearNervosDaoData } from 'states/stateProvider/actionCreators' + +import calculateFee from 'utils/calculateFee' +import { shannonToCKBFormatter } from 'utils/formatters' +import { MIN_DEPOSIT_AMOUNT, MEDIUM_FEE_RATE, SHANNON_CKB_RATIO } from 'utils/const' + +import { generateDepositTx, generateWithdrawTx, generateClaimTx } from 'services/remote' +import { epochParser } from 'utils/parsers' + +import DAORecord from 'components/CustomRows/DAORecordRow' + +import DepositDialog from './DepositDialog' +import WithdrawDialog from './WithdrawDialog' + +let timer: NodeJS.Timeout + +const NervosDAO = ({ + app: { + send = appState.send, + loadings: { sending = false }, + tipBlockNumber, + tipBlockHash, + epoch, + }, + wallet, + dispatch, + nervosDAO: { records }, +}: React.PropsWithoutRef) => { + const [t] = useTranslation() + const [depositValue, setDepositValue] = useState(`${MIN_DEPOSIT_AMOUNT}`) + const [showDepositDialog, setShowDepositDialog] = useState(false) + const [activeRecord, setActiveRecord] = useState(null) + const [errorMessage, setErrorMessage] = useState('') + + const clearGeneratedTx = useCallback(() => { + dispatch({ + type: AppActions.ClearSendState, + payload: null, + }) + }, [dispatch]) + + const updateDepositValue = useCallback( + (value: string) => { + if (Number.isNaN(+value) || /[^\d.]/.test(value) || +value < 0) { + return + } + clearTimeout(timer) + timer = setTimeout(() => { + if (+value < MIN_DEPOSIT_AMOUNT) { + setErrorMessage(t('nervos-dao.minimal-fee-required', { minimal: MIN_DEPOSIT_AMOUNT })) + clearGeneratedTx() + } else { + setErrorMessage('') + generateDepositTx({ + feeRate: `${MEDIUM_FEE_RATE}`, + capacity: (BigInt(value) * BigInt(SHANNON_CKB_RATIO)).toString(), + walletID: wallet.id, + }).then(res => { + if (res.status === 1) { + dispatch({ + type: AppActions.UpdateGeneratedTx, + payload: res.result, + }) + } else { + clearGeneratedTx() + setErrorMessage(`${typeof res.message === 'string' ? res.message : res.message.content}`) + } + }) + } + }, 500) + setDepositValue(value) + }, + [clearGeneratedTx, dispatch, wallet.id, t] + ) + + useEffect(() => { + updateNervosDaoData({ walletID: wallet.id })(dispatch) + updateDepositValue(`${MIN_DEPOSIT_AMOUNT}`) + return () => { + clearNervosDaoData()(dispatch) + clearGeneratedTx() + } + }, [clearGeneratedTx, dispatch, updateDepositValue, wallet.id]) + + const onDepositDialogDismiss = () => { + setShowDepositDialog(false) + setDepositValue(`${MIN_DEPOSIT_AMOUNT}`) + setErrorMessage('') + } + + const onDepositDialogSubmit = () => { + setShowDepositDialog(false) + setDepositValue(`${MIN_DEPOSIT_AMOUNT}`) + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: wallet.id, + actionType: 'send', + }, + }) + } + + const onWithdrawDialogDismiss = () => { + setActiveRecord(null) + } + + const onWithdrawDialogSubmit = () => { + setErrorMessage('') + if (activeRecord) { + ;(activeRecord.depositOutPoint + ? generateClaimTx({ + walletID: wallet.id, + withdrawingOutPoint: activeRecord.outPoint, + depositOutPoint: activeRecord.depositOutPoint, + feeRate: `${MEDIUM_FEE_RATE}`, + }) + : generateWithdrawTx({ + walletID: wallet.id, + outPoint: activeRecord.outPoint, + feeRate: `${MEDIUM_FEE_RATE}`, + }) + ) + .then((res: any) => { + if (res.status === 1) { + dispatch({ + type: AppActions.UpdateGeneratedTx, + payload: res.result, + }) + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: wallet.id, + actionType: 'send', + }, + }) + } else { + clearGeneratedTx() + setErrorMessage(`${typeof res.message === 'string' ? res.message : res.message.content}`) + } + }) + .catch((err: Error) => { + dispatch({ + type: AppActions.AddNotification, + payload: { + type: 'alert', + timestamp: +new Date(), + content: err.message, + }, + }) + }) + } + setActiveRecord(null) + } + + const onActionClick = useCallback( + (e: any) => { + const { dataset } = e.target + const outPoint = { + txHash: dataset.txHash, + index: dataset.index, + } + const record = records.find(r => r.outPoint.txHash === outPoint.txHash && r.outPoint.index === outPoint.index) + if (record) { + setActiveRecord(record) + } + }, + [records] + ) + + const fee = `${shannonToCKBFormatter( + send.generatedTx ? send.generatedTx.fee || calculateFee(send.generatedTx) : '0' + )} CKB` + + const Records = useMemo(() => { + return ( + <> + + {t('nervos-dao.deposit-records')} + + + {records.map(record => { + let stage = 'deposited' + if (record.depositOutPoint) { + stage = 'withdrawing' + } + return ( + + ) + })} + + + ) + }, [records, t, tipBlockHash, onActionClick, tipBlockNumber, epoch]) + + let free = BigInt(0) + let locked = BigInt(0) + records.forEach(r => { + if (!r.depositOutPoint) { + locked += BigInt(r.capacity) + } else { + free += BigInt(r.capacity) + } + }) + + const EpochInfo = useMemo(() => { + if (!epoch) { + return + } + const epochInfo = epochParser(epoch) + return ( + + {`Epoch number: ${epochInfo.number}`} + {`Epoch index: ${epochInfo.index}`} + {`Epoch length: ${epochInfo.length}`} + + ) + }, [epoch]) + + return ( + <> + + + {wallet.name} + + + + + {`${t('nervos-dao.free')}: `} + {`${shannonToCKBFormatter(`${free}`)} CKB`} + + + {`${t('nervos-dao.locked')}: `} + {`${shannonToCKBFormatter(`${locked}`)} CKB`} + + + + setShowDepositDialog(true)} + /> + + + + + + {Records} + + updateDepositValue(value)} + onDismiss={onDepositDialogDismiss} + onSubmit={onDepositDialogSubmit} + onSlide={(value: number) => updateDepositValue(`${value}`)} + balance={wallet.balance} + isDepositing={sending} + errorMessage={errorMessage} + /> + {activeRecord ? ( + + ) : null} + + ) +} + +NervosDAO.displayName = 'NervosDAOao' + +export default NervosDAO diff --git a/packages/neuron-ui/src/components/Overview/index.tsx b/packages/neuron-ui/src/components/Overview/index.tsx index bfe23b7900..0dedc52dcf 100644 --- a/packages/neuron-ui/src/components/Overview/index.tsx +++ b/packages/neuron-ui/src/components/Overview/index.tsx @@ -100,7 +100,7 @@ const Overview = ({ }, { label: t('overview.epoch'), - value: epochParser(epoch).index, + value: epochParser(epoch).number.toString(), }, { label: t('overview.difficulty'), diff --git a/packages/neuron-ui/src/components/Receive/index.tsx b/packages/neuron-ui/src/components/Receive/index.tsx index 9c99c7cd8b..4ff1032878 100644 --- a/packages/neuron-ui/src/components/Receive/index.tsx +++ b/packages/neuron-ui/src/components/Receive/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback, useMemo } from 'react' import { RouteComponentProps } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Stack, Text, TextField, TooltipHost, Modal, FontSizes, IconButton } from 'office-ui-fabric-react' +import { Stack, Text, TextField, TooltipHost, Modal, IconButton } from 'office-ui-fabric-react' import { StateWithDispatch } from 'states/stateProvider/reducer' import QRCode from 'widgets/QRCode' @@ -28,35 +28,43 @@ const Receive = ({ addPopup('addr-copied')(dispatch) }, [accountAddress, dispatch]) + const Address = useMemo( + () => ( + + + + + + + + + ), + [copyAddress, accountAddress, t] + ) + if (!accountAddress) { return
{t('receive.address-not-found')}
} return ( <> - - - - - - - - - - + + + {`${t('receive.address', { network: accountAddress.startsWith('ckb') ? 'CKB Mainnet' : 'CKB Testnet' })}`} + + + {t('receive.prompt')} + setShowLargeQRCode(false)}> diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts index 1216c6ca25..7bea9fe347 100644 --- a/packages/neuron-ui/src/containers/Main/hooks.ts +++ b/packages/neuron-ui/src/containers/Main/hooks.ts @@ -19,7 +19,7 @@ import { SyncedBlockNumber as SyncedBlockNumberSubject, Command as CommandSubject, } from 'services/subjects' -import { ckbCore, getTipBlockNumber, getBlockchainInfo } from 'services/chain' +import { ckbCore, getBlockchainInfo, getTipHeader } from 'services/chain' import { ConnectionStatus, ErrorCode } from 'utils/const' import { networks as networksCache, @@ -32,47 +32,25 @@ const SYNC_INTERVAL_TIME = 10000 export const useSyncChainData = ({ chainURL, dispatch }: { chainURL: string; dispatch: StateDispatch }) => { useEffect(() => { - const syncTipNumber = () => - getTipBlockNumber() - .then(tipBlockNumber => { + const syncBlockchainInfo = () => { + Promise.all([getTipHeader(), getBlockchainInfo()]) + .then(([header, chainInfo]) => { dispatch({ - type: AppActions.UpdateTipBlockNumber, - payload: BigInt(tipBlockNumber).toString(), + type: AppActions.UpdateChainInfo, + payload: { + tipBlockNumber: `${BigInt(header.number)}`, + tipBlockHash: header.hash, + chain: chainInfo.chain, + difficulty: `${BigInt(chainInfo.difficulty)}`, + epoch: chainInfo.epoch, + }, }) + dispatch({ type: AppActions.ClearNotificationsOfCode, payload: ErrorCode.NodeDisconnected, }) }) - .catch((err: Error) => { - if (process.env.NODE_ENV === 'development') { - console.error(err) - } - }) - - const syncBlockchainInfo = () => { - getBlockchainInfo() - .then(info => { - if (info) { - const { chain = '', difficulty: difficultyHex = '', epoch: epochHex = '', alerts = [] } = info - const difficulty = BigInt(difficultyHex).toString() - const epoch = BigInt(epochHex).toString() - if (alerts.length) { - alerts.forEach(a => { - // TODO: display alerts in Notification - console.info(a) - }) - } - dispatch({ - type: AppActions.UpdateChainInfo, - payload: { - chain, - difficulty, - epoch, - }, - }) - } - }) .catch((err: Error) => { if (process.env.NODE_ENV === 'development') { console.warn(err) @@ -82,10 +60,8 @@ export const useSyncChainData = ({ chainURL, dispatch }: { chainURL: string; dis clearInterval(timer) if (chainURL) { ckbCore.setNode(chainURL) - syncTipNumber() syncBlockchainInfo() timer = setInterval(() => { - syncTipNumber() syncBlockchainInfo() }, SYNC_INTERVAL_TIME) } else { diff --git a/packages/neuron-ui/src/containers/Main/index.tsx b/packages/neuron-ui/src/containers/Main/index.tsx index be2ba32dde..a065e52df6 100644 --- a/packages/neuron-ui/src/containers/Main/index.tsx +++ b/packages/neuron-ui/src/containers/Main/index.tsx @@ -18,6 +18,7 @@ import NetworkEditor from 'components/NetworkEditor' import WalletEditor from 'components/WalletEditor' import LaunchScreen from 'components/LaunchScreen' import PasswordRequest from 'components/PasswordRequest' +import NervosDAO from 'components/NervosDAO' import { Routes } from 'utils/const' @@ -107,6 +108,12 @@ export const mainContents: CustomRouter.Route[] = [ exact: false, comp: PasswordRequest, }, + { + name: `NervosDAO`, + path: Routes.NervosDAO, + exact: true, + comp: NervosDAO, + }, ] const MainContent = ({ diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx index 2d6068681d..9e39018fe8 100644 --- a/packages/neuron-ui/src/containers/Navbar/index.tsx +++ b/packages/neuron-ui/src/containers/Navbar/index.tsx @@ -13,6 +13,7 @@ const menuItems = [ { name: 'navbar.send', key: Routes.Send.slice(1), url: Routes.Send }, { name: 'navbar.receive', key: Routes.Receive.slice(1), url: Routes.Receive }, { name: 'navbar.history', key: Routes.History.slice(1), url: Routes.History }, + { name: 'navbar.nervos-dao', key: Routes.NervosDAO.slice(1), url: Routes.NervosDAO }, { name: 'navbar.addresses', key: Routes.Addresses.slice(1), url: Routes.Addresses }, ] diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 4dbcb0152f..1258eca584 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -10,7 +10,8 @@ "receive": "Receive", "history": "History", "addresses": "Addresses", - "settings": "Settings" + "settings": "Settings", + "nervos-dao": "Nervos DAO" }, "overview": { "balance": "Balance", @@ -106,7 +107,8 @@ "click-to-copy": "Click to copy the address", "address-not-found": "Address not found", "prompt": "Neuron picks a new receiving address for better privacy. Please go to the Address Book if you want to use a previously used receiving address.", - "address-qrcode": "Address QR Code" + "address-qrcode": "Address QR Code", + "address": "{{network}} Address" }, "history": { "meta": "Meta", @@ -304,6 +306,32 @@ "last-page": "Last page", "page": "Page", "selected": "Current page" + }, + "nervos-dao": { + "free": "Free", + "locked": "Locked", + "deposit": "Deposit", + "deposit-records": "Deposit Records", + "apy": "APY", + "deposit-at": "Deposit at {{time}}", + "claim": "Claim", + "withdraw": "Withdraw", + "fee": "Transaction fee", + "deposit-to-nervos-dao": "Deposit to Nervos DAO", + "withdraw-from-nervos-dao": "Withdraw from Nervos DAO", + "notice": "Notice", + "cancel": "Cancel", + "proceed": "Proceed", + "deposit-value": "Deposit", + "interest": "Interest", + "yield": "Yield", + "notice-wait-time": "Notice: You need to wait {{epochs}} epochs {{blocks}} blocks(~{{days}} days) to claim the saving.", + "deposit-terms": "Nervos DAO is a system layer decentralized infrastructure. Your saving here is secure.\nAccording to the Nervos DAO protocol, you need at least 180 epochs to withdraw your deposit", + "deposited-action-label": "Withdraw", + "withdrawing-action-label": "Claim", + "minimal-fee-required": "The minimum deposit capacity is {{minimal}} CKB", + "interest-accumulated": "{{blockNumber}} blocks interest accumulated", + "blocks-left": "{{epochs}} epochs {{blocks}} blocks left(~{{days}} days)" } } } diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index c8dd4d46f7..e01e5e5d4b 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -10,7 +10,8 @@ "receive": "收款", "history": "交易历史", "addresses": "地址管理", - "settings": "设置" + "settings": "设置", + "nervos-dao": "Nervos DAO" }, "overview": { "balance": "余额", @@ -106,7 +107,8 @@ "click-to-copy": "点击复制地址", "address-not-found": "未找到地址", "prompt": "为了保护隐私,Neuron 会自动选择一个新收款地址。如果您想使用旧的收款地址,请访问地址管理页面。", - "address-qrcode": "地址二维码" + "address-qrcode": "地址二维码", + "address": "{{network}} 地址" }, "history": { "meta": "元信息", @@ -304,6 +306,32 @@ "last-page": "尾页", "page": "页码", "selected": "当前页" + }, + "nervos-dao": { + "free": "当前可用", + "locked": "已锁定", + "deposit": "存入", + "deposit-records": "存款记录", + "apy": "预期年化利率", + "deposit-at": "存入于{{time}}", + "claim": "Claim", + "withdraw": "Withdraw", + "fee": "手续费", + "deposit-to-nervos-dao": "存入 Nervos DAO", + "withdraw-from-nervos-dao": "从 Nervos DAO 取出", + "notice": "注意", + "cancel": "取消", + "proceed": "继续", + "deposit-value": "存款", + "interest": "利息", + "yield": "Yield", + "notice-wait-time": "注意: 您需要等待 {{epochs}} epochs {{blocks}} 区块(~{{days}}天)完成最终取款。", + "deposit-terms": "Nervos DAO is a system layer decentralized infrastructure. Your saving here is secure.\nAccording to the Nervos DAO protocol, you need at least 180 epochs to withdraw your deposit", + "deposited-action-label": "Withdraw", + "withdrawing-action-label": "Claim", + "minimal-fee-required": "存入金额应不少于 {{minimal}} CKB", + "interest-accumulated": "已累计 {{blockNumber}} 个块的利息", + "blocks-left": " 还需等待 {{epochs}} epochs {{blocks}} 个块(~{{days}} 天)" } } } diff --git a/packages/neuron-ui/src/services/chain.ts b/packages/neuron-ui/src/services/chain.ts index 71aa296ecf..be8bd723b9 100644 --- a/packages/neuron-ui/src/services/chain.ts +++ b/packages/neuron-ui/src/services/chain.ts @@ -2,10 +2,17 @@ import CKBCore from '@nervosnetwork/ckb-sdk-core' export const ckbCore = new CKBCore('') -export const { getTipBlockNumber, getBlockchainInfo } = ckbCore.rpc +ckbCore.rpc.addMethod({ + name: 'calculateDaoMaximumWithdraw', + method: 'calculate_dao_maximum_withdraw', + paramsFormatters: [ckbCore.rpc.paramsFormatter.toOutPoint, ckbCore.rpc.paramsFormatter.toHash], +}) + +export const { getBlockchainInfo, getTipHeader, getBlockByNumber } = ckbCore.rpc export default { ckbCore, - getTipBlockNumber, getBlockchainInfo, + getTipHeader, + getBlockByNumber, } diff --git a/packages/neuron-ui/src/services/remote/apiMethodWrapper.ts b/packages/neuron-ui/src/services/remote/apiMethodWrapper.ts index d21f8db749..3d09cebaf5 100644 --- a/packages/neuron-ui/src/services/remote/apiMethodWrapper.ts +++ b/packages/neuron-ui/src/services/remote/apiMethodWrapper.ts @@ -47,8 +47,9 @@ export const apiMethodWrapper = ( })) if (process.env.NODE_ENV === 'development' && window.localStorage.getItem('log-response')) { - console.group('api controller') - console.info(JSON.stringify(res, null, 2)) + console.group(callControllerMethod) + console.info(`params: ${JSON.stringify(realParams, null, 2)}`) + console.info(`res: ${JSON.stringify(res, null, 2)}`) console.groupEnd() } diff --git a/packages/neuron-ui/src/services/remote/wallets.ts b/packages/neuron-ui/src/services/remote/wallets.ts index 2ab31713d1..0b807ec406 100644 --- a/packages/neuron-ui/src/services/remote/wallets.ts +++ b/packages/neuron-ui/src/services/remote/wallets.ts @@ -38,6 +38,20 @@ export const updateAddressDescription = apiMethodWrapper(api => (params: Control api.updateAddressDescription(params) ) +export const getNervosDaoData = apiMethodWrapper(api => (params: Controller.GetNervosDaoDataParams) => + api.getDaoCells(params) +) + +export const generateDepositTx = apiMethodWrapper(api => (params: Controller.DepositParams) => + api.generateDepositTx(params) +) + +export const generateWithdrawTx = apiMethodWrapper(api => (params: Controller.WithdrawParams) => + api.startWithdrawFromDao(params) +) + +export const generateClaimTx = apiMethodWrapper(api => (params: Controller.ClaimParams) => api.withdrawFromDao(params)) + export default { updateWallet, getWalletList, @@ -51,4 +65,8 @@ export default { sendTx, getAddressesByWalletID, updateAddressDescription, + getNervosDaoData, + generateDepositTx, + generateWithdrawTx, + generateClaimTx, } diff --git a/packages/neuron-ui/src/states/initStates/app.ts b/packages/neuron-ui/src/states/initStates/app.ts index c4532a26db..690b1e9b43 100644 --- a/packages/neuron-ui/src/states/initStates/app.ts +++ b/packages/neuron-ui/src/states/initStates/app.ts @@ -2,6 +2,7 @@ import { CapacityUnit } from 'utils/const' const appState: State.App = { tipBlockNumber: '', + tipBlockHash: '', chain: '', difficulty: '', epoch: '', diff --git a/packages/neuron-ui/src/states/initStates/index.ts b/packages/neuron-ui/src/states/initStates/index.ts index 456175eb1d..9cac8f7eee 100644 --- a/packages/neuron-ui/src/states/initStates/index.ts +++ b/packages/neuron-ui/src/states/initStates/index.ts @@ -2,17 +2,20 @@ import app from './app' import chain from './chain' import wallet from './wallet' import settings from './settings' +import nervosDAO from './nervosDAO' export * from './app' export * from './chain' export * from './wallet' export * from './settings' +export * from './nervosDAO' const initStates = { app, chain, wallet, settings, + nervosDAO, } export default initStates diff --git a/packages/neuron-ui/src/states/initStates/nervosDAO.ts b/packages/neuron-ui/src/states/initStates/nervosDAO.ts new file mode 100644 index 0000000000..da0414c1e3 --- /dev/null +++ b/packages/neuron-ui/src/states/initStates/nervosDAO.ts @@ -0,0 +1,5 @@ +export const emptyNervosDaoData: State.NervosDAO = { + records: [], +} + +export default emptyNervosDaoData diff --git a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts index 4e59511fbf..1a44e0961e 100644 --- a/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts +++ b/packages/neuron-ui/src/states/stateProvider/actionCreators/wallets.ts @@ -7,6 +7,7 @@ import { getCurrentWallet, updateWallet, setCurrentWallet as setRemoteCurrentWallet, + getNervosDaoData, sendTx, getAddressesByWalletID, updateAddressDescription as updateRemoteAddressDescription, @@ -15,6 +16,7 @@ import { showErrorMessage, } from 'services/remote' import { emptyWallet } from 'states/initStates/wallet' +import { emptyNervosDaoData } from 'states/initStates/nervosDAO' import { WalletWizardPath } from 'components/WalletWizard' import i18n from 'utils/i18n' import { wallets as walletsCache, currentWallet as currentWalletCache } from 'services/localCache' @@ -265,6 +267,27 @@ export const backupWallet = (params: Controller.BackupWalletParams) => (dispatch } }) } + +export const updateNervosDaoData = (walletID: Controller.GetNervosDaoDataParams) => (dispatch: StateDispatch) => { + getNervosDaoData(walletID).then(res => { + if (res.status === 1) { + dispatch({ + type: NeuronWalletActions.UpdateNervosDaoData, + payload: { records: res.result }, + }) + } else { + addNotification(failureResToNotification(res))(dispatch) + } + }) +} + +export const clearNervosDaoData = () => (dispatch: StateDispatch) => { + dispatch({ + type: NeuronWalletActions.UpdateNervosDaoData, + payload: emptyNervosDaoData, + }) +} + export default { createWalletWithMnemonic, importWalletWithMnemonic, @@ -277,4 +300,5 @@ export default { updateAddressDescription, deleteWallet, backupWallet, + updateNervosDaoData, } diff --git a/packages/neuron-ui/src/states/stateProvider/reducer.ts b/packages/neuron-ui/src/states/stateProvider/reducer.ts index 84beb07297..c464867d10 100644 --- a/packages/neuron-ui/src/states/stateProvider/reducer.ts +++ b/packages/neuron-ui/src/states/stateProvider/reducer.ts @@ -18,6 +18,8 @@ export enum NeuronWalletActions { // Connection UpdateConnectionStatus = 'updateConnectionStatus', UpdateSyncedBlockNumber = 'updateSyncedBlockNumber', + // dao + UpdateNervosDaoData = 'updateNervosDaoData', } export enum AppActions { UpdateTransactionID = 'updateTransactionID', @@ -38,7 +40,6 @@ export enum AppActions { RequestPassword = 'requestPassword', DismissPasswordRequest = 'dismissPasswordRequest', UpdatePassword = 'updatePassword', - UpdateTipBlockNumber = 'updateTipBlockNumber', UpdateChainInfo = 'updateChainInfo', UpdateLoadings = 'updateLoadings', @@ -223,19 +224,13 @@ export const reducer = ( }, } } - // Actions of App - case AppActions.UpdateTipBlockNumber: { - /** - * paylaod: tipBlockNumber - */ + case NeuronWalletActions.UpdateNervosDaoData: { return { ...state, - app: { - ...state.app, - tipBlockNumber: payload, - }, + nervosDAO: payload, } } + // Actions of App case AppActions.UpdateChainInfo: { return { ...state, diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 377191480b..e214ee89dd 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -74,6 +74,7 @@ declare namespace State { interface App { tipBlockNumber: string + tipBlockHash: string chain: string difficulty: string epoch: string @@ -148,11 +149,45 @@ declare namespace State { wallets: WalletIdentity[] } + interface NervosDAORecord { + blockNumber: string + blockHash: string + capacity: string + lock: { + codeHash: string + hashType: string + args: string + } + lockHash: string + outPoint: { + txHash: string + index: string + } + depositOutPoint?: { + txHash: string + index: string + } + status: 'live' | 'dead' + type: { + codeHash: string + hashType: string + args: string + } + typeHash: string | null + daoData: string + timestamp: string + } + + interface NervosDAO { + records: NervosDAORecord[] + } + interface AppWithNeuronWallet { app: App chain: Chain settings: Settings wallet: Wallet + nervosDAO: NervosDAO } } diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index b7d56a6a98..d111975962 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -76,4 +76,39 @@ declare namespace Controller { description: string } type SetSkipAndTypeParam = boolean + + type GetNervosDaoDataParams = { + walletID: string + } + + // the generate deposit tx method in neuron wallet + interface DepositParams { + walletID: string + capacity: string + feeRate: string + } + + // the start withdraw from dao method in neuron wallet + interface WithdrawParams { + walletID: string + outPoint: { + txHash: string + index: string + } + feeRate: string + } + + // the withdraw from dao method in neuron wallet + interface ClaimParams { + walletID: string + depositOutPoint: { + txHash: string + index: string + } + withdrawingOutPoint: { + txHash: string + index: string + } + feeRate: string + } } diff --git a/packages/neuron-ui/src/utils/calculateAPY.ts b/packages/neuron-ui/src/utils/calculateAPY.ts new file mode 100644 index 0000000000..4d08701d29 --- /dev/null +++ b/packages/neuron-ui/src/utils/calculateAPY.ts @@ -0,0 +1,7 @@ +const YEAR = 365 * 30 * 24 * 60 * 60 * 1000 + +export default (interest: string, amount: string, duration: string) => { + const BASE = 10000 + const v = Math.floor(+((BigInt(interest) * BigInt(BASE)) / BigInt(amount)).toString()) * (YEAR / +duration) + return `${(v / BASE).toFixed(2)}` +} diff --git a/packages/neuron-ui/src/utils/calculateClaimEpochNumber.ts b/packages/neuron-ui/src/utils/calculateClaimEpochNumber.ts new file mode 100644 index 0000000000..5ebadb8e7c --- /dev/null +++ b/packages/neuron-ui/src/utils/calculateClaimEpochNumber.ts @@ -0,0 +1,20 @@ +import { WITHDRAW_EPOCHS } from 'utils/const' + +interface EpochInfo { + index: bigint + number: bigint + length: bigint +} + +export default (depositEpochInfo: EpochInfo, currentEpochInfo: EpochInfo) => { + let depositedEpochs = currentEpochInfo.number - depositEpochInfo.number + const depositEpochFraction = depositEpochInfo.index * currentEpochInfo.length + const currentEpochFraction = currentEpochInfo.index * depositEpochInfo.length + if (currentEpochFraction > depositEpochFraction) { + depositedEpochs += BigInt(1) + } + const minLockEpochs = + ((depositedEpochs + BigInt(WITHDRAW_EPOCHS - 1)) / BigInt(WITHDRAW_EPOCHS)) * BigInt(WITHDRAW_EPOCHS) + const targetEpochNumber = depositEpochInfo.number + minLockEpochs + return targetEpochNumber +} diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 135ae4ab4f..b56c8dffd7 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -11,6 +11,13 @@ export const CONFIRMATION_THRESHOLD = 30 export const MAX_DECIMAL_DIGITS = 8 +export const MIN_DEPOSIT_AMOUNT = 102 + +export const SHANNON_CKB_RATIO = 1e8 + +export const MEDIUM_FEE_RATE = 6000 +export const WITHDRAW_EPOCHS = 180 + export enum ConnectionStatus { Online = 'online', Offline = 'offline', @@ -36,6 +43,7 @@ export enum Routes { NetworkEditor = '/network', WalletEditor = '/editwallet', Prompt = '/prompt', + NervosDAO = '/nervos-dao', } export enum CapacityUnit { diff --git a/packages/neuron-ui/src/utils/formatters.ts b/packages/neuron-ui/src/utils/formatters.ts index a39bbc2835..0240e1a73a 100644 --- a/packages/neuron-ui/src/utils/formatters.ts +++ b/packages/neuron-ui/src/utils/formatters.ts @@ -133,12 +133,13 @@ export const shannonToCKBFormatter = (shannon: string = '0', showPositiveSign?: return +unsignedCKB === 0 ? '0' : `${sign}${unsignedCKB}` } -export const localNumberFormatter = (num: string | number = 0) => { - if (Number.isNaN(+num)) { +export const localNumberFormatter = (num: string | number | bigint = 0) => { + if (typeof num !== 'bigint' && Number.isNaN(+num)) { console.warn(`Nuumber is not a valid number`) return num } - return numberFormatter.format(+num) + const n: any = BigInt(num) + return numberFormatter.format(n) } export const uniformTimeFormatter = (time: string | number | Date) => { diff --git a/packages/neuron-ui/src/utils/parsers.ts b/packages/neuron-ui/src/utils/parsers.ts index 65a76e0f9a..eb89147814 100644 --- a/packages/neuron-ui/src/utils/parsers.ts +++ b/packages/neuron-ui/src/utils/parsers.ts @@ -24,8 +24,11 @@ export const prompt = (search: string) => { export const queryParsers = { history, prompt } export const epochParser = (epoch: string) => { + const e = BigInt(epoch) return { - index: +epoch & 0xffff, + length: (e >> BigInt(40)) & BigInt(0xffff), + index: (e >> BigInt(24)) & BigInt(0xffff), + number: e & BigInt(0xffffff), } } diff --git a/packages/neuron-ui/src/widgets/QRCode/index.tsx b/packages/neuron-ui/src/widgets/QRCode/index.tsx index af5ce69a19..7ef814096d 100644 --- a/packages/neuron-ui/src/widgets/QRCode/index.tsx +++ b/packages/neuron-ui/src/widgets/QRCode/index.tsx @@ -82,6 +82,7 @@ const QRCode = ({ includeMargin = false, exportable = false, dispatch, + remark, }: { value: string size: number @@ -93,6 +94,7 @@ const QRCode = ({ includeMargin?: boolean exportable?: boolean dispatch: StateDispatch + remark?: JSX.Element }) => { const [t] = useTranslation() const qrcode = new QRCodeImpl(-1, level) @@ -156,8 +158,9 @@ const QRCode = ({ + {remark || null} {exportable ? ( - + {t('qrcode.copy')} {t('qrcode.save')} diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json index 150733eb54..429f913134 100644 --- a/packages/neuron-wallet/package.json +++ b/packages/neuron-wallet/package.json @@ -3,7 +3,7 @@ "productName": "Neuron", "description": "CKB Neuron Wallet", "homepage": "https://www.nervos.org/", - "version": "0.24.1", + "version": "0.24.2", "private": true, "author": { "name": "Nervos Core Dev", @@ -64,7 +64,7 @@ "electron-devtools-installer": "2.2.4", "electron-notarize": "0.1.1", "lint-staged": "9.2.5", - "neuron-ui": "0.24.1", + "neuron-ui": "0.24.2", "rimraf": "3.0.0", "spectron": "8.0.0", "ts-transformer-imports": "0.4.3", diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 3648782f6e..189dc30381 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -269,7 +269,7 @@ export default class ApiController { @MapApiResponse public static async getTransactionList( - params: Controller.Params.TransactionsByKeywords, + params: Controller.Params.TransactionsByKeywords ) { return TransactionsController.getAllByKeywords(params) } @@ -284,15 +284,15 @@ export default class ApiController { return TransactionsController.updateDescription(params) } + @MapApiResponse public static async showTransactionDetails(hash: string) { showWindow(`${env.mainURL}#/transaction/${hash}`, i18n.t(`messageBox.transaction.title`, { hash })) } // Dao - @MapApiResponse public static async getDaoCells( - params: Controller.Params.GetDaoCellsParams, + params: Controller.Params.GetDaoCellsParams ) { return DaoController.getDaoCells(params) } diff --git a/packages/neuron-wallet/src/controllers/app/index.ts b/packages/neuron-wallet/src/controllers/app/index.ts index 3d52c5eebc..e5e404d159 100644 --- a/packages/neuron-wallet/src/controllers/app/index.ts +++ b/packages/neuron-wallet/src/controllers/app/index.ts @@ -38,7 +38,7 @@ export default class AppController { createWindow = () => { const windowState = windowStateKeeper({ defaultWidth: 1366, - defaultHeight: 768, + defaultHeight: 900, }) this.mainWindow = new BrowserWindow({ @@ -46,7 +46,7 @@ export default class AppController { y: windowState.y, width: windowState.width, height: windowState.height, - minWidth: 800, + minWidth: 900, minHeight: 600, show: false, backgroundColor: '#e9ecef', diff --git a/packages/neuron-wallet/src/services/wallets.ts b/packages/neuron-wallet/src/services/wallets.ts index 81c922e8fe..48765c75b5 100644 --- a/packages/neuron-wallet/src/services/wallets.ts +++ b/packages/neuron-wallet/src/services/wallets.ts @@ -749,13 +749,16 @@ export default class WalletService { } public calculateDaoMaximumWithdraw = async (depositOutPoint: OutPoint, withdrawBlockHash: string): Promise => { - const calculateDaoMaximumWithdrawMethod = { - name: 'calculateDaoMaximumWithdraw', - method: 'calculate_dao_maximum_withdraw', - paramsFormatters: [core.rpc.paramsFormatter.toOutPoint, core.rpc.paramsFormatter.toHash], + if (!(core.rpc as any).calculateDaoMaximumWithdraw) { + const calculateDaoMaximumWithdrawMethod = { + name: 'calculateDaoMaximumWithdraw', + method: 'calculate_dao_maximum_withdraw', + paramsFormatters: [core.rpc.paramsFormatter.toOutPoint, core.rpc.paramsFormatter.toHash], + } + + core.rpc.addMethod(calculateDaoMaximumWithdrawMethod) } - core.rpc.addMethod(calculateDaoMaximumWithdrawMethod) const result = await (core.rpc as any).calculateDaoMaximumWithdraw( ConvertTo.toSdkOutPoint(depositOutPoint), diff --git a/yarn.lock b/yarn.lock index 78f5e5031e..d9217b3d51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14005,10 +14005,10 @@ react-hotkeys@2.0.0-pre4: dependencies: prop-types "^15.6.1" -react-i18next@10.12.2: - version "10.12.2" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-10.12.2.tgz#2f2d79b84c1f3e3844d110e4c9d5c73a48f99418" - integrity sha512-tZCBhUz8rJtgmTi1z2pWEoQBvFHjwOS2+TQ7L4RfJq1LDirXi2m+3Pwg6gUECVCGenWomLufWNiTwRF9fmBrUQ== +react-i18next@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.0.1.tgz#179b10cf0026677739fa96df0a7b5494df435bd1" + integrity sha512-TazSMob+cEztaaMcX3IQQLltzv4+b3cl6dfs9tUMVj2fD0TRDbTsQ75AaLncw6jqZf08I0yAGHeJnqbBE0eZDw== dependencies: "@babel/runtime" "^7.3.1" html-parse-stringify2 "2.0.1"