Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8586fdd
feat: support spore nft
homura Aug 21, 2023
918b913
feat: display cluster name in asset list
homura Aug 28, 2023
c0f3a4a
Merge remote-tracking branch 'origin/develop' into spore
homura Aug 28, 2023
f4bbc4a
chore: update lockfile
homura Aug 28, 2023
0b657e1
fix: avoid circular dep
homura Aug 30, 2023
634ffaf
fix: avoid circular dep
homura Aug 30, 2023
174c10b
Merge remote-tracking branch 'origin/develop' into spore
homura Aug 30, 2023
aab28b1
refactor: rm unused devnet config of spore
homura Sep 5, 2023
dbc5a5b
refactor: rm unused spore's unbound mark
homura Sep 5, 2023
9c56406
refactor: rm unsuitable button type
homura Sep 6, 2023
b177e73
chore: fix dep version
homura Sep 6, 2023
eb88b79
chore: reason for rpc is required when getCustomizedAsset
homura Sep 6, 2023
667bcab
test: make sporeFormatter testable
homura Sep 6, 2023
a385812
refactor: switch-case to more clear
homura Sep 6, 2023
55e7a93
Merge remote-tracking branch 'origin/develop' into spore
homura Sep 7, 2023
a1b3698
chore: update lock file
homura Sep 7, 2023
4322145
Merge remote-tracking branch 'origin/develop' into spore
homura Sep 18, 2023
b8e4952
chore: update lock file
homura Sep 18, 2023
8a1b45e
feat: copyable spore info
homura Sep 18, 2023
3d34fb0
feat: display the cluster's description
homura Sep 19, 2023
e36dab9
feat: support sending spore
homura Sep 21, 2023
0f011a5
fix: hide spore when invisible
homura Sep 27, 2023
e0e88d7
Merge remote-tracking branch 'origin/develop' into spore
homura Oct 27, 2023
a4e6f46
feat: support the nft transferring in light client mode
homura Nov 6, 2023
350ef66
Merge remote-tracking branch 'origin/develop' into spore
homura Nov 6, 2023
869aaf9
Merge remote-tracking branch 'origin/develop' into spore
homura Nov 15, 2023
5107afd
refactor: migrate NodeService to NetworksService
homura Nov 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/neuron-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"last 2 chrome versions"
],
"dependencies": {
"@ckb-lumos/base": "0.21.0-next.1",
"@ckb-lumos/codec": "0.21.0-next.1",
"@nervosnetwork/ckb-sdk-core": "0.109.0",
"@nervosnetwork/ckb-sdk-utils": "0.109.0",
"canvg": "2.0.0",
Expand Down
27 changes: 23 additions & 4 deletions packages/neuron-ui/src/components/NFTSend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useState, useCallback, useReducer, useMemo, useRef, useEffect } from 'react'
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useState as useGlobalState, useDispatch, AppActions } from 'states'
import { AppActions, useDispatch, useState as useGlobalState } from 'states'
import { isMainnet as isMainnetUtil, isSuccessResponse, validateAddress } from 'utils'
import useGetCountDownAndFeeRateStats from 'utils/hooks/useGetCountDownAndFeeRateStats'
import TextField from 'widgets/TextField'
import Dialog from 'widgets/Dialog'
import { generateNFTSendTransaction } from 'services/remote'
import { isErrorWithI18n } from 'exceptions'
import styles from './NFTSend.module.scss'
import { NFTType } from '../SpecialAssetList'
import { generateSporeSendTransaction } from '../../services/remote/spore'

enum Fields {
Address = 'address',
Expand Down Expand Up @@ -38,7 +40,9 @@ const NFTSend = ({
onCancel,
cell,
onSuccess,
nftType = NFTType.NFT,
}: {
nftType?: NFTType
onCancel: () => void
cell: {
nftId: string
Expand Down Expand Up @@ -138,7 +142,17 @@ const NFTSend = ({
feeRate: `${suggestFeeRate}`,
}

generateNFTSendTransaction(params)
const generate = (() => {
switch (nftType) {
case NFTType.Spore: {
return generateSporeSendTransaction
}
default:
return generateNFTSendTransaction
}
})()

generate(params)
.then(res => {
if (isSuccessResponse(res)) {
globalDispatch({
Expand All @@ -156,10 +170,15 @@ const NFTSend = ({
return clearTimer
}, [isSubmittable, globalDispatch, sendState, walletId, outPoint, suggestFeeRate])

const displayNftType = (() => {
if (nftType === NFTType.Spore) return 'Spore'
return 'mNFT'
})()

return (
<Dialog
show
title={`${t('special-assets.transfer-nft')} #${nftId} mNFT`}
title={`${t('special-assets.transfer-nft')} #${nftId} ${displayNftType}`}
disabled={!isSubmittable}
onCancel={onCancel}
onConfirm={onSubmit}
Expand Down
45 changes: 39 additions & 6 deletions packages/neuron-ui/src/components/SpecialAssetList/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
nftFormatter,
PresetScript,
shannonToCKBFormatter,
sporeFormatter,
sudtValueToAmount,
toUint128Le,
useDialogWrapper,
Expand Down Expand Up @@ -193,15 +194,47 @@ export const useSpecialAssetColumnInfo = ({
const isLockedCheque = status === 'withdraw-asset' && targetTime && Date.now() < targetTime
const isNFTTransferable = assetInfo.type === NFTType.NFT && assetInfo.data === 'transferable'
const isNFTClassOrIssuer = assetInfo.type === NFTType.NFTClass || assetInfo.type === NFTType.NFTIssuer
const isSpore = assetInfo.type === NFTType.Spore

if (assetInfo.type === NFTType.NFT) {
amount = nftFormatter(type?.args)
status = 'transfer-nft'
} else if (isNFTClassOrIssuer || assetInfo.type === 'Unknown') {
amount = t('special-assets.unknown-asset')
let sporeClusterInfo: { name: string; description: string } | undefined

switch (assetInfo.type) {
case NFTType.NFT: {
amount = nftFormatter(type?.args)
status = 'transfer-nft'
break
}
case NFTType.Spore: {
if (type) {
// every spore cell is transferable
status = 'transfer-nft'
sporeClusterInfo = JSON.parse(item.customizedAssetInfo.data)
amount = sporeFormatter({ args: type.args, data: item.data, clusterName: sporeClusterInfo?.name })
}
break
}
case NFTType.NFTClass:
case NFTType.NFTIssuer:
case 'Unknown': {
amount = t('special-assets.unknown-asset')
break
}
default: {
break
}
}

return { amount, status, targetTime, isLockedCheque, isNFTTransferable, isNFTClassOrIssuer, epochsInfo }
return {
amount,
status,
targetTime,
isLockedCheque,
isNFTTransferable,
isNFTClassOrIssuer,
epochsInfo,
isSpore,
sporeClusterInfo,
}
},
[epoch, bestKnownBlockTimestamp, tokenInfoList, t]
)
Expand Down
42 changes: 38 additions & 4 deletions packages/neuron-ui/src/components/SpecialAssetList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
uniformTimeFormatter,
getExplorerUrl,
ConnectionStatus,
sporeFormatter,
} from 'utils'
import { NetworkType, HIDE_BALANCE } from 'utils/const'
import useGetCountDownAndFeeRateStats from 'utils/hooks/useGetCountDownAndFeeRateStats'
Expand All @@ -41,6 +42,7 @@ import TableNoData from 'widgets/Icons/TableNoData.png'
import { useGetAssetAccounts, useSpecialAssetColumnInfo, SpecialAssetCell } from './hooks'

import styles from './specialAssetList.module.scss'
import CopyZone from '../../widgets/CopyZone'

export interface LocktimeAssetInfo {
data: string
Expand All @@ -58,6 +60,9 @@ export enum NFTType {
NFT = 'NFT',
NFTClass = 'NFTClass',
NFTIssuer = 'NFTIssuer',

Spore = 'Spore',
Cluster = 'SporeCluster',
}

export interface NFTAssetInfo {
Expand Down Expand Up @@ -92,6 +97,7 @@ const SpecialAssetList = () => {
const [isExistAccountDialogOpen, setIsExistAccountDialogOpen] = useState<boolean>(false)
const [nFTSendCell, setNFTSendCell] = useState<
| {
nftType?: NFTType
nftId: string
outPoint: {
index: string
Expand Down Expand Up @@ -286,6 +292,16 @@ const SpecialAssetList = () => {
})
return
}
if (cell.customizedAssetInfo.type === 'Spore') {
setNFTSendCell({
// unnecessary id for the spore
nftId: cell.type?.args ?? '',
outPoint: cell.outPoint,
nftType: NFTType.Spore,
})
return
}

const handleRes =
(actionType: 'unlock' | 'withdraw-cheque' | 'claim-cheque') => (res: ControllerResponse<any>) => {
if (isSuccessResponse(res)) {
Expand Down Expand Up @@ -392,7 +408,22 @@ const SpecialAssetList = () => {
isBalance: true,
minWidth: '200px',
render(_, __, item, show) {
const { amount } = handleGetSpecialAssetColumnInfo(item)
const { amount, isSpore, sporeClusterInfo } = handleGetSpecialAssetColumnInfo(item)

if (isSpore && item.type && show) {
const formattedSporeInfo = sporeFormatter({
args: item.type.args,
data: item.data,
truncate: Infinity,
clusterName: sporeClusterInfo?.name,
})
return (
<CopyZone content={formattedSporeInfo} title={sporeClusterInfo?.description}>
{amount}
</CopyZone>
)
}

return show ? amount : HIDE_BALANCE
},
},
Expand All @@ -413,7 +444,6 @@ const SpecialAssetList = () => {
return (
<div className={styles.actionBtnBox}>
<Button
type="cancel"
label={t('special-assets.view-details')}
className={`${styles.actionBtn} ${styles.detailBtn}`}
onClick={() => onViewDetail(item)}
Expand Down Expand Up @@ -479,7 +509,6 @@ const SpecialAssetList = () => {
/>
)}
<Button
type="cancel"
label={t('special-assets.view-details')}
className={`${styles.actionBtn} ${styles.detailBtn}`}
onClick={() => onViewDetail(item)}
Expand Down Expand Up @@ -549,7 +578,12 @@ const SpecialAssetList = () => {
) : null}

{nFTSendCell ? (
<NFTSend cell={nFTSendCell} onCancel={() => setNFTSendCell(undefined)} onSuccess={handleActionSuccess} />
<NFTSend
nftType={nFTSendCell.nftType}
cell={nFTSendCell}
onCancel={() => setNFTSendCell(undefined)}
onSuccess={handleActionSuccess}
/>
) : null}

<Toast content={notice} onDismiss={() => setNotice('')} />
Expand Down
2 changes: 2 additions & 0 deletions packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ type Action =
| 'get-hold-sudt-cell-capacity'
| 'start-migrate'
| 'get-sync-progress-by-addresses'
// spore
| 'generate-transfer-spore-tx'

export const remoteApi =
<P = any, R = any>(action: Action) =>
Expand Down
7 changes: 7 additions & 0 deletions packages/neuron-ui/src/services/remote/spore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { remoteApi } from './remoteApiWrapper'

// eslint-disable-next-line import/prefer-default-export
export const generateSporeSendTransaction = remoteApi<
Controller.CreateNFTSendTransaction.Params,
Controller.CreateNFTSendTransaction.Response
>('generate-transfer-spore-tx')
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { sporeFormatter, truncateMiddle } from 'utils/formatters'

describe('sporeFormatter', () => {
// https://pudge.explorer.nervos.org/nft-info/0xd3b1f0634da710628a6f9faa73db028708dc79eb75de936bf39f0960cb882652/45251453428383742336079001511900429529583900220860399226448158779453103373468

const sporeArgs = '0x640b6a3dd74ff4c87f44fc459bfb1bfa3bae60d8ba593f43796383860b1b7c9c'
const sporeData =
'0xde010000100000001e000000ba0100000a000000696d6167652f6a70656798010000ffd8ffe000104a46494600010101004800480000ffdb0043000a07070807060a0808080b0a0a0b0e18100e0d0d0e1d15161118231f2524221f2221262b372f26293429212230413134393b3e3e3e252e4449433c48373d3e3bffdb0043010a0b0b0e0d0e1c10101c3b2822283b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3bffc0001108000a000a03012200021101031101ffc4001500010100000000000000000000000000000407ffc4001f1000030002020301010000000000000000010203040500110621314114ffc40014010100000000000000000000000000000000ffc40014110100000000000000000000000000000000ffda000c03010002110311003f0064773b5c2d6f9b6c2db3cc7c76ced8e1499eec7f8dd10b40a127a9a92ce9ebd9631007de50fc4ed5c9f0ed2def57ad6baf83d28ec599d8cd49249fa49fde29f4daaa62e462beb30db1f2aa6d91230529672412cc3ae99bb00f67dfa1c4c632c684e10924a5250939a28554503a0001f001f9c0ffd9200000000c800b63cb44a925e1fbce395e76ceb6f115518130ff210be32b922a93bc5d64'
const clusterId = '0x0c800b63cb44a925e1fbce395e76ceb6f115518130ff210be32b922a93bc5d64'

const truncatedSporeId = truncateMiddle(sporeArgs)
const truncatedClusterId = truncateMiddle(clusterId)

it('should truncate args', () => {
expect(truncatedSporeId).toBe('0x640b6a...0b1b7c9c')
expect(truncatedClusterId).toBe('0x0c800b...93bc5d64')
})

it('should work as expected without cluster', () => {
const formatted = sporeFormatter({ args: sporeArgs })
expect(formatted).toBe(`[${truncatedSporeId}] Spore`)
})

it('should work as expected with cluster', () => {
const withoutName = sporeFormatter({ args: sporeArgs, data: sporeData })
expect(withoutName).toBe(`[${truncatedSporeId}] [${truncatedClusterId}] Spore`)

const clusterName = 'a very long cluster name'
const withName = sporeFormatter({ args: sporeArgs, data: sporeData, clusterName })
expect(withName).toBe(`[${truncatedSporeId}] [${truncateMiddle(clusterName)}] Spore`)
})
})
48 changes: 48 additions & 0 deletions packages/neuron-ui/src/utils/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { molecule } from '@ckb-lumos/codec'
import { blockchain } from '@ckb-lumos/base'
import { TFunction } from 'i18next'
import { FailureFromController } from 'services/remote/remoteApiWrapper'
import { CapacityUnit } from './enums'
Expand Down Expand Up @@ -295,6 +297,52 @@ export const nftFormatter = (hex?: string, idOnly = false) => {
return `#${id} mNFT`
}

export function truncateMiddle(str: string, start = 8, end = start): string {
if (str.length <= start + end) {
return str
}
return `${str.slice(0, start)}...${str.slice(-end)}`
}

type FormatterOptions = { args: string; data?: string; clusterName?: string; truncate?: number }
export const sporeFormatter = ({ args, data, clusterName, truncate }: FormatterOptions) => {
let format = 'Spore'

const SporeData = molecule.table(
{
contentType: blockchain.Bytes,
content: blockchain.Bytes,
clusterId: blockchain.BytesOpt,
},
['contentType', 'content', 'clusterId']
)

if (data) {
try {
const { clusterId } = SporeData.unpack(data)

// the name may be empty when it works with the light client.
// a spore cell may appear before the cluster cell is found in the light client.
// So we need a placeholder for the name.
if (clusterId && !clusterName) {
format = `[${truncateMiddle(clusterId, truncate)}] ${format}`
}
if (clusterId && clusterName) {
format = `[${truncateMiddle(clusterName, truncate)}] ${format}`
}
} catch {
// the Spore contract seems not guarantee the data always valid
// empty catch here to avoid crash
}
}

if (args) {
format = `[${truncateMiddle(args, truncate)}] ${format}`
}

return format
}

export const errorFormatter = (error: string | FailureFromController['message'], t: TFunction) => {
// empty string should return unknown error too
const unknownError = t('messages.unknown-error')
Expand Down
13 changes: 11 additions & 2 deletions packages/neuron-ui/src/widgets/CopyZone/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@ type CopyZoneProps = React.PropsWithChildren<{
style?: React.CSSProperties
className?: string
maskRadius?: number
title?: string
}>
const CopyZone = ({ children, content, name, style, className = '', maskRadius = 16 }: CopyZoneProps) => {
const CopyZone = ({
children,
content,
name,
style,
className = '',
maskRadius = 16,
title = content,
}: CopyZoneProps) => {
const [t] = useTranslation()
const [copied, setCopied] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout>>()
Expand All @@ -36,7 +45,7 @@ const CopyZone = ({ children, content, name, style, className = '', maskRadius =
onClick={onCopy}
className={`${styles.container} ${className}`}
style={style}
title={content}
title={title}
>
{children}
<div className={styles.hoverShow} style={{ borderRadius: `${maskRadius}px` }}>
Expand Down
1 change: 1 addition & 0 deletions packages/neuron-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@ckb-lumos/rpc": "^0.21.0-next.1",
"@iarna/toml": "2.2.5",
"@ledgerhq/hw-transport-node-hid": "6.27.16",
"@spore-sdk/core": "0.1.0-beta.9",
"archiver": "5.3.0",
"async": "3.2.4",
"bn.js": "4.12.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export default class LightConnector extends Connector<CKBComponents.Hash> {
assetAccountInfo.getNftIssuerInfo().cellDep,
assetAccountInfo.getLegacyAnyoneCanPayInfo().cellDep,
assetAccountInfo.getChequeInfo().cellDep,
...assetAccountInfo.getSporeInfos().map(info => info.cellDep),
...assetAccountInfo.getSporeClusterInfo().map(info => info.cellDep),
]
const fetchTxHashes = fetchCellDeps.map(v => v.outPoint.txHash).map<[string, string]>(v => ['fetchTransaction', v])
const txs = await this.lightRpc
Expand Down
Loading