From 97fdaa98079a04728d24ccaebd5d641ba273bba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=83=E5=81=B6=E4=BB=80=E4=B9=88=E7=9A=84=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E5=B8=83=E5=81=B6?= Date: Tue, 25 Jun 2024 15:35:29 +0800 Subject: [PATCH] feat: transaction page support node-connect mode (#350) * feat: transaction page support node-connect mode * chore: optimize english text for readable * feat: support cellbase display * feat: optimize ver name * feat: support node-connect mode state hold * feat: home page support get lastest tx & block by node * chore: fix typo --- package.json | 4 + src/App.tsx | 25 +- .../Header/CKBNodeComp/CKBNodeModal.tsx | 44 +++ .../Header/CKBNodeComp/NodeAlert.tsx | 25 ++ .../Header/CKBNodeComp/style.module.scss | 101 ++++++ src/components/Header/MaintainAlert/index.tsx | 4 +- .../Header/MenusComp/index.module.scss | 26 ++ src/components/Header/MenusComp/index.tsx | 28 +- src/components/Header/index.tsx | 5 + src/components/RawTransactionView/index.tsx | 6 +- src/components/Transaction/Cellbase/index.tsx | 13 +- .../TransactionCellArrow/index.tsx | 36 ++- .../TransactionItemCell/index.tsx | 22 +- src/components/TransactionItem/index.tsx | 6 +- .../TransactionParameters/index.tsx | 5 +- src/components/ui/Alert.tsx | 1 - src/components/ui/RadioGroup.tsx | 1 - src/components/ui/Switch.module.scss | 57 ++++ src/components/ui/Switch.tsx | 16 + src/components/ui/Tabs.tsx | 1 - src/constants/common.ts | 2 +- src/hooks/block.tsx | 75 +++++ src/hooks/useCKBNode.tsx | 38 +++ src/locales/en.json | 7 + src/locales/zh.json | 7 + src/pages/Home/TableCard/index.tsx | 22 +- src/pages/Home/index.tsx | 146 +++++---- .../AddressConversion/AddressToScript.tsx | 1 - .../parseMultiVersionAddress.ts | 1 - src/pages/Tools/BroadcastTx/index.tsx | 5 +- src/pages/Tools/MoleculeParser/DataInput.tsx | 1 - src/pages/Tools/MoleculeParser/Molecule.tsx | 1 - .../Tools/MoleculeParser/SchemaSelect.tsx | 1 - src/pages/Tools/MoleculeParser/constants.ts | 1 - src/pages/Tools/MoleculeParser/index.tsx | 2 - .../TransactionCell/NodeTransactionCell.tsx | 288 ++++++++++++++++++ .../Transaction/TransactionCell/index.tsx | 62 ++-- .../Transaction/TransactionCell/styled.tsx | 30 +- .../NodeTransactionCellBase.tsx | 113 +++++++ .../NodeTransactionCellList.tsx | 63 ++++ .../TransactionCellList.tsx | 23 ++ .../Transaction/TransactionCellList/index.tsx | 87 ++---- .../TransactionCellList/styled.tsx | 22 +- .../useIsDeprecatedAddressesDisplayed.tsx | 18 ++ .../TransactionCellScript/index.tsx | 159 ++++++++++ .../TransactionComp/NodeTransactionComp.tsx | 49 +++ .../NodeTransactionOverview.tsx | 248 +++++++++++++++ .../TransactionComp/TransactionComp.tsx | 33 +- .../TransactionDetailsHeader.tsx | 7 +- .../TransactionLite/TransactionLite.tsx | 12 +- .../TransactionComp/TransactionOverview.tsx | 2 +- .../Transaction/TransactionReward/index.tsx | 28 +- src/pages/Transaction/index.tsx | 83 +++-- src/pages/Xudt/UDTComp.tsx | 2 +- src/services/NodeService/index.ts | 128 ++++++-- src/utils/address.ts | 15 + src/utils/cell.ts | 81 +++++ src/utils/number.ts | 32 ++ src/utils/transaction.ts | 84 +++++ yarn.lock | 30 ++ 60 files changed, 2099 insertions(+), 336 deletions(-) create mode 100644 src/components/Header/CKBNodeComp/CKBNodeModal.tsx create mode 100644 src/components/Header/CKBNodeComp/NodeAlert.tsx create mode 100644 src/components/Header/CKBNodeComp/style.module.scss create mode 100644 src/components/ui/Switch.module.scss create mode 100644 src/components/ui/Switch.tsx create mode 100644 src/hooks/block.tsx create mode 100644 src/hooks/useCKBNode.tsx create mode 100644 src/pages/Transaction/TransactionCell/NodeTransactionCell.tsx create mode 100644 src/pages/Transaction/TransactionCellList/NodeTransactionCellBase.tsx create mode 100644 src/pages/Transaction/TransactionCellList/NodeTransactionCellList.tsx create mode 100644 src/pages/Transaction/TransactionCellList/TransactionCellList.tsx create mode 100644 src/pages/Transaction/TransactionCellList/useIsDeprecatedAddressesDisplayed.tsx create mode 100644 src/pages/Transaction/TransactionComp/NodeTransactionComp.tsx create mode 100644 src/pages/Transaction/TransactionComp/NodeTransactionOverview.tsx create mode 100644 src/utils/address.ts create mode 100644 src/utils/cell.ts create mode 100644 src/utils/transaction.ts diff --git a/package.json b/package.json index 66f8b4611..0d2ad0056 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,19 @@ "dependencies": { "@ant-design/icons": "4.8.1", "@ckb-lumos/base": "0.23.0", + "@ckb-lumos/bi": "0.23.0", "@ckb-lumos/codec": "0.23.0", "@ckb-lumos/config-manager": "0.23.0", + "@ckb-lumos/common-scripts": "0.23.0", "@ckb-lumos/helpers": "0.23.0", "@ckb-lumos/molecule": "0.23.0", + "@ckb-lumos/rpc": "0.23.0", "@microlink/react-json-view": "1.23.0", "@nervosnetwork/ckb-sdk-rpc": "0.109.1", "@nervosnetwork/ckb-sdk-utils": "0.109.1", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-radio-group": "1.1.3", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@rgbpp-sdk/ckb": "0.0.0-snap-20240408100333", "@sentry/react": "7.117.0", diff --git a/src/App.tsx b/src/App.tsx index 60ada71e3..ed6fd8ca8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,8 +5,12 @@ import Routers from './routes' import Toast from './components/Toast' import { isMainnet } from './utils/chain' import { DASQueryContextProvider } from './hooks/useDASAccount' +import { CKBNodeProvider } from './hooks/useCKBNode' import { getPrimaryColor, getSecondaryColor } from './constants/common' import Decoder from './components/Decoder' +import config from './config' + +const { BACKUP_NODES: backupNodes } = config const appStyle = { width: '100vw', @@ -14,7 +18,16 @@ const appStyle = { maxWidth: '100%', } -const queryClient = new QueryClient() +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: Infinity, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, + }, + }, +}) const App = () => { const theme = useMemo( @@ -30,10 +43,12 @@ const App = () => {
- - - - + + + + + +
diff --git a/src/components/Header/CKBNodeComp/CKBNodeModal.tsx b/src/components/Header/CKBNodeComp/CKBNodeModal.tsx new file mode 100644 index 000000000..ec84308bf --- /dev/null +++ b/src/components/Header/CKBNodeComp/CKBNodeModal.tsx @@ -0,0 +1,44 @@ +import { useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { useCKBNode } from '../../../hooks/useCKBNode' +import CommonModal from '../../CommonModal' +import { HelpTip } from '../../HelpTip' +import { Switch } from '../../ui/Switch' +import CloseIcon from '../../../assets/modal_close.png' +import styles from './style.module.scss' + +export const CKBNodeModal = ({ onClose }: { onClose: () => void }) => { + const { t } = useTranslation() + const ref = useRef(null) + const { isActivated, setIsActivated } = useCKBNode() + + return ( + +
+
+
+

{t('navbar.node')}

+ +
+ +
+ + + setIsActivated(checked)} + /> +
+ + +
+
+
+ ) +} diff --git a/src/components/Header/CKBNodeComp/NodeAlert.tsx b/src/components/Header/CKBNodeComp/NodeAlert.tsx new file mode 100644 index 000000000..4aee19aca --- /dev/null +++ b/src/components/Header/CKBNodeComp/NodeAlert.tsx @@ -0,0 +1,25 @@ +import { useState } from 'react' +import { Trans } from 'react-i18next' +import { CKBNodeModal } from './CKBNodeModal' + +import styles from './style.module.scss' + +const NodeAlert = () => { + const [nodeModalVisible, setNodeModalVisible] = useState(false) + return ( + <> +
+ setNodeModalVisible(true)} />, + }} + /> +
+ {nodeModalVisible ? setNodeModalVisible(false)} /> : null} + + ) +} + +export default NodeAlert diff --git a/src/components/Header/CKBNodeComp/style.module.scss b/src/components/Header/CKBNodeComp/style.module.scss new file mode 100644 index 000000000..4ce2eb30d --- /dev/null +++ b/src/components/Header/CKBNodeComp/style.module.scss @@ -0,0 +1,101 @@ +@import '../../../styles/variables.module'; + +.contentWrapper { + background-color: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 4px; + width: 80vw; + max-width: 280px; + padding: 24px; + + @media screen and (width <= 750px) { + padding: 16px; + width: calc(100vw - 32px); + } + + p { + margin: 0; + } +} + +.modalTitle { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + color: #333; + font-weight: 600; + font-size: 16px; + line-height: 20px; +} + +.modalContent { + width: 100%; + margin-top: 32px; +} + +.languageSelect { + color: #333; +} + +.closeBtn { + border: none; + background: transparent; + cursor: pointer; + + img { + width: 13px; + height: 13px; + } +} + +.doneBtn { + width: 100%; + margin-top: 32px; + height: 47px; + background: var(--primary-color); + border: none; + border-radius: 4px; + color: #fff; + font-size: 16px; + cursor: pointer; +} + +.loading { + text-align: center; +} + +.switcher { + margin-top: 32px; + width: 100%; + display: flex; + align-items: center; + + label { + color: black; + } +} + +.alert { + position: sticky; + color: #fff; + font-size: 14px; + font-weight: 450; + line-height: 1.2; + text-align: center; + padding: 1rem; + background-color: #fa8f00; + + @media (width <= $mobileBreakPoint) { + text-align: left; + } +} + +.clickable { + color: var(--primary-color); + cursor: pointer; + text-decoration: underline; +} diff --git a/src/components/Header/MaintainAlert/index.tsx b/src/components/Header/MaintainAlert/index.tsx index d871f38c7..6361dc4d7 100644 --- a/src/components/Header/MaintainAlert/index.tsx +++ b/src/components/Header/MaintainAlert/index.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import axios from 'axios' import config from '../../../config' +import { useCKBNode } from '../../../hooks/useCKBNode' import { useLatestBlockNumber } from '../../../services/ExplorerService' import styles from './styles.module.scss' @@ -25,6 +26,7 @@ const getTipFromNode = (url: string): Promise => const MaintainAlert = () => { const { t } = useTranslation() const synced = useLatestBlockNumber() + const { isActivated } = useCKBNode() const { data: tip } = useQuery( ['backup_nodes'], async () => { @@ -49,7 +51,7 @@ const MaintainAlert = () => { const lag = tip && synced ? tip - synced : 0 - return lag >= threshold ? ( + return lag >= threshold && !isActivated ? (
{t('error.maintain', { tip: tip?.toLocaleString('en'), lag: lag.toLocaleString('en') })}
diff --git a/src/components/Header/MenusComp/index.module.scss b/src/components/Header/MenusComp/index.module.scss index b8f162ec9..ff4a3a407 100644 --- a/src/components/Header/MenusComp/index.module.scss +++ b/src/components/Header/MenusComp/index.module.scss @@ -85,3 +85,29 @@ .moreMenus { margin-right: 0; } + +.linkWithBadge { + display: flex !important; + align-items: center; + gap: 8px; +} + +.nodeStatus { + display: block; + width: 12px; + height: 12px; + border-radius: 999px; +} + +.activate { + background-color: #00cc9b; +} + +.clickable { + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: var(--primary-color); + } +} diff --git a/src/components/Header/MenusComp/index.tsx b/src/components/Header/MenusComp/index.tsx index 197223208..b9e3def97 100644 --- a/src/components/Header/MenusComp/index.tsx +++ b/src/components/Header/MenusComp/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { FC, memo, PropsWithChildren, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -8,9 +7,11 @@ import { Link } from '../../Link' import { MobileMenuItem, MobileMenuOuterLink, HeaderMenuPanel, MobileMenuInnerLink } from './styled' import styles from './index.module.scss' import { LanguageModal } from '../LanguageComp/LanguageModal' +import { CKBNodeModal } from '../CKBNodeComp/CKBNodeModal' import { ReactComponent as ArrowIcon } from './arrow.svg' import { IS_MAINNET } from '../../../constants/common' import { ReactComponent as MenuIcon } from './menu.svg' +import { useCKBNode } from '../../../hooks/useCKBNode' export enum LinkType { Inner, @@ -120,8 +121,10 @@ const SubmenuDropdown: FC { const { t } = useTranslation() + const { isActivated } = useCKBNode() const [open, setOpen] = useState(false) const [languageModalVisible, setLanguageModalVisible] = useState(false) + const [nodeModalVisible, setNodeModalVisible] = useState(false) const Wrapper = isMobile ? MobileMenuItem : ({ children }: PropsWithChildren<{}>) => <>{children} @@ -135,15 +138,25 @@ export const MoreMenu = ({ isMobile = false }: { isMobile?: boolean }) => { {t('footer.tools')} - { setOpen(false) setLanguageModalVisible(true) }} > {t('navbar.language')} - + + { + setOpen(false) + setNodeModalVisible(true) + }} + > + {t('navbar.node')} + + } mouseEnterDelay={0} @@ -156,12 +169,15 @@ export const MoreMenu = ({ isMobile = false }: { isMobile?: boolean }) => { ) : ( - + - + )} {languageModalVisible ? setLanguageModalVisible(false)} /> : null} + {nodeModalVisible ? setNodeModalVisible(false)} /> : null} ) } diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index fe42f1641..e11a9a4fc 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -9,9 +9,12 @@ import BlockchainComp from './BlockchainComp' import { useMediaQuery } from '../../hooks' import styles from './index.module.scss' import MaintainAlert from './MaintainAlert' +import NodeAlert from './CKBNodeComp/NodeAlert' + import Sheet from './Sheet' import { createGlobalState, useGlobalState } from '../../utils/state' import MobileMenu from './MobileMenu' +import { useCKBNode } from '../../hooks/useCKBNode' const LogoComp = () => ( @@ -54,6 +57,7 @@ export function useIsShowSearchBarInHeader() { export default () => { const isMobile = useMediaQuery(`(max-width: 1023px)`) + const { isActivated } = useCKBNode() const { pathname } = useLocation() const history = useHistory() // TODO: This hard-coded implementation is not ideal, but currently the header is loaded before the page component, @@ -98,6 +102,7 @@ export default () => { )} + {isActivated ? : null} {mobileMenuVisible && isMobile && setMobileMenuVisible(false)} />} diff --git a/src/components/RawTransactionView/index.tsx b/src/components/RawTransactionView/index.tsx index 057cdf089..ac4835291 100644 --- a/src/components/RawTransactionView/index.tsx +++ b/src/components/RawTransactionView/index.tsx @@ -2,12 +2,12 @@ import type { FC } from 'react' import { useQuery } from '@tanstack/react-query' import JsonView from '@microlink/react-json-view' import Loading from '../AwesomeLoadings/Spinner' - -import { getTx } from '../../services/NodeService' +import { useCKBNode } from '../../hooks/useCKBNode' import styles from './styles.module.scss' const RawTransactionView: FC<{ hash: string }> = ({ hash }) => { - const { data, isLoading } = useQuery(['tx', hash], () => getTx(hash)) + const { nodeService } = useCKBNode() + const { data, isLoading } = useQuery(['tx', hash], () => nodeService.getTx(hash)) if (isLoading) { return (
diff --git a/src/components/Transaction/Cellbase/index.tsx b/src/components/Transaction/Cellbase/index.tsx index 35c8eee3c..f1462efda 100644 --- a/src/components/Transaction/Cellbase/index.tsx +++ b/src/components/Transaction/Cellbase/index.tsx @@ -2,14 +2,19 @@ import { Tooltip } from 'antd' import { Trans } from 'react-i18next' import { Link } from '../../Link' import { CellbasePanel } from './styled' -import { CellType } from '../../../constants/common' -import TransactionCellArrow from '../TransactionCellArrow' +import { CellInputIcon } from '../TransactionCellArrow' import { localeNumberString } from '../../../utils/number' import HelpIcon from '../../../assets/qa_help.png' import styles from './index.module.scss' import { Cell } from '../../../models/Cell' -const Cellbase = ({ cell, cellType, isDetail }: { cell: Cell; cellType: CellType; isDetail?: boolean }) => { +const Cellbase = ({ + cell, + isDetail, +}: { + cell: Partial> + isDetail?: boolean +}) => { if (!cell.targetBlockNumber || cell.targetBlockNumber <= 0) { return ( @@ -30,7 +35,7 @@ const Cellbase = ({ cell, cellType, isDetail }: { cell: Cell; cellType: CellType return ( - {cellType === CellType.Input && } +
Cellbase for Block
cellbase help diff --git a/src/components/Transaction/TransactionCellArrow/index.tsx b/src/components/Transaction/TransactionCellArrow/index.tsx index e85751bea..155ea78de 100644 --- a/src/components/Transaction/TransactionCellArrow/index.tsx +++ b/src/components/Transaction/TransactionCellArrow/index.tsx @@ -1,7 +1,7 @@ import { Tooltip } from 'antd' import { useTranslation } from 'react-i18next' import { Link } from '../../Link' -import { CellType } from '../../../constants/common' +import { IOType } from '../../../constants/common' import RightGreenArrow from './right_green_arrow.png' import RightBlueArrow from './right_blue_arrow.png' import LiveCellIcon from './live_cell.png' @@ -10,33 +10,45 @@ import { isMainnet } from '../../../utils/chain' import { RightArrowImage, LeftArrowImage } from './styled' import { Cell } from '../../../models/Cell' -const CellInputIcon = ({ cell }: { cell: Cell }) => +export const RightArrow = ({ status = 'live' }: { status?: Cell['status'] }) => { + if (status === 'live') { + return + } + + return +} + +export const LeftArrow = () => ( + +) + +export const CellInputIcon = ({ cell }: { cell: Partial> }) => cell.generatedTxHash ? ( - + ) : null -const CellOutputIcon = ({ cell }: { cell: Cell }) => { +export const CellOutputIcon = ({ cell }: { cell: Pick }) => { const { t } = useTranslation() if (cell.status === 'dead') { return ( - + ) } return ( - + ) } -export default ({ cell, cellType }: { cell: Cell; cellType: CellType }) => - cellType === CellType.Input ? : +export default ({ cell, ioType }: { cell: Cell; ioType: IOType }) => + ioType === IOType.Input ? : diff --git a/src/components/TransactionItem/TransactionItemCell/index.tsx b/src/components/TransactionItem/TransactionItemCell/index.tsx index 1bcd1e435..47690d438 100644 --- a/src/components/TransactionItem/TransactionItemCell/index.tsx +++ b/src/components/TransactionItem/TransactionItemCell/index.tsx @@ -18,8 +18,8 @@ import { TransactionCellWithdraw, TransactionCellUDTPanel, } from './styled' -import { CellType } from '../../../constants/common' -import TransactionCellArrow from '../../Transaction/TransactionCellArrow' +import { IOType } from '../../../constants/common' +import { CellInputIcon, CellOutputIcon } from '../../Transaction/TransactionCellArrow' import Capacity from '../../Capacity' import { parseDiffDate } from '../../../utils/date' import Cellbase from '../../Transaction/Cellbase' @@ -162,13 +162,13 @@ const WithdrawPopoverInfo = ({ cell }: { cell: Cell }) => { ) } -const TransactionCellNervosDao = ({ cell, cellType }: { cell: Cell; cellType: CellType }) => { +const TransactionCellNervosDao = ({ cell, ioType }: { cell: Cell; ioType: IOType }) => { const isMobile = useIsMobile() const { t } = useTranslation() return ( - {cellType === CellType.Input ? ( + {ioType === IOType.Input ? ( } trigger="click"> withdraw @@ -209,9 +209,9 @@ const TransactionCellUDT = ({ cell }: { cell: Cell$UDT }) => { ) } -const TransactionCellCapacity = ({ cell, cellType }: { cell: Cell; cellType: CellType }) => { +const TransactionCellCapacity = ({ cell, ioType }: { cell: Cell; ioType: IOType }) => { if (isDaoCell(cell.cellType)) { - return + return } if (cell.cellType === 'udt') { @@ -255,11 +255,11 @@ const TransactionCellCapacity = ({ cell, cellType }: { cell: Cell; cellType: Cel ) } -const TransactionCell = ({ cell, address, cellType }: { cell: Cell; address?: string; cellType: CellType }) => { +const TransactionCell = ({ cell, address, ioType }: { cell: Cell; address?: string; ioType: IOType }) => { const isMobile = useIsMobile() const { t } = useTranslation() if (cell.fromCellbase) { - return + return } let addressText = t('address.unable_decode_address') @@ -274,12 +274,12 @@ const TransactionCell = ({ cell, address, cellType }: { cell: Cell; address?: st return (
- {cellType === CellType.Input && } + {ioType === IOType.Input && } - {cellType === CellType.Output && } + {ioType === IOType.Output && } {!highLight && !isMobile && ( current Address @@ -292,7 +292,7 @@ const TransactionCell = ({ cell, address, cellType }: { cell: Cell; address?: st current Address )} - + ) diff --git a/src/components/TransactionItem/index.tsx b/src/components/TransactionItem/index.tsx index c15d64fe2..0accbf2d3 100644 --- a/src/components/TransactionItem/index.tsx +++ b/src/components/TransactionItem/index.tsx @@ -7,7 +7,7 @@ import TransactionCell from './TransactionItemCell' import TransactionCellList from './TransactionItemCellList' import TransactionIncome from './TransactionIncome' import { FullPanel, TransactionHashBlockPanel, TransactionCellPanel, TransactionPanel } from './styled' -import { CellType } from '../../constants/common' +import { IOType } from '../../constants/common' import AddressText from '../AddressText' import { useParsedDate } from '../../hooks' import { Transaction } from '../../models/Transaction' @@ -131,7 +131,7 @@ const TransactionItem = ({ } + render={cell => } />
@@ -142,7 +142,7 @@ const TransactionItem = ({ transaction={transaction} render={cell => ( - + )} /> diff --git a/src/components/TransactionParameters/index.tsx b/src/components/TransactionParameters/index.tsx index 9e4462daa..dbe6ce379 100644 --- a/src/components/TransactionParameters/index.tsx +++ b/src/components/TransactionParameters/index.tsx @@ -3,7 +3,7 @@ import { FC, ReactNode } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import classNames from 'classnames' -import { getTx } from '../../services/NodeService' +import { useCKBNode } from '../../hooks/useCKBNode' import { matchTxHash } from '../../utils/util' import Loading from '../AwesomeLoadings/Spinner' import HashTag from '../HashTag' @@ -48,8 +48,9 @@ const Field = ({ const TransactionParameters: FC<{ hash: string }> = ({ hash }) => { const [t] = useTranslation() + const { nodeService } = useCKBNode() - const { data, isLoading } = useQuery(['tx', hash], () => getTx(hash)) + const { data, isLoading } = useQuery(['tx', hash], () => nodeService.getTx(hash)) if (isLoading) { return (
diff --git a/src/components/ui/Alert.tsx b/src/components/ui/Alert.tsx index 338f87d0c..472b57262 100644 --- a/src/components/ui/Alert.tsx +++ b/src/components/ui/Alert.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import * as React from 'react' import classnames from 'classnames' import styles from './Alert.module.scss' diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx index 4689827b3..df2ac708f 100644 --- a/src/components/ui/RadioGroup.tsx +++ b/src/components/ui/RadioGroup.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import React from 'react' import { CheckIcon } from '@radix-ui/react-icons' import * as RadioGroupPrimitive from '@radix-ui/react-radio-group' diff --git a/src/components/ui/Switch.module.scss b/src/components/ui/Switch.module.scss new file mode 100644 index 000000000..eee946d79 --- /dev/null +++ b/src/components/ui/Switch.module.scss @@ -0,0 +1,57 @@ +.switch { + display: inline-flex; + flex-shrink: 0; + align-items: center; + border-radius: 9999px; + border-width: 2px; + border-color: transparent; + width: 2.75rem; + height: 1.5rem; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + cursor: pointer; + + &:focus-visible { + outline-style: none; + box-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + + --ring-offset-width: 2px; + + box-shadow: 0 0 0 var(--ring-offset-width) var(--ring-offset-color), var(--ring-shadow); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &[data-state='checked'] { + background-color: var(--primary-color); + } + + &[data-state='unchecked'] { + background-color: #aaa; + } +} + +.thumb { + background-color: #fff; + display: block; + border-radius: 9999px; + width: 1.25rem; + height: 1.25rem; + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + pointer-events: none; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 10%), 0 4px 6px -2px rgb(0 0 0 / 5%); + + &[data-state='checked'] { + transform: translateX(1.25rem); + } + + &[data-state='unchecked'] { + transform: translateX(0); + } +} diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx new file mode 100644 index 000000000..1743bbea1 --- /dev/null +++ b/src/components/ui/Switch.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import * as SwitchPrimitives from '@radix-ui/react-switch' +import classnames from 'classnames' +import styles from './Switch.module.scss' + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx index 668040fde..5c1d55ad2 100644 --- a/src/components/ui/Tabs.tsx +++ b/src/components/ui/Tabs.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import * as React from 'react' import * as TabsPrimitive from '@radix-ui/react-tabs' import classnames from 'classnames' diff --git a/src/constants/common.ts b/src/constants/common.ts index de0146c16..339849c55 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -102,7 +102,7 @@ export const SearchFailType = { CHAIN_ERROR: 'chain_error', } -export enum CellType { +export enum IOType { Input = 'input', Output = 'output', } diff --git a/src/hooks/block.tsx b/src/hooks/block.tsx new file mode 100644 index 000000000..2c2a7c527 --- /dev/null +++ b/src/hooks/block.tsx @@ -0,0 +1,75 @@ +import { useQueries, useQuery } from '@tanstack/react-query' +import { parseEpoch } from '@ckb-lumos/base/lib/since' +import { Block, Consensus } from '@ckb-lumos/base' +import { useCKBNode } from './useCKBNode' +import { encodeNewAddress } from '../utils/address' + +function calculateBaseReward(epochString: string, consensus: Consensus) { + const epoch = parseEpoch(epochString) + const halvingTimes = Math.floor(epoch.number / parseInt(consensus.primaryEpochRewardHalvingInterval, 16)) + return parseInt(consensus.initialPrimaryEpochReward, 16) / (halvingTimes * 2) / epoch.length +} + +function useLatestBlocks(count = 15) { + const { nodeService, isActivated } = useCKBNode() + const { data: tipBlockNumber } = useQuery(['node', 'tipBlockNumber'], () => nodeService.rpc.getTipBlockNumber(), { + refetchOnReconnect: true, + refetchOnWindowFocus: true, + staleTime: 10 * 1000, + refetchInterval: 12 * 1000, + keepPreviousData: true, + enabled: isActivated, + }) + + const blockNumbers = tipBlockNumber ? Array.from({ length: count }, (_, i) => parseInt(tipBlockNumber, 16) - i) : [] + + const queries = useQueries({ + queries: blockNumbers.map(blockNumber => ({ + queryKey: ['node', 'block', blockNumber], + queryFn: () => nodeService.rpc.getBlockByNumber(`0x${blockNumber.toString(16)}`), + })), + }) + + return queries.map(({ data }) => data).filter(data => data) as Block[] +} + +export function useNodeLatestBlocks(count = 15) { + const { nodeService } = useCKBNode() + const blocks = useLatestBlocks(count) + const { data: consensus } = useQuery(['node', 'consensus'], () => nodeService.rpc.getConsensus()) + + return blocks.map(block => ({ + number: parseInt(block.header.number, 16), + timestamp: parseInt(block.header.timestamp, 16), + liveCellChanges: block.transactions.reduce((acc, tx) => acc + (tx.outputs.length - tx.inputs.length), 1).toString(), + reward: consensus ? calculateBaseReward(block.header.epoch, consensus).toString() : '0', + transactionsCount: block.transactions.length, + minerHash: encodeNewAddress(block.transactions[0].outputs[0].lock), + })) +} + +export function useNodeLatestTransactions(count = 15) { + const blocks = useLatestBlocks(count) + + const blockTransactions = blocks.reduce( + (acc, block) => [ + ...acc, + ...block.transactions.map((tx, i) => ({ + transactionHash: tx.hash!, + blockNumber: block.header.number, + blockTimestamp: block.header.timestamp, + capacityInvolved: tx.outputs.reduce((acc, output) => acc + parseInt(output.capacity, 16), 0).toString(), + liveCellChanges: ((i === 0 ? 1 : 0) + tx.outputs.length - tx.inputs.length).toString(), + })), + ], + [] as { + transactionHash: string + blockNumber: string | number + blockTimestamp: string | number + capacityInvolved: string + liveCellChanges: string + }[], + ) + + return blockTransactions.slice(0, count) +} diff --git a/src/hooks/useCKBNode.tsx b/src/hooks/useCKBNode.tsx new file mode 100644 index 000000000..54849aecf --- /dev/null +++ b/src/hooks/useCKBNode.tsx @@ -0,0 +1,38 @@ +import { useContext, createContext, useState, PropsWithChildren } from 'react' +import { NodeService } from '../services/NodeService' + +const NODE_CONNECT_MODE_KEY = 'node_connect_mode' + +export interface ICKBNodeContext { + nodeService: NodeService + isActivated: boolean + setIsActivated: (isActivated: boolean) => void +} + +export const CKBNodeContext = createContext(undefined) + +export const useCKBNode = (): ICKBNodeContext => { + const context = useContext(CKBNodeContext) + if (!context) { + throw new Error('No CKBNodeContext.Provider found when calling useCKBNode.') + } + return context +} + +interface CKBNodeProviderProps { + defaultEndpoint: string +} + +export const CKBNodeProvider = ({ children, defaultEndpoint }: PropsWithChildren) => { + const nodeService = new NodeService(defaultEndpoint) + const [isActivated, _setIsActivated] = useState(localStorage.getItem(NODE_CONNECT_MODE_KEY) === 'true') + + const setIsActivated = (value: boolean) => { + localStorage.setItem(NODE_CONNECT_MODE_KEY, value.toString()) + _setIsActivated(value) + } + + return ( + {children} + ) +} diff --git a/src/locales/en.json b/src/locales/en.json index ee15ce06e..440cb1aa2 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -179,6 +179,7 @@ "language": "Language", "language_en": "EN", "language_zh": "简体中文", + "node": "Node", "home": "Home", "wallets": "Wallets", "wallet_abc": "ABC Wallet", @@ -973,6 +974,12 @@ "transaction": "Transaction", "tx_in_snake_case": "Transaction in Snake Case", "tx_in_camel_case": "Transaction in Camel Case" + }, + "node": { + "alert": "Currently on the node-connect mode. The explorer data will be fetched from the node, so some features/data cannot be supported. You can click here to switch", + "done": "Done", + "node_connect_mode": "Node Connect Mode", + "node_connect_tooltip": "When the node-connection mode is enabled, the data will be fetched from the node directly, so some features/data will not be supported." } } } diff --git a/src/locales/zh.json b/src/locales/zh.json index 24f2e8e9a..aeea14782 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -179,6 +179,7 @@ "language": "语言", "language_en": "EN", "language_zh": "简体中文", + "node": "节点", "home": "首页", "wallets": "钱包", "wallet_abc": "ABC Wallet", @@ -974,6 +975,12 @@ "transaction": "交易", "tx_in_snake_case": "Snake Case 格式交易", "tx_in_camel_case": "Camel Case 格式交易" + }, + "node": { + "alert": "当前使用节点模式。在节点模式中,浏览器数据将取自节点,因此部分功能/数据无法不再支持,同时。您可在浏览器右上角或点击此处切换节点", + "done": "完成", + "node_connect_mode": "节点连接模式", + "node_connect_tooltip": "打开节点连接模式时,浏览器数据将从节点获取,因此无法再支持某些功能/数据。" } } } diff --git a/src/pages/Home/TableCard/index.tsx b/src/pages/Home/TableCard/index.tsx index 6b15696f7..408231836 100644 --- a/src/pages/Home/TableCard/index.tsx +++ b/src/pages/Home/TableCard/index.tsx @@ -9,11 +9,19 @@ import { BlockCardPanel, TransactionCardPanel } from './styled' import AddressText from '../../../components/AddressText' import styles from './index.module.scss' import { useParsedDate } from '../../../hooks' -import { Block } from '../../../models/Block' -import { Transaction } from '../../../models/Transaction' // eslint-disable-next-line no-underscore-dangle -const _BlockCardItem: FC<{ block: Block; isDelayBlock?: boolean }> = ({ block, isDelayBlock }) => { +const _BlockCardItem: FC<{ + block: { + number: number + timestamp: number + liveCellChanges: string + reward: string + transactionsCount: number + minerHash: string + } + isDelayBlock?: boolean +}> = ({ block, isDelayBlock }) => { const { t } = useTranslation() const liveCellChanges = Number(block.liveCellChanges) const [int, dec] = new BigNumber(shannonToCkb(block.reward)).toFormat(2, BigNumber.ROUND_FLOOR).split('.') @@ -61,7 +69,13 @@ export const BlockCardItem = memo( // eslint-disable-next-line no-underscore-dangle const _TransactionCardItem: FC<{ - transaction: Transaction + transaction: { + transactionHash: string + blockNumber: string | number + blockTimestamp: string | number + capacityInvolved: string + liveCellChanges: string + } tipBlockNumber: number }> = ({ transaction, tipBlockNumber }) => { const { t } = useTranslation() diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 6b3af035e..220fc2480 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -23,6 +23,7 @@ import LatestTransactionsIcon from './latest_transactions.png' import { BlockCardItem, TransactionCardItem } from './TableCard' import Loading from '../../components/Loading/SmallLoading' import { useElementIntersecting, useIsExtraLarge, useIsMobile, useSingleHalving } from '../../hooks' +import { useNodeLatestBlocks, useNodeLatestTransactions } from '../../hooks/block' import Banner from './Banner' import { HalvingBanner } from './Banner/HalvingBanner' import Search from '../../components/Search' @@ -32,8 +33,7 @@ import styles from './index.module.scss' import { RouteState } from '../../routes/state' import { explorerService, useLatestBlockNumber, useStatistics } from '../../services/ExplorerService' import { useShowSearchBarInHeader } from '../../components/Header' -import { Block } from '../../models/Block' -import { Transaction } from '../../models/Transaction' +import { useCKBNode } from '../../hooks/useCKBNode' interface BlockchainData { name: string @@ -151,7 +151,16 @@ const HomeHeaderTopPanel: FC = memo(() => { ) }) -const BlockList: FC<{ blocks: Block[] }> = memo(({ blocks }) => { +const BlockList: FC<{ + blocks: { + number: number + timestamp: number + liveCellChanges: string + reward: string + transactionsCount: number + minerHash: string + }[] +}> = memo(({ blocks }) => { return blocks.length > 0 ? ( <> {blocks.map((block, index) => ( @@ -166,22 +175,29 @@ const BlockList: FC<{ blocks: Block[] }> = memo(({ blocks }) => { ) }) -const TransactionList: FC<{ transactions: Transaction[]; tipBlockNumber: number }> = memo( - ({ transactions, tipBlockNumber }) => { - return transactions.length > 0 ? ( - <> - {transactions.map((transaction, index) => ( -
- - {transactions.length - 1 !== index &&
} -
- ))} - - ) : ( - - ) - }, -) +const TransactionList: FC<{ + transactions: { + transactionHash: string + blockNumber: string | number + blockTimestamp: string | number + capacityInvolved: string + liveCellChanges: string + }[] + tipBlockNumber: number +}> = memo(({ transactions, tipBlockNumber }) => { + return transactions.length > 0 ? ( + <> + {transactions.map((transaction, index) => ( +
+ + {transactions.length - 1 !== index &&
} +
+ ))} + + ) : ( + + ) +}) export default () => { const isMobile = useIsMobile() @@ -229,6 +245,9 @@ export default () => { () => transactionsQuery.data?.transactions.slice(0, maxDisplaysCount) ?? [], [transactionsQuery.data?.transactions], ) + const { isActivated: nodeConnectModeActivated } = useCKBNode() + const nodeLatestBlocks = useNodeLatestBlocks() + const nodeLatestTransactions = useNodeLatestTransactions() const { currentEpoch, targetEpoch } = useSingleHalving() const isHalvingHidden = !currentEpoch || @@ -242,47 +261,51 @@ export default () => { {isMainnet() && !isHalvingHidden ? : }
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- {!isXL ? ( - blockchainDataList - .slice(4) - .map((data: BlockchainData) => ) - ) : ( - <> -
- {blockchainDataList.slice(4, 6).map((data: BlockchainData) => ( - - ))} + {!nodeConnectModeActivated && ( + <> +
+
+
+ + +
+
+ +
-
-
- {blockchainDataList.slice(6).map((data: BlockchainData) => ( - - ))} +
+
+ + +
+
+ +
- - )} -
+
+
+ {!isXL ? ( + blockchainDataList + .slice(4) + .map((data: BlockchainData) => ) + ) : ( + <> +
+ {blockchainDataList.slice(4, 6).map((data: BlockchainData) => ( + + ))} +
+
+
+ {blockchainDataList.slice(6).map((data: BlockchainData) => ( + + ))} +
+ + )} +
+ + )}
@@ -290,7 +313,7 @@ export default () => { latest blocks {t('home.latest_blocks')} - + { history.push( @@ -314,7 +337,10 @@ export default () => { latest transactions {t('home.latest_transactions')} - + { history.push( diff --git a/src/pages/Tools/AddressConversion/AddressToScript.tsx b/src/pages/Tools/AddressConversion/AddressToScript.tsx index c144b7966..98126a68f 100644 --- a/src/pages/Tools/AddressConversion/AddressToScript.tsx +++ b/src/pages/Tools/AddressConversion/AddressToScript.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import React, { useMemo, useState } from 'react' import type { Script } from '@ckb-lumos/base' import { useTranslation } from 'react-i18next' diff --git a/src/pages/Tools/AddressConversion/parseMultiVersionAddress.ts b/src/pages/Tools/AddressConversion/parseMultiVersionAddress.ts index 1676500cb..223a34dd9 100644 --- a/src/pages/Tools/AddressConversion/parseMultiVersionAddress.ts +++ b/src/pages/Tools/AddressConversion/parseMultiVersionAddress.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import { Script } from '@ckb-lumos/base' import { helpers, type Config } from '@ckb-lumos/config-manager' import { encodeToAddress } from '@ckb-lumos/helpers' diff --git a/src/pages/Tools/BroadcastTx/index.tsx b/src/pages/Tools/BroadcastTx/index.tsx index 2a563e81c..e8f378fda 100644 --- a/src/pages/Tools/BroadcastTx/index.tsx +++ b/src/pages/Tools/BroadcastTx/index.tsx @@ -4,13 +4,14 @@ import formatter from '@nervosnetwork/ckb-sdk-rpc/lib/paramsFormatter' import ToolsContainer from '../ToolsContainer' import CopyableText from '../../../components/CopyableText' import styles from './style.module.scss' -import { sendTransaction } from '../../../services/NodeService' +import { useCKBNode } from '../../../hooks/useCKBNode' const BroadcastTx: FC = () => { const [isLoading, setIsLoading] = useState(false) const [result, setResult] = useState>>({}) const { t } = useTranslation() + const { nodeService } = useCKBNode() const handleSubmit = async (e: React.FormEvent) => { e.stopPropagation() @@ -30,7 +31,7 @@ const BroadcastTx: FC = () => { // should be converted to snake_case tx = formatter.toRawTransaction(tx) } - const r = await sendTransaction(tx) + const r = await nodeService.sendTransaction(tx) if (r.error) { throw new Error(r.error.message) } diff --git a/src/pages/Tools/MoleculeParser/DataInput.tsx b/src/pages/Tools/MoleculeParser/DataInput.tsx index 71ee889d0..0f2168c22 100644 --- a/src/pages/Tools/MoleculeParser/DataInput.tsx +++ b/src/pages/Tools/MoleculeParser/DataInput.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import React, { useState } from 'react' import { BytesCodec } from '@ckb-lumos/codec/lib/base' import { JSONTree } from 'react-json-tree' diff --git a/src/pages/Tools/MoleculeParser/Molecule.tsx b/src/pages/Tools/MoleculeParser/Molecule.tsx index 9181d9b46..232dd1a99 100644 --- a/src/pages/Tools/MoleculeParser/Molecule.tsx +++ b/src/pages/Tools/MoleculeParser/Molecule.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import React, { useCallback, useState } from 'react' import { createParser } from '@ckb-lumos/molecule' import { HelpTip } from '../../../components/HelpTip' diff --git a/src/pages/Tools/MoleculeParser/SchemaSelect.tsx b/src/pages/Tools/MoleculeParser/SchemaSelect.tsx index 4de6d1550..8182a28bb 100644 --- a/src/pages/Tools/MoleculeParser/SchemaSelect.tsx +++ b/src/pages/Tools/MoleculeParser/SchemaSelect.tsx @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import React from 'react' import { CodecMap } from '@ckb-lumos/molecule' import CommonSelect from '../../../components/CommonSelect' diff --git a/src/pages/Tools/MoleculeParser/constants.ts b/src/pages/Tools/MoleculeParser/constants.ts index d643ce57a..3203a12a3 100644 --- a/src/pages/Tools/MoleculeParser/constants.ts +++ b/src/pages/Tools/MoleculeParser/constants.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/no-extraneous-dependencies */ import type { CodecMap } from '@ckb-lumos/molecule' import { BI, BIish } from '@ckb-lumos/bi' import { number, AnyCodec } from '@ckb-lumos/codec' diff --git a/src/pages/Tools/MoleculeParser/index.tsx b/src/pages/Tools/MoleculeParser/index.tsx index c6d67e033..095c2b214 100644 --- a/src/pages/Tools/MoleculeParser/index.tsx +++ b/src/pages/Tools/MoleculeParser/index.tsx @@ -1,5 +1,3 @@ -/* eslint-disable no-console */ -/* eslint-disable import/no-extraneous-dependencies */ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { CodecMap, createParser } from '@ckb-lumos/molecule' diff --git a/src/pages/Transaction/TransactionCell/NodeTransactionCell.tsx b/src/pages/Transaction/TransactionCell/NodeTransactionCell.tsx new file mode 100644 index 000000000..f544b272c --- /dev/null +++ b/src/pages/Transaction/TransactionCell/NodeTransactionCell.tsx @@ -0,0 +1,288 @@ +import { useState, ReactNode } from 'react' +import { Tooltip } from 'antd' +import type { Cell } from '@ckb-lumos/base' +import { useTranslation } from 'react-i18next' +import { useQuery } from '@tanstack/react-query' +import { Link } from '../../../components/Link' +import { IOType } from '../../../constants/common' +import { parseUDTAmount } from '../../../utils/number' +import { shannonToCkb, shannonToCkbDecimal } from '../../../utils/util' +import { + TransactionCellContentPanel, + TransactionCellDetailPanel, + TransactionCellHashPanel, + TransactionCellPanel, + TransactionCellDetailModal, + TransactionCellCardPanel, + TransactionCellCardSeparate, + TransactionCellAddressPanel, + TransactionCellInfoPanel, + TransactionCellMobileItem, +} from './styled' +import { LeftArrow } from '../../../components/Transaction/TransactionCellArrow' +import Capacity from '../../../components/Capacity' +import NervosDAODepositIcon from '../../../assets/nervos_dao_cell.png' +import NervosDAOWithdrawingIcon from '../../../assets/nervos_dao_withdrawing.png' +import UDTTokenIcon from '../../../assets/udt_token.png' +import NFTIssuerIcon from './m_nft_issuer.svg' +import NFTClassIcon from './m_nft_class.svg' +import NFTTokenIcon from './m_nft.svg' +import CoTACellIcon from './cota_cell.svg' +import CoTARegCellIcon from './cota_reg_cell.svg' +import SporeCellIcon from './spore.svg' +import { CellInfoModal } from '../TransactionCellScript' +import SimpleModal from '../../../components/Modal' +import SimpleButton from '../../../components/SimpleButton' +import { useIsMobile } from '../../../hooks' +import { explorerService } from '../../../services/ExplorerService' +import { UDT_CELL_TYPES, getCellType, calculateScriptHash, getUDTAmountByData } from '../../../utils/cell' +import { encodeNewAddress, encodeDeprecatedAddress } from '../../../utils/address' +import { Addr } from './index' + +const TransactionCellIndexAddress = ({ + cell, + ioType, + index, + isAddrNew, +}: { + cell: Cell + ioType: IOType + index: number + isAddrNew: boolean +}) => { + const deprecatedAddr = encodeDeprecatedAddress(cell.cellOutput.lock) + const newAddr = encodeNewAddress(cell.cellOutput.lock) + const address = isAddrNew ? newAddr : deprecatedAddr + + return ( + +
+
{`#${index}`}
+
+ + {ioType === IOType.Input && cell.outPoint && ( + + + + )} + + +
+ ) +} + +export const TransactionCellDetail = ({ cell }: { cell: Cell }) => { + const { t } = useTranslation() + const cellType = getCellType(cell) + let detailTitle = t('transaction.ckb_capacity') + let detailIcon + let tooltip: string | ReactNode = '' + switch (cellType) { + case 'nervos_dao_deposit': + detailTitle = t('transaction.nervos_dao_deposit') + detailIcon = NervosDAODepositIcon + break + case 'nervos_dao_withdrawing': + detailTitle = t('transaction.nervos_dao_withdraw') + detailIcon = NervosDAOWithdrawingIcon + break + case 'udt': + detailTitle = t('transaction.udt_cell') + detailIcon = UDTTokenIcon + tooltip = `Capacity: ${shannonToCkbDecimal(cell.cellOutput.capacity, 8)} CKB` + break + case 'm_nft_issuer': + detailTitle = t('transaction.m_nft_issuer') + detailIcon = NFTIssuerIcon + break + case 'm_nft_class': + detailTitle = t('transaction.m_nft_class') + detailIcon = NFTClassIcon + break + case 'm_nft_token': + detailTitle = t('transaction.m_nft_token') + detailIcon = NFTTokenIcon + break + case 'nrc_721_token': + detailTitle = t('transaction.nrc_721_token') + detailIcon = NFTTokenIcon + break + case 'cota_registry': { + detailTitle = t('transaction.cota_registry') + detailIcon = CoTARegCellIcon + tooltip = detailTitle + break + } + case 'cota_regular': { + detailTitle = t('transaction.cota') + detailIcon = CoTACellIcon + tooltip = detailTitle + break + } + case 'spore_cluster': { + detailTitle = t('nft.dob') + detailIcon = SporeCellIcon + tooltip = t('transaction.spore_cluster') + break + } + case 'spore_cell': { + detailTitle = t('nft.dob') + detailIcon = SporeCellIcon + tooltip = t('transaction.spore') + break + } + case 'omiga_inscription': { + detailTitle = 'xUDT' + detailIcon = UDTTokenIcon + tooltip = detailTitle + break + } + case 'xudt': { + detailTitle = 'xUDT' + detailIcon = UDTTokenIcon + tooltip = detailTitle + break + } + default: + break + } + + return ( + +
+ {tooltip ? ( + + cell detail + + ) : ( + detailIcon && cell detail + )} +
{detailTitle}
+
+
+ ) +} + +const TransactionCellInfo = ({ + cell, + children, + isDefaultStyle = true, +}: { + cell: Cell + children: string | ReactNode + isDefaultStyle?: boolean +}) => { + const [showModal, setShowModal] = useState(false) + return ( + + { + setShowModal(true) + }} + > +
{children}
+
+ + + + + setShowModal(false)} /> + + + + ) +} + +const TransactionCellCapacityAmount = ({ cell }: { cell: Cell }) => { + const { t } = useTranslation() + const cellType = getCellType(cell) + const isUDTCell = UDT_CELL_TYPES.findIndex(type => type === cellType) !== -1 + const udtTypeHash = isUDTCell ? calculateScriptHash(cell.cellOutput.type!) : undefined + const udtInfo = useQuery( + ['udt', udtTypeHash], + () => { + if (!udtTypeHash) return undefined + return explorerService.api.fetchSimpleUDT(udtTypeHash) + }, + { + enabled: isUDTCell, + staleTime: Infinity, + }, + ) + + if (isUDTCell && udtTypeHash && udtInfo.data) { + const amount = getUDTAmountByData(cell.data) + if (cellType === 'udt' && udtInfo.data.published) { + return {`${parseUDTAmount(amount, udtInfo.data.decimal)} ${udtInfo.data.symbol}`} + } + + if (cellType === 'xudt' && udtInfo.data.decimal && udtInfo.data.symbol) { + return {`${parseUDTAmount(amount, udtInfo.data.decimal)} ${udtInfo.data.symbol}`} + } + + return {`${t('udt.unknown_token')} #${udtTypeHash.substring(udtTypeHash.length - 4)}`} + } + + return +} + +export default ({ + cell, + index, + ioType, + isAddrNew, +}: { + cell: Cell + index: number + ioType: IOType + isAddrNew: boolean +}) => { + const isMobile = useIsMobile() + const { t } = useTranslation() + + if (isMobile) { + return ( + + + } + /> + + + + } + /> + } + /> + + ) + } + + return ( + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ Cell Info +
+
+
+ ) +} diff --git a/src/pages/Transaction/TransactionCell/index.tsx b/src/pages/Transaction/TransactionCell/index.tsx index 0fb8cca17..1e28fd8c7 100644 --- a/src/pages/Transaction/TransactionCell/index.tsx +++ b/src/pages/Transaction/TransactionCell/index.tsx @@ -2,7 +2,7 @@ import { useState, ReactNode, FC } from 'react' import { Tooltip } from 'antd' import { useTranslation } from 'react-i18next' import { Link } from '../../../components/Link' -import { CellType } from '../../../constants/common' +import { IOType } from '../../../constants/common' import { parseUDTAmount } from '../../../utils/number' import { parseSimpleDate } from '../../../utils/date' import { sliceNftName } from '../../../utils/string' @@ -16,8 +16,9 @@ import { TransactionCellCardPanel, TransactionCellAddressPanel, TransactionCellInfoPanel, - TransactionCellCardContent, + TransactionCellMobileItem, TransactionCellNftInfo, + TransactionCellCardSeparate, } from './styled' import TransactionCellArrow from '../../../components/Transaction/TransactionCellArrow' import Capacity from '../../../components/Capacity' @@ -84,19 +85,19 @@ export const Addr: FC<{ address: string; isCellBase: boolean }> = ({ address, is const TransactionCellIndexAddress = ({ cell, - cellType, + ioType, index, isAddrNew, }: { cell: Cell - cellType: CellType + ioType: IOType index: number isAddrNew: boolean }) => { const { t } = useTranslation() const deprecatedAddr = useDeprecatedAddr(cell.addressHash)! const newAddr = useNewAddr(cell.addressHash) - const address = !isAddrNew ? deprecatedAddr : newAddr + const address = isAddrNew ? newAddr : deprecatedAddr let since try { @@ -119,13 +120,13 @@ const TransactionCellIndexAddress = ({
{`#${index}`}
- {!cell.fromCellbase && cellType === CellType.Input && ( + {!cell.fromCellbase && ioType === IOType.Input && ( - + )} - {cellType === CellType.Output && } + {ioType === IOType.Output && } {since ? ( { return } -const TransactionCellMobileItem = ({ title, value = null }: { title: string | ReactNode; value?: ReactNode }) => ( - -
{title}
-
{value}
-
-) - export default ({ cell, - cellType, + ioType, index, txHash, showReward, isAddrNew, }: { cell: Cell - cellType: CellType + ioType: IOType index: number txHash?: string showReward?: boolean @@ -355,21 +349,29 @@ export default ({ const isMobile = useIsMobile() const { t } = useTranslation() + const cellbaseReward = (() => { + if (!showReward) { + return null + } + + return + })() + if (isMobile) { return ( -
+ + cell.fromCellbase && ioType === IOType.Input ? ( + ) : ( - + ) } /> - {cell.fromCellbase && cellType === CellType.Input ? ( - + {cell.fromCellbase && showReward && ioType === IOType.Input ? ( + cellbaseReward ) : ( <> +
- {cell.fromCellbase && cellType === CellType.Input ? ( - + {cell.fromCellbase && ioType === IOType.Input ? ( + ) : ( - + )}
- {cell.fromCellbase && cellType === CellType.Input ? ( - - ) : ( - - )} + {cell.fromCellbase && ioType === IOType.Input ? cellbaseReward : }
diff --git a/src/pages/Transaction/TransactionCell/styled.tsx b/src/pages/Transaction/TransactionCell/styled.tsx index 468ad7052..9183bd16e 100644 --- a/src/pages/Transaction/TransactionCell/styled.tsx +++ b/src/pages/Transaction/TransactionCell/styled.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from 'react' import styled from 'styled-components' import variables from '../../../styles/variables.module.scss' @@ -182,15 +183,15 @@ export const TransactionCellDetailModal = styled.div` } ` -export const TransactionCellCardPanel = styled.div` - .transactionCellCardSeparate { - width: 100%; - height: 1px; - background: #ccc; - margin: 8px 0; - transform: ${() => `scaleY(${Math.ceil((1.0 / window.devicePixelRatio) * 10.0) / 10.0})`}; - } +export const TransactionCellCardSeparate = styled.div` + width: 100%; + height: 1px; + background: #ccc; + margin: 8px 0; + transform: ${() => `scaleY(${Math.ceil((1.0 / window.devicePixelRatio) * 10.0) / 10.0})`}; +` +export const TransactionCellCardPanel = styled.div` > div:nth-child(2) { margin-bottom: 15px; } @@ -228,3 +229,16 @@ export const TransactionCellCardContent = styled.div` color: ${props => props.theme.primary}; } ` + +export const TransactionCellMobileItem = ({ + title, + value = null, +}: { + title: string | ReactNode + value?: ReactNode +}) => ( + +
{title}
+
{value}
+
+) diff --git a/src/pages/Transaction/TransactionCellList/NodeTransactionCellBase.tsx b/src/pages/Transaction/TransactionCellList/NodeTransactionCellBase.tsx new file mode 100644 index 000000000..a437342cc --- /dev/null +++ b/src/pages/Transaction/TransactionCellList/NodeTransactionCellBase.tsx @@ -0,0 +1,113 @@ +import { Tooltip } from 'antd' +import { useQuery } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { + TransactionCellCardPanel, + TransactionCellMobileItem, + TransactionCellPanel, + TransactionCellContentPanel, +} from '../TransactionCell/styled' +import { ReactComponent as DeprecatedAddrOn } from './deprecated_addr_on.svg' +import { ReactComponent as DeprecatedAddrOff } from './deprecated_addr_off.svg' +import { ReactComponent as Warning } from './warning.svg' +import styles from './styles.module.scss' +import { useIsDeprecatedAddressesDisplayed } from './useIsDeprecatedAddressesDisplayed' +import { TransactionCellList } from './TransactionCellList' +import { useCKBNode } from '../../../hooks/useCKBNode' +import { useIsMobile } from '../../../hooks' +import Cellbase from '../../../components/Transaction/Cellbase' +import TransactionReward from '../TransactionReward' +import Loading from '../../../components/Loading' + +export default ({ blockHash, blockNumber }: { blockHash?: string; blockNumber?: string }) => { + const { t } = useTranslation() + const { nodeService } = useCKBNode() + const isMobile = useIsMobile() + const { data: blockEconomic, isLoading } = useQuery( + ['block', 'economic', blockHash], + () => (blockHash ? nodeService.rpc.getBlockEconomicState(blockHash) : null), + { + enabled: Boolean(blockHash), + }, + ) + + const [isDeprecatedAddressesDisplayed, addrFormatToggleURL] = useIsDeprecatedAddressesDisplayed() + + const cellTitle = (() => { + return ( +
+ {`${t('transaction.input')} (1)`} + + + {!isDeprecatedAddressesDisplayed ? : } + + + {!isDeprecatedAddressesDisplayed ? null : ( + + + + )} +
+ ) + })() + + const cellbaseContent = (() => { + if (isLoading) { + return + } + + if (!blockEconomic || !blockNumber || !blockHash || parseInt(blockNumber, 16) < 12) { + return ( + + } /> + + ) + } + + const reward = { + baseReward: blockEconomic.minerReward.primary, + secondaryReward: blockEconomic.minerReward.secondary, + commitReward: blockEconomic.minerReward.committed, + proposalReward: blockEconomic.minerReward.proposal, + } + + if (isMobile) { + return ( + + } + /> + + + ) + } + + return ( + + +
+ +
+ +
+ +
+
+
+ ) + })() + + return ( + +
{t('transaction.reward_info')}
+ + } + > + {cellbaseContent} +
+ ) +} diff --git a/src/pages/Transaction/TransactionCellList/NodeTransactionCellList.tsx b/src/pages/Transaction/TransactionCellList/NodeTransactionCellList.tsx new file mode 100644 index 000000000..e9ca3006e --- /dev/null +++ b/src/pages/Transaction/TransactionCellList/NodeTransactionCellList.tsx @@ -0,0 +1,63 @@ +import { Tooltip } from 'antd' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import type { Cell } from '@ckb-lumos/base' +import { IOType } from '../../../constants/common' +import NodeTransactionCell from '../TransactionCell/NodeTransactionCell' +import { ReactComponent as DeprecatedAddrOn } from './deprecated_addr_on.svg' +import { ReactComponent as DeprecatedAddrOff } from './deprecated_addr_off.svg' +import { ReactComponent as Warning } from './warning.svg' +import { TransactionCellList } from './TransactionCellList' +import styles from './styles.module.scss' +import { useIsDeprecatedAddressesDisplayed } from './useIsDeprecatedAddressesDisplayed' + +export default ({ cells = [], ioType }: { cells?: Cell[]; ioType: IOType }) => { + const { t } = useTranslation() + + const [isDeprecatedAddressesDisplayed, addrFormatToggleURL] = useIsDeprecatedAddressesDisplayed() + + const cellTitle = (() => { + const title = ioType === IOType.Input ? t('transaction.input') : t('transaction.output') + return ( +
+ {`${title} (${cells.length ?? '-'})`} + + + {!isDeprecatedAddressesDisplayed ? : } + + + {!isDeprecatedAddressesDisplayed ? null : ( + + + + )} +
+ ) + })() + + return ( + +
{t('transaction.detail')}
+
{t('transaction.capacity_amount')}
+ + } + > + {!cells.length ? ( +
{t('transaction.data-being-processed')}
+ ) : ( + cells?.map((cell, index) => ( + + )) + )} +
+ ) +} diff --git a/src/pages/Transaction/TransactionCellList/TransactionCellList.tsx b/src/pages/Transaction/TransactionCellList/TransactionCellList.tsx new file mode 100644 index 000000000..7b169aaac --- /dev/null +++ b/src/pages/Transaction/TransactionCellList/TransactionCellList.tsx @@ -0,0 +1,23 @@ +import { FC, PropsWithChildren } from 'react' +import { TransactionCellListPanel, TransactionCellListTitlePanel } from './styled' +import { useIsMobile } from '../../../hooks' + +export const TransactionCellList: FC< + PropsWithChildren<{ + title: React.ReactNode + extra?: React.ReactNode + }> +> = ({ title, extra, children }) => { + const isMobile = useIsMobile() + return ( + + +
+ {title} + {!isMobile ? extra : null} +
+
+ {children} +
+ ) +} diff --git a/src/pages/Transaction/TransactionCellList/index.tsx b/src/pages/Transaction/TransactionCellList/index.tsx index f237135ed..ccdc74869 100644 --- a/src/pages/Transaction/TransactionCellList/index.tsx +++ b/src/pages/Transaction/TransactionCellList/index.tsx @@ -1,32 +1,15 @@ import { Tooltip } from 'antd' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' -import { CellType } from '../../../constants/common' +import { IOType } from '../../../constants/common' import TransactionCell from '../TransactionCell' -import { TransactionCellListPanel, TransactionCellListTitlePanel, TransactionCellsPanel } from './styled' import { ReactComponent as DeprecatedAddrOn } from './deprecated_addr_on.svg' import { ReactComponent as DeprecatedAddrOff } from './deprecated_addr_off.svg' import { ReactComponent as Warning } from './warning.svg' import styles from './styles.module.scss' import { Cell } from '../../../models/Cell' -import { useSearchParams, useUpdateSearchParams } from '../../../hooks' - -function useIsDeprecatedAddressesDisplayed() { - const { addr_format } = useSearchParams('addr_format') - const updateSearchParams = useUpdateSearchParams<'addr_format'>() - - const isDeprecatedAddressesDisplayed = addr_format === 'deprecated' - const addrFormatToggleURL = updateSearchParams( - params => ({ - ...params, - addr_format: isDeprecatedAddressesDisplayed ? null : 'deprecated', - }), - false, - false, - ) - - return [isDeprecatedAddressesDisplayed, addrFormatToggleURL] as const -} +import { useIsDeprecatedAddressesDisplayed } from './useIsDeprecatedAddressesDisplayed' +import { TransactionCellList } from './TransactionCellList' export default ({ total, @@ -49,7 +32,7 @@ export default ({ const [isDeprecatedAddressesDisplayed, addrFormatToggleURL] = useIsDeprecatedAddressesDisplayed() - const cellTitle = () => { + const cellTitle = (() => { const title = inputs ? t('transaction.input') : t('transaction.output') return (
@@ -66,47 +49,33 @@ export default ({ )}
) - } - if (!cells.length) { - return ( - - -
-
{cellTitle()}
-
{isCellbaseInput ? t('transaction.reward_info') : t('transaction.detail')}
-
{isCellbaseInput ? '' : t('transaction.capacity_amount')}
-
-
-
{t('transaction.data-being-processed')}
-
- ) - } + })() return ( - - -
-
{cellTitle()}
+
{isCellbaseInput ? t('transaction.reward_info') : t('transaction.detail')}
{isCellbaseInput ? '' : t('transaction.capacity_amount')}
-
-
- -
{cellTitle()}
-
- {cells?.map((cell, index) => ( - - ))} -
-
-
+ + } + > + {!cells.length ? ( +
{t('transaction.data-being-processed')}
+ ) : ( + cells?.map((cell, index) => ( + + )) + )} + ) } diff --git a/src/pages/Transaction/TransactionCellList/styled.tsx b/src/pages/Transaction/TransactionCellList/styled.tsx index 209539c34..1199f5730 100644 --- a/src/pages/Transaction/TransactionCellList/styled.tsx +++ b/src/pages/Transaction/TransactionCellList/styled.tsx @@ -6,7 +6,10 @@ export const TransactionCellListTitlePanel = styled.div` flex-direction: column; @media (max-width: ${variables.mobileBreakPoint}) { - display: none; + color: #000; + font-weight: 600; + font-size: 20px; + margin-left: 10px; } .transactionCellListTitles { @@ -49,6 +52,10 @@ export const TransactionCellListTitlePanel = styled.div` height: 1px; margin-top: 20px; transform: ${() => `scaleY(${Math.ceil((1.0 / window.devicePixelRatio) * 10.0) / 10.0})`}; + + @media (max-width: ${variables.mobileBreakPoint}) { + display: none; + } } ` @@ -63,16 +70,3 @@ export const TransactionCellListPanel = styled.div` padding: 12px 10px 3px; } ` - -export const TransactionCellsPanel = styled.div` - .transactionCellTitle { - color: #000; - font-weight: 600; - font-size: 20px; - margin-left: 10px; - - @media (min-width: ${variables.mobileBreakPoint}) { - display: none; - } - } -` diff --git a/src/pages/Transaction/TransactionCellList/useIsDeprecatedAddressesDisplayed.tsx b/src/pages/Transaction/TransactionCellList/useIsDeprecatedAddressesDisplayed.tsx new file mode 100644 index 000000000..6e077193f --- /dev/null +++ b/src/pages/Transaction/TransactionCellList/useIsDeprecatedAddressesDisplayed.tsx @@ -0,0 +1,18 @@ +import { useSearchParams, useUpdateSearchParams } from '../../../hooks' + +export function useIsDeprecatedAddressesDisplayed() { + const { addr_format } = useSearchParams('addr_format') + const updateSearchParams = useUpdateSearchParams<'addr_format'>() + + const isDeprecatedAddressesDisplayed = addr_format === 'deprecated' + const addrFormatToggleURL = updateSearchParams( + params => ({ + ...params, + addr_format: isDeprecatedAddressesDisplayed ? null : 'deprecated', + }), + false, + false, + ) + + return [isDeprecatedAddressesDisplayed, addrFormatToggleURL] as const +} diff --git a/src/pages/Transaction/TransactionCellScript/index.tsx b/src/pages/Transaction/TransactionCellScript/index.tsx index a77d4d3ba..601dd9f49 100644 --- a/src/pages/Transaction/TransactionCellScript/index.tsx +++ b/src/pages/Transaction/TransactionCellScript/index.tsx @@ -2,6 +2,7 @@ import { useState, ReactNode, useRef, FC } from 'react' import BigNumber from 'bignumber.js' import { useTranslation } from 'react-i18next' +import type { Cell } from '@ckb-lumos/base' import { useQuery } from '@tanstack/react-query' import { scriptToHash } from '@nervosnetwork/ckb-sdk-utils' import { explorerService } from '../../../services/ExplorerService' @@ -20,6 +21,7 @@ import CloseIcon from './modal_close.png' import config from '../../../config' import { getBtcTimeLockInfo, getBtcUtxo, getContractHashTag } from '../../../utils/util' import { localeNumberString } from '../../../utils/number' +import { cellOccupied } from '../../../utils/cell' import HashTag from '../../../components/HashTag' import { ReactComponent as CopyIcon } from '../../../assets/copy_icon.svg' import { ReactComponent as OuterLinkIcon } from './outer_link_icon.svg' @@ -270,6 +272,163 @@ const CellInfoValueJSONView = ({ content, state }: { content: CellInfoValue; sta ) +export const CellInfoModal = ({ cell, onClose }: { cell: Cell; onClose: Function }) => { + const setToast = useSetToast() + const { t } = useTranslation() + const [selectedInfo, setSelectedInfo] = useState(CellInfo.LOCK) + + const content = ((): CellInfoValue => { + if (selectedInfo === CellInfo.LOCK) { + return cell.cellOutput.lock + } + + if (selectedInfo === CellInfo.TYPE) { + return cell.cellOutput.type + } + + if (selectedInfo === CellInfo.DATA) { + return { + data: cell.data, + } + } + + if (selectedInfo === CellInfo.CAPACITY) { + const declared = new BigNumber(cell.cellOutput.capacity) + const occupied = new BigNumber(cellOccupied(cell)) + + return { + declared: `${localeNumberString(declared.dividedBy(10 ** 8))} CKBytes`, + occupied: `${localeNumberString(occupied.dividedBy(10 ** 8))} CKBytes`, + } + } + + return null + })() + + const ref = useRef(null) + + const changeType = (newState: CellInfo) => { + setSelectedInfo(selectedInfo !== newState ? newState : selectedInfo) + } + + const onCopy = (e: React.SyntheticEvent) => { + const { role } = e.currentTarget.dataset + + let v = '' + + switch (role) { + case 'copy-script': { + v = getContentJSONWithSnakeCase(content) + break + } + case 'copy-script-hash': { + if (isScript(content)) { + v = scriptToHash(content as CKBComponents.Script) + } + break + } + default: { + // ignore + } + } + if (!v) return + + navigator.clipboard.writeText(v).then( + () => setToast({ message: t('common.copied') }), + error => { + console.error(error) + }, + ) + } + + return ( + + + + close icon {}} onClick={() => onClose()} /> +
+ } + tabBarStyle={{ fontSize: '10px' }} + onTabClick={key => { + const state = parseInt(key, 10) + if (state && !Number.isNaN(state)) { + changeType(state) + } + }} + > + + {t('transaction.lock_script')} + + + } + key={CellInfo.LOCK} + /> + + {t('transaction.type_script')} + + + } + key={CellInfo.TYPE} + /> + {t('transaction.data')}} + key={CellInfo.DATA} + /> + + {t('transaction.capacity_usage')} + + + } + key={CellInfo.CAPACITY} + /> + + + + +
+ +
+ {!content ? null : ( +
+ + + {isScript(content) ? ( + + ) : null} + + {isScript(content) ? ( + +
{`${t('scripts.script')} Info`}
+ +
+ ) : null} +
+ )} +
+ + ) +} + export default ({ cell, onClose }: TransactionCellScriptProps) => { const setToast = useSetToast() const { t } = useTranslation() diff --git a/src/pages/Transaction/TransactionComp/NodeTransactionComp.tsx b/src/pages/Transaction/TransactionComp/NodeTransactionComp.tsx new file mode 100644 index 000000000..b7d1e1dd2 --- /dev/null +++ b/src/pages/Transaction/TransactionComp/NodeTransactionComp.tsx @@ -0,0 +1,49 @@ +import { Transaction } from '@ckb-lumos/base' +import { useQuery } from '@tanstack/react-query' +import NodeTransactionCellList from '../TransactionCellList/NodeTransactionCellList' +import NodeTransactionCellBase from '../TransactionCellList/NodeTransactionCellBase' +import { useCKBNode } from '../../../hooks/useCKBNode' +import { checkIsCellBase, getTransactionOutputCells } from '../../../utils/transaction' +import Loading from '../../../components/Loading' +import { IOType } from '../../../constants/common' + +export const NodeTransactionComp = ({ + transaction, + blockNumber, +}: { + transaction: Transaction + blockNumber?: string +}) => { + const isCellBase = checkIsCellBase(transaction) + const { nodeService } = useCKBNode() + const { data: cellBaseBlockHeader } = useQuery( + ['node', 'block', blockNumber ? parseInt(blockNumber, 16) - 11 : null], + () => (blockNumber ? nodeService.rpc.getBlockByNumber((parseInt(blockNumber, 16) - 11).toString(16)) : null), + { + enabled: isCellBase, + }, + ) + + const { data: inputCells, isFetching: isInputsLoading } = useQuery(['node', 'inputCells', transaction?.hash], () => + nodeService.getInputCells(transaction.inputs), + ) + const outputCells = getTransactionOutputCells(transaction) + + return ( + <> +
+ {isCellBase ? ( + + ) : ( + <> + + + + )} +
+
+ +
+ + ) +} diff --git a/src/pages/Transaction/TransactionComp/NodeTransactionOverview.tsx b/src/pages/Transaction/TransactionComp/NodeTransactionOverview.tsx new file mode 100644 index 000000000..b555b2e9b --- /dev/null +++ b/src/pages/Transaction/TransactionComp/NodeTransactionOverview.tsx @@ -0,0 +1,248 @@ +import { useState, FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Tooltip } from 'antd' +import { useQuery } from '@tanstack/react-query' +import { ResultFormatter } from '@ckb-lumos/rpc' +import { Link } from '../../../components/Link' +import Capacity from '../../../components/Capacity' +import SimpleButton from '../../../components/SimpleButton' +import { parseSimpleDate } from '../../../utils/date' +import { localeNumberString } from '../../../utils/number' +import { shannonToCkb, useFormatConfirmation } from '../../../utils/util' +import { + getTransactionOutputCells, + calculateFeeByTxIO, + checkIsCellBase, + calculateTransactionSize, +} from '../../../utils/transaction' +import { TransactionBlockHeightPanel, TransactionOverviewPanel } from './styled' +import { useLatestBlockNumber } from '../../../services/ExplorerService' +import { NodeRpc } from '../../../services/NodeService' +import { Card, CardCellInfo, CardCellsLayout, HashCardHeader } from '../../../components/Card' +import RawTransactionView from '../../../components/RawTransactionView' +import { ReactComponent as DownloadIcon } from './download.svg' +import { useIsMobile } from '../../../hooks' +import { useCKBNode } from '../../../hooks/useCKBNode' +import styles from './TransactionOverview.module.scss' +import TransactionParameters from '../../../components/TransactionParameters' + +const showTxStatus = (txStatus: string) => txStatus?.replace(/^\S/, s => s.toUpperCase()) ?? '-' +const TransactionBlockHeight = ({ txStatus }: { txStatus: NodeRpc.TransactionWithStatus['tx_status'] }) => ( + + {txStatus.status === 'committed' ? ( + {localeNumberString(txStatus.block_number)} + ) : ( + {showTxStatus(txStatus.status)} + )} + +) + +export const NodeTransactionOverviewCard: FC<{ + transactionWithStatus: NodeRpc.TransactionWithStatus +}> = ({ transactionWithStatus }) => { + const { transaction: rawTransaction, tx_status: txStatus, cycles } = transactionWithStatus + const { nodeService } = useCKBNode() + const { t } = useTranslation() + const isMobile = useIsMobile() + const tipBlockNumber = useLatestBlockNumber() + const [detailTab, setDetailTab] = useState<'params' | 'raw' | null>(null) + + const header = useQuery( + ['node', 'header', txStatus.status === 'committed' ? txStatus.block_number : null], + () => (txStatus.status === 'committed' ? nodeService.rpc.getHeaderByNumber(txStatus.block_number) : null), + { + staleTime: Infinity, + enabled: txStatus.status === 'committed', + }, + ) + + const inputCells = useQuery( + ['node', 'inputCells', rawTransaction?.hash], + () => (rawTransaction ? nodeService.getInputCells(ResultFormatter.toTransaction(rawTransaction).inputs) : []), + { + enabled: Boolean(rawTransaction), + }, + ) + const confirmation = (() => { + if (tipBlockNumber && txStatus.status === 'committed' && txStatus.block_number) { + return Number(tipBlockNumber) - Number(txStatus.block_number) + } + + return 0 + })() + const formatConfirmation = useFormatConfirmation() + + if (!rawTransaction) { + return null + } + + const tx = ResultFormatter.toTransaction(rawTransaction) + const isCellBase = checkIsCellBase(tx) + const txSize = calculateTransactionSize(tx) + const outputCells = getTransactionOutputCells(tx) + const { fee, feeRate } = + inputCells.data && !isCellBase + ? calculateFeeByTxIO(inputCells.data, outputCells, txSize) + : { + fee: 0, + feeRate: 0, + } + + const blockHeightData: CardCellInfo = { + title: t('block.block_height'), + tooltip: t('glossary.block_height'), + content: , + className: styles.firstCardCell, + } + + const timestampData: CardCellInfo = { + title: t('block.timestamp'), + tooltip: t('glossary.timestamp'), + content: header.isLoading || !header.data ? '...' : parseSimpleDate(header.data.timestamp), + } + + const feeWithFeeRateData: CardCellInfo = { + title: `${t('transaction.transaction_fee')} | ${t('transaction.fee_rate')}`, + content: ( +
+ + {` | ${Number(feeRate).toLocaleString('en')} shannons/kB`} +
+ ), + } + + const txStatusData: CardCellInfo = { + title: t('transaction.status'), + tooltip: t('glossary.transaction_status'), + content: formatConfirmation(confirmation), + } + + const txSizeData: CardCellInfo = { + title: t('transaction.size'), + content: ( +
+ {`${(txSize - 4).toLocaleString('en')} Bytes`} +
+ ), + } + + const txCyclesData: CardCellInfo = { + title: t('transaction.cycles'), + content: ( +
+ {`${Number(cycles).toLocaleString('en')}`} +
+ ), + } + + const overviewItems: CardCellInfo<'left' | 'right'>[] = (() => { + if (txStatus.status === 'committed') { + return [ + blockHeightData, + timestampData, + ...(confirmation >= 0 ? [feeWithFeeRateData, txStatusData] : []), + txSizeData, + txCyclesData, + ] + } + + if (txStatus.status === 'rejected') { + return [ + blockHeightData, + { + ...timestampData, + content: 'Rejected', + }, + { + ...txStatusData, + content: 'Rejected', + contentTooltip: txStatus.reason, + }, + txSizeData, + txCyclesData, + ] + } + + return [ + { + ...blockHeightData, + content: '···', + }, + { + ...timestampData, + content: '···', + }, + { + ...txStatusData, + content: 'Pending', + }, + txSizeData, + txCyclesData, + ] + })() + + const handleExportTxClick = () => { + const blob = new Blob([JSON.stringify(rawTransaction, null, 2)]) + + const link = document.createElement('a') + link.download = `tx-${rawTransaction.hash}.json` + link.href = URL.createObjectURL(blob) + document.body.append(link) + link.click() + link.remove() + } + + return ( + + + + + + , + ]} + /> + + + +
+
+ setDetailTab(p => (p === 'params' ? null : 'params'))} + > +
{t('transaction.transaction_parameters')}
+
+ + setDetailTab(p => (p === 'raw' ? null : 'raw'))} + > +
{t('transaction.raw_transaction')}
+
+ +
+ {detailTab === 'params' ? : null} + {detailTab === 'raw' ? : null} +
+ + + ) +} diff --git a/src/pages/Transaction/TransactionComp/TransactionComp.tsx b/src/pages/Transaction/TransactionComp/TransactionComp.tsx index 8f4e2202c..ca17f1e80 100644 --- a/src/pages/Transaction/TransactionComp/TransactionComp.tsx +++ b/src/pages/Transaction/TransactionComp/TransactionComp.tsx @@ -53,34 +53,17 @@ export const TransactionComp = ({ const outputsPage = page_of_outputs && Number.isNaN(+page_of_outputs) ? 1 : +page_of_outputs const pageSize = Number.isNaN(+page_size) ? PAGE_SIZE : +page_size - const { data: displayInputs, isFetching: isInputsLoading } = useQuery( + const { data: displayInputs = emptyList, isFetching: isInputsLoading } = useQuery( ['transaction_inputs', txHash, inputsPage, pageSize], - async () => { - try { - const res = await explorerService.api.fetchCellsByTxHash(txHash, 'inputs', { no: inputsPage, size: pageSize }) - return res - } catch (e) { - return emptyList - } - }, - { - initialData: emptyList, - }, + () => explorerService.api.fetchCellsByTxHash(txHash, 'inputs', { no: inputsPage, size: pageSize }), ) - const { data: displayOutputs, isFetching: isOutputsLoading } = useQuery( + const { data: displayOutputs = emptyList, isFetching: isOutputsLoading } = useQuery( ['transaction_outputs', txHash, outputsPage, pageSize], - async () => { - try { - const res = await explorerService.api.fetchCellsByTxHash(txHash, 'outputs', { no: outputsPage, size: pageSize }) - return res - } catch (e) { - return emptyList - } - }, - { - initialData: emptyList, - }, + () => + explorerService.api + .fetchCellsByTxHash(txHash, 'outputs', { no: outputsPage, size: pageSize }) + .catch(() => emptyList), ) const inputs = handleCellbaseInputs(displayInputs.data, displayOutputs.data) @@ -110,7 +93,7 @@ export const TransactionComp = ({ total={displayInputs.meta?.total} inputs={inputs} startIndex={(inputsPage - 1) * pageSize} - showReward={blockNumber > 0 && isCellbase} + showReward={Number(blockNumber) - 11 > 0 && isCellbase} />
= ({ layout }) => { + showLayoutSwitcher?: boolean +}> = ({ showLayoutSwitcher = true, layout }) => { const { t } = useTranslation() const isMobile = useIsMobile() @@ -43,9 +44,9 @@ export const TransactionDetailsHeader: FC<{

{t('transaction.transaction_details')}

- {!isMobile && professionalLiteBox} + {!isMobile && showLayoutSwitcher && professionalLiteBox}
- {isMobile && professionalLiteBox} + {isMobile && showLayoutSwitcher && professionalLiteBox}
) } diff --git a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx index 967a2fa5f..771774826 100644 --- a/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx +++ b/src/pages/Transaction/TransactionComp/TransactionLite/TransactionLite.tsx @@ -15,18 +15,12 @@ export const TransactionCompLite: FC<{ isCellbase: boolean }> = ({ isCellbase }) const { hash: txHash } = useParams<{ hash: string }>() const [t] = useTranslation() - const query = useQuery( + const { data: transactionLiteDetails = defaultTransactionLiteDetails } = useQuery( ['ckb_transaction_details', txHash], - async () => { - const ckbTransactionDetails = await explorerService.api.fetchTransactionLiteDetailsByHash(txHash) - return ckbTransactionDetails.data - }, - { - initialData: defaultTransactionLiteDetails, - }, + () => explorerService.api.fetchTransactionLiteDetailsByHash(txHash).then(res => res.data), ) - const txList = query.data.map(tx => ({ + const txList = transactionLiteDetails.map(tx => ({ address: tx.address, transfers: tx.transfers.map(getTransfer), })) diff --git a/src/pages/Transaction/TransactionComp/TransactionOverview.tsx b/src/pages/Transaction/TransactionComp/TransactionOverview.tsx index 9e24d87d5..6c9e053b1 100644 --- a/src/pages/Transaction/TransactionComp/TransactionOverview.tsx +++ b/src/pages/Transaction/TransactionComp/TransactionOverview.tsx @@ -245,7 +245,7 @@ export const TransactionOverviewCard: FC<{ } /> - {(txStatus !== 'committed' || blockTimestamp > 0) && ( + {(txStatus !== 'committed' || Number(blockTimestamp) > 0) && ( {isProfessional && ( diff --git a/src/pages/Transaction/TransactionReward/index.tsx b/src/pages/Transaction/TransactionReward/index.tsx index bc48ed3a8..8adc94241 100644 --- a/src/pages/Transaction/TransactionReward/index.tsx +++ b/src/pages/Transaction/TransactionReward/index.tsx @@ -2,39 +2,43 @@ import { useTranslation } from 'react-i18next' import { shannonToCkb } from '../../../utils/util' import { RewardPenal, RewardItemPenal } from './styled' import { useIsMobile } from '../../../hooks' -import { Cell } from '../../../models/Cell' import Capacity from '../../../components/Capacity' -const useRewards = (cell: Cell, isMobile: boolean) => { +export type Reward = { + baseReward: string + secondaryReward: string + commitReward: string + proposalReward: string +} + +const useRewards = (reward: Reward, isMobile: boolean) => { const { t } = useTranslation() return [ { name: isMobile ? t('transaction.base') : t('transaction.base_reward'), - capacity: cell.baseReward, + capacity: reward.baseReward, }, { name: isMobile ? t('transaction.secondary') : t('transaction.secondary_reward'), - capacity: cell.secondaryReward, + capacity: reward.secondaryReward, }, { name: isMobile ? t('transaction.commit') : t('transaction.commit_reward'), - capacity: cell.commitReward, + capacity: reward.commitReward, }, { name: isMobile ? t('transaction.proposal') : t('transaction.proposal_reward'), - capacity: cell.proposalReward, + capacity: reward.proposalReward, }, ] } -const TransactionReward = ({ cell, showReward }: { cell: Cell; showReward?: boolean }) => { +const TransactionReward = ({ reward }: { reward: Reward }) => { const isMobile = useIsMobile() const { t } = useTranslation() - // [0, 11] block doesn't show block reward and only cellbase show block reward - const showBlockReward = showReward && cell.targetBlockNumber > 0 - const rewards = useRewards(cell, isMobile) + const rewards = useRewards(reward, isMobile) - return showBlockReward ? ( + return (
{t('transaction.reward_info')}
{rewards.map(reward => ( @@ -46,7 +50,7 @@ const TransactionReward = ({ cell, showReward }: { cell: Cell; showReward?: bool ))}
- ) : null + ) } export default TransactionReward diff --git a/src/pages/Transaction/index.tsx b/src/pages/Transaction/index.tsx index 8391d3f8c..804ea0b91 100644 --- a/src/pages/Transaction/index.tsx +++ b/src/pages/Transaction/index.tsx @@ -1,29 +1,43 @@ import { useParams } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' +import { ResultFormatter } from '@ckb-lumos/rpc' import Content from '../../components/Content' import { TransactionDiv as TransactionPanel } from './TransactionComp/styled' import { explorerService } from '../../services/ExplorerService' import { QueryResult } from '../../components/QueryResult' import { defaultTransactionInfo } from './state' import { useSearchParams } from '../../hooks' +import { useCKBNode } from '../../hooks/useCKBNode' import { LayoutLiteProfessional } from '../../constants/common' import { TransactionCompLite } from './TransactionComp/TransactionLite/TransactionLite' import { TransactionComp } from './TransactionComp/TransactionComp' +import { NodeTransactionComp } from './TransactionComp/NodeTransactionComp' import { TransactionOverviewCard } from './TransactionComp/TransactionOverview' +import { NodeTransactionOverviewCard } from './TransactionComp/NodeTransactionOverview' import { TransactionDetailsHeader } from './TransactionComp/TransactionDetailsHeader' import { RGBDigestComp } from './TransactionComp/RGBDigestComp' export default () => { const { Professional, Lite } = LayoutLiteProfessional const { hash: txHash } = useParams<{ hash: string }>() + const { nodeService, isActivated: nodeActivated } = useCKBNode() - const query = useQuery(['transaction', txHash], async () => { - const transaction = await explorerService.api.fetchTransactionByHash(txHash) - // TODO: When will displayOutputs be empty? Its type description indicates that it will not be empty. - if (transaction.displayOutputs && transaction.displayOutputs.length > 0) { - transaction.displayOutputs[0].isGenesisOutput = transaction.blockNumber === 0 - } - return transaction + const query = useQuery( + ['transaction', txHash], + async () => { + const transaction = await explorerService.api.fetchTransactionByHash(txHash) + if (transaction.displayOutputs && transaction.displayOutputs.length > 0) { + transaction.displayOutputs[0].isGenesisOutput = transaction.blockNumber === 0 + } + return transaction + }, + { + enabled: !nodeActivated, + }, + ) + + const nodeTxQuery = useQuery(['node', 'transaction', txHash], () => nodeService.getTx(txHash), { + enabled: nodeActivated, }) const transaction = query.data ?? defaultTransactionInfo @@ -33,25 +47,54 @@ export default () => { return ( - + {nodeActivated ? ( + + {nodeTx => + nodeTx ? ( + + ) : ( +
{`Transaction ${txHash} not loaded`}
+ ) + } +
+ ) : ( + + )} {transaction.isRgbTransaction && } - + - {layout === Professional ? ( - - {transaction => (transaction ? :
)} + {nodeActivated ? ( + + {nodeTx => + nodeTx && nodeTx.result.transaction ? ( + + ) : ( +
{`Transaction ${txHash} not loaded`}
+ ) + }
) : ( - - {transaction => } - + <> + {layout === Professional ? ( + + {transaction => (transaction ? :
)} + + ) : ( + + {transaction => } + + )} + )} diff --git a/src/pages/Xudt/UDTComp.tsx b/src/pages/Xudt/UDTComp.tsx index 72c345863..05f5cee16 100644 --- a/src/pages/Xudt/UDTComp.tsx +++ b/src/pages/Xudt/UDTComp.tsx @@ -219,7 +219,7 @@ export const UDTOverviewCard = ({
{xudt?.xudtTags?.map(tag => ( - + ))}
diff --git a/src/services/NodeService/index.ts b/src/services/NodeService/index.ts index c2dac8b32..634b1de77 100644 --- a/src/services/NodeService/index.ts +++ b/src/services/NodeService/index.ts @@ -1,52 +1,125 @@ import axios from 'axios' -import config from '../../config' +import { Input, Cell, OutPoint } from '@ckb-lumos/base' +import { RPC, ResultFormatter } from '@ckb-lumos/rpc' +import { outputToCell } from '../../utils/transaction' -const { BACKUP_NODES: backupNodes } = config +export class NodeService { + nodeEndpoint: string + public rpc: RPC -const node = backupNodes[0] + constructor(nodeEndpoint: string) { + this.nodeEndpoint = nodeEndpoint + this.rpc = new RPC(nodeEndpoint) + } -if (!node) { - throw new Error('NodeService not implemented') -} + async getTx(hash: string): Promise<{ + result: NodeRpc.TransactionWithStatus + }> { + const body = { + id: 1, + jsonrpc: '2.0', + method: 'get_transaction', + params: [hash], + } -export const getTx = async (hash: string): Promise<{ result: { transaction: NodeRpc.RawTransaction | null } }> => { - const body = { - id: 1, - jsonrpc: '2.0', - method: 'get_transaction', - params: [hash], + return axios + .post(this.nodeEndpoint, body) + .then(res => res.data) + .catch(() => null) } - return axios - .post(node, body) - .then(res => res.data) - .catch(() => null) -} + async sendTransaction(tx: NodeRpc.RawTransaction) { + const body = { + id: 1, + jsonrpc: '2.0', + method: 'send_transaction', + params: [tx, 'passthrough'], + } + + return axios.post(this.nodeEndpoint, body).then(res => res.data) + } + + async getCellByOutPoint(outPoint: OutPoint): Promise { + const { + result: { transaction: rawTransaction, tx_status: txStatus }, + } = await this.getTx(outPoint.txHash) + if (!rawTransaction) { + return undefined + } -export const sendTransaction = async (tx: NodeRpc.RawTransaction) => { - const body = { - id: 1, - jsonrpc: '2.0', - method: 'send_transaction', - params: [tx, 'passthrough'], + const transaction = ResultFormatter.toTransaction(rawTransaction) + const index = parseInt(outPoint.index, 16) + + const status = + txStatus.status === 'committed' + ? { + blockHash: txStatus.block_hash, + blockNumber: txStatus.block_number, + } + : {} + + const { blockHash, blockNumber } = status + + return outputToCell(transaction.outputs[index], transaction.outputsData[index], { + outPoint, + blockHash, + blockNumber, + }) } - return axios.post(node, body).then(res => res.data) + async getInputCells(inputs: Input[]): Promise { + const cells = await Promise.all(inputs.map(input => this.getCellByOutPoint(input.previousOutput))) + return cells.filter(i => i) as Cell[] + } } -namespace NodeRpc { +export namespace NodeRpc { + export enum TransactionStatus { + Pending = 'pending', + Proposed = 'proposed', + Committed = 'committed', + Unknown = 'unknown', + Rejected = 'rejected', + } + + export interface TransactionWithStatus { + time_added_to_pool: string | null + cycles: string | null + fee: string | null + min_replace_fee: string | null + transaction: NodeRpc.RawTransaction | null + tx_status: + | { + block_hash: string + block_number: string + status: NodeRpc.TransactionStatus.Committed + } + | { + block_hash: null + block_number: null + reason?: string + status: + | NodeRpc.TransactionStatus.Pending + | NodeRpc.TransactionStatus.Proposed + | NodeRpc.TransactionStatus.Unknown + | NodeRpc.TransactionStatus.Rejected + } + } + interface Script { code_hash: string args: string hash_type: 'data' | 'type' | 'data1' | 'data2' } + type DepType = 'code' | 'dep_group' + interface CellDep { out_point: { tx_hash: string index: string } - dep_type: string + dep_type: DepType } interface CellInput { @@ -60,9 +133,10 @@ namespace NodeRpc { interface CellOutput { capacity: string lock: Script - type: Script | null + type?: Script | undefined } export interface RawTransaction { + hash: string version: string cell_deps: CellDep[] header_deps: string[] diff --git a/src/utils/address.ts b/src/utils/address.ts new file mode 100644 index 000000000..8beb7f031 --- /dev/null +++ b/src/utils/address.ts @@ -0,0 +1,15 @@ +import { Script } from '@ckb-lumos/base' +import { encodeToAddress, generateAddress } from '@ckb-lumos/helpers' + +import { IS_MAINNET } from '../constants/common' +import { LUMOS_MAINNET_CONFIG, LUMOS_TESTNET_CONFIG } from '../constants/scripts' + +const lumosConfig = IS_MAINNET ? LUMOS_MAINNET_CONFIG : LUMOS_TESTNET_CONFIG + +export const encodeNewAddress = (script: Script) => { + return encodeToAddress(script, { config: lumosConfig }) +} + +export const encodeDeprecatedAddress = (script: Script) => { + return generateAddress(script, { config: lumosConfig }) +} diff --git a/src/utils/cell.ts b/src/utils/cell.ts new file mode 100644 index 000000000..a04db1ed2 --- /dev/null +++ b/src/utils/cell.ts @@ -0,0 +1,81 @@ +import type { Cell, Script } from '@ckb-lumos/base' +import { ckbHash } from '@ckb-lumos/base/lib/utils' +import { minimalCellCapacityCompatible } from '@ckb-lumos/helpers' +import { blockchain } from '@ckb-lumos/base' +import { BI } from '@ckb-lumos/bi' +import { IS_MAINNET } from '../constants/common' +import { MainnetContractHashTags, TestnetContractHashTags } from '../constants/scripts' + +const DEPOSIT_DAO_DATA = '0x0000000000000000' + +export const UDT_CELL_TYPES = ['sudt', 'xudt'] + +export function getCellType(cell: Cell): string { + const scriptSet = IS_MAINNET ? MainnetContractHashTags : TestnetContractHashTags + + const cellTypeScript = cell.cellOutput.type + + if (!cellTypeScript) { + return 'normal' + } + + const matchedScript = scriptSet.find(script => + script.codeHashes.find(codeHash => codeHash === cellTypeScript.codeHash), + ) + + if (!matchedScript) { + return 'normal' + } + + switch (matchedScript.tag) { + case 'nervos dao': + if (cell.data === DEPOSIT_DAO_DATA) { + return 'nervos_dao_deposit' + } + + return 'nervos_dao_withdrawing' + case 'sudt': + return 'udt' + case 'm-nft_issuer': + return 'm_nft_issuer' + case 'm-nft_class': + return 'm_nft_class' + case 'm-nft': + return 'm_nft_token' + case 'cota_registry': + return 'cota_registry' + case 'cota': + return 'cota_regular' + case 'Spore Cluster': + return 'spore_cluster' + case 'Spore': + return 'spore' + case 'xUDT': + return 'xudt' + case 'Unique Cell': + return 'unique_cell' + default: + break + } + + return 'normal' +} + +export function calculateScriptHash(script: Script): string { + return ckbHash(blockchain.Script.pack(script)) +} + +export const getUDTAmountByData = (data: string) => { + // to big endian + const bytes = data.slice(2).match(/\w{2}/g) + if (!bytes) return '0x00' + const be = `0x${bytes.reverse().join('')}` + if (Number.isNaN(+be)) { + throw new Error('Invalid little-endian') + } + return BI.from(be).toHexString() +} + +export const cellOccupied = (cell: Cell) => { + return minimalCellCapacityCompatible(cell).toNumber() +} diff --git a/src/utils/number.ts b/src/utils/number.ts index 4a86dff84..167cfe525 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js' +import { BI } from '@ckb-lumos/bi' export const localeNumberString = (value: BigNumber | string | number): string => { if (!value) return '0' @@ -23,6 +24,37 @@ export const localeNumberString = (value: BigNumber | string | number): string = return origin.isNegative() ? `-${text}` : text } +// reference: https://github.com/nervosnetwork/ckb/wiki/Header-%C2%BB-compact_target +function compactToTarget(compact: number) { + const exponent = BI.from(compact).shr(BI.from(24)) + let mantissa = BI.from(compact).and(BI.from(0x00ff_ffff)) + + let target = BI.from(0) + if (exponent.lte(BI.from(3))) { + mantissa = mantissa.shr(BI.from(8).mul(BI.from(3).sub(exponent))) + target = mantissa + } else { + target = mantissa + target = target.shl(BI.from(8).mul(exponent.sub(BI.from(3)))) + } + + const overflow = !mantissa.isZero() && exponent.gt(BI.from(32)) + return { target, overflow } +} + +export function compactToDifficulty(compact: number) { + const { target } = compactToTarget(compact) + + const u256MaxValue = BI.from(2).pow(256).sub(1) + const hspace = BI.from('0x10000000000000000000000000000000000000000000000000000000000000000') + + if (target.isZero()) { + return u256MaxValue.toHexString() + } + + return hspace.div(target).toHexString() +} + const MIN_VALUE = new BigNumber(1) export const handleDifficulty = (value: BigNumber | string | number) => { if (!value) return '0' diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts new file mode 100644 index 000000000..ae59de918 --- /dev/null +++ b/src/utils/transaction.ts @@ -0,0 +1,84 @@ +import type { Cell, OutPoint, Output, Transaction } from '@ckb-lumos/base' +import { common } from '@ckb-lumos/common-scripts' +import { BI } from '@ckb-lumos/bi' + +const CELLBASE_TX_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' + +export function outputToCell( + output: Output, + data: string, + { + outPoint, + blockHash, + blockNumber, + txIndex, + }: { + outPoint?: OutPoint + blockHash?: string + blockNumber?: string + txIndex?: string + }, +): Cell { + return { + cellOutput: { + capacity: output.capacity, + lock: output.lock, + type: output.type, + }, + data, + outPoint, + blockHash, + blockNumber, + txIndex, + } +} + +function calculateFeeRate(size: number, fee: BI): BI { + const ratio = BI.from(1000) + return fee.mul(ratio).div(BI.from(size)) +} + +export function calculateFeeByTxIO( + inputs: Cell[], + outputs: Cell[], + transactionSize: number, +): { + fee: number + feeRate: number +} { + const inputCapacities = inputs.reduce((sum, cell) => sum.add(BI.from(cell.cellOutput.capacity)), BI.from(0)) + const outputCapacities = outputs.reduce((sum, cell) => sum.add(BI.from(cell.cellOutput.capacity)), BI.from(0)) + const fee = inputCapacities.sub(outputCapacities) + const feeRate = calculateFeeRate(transactionSize, fee) + + return { + fee: fee.toNumber(), + feeRate: feeRate.toNumber(), + } +} + +export function getTransactionOutputCells(tx: Transaction): Cell[] { + return tx.outputs.map((output, index) => + outputToCell(output, tx.outputsData[index], { + outPoint: tx.hash + ? { + txHash: tx.hash, + index: `0x${index.toString(16)}`, + } + : undefined, + }), + ) +} + +export function calculateTransactionSize(tx: Transaction): number { + // eslint-disable-next-line no-underscore-dangle + return common.__tests__.getTransactionSizeByTx(tx) +} + +export function checkIsCellBase(tx: Transaction): boolean { + if (tx.inputs.length !== 1) { + return false + } + + return tx.inputs[0].previousOutput.txHash === CELLBASE_TX_HASH +} diff --git a/yarn.lock b/yarn.lock index 9555a78b8..7a42f3de3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2760,6 +2760,22 @@ bs58 "^5.0.0" immutable "^4.3.0" +"@ckb-lumos/common-scripts@0.23.0": + version "0.23.0" + resolved "https://registry.npmjs.org/@ckb-lumos/common-scripts/-/common-scripts-0.23.0.tgz#ccb04214ca7186829036ddcbb0dc1b5326127a90" + integrity sha512-Dwic0Al94afdGNu+TGAMmZiU5OVF/zvXbzhCvNmkFS25t8BxPdFjGEc0MlWBI4ZSEoGRrC0O+BOxjzfl5VxSYg== + dependencies: + "@ckb-lumos/base" "0.23.0" + "@ckb-lumos/bi" "0.23.0" + "@ckb-lumos/codec" "0.23.0" + "@ckb-lumos/config-manager" "0.23.0" + "@ckb-lumos/helpers" "0.23.0" + "@ckb-lumos/rpc" "0.23.0" + "@ckb-lumos/toolkit" "0.23.0" + bech32 "^2.0.0" + bs58 "^5.0.0" + immutable "^4.3.0" + "@ckb-lumos/config-manager@0.22.0-next.5": version "0.22.0-next.5" resolved "https://registry.yarnpkg.com/@ckb-lumos/config-manager/-/config-manager-0.22.0-next.5.tgz#45e10c45ca85282ae122be32269789b6a3d98a17" @@ -4069,6 +4085,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"