From 09fa9c558a3dfbc830a521bd349efc349eee436a Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Sun, 18 Feb 2024 21:36:52 +0800 Subject: [PATCH] feat: Non-Native token transfer in history add more details about CKB --- .../CellInfoDialog/cellInfoDialog.module.scss | 272 +++++++++++++++++ .../src/components/CellInfoDialog/index.tsx | 158 ++++++++++ .../src/components/CellManagement/hooks.ts | 57 +--- .../src/components/CellManagement/index.tsx | 75 +---- .../formattedTokenAmount.module.scss | 9 +- .../components/FormattedTokenAmount/index.tsx | 52 +++- .../components/History/history.module.scss | 10 +- .../neuron-ui/src/components/History/hooks.ts | 4 +- .../src/components/History/index.tsx | 33 +- .../historyDetailPage.module.scss | 75 +++-- .../src/components/HistoryDetailPage/hooks.ts | 32 ++ .../components/HistoryDetailPage/index.tsx | 286 +++++++++++++----- .../transactionStatusWrap.module.scss | 1 + packages/neuron-ui/src/locales/en.json | 21 +- packages/neuron-ui/src/locales/es.json | 21 +- packages/neuron-ui/src/locales/fr.json | 21 +- packages/neuron-ui/src/locales/zh-tw.json | 19 +- packages/neuron-ui/src/locales/zh.json | 21 +- packages/neuron-ui/src/services/chain.ts | 2 - .../src/services/remote/remoteApiWrapper.ts | 1 + .../src/services/remote/transactions.ts | 2 + .../neuron-ui/src/services/remote/wallets.ts | 1 + .../neuron-ui/src/tests/parsers/index.test.ts | 18 ++ packages/neuron-ui/src/types/App/index.d.ts | 1 + packages/neuron-ui/src/utils/parsers.ts | 2 + .../neuron-ui/src/widgets/Switch/index.tsx | 28 ++ .../src/widgets/Switch/switch.module.scss | 34 +++ .../neuron-ui/src/widgets/Table/index.tsx | 34 ++- packages/neuron-wallet/src/controllers/api.ts | 12 +- .../src/controllers/cell-management.ts | 8 +- packages/neuron-wallet/src/controllers/dao.ts | 30 +- .../src/controllers/transactions.ts | 14 +- .../models/chain/transaction-with-status.ts | 10 +- .../src/models/chain/transaction.ts | 1 + packages/neuron-wallet/src/services/cells.ts | 39 ++- .../src/services/tx/transaction-service.ts | 52 +++- .../neuron-wallet/src/types/controller.d.ts | 2 + .../tests/services/cells.test.ts | 145 +++++++-- 38 files changed, 1265 insertions(+), 338 deletions(-) create mode 100644 packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss create mode 100644 packages/neuron-ui/src/components/CellInfoDialog/index.tsx create mode 100644 packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts create mode 100644 packages/neuron-ui/src/widgets/Switch/index.tsx create mode 100644 packages/neuron-ui/src/widgets/Switch/switch.module.scss diff --git a/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss b/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss new file mode 100644 index 0000000000..148d7b4f99 --- /dev/null +++ b/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss @@ -0,0 +1,272 @@ +.cellInfoDialog { + min-width: 650px; +} + +.title { + display: flex; + align-items: center; + gap: 4px; + font-size: 14px; + + .outPoint { + display: flex; + padding: 5px 8px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 16px; + border: 1px solid var(--divide-line-color); + background: var(--input-disabled-color); + font-weight: 400; + color: var(--main-text-color); + + .hash { + font-family: 'JetBrains Mono'; + } + + & > svg { + path { + fill: var(--secondary-text-color); + } + } + } +} + +.head { + display: flex; + gap: 4px; + align-items: center; + margin-bottom: 12px; + + & > svg { + transform: rotate(-90deg); + path { + fill: var(--border-color); + } + } + + [data-type='hash'] { + color: var(--third-text-color); + font-family: 'JetBrains Mono'; + } + + .liveIcon { + width: 8px; + height: 8px; + border-radius: 8px; + background-color: var(--primary-color); + } +} + +.tabsClassName { + .tabsWrapClassName { + position: relative; + display: flex; + margin-bottom: 16px; + border-bottom: 1px solid var(--divide-line-color); + + .tabsColumnClassName { + position: relative; + padding: 10px 20px; + font-size: 14px; + font-weight: 500; + color: var(--tabs-default-color); + background-color: transparent; + border: none; + margin-right: 10px; + + &:hover { + font-weight: bold; + color: var(--tabs-active); + } + + &.active { + color: var(--tabs-active); + border-bottom: 2px solid var(--tabs-active); + } + } + } +} + +.scriptTable { + border-collapse: separate; + border-spacing: 0px; + width: 100%; + color: var(--main-text-color); + + td { + padding: 8px; + border: 1px solid var(--divide-line-color); + text-align: center; + } + + thead { + td { + border-bottom: 0; + &:nth-child(1) { + border-top-left-radius: 8px; + border-right: 0; + } + &:nth-last-child(1) { + border-top-right-radius: 8px; + } + } + } + + tbody { + tr { + &:not(:nth-last-child(1)) { + td { + border-bottom: 0; + } + } + &:nth-last-child(1) { + td { + &:nth-child(1) { + border-bottom-left-radius: 8px; + } + &:nth-last-child(1) { + border-bottom-right-radius: 8px; + } + } + } + } + td { + &:not(:nth-last-child(1)) { + border-right: 0; + } + } + } +} + +.content { + position: relative; + + .switchFormat { + position: absolute; + right: 0; + top: 10px; + color: var(--main-text-color); + display: flex; + align-items: center; + } +} + +.preStyle { + border: 1px solid var(--lock-info-title-border); + border-radius: 12px; + padding: 16px; + overflow-y: auto; + margin: 0; + color: var(--main-text-color); + font-family: 'JetBrains Mono'; + display: block; + width: calc(100% - 32px); + + &::selection { + background-color: var(--primary-color); + color: var(--primary-text-color); + } +} + +.capacityUsed { + padding: 0 16px 16px; + .slider { + width: 616px; + height: 16px; + border-radius: 8px; + border: 1px solid var(--divide-line-color); + background-color: var(--secondary-background-color); + position: relative; + + & > div { + position: absolute; + left: -1px; + top: -1px; + height: 100%; + border-radius: 8px; + border: 1px solid var(--primary-color); + background-color: var(--primary-color); + } + } + .capacityDetail { + color: var(--main-text-color); + font-style: normal; + font-weight: 400; + line-height: normal; + margin-top: 12px; + } +} + +.dataAction { + position: fixed; + width: 496px; + border-radius: 8px; + background: var(--secondary-background-color); + box-shadow: 0px 2px 10px 0px var(--main-shadow-color); + + .dataView { + color: var(--main-text-color); + font-weight: 500; + padding: 12px; + border-bottom: 1px solid var(--divide-line-color); + display: flex; + align-items: center; + + .formatTypeBtn { + border: none; + cursor: pointer; + background-color: transparent; + + &:hover, + &[data-activity='true'] { + color: var(--primary-color); + } + + &[data-open='true'] { + & > svg { + transform: rotate(180deg); + } + } + + & > svg { + margin-left: 4px; + path { + fill: var(--primary-color); + } + } + } + + .dropdown { + position: relative; + margin-left: 12px; + .formatTypes { + position: absolute; + top: 100%; + left: 0; + background-color: var(--secondary-background-color); + border-radius: 4px; + box-shadow: 0px 2px 10px 0px var(--main-shadow-color); + display: flex; + flex-direction: column; + padding: 12px 8px; + margin: 0; + gap: 16px; + white-space: nowrap; + z-index: 10; + } + } + } + + .dataFormat { + padding: 12px; + word-wrap: break-word; + } +} + +.copyAddress { + margin-left: 4px; + position: relative; + top: 4px; + cursor: pointer; +} diff --git a/packages/neuron-ui/src/components/CellInfoDialog/index.tsx b/packages/neuron-ui/src/components/CellInfoDialog/index.tsx new file mode 100644 index 0000000000..20bc08a669 --- /dev/null +++ b/packages/neuron-ui/src/components/CellInfoDialog/index.tsx @@ -0,0 +1,158 @@ +import React, { useMemo, useState } from 'react' +import Dialog from 'widgets/Dialog' +import { calculateUsedCapacity, shannonToCKBFormatter } from 'utils' +import { useTranslation } from 'react-i18next' +import Tabs from 'widgets/Tabs' +import { type TFunction } from 'i18next' +import { Script } from '@ckb-lumos/base' +import Switch from 'widgets/Switch' +import styles from './cellInfoDialog.module.scss' + +type ScriptRenderType = 'table' | 'raw' + +const ScriptRender = ({ script, renderType }: { script?: Script; renderType: ScriptRenderType }) => { + if (renderType === 'raw') { + const scriptRaw = script + ? `{ + "code_hash": "${script.codeHash}" + "hash_type": "${script.hashType}" + "args": "${script.args}" +}` + : `{ + "null" +}` + return
{scriptRaw}
+ } + return ( + + + + + + + + + + + + + + + + + + + + + +
KEYVALUE
code_hash{script?.codeHash ?? '--'}
hash_type{script?.hashType ?? '--'}
args{script?.args ?? '--'}
+ ) +} + +const tabIds = { + lock: 'lock', + type: 'type', + data: 'data', + capacityUsage: 'capacityUsage', +} + +const useTabs = ({ t, output }: { t: TFunction; output?: State.DetailedOutput }) => { + const [scriptRenderType, setScriptRenderType] = useState('table') + const usedCapacity = useMemo(() => (output ? calculateUsedCapacity(output) : 0), [output]) + const tabs = [ + { + id: tabIds.lock, + label: 'Lock Script', + render() { + return + }, + }, + { + id: tabIds.type, + label: 'Type Script', + render() { + return + }, + }, + { + id: tabIds.data, + label: 'Data', + render() { + return
{output?.data}
+ }, + }, + { + id: tabIds.capacityUsage, + label: t('cell-manage.cell-detail-dialog.capacity-used'), + render() { + return ( +
+
+
+
+
+ {`Occupied ${usedCapacity} CKB, Declared ${shannonToCKBFormatter(output?.capacity ?? '')} CKB`} +
+
+ ) + }, + }, + ] + const [currentTab, setCurrentTab] = useState(tabs[0]) + return { + currentTab, + setCurrentTab, + tabs, + setScriptRenderType, + scriptRenderType, + } +} + +const CellInfoDialog = ({ onCancel, output }: { onCancel: () => void; output?: State.DetailedOutput }) => { + const [t] = useTranslation() + + const { tabs, currentTab, setCurrentTab, scriptRenderType, setScriptRenderType } = useTabs({ + t, + output, + }) + if (!output) { + return null + } + return ( + + {t('cell-manage.cell-detail-dialog.title')} + + } + onCancel={onCancel} + showFooter={false} + className={styles.cellInfoDialog} + > +
+ + {[tabIds.lock, tabIds.type].includes(currentTab.id) ? ( +
+ Raw Data   + setScriptRenderType(checked ? 'raw' : 'table')} + /> +
+ ) : null} +
+
+ ) +} + +CellInfoDialog.displayName = 'CellInfoDialog' + +export default CellInfoDialog diff --git a/packages/neuron-ui/src/components/CellManagement/hooks.ts b/packages/neuron-ui/src/components/CellManagement/hooks.ts index 6dca24e92f..9076750ece 100644 --- a/packages/neuron-ui/src/components/CellManagement/hooks.ts +++ b/packages/neuron-ui/src/components/CellManagement/hooks.ts @@ -1,21 +1,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { - openExternal, getLiveCells, updateLiveCellsLocalInfo, updateLiveCellsLockStatus as updateLiveCellsLockStatusAPI, } from 'services/remote' import { AppActions, useDispatch } from 'states' -import { - LockScriptCategory, - RoutePath, - TypeScriptCategory, - calculateUsedCapacity, - getExplorerUrl, - isSuccessResponse, - outPointToStr, -} from 'utils' +import { LockScriptCategory, RoutePath, TypeScriptCategory, isSuccessResponse, outPointToStr } from 'utils' import { SortType } from 'widgets/Table' const cellTypeOrder: Record = { @@ -351,52 +342,6 @@ export const useSelect = (liveCells: State.LiveCellWithLocalInfo[]) => { } } -export const useViewCell = ({ isMainnet, viewCell }: { isMainnet: boolean; viewCell: State.LiveCellWithLocalInfo }) => { - const onViewDetail = useCallback( - (e: React.SyntheticEvent) => { - const { - dataset: { txHash }, - } = e.currentTarget - if (!txHash) { - return - } - const explorerUrl = getExplorerUrl(isMainnet) - openExternal(`${explorerUrl}/transaction/${txHash}`) - }, - [isMainnet] - ) - const rawLock = `{ - "code_hash": "${viewCell?.lock.codeHash}" - "hash_type": "${viewCell?.lock.hashType}" - "args": "${viewCell?.lock.args}" -}` - const rawType = viewCell?.type - ? `{ - "code_hash": "${viewCell.type.codeHash}" - "hash_type": "${viewCell.type.hashType}" - "args": "${viewCell.type.args}" -}` - : `{ - "null" -}` - const rawData = `{ - "data": "${viewCell?.data ?? '0x'}" -}` - const usedCapacity = useMemo(() => { - if (!viewCell) { - return 0 - } - return calculateUsedCapacity(viewCell) - }, [viewCell]) - return { - onViewDetail, - rawData, - rawLock, - rawType, - usedCapacity, - } -} - export const usePassword = () => { const [password, setPassword] = useState('') const [error, setError] = useState('') diff --git a/packages/neuron-ui/src/components/CellManagement/index.tsx b/packages/neuron-ui/src/components/CellManagement/index.tsx index 62769ac99a..e8752c869c 100644 --- a/packages/neuron-ui/src/components/CellManagement/index.tsx +++ b/packages/neuron-ui/src/components/CellManagement/index.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from 'react' -import { Attention, Consume, Copy, DetailIcon, EyesClose, EyesOpen, LockCell, NewTab, UnLock } from 'widgets/Icons/icon' +import { Attention, Consume, DetailIcon, EyesClose, EyesOpen, LockCell, UnLock } from 'widgets/Icons/icon' import PageContainer from 'components/PageContainer' import { useTranslation } from 'react-i18next' import Breadcrum from 'widgets/Breadcrum' @@ -10,9 +10,6 @@ import { shannonToCKBFormatter, uniformTimeFormatter, usePagination, - isMainnet as isMainnetUtil, - useCopy, - clsx, outPointToStr, LockScriptCategory, getLockTimestamp, @@ -20,12 +17,13 @@ import { import { HIDE_BALANCE } from 'utils/const' import Tooltip from 'widgets/Tooltip' import Dialog from 'widgets/Dialog' -import Alert from 'widgets/Alert' import ShowOrEditDesc from 'widgets/ShowOrEditDesc' import { TFunction } from 'i18next' import TextField from 'widgets/TextField' import { useSearchParams } from 'react-router-dom' -import { Actions, useAction, useLiveCells, usePassword, useSelect, useViewCell } from './hooks' +import CellInfoDialog from 'components/CellInfoDialog' +import { computeScriptHash } from '@ckb-lumos/base/lib/utils' +import { Actions, useAction, useLiveCells, usePassword, useSelect } from './hooks' import styles from './cellManagement.module.scss' const getColumns = ({ @@ -218,12 +216,9 @@ const CellManagement = () => { app: { epoch }, wallet: { balance = '' }, chain: { - networkID, syncState: { bestKnownBlockTimestamp }, }, - settings: { networks }, } = useGlobalState() - const isMainnet = isMainnetUtil(networks, networkID) const [t] = useTranslation() const [searchParams] = useSearchParams() const breadPages = useMemo(() => [{ label: t('cell-manage.title') }], [t]) @@ -277,11 +272,6 @@ const CellManagement = () => { bestKnownBlockTimestamp, ] ) - const { copied, onCopy, copyTimes } = useCopy() - const { onViewDetail, rawData, rawLock, rawType, usedCapacity } = useViewCell({ - viewCell: operateCells[0], - isMainnet, - }) const totalCapacity = useMemo( () => shannonToCKBFormatter(operateCells.reduce((pre, cur) => pre + BigInt(cur.capacity), BigInt(0)).toString()), [operateCells] @@ -332,57 +322,14 @@ const CellManagement = () => { pageNo={pageNo} onChange={onPageChange} /> - -
-
OutPoint.TxHash
-
-

{operateCells[0]?.outPoint.txHash}

-
- onCopy(operateCells[0]!.outPoint.txHash)} /> - -
-
-
-
-
Lock Script
-
{rawLock}
-
-
-
Type Script
-
{rawType}
-
-
-
{t('cell-manage.cell-detail-dialog.data')}
-
{rawData}
-
- {copied ? ( - - {t('common.copied')} - - ) : null} -
-
- {t('cell-manage.cell-detail-dialog.capacity-used')} -
- {t('cell-manage.cell-detail-dialog.total')} -  {shannonToCKBFormatter(operateCells[0]?.capacity ?? '')} CKB , - {t('cell-manage.cell-detail-dialog.used')} -  {usedCapacity} CKB -
-
-
-
-
-
-
+ /> & { - sudtAmount: string + sudtAmount?: string isReceive: boolean amount: string symbolClassName?: string } -const Amount = ({ sudtAmount, show, item, isReceive, amount, symbolClassName }: AmountProps) => { +const Amount = ({ sudtAmount, show, item, isReceive, amount, symbolClassName, symbol }: AmountProps) => { return sudtAmount ? ( -
- {show ? `${!sudtAmount.includes('-') ? '+' : ''}${sudtAmount}` : HIDE_BALANCE}  +
+ + {show ? `${isReceive ? '+' : ''}${sudtAmount}` : HIDE_BALANCE}  +
) : ( - {amount} +
+ + {amount} + +  {symbol} +
) } @@ -36,6 +43,7 @@ export const FormattedTokenAmount = ({ item, show, symbolClassName }: FormattedT let sudtAmount = '' let copyText = amount let isReceive = false + let symbol = '' if (item.blockNumber !== undefined) { if (item.nftInfo) { @@ -43,20 +51,46 @@ export const FormattedTokenAmount = ({ item, show, symbolClassName }: FormattedT const { type, data } = item.nftInfo amount = show ? `${type === 'receive' ? '+' : '-'}${nftFormatter(data)}` : `${HIDE_BALANCE}mNFT` copyText = amount + symbol = amount.includes('mNFT') ? 'mNFT' : '' + amount = amount.replace('mNFT', '') isReceive = type === 'receive' } else if (item.sudtInfo?.sUDT) { if (item.sudtInfo.sUDT.decimal) { sudtAmount = sUDTAmountFormatter(sudtValueToAmount(item.sudtInfo.amount, item.sudtInfo.sUDT.decimal)) copyText = `${sudtValueToAmount(item.sudtInfo.amount, item.sudtInfo.sUDT.decimal)} ${item.sudtInfo.sUDT.symbol}` + isReceive = !sudtAmount.includes('-') } } else { - amount = show ? `${shannonToCKBFormatter(item.value, true)} CKB` : `${HIDE_BALANCE} CKB` + amount = show ? `${shannonToCKBFormatter(item.value, true)}` : `${HIDE_BALANCE}` isReceive = !amount.includes('-') - copyText = amount + copyText = `${amount} CKB` + symbol = 'CKB' } } - const props = { sudtAmount, show, item, isReceive, amount, symbolClassName } + const props = { sudtAmount, show, item, isReceive, amount, symbolClassName, symbol } + + return show ? ( + + + + ) : ( + + ) +} + +export const FormattedCKBBalanceChange = ({ item, show, symbolClassName }: FormattedTokenAmountProps) => { + let amount = '--' + let copyText = amount + let isReceive = false + + if (item.blockNumber !== undefined) { + amount = show ? `${shannonToCKBFormatter(item.value, true)}` : `${HIDE_BALANCE}` + isReceive = !amount.includes('-') + copyText = amount + } + + const props = { show, item, isReceive, amount, symbolClassName } return show ? ( diff --git a/packages/neuron-ui/src/components/History/history.module.scss b/packages/neuron-ui/src/components/History/history.module.scss index c389de4ac8..c749dc8d9e 100644 --- a/packages/neuron-ui/src/components/History/history.module.scss +++ b/packages/neuron-ui/src/components/History/history.module.scss @@ -88,11 +88,16 @@ body { .infoBox { display: flex; + .infoBlock { + margin-bottom: 0; + } } .infoBlock { + display: flex; + margin-bottom: 20px; &Title { - margin-bottom: 8px; + margin-right: 12px; color: $history-info-title-color; } @@ -104,7 +109,7 @@ body { } .descText { - padding-bottom: 16px; + padding-bottom: 20px; word-break: break-all; cursor: pointer; &:hover { @@ -130,7 +135,6 @@ body { .infoOperationBox { display: flex; - margin-top: 20px; gap: 20px; .explorerNavButton, diff --git a/packages/neuron-ui/src/components/History/hooks.ts b/packages/neuron-ui/src/components/History/hooks.ts index 94ba80e0e7..19a40d836f 100644 --- a/packages/neuron-ui/src/components/History/hooks.ts +++ b/packages/neuron-ui/src/components/History/hooks.ts @@ -4,6 +4,7 @@ import { listParams, backToTop } from 'utils' export const useSearch = (search: string, walletID: string, dispatch: React.Dispatch) => { const [keywords, setKeywords] = useState('') + const [sortInfo, setSortInfo] = useState({ sort: '', direction: '' }) const onKeywordsChange = (_e?: React.FormEvent, newValue?: string) => { if (undefined !== newValue) { @@ -15,9 +16,10 @@ export const useSearch = (search: string, walletID: string, dispatch: React.Disp backToTop() const params = listParams(search) setKeywords(params.keywords) + setSortInfo({ sort: params.sort, direction: params.direction }) updateTransactionList({ ...params, keywords: params.keywords, walletID })(dispatch) }, [search, walletID, dispatch]) - return { keywords, onKeywordsChange, setKeywords } + return { keywords, onKeywordsChange, setKeywords, sortInfo } } export default { diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 38c96c5429..73a9bdbb55 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -10,7 +10,7 @@ import { Download, Search, ArrowNext, Clean } from 'widgets/Icons/icon' import PageContainer from 'components/PageContainer' import TransactionStatusWrap from 'components/TransactionStatusWrap' -import FormattedTokenAmount from 'components/FormattedTokenAmount' +import FormattedTokenAmount, { FormattedCKBBalanceChange } from 'components/FormattedTokenAmount' import { useState as useGlobalState, useDispatch } from 'states' import { exportTransactions } from 'services/remote' @@ -45,8 +45,11 @@ const History = () => { const [isExporting, setIsExporting] = useState(false) const isMainnet = isMainnetUtil(networks, networkID) - const { keywords, onKeywordsChange } = useSearch(search, id, dispatch) - const onSearch = useCallback(() => navigate(`${RoutePath.History}?keywords=${keywords}`), [navigate, keywords]) + const { keywords, onKeywordsChange, sortInfo } = useSearch(search, id, dispatch) + const onSearch = useCallback( + () => navigate(`${RoutePath.History}?keywords=${keywords}&sort=${sortInfo.sort}&direction=${sortInfo.direction}`), + [navigate, keywords] + ) const onClean = useCallback(() => onKeywordsChange(undefined, ''), [onKeywordsChange]) const onExport = useCallback(() => { setIsExporting(true) @@ -109,7 +112,7 @@ const History = () => { title: t('history.table.type'), dataIndex: 'type', align: 'left', - minWidth: '120px', + minWidth: '100px', render: (_, __, item) => { return ( { /> ) }, + sortable: true, }, { title: t('history.table.amount'), dataIndex: 'amount', align: 'left', isBalance: true, - minWidth: '200px', + minWidth: '140px', render(_, __, item, show) { return }, }, + { + title: t('history.table.balance'), + dataIndex: 'value', + align: 'left', + isBalance: true, + minWidth: '140px', + render(_, __, item, show) { + return + }, + sortable: true, + }, { title: t('history.table.timestamp'), dataIndex: 'timestamp', align: 'left', minWidth: '150px', render: (_, __, item) => uniformTimeFormatter(item.timestamp), + sortable: true, }, { title: t('history.table.status'), @@ -216,6 +232,9 @@ const History = () => { )} expandedRow={expandedRow} onRowClick={(_, __, idx) => handleExpandClick(idx)} + onSorted={(key, type) => { + navigate(`${RoutePath.History}?pageNo=${pageNo}&keywords=${keywords}&sort=${key}&direction=${type}`) + }} />
@@ -225,7 +244,9 @@ const History = () => { pageSize={pageSize} pageNo={pageNo} onChange={(no: number) => { - navigate(`${RoutePath.History}?pageNo=${no}&keywords=${keywords}`) + navigate( + `${RoutePath.History}?pageNo=${no}&keywords=${keywords}&sort=${sortInfo.sort}&direction=${sortInfo.direction}` + ) }} />
diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss b/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss index c3242948b3..066467f75b 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss +++ b/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss @@ -1,26 +1,36 @@ @import '../../styles/mixin.scss'; -.basicInfoWrap { +.tx { + @include card; margin-bottom: 16px; +} - .basicInfoTitle { - font-weight: 500; - font-size: 16px; +.basicInfoWrap { + .basicInfoItemBox { + display: flex; + font-size: 14px; padding: 16px; - } - .basicInfoItemWrap { - .basicInfoItemBox { - display: flex; - font-size: 14px; - padding: 16px; - - .infoItemLabel { - min-width: 125px; - } + .infoItemLabel { + min-width: 125px; } - .basicInfoMiddleWrap { - display: flex; + } + .twoColumns { + display: flex; + } + .txHash { + flex: 1; + border: 1px solid var(--table-head-border-color); + border-left: none; + border-right: none; + } + .flexItem { + display: flex; + align-items: center; + + & > svg { + margin-left: 8px; + cursor: pointer; } } } @@ -81,22 +91,6 @@ } } -.income { - display: flex; - align-items: center; - - .incomeCopy { - height: 20px; - line-height: 20px; - cursor: pointer; - } - - & > svg { - margin-left: 8px; - cursor: pointer; - } -} - .amount { height: 32px; line-height: 32px; @@ -115,3 +109,20 @@ .scriptTag:hover { color: var(--primary-color); } + +.cellInfo { + display: flex; + align-items: center; + border: none; + background: transparent; + cursor: pointer; + &:hover { + & > span { + color: var(--primary-color); + } + } + & > span { + color: var(--main-text-color); + margin-right: 4px; + } +} diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts b/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts new file mode 100644 index 0000000000..f8c8d65832 --- /dev/null +++ b/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts @@ -0,0 +1,32 @@ +import { TFunction } from 'i18next' +import { useCallback, useState } from 'react' + +export const TabId = { + Basic: 'Basic', + Topology: 'Topology', +} + +export const useTxTabs = ({ t }: { t: TFunction }) => { + const tabs = [ + { id: TabId.Basic, label: t('send-tx-detail.basic-info') }, + { id: TabId.Topology, label: t('send-tx-detail.topology') }, + ] + const [currentTab, setCurrentTab] = useState(tabs[0]) + return { + currentTab, + setCurrentTab, + tabs, + } +} + +export const useCellInfoDialog = () => { + const [outputCell, setOutputCell] = useState() + const onCancel = useCallback(() => { + setOutputCell(undefined) + }, []) + return { + outputCell, + setOutputCell, + onCancel, + } +} diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx index 5dbc8515e4..7a6b141652 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx +++ b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx @@ -2,16 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { scriptToAddress } from '@nervosnetwork/ckb-sdk-utils' -import { getTransaction } from 'services/remote' +import { calculateUnlockDaoMaximumWithdraw, getTransaction } from 'services/remote' import { showPageNotice, transactionState, useDispatch, useState as useGlobalState } from 'states' import PageContainer from 'components/PageContainer' import LockInfoDialog from 'components/LockInfoDialog' import ScriptTag from 'components/ScriptTag' import AlertDialog from 'widgets/AlertDialog' import Tabs from 'widgets/Tabs' -import Table from 'widgets/Table' +import Table, { TableProps } from 'widgets/Table' import CopyZone from 'widgets/CopyZone' -import { BalanceHide, BalanceShow, Copy } from 'widgets/Icons/icon' +import { ArrowNext, BalanceHide, BalanceShow, Copy } from 'widgets/Icons/icon' import Tooltip from 'widgets/Tooltip' import Breadcrum from 'widgets/Breadcrum' @@ -22,9 +22,16 @@ import { shannonToCKBFormatter, isSuccessResponse, isMainnet as isMainnetUtil, + calculateFee, } from 'utils' -import { HIDE_BALANCE } from 'utils/const' +import { CONFIRMATION_THRESHOLD, HIDE_BALANCE } from 'utils/const' +import TxTopology from 'components/SendTxDetail/TxTopology' +import FormattedTokenAmount, { FormattedCKBBalanceChange } from 'components/FormattedTokenAmount' +import TransactionStatusWrap from 'components/TransactionStatusWrap' +import CellInfoDialog from 'components/CellInfoDialog' +import TransactionType from 'components/TransactionType' +import { TabId, useCellInfoDialog, useTxTabs } from './hooks' import styles from './historyDetailPage.module.scss' type InputOrOutputType = (State.DetailedInput | State.DetailedOutput) & { idx: number } @@ -36,18 +43,147 @@ const InfoItem = ({ label, value, className }: { label: string; value: React.Rea
) +const BasicInfo = ({ + transaction, + cacheTipBlockNumber, + bestKnownBlockNumber, + txFee, +}: { + transaction: State.DetailedTransaction + cacheTipBlockNumber: number + bestKnownBlockNumber: number + txFee: string +}) => { + const [t] = useTranslation() + const dispatch = useDispatch() + const onCopy = useCallback(() => { + window.navigator.clipboard.writeText(transaction.hash) + showPageNotice('common.copied')(dispatch) + }, [transaction.hash, dispatch]) + const [isAmountShow, setIsAmountShow] = useState(true) + const onChangeAmountShow = useCallback(() => { + setIsAmountShow(v => !v) + }, []) + const [isBalanceShow, setIsBalanceShow] = useState(true) + const onChangeBalanceShow = useCallback(() => { + setIsBalanceShow(v => !v) + }, []) + const bestBlockNumber = Math.max(cacheTipBlockNumber, bestKnownBlockNumber) + const confirmationCount = 1 + bestBlockNumber - +transaction.blockNumber + const status = + transaction.status === 'success' && confirmationCount < CONFIRMATION_THRESHOLD ? 'confirming' : transaction.status + + const infos = { + hash: { + label: t('transaction.transaction-hash'), + value: ( +
+ {transaction.hash} + +
+ ), + }, + blockNumber: { + label: t('transaction.block-number'), + value: transaction.blockNumber ? localNumberFormatter(transaction.blockNumber) : 'none', + }, + time: { + label: t('transaction.date'), + value: +(transaction.timestamp || transaction.createdAt) + ? uniformTimeFormatter(+(transaction.timestamp || transaction.createdAt)) + : 'none', + }, + type: { + label: t('transaction.type'), + value: ( + + ), + }, + fee: { + label: t('transaction.fee'), + value: `${shannonToCKBFormatter(txFee)} CKB`, + }, + amount: { + label: t('transaction.assets'), + value: ( +
+ + {isAmountShow ? : } +
+ ), + }, + balance: { + label: t('transaction.balance'), + value: ( +
+ + {isBalanceShow ? ( + + ) : ( + + )} +
+ ), + }, + status: { + label: t('transaction.status'), + value: , + }, + size: { + label: t('transaction.size'), + value: `${transaction.size} Bytes`, + }, + cycles: { + label: t('transaction.cycles'), + value: transaction.cycles ? +transaction.cycles : '--', + }, + } + return ( +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} + const HistoryDetailPage = () => { const { hash } = useParams() const navigate = useNavigate() const { app: { pageNotice }, - chain: { networkID }, + chain: { + networkID, + syncState: { cacheTipBlockNumber, bestKnownBlockNumber }, + transactions: { items = [] }, + }, settings: { networks }, wallet: currentWallet, } = useGlobalState() const isMainnet = isMainnetUtil(networks, networkID) const [t] = useTranslation() const [transaction, setTransaction] = useState(transactionState) + const [daoMaximumWithdraw, setDaoMaximumWithdraw] = useState() const [error, setError] = useState({ code: '', message: '' }) const [failedMessage, setFailedMessage] = useState('') const [lockInfo, setLockInfo] = useState(null) @@ -61,7 +197,8 @@ const HistoryDetailPage = () => { getTransaction({ hash, walletID: currentWallet.id }) .then(res => { if (isSuccessResponse(res)) { - setTransaction(res.result) + const tx = items.find(v => v.hash === hash) + setTransaction({ ...tx, ...res.result, nervosDao: tx?.nervosDao ?? res.result.nervosDao }) } else { setFailedMessage(t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' })) } @@ -75,57 +212,15 @@ const HistoryDetailPage = () => { } }, [t, hash, currentWallet]) - const dispatch = useDispatch() - const onCopy = useCallback(() => { - window.navigator.clipboard.writeText(transaction.hash) - showPageNotice('common.copied')(dispatch) - }, [transaction.hash, dispatch]) - const [isIncomeShow, setIsIncomeShow] = useState(true) - const onChangeIncomeShow = useCallback(() => { - setIsIncomeShow(v => !v) - }, []) - - const infos = [ - { - label: t('transaction.transaction-hash'), - value: ( -
- {transaction.hash} - -
- ), - }, - { - label: t('transaction.block-number'), - value: transaction.blockNumber ? localNumberFormatter(transaction.blockNumber) : 'none', - }, - { - label: t('transaction.date'), - value: +(transaction.timestamp || transaction.createdAt) - ? uniformTimeFormatter(+(transaction.timestamp || transaction.createdAt)) - : 'none', - }, - { - label: t('transaction.income'), - value: isIncomeShow ? ( -
- - {`${shannonToCKBFormatter(transaction.value)} CKB`} - - -
- ) : ( -
- {`${HIDE_BALANCE} CKB`} - -
- ), - }, - ] + useEffect(() => { + if (hash) { + calculateUnlockDaoMaximumWithdraw(hash).then(res => { + if (isSuccessResponse(res) && res.result) { + setDaoMaximumWithdraw(res.result) + } + }) + } + }, [hash]) const inputsTitle = useMemo( () => `${t('transaction.inputs')} (${transaction.inputs.length}/${localNumberFormatter(transaction.inputsCount)})`, @@ -180,19 +275,11 @@ const HistoryDetailPage = () => { ) } - const columns: { - title: string - dataIndex: string - isBalance?: boolean - render?: (v: any, idx: number, item: InputOrOutputType, showBalance: boolean) => React.ReactNode - width?: string - align?: 'left' | 'right' | 'center' - className?: string - }[] = [ + const columns: TableProps['columns'] = [ { title: t('transaction.index'), dataIndex: 'idx', - width: '90px', + width: '60px', render(_, __, item) { return <>{item.idx} }, @@ -201,7 +288,7 @@ const HistoryDetailPage = () => { title: t('transaction.address'), dataIndex: 'type', align: 'left', - width: '580px', + width: '560px', render: (_, __, item) => { const { address } = handleListData(item) return ( @@ -245,8 +332,39 @@ const HistoryDetailPage = () => { }, ] + const { setOutputCell, outputCell, onCancel } = useCellInfoDialog() + + const cellInfoColumn: TableProps['columns'][number] = { + title: '', + dataIndex: 'cellInfo', + align: 'left', + width: '100px', + render(_, __, item) { + return ( + + ) + }, + } + const breadPages = useMemo(() => [{ label: t('history.title-detail') }], [t]) + const { tabs: txTabs, setCurrentTab: setTxCurrentTab, currentTab: currentTxTab } = useTxTabs({ t }) + const txFee = useMemo(() => { + if (daoMaximumWithdraw) { + return ( + BigInt(daoMaximumWithdraw) - + transaction.outputs.reduce( + (result: bigint, output: { capacity: string }) => result + BigInt(output.capacity), + BigInt(0) + ) + ).toString() + } + return calculateFee(transaction) + }, [transaction, daoMaximumWithdraw]) + return ( { @@ -256,16 +374,25 @@ const HistoryDetailPage = () => { head={} notice={pageNotice} > -
-
{t('history.basic-information')}
-
- -
- - -
- -
+
+ + {currentTxTab.id === TabId.Basic ? ( + + ) : ( + + )}
{ activeColumnClassName={styles.active} /> } - columns={columns} + columns={currentTab.id === tabs[0].id ? columns : [...columns, cellInfoColumn]} dataSource={currentTab.id === tabs[0].id ? inputsData : outputsData} noDataContent={t('overview.no-recent-activities')} hasHoverTrBg={false} @@ -295,6 +422,7 @@ const HistoryDetailPage = () => { type="failed" onCancel={() => navigate(-1)} /> + ) } diff --git a/packages/neuron-ui/src/components/TransactionStatusWrap/transactionStatusWrap.module.scss b/packages/neuron-ui/src/components/TransactionStatusWrap/transactionStatusWrap.module.scss index c19f1df9b9..90ce8c1e9f 100644 --- a/packages/neuron-ui/src/components/TransactionStatusWrap/transactionStatusWrap.module.scss +++ b/packages/neuron-ui/src/components/TransactionStatusWrap/transactionStatusWrap.module.scss @@ -5,6 +5,7 @@ width: fit-content; display: flex; align-items: center; + flex-wrap: wrap; .confirm { @include infinite-rotation; diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index 51bcfd0e84..fb0619c3d2 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -308,6 +308,7 @@ "table": { "name": "Wallet Name", "type": "Type", + "balance": "Balance", "amount": "Amount", "timestamp": "Time", "status": "Status", @@ -327,7 +328,8 @@ "copy-balance": "Copy Balance", "copy-address": "Copy Address", "create": "Create {{name}} Asset Account", - "destroy": "Destroy {{name}} Asset Account" + "destroy": "Destroy {{name}} Asset Account", + "fee": "Transaction Fee" }, "transaction": { "window-title": "Transaction: {{hash}}", @@ -344,7 +346,15 @@ "cell-from-cellbase": "From cellbase", "lock-script": "Lock Script", "lock-script-title": "Address Information", - "deprecated-address-format": "Address in deprecated format" + "deprecated-address-format": "Address in deprecated format", + "type": "Type", + "fee": "Transaction Fee", + "assets": "Assets", + "balance": "Balance", + "status": "Status", + "size": "Size", + "cycles": "Cycles", + "cell-detail": "Cell Detail" }, "addresses": { "title": "Address Book", @@ -1191,7 +1201,12 @@ "capacity-used": "Used Capacity", "data": "Data", "total": "Total", - "used": "Used" + "used": "Used", + "view-raw-data": "Raw data", + "UTF8": "UTF-8", + "address": "Address", + "number": "Number", + "bigEndian": "Big-Endian" }, "locked-reason": { "multi-locktime": "The cell has a time lock and you can't use it until {{time}}.", diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index f003afb51d..5933db645b 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -300,6 +300,7 @@ "table": { "name": "Nombre de la Billetera", "type": "Tipo", + "balance": "Saldo", "amount": "Cantidad", "timestamp": "Tiempo", "status": "Estado", @@ -319,7 +320,8 @@ "copy-balance": "Copiar Saldo", "copy-address": "Copiar Dirección", "create": "Crear Cuenta de Activos {{name}}", - "destroy": "Destruir Cuenta de Activos {{name}}" + "destroy": "Destruir Cuenta de Activos {{name}}", + "fee": "Tarifa de transacción" }, "transaction": { "window-title": "Transacción: {{hash}}", @@ -336,7 +338,15 @@ "cell-from-cellbase": "Desde cellbase", "lock-script": "Script de Bloqueo", "lock-script-title": "Información de Dirección", - "deprecated-address-format": "Dirección en formato obsoleto" + "deprecated-address-format": "Dirección en formato obsoleto", + "type": "Tipo", + "fee": "Tarifa de transacción", + "assets": "Activos", + "balance": "Saldo", + "status": "Estado", + "size": "Tamaño", + "cycles": "Ciclos", + "cell-detail": "Detalles de la Cell" }, "addresses": { "title": "Libro de Direcciones", @@ -1170,7 +1180,12 @@ "capacity-used": "Capacidad utilizada", "data": "Datos", "total": "Total", - "used": "Usado" + "used": "Usado", + "view-raw-data": "Datos sin procesar", + "UTF8": "UTF-8", + "address": "Dirección", + "number": "Número", + "bigEndian": "Big-Endian" }, "locked-reason": { "multi-locktime": "La Cell tiene un bloqueo de tiempo y no se puede usar hasta {{time}}.", diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index 2824ca14ec..76affbaa48 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -307,6 +307,7 @@ "table": { "name": "Nom du Wallet", "type": "Type", + "balance": "Solde", "amount": "Montant", "timestamp": "Heure", "status": "Statut", @@ -326,7 +327,8 @@ "copy-balance": "Copier le solde", "copy-address": "Copier l'adresse", "create": "Créer le compte d'actif {{name}}", - "destroy": "Détruire le compte d'actif {{name}}" + "destroy": "Détruire le compte d'actif {{name}}", + "fee": "Frais de transaction" }, "transaction": { "window-title": "Transaction : {{hash}}", @@ -343,7 +345,15 @@ "cell-from-cellbase": "De cellbase", "lock-script": "Script de verrouillage", "lock-script-title": "Informations sur l'adresse", - "deprecated-address-format": "Adresse au format obsolète" + "deprecated-address-format": "Adresse au format obsolète", + "type": "Type", + "fee": "Frais de transaction", + "assets": "Actifs", + "balance": "Solde", + "status": "Statut", + "size": "taille", + "cycles": "Cycles", + "cell-detail": "Détail de la cellule" }, "addresses": { "title": "Carnet d'adresses", @@ -1181,7 +1191,12 @@ "capacity-used": "Capacité utilisée", "data": "Données", "total": "Total", - "used": "Utilisé" + "used": "Utilisé", + "view-raw-data": "Données", + "UTF8": "UTF-8", + "address": "Adresse", + "number": "Numéro", + "bigEndian": "Big-Endian" }, "locked-reason": { "multi-locktime": "La cellule est verrouillée dans le temps et vous ne pouvez pas l'utiliser avant {{time}}.", diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 07fcb9cad2..e08c32e39a 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -303,6 +303,7 @@ "table": { "name": "錢包名稱", "type": "類型", + "balance": "余額", "amount": "金額", "timestamp": "時間", "status": "狀態", @@ -322,7 +323,8 @@ "copy-balance": "複製餘額", "copy-address": "複製地址", "create": "創建 {{name}} 資產賬戶", - "destroy": "銷毀 {{name}} 資產賬戶" + "destroy": "銷毀 {{name}} 資產賬戶", + "fee": "交易費用" }, "transaction": { "window-title": "交易: {{hash}}", @@ -339,7 +341,15 @@ "cell-from-cellbase": "來自 Cellbase", "lock-script": "腳本鎖定", "lock-script-title": "地址資訊", - "deprecated-address-format": "已棄用格式的地址" + "deprecated-address-format": "已棄用格式的地址", + "type": "類型", + "fee": "交易費用", + "assets": "資產", + "balance": "余額", + "status": "狀態", + "size": "大小", + "cycles": "周期", + "cell-detail": "Cell詳情" }, "addresses": { "title": "地址簿", @@ -1021,7 +1031,7 @@ "address": "簽名者地址", "alias": "别名", "type": "類型", - "balance": "余额", + "balance": "余額", "copy-address": "復製地址", "action": "操作", "more": "更多", @@ -1162,7 +1172,8 @@ "capacity-used": "容量使用", "data": "數據", "total": "總量", - "used": "已用" + "used": "已用", + "view-raw-data": "原始數據" }, "locked-reason": { "multi-locktime": "這是壹個有時間鎖的 cell, 只能在 {{time}} 之後使用。", diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 402db46a6c..036ab6d4dd 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -301,6 +301,7 @@ "table": { "name": "钱包名称", "type": "类型", + "balance": "余额", "amount": "金额", "timestamp": "时间", "status": "状态", @@ -320,7 +321,8 @@ "copy-balance": "复制余额", "copy-address": "复制地址", "create": "创建 {{name}} 资产账户", - "destroy": "销毁 {{name}} 资产账户" + "destroy": "销毁 {{name}} 资产账户", + "fee": "交易费用" }, "transaction": { "window-title": "交易: {{hash}}", @@ -337,7 +339,15 @@ "cell-from-cellbase": "来自 Cellbase", "lock-script": "脚本锁定", "lock-script-title": "地址信息", - "deprecated-address-format": "已弃用格式的地址" + "deprecated-address-format": "已弃用格式的地址", + "type": "类型", + "fee": "交易费用", + "assets": "资产", + "balance": "余额", + "status": "状态", + "size": "大小", + "cycles": "周期", + "cell-detail": "Cell详情" }, "addresses": { "title": "地址簿", @@ -1183,7 +1193,12 @@ "capacity-used": "容量使用", "data": "数据", "total": "总量", - "used": "已用" + "used": "已用", + "view-raw-data": "原始数据", + "UTF8": "UTF-8", + "address": "地址", + "number": "数字", + "bigEndian": "大端序" }, "locked-reason": { "multi-locktime": "这是一个有时间锁的 cell, 只能在 {{time}} 之后使用。", diff --git a/packages/neuron-ui/src/services/chain.ts b/packages/neuron-ui/src/services/chain.ts index ac21b7dd2e..0ca47c4951 100644 --- a/packages/neuron-ui/src/services/chain.ts +++ b/packages/neuron-ui/src/services/chain.ts @@ -4,8 +4,6 @@ export const ckbCore = new CKBCore('') export const { getHeader, getBlockchainInfo, getTipHeader, getHeaderByNumber, getFeeRateStats, getTransaction } = ckbCore.rpc -export const { calculateDaoMaximumWithdraw } = ckbCore - export const { toUint64Le, parseEpoch } = ckbCore.utils export default { diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts index 5e176dd251..a1f270ce63 100644 --- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts +++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts @@ -85,6 +85,7 @@ type Action = | 'generate-dao-deposit-all-tx' | 'start-withdraw-from-dao' | 'withdraw-from-dao' + | 'calculate-unlock-dao-maximum-withdraw' // Special Assets | 'get-customized-asset-cells' | 'generate-withdraw-customized-cell-tx' diff --git a/packages/neuron-ui/src/services/remote/transactions.ts b/packages/neuron-ui/src/services/remote/transactions.ts index 73a6f81249..0877418a24 100644 --- a/packages/neuron-ui/src/services/remote/transactions.ts +++ b/packages/neuron-ui/src/services/remote/transactions.ts @@ -5,6 +5,8 @@ export interface GetTransactionListParams { pageSize: number keywords?: string walletID: string + sort?: string + direction?: string } export const getTransactionList = remoteApi('get-transaction-list') diff --git a/packages/neuron-ui/src/services/remote/wallets.ts b/packages/neuron-ui/src/services/remote/wallets.ts index 33c26d1f82..3f97e5b4ea 100644 --- a/packages/neuron-ui/src/services/remote/wallets.ts +++ b/packages/neuron-ui/src/services/remote/wallets.ts @@ -31,6 +31,7 @@ export const generateDaoDepositAllTx = remoteApi('start-withdraw-from-dao') export const generateDaoClaimTx = remoteApi('withdraw-from-dao') +export const calculateUnlockDaoMaximumWithdraw = remoteApi('calculate-unlock-dao-maximum-withdraw') // Sign and Verify export const signMessage = remoteApi('sign-message') diff --git a/packages/neuron-ui/src/tests/parsers/index.test.ts b/packages/neuron-ui/src/tests/parsers/index.test.ts index 2ab0f07925..a7a240565b 100644 --- a/packages/neuron-ui/src/tests/parsers/index.test.ts +++ b/packages/neuron-ui/src/tests/parsers/index.test.ts @@ -9,6 +9,8 @@ describe('listParams', () => { pageNo: 1, pageSize: 15, keywords: 'foo', + sort: '', + direction: '', }, } expect(listParams(fixture.params)).toEqual(fixture.expected) @@ -21,6 +23,22 @@ describe('listParams', () => { pageNo: 2, pageSize: 30, keywords: 'foo', + sort: '', + direction: '', + }, + } + expect(listParams(fixture.params)).toEqual(fixture.expected) + }) + + it('should use passed sort and direction', () => { + const fixture = { + params: '?sort=foo&direction=asc', + expected: { + pageNo: 1, + pageSize: 15, + keywords: '', + sort: 'foo', + direction: 'asc', }, } expect(listParams(fixture.params)).toEqual(fixture.expected) diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index a7187731a4..b8559efbb0 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -54,6 +54,7 @@ declare namespace State { witnesses: string[] size?: number isLastChange?: boolean + cycles?: string } interface Output { address: string | undefined diff --git a/packages/neuron-ui/src/utils/parsers.ts b/packages/neuron-ui/src/utils/parsers.ts index d53d3ecf3d..28663dd69c 100644 --- a/packages/neuron-ui/src/utils/parsers.ts +++ b/packages/neuron-ui/src/utils/parsers.ts @@ -9,6 +9,8 @@ export const listParams = (search: string) => { pageNo: +(query.get('pageNo') || 1), pageSize: +(query.get('pageSize') || PAGE_SIZE), keywords, + sort: query.get('sort') || '', + direction: query.get('direction') || '', } return params } diff --git a/packages/neuron-ui/src/widgets/Switch/index.tsx b/packages/neuron-ui/src/widgets/Switch/index.tsx new file mode 100644 index 0000000000..6f79bb3bb8 --- /dev/null +++ b/packages/neuron-ui/src/widgets/Switch/index.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import styles from './switch.module.scss' + +const Switch = ({ + disabled, + checked, + onChange, +}: { + disabled?: boolean + checked: boolean + onChange: (checked: boolean) => void +}) => { + return ( + + ) +} + +Switch.displayName = 'Switch' + +export default Switch diff --git a/packages/neuron-ui/src/widgets/Switch/switch.module.scss b/packages/neuron-ui/src/widgets/Switch/switch.module.scss new file mode 100644 index 0000000000..f86653d99c --- /dev/null +++ b/packages/neuron-ui/src/widgets/Switch/switch.module.scss @@ -0,0 +1,34 @@ +.switchRoot { + border: none; + border-radius: 8px; + background-color: var(--primary-color); + width: 26px; + height: 14px; + display: flex; + align-items: center; + position: relative; + cursor: pointer; + + .slider { + width: 12px; + height: 12px; + border-radius: 12px; + background-color: var(--primary-text-color); + display: inline-block; + position: absolute; + right: 2px; + transition: right 0.25s; + } + + &[data-checked='false'] { + background-color: var(--secondary-text-color); + .slider { + right: calc(100% - 14px); + } + } + + &[disabled] { + cursor: not-allowed; + background-color: var(--secondary-text-color); + } +} diff --git a/packages/neuron-ui/src/widgets/Table/index.tsx b/packages/neuron-ui/src/widgets/Table/index.tsx index 2cb92df1c0..e94ba2e65e 100755 --- a/packages/neuron-ui/src/widgets/Table/index.tsx +++ b/packages/neuron-ui/src/widgets/Table/index.tsx @@ -58,10 +58,22 @@ const Table = >(props: TableProps) => { onSorted, initSortInfo, } = props - const [showBalance, setShowBalance] = useState(true) - const onClickBalanceIcon = useCallback(() => { - setShowBalance(v => !v) - }, [setShowBalance]) + const [showBalance, setShowBalance] = useState>( + columns.filter(v => v.isBalance).reduce((pre, cur) => ({ ...pre, [cur.dataIndex]: true }), {}) + ) + const onClickBalanceIcon = useCallback( + (e: React.SyntheticEvent) => { + const { + dataset: { index }, + } = e.currentTarget + if (!index) return + setShowBalance(v => ({ + ...v, + [index]: !v[index], + })) + }, + [setShowBalance] + ) const handleRowClick = (e: React.SyntheticEvent, item: T, idx: number) => { onRowClick?.(e, item, idx) @@ -141,9 +153,17 @@ const Table = >(props: TableProps) => {
{title} {showBalance ? ( - + ) : ( - + )}
) : ( @@ -200,7 +220,7 @@ const Table = >(props: TableProps) => { expandedRow === idx && rowExtendRender ? styles.noBorder : '' }`} > - {render ? render(item[dataIndex], idx, item, showBalance) : item[dataIndex]} + {render ? render(item[dataIndex], idx, item, showBalance[dataIndex]) : item[dataIndex]} ) )} diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index 70c9526c7f..1f0bae8933 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -33,7 +33,6 @@ import NetworksController from '../controllers/networks' import UpdateController from '../controllers/update' import MultisigController from '../controllers/multisig' import Transaction from '../models/chain/transaction' -import OutPoint from '../models/chain/out-point' import SignMessageController from '../controllers/sign-message' import CustomizedAssetsController from './customized-assets' import SystemScriptInfo from '../models/system-script-info' @@ -65,6 +64,7 @@ import DataUpdateSubject from '../models/subjects/data-update' import CellManagement from './cell-management' import { UpdateCellLocalInfo } from '../database/chain/entities/cell-local-info' import { CKBLightRunner } from '../services/light-runner' +import { OutPoint } from '@ckb-lumos/base' export type Command = 'export-xpubkey' | 'import-xpubkey' | 'delete-wallet' | 'backup-wallet' | 'migrate-acp' // Handle channel messages from renderer process and user actions. @@ -434,7 +434,7 @@ export default class ApiController { items: { address: string; capacity: string }[] fee: string feeRate: string - consumeOutPoints?: CKBComponents.OutPoint[] + consumeOutPoints?: OutPoint[] enableUseSentCell?: boolean } ) => { @@ -451,7 +451,7 @@ export default class ApiController { items: { address: string; capacity: string }[] fee: string feeRate: string - consumeOutPoints: CKBComponents.OutPoint[] + consumeOutPoints: OutPoint[] enableUseSentCell?: boolean } ) => { @@ -551,6 +551,10 @@ export default class ApiController { } ) + handle('calculate-unlock-dao-maximum-withdraw', async (_, unlockHash: string) => { + return this.#daoController.calculateUnlockDaoMaximumWithdraw(unlockHash) + }) + // Customized Asset handle('get-customized-asset-cells', async (_, params: Controller.Params.GetCustomizedAssetCellsParams) => { return this.#customizedAssetsController.getCustomizedAssetCells(params) @@ -920,7 +924,7 @@ export default class ApiController { async ( _, params: { - outPoints: CKBComponents.OutPoint[] + outPoints: OutPoint[] locked: boolean password: string lockScripts: CKBComponents.Script[] diff --git a/packages/neuron-wallet/src/controllers/cell-management.ts b/packages/neuron-wallet/src/controllers/cell-management.ts index 4757e3a929..690de0d4c7 100644 --- a/packages/neuron-wallet/src/controllers/cell-management.ts +++ b/packages/neuron-wallet/src/controllers/cell-management.ts @@ -1,4 +1,4 @@ -import type { OutPoint, Script } from '@ckb-lumos/base' +import type { OutPoint as OutPointSDK, Script } from '@ckb-lumos/base' import CellLocalInfo, { UpdateCellLocalInfo } from '../database/chain/entities/cell-local-info' import Output from '../models/chain/output' import CellsService, { LockScriptCategory, TypeScriptCategory } from '../services/cells' @@ -17,7 +17,9 @@ export default class CellManagement { const liveCells = (await CellsService.getLiveOrSentCellByWalletId(currentWallet.id, { includeCheque: true })).map( v => v.toModel() ) - const outPoints = liveCells.filter((v): v is Output & { outPoint: OutPoint } => !!v.outPoint).map(v => v.outPoint) + const outPoints = liveCells + .filter((v): v is Output & { outPoint: OutPointSDK } => !!v.outPoint) + .map(v => v.outPoint) const cellLocalInfoMap = await CellLocalInfoService.getCellLocalInfoMap(outPoints) return liveCells .map< @@ -56,7 +58,7 @@ export default class CellManagement { } static async updateLiveCellsLockStatus( - outPoints: OutPoint[], + outPoints: OutPointSDK[], locked: boolean, lockScripts: Script[], password: string diff --git a/packages/neuron-wallet/src/controllers/dao.ts b/packages/neuron-wallet/src/controllers/dao.ts index e460b0b13a..2dccf9e798 100644 --- a/packages/neuron-wallet/src/controllers/dao.ts +++ b/packages/neuron-wallet/src/controllers/dao.ts @@ -1,3 +1,4 @@ +import { type OutPoint as OutPointSDK } from '@ckb-lumos/base' import { ServiceHasNoResponse, IsRequired } from '../exceptions' import { ResponseCode } from '../utils/const' import CellsService from '../services/cells' @@ -67,7 +68,7 @@ export default class DaoController { public async startWithdrawFromDao(params: { walletID: string - outPoint: OutPoint + outPoint: OutPointSDK fee: string feeRate: string }): Promise> { @@ -77,7 +78,7 @@ export default class DaoController { const tx = await new TransactionSender().startWithdrawFromDao( params.walletID, - new OutPoint(params.outPoint.txHash, params.outPoint.index), + OutPoint.fromSDK(params.outPoint), params.fee, params.feeRate ) @@ -89,8 +90,8 @@ export default class DaoController { public async withdrawFromDao(params: { walletID: string - depositOutPoint: OutPoint - withdrawingOutPoint: OutPoint + depositOutPoint: OutPointSDK + withdrawingOutPoint: OutPointSDK fee: string feeRate: string }): Promise> { @@ -100,8 +101,8 @@ export default class DaoController { const tx = await new TransactionSender().withdrawFromDao( params.walletID, - new OutPoint(params.depositOutPoint.txHash, params.depositOutPoint.index), - new OutPoint(params.withdrawingOutPoint.txHash, params.withdrawingOutPoint.index), + OutPoint.fromSDK(params.depositOutPoint), + OutPoint.fromSDK(params.withdrawingOutPoint), params.fee, params.feeRate ) @@ -110,4 +111,21 @@ export default class DaoController { result: tx, } } + + public async calculateUnlockDaoMaximumWithdraw(unlockHash: string): Promise> { + const depositAndWithdrawInfo = await CellsService.getDaoWithdrawAndDeposit(unlockHash) + let total = BigInt(0) + for (let index = 0; index < depositAndWithdrawInfo.length; index++) { + total = + total + + (await new TransactionSender().calculateDaoMaximumWithdraw( + depositAndWithdrawInfo[index].depositOutPoint, + depositAndWithdrawInfo[index].withdrawBlockHash + )) + } + return { + status: ResponseCode.Success, + result: total.toString(), + } + } } diff --git a/packages/neuron-wallet/src/controllers/transactions.ts b/packages/neuron-wallet/src/controllers/transactions.ts index 4cf2f38068..eb3a4fabfc 100644 --- a/packages/neuron-wallet/src/controllers/transactions.ts +++ b/packages/neuron-wallet/src/controllers/transactions.ts @@ -9,19 +9,18 @@ import { TransactionNotFound, CurrentWalletNotSet } from '../exceptions' import Transaction from '../models/chain/transaction' import { set as setDescription, get as getDescription } from '../services/tx/transaction-description' -import AddressParser from '../models/address-parser' import ShowGlobalDialogSubject from '../models/subjects/show-global-dialog' export default class TransactionsController { public async getAll( params: Controller.Params.TransactionsByKeywords ): Promise & Controller.Params.TransactionsByKeywords>> { - const { pageNo = 1, pageSize = 15, keywords = '', walletID = '' } = params + const { pageNo = 1, pageSize = 15, keywords = '', walletID = '', sort, direction } = params const addresses = (await AddressesService.getAddressesByWalletId(walletID)).map(addr => addr.address) const transactions = await TransactionsService.getAllByAddresses( - { walletID, pageNo, pageSize, addresses }, + { walletID, pageNo, pageSize, addresses, sort, direction }, keywords.trim() ).catch(() => ({ totalCount: 0, @@ -61,17 +60,16 @@ export default class TransactionsController { throw new CurrentWalletNotSet() } - const addresses: string[] = (await AddressesService.getAddressesByWalletId(wallet.id)).map(addr => addr.address) - const lockHashes: string[] = AddressParser.batchToLockHash(addresses) + const lockArgs: string[] = (await AddressesService.getAddressesByWalletId(wallet.id)).map(addr => addr.blake160) const outputCapacities: bigint = transaction - .outputs!.filter(o => lockHashes.includes(o.lockHash!)) + .outputs!.filter(o => lockArgs.includes(o.lock.args)) .map(o => BigInt(o.capacity)) .reduce((result, c) => result + c, BigInt(0)) const inputCapacities: bigint = transaction .inputs!.filter(i => { - if (i.lockHash) { - return lockHashes.includes(i.lockHash) + if (i.lock?.args) { + return lockArgs.includes(i.lock?.args) } return false }) diff --git a/packages/neuron-wallet/src/models/chain/transaction-with-status.ts b/packages/neuron-wallet/src/models/chain/transaction-with-status.ts index 11b14fb521..62899b088b 100644 --- a/packages/neuron-wallet/src/models/chain/transaction-with-status.ts +++ b/packages/neuron-wallet/src/models/chain/transaction-with-status.ts @@ -1,19 +1,23 @@ +import { TransactionWithStatus as APITransactionWithStatus } from '@ckb-lumos/base/lib/api' import Transaction from './transaction' import TxStatus from './tx-status' export default class TransactionWithStatus { public transaction: Transaction public txStatus: TxStatus + public cycles: APITransactionWithStatus['cycles'] - constructor(transaction: Transaction, txStatus: TxStatus) { + constructor(transaction: Transaction, txStatus: TxStatus, cycles: APITransactionWithStatus['cycles']) { this.transaction = transaction this.txStatus = txStatus + this.cycles = cycles } - public static fromSDK(txWithStatus: CKBComponents.TransactionWithStatus): TransactionWithStatus { + public static fromSDK(txWithStatus: APITransactionWithStatus): TransactionWithStatus { return new TransactionWithStatus( Transaction.fromSDK(txWithStatus.transaction), - TxStatus.fromSDK(txWithStatus.txStatus) + TxStatus.fromSDK(txWithStatus.txStatus), + txWithStatus.cycles ) } } diff --git a/packages/neuron-wallet/src/models/chain/transaction.ts b/packages/neuron-wallet/src/models/chain/transaction.ts index 2bd405c8bb..1f4a0d1223 100644 --- a/packages/neuron-wallet/src/models/chain/transaction.ts +++ b/packages/neuron-wallet/src/models/chain/transaction.ts @@ -61,6 +61,7 @@ export default class Transaction { public value?: string public fee?: string public size?: number + public cycles?: string | null public interest?: string public type?: string diff --git a/packages/neuron-wallet/src/services/cells.ts b/packages/neuron-wallet/src/services/cells.ts index ad3b73c57c..82972a2d65 100644 --- a/packages/neuron-wallet/src/services/cells.ts +++ b/packages/neuron-wallet/src/services/cells.ts @@ -1,4 +1,4 @@ -import { Brackets, In, IsNull, type ObjectLiteral } from 'typeorm' +import { Brackets, In, IsNull, Not, type ObjectLiteral } from 'typeorm' import { computeScriptHash as scriptToHash } from '@ckb-lumos/base/lib/utils' import { getConnection } from '../database/chain/connection' import { scriptToAddress, addressToScript } from '../utils/scriptAndAddress' @@ -1379,4 +1379,41 @@ export default class CellsService { } } } + + public static async getDaoWithdrawAndDeposit(unlockHash: string) { + const inputEntities = await getConnection() + .getRepository(InputEntity) + .createQueryBuilder('input') + .where({ + transactionHash: unlockHash, + data: Not('0x'), + typeCodeHash: SystemScriptInfo.DAO_CODE_HASH, + typeHashType: SystemScriptInfo.DAO_HASH_TYPE, + }) + .getMany() + if (!inputEntities.length) throw new Error(`No unlock transaction use ${unlockHash} as input`) + const inputPreviousTxHashes = inputEntities.map(v => v.outPointTxHash) + const outputEntities = await getConnection() + .getRepository(OutputEntity) + .createQueryBuilder('output') + .leftJoinAndSelect('output.transaction', 'tx') + .where({ outPointTxHash: In(inputPreviousTxHashes), depositTxHash: Not(IsNull()) }) + .getMany() + if (!outputEntities.length) throw new Error(`${unlockHash} is not a DAO transaction`) + return inputEntities + .map(v => { + const withdrawOutput = outputEntities.find( + o => o.outPointTxHash === v.outPointTxHash && o.outPointIndex === v.outPointIndex + ) + if (!withdrawOutput) return + return { + withdrawBlockHash: withdrawOutput.transaction.blockHash, + depositOutPoint: OutPoint.fromSDK({ + txHash: withdrawOutput!.depositTxHash, + index: withdrawOutput!.depositIndex, + }), + } + }) + .filter((v): v is { withdrawBlockHash: string; depositOutPoint: OutPoint } => !!v) + } } diff --git a/packages/neuron-wallet/src/services/tx/transaction-service.ts b/packages/neuron-wallet/src/services/tx/transaction-service.ts index e58a36e84f..eb5cf93dd3 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-service.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-service.ts @@ -20,12 +20,16 @@ import NetworksService from '../networks' import Script from '../../models/chain/script' import Input from '../../models/chain/input' import SudtTokenInfoService from '../sudt-token-info' +import TransactionSize from '../../models/transaction-size' +import Output from '../../models/chain/output' export interface TransactionsByAddressesParam { pageNo: number pageSize: number addresses: string[] walletID: string + sort?: string + direction?: string } export interface PaginationResult { @@ -42,6 +46,8 @@ export enum SearchType { Unknown = 'unknown', } +const DESC = 'decrease' + export class TransactionsService { public static filterSearchType(value: string) { if (value === '') { @@ -63,6 +69,30 @@ export class TransactionsService { return SearchType.TokenInfo } + public static ComparedTxType(a: Transaction, b: Transaction, direction: string) { + const aType = a.type ?? '' + const bType = b.type ?? '' + if (aType > bType) return direction === DESC ? 1 : -1 + if (aType < bType) return direction === DESC ? -1 : 1 + return 0 + } + + public static ComparedTxTimestamp(a: Transaction, b: Transaction, direction: string) { + const aTimestamp = a.timestamp ? +a.timestamp : 0 + const bTimestamp = b.timestamp ? +b.timestamp : 0 + if (aTimestamp > bTimestamp) return direction === DESC ? 1 : -1 + if (aTimestamp < bTimestamp) return direction === DESC ? -1 : 1 + return 0 + } + + public static ComparedTxBalance(a: Transaction, b: Transaction, direction: string) { + const aBalance = a.value ? BigInt(a.value) : BigInt(0) + const bBalance = b.value ? BigInt(b.value) : BigInt(0) + if (aBalance > bBalance) return direction === DESC ? 1 : -1 + if (aBalance < bBalance) return direction === DESC ? -1 : 1 + return 0 + } + public static async getAllByAddresses( params: TransactionsByAddressesParam, searchValue: string = '' @@ -207,7 +237,10 @@ export class TransactionsService { } const skip = (params.pageNo - 1) * params.pageSize - const txHashes = allTxHashes.slice(skip, skip + params.pageSize) + const needSort = + ['type', 'value', 'timestamp'].includes(params.sort ?? '') && + ['increase', 'decrease'].includes(params.direction ?? '') + const txHashes = needSort ? allTxHashes : allTxHashes.slice(skip, skip + params.pageSize) const transactions = await connection .getRepository(TransactionEntity) @@ -467,7 +500,16 @@ export class TransactionsService { return { totalCount, - items: txs, + items: needSort + ? txs + .sort((a, b) => { + if (params.sort === 'type') return TransactionsService.ComparedTxType(a, b, params.direction!) + if (params.sort === 'value') return TransactionsService.ComparedTxBalance(a, b, params.direction!) + if (params.sort === 'timestamp') return TransactionsService.ComparedTxTimestamp(a, b, params.direction!) + return 0 + }) + .slice(skip, skip + params.pageSize) + : txs, } } @@ -488,7 +530,11 @@ export class TransactionsService { } const tx = txInDB.toModel() tx.inputs = await this.fillInputFields(txWithStatus.transaction.inputs) - tx.outputs = txWithStatus.transaction.outputs + tx.outputs = txWithStatus.transaction.outputs.map((v, idx) => + Output.fromObject({ ...v, data: txWithStatus.transaction.outputsData[idx] }) + ) + tx.size = TransactionSize.tx(tx) + tx.cycles = txWithStatus.cycles return tx } diff --git a/packages/neuron-wallet/src/types/controller.d.ts b/packages/neuron-wallet/src/types/controller.d.ts index ea54bc7f0f..a59ea9fa0a 100644 --- a/packages/neuron-wallet/src/types/controller.d.ts +++ b/packages/neuron-wallet/src/types/controller.d.ts @@ -16,6 +16,8 @@ declare namespace Controller { pageSize: number keywords: string walletID: string + sort?: string + direction?: string } interface GenerateTransferNftTxParams { walletID: string diff --git a/packages/neuron-wallet/tests/services/cells.test.ts b/packages/neuron-wallet/tests/services/cells.test.ts index 085ca35b3c..3bb81943f1 100644 --- a/packages/neuron-wallet/tests/services/cells.test.ts +++ b/packages/neuron-wallet/tests/services/cells.test.ts @@ -2,6 +2,7 @@ import type { OutPoint as OutPointSDK } from '@ckb-lumos/base' import { scriptToAddress } from '../../src/utils/scriptAndAddress' import { bytes } from '@ckb-lumos/codec' import OutputEntity from '../../src/database/chain/entities/output' +import InputEntity from '../../src/database/chain/entities/input' import { OutputStatus } from '../../src/models/chain/output' import CellsService, { CustomizedLock, @@ -192,6 +193,19 @@ describe('CellsService', () => { return multisigCell } + const generateTx = (hash: string, timestamp: string, inputs: InputEntity[] = []) => { + const tx = new TransactionEntity() + tx.hash = hash + tx.version = '0x0' + tx.timestamp = timestamp + tx.status = TransactionStatus.Success + tx.witnesses = [] + tx.blockNumber = '1' + tx.blockHash = '0x' + '10'.repeat(32) + tx.inputs = inputs + return tx + } + const typeScript = new Script(randomHex(), '0x', ScriptHashType.Data) it('getLiveCell', async () => { @@ -834,17 +848,6 @@ describe('CellsService', () => { describe('#getDaoCells', () => { const depositData = '0x0000000000000000' const withdrawData = '0x000000000000000a' - const generateTx = (hash: string, timestamp: string) => { - const tx = new TransactionEntity() - tx.hash = hash - tx.version = '0x0' - tx.timestamp = timestamp - tx.status = TransactionStatus.Success - tx.witnesses = [] - tx.blockNumber = '1' - tx.blockHash = '0x' + '10'.repeat(32) - return tx - } const createCells = async () => { const tx1 = generateTx('0x1234', '1572862777481') @@ -1066,17 +1069,6 @@ describe('CellsService', () => { describe('#addUnlockInfo', () => { const depositData = '0x0000000000000000' const withdrawData = '0x000000000000000a' - const generateTx = (hash: string, timestamp: string) => { - const tx = new TransactionEntity() - tx.hash = hash - tx.version = '0x0' - tx.timestamp = timestamp - tx.status = TransactionStatus.Success - tx.witnesses = [] - tx.blockNumber = '1' - tx.blockHash = '0x' + '10'.repeat(32) - return tx - } const withdrawTxHash = '0x' + '2'.repeat(64) @@ -1138,17 +1130,6 @@ describe('CellsService', () => { describe('#addDepositInfo', () => { const depositData = '0x0000000000000000' const withdrawData = '0x000000000000000a' - const generateTx = (hash: string, timestamp: string) => { - const tx = new TransactionEntity() - tx.hash = hash - tx.version = '0x0' - tx.timestamp = timestamp - tx.status = TransactionStatus.Success - tx.witnesses = [] - tx.blockNumber = '1' - tx.blockHash = '0x' + '10'.repeat(32) - return tx - } const depositTxHash = '0x' + '0'.repeat(64) const depositTx = Transaction.fromObject({ @@ -1848,4 +1829,102 @@ describe('CellsService', () => { expect(CellsService.getCellTypeType(output)).toBe(TypeScriptCategory.Unknown) }) }) + + describe('getDaoWithdrawAndDeposit', () => { + const generateInput = ( + params: { + capacity?: string + typeScript?: Script | null + who?: any + data?: string + transaction?: TransactionEntity | null + transactionHash?: string + } = {} + ) => { + const input = new InputEntity() + input.transactionHash = params.transaction?.hash ?? params.transactionHash ?? randomHex() + input.outPointTxHash = randomHex() + input.outPointIndex = '0' + input.capacity = params?.capacity ?? toShannon('1000') + const who = params.who ?? bob + input.lockCodeHash = who.lockScript.codeHash + input.lockArgs = who.lockScript.args + input.lockHashType = who.lockScript.hashType + if (who.lockScript.codeHash === SystemScriptInfo.MULTI_SIGN_CODE_HASH) { + input.multiSignBlake160 = who.lockScript.args + } + input.lockHash = who.lockScript.computeHash() + input.data = params.data ?? '0x' + const typeScript = params.typeScript ?? SystemScriptInfo.generateDaoScript('0x') + if (typeScript) { + input.typeCodeHash = typeScript.codeHash + input.typeArgs = typeScript.args + input.typeHashType = typeScript.hashType + input.typeHash = typeScript.computeHash() + } + if (params.transaction) { + input.transaction = params.transaction + } + input.since = '0' + return input + } + const saveTxAndInput = async ( + params: { + capacity?: string + typeScript?: Script | null + who?: any + data?: string + } = {} + ) => { + const tx = generateTx(randomHex(), '1572862777481') + const input = await generateInput({ ...params, transactionHash: tx.hash }) + await getConnection().manager.save([tx, input]) + return input + } + it('no input', async () => { + const input = await saveTxAndInput() + await expect(CellsService.getDaoWithdrawAndDeposit(input.transactionHash)).rejects.toThrow( + new Error(`No unlock transaction use ${input.transactionHash} as input`) + ) + }) + it('can not find output', async () => { + const input = await saveTxAndInput({ data: '0x1234' }) + const output = await generateCell(toShannon('1000'), OutputStatus.Dead, false, null) + await getConnection().manager.save(output) + await expect(CellsService.getDaoWithdrawAndDeposit(input.transactionHash)).rejects.toThrow( + new Error(`${input.transactionHash} is not a DAO transaction`) + ) + }) + it('output without deposit tx', async () => { + const input = await saveTxAndInput({ data: '0x1234' }) + const output = await generateCell(toShannon('1000'), OutputStatus.Dead, false, null) + output.outPointTxHash = input.outPointTxHash! + output.outPointIndex = input.outPointIndex! + await getConnection().manager.save(output) + await expect(CellsService.getDaoWithdrawAndDeposit(input.transactionHash)).rejects.toThrow( + new Error(`${input.transactionHash} is not a DAO transaction`) + ) + }) + it('success', async () => { + const input = await saveTxAndInput({ data: '0x1234' }) + const output = await generateCell(toShannon('1000'), OutputStatus.Dead, false, null) + const tx = generateTx(randomHex(), '1572862777481') + output.outPointTxHash = input.outPointTxHash! + output.outPointIndex = input.outPointIndex! + output.depositTxHash = randomHex() + output.depositIndex = '0' + tx.outputs = [output] + await getConnection().manager.save([tx, output]) + const res = await CellsService.getDaoWithdrawAndDeposit(input.transactionHash) + expect(res).toStrictEqual([ + { + withdrawBlockHash: tx.blockHash, + depositOutPoint: OutPoint.fromSDK({ + txHash: output.depositTxHash, + index: output.depositIndex, + }), + }, + ]) + }) + }) })