From 2ec3ff82ca2bbcf5d614758e3a2569773a0de7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=A5=E5=9B=BD=E5=AE=87?= <841185308@qq.com> Date: Fri, 19 Jul 2024 16:29:11 +0800 Subject: [PATCH] feat: Support XUDT (#3206) * feat: Support XUDT * fix: Support xudt args length * fix: Dynamic calculate min capacity * fix: Send xudt with live acp udt cell --- packages/neuron-ui/package.json | 17 +- .../CellManagement/cellManagement.module.scss | 2 +- .../src/components/CellManagement/hooks.ts | 7 +- .../src/components/CellManagement/index.tsx | 4 +- .../src/components/NervosDAO/hooks.ts | 2 +- .../src/components/SUDTAccountList/index.tsx | 10 +- .../src/components/SUDTCreateDialog/index.tsx | 36 ++- .../components/SUDTMigrateDialog/index.tsx | 8 +- .../SUDTMigrateToExistAccountDialog/index.tsx | 13 +- .../SUDTMigrateToNewAccountDialog/index.tsx | 8 +- .../src/components/SUDTSend/hooks.ts | 5 +- .../src/components/SUDTSend/index.tsx | 15 +- .../src/components/SUDTUpdateDialog/index.tsx | 12 +- .../src/components/SpecialAssetList/hooks.ts | 16 +- .../src/components/SpecialAssetList/index.tsx | 50 ++-- .../src/components/TransactionType/index.tsx | 2 +- packages/neuron-ui/src/locales/en.json | 25 +- packages/neuron-ui/src/locales/es.json | 25 +- packages/neuron-ui/src/locales/fr.json | 25 +- packages/neuron-ui/src/locales/zh-tw.json | 25 +- packages/neuron-ui/src/locales/zh.json | 25 +- .../neuron-ui/src/services/remote/sudt.ts | 13 +- .../src/states/stateProvider/reducer.ts | 3 +- .../src/stories/SUDTCreateDialog.stories.tsx | 6 +- packages/neuron-ui/src/types/App/index.d.ts | 9 +- .../neuron-ui/src/types/Controller/index.d.ts | 15 +- packages/neuron-ui/src/utils/const.ts | 1 + packages/neuron-ui/src/utils/enums.ts | 7 + .../src/utils/hooks/createSUDTAccount.ts | 36 ++- packages/neuron-ui/src/utils/hooks/index.ts | 6 +- packages/neuron-ui/src/utils/is.ts | 28 ++ .../neuron-ui/src/utils/validators/tokenId.ts | 9 +- .../sync/light-synchronizer.ts | 1 + .../src/block-sync-renderer/sync/queue.ts | 6 +- .../sync/tx-address-finder.ts | 15 + .../src/controllers/anyone-can-pay.ts | 7 +- packages/neuron-wallet/src/controllers/api.ts | 15 +- .../src/controllers/asset-account.ts | 14 +- .../neuron-wallet/src/controllers/sudt.ts | 10 - .../database/chain/entities/asset-account.ts | 14 +- .../chain/entities/sudt-token-info.ts | 10 +- .../migrations/1720089814860-AddUdtType.ts | 22 ++ .../src/database/chain/ormconfig.ts | 2 + .../src/models/asset-account-info.ts | 86 +++--- .../neuron-wallet/src/models/asset-account.ts | 11 +- .../neuron-wallet/src/models/chain/output.ts | 29 +- .../src/models/chain/transaction.ts | 1 + .../src/services/anyone-can-pay.ts | 44 ++- .../src/services/asset-account-service.ts | 144 ++++++--- packages/neuron-wallet/src/services/cells.ts | 51 +++- .../src/services/sudt-token-info.ts | 6 +- .../src/services/tx/transaction-generator.ts | 140 ++++++--- .../src/services/tx/transaction-service.ts | 283 +++++++++++------- packages/neuron-wallet/src/utils/ckb-rpc.ts | 2 +- packages/neuron-wallet/src/utils/const.ts | 5 + .../tests/controllers/anyone-can-pay.test.ts | 6 +- .../tests/models/asset-account-info.test.ts | 20 +- .../tests/services/anyone-can-pay.test.ts | 38 ++- .../services/asset-account-service.test.ts | 59 +++- .../tests/services/cells.test.ts | 27 +- .../tests/services/multisig.test.ts | 16 +- .../tests/services/networks.test.ts | 37 ++- .../tests/services/sudt-token-info.test.ts | 7 +- .../services/tx/transaction-generator.test.ts | 90 ++++-- .../setupAndTeardown/accounts.fixture.ts | 2 + 65 files changed, 1117 insertions(+), 568 deletions(-) create mode 100644 packages/neuron-wallet/src/database/chain/migrations/1720089814860-AddUdtType.ts diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index c832d47fd1..633abb7d55 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -50,14 +50,15 @@ "last 2 chrome versions" ], "dependencies": { - "@ckb-lumos/bi": "0.21.1", - "@ckb-lumos/rpc": "0.21.1", - "@ckb-lumos/base": "0.21.1", - "@ckb-lumos/codec": "0.21.1", - "@ckb-lumos/hd": "0.21.1", - "@ckb-lumos/helpers": "0.21.1", - "@ckb-lumos/config-manager": "0.21.1", - "@ckb-lumos/common-scripts": "0.21.1", + "@ckb-lumos/lumos": "0.23.0", + "@ckb-lumos/bi": "0.23.0", + "@ckb-lumos/rpc": "0.23.0", + "@ckb-lumos/base": "0.23.0", + "@ckb-lumos/codec": "0.23.0", + "@ckb-lumos/hd": "0.23.0", + "@ckb-lumos/helpers": "0.23.0", + "@ckb-lumos/config-manager": "0.23.0", + "@ckb-lumos/common-scripts": "0.23.0", "canvg": "2.0.0", "i18next": "23.7.11", "immer": "9.0.21", diff --git a/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss b/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss index a142528c7e..a36bad6e1e 100644 --- a/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss +++ b/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss @@ -53,7 +53,7 @@ .actions { display: flex; gap: 16px; - & > svg { + & svg { cursor: pointer; &[data-disabled='true'] { cursor: not-allowed; diff --git a/packages/neuron-ui/src/components/CellManagement/hooks.ts b/packages/neuron-ui/src/components/CellManagement/hooks.ts index 1823cc98c0..4977cc6ced 100644 --- a/packages/neuron-ui/src/components/CellManagement/hooks.ts +++ b/packages/neuron-ui/src/components/CellManagement/hooks.ts @@ -18,7 +18,8 @@ import { ErrorCode, LockScriptCategory, RoutePath, TypeScriptCategory, isSuccess import { SortType } from 'widgets/Table' const cellTypeOrder: Record = { - [TypeScriptCategory.SUDT]: 1, + [TypeScriptCategory.SUDT]: 0, + [TypeScriptCategory.XUDT]: 1, [TypeScriptCategory.NFT]: 2, [TypeScriptCategory.Spore]: 3, [TypeScriptCategory.Unknown]: 4, @@ -47,6 +48,9 @@ const getLockStatusAndReason = (item: State.LiveCellWithLocalInfo) => { case TypeScriptCategory.DAO: lockedReason = { key: 'cell-manage.locked-reason.NFT-SUDT-DAO', params: { type: 'Nervos DAO' } } break + case TypeScriptCategory.XUDT: + lockedReason = { key: 'cell-manage.locked-reason.NFT-SUDT-DAO', params: { type: 'XUDT' } } + break case TypeScriptCategory.Unknown: lockedReason = { key: 'cell-manage.locked-reason.Unknown' } break @@ -82,6 +86,7 @@ const getCellType = (item: State.LiveCellWithLocalInfo) => { switch (item.typeScriptType) { case TypeScriptCategory.NFT: case TypeScriptCategory.SUDT: + case TypeScriptCategory.XUDT: case TypeScriptCategory.Spore: case TypeScriptCategory.Unknown: return item.typeScriptType diff --git a/packages/neuron-ui/src/components/CellManagement/index.tsx b/packages/neuron-ui/src/components/CellManagement/index.tsx index 994c88e042..7fbb4860bc 100644 --- a/packages/neuron-ui/src/components/CellManagement/index.tsx +++ b/packages/neuron-ui/src/components/CellManagement/index.tsx @@ -193,7 +193,7 @@ const getColumns = ({ @@ -202,7 +202,7 @@ const getColumns = ({ diff --git a/packages/neuron-ui/src/components/NervosDAO/hooks.ts b/packages/neuron-ui/src/components/NervosDAO/hooks.ts index fe48897e12..e0e900c72c 100644 --- a/packages/neuron-ui/src/components/NervosDAO/hooks.ts +++ b/packages/neuron-ui/src/components/NervosDAO/hooks.ts @@ -337,7 +337,7 @@ export const useUpdateDepositEpochList = ({ useEffect(() => { if (connectionStatus === 'online') { getBlockHashes(records.map(v => v.depositOutPoint?.txHash).filter(v => !!v) as string[]).then( - depositBlockHashes => { + (depositBlockHashes: { txHash: string; blockHash: string | undefined }[]) => { const recordKeyIdx: string[] = [] const batchParams: ['getHeader', string][] = [] records.forEach(record => { diff --git a/packages/neuron-ui/src/components/SUDTAccountList/index.tsx b/packages/neuron-ui/src/components/SUDTAccountList/index.tsx index fa4f6972ef..b961ca079d 100644 --- a/packages/neuron-ui/src/components/SUDTAccountList/index.tsx +++ b/packages/neuron-ui/src/components/SUDTAccountList/index.tsx @@ -24,6 +24,7 @@ import { isSuccessResponse, useIsInsufficientToCreateSUDTAccount, useOnGenerateNewAccountTransaction, + UDTType, } from 'utils' import { getSUDTAccountList, updateSUDTAccount } from 'services/remote' @@ -48,7 +49,11 @@ const SUDTAccountList = () => { const [keyword, setKeyword] = useState('') const [dialog, setDialog] = useState<{ id: string; action: 'create' | 'update' } | null>(null) const [isLoaded, setIsLoaded] = useState(false) - const [insufficient, setInsufficient] = useState({ [AccountType.CKB]: false, [AccountType.SUDT]: false }) + const [insufficient, setInsufficient] = useState({ + [AccountType.CKB]: false, + [AccountType.SUDT]: false, + [AccountType.XUDT]: false, + }) const isMainnet = isMainnetUtil(networks, networkID) const [receiveData, setReceiveData] = useState(null) @@ -178,6 +183,7 @@ const SUDTAccountList = () => { tokenName: accountToUpdate.tokenName || DEFAULT_SUDT_FIELDS.tokenName, symbol: accountToUpdate.symbol || DEFAULT_SUDT_FIELDS.symbol, isCKB: accountToUpdate.tokenId === DEFAULT_SUDT_FIELDS.CKBTokenId, + udtType: accountToUpdate.udtType, onSubmit: (info: Omit) => { const params: any = { id: accountToUpdate.accountId } Object.keys(info).forEach(key => { @@ -208,7 +214,7 @@ const SUDTAccountList = () => { : undefined const handleCreateAccount = useCallback( - (info: TokenInfo) => { + (info: TokenInfo & { udtType?: UDTType }) => { createAccount(info, () => { setNotice(t('s-udt.create-account-success')) }) diff --git a/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx b/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx index fa9c5a0b19..c4710b31c8 100644 --- a/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx +++ b/packages/neuron-ui/src/components/SUDTCreateDialog/index.tsx @@ -10,13 +10,15 @@ import { isSuccessResponse, useSUDTAccountInfoErrors, useFetchTokenInfoList, - useOpenSUDTTokenUrl, + useOpenUDTTokenUrl, + UDTType, } from 'utils' import { DEFAULT_SUDT_FIELDS } from 'utils/const' import styles from './sUDTCreateDialog.module.scss' export enum AccountType { SUDT = 'sudt', + XUDT = 'xudt', CKB = 'ckb', } @@ -33,10 +35,10 @@ export interface TokenInfo extends BasicInfo { export interface SUDTCreateDialogProps extends TokenInfo { isMainnet: boolean - onSubmit: (info: TokenInfo) => void + onSubmit: (info: TokenInfo & { udtType?: UDTType }) => void onCancel: () => void existingAccountNames?: string[] - insufficient?: { [AccountType.CKB]: boolean; [AccountType.SUDT]: boolean } + insufficient?: { [P in AccountType]: boolean } } enum DialogSection { @@ -49,6 +51,10 @@ const accountTypes: { key: AccountType; label: string }[] = [ key: AccountType.SUDT, label: 's-udt.create-dialog.sudt-account', }, + { + key: AccountType.XUDT, + label: 's-udt.create-dialog.xudt-account', + }, { key: AccountType.CKB, label: 's-udt.create-dialog.ckb-account', @@ -121,13 +127,18 @@ const SUDTCreateDialog = ({ onSubmit, onCancel, existingAccountNames = [], - insufficient = { [AccountType.CKB]: false, [AccountType.SUDT]: false }, + insufficient = { [AccountType.CKB]: false, [AccountType.SUDT]: false, [AccountType.XUDT]: false }, isMainnet, }: Partial> & Pick) => { const [t] = useTranslation() const [info, dispatch] = useReducer(reducer, { accountName, tokenId, tokenName, symbol, decimal }) - const [accountType, setAccountType] = useState([AccountType.SUDT, AccountType.CKB].find(at => !insufficient[at])) + const [accountType, setAccountType] = useState( + [AccountType.SUDT, AccountType.CKB, AccountType.XUDT].find(at => !insufficient[at]) + ) + const isUDT = accountType === AccountType.SUDT || accountType === AccountType.XUDT + // eslint-disable-next-line no-nested-ternary + const udtType = isUDT ? (accountType === AccountType.SUDT ? UDTType.SUDT : UDTType.XUDT) : undefined const [step, setStep] = useState(DialogSection.Account) const tokenInfoList = useFetchTokenInfoList() @@ -135,9 +146,10 @@ const SUDTCreateDialog = ({ const tokenErrors = useSUDTAccountInfoErrors({ info, - isCKB: AccountType.CKB === accountType, + isCKB: !isUDT, existingAccountNames, t, + udtType, }) const isAccountNameReady = info.accountName.trim() && !tokenErrors.accountName && accountType @@ -190,7 +202,7 @@ const SUDTCreateDialog = ({ } case DialogSection.Token: { if (isTokenReady) { - onSubmit({ ...info, accountName: info.accountName.trim(), tokenName: info.tokenName.trim() }) + onSubmit({ ...info, udtType, accountName: info.accountName.trim(), tokenName: info.tokenName.trim() }) } break } @@ -216,7 +228,7 @@ const SUDTCreateDialog = ({ } } } - const openSUDTTokenUrl = useOpenSUDTTokenUrl(info.tokenId, isMainnet) + const openSUDTTokenUrl = useOpenUDTTokenUrl(info.tokenId, udtType, isMainnet) return ( ))} - {accountType === AccountType.SUDT && !tokenErrors.tokenId && info.tokenId && ( + {isUDT && !tokenErrors.tokenId && info.tokenId && ( )} diff --git a/packages/neuron-ui/src/components/SpecialAssetList/hooks.ts b/packages/neuron-ui/src/components/SpecialAssetList/hooks.ts index de2af6f2db..d1ff65cc43 100644 --- a/packages/neuron-ui/src/components/SpecialAssetList/hooks.ts +++ b/packages/neuron-ui/src/components/SpecialAssetList/hooks.ts @@ -180,13 +180,6 @@ export const useSpecialAssetColumnInfo = ({ } break } - case PresetScript.SUDT: { - status = 'user-defined-token' - const tokenInfo = tokenInfoList.find(info => info.tokenID === type?.args) - const amountInfo = getSUDTAmount({ tokenInfo, data }) - amount = amountInfo.amount - break - } default: { // ignore } @@ -220,6 +213,14 @@ export const useSpecialAssetColumnInfo = ({ amount = t('special-assets.unknown-asset') break } + case PresetScript.XUDT: + case PresetScript.SUDT: { + status = 'user-defined-token' + const tokenInfo = tokenInfoList.find(info => info.tokenID === type?.args) + const amountInfo = getSUDTAmount({ tokenInfo, data }) + amount = amountInfo.amount + break + } default: { break } @@ -235,6 +236,7 @@ export const useSpecialAssetColumnInfo = ({ epochsInfo, isSpore, sporeClusterInfo, + udtType: assetInfo.type, } }, [epoch, bestKnownBlockTimestamp, tokenInfoList, t] diff --git a/packages/neuron-ui/src/components/SpecialAssetList/index.tsx b/packages/neuron-ui/src/components/SpecialAssetList/index.tsx index 171aa745cb..7f4c5eb6b9 100644 --- a/packages/neuron-ui/src/components/SpecialAssetList/index.tsx +++ b/packages/neuron-ui/src/components/SpecialAssetList/index.tsx @@ -238,6 +238,7 @@ const SpecialAssetList = () => { symbol: (accountToClaim.account.symbol || foundTokenInfo?.symbol) ?? '', decimal: (accountToClaim.account.decimal || foundTokenInfo?.decimal) ?? '', isCKB: false, + udtType: accountToClaim.account.udtType, onSubmit: (info: Omit) => { const params: any = accountToClaim?.account || {} Object.keys(info).forEach(key => { @@ -371,6 +372,12 @@ const SpecialAssetList = () => { } break } + default: { + // ignore + } + } + switch (cell.customizedAssetInfo.type) { + case PresetScript.XUDT: case PresetScript.SUDT: { setMigrateCell(cell) const findTokenInfo = tokenInfoList.find(info => info.tokenID === cell.type?.args) @@ -379,9 +386,8 @@ const SpecialAssetList = () => { setIsMigrateDialogOpen(true) break } - default: { - // ignore - } + default: + break } }, [cells, id, dispatch, setAccountToClaim, navigate, setIsMigrateDialogOpen, tokenInfoList, handleActionSuccess] @@ -437,8 +443,15 @@ const SpecialAssetList = () => { customizedAssetInfo, } = item - const { status, targetTime, isLockedCheque, isNFTTransferable, isNFTClassOrIssuer, epochsInfo } = - handleGetSpecialAssetColumnInfo(item) + const { + status, + targetTime, + isLockedCheque, + isNFTTransferable, + isNFTClassOrIssuer, + epochsInfo, + udtType, + } = handleGetSpecialAssetColumnInfo(item) if (isNFTClassOrIssuer || (customizedAssetInfo.type === NFTType.NFT && !isNFTTransferable)) { return ( @@ -458,22 +471,15 @@ const SpecialAssetList = () => { ['user-defined-asset', 'locked-asset', 'user-defined-token'].includes(status) || isLockedCheque if (showTip) { - if (customizedAssetInfo.lock !== PresetScript.Cheque || isLockedCheque) { - tip = t(`special-assets.${status}-tooltip`, { - epochs: epochsInfo?.target.toFixed(2), - year: targetTime ? new Date(targetTime).getFullYear() : '', - month: targetTime ? new Date(targetTime).getMonth() + 1 : '', - day: targetTime ? new Date(targetTime).getDate() : '', - hour: targetTime ? new Date(targetTime).getHours() : '', - minute: targetTime ? new Date(targetTime).getMinutes() : '', - }) - } - if (status === 'user-defined-token') { - tip = t('special-assets.user-defined-asset-tooltip') - } - if (status === 'user-defined-token') { - tip = t('special-assets.user-defined-token-tooltip') - } + tip = t(`special-assets.${status}-tooltip`, { + udtType, + epochs: epochsInfo?.target.toFixed(2), + year: targetTime ? new Date(targetTime).getFullYear() : '', + month: targetTime ? new Date(targetTime).getMonth() + 1 : '', + day: targetTime ? new Date(targetTime).getDate() : '', + hour: targetTime ? new Date(targetTime).getHours() : '', + minute: targetTime ? new Date(targetTime).getMinutes() : '', + }) } const btnDisabled = @@ -490,7 +496,7 @@ const SpecialAssetList = () => { data-idx={index} data-status={status} type="primary" - label={t(`special-assets.${status}`)} + label={t(`special-assets.${status}`, { udtType })} className={styles.actionBtn} onClick={handleAction} disabled={!!btnDisabled} diff --git a/packages/neuron-ui/src/components/TransactionType/index.tsx b/packages/neuron-ui/src/components/TransactionType/index.tsx index 59ed2b91ab..068702b92f 100644 --- a/packages/neuron-ui/src/components/TransactionType/index.tsx +++ b/packages/neuron-ui/src/components/TransactionType/index.tsx @@ -74,7 +74,7 @@ const TransactionType = ({ i18nKey: `overview.${item.type}SUDT`, components: [ , diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json index db85a72669..a500247b60 100644 --- a/packages/neuron-ui/src/locales/en.json +++ b/packages/neuron-ui/src/locales/en.json @@ -516,10 +516,10 @@ "title": "Create Asset Account" }, "send-sudt": { - "title": "Send sUDT" + "title": "Send UDT" }, "transfer-to-sudt": { - "title": "Send to sUDT account" + "title": "Send to UDT account" }, "send-ckb-asset": { "title": "Send CKB" @@ -528,7 +528,7 @@ "title": "Send CKB" }, "send-acp-sudt-to-new-cell": { - "title": "Send sUDT" + "title": "Send UDT" }, "send-acp-ckb-to-new-cell": { "title": "Send CKB" @@ -960,7 +960,7 @@ "locked-asset": "Locked", "locked-asset-tooltip": "Lock parameter is {{epochs}} epochs, estimated release date is {{year}}-{{month}}-{{day}}(according to the actual running block height, there may be some time variances in locktime).", "withdraw-asset-tooltip": "Estimate release time is {{year}}-{{month}}-{{day}} {{hour}}:{{minute}}(according to the actual running block height).", - "user-defined-token-tooltip": "Migrate sUDT asset to a sUDT Asset Account", + "user-defined-token-tooltip": "Migrate {{udtType}} asset to a {{udtType}} Asset Account", "claim-asset": "Claim", "withdraw-asset": "Withdraw", "view-details": "View Details", @@ -971,14 +971,14 @@ "transfer-nft": "Send", "user-defined-token": "Migrate", "transfer-nft-success": "Transfer of assets successful", - "migrate-sudt-success": "Conversion to new sUDT asset account successful", - "send-sudt-success": "Transfer to sUDT account successful", + "migrate-sudt-success": "Conversion to new {{udtType}} asset account successful", + "send-sudt-success": "Transfer to {{udtType}} account successful", "unlock-success": "Asset claimed, go to transaction history for details", "withdraw-cheque-success": "Asset withdrawn, go to transaction history for details", "claim-cheque-success": "Asset claimed, go to transaction history for details" }, "migrate-sudt": { - "title": "Migrate to a sUDT Asset Account", + "title": "Migrate to a {{udtType}} Asset Account", "choose-title": "Migration mode", "next": "Next", "back": "Back", @@ -986,14 +986,14 @@ "input-symbol": "Please input symbol", "input-decimal": "Please input decimal", "turn-into-new-account": { - "title": "Turn into a new sUDT Asset Account", - "sub-title": "Turn the sUDT Asset into a new sUDT Account, occupies at least 142 CKBytes", + "title": "Turn into a new {{udtType}} Asset Account", + "sub-title": "Turn the {{udtType}} Asset into a new {{udtType}} Account, occupies at least 142 CKBytes", "cancel": "Cancel", "confirm": "Confirm" }, "transfer-to-exist-account": { - "title": "Transfer to a sUDT Asset Account", - "sub-title": "Transfer all sUDT balance to an existing sUDT Account, please make sure the target account is alive" + "title": "Transfer to a {{udtType}} Asset Account", + "sub-title": "Transfer all {{udtType}} balance to an existing {{udtType}} Account, please make sure the target account is alive" }, "cancel": "Cancel", "confirm": "Confirm", @@ -1019,6 +1019,7 @@ "select-account-type": "Select account type", "account-name": "Account Name", "sudt-account": "sUDT Account", + "xudt-account": "xUDT Account", "delete-failed": "Failed to delete the multisig config, reason for failure: {{reason}}", "ckb-account": "CKB Account", "set-token-info": "Set Token Info", @@ -1040,7 +1041,7 @@ "decimal": "Please input decimal" }, "placeholder": { - "token-id": "Token ID is the Args of the sUDT Type Script, which is the same as the lock hash of the token issuer." + "token-id": "Token ID is the Args of the {{udtType}} Type Script, which is the same as the lock hash of the token issuer." } }, "send": { diff --git a/packages/neuron-ui/src/locales/es.json b/packages/neuron-ui/src/locales/es.json index 6e2fd2f7cc..94b69501d7 100644 --- a/packages/neuron-ui/src/locales/es.json +++ b/packages/neuron-ui/src/locales/es.json @@ -499,10 +499,10 @@ "title": "Crear Cuenta de Activos" }, "send-sudt": { - "title": "Enviar sUDT" + "title": "Enviar UDT" }, "transfer-to-sudt": { - "title": "Enviar a cuenta sUDT" + "title": "Enviar a cuenta UDT" }, "send-ckb-asset": { "title": "Enviar CKB" @@ -511,7 +511,7 @@ "title": "Enviar CKB" }, "send-acp-sudt-to-new-cell": { - "title": "Enviar sUDT" + "title": "Enviar UDT" }, "send-acp-ckb-to-new-cell": { "title": "Enviar CKB" @@ -943,7 +943,7 @@ "locked-asset": "Bloqueado", "locked-asset-tooltip": "El parámetro de bloqueo es de {{epochs}} épocas, la fecha de lanzamiento estimada es {{year}}-{{month}}-{{day}} (según la altura del bloque en ejecución real, pueden haber algunas variaciones de tiempo en el tiempo de bloqueo).", "withdraw-asset-tooltip": "Hora estimada de lanzamiento es {{year}}-{{month}}-{{day}} {{hour}}:{{minute}} (según la altura del bloque en ejecución real).", - "user-defined-token-tooltip": "Migre el activo sUDT a una cuenta de activos sUDT", + "user-defined-token-tooltip": "Migre el activo {{udtType}} a una cuenta de activos {{udtType}}", "claim-asset": "Reclamar", "withdraw-asset": "Retirar", "view-details": "Ver detalles", @@ -954,14 +954,14 @@ "transfer-nft": "Enviar", "user-defined-token": "Migrar", "transfer-nft-success": "Transferencia de activos exitosa", - "migrate-sudt-success": "Conversión a nueva cuenta de activo sUDT exitosa", - "send-sudt-success": "Transferencia exitosa a la cuenta de activo sUDT", + "migrate-sudt-success": "Conversión a nueva cuenta de activo {{udtType}} exitosa", + "send-sudt-success": "Transferencia exitosa a la cuenta de activo {{udtType}}", "unlock-success": "Activo reclamado. Vaya al historial de transacciones para obtener más detalles.", "withdraw-cheque-success": "El activo fue retirado. Vaya al historial de transacciones para obtener más detalles.", "claim-cheque-success": "Activo reclamado. Vaya al historial de transacciones para obtener más detalles." }, "migrate-sudt": { - "title": "Migrar a una cuenta de activo sUDT", + "title": "Migrar a una cuenta de activo {{udtType}}", "choose-title": "Modo de migración", "next": "Siguiente", "back": "Atrás", @@ -969,14 +969,14 @@ "input-symbol": "Por favor ingrese el símbolo", "input-decimal": "Por favor ingrese el decimal", "turn-into-new-account": { - "title": "Conviértalo en una nueva cuenta de activo sUDT", - "sub-title": "Convierta el activo sUDT en una nueva cuenta sUDT, ocupa al menos 142 CKbytes.", + "title": "Conviértalo en una nueva cuenta de activo {{udtType}}", + "sub-title": "Convierta el activo {{udtType}} en una nueva cuenta {{udtType}}, ocupa al menos 142 CKbytes.", "cancel": "Cancelar", "confirm": "Confirmar" }, "transfer-to-exist-account": { - "title": "Transferir a una cuenta de activo sUDT", - "sub-title": "Transfiera todo el saldo sUDT a una cuenta sUDT existente. Asegúrese de que la cuenta objetivo esté activa." + "title": "Transferir a una cuenta de activo {{udtType}}", + "sub-title": "Transfiera todo el saldo {{udtType}} a una cuenta {{udtType}} existente. Asegúrese de que la cuenta objetivo esté activa." }, "cancel": "Cancelar", "confirm": "Confirmar", @@ -1002,6 +1002,7 @@ "select-account-type": "Seleccione el tipo de cuenta", "account-name": "Nombre de la cuenta", "sudt-account": "Cuenta de activos sUDT", + "xudt-account": "Cuenta de activos xUDT", "delete-failed": "No se pudo eliminar la configuración multifirma, el motivo del fallo: {{reason}}", "ckb-account": "Cuenta CKB", "set-token-info": "Establecer información del token", @@ -1023,7 +1024,7 @@ "decimal": "Ingrese el decimal" }, "placeholder": { - "token-id": "El ID del token es el Args del sUDT Type Script, que es el mismo que el hash de bloqueo del emisor del token." + "token-id": "El ID del token es el Args del {{udtType}} Type Script, que es el mismo que el hash de bloqueo del emisor del token." } }, "send": { diff --git a/packages/neuron-ui/src/locales/fr.json b/packages/neuron-ui/src/locales/fr.json index ec0df27270..39b46403ba 100644 --- a/packages/neuron-ui/src/locales/fr.json +++ b/packages/neuron-ui/src/locales/fr.json @@ -506,10 +506,10 @@ "title": "Créer un compte d'actif" }, "send-sudt": { - "title": "Envoyer sUDT" + "title": "Envoyer UDT" }, "transfer-to-sudt": { - "title": "Envoyer vers le compte sUDT" + "title": "Envoyer vers le compte UDT" }, "send-ckb-asset": { "title": "Envoyer CKB" @@ -518,7 +518,7 @@ "title": "Envoyer CKB" }, "send-acp-sudt-to-new-cell": { - "title": "Envoyer sUDT" + "title": "Envoyer UDT" }, "send-acp-ckb-to-new-cell": { "title": "Envoyer CKB" @@ -950,7 +950,7 @@ "locked-asset": "Verrouillé", "locked-asset-tooltip": "Le paramètre de verrouillage est de {{epochs}} époques, la date de libération estimée est le {{year}}-{{month}}-{{day}} (selon la hauteur de bloc réelle en cours d'exécution, il peut y avoir des variances de temps dans le verrouillage).", "withdraw-asset-tooltip": "L'heure de libération estimée est le {{year}}-{{month}}-{{day}} à {{hour}}:{{minute}} (selon la hauteur de bloc réelle en cours d'exécution).", - "user-defined-token-tooltip": "Migrer l'actif sUDT vers un compte d'actif sUDT", + "user-defined-token-tooltip": "Migrer l'actif {{udtType}} vers un compte d'actif {{udtType}}", "claim-asset": "Réclamation", "withdraw-asset": "Retrait", "view-details": "Voir les détails", @@ -961,14 +961,14 @@ "transfer-nft": "Envoyer", "user-defined-token": "Migrer", "transfer-nft-success": "Transfert d'actifs réussi", - "migrate-sudt-success": "Conversion en nouveau compte d'actif sUDT réussie", - "send-sudt-success": "Transfert vers le compte sUDT réussi", + "migrate-sudt-success": "Conversion en nouveau compte d'actif {{udtType}} réussie", + "send-sudt-success": "Transfert vers le compte {{udtType}} réussi", "unlock-success": "Actif réclamé, consultez l'historique des transactions pour plus de détails", "withdraw-cheque-success": "Actif retiré, consultez l'historique des transactions pour plus de détails", "claim-cheque-success": "Actif réclamé, consultez l'historique des transactions pour plus de détails" }, "migrate-sudt": { - "title": "Migrer vers un compte d'actif sUDT", + "title": "Migrer vers un compte d'actif {{udtType}}", "choose-title": "Mode de migration", "next": "Suivant", "back": "Retour", @@ -976,14 +976,14 @@ "input-symbol": "Veuillez saisir le symbole", "input-decimal": "Veuillez saisir la décimale", "turn-into-new-account": { - "title": "Transformez en un nouveau compte d'actif sUDT", - "sub-title": "Transformez l'actif sUDT en un nouveau compte sUDT, occupe au moins 142 CKBytes", + "title": "Transformez en un nouveau compte d'actif {{udtType}}", + "sub-title": "Transformez l'actif {{udtType}} en un nouveau compte {{udtType}}, occupe au moins 142 CKBytes", "cancel": "Annuler", "confirm": "Confirmer" }, "transfer-to-exist-account": { - "title": "Transférer vers un compte d'actif sUDT", - "sub-title": "Transférez tout le solde sUDT vers un compte sUDT existant, assurez-vous que le compte cible est actif" + "title": "Transférer vers un compte d'actif {{udtType}}", + "sub-title": "Transférez tout le solde {{udtType}} vers un compte {{udtType}} existant, assurez-vous que le compte cible est actif" }, "cancel": "Annuler", "confirm": "Confirmer", @@ -1009,6 +1009,7 @@ "select-account-type": "Sélectionner le type de compte", "account-name": "Nom du compte", "sudt-account": "Compte sUDT", + "xudt-account": "Compte xUDT", "delete-failed": "Échec de la suppression de la configuration multisig, raison de l'échec : {{reason}}", "ckb-account": "Compte CKB", "set-token-info": "Définir les informations du token", @@ -1030,7 +1031,7 @@ "decimal": "Veuillez saisir la décimale" }, "placeholder": { - "token-id": "L'ID du token est l'argument du script de type sUDT, qui est identique au hachage de verrouillage de l'émetteur du token." + "token-id": "L'ID du token est l'argument du script de type {{udtType}}, qui est identique au hachage de verrouillage de l'émetteur du token." } }, "send": { diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json index 0bb1ea2f02..491f51fad9 100644 --- a/packages/neuron-ui/src/locales/zh-tw.json +++ b/packages/neuron-ui/src/locales/zh-tw.json @@ -510,10 +510,10 @@ "title": "創建資產賬戶" }, "send-sudt": { - "title": "發起 sUDT 交易" + "title": "發起 UDT 交易" }, "transfer-to-sudt": { - "title": "發起 sUDT 交易" + "title": "發起 UDT 交易" }, "send-ckb-asset": { "title": "發起 CKB 交易" @@ -522,7 +522,7 @@ "title": "發起 CKB 交易" }, "send-acp-sudt-to-new-cell": { - "title": "發起 sUDT 交易" + "title": "發起 UDT 交易" }, "send-acp-ckb-to-new-cell": { "title": "發起 CKB 交易" @@ -954,7 +954,7 @@ "locked-asset": "已鎖定", "locked-asset-tooltip": "鎖定參數為 {{epochs}} epochs, 預計解鎖時間為 {{year}}年{{month}}月{{day}}日(鎖定時間根據區塊鏈實際運行情況會有一定的誤差)。", "withdraw-asset-tooltip": "預計解鎖時間為 {{year}}年{{month}}月{{day}}日 {{hour}}時{{minute}}分(根據區塊鏈實際運行情況會有一定的誤差)。", - "user-defined-token-tooltip": "將 sUDT 資產轉移到 sUDT 賬戶", + "user-defined-token-tooltip": "將 {{udtType}} 資產轉移到 {{udtType}} 賬戶", "claim-asset": "領取", "withdraw-asset": "撤回", "view-details": "查看詳情", @@ -965,14 +965,14 @@ "transfer-nft": "轉讓", "user-defined-token": "遷移", "transfer-nft-success": "轉讓資產成功", - "migrate-sudt-success": "轉換成新的 sUDT 資產帳戶成功", - "send-sudt-success": "轉入 sUDT 帳戶成功", + "migrate-sudt-success": "轉換成新的 {{udtType}} 資產帳戶成功", + "send-sudt-success": "轉入 {{udtType}} 帳戶成功", "unlock-success": "資產已領取,可前往交易歷史查看詳情", "withdraw-cheque-success": "資產已撤回,可前往交易歷史查看詳情", "claim-cheque-success": "資產已領取,可前往交易歷史查看詳情" }, "migrate-sudt": { - "title": "遷移至 sUDT 資產賬戶", + "title": "遷移至 {{udtType}} 資產賬戶", "choose-title": "選擇遷移方式", "next": "下一步", "back": "上一步", @@ -980,14 +980,14 @@ "input-symbol": "請輸入簡稱", "input-decimal": "請輸入小數位", "turn-into-new-account": { - "title": "轉換成新的 sUDT 資產賬戶", - "sub-title": "將 sUDT 資產轉換成 sUDT 資產賬戶, 至少占用 142 CKBytes", + "title": "轉換成新的 {{udtType}} 資產賬戶", + "sub-title": "將 {{udtType}} 資產轉換成 {{udtType}} 資產賬戶, 至少占用 142 CKBytes", "cancel": "取消", "confirm": "確認" }, "transfer-to-exist-account": { - "title": "轉入 sUDT 賬戶", - "sub-title": "將全部 sUDT 余額轉入已存在的 sUDT 資產賬戶, 請確保目標賬戶存在" + "title": "轉入 {{udtType}} 賬戶", + "sub-title": "將全部 {{udtType}} 余額轉入已存在的 {{udtType}} 資產賬戶, 請確保目標賬戶存在" }, "cancel": "取消", "confirm": "確認", @@ -1013,6 +1013,7 @@ "select-account-type": "選擇帳戶類型", "account-name": "賬戶名稱", "sudt-account": "sUDT 賬戶", + "xudt-account": "xUDT 賬戶", "ckb-account": "CKB 賬戶", "set-token-info": "設置代幣信息", "token-id": "代幣ID", @@ -1033,7 +1034,7 @@ "decimal": "請輸入小數位" }, "placeholder": { - "token-id": "代幣 ID 即 sUDT Type Script 的 Args,等同於該 token 發行者的 lock hash。" + "token-id": "代幣 ID 即 {{udtType}} Type Script 的 Args,等同於該 token 發行者的 lock hash。" } }, "send": { diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json index 9481f370b7..ec23a10763 100644 --- a/packages/neuron-ui/src/locales/zh.json +++ b/packages/neuron-ui/src/locales/zh.json @@ -509,10 +509,10 @@ "title": "创建资产账户" }, "send-sudt": { - "title": "发起 sUDT 交易" + "title": "发起 UDT 交易" }, "transfer-to-sudt": { - "title": "转入 sUDT 账户" + "title": "转入 UDT 账户" }, "send-ckb-asset": { "title": "发起 CKB 交易" @@ -521,7 +521,7 @@ "title": "发起 CKB 交易" }, "send-acp-sudt-to-new-cell": { - "title": "发起 sUDT 交易" + "title": "发起 UDT 交易" }, "send-acp-ckb-to-new-cell": { "title": "发起 CKB 交易" @@ -953,7 +953,7 @@ "locked-asset": "已锁定", "locked-asset-tooltip": "锁定参数为 {{epochs}} epochs, 预计解锁时间为 {{year}}年{{month}}月{{day}}日(锁定时间根据区块链实际运行情况会有一定的误差)。", "withdraw-asset-tooltip": "预计解锁时间为 {{year}}年{{month}}月{{day}}日 {{hour}}时{{minute}}分(根据区块链实际运行情况会有一定的误差)。", - "user-defined-token-tooltip": "将 sUDT 资产转移到 sUDT 账户", + "user-defined-token-tooltip": "将 {{udtType}} 资产转移到 {{udtType}} 账户", "claim-asset": "领取", "withdraw-asset": "撤回", "view-details": "查看详情", @@ -964,14 +964,14 @@ "transfer-nft": "转让", "user-defined-token": "迁移", "transfer-nft-success": "转让资产成功", - "migrate-sudt-success": "转换成新的 sUDT 资产账户成功", - "send-sudt-success": "转入 sUDT 账户成功", + "migrate-sudt-success": "转换成新的 {{udtType}} 资产账户成功", + "send-sudt-success": "转入 {{udtType}} 账户成功", "unlock-success": "资产已领取,可前往交易历史查看详情", "withdraw-cheque-success": "资产已撤回,可前往交易历史查看详情", "claim-cheque-success": "资产已领取,可前往交易历史查看详情" }, "migrate-sudt": { - "title": "迁移至 sUDT 资产账户", + "title": "迁移至 {{udtType}} 资产账户", "choose-title": "选择迁移方式", "next": "下一步", "back": "上一步", @@ -979,14 +979,14 @@ "input-symbol": "请输入简称", "input-decimal": "请输入小数位", "turn-into-new-account": { - "title": "转换成新的 sUDT 资产账户", - "sub-title": "将 sUDT 资产转换成 sUDT 资产账户, 至少占用 142 CKBytes", + "title": "转换成新的 {{udtType}} 资产账户", + "sub-title": "将 {{udtType}} 资产转换成 {{udtType}} 资产账户, 至少占用 142 CKBytes", "cancel": "取消", "confirm": "确认" }, "transfer-to-exist-account": { - "title": "转入 sUDT 账户", - "sub-title": "将全部 sUDT 余额转入已存在的 sUDT 资产账户, 请确保目标账户存在" + "title": "转入 {{udtType}} 账户", + "sub-title": "将全部 {{udtType}} 余额转入已存在的 {{udtType}} 资产账户, 请确保目标账户存在" }, "cancel": "取消", "confirm": "确认", @@ -1012,6 +1012,7 @@ "select-account-type": "选择账户类型", "account-name": "账户名称", "sudt-account": "sUDT 账户", + "xudt-account": "xUDT 账户", "ckb-account": "CKB 账户", "set-token-info": "设置代币信息", "token-id": "代币ID", @@ -1032,7 +1033,7 @@ "decimal": "请输入小数位" }, "placeholder": { - "token-id": "代币 ID 即 sUDT Type Script 的 Args,等同于该 token 发行者的 lock hash。" + "token-id": "代币 ID 即 {{udtType}} Type Script 的 Args,等同于该 token 发行者的 lock hash。" } }, "send": { diff --git a/packages/neuron-ui/src/services/remote/sudt.ts b/packages/neuron-ui/src/services/remote/sudt.ts index 0faf64622c..3b78e253a8 100644 --- a/packages/neuron-ui/src/services/remote/sudt.ts +++ b/packages/neuron-ui/src/services/remote/sudt.ts @@ -1,3 +1,4 @@ +import { UDTType } from 'utils' import { remoteApi } from './remoteApiWrapper' export const getAnyoneCanPayScript = remoteApi('get-anyone-can-pay-script') @@ -21,9 +22,10 @@ export const generateSUDTTransaction = remoteApi( - 'get-hold-sudt-cell-capacity' -) +export const getHoldSUDTCellCapacity = remoteApi< + { address: string; tokenID: string; udtType?: UDTType }, + string | undefined +>('get-hold-sudt-cell-capacity') export const sendSUDTTransaction = remoteApi('send-to-anyone-can-pay') @@ -33,9 +35,4 @@ export const getSUDTTokenInfo = remoteApi('get-sudt-type-script-hash') - export const generateSudtMigrateAcpTx = remoteApi('generate-sudt-migrate-acp-tx') diff --git a/packages/neuron-ui/src/states/stateProvider/reducer.ts b/packages/neuron-ui/src/states/stateProvider/reducer.ts index 76488295fe..23995000a3 100644 --- a/packages/neuron-ui/src/states/stateProvider/reducer.ts +++ b/packages/neuron-ui/src/states/stateProvider/reducer.ts @@ -234,7 +234,7 @@ export const reducer = produce((state: Draft, action: state.sUDTAccounts = action.payload .filter(account => account.id !== undefined) .sort(sortAccounts) - .map(({ id, accountName, tokenName, symbol, tokenID, balance: accountBalance, address, decimal }) => ({ + .map(({ id, accountName, tokenName, symbol, tokenID, balance: accountBalance, address, decimal, udtType }) => ({ accountId: id!.toString(), accountName, tokenName, @@ -243,6 +243,7 @@ export const reducer = produce((state: Draft, action: address, decimal, tokenId: tokenID, + udtType, })) break } diff --git a/packages/neuron-ui/src/stories/SUDTCreateDialog.stories.tsx b/packages/neuron-ui/src/stories/SUDTCreateDialog.stories.tsx index 744a73dc7c..887c65fc41 100644 --- a/packages/neuron-ui/src/stories/SUDTCreateDialog.stories.tsx +++ b/packages/neuron-ui/src/stories/SUDTCreateDialog.stories.tsx @@ -20,9 +20,9 @@ const baseProps = { } const propsList: { [name: string]: SUDTCreateDialogProps } = { Basic: baseProps, - InsufficientForSUDT: { ...baseProps, insufficient: { ckb: false, sudt: true } }, - InsufficientForCKB: { ...baseProps, insufficient: { ckb: true, sudt: false } }, - InsufficientForCKBAndSUDT: { ...baseProps, insufficient: { ckb: true, sudt: true } }, + InsufficientForSUDT: { ...baseProps, insufficient: { ckb: false, sudt: true, xudt: true } }, + InsufficientForCKB: { ...baseProps, insufficient: { ckb: true, sudt: false, xudt: false } }, + InsufficientForCKBAndSUDT: { ...baseProps, insufficient: { ckb: true, sudt: true, xudt: true } }, } const meta: Meta = { diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index 2a237caef5..649cf62493 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -132,6 +132,11 @@ declare namespace State { amendHash?: string } + enum UDTType { + SUDT = 'sUDT', + XUDT = 'xUDT', + } + interface SUDTAccount { accountId: string accountName?: string @@ -141,6 +146,7 @@ declare namespace State { tokenId: string address: string decimal: string + udtType?: UDTType } type GlobalAlertDialog = { @@ -364,6 +370,7 @@ declare namespace State { NFTClass = 'NFTClass', NFTIssuer = 'NFTIssuer', SUDT = 'SUDT', + XUDT = 'XUDT', Spore = 'Spore', Unknown = 'Unknown', } @@ -382,7 +389,7 @@ declare namespace State { } interface LiveCellWithLocalInfo extends LiveCellWithLocalInfoAPI { lockedReason?: { key: string; params?: Record } - cellType?: 'CKB' | 'SUDT' | 'NFT' | 'Spore' | 'Unknown' + cellType?: 'CKB' | 'SUDT' | 'XUDT' | 'NFT' | 'Spore' | 'Unknown' } interface UpdateLiveCellLocalInfo { diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts index 83d7cab827..5737d4f79c 100644 --- a/packages/neuron-ui/src/types/Controller/index.d.ts +++ b/packages/neuron-ui/src/types/Controller/index.d.ts @@ -1,4 +1,9 @@ declare namespace Controller { + enum UDTType { + SUDT = 'sUDT', + XUDT = 'xUDT', + } + interface RequestOpenInExplorerParams { key: string type: 'transaction' @@ -217,6 +222,7 @@ declare namespace Controller { balance: string blake160: string address: string + udtType?: UDTType } namespace GetSUDTAccount { @@ -244,6 +250,7 @@ declare namespace Controller { symbol: string decimal: string feeRate: string + udtType?: UDTType } interface Response { assetAccount: any @@ -254,7 +261,7 @@ declare namespace Controller { namespace SendCreateSUDTAccountTransaction { interface Params { walletID: string - assetAccount: Pick + assetAccount: Pick tx: any password?: string } @@ -385,9 +392,9 @@ declare namespace Controller { } namespace GenerateClaimChequeTransaction { - type AssetAccount = Record< - 'accountName' | 'balance' | 'blake160' | 'decimal' | 'symbol' | 'tokenID' | 'tokenName', - string + type AssetAccount = Pick< + SUDTAccount, + 'accountName' | 'balance' | 'blake160' | 'decimal' | 'symbol' | 'tokenID' | 'tokenName' | 'udtType' > interface Params { diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts index 2dda7ff4d3..02dc639675 100644 --- a/packages/neuron-ui/src/utils/const.ts +++ b/packages/neuron-ui/src/utils/const.ts @@ -20,6 +20,7 @@ export const MAINNET_CLIENT_LIST = [FULL_NODE_MAINNET, LIGHT_CLIENT_MAINNET] export const MIN_DEPOSIT_AMOUNT = 102 export const TOKEN_ID_LENGTH = 66 +export const XUDT_TOKEN_ID_LENGTH = 68 export const SHANNON_CKB_RATIO = 1e8 diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts index 23c0b96b6f..2ae3a13711 100644 --- a/packages/neuron-ui/src/utils/enums.ts +++ b/packages/neuron-ui/src/utils/enums.ts @@ -126,6 +126,7 @@ export enum PresetScript { Locktime = 'SingleMultiSign', Cheque = 'Cheque', SUDT = 'SUDT', + XUDT = 'XUDT', } export enum CompensationPeriod { @@ -241,6 +242,12 @@ export enum TypeScriptCategory { NFTClass = 'NFTClass', NFTIssuer = 'NFTIssuer', SUDT = 'SUDT', + XUDT = 'XUDT', Spore = 'Spore', Unknown = 'Unknown', } + +export enum UDTType { + SUDT = 'sUDT', + XUDT = 'xUDT', +} diff --git a/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts b/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts index 3eb7e94b15..6efb93673c 100644 --- a/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts +++ b/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts @@ -3,15 +3,12 @@ import { TFunction } from 'i18next' import { useEffect, useCallback } from 'react' import { AccountType, TokenInfo } from 'components/SUDTCreateDialog' import { AppActions, StateAction } from 'states' -import { - generateCreateSUDTAccountTransaction, - openExternal, - getSUDTTypeScriptHash, - invokeShowErrorMessage, -} from 'services/remote' +import { generateCreateSUDTAccountTransaction, openExternal, invokeShowErrorMessage } from 'services/remote' import { getExplorerUrl } from 'utils' +import { predefined } from '@ckb-lumos/config-manager' +import { utils } from '@ckb-lumos/base' import useGetCountDownAndFeeRateStats from './useGetCountDownAndFeeRateStats' -import { ErrorCode } from '../enums' +import { ErrorCode, UDTType } from '../enums' import { isSuccessResponse } from '../is' import { MIN_CKB_REQUIRED_BY_CKB_SUDT, @@ -46,6 +43,7 @@ export const useIsInsufficientToCreateSUDTAccount = ({ symbol: DEFAULT_SUDT_FIELDS.symbol, decimal: '0', feeRate: `${suggestFeeRate}`, + udtType: UDTType.SUDT, } return generateCreateSUDTAccountTransaction(params).catch(() => false) } @@ -77,6 +75,7 @@ export const useIsInsufficientToCreateSUDTAccount = ({ setInsufficient({ [AccountType.CKB]: insufficientToCreateCKBAccount, [AccountType.SUDT]: insufficientToCreateSUDTAccount, + [AccountType.XUDT]: insufficientToCreateSUDTAccount, }) }) }, [walletId, balance, setInsufficient, suggestFeeRate]) @@ -94,7 +93,10 @@ export const useOnGenerateNewAccountTransaction = ({ t: TFunction }) => useCallback( - ({ tokenId, tokenName, accountName, symbol, decimal }: TokenInfo, onSuccess?: () => void) => { + ( + { tokenId, tokenName, accountName, symbol, decimal, udtType }: TokenInfo & { udtType?: UDTType }, + onSuccess?: () => void + ) => { return generateCreateSUDTAccountTransaction({ walletID: walletId, tokenID: tokenId, @@ -103,6 +105,7 @@ export const useOnGenerateNewAccountTransaction = ({ symbol, decimal, feeRate: `${MEDIUM_FEE_RATE}`, + udtType, }) .then(res => { if (isSuccessResponse(res)) { @@ -131,15 +134,18 @@ export const useOnGenerateNewAccountTransaction = ({ [onGenerated, walletId, dispatch, t] ) -export const useOpenSUDTTokenUrl = (tokenID: string, isMainnet?: boolean) => +export const useOpenUDTTokenUrl = (tokenID: string, udtType?: UDTType, isMainnet?: boolean) => useCallback(() => { - if (tokenID) { - getSUDTTypeScriptHash({ tokenID }).then(res => { - if (isSuccessResponse(res) && res.result) { - openExternal(`${getExplorerUrl(isMainnet)}/sudt/${res.result}`) - } + if (tokenID && udtType) { + const { SUDT, XUDT } = isMainnet ? predefined.LINA.SCRIPTS : predefined.AGGRON4.SCRIPTS + const udtScript = udtType === UDTType.SUDT ? SUDT : XUDT + const scriptHash = utils.computeScriptHash({ + codeHash: udtScript.CODE_HASH, + hashType: udtScript.HASH_TYPE, + args: tokenID, }) + openExternal(`${getExplorerUrl(isMainnet)}/${udtType === UDTType.SUDT ? 'sudt' : 'xudt'}/${scriptHash}`) } }, [isMainnet, tokenID]) -export default { useIsInsufficientToCreateSUDTAccount, useOnGenerateNewAccountTransaction, useOpenSUDTTokenUrl } +export default { useIsInsufficientToCreateSUDTAccount, useOnGenerateNewAccountTransaction, useOpenUDTTokenUrl } diff --git a/packages/neuron-ui/src/utils/hooks/index.ts b/packages/neuron-ui/src/utils/hooks/index.ts index 9496c06422..82a29776f7 100644 --- a/packages/neuron-ui/src/utils/hooks/index.ts +++ b/packages/neuron-ui/src/utils/hooks/index.ts @@ -13,7 +13,7 @@ import { showPageNotice, useDispatch, } from 'states' -import { epochParser, isReadyByVersion, calculateClaimEpochValue, CONSTANTS, isSuccessResponse } from 'utils' +import { epochParser, isReadyByVersion, calculateClaimEpochValue, CONSTANTS, isSuccessResponse, UDTType } from 'utils' import { validateTokenId, validateAssetAccountName, @@ -282,6 +282,7 @@ export const useSUDTAccountInfoErrors = ({ existingAccountNames, isCKB, t, + udtType, }: { info: { accountName: string @@ -294,6 +295,7 @@ export const useSUDTAccountInfoErrors = ({ existingAccountNames: string[] isCKB: boolean t: TFunction + udtType?: UDTType }) => useMemo(() => { const tokenErrors = { @@ -311,7 +313,7 @@ export const useSUDTAccountInfoErrors = ({ validator: validateAssetAccountName, }, symbol: { params: { symbol, isCKB }, validator: validateSymbol }, - tokenId: { params: { tokenId, isCKB }, validator: validateTokenId }, + tokenId: { params: { tokenId, isCKB, udtType }, validator: validateTokenId }, tokenName: { params: { tokenName, isCKB }, validator: validateTokenName }, decimal: { params: { decimal }, validator: validateDecimal }, balance: { params: { balance }, validator: typeof balance === 'undefined' ? () => {} : validateDecimal }, diff --git a/packages/neuron-ui/src/utils/is.ts b/packages/neuron-ui/src/utils/is.ts index 5a82900882..1e53b008b2 100644 --- a/packages/neuron-ui/src/utils/is.ts +++ b/packages/neuron-ui/src/utils/is.ts @@ -7,7 +7,10 @@ import { AnyoneCanPayLockInfoOnLina, PwAcpLockInfoOnMainNet, PwAcpLockInfoOnTestNet, + UDTType, } from 'utils/enums' +import { type Script } from '@ckb-lumos/base' +import { predefined } from '@ckb-lumos/config-manager' import { MAINNET_CLIENT_LIST } from './const' export const isMainnet = (networks: Readonly, networkID: string) => { @@ -62,3 +65,28 @@ export const isAnyoneCanPayAddress = (address: string, isMainnetAddress: boolean return false } } + +// Add string type to recognize the return type of API from https://github.com/nervosnetwork/neuron/blob/v0.116.2/packages/neuron-wallet/src/models/chain/output.ts#L20 +// TODO, make the UDT code hash globally configurable to ensure it can test with the devnet +export const getUdtType = (type: Script | string | null) => { + if (type === null) return undefined + if (typeof type === 'string') { + switch (type) { + case UDTType.SUDT: + case UDTType.XUDT: + return type + default: + return undefined + } + } + switch (type.codeHash) { + case predefined.AGGRON4.SCRIPTS.SUDT.CODE_HASH: + case predefined.LINA.SCRIPTS.SUDT.CODE_HASH: + return UDTType.SUDT + case predefined.AGGRON4.SCRIPTS.XUDT.CODE_HASH: + case predefined.LINA.SCRIPTS.XUDT.CODE_HASH: + return UDTType.XUDT + default: + return undefined + } +} diff --git a/packages/neuron-ui/src/utils/validators/tokenId.ts b/packages/neuron-ui/src/utils/validators/tokenId.ts index dfd64c96d7..7bdb5fed1a 100644 --- a/packages/neuron-ui/src/utils/validators/tokenId.ts +++ b/packages/neuron-ui/src/utils/validators/tokenId.ts @@ -1,14 +1,17 @@ -import { DEFAULT_SUDT_FIELDS, TOKEN_ID_LENGTH } from 'utils/const' +import { DEFAULT_SUDT_FIELDS, TOKEN_ID_LENGTH, XUDT_TOKEN_ID_LENGTH } from 'utils/const' import { FieldRequiredException, FieldInvalidException } from 'exceptions' +import { UDTType } from 'utils/enums' export const validateTokenId = ({ tokenId, isCKB = false, required = false, + udtType, }: { tokenId: string isCKB: boolean required: boolean + udtType?: UDTType }) => { if (!tokenId) { if (required) { @@ -22,7 +25,9 @@ export const validateTokenId = ({ return true } - if (!isCKB && tokenId.startsWith('0x') && tokenId.length === TOKEN_ID_LENGTH && !Number.isNaN(+tokenId)) { + const tokenLength = udtType === UDTType.SUDT ? [TOKEN_ID_LENGTH] : [TOKEN_ID_LENGTH, XUDT_TOKEN_ID_LENGTH] + + if (!isCKB && tokenId.startsWith('0x') && tokenLength.includes(tokenId.length) && !Number.isNaN(+tokenId)) { return true } diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/light-synchronizer.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/light-synchronizer.ts index 765f69c41e..aafa963859 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/light-synchronizer.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/light-synchronizer.ts @@ -42,6 +42,7 @@ export default class LightSynchronizer extends Synchronizer { const fetchCellDeps = [ assetAccountInfo.anyoneCanPayCellDep, assetAccountInfo.sudtCellDep, + assetAccountInfo.xudtCellDep, assetAccountInfo.getNftClassInfo().cellDep, assetAccountInfo.getNftInfo().cellDep, assetAccountInfo.getNftIssuerInfo().cellDep, diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts index 3d471ea54e..890cbac985 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/queue.ts @@ -133,8 +133,8 @@ export default class Queue { for (let index = 0; index < txsWithStatus.length; index++) { if (txsWithStatus[index]?.transaction) { const tx = Transaction.fromSDK(txsWithStatus[index].transaction) - tx.blockHash = txsWithStatus[index].txStatus.blockHash! - blockHashes.push(tx.blockHash) + tx.blockHash = txsWithStatus[index].txStatus.blockHash + blockHashes.push(tx.blockHash!) txs.push(tx) } else { if ((txsWithStatus[index].txStatus as any) === 'rejected') { @@ -231,7 +231,7 @@ export default class Queue { } await TransactionPersistor.saveFetchTx(tx, this.#lockArgsSet) for (const info of anyoneCanPayInfos) { - await AssetAccountService.checkAndSaveAssetAccountWhenSync(info.tokenID, info.blake160) + await AssetAccountService.checkAndSaveAssetAccountWhenSync(info.tokenID, info.blake160, info.udtType) } await this.#checkAndGenerateAddressesByTx(tx) diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/tx-address-finder.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/tx-address-finder.ts index 200aaeded3..8cb87fffd7 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/tx-address-finder.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/tx-address-finder.ts @@ -6,10 +6,13 @@ import OutPoint from '../../models/chain/out-point' import Transaction from '../../models/chain/transaction' import SystemScriptInfo from '../../models/system-script-info' import { getConnection } from '../../database/chain/connection' +import { UDTType } from '../../utils/const' +import AssetAccountInfo from '../../models/asset-account-info' export interface AnyoneCanPayInfo { tokenID: string blake160: string + udtType?: UDTType } // Search for all addresses related to a transaction. These addresses include: @@ -47,6 +50,7 @@ export default class TxAddressFinder { let shouldSync = false // const anyoneCanPayBlake160s: string[] = [] const anyoneCanPayInfos: AnyoneCanPayInfo[] = [] + const assetAccountInfo = new AssetAccountInfo() const outputs: Output[] = this.tx .outputs!.map((output, index) => { if (SystemScriptInfo.isMultiSignScript(output.lock)) { @@ -62,6 +66,11 @@ export default class TxAddressFinder { anyoneCanPayInfos.push({ blake160: output.lock.args, tokenID: output.type?.args || 'CKBytes', + udtType: output.type + ? assetAccountInfo.isSudtScript(output.type) + ? UDTType.SUDT + : UDTType.XUDT + : undefined, }) } if (this.lockHashes.has(output.lockHash!)) { @@ -86,6 +95,7 @@ export default class TxAddressFinder { const anyoneCanPayInfos: AnyoneCanPayInfo[] = [] const inputs = this.tx.inputs!.filter(i => i.previousOutput !== null) const isMainnet = NetworksService.getInstance().isMainnet() + const assetAccountInfo = new AssetAccountInfo() let shouldSync = false for (const input of inputs) { @@ -100,6 +110,11 @@ export default class TxAddressFinder { anyoneCanPayInfos.push({ blake160: output.lockArgs, tokenID: output.typeArgs || 'CKBytes', + udtType: output.typeScript() + ? assetAccountInfo.isSudtScript(output.typeScript()!) + ? UDTType.SUDT + : UDTType.XUDT + : undefined, }) } if (output && this.lockHashes.has(output.lockHash)) { diff --git a/packages/neuron-wallet/src/controllers/anyone-can-pay.ts b/packages/neuron-wallet/src/controllers/anyone-can-pay.ts index df092470bf..9848f808ea 100644 --- a/packages/neuron-wallet/src/controllers/anyone-can-pay.ts +++ b/packages/neuron-wallet/src/controllers/anyone-can-pay.ts @@ -1,7 +1,7 @@ import AssetAccountInfo from '../models/asset-account-info' import Transaction from '../models/chain/transaction' import { ServiceHasNoResponse } from '../exceptions' -import { ResponseCode } from '../utils/const' +import { ResponseCode, UDTType } from '../utils/const' import AnyoneCanPayService from '../services/anyone-can-pay' import TransactionSender from '../services/transaction-sender' import { set as setDescription } from '../services/tx/transaction-description' @@ -49,10 +49,11 @@ export default class AnyoneCanPayController { public async getHoldSudtCellCapacity( receiveAddress: string, - tokenID: string + tokenID: string, + udtType?: UDTType ): Promise> { const lockScript = AddressParser.parse(receiveAddress) - const extraCKB = await AnyoneCanPayService.getHoldSUDTCellCapacity(lockScript, tokenID) + const extraCKB = await AnyoneCanPayService.getHoldSUDTCellCapacity(lockScript, tokenID, udtType) return { status: ResponseCode.Success, result: extraCKB, diff --git a/packages/neuron-wallet/src/controllers/api.ts b/packages/neuron-wallet/src/controllers/api.ts index cecdf4a039..d60b6325af 100644 --- a/packages/neuron-wallet/src/controllers/api.ts +++ b/packages/neuron-wallet/src/controllers/api.ts @@ -24,7 +24,7 @@ import { ConnectionStatusSubject } from '../models/subjects/node' import NetworksService from '../services/networks' import WalletsService from '../services/wallets' import SettingsService, { Locale } from '../services/settings' -import { ResponseCode } from '../utils/const' +import { ResponseCode, UDTType } from '../utils/const' import { clean as cleanChain } from '../database/chain' import WalletsController from '../controllers/wallets' import TransactionsController from '../controllers/transactions' @@ -801,9 +801,12 @@ export default class ApiController { return this.#anyoneCanPayController.generateTx(params) }) - handle('get-hold-sudt-cell-capacity', async (_, params: { address: string; tokenID: string }) => { - return this.#anyoneCanPayController.getHoldSudtCellCapacity(params.address, params.tokenID) - }) + handle( + 'get-hold-sudt-cell-capacity', + async (_, params: { address: string; tokenID: string; udtType?: UDTType }) => { + return this.#anyoneCanPayController.getHoldSudtCellCapacity(params.address, params.tokenID, params.udtType) + } + ) handle('send-to-anyone-can-pay', async (_, params: SendAnyoneCanPayTxParams) => { return this.#anyoneCanPayController.sendTx(params) @@ -817,10 +820,6 @@ export default class ApiController { return this.#sudtController.getSUDTTokenInfo(params) }) - handle('get-sudt-type-script-hash', async (_, params: { tokenID: string }) => { - return this.#sudtController.getSUDTTypeScriptHash(params) - }) - handle('generate-destroy-asset-account-tx', async (_, params: { walletID: string; id: number }) => { return this.#assetAccountController.destroyAssetAccount(params) }) diff --git a/packages/neuron-wallet/src/controllers/asset-account.ts b/packages/neuron-wallet/src/controllers/asset-account.ts index 1469996125..1fefd3e22e 100644 --- a/packages/neuron-wallet/src/controllers/asset-account.ts +++ b/packages/neuron-wallet/src/controllers/asset-account.ts @@ -3,7 +3,7 @@ import AssetAccount from '../models/asset-account' import Transaction from '../models/chain/transaction' import AssetAccountService from '../services/asset-account-service' import { ServiceHasNoResponse } from '../exceptions' -import { ResponseCode } from '../utils/const' +import { ResponseCode, UDTType } from '../utils/const' import NetworksService from '../services/networks' import AssetAccountInfo from '../models/asset-account-info' import TransactionSender from '../services/transaction-sender' @@ -22,6 +22,7 @@ export interface GenerateCreateAssetAccountTxParams { decimal: string feeRate: string fee: string + udtType?: UDTType } export interface SendCreateAssetAccountTxParams { @@ -137,16 +138,7 @@ export default class AssetAccountController { tx: Transaction }> > { - const result = await AssetAccountService.generateCreateTx( - params.walletID, - params.tokenID, - params.symbol, - params.accountName, - params.tokenName, - params.decimal, - params.feeRate, - params.fee - ) + const result = await AssetAccountService.generateCreateTx(params) if (!result) { throw new ServiceHasNoResponse('AssetAccount') diff --git a/packages/neuron-wallet/src/controllers/sudt.ts b/packages/neuron-wallet/src/controllers/sudt.ts index ad25bbdb6a..c643890635 100644 --- a/packages/neuron-wallet/src/controllers/sudt.ts +++ b/packages/neuron-wallet/src/controllers/sudt.ts @@ -1,4 +1,3 @@ -import { computeScriptHash as scriptToHash } from '@ckb-lumos/lumos/utils' import LiveCellService from '../services/live-cell-service' import AssetAccountInfo from '../models/asset-account-info' import Script, { ScriptHashType } from '../models/chain/script' @@ -35,13 +34,4 @@ export default class SUDTController { result: { tokenID: params.tokenID, symbol: symbol, tokenName: name, decimal: decimal }, } } - - public getSUDTTypeScriptHash(params: { tokenID: string }): Controller.Response { - const assetAccount = new AssetAccountInfo() - const script = new Script(assetAccount.infos.sudt.codeHash, params.tokenID, assetAccount.infos.sudt.hashType) - return { - status: ResponseCode.Success, - result: scriptToHash(script.toSDK()), - } - } } diff --git a/packages/neuron-wallet/src/database/chain/entities/asset-account.ts b/packages/neuron-wallet/src/database/chain/entities/asset-account.ts index d59d67ecd4..009e30b8e3 100644 --- a/packages/neuron-wallet/src/database/chain/entities/asset-account.ts +++ b/packages/neuron-wallet/src/database/chain/entities/asset-account.ts @@ -1,9 +1,10 @@ import { Entity, Column, PrimaryGeneratedColumn, Index, ManyToOne, JoinColumn } from 'typeorm' import AssetAccountModel from '../../../models/asset-account' import SudtTokenInfo from './sudt-token-info' +import { UDTType } from '../../../utils/const' @Entity() -@Index(['tokenID', 'blake160'], { unique: true }) +@Index(['tokenID', 'blake160', 'udtType'], { unique: true }) export default class AssetAccount { @PrimaryGeneratedColumn() id!: number @@ -13,6 +14,12 @@ export default class AssetAccount { }) tokenID!: string + @Column({ + type: 'varchar', + nullable: true, + }) + udtType?: UDTType + @Column({ type: 'varchar', default: '', @@ -40,12 +47,14 @@ export default class AssetAccount { assetAccount.accountName = info.accountName assetAccount.balance = info.balance assetAccount.blake160 = info.blake160 + assetAccount.udtType = info.udtType const sudtTokenInfo = new SudtTokenInfo() sudtTokenInfo.tokenID = info.tokenID sudtTokenInfo.symbol = info.symbol sudtTokenInfo.tokenName = info.tokenName sudtTokenInfo.decimal = info.decimal + sudtTokenInfo.udtType = info.udtType assetAccount.sudtTokenInfo = sudtTokenInfo return assetAccount @@ -60,7 +69,8 @@ export default class AssetAccount { this.sudtTokenInfo.decimal, this.balance, this.blake160, - this.id + this.id, + this.udtType ) } } diff --git a/packages/neuron-wallet/src/database/chain/entities/sudt-token-info.ts b/packages/neuron-wallet/src/database/chain/entities/sudt-token-info.ts index ec0614395b..35c6512f3a 100644 --- a/packages/neuron-wallet/src/database/chain/entities/sudt-token-info.ts +++ b/packages/neuron-wallet/src/database/chain/entities/sudt-token-info.ts @@ -1,14 +1,21 @@ import { Entity, Column, Index, OneToMany, PrimaryColumn } from 'typeorm' import AssetAccount from './asset-account' +import { UDTType } from '../../../utils/const' @Entity() -@Index(['tokenID'], { unique: true }) +@Index(['tokenID', 'udtType'], { unique: true }) export default class SudtTokenInfo { @PrimaryColumn({ type: 'varchar', }) tokenID!: string + @Column({ + type: 'varchar', + nullable: true, + }) + udtType?: UDTType + @Column({ type: 'varchar', }) @@ -33,6 +40,7 @@ export default class SudtTokenInfo { tokenName: this.tokenName, symbol: this.symbol, decimal: this.decimal, + udtType: this.udtType, } } } diff --git a/packages/neuron-wallet/src/database/chain/migrations/1720089814860-AddUdtType.ts b/packages/neuron-wallet/src/database/chain/migrations/1720089814860-AddUdtType.ts new file mode 100644 index 0000000000..b30e1bf7f3 --- /dev/null +++ b/packages/neuron-wallet/src/database/chain/migrations/1720089814860-AddUdtType.ts @@ -0,0 +1,22 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class AddUdtType1720089814860 implements MigrationInterface { + name = 'AddUdtType1720089814860' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sudt_token_info" ADD COLUMN "udtType" varchar;`) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_37bb4f2a4a849bf0b1dadee2c7" ON "sudt_token_info" ("tokenID", "udtType") `); + await queryRunner.query(`UPDATE "sudt_token_info" set udtType="sUDT" where tokenID!="CKBytes"`) + await queryRunner.query(`ALTER TABLE "asset_account" ADD COLUMN "udtType" varchar;`) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5139df6b311e63ecdd93cd17ed" ON "asset_account" ("tokenID", "blake160", "udtType") `); + await queryRunner.query(`UPDATE "asset_account" set udtType="sUDT" where tokenID!="CKBytes"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_37bb4f2a4a849bf0b1dadee2c7"`); + await queryRunner.query(`ALTER TABLE "sudt_token_info" DROP COLUMN "udtType";`); + await queryRunner.query(`DROP INDEX "IDX_5139df6b311e63ecdd93cd17ed"`); + await queryRunner.query(`ALTER TABLE "asset_account" DROP COLUMN "udtType";`); + } + +} diff --git a/packages/neuron-wallet/src/database/chain/ormconfig.ts b/packages/neuron-wallet/src/database/chain/ormconfig.ts index c3458375cd..b8699466d2 100644 --- a/packages/neuron-wallet/src/database/chain/ormconfig.ts +++ b/packages/neuron-wallet/src/database/chain/ormconfig.ts @@ -69,6 +69,7 @@ import TxDescriptionSubscribe from './subscriber/tx-description-subscriber' import SudtTokenInfoSubscribe from './subscriber/sudt-token-info-subscriber' import AssetAccountSubscribe from './subscriber/asset-account-subscriber' import { AddStartBlockNumber1716539079505 } from './migrations/1716539079505-AddStartBlockNumber' +import { AddUdtType1720089814860 } from './migrations/1720089814860-AddUdtType' export const CONNECTION_NOT_FOUND_NAME = 'ConnectionNotFoundError' export type ConnectionName = 'light' | 'full' @@ -186,6 +187,7 @@ const getConnectionOptions = (genesisBlockHash: string, connectionName: Connecti RemoveAddressInIndexerCache1704357651876, AmendTransaction1709008125088, AddStartBlockNumber1716539079505, + AddUdtType1720089814860, ], subscribers: [ AddressSubscribe, diff --git a/packages/neuron-wallet/src/models/asset-account-info.ts b/packages/neuron-wallet/src/models/asset-account-info.ts index 4293a40cbc..1cd76177ba 100644 --- a/packages/neuron-wallet/src/models/asset-account-info.ts +++ b/packages/neuron-wallet/src/models/asset-account-info.ts @@ -1,4 +1,5 @@ import { bytes, struct, createFixedBytesCodec } from '@ckb-lumos/lumos/codec' +import { predefined } from '@ckb-lumos/config-manager' import CellDep, { DepType } from './chain/cell-dep' import Script, { ScriptHashType } from './chain/script' import OutPoint from './chain/out-point' @@ -6,6 +7,7 @@ import NetworksService from '../services/networks' import Transaction from './chain/transaction' import SystemScriptInfo from './system-script-info' import { Address } from './address' +import { UDTType } from '../utils/const' import { predefinedSporeConfigs, SporeConfig, SporeScript } from '@spore-sdk/core' const createFixedHexBytesCodec = (byteLength: number) => @@ -27,6 +29,7 @@ export default class AssetAccountInfo { private nftIssuerInfo: ScriptCellInfo private nftClassInfo: ScriptCellInfo private nftInfo: ScriptCellInfo + private xudt: ScriptCellInfo private sporeInfos: ScriptCellInfo[] private sporeClusterInfos: ScriptCellInfo[] @@ -39,19 +42,32 @@ export default class AssetAccountInfo { sudt: this.sudt, sudtInfo: this.sudtInfo, anyoneCanPay: this.anyoneCanPayInfo, + xudt: this.xudt, } } constructor(genesisBlockHash: string = NetworksService.getInstance().getCurrent().genesisHash) { - if (genesisBlockHash === AssetAccountInfo.MAINNET_GENESIS_BLOCK_HASH) { - this.sudt = { - cellDep: new CellDep( - new OutPoint(process.env.MAINNET_SUDT_DEP_TXHASH!, process.env.MAINNET_SUDT_DEP_INDEX!), - process.env.MAINNET_SUDT_DEP_TYPE! as DepType - ), - codeHash: process.env.MAINNET_SUDT_SCRIPT_CODEHASH!, - hashType: process.env.MAINNET_SUDT_SCRIPT_HASHTYPE! as ScriptHashType, - } + const isMainnet = genesisBlockHash === AssetAccountInfo.MAINNET_GENESIS_BLOCK_HASH + const { XUDT, SUDT, ANYONE_CAN_PAY } = isMainnet ? predefined.LINA.SCRIPTS : predefined.AGGRON4.SCRIPTS + this.xudt = { + cellDep: new CellDep(new OutPoint(XUDT.TX_HASH, XUDT.INDEX), XUDT.DEP_TYPE as DepType), + codeHash: XUDT.CODE_HASH, + hashType: XUDT.HASH_TYPE as ScriptHashType, + } + this.sudt = { + cellDep: new CellDep(new OutPoint(SUDT.TX_HASH, SUDT.INDEX), SUDT.DEP_TYPE as DepType), + codeHash: SUDT.CODE_HASH, + hashType: SUDT.HASH_TYPE as ScriptHashType, + } + this.anyoneCanPayInfo = { + cellDep: new CellDep( + new OutPoint(ANYONE_CAN_PAY.TX_HASH, ANYONE_CAN_PAY.INDEX), + ANYONE_CAN_PAY.DEP_TYPE as DepType + ), + codeHash: ANYONE_CAN_PAY.CODE_HASH, + hashType: ANYONE_CAN_PAY.HASH_TYPE as ScriptHashType, + } + if (isMainnet) { this.sudtInfo = { cellDep: new CellDep( new OutPoint(process.env.MAINNET_SUDT_INFO_DEP_TXHASH!, process.env.MAINNET_SUDT_INFO_DEP_INDEX!), @@ -60,14 +76,6 @@ export default class AssetAccountInfo { codeHash: process.env.MAINNET_SUDT_INFO_SCRIPT_CODEHASH!, hashType: process.env.MAINNET_SUDT_INFO_SCRIPT_HASHTYPE! as ScriptHashType, } - this.anyoneCanPayInfo = { - cellDep: new CellDep( - new OutPoint(process.env.MAINNET_ACP_DEP_TXHASH!, process.env.MAINNET_ACP_DEP_INDEX!), - process.env.MAINNET_ACP_DEP_TYPE! as DepType - ), - codeHash: process.env.MAINNET_ACP_SCRIPT_CODEHASH!, - hashType: process.env.MAINNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType, - } this.legacyAnyoneCanPayInfo = { cellDep: new CellDep( new OutPoint(process.env.LEGACY_MAINNET_ACP_DEP_TXHASH!, process.env.LEGACY_MAINNET_ACP_DEP_INDEX!), @@ -116,19 +124,10 @@ export default class AssetAccountInfo { codeHash: process.env.MAINNET_NFT_SCRIPT_CODEHASH!, hashType: process.env.MAINNET_NFT_SCRIPT_HASH_TYPE! as ScriptHashType, } - // TODO infos for mainnet this.sporeInfos = [] this.sporeClusterInfos = [] } else { - this.sudt = { - cellDep: new CellDep( - new OutPoint(process.env.TESTNET_SUDT_DEP_TXHASH!, process.env.TESTNET_SUDT_DEP_INDEX!), - process.env.TESTNET_SUDT_DEP_TYPE! as DepType - ), - codeHash: process.env.TESTNET_SUDT_SCRIPT_CODEHASH!, - hashType: process.env.TESTNET_SUDT_SCRIPT_HASHTYPE! as ScriptHashType, - } this.sudtInfo = { cellDep: new CellDep( new OutPoint(process.env.TESTNET_SUDT_INFO_DEP_TXHASH!, process.env.TESTNET_SUDT_INFO_DEP_INDEX!), @@ -137,14 +136,6 @@ export default class AssetAccountInfo { codeHash: process.env.TESTNET_SUDT_INFO_SCRIPT_CODEHASH!, hashType: process.env.TESTNET_SUDT_INFO_SCRIPT_HASHTYPE! as ScriptHashType, } - this.anyoneCanPayInfo = { - cellDep: new CellDep( - new OutPoint(process.env.TESTNET_ACP_DEP_TXHASH!, process.env.TESTNET_ACP_DEP_INDEX!), - process.env.TESTNET_ACP_DEP_TYPE! as DepType - ), - codeHash: process.env.TESTNET_ACP_SCRIPT_CODEHASH!, - hashType: process.env.TESTNET_ACP_SCRIPT_HASHTYPE! as ScriptHashType, - } this.legacyAnyoneCanPayInfo = { cellDep: new CellDep( new OutPoint(process.env.LEGACY_TESTNET_ACP_DEP_TXHASH!, process.env.LEGACY_TESTNET_ACP_DEP_INDEX!), @@ -213,6 +204,10 @@ export default class AssetAccountInfo { return this.anyoneCanPayInfo.cellDep } + public get xudtCellDep(): CellDep { + return this.xudt.cellDep + } + public get anyoneCanPayCodeHash(): string { return this.anyoneCanPayInfo.codeHash } @@ -245,10 +240,6 @@ export default class AssetAccountInfo { return this.sporeClusterInfos } - public getAcpCodeHash(): string { - return this.anyoneCanPayInfo.codeHash - } - public getSudtCodeHash(): string { return this.sudt.codeHash } @@ -273,10 +264,29 @@ export default class AssetAccountInfo { return new Script(info.codeHash, args, info.hashType) } + public generateXudtScript(args: string): Script { + return new Script(this.xudt.codeHash, args, this.xudt.hashType) + } + + public generateUdtScript(args: string, udtType?: UDTType): Script | undefined { + switch (udtType) { + case UDTType.SUDT: + return this.generateSudtScript(args) + case UDTType.XUDT: + return this.generateXudtScript(args) + default: + return undefined + } + } + public isSudtScript(script: Script): boolean { return script.codeHash === this.sudt.codeHash && script.hashType === this.sudt.hashType } + public isXudtScript(script: Script): boolean { + return script.codeHash === this.xudt.codeHash && script.hashType === this.xudt.hashType + } + public isAnyoneCanPayScript(script: Script): boolean { const acpScripts = [this.anyoneCanPayInfo, this.pwAnyoneCanPayInfo] const exist = acpScripts.find(acpScript => { diff --git a/packages/neuron-wallet/src/models/asset-account.ts b/packages/neuron-wallet/src/models/asset-account.ts index 38f295423b..06e3c09ea8 100644 --- a/packages/neuron-wallet/src/models/asset-account.ts +++ b/packages/neuron-wallet/src/models/asset-account.ts @@ -1,3 +1,5 @@ +import { UDTType } from '../utils/const' + export default class AssetAccount { public id?: number public tokenID: string @@ -7,6 +9,7 @@ export default class AssetAccount { public decimal: string public balance: string public blake160: string + public udtType?: UDTType constructor( tokenID: string, @@ -16,7 +19,8 @@ export default class AssetAccount { decimal: string, balance: string, blake160: string, - id?: number + id?: number, + udtType?: UDTType ) { this.tokenID = tokenID this.symbol = symbol @@ -26,6 +30,7 @@ export default class AssetAccount { this.balance = balance this.blake160 = blake160 this.id = id + this.udtType = udtType } public static fromObject(params: { @@ -37,6 +42,7 @@ export default class AssetAccount { balance: string blake160: string id?: number + udtType?: UDTType }): AssetAccount { return new AssetAccount( params.tokenID, @@ -46,7 +52,8 @@ export default class AssetAccount { params.decimal, params.balance, params.blake160, - params.id + params.id, + params.udtType ) } } diff --git a/packages/neuron-wallet/src/models/chain/output.ts b/packages/neuron-wallet/src/models/chain/output.ts index 53ee2579a5..3d7804c28c 100644 --- a/packages/neuron-wallet/src/models/chain/output.ts +++ b/packages/neuron-wallet/src/models/chain/output.ts @@ -1,8 +1,9 @@ import Script from './script' import OutPoint from './out-point' import { bytes as byteUtils } from '@ckb-lumos/lumos/codec' -import { BI } from '@ckb-lumos/lumos' +import { BI, helpers } from '@ckb-lumos/lumos' import TypeChecker from '../../utils/type-checker' +import { MIN_CELL_CAPACITY } from '../../utils/const' // sent: pending transaction's output // pending: pending transaction's input @@ -109,7 +110,7 @@ export default class Output { depositTimestamp, multiSignBlake160, }: { - capacity: string + capacity?: string data?: string lock: Script type?: Script | null @@ -126,7 +127,18 @@ export default class Output { multiSignBlake160?: string | null }): Output { return new Output( - capacity, + capacity ?? + helpers + .minimalCellCapacity({ + cellOutput: { + // use MIN_CELL_CAPACITY to place holder + capacity: MIN_CELL_CAPACITY.toString(), + lock: Script.fromObject(lock), + type: type ? Script.fromObject(type) : undefined, + }, + data: data ?? '0x', + }) + .toString(), Script.fromObject(lock), type ? Script.fromObject(type) : type, data, @@ -227,4 +239,15 @@ export default class Output { output.type ? Script.fromSDK(output.type) : output.type ) } + + public minimalCellCapacity() { + return helpers.minimalCellCapacity({ + cellOutput: { + capacity: this.capacity, + lock: this.lock, + type: this.type ?? undefined, + }, + data: this.data, + }) + } } diff --git a/packages/neuron-wallet/src/models/chain/transaction.ts b/packages/neuron-wallet/src/models/chain/transaction.ts index 762d4b5312..1dcf73993c 100644 --- a/packages/neuron-wallet/src/models/chain/transaction.ts +++ b/packages/neuron-wallet/src/models/chain/transaction.ts @@ -42,6 +42,7 @@ export interface NFTInfo { export enum AssetAccountType { CKB = 'CKB', SUDT = 'sUDT', + XUDT = 'xUDT', } export default class Transaction { diff --git a/packages/neuron-wallet/src/services/anyone-can-pay.ts b/packages/neuron-wallet/src/services/anyone-can-pay.ts index bfe9b8af33..054fbbc625 100644 --- a/packages/neuron-wallet/src/services/anyone-can-pay.ts +++ b/packages/neuron-wallet/src/services/anyone-can-pay.ts @@ -18,9 +18,11 @@ import LiveCellService from './live-cell-service' import WalletService from './wallets' import SystemScriptInfo from '../models/system-script-info' import CellsService from './cells' -import { MIN_SUDT_CAPACITY } from '../utils/const' +import { MIN_SUDT_CAPACITY, UDTType } from '../utils/const' import NetworksService from './networks' import { NetworkType } from '../models/network' +import BufferUtils from '../utils/buffer' +import { helpers } from '@ckb-lumos/lumos' export default class AnyoneCanPayService { public static async generateAnyoneCanPayTx( @@ -57,7 +59,7 @@ export default class AnyoneCanPayService { const targetOutput = isCKB ? await AnyoneCanPayService.getCKBTargetOutput(targetLockScript) - : await AnyoneCanPayService.getSUDTTargetOutput(targetLockScript, tokenID) + : await AnyoneCanPayService.getSUDTTargetOutput(targetLockScript, tokenID, assetAccount.udtType!) const wallet = WalletService.getInstance().get(walletID) const changeBlake160: string = (await wallet.getNextChangeAddress())!.blake160 @@ -112,18 +114,19 @@ export default class AnyoneCanPayService { throw new TargetLockError() } - private static async getSUDTTargetOutput(lockScript: Script, tokenID: string) { + private static async getSUDTTargetOutput(lockScript: Script, tokenID: string, udtType: UDTType) { if (SystemScriptInfo.isSecpScript(lockScript)) { return Output.fromObject({ - capacity: BigInt(MIN_SUDT_CAPACITY).toString(), lock: lockScript, - type: new AssetAccountInfo().generateSudtScript(tokenID), + type: new AssetAccountInfo().generateUdtScript(tokenID, udtType), + // use amount 0 to place holder amount + data: BufferUtils.writeBigUInt128LE(BigInt(0)), }) } const liveCellService = LiveCellService.getInstance() const targetOutputLiveCell: LiveCell | null = await liveCellService.getOneByLockScriptAndTypeScript( lockScript, - new AssetAccountInfo().generateSudtScript(tokenID) + new AssetAccountInfo().generateUdtScript(tokenID, udtType)! ) if (targetOutputLiveCell && new AssetAccountInfo().isAnyoneCanPayScript(lockScript)) { return Output.fromObject({ @@ -136,29 +139,38 @@ export default class AnyoneCanPayService { } return Output.fromObject({ - capacity: AnyoneCanPayService.getSUDTAddCapacity(lockScript.args), lock: lockScript, - type: new AssetAccountInfo().generateSudtScript(tokenID), + type: new AssetAccountInfo().generateUdtScript(tokenID, udtType), + // use amount 0 to place holder amount + data: BufferUtils.writeBigUInt128LE(BigInt(0)), }) } - private static getSUDTAddCapacity(args: string) { - const addArgsLength = BigInt(args.slice(2).length / 2 - 20) * BigInt(10 ** 8) - return (addArgsLength + BigInt(MIN_SUDT_CAPACITY)).toString() - } - - public static async getHoldSUDTCellCapacity(lockScript: Script, tokenID: string) { + public static async getHoldSUDTCellCapacity(lockScript: Script, tokenID: string, udtType?: UDTType) { if (SystemScriptInfo.isSecpScript(lockScript) || tokenID === 'CKBytes') { return undefined } const liveCellService = LiveCellService.getInstance() + const typeScript = new AssetAccountInfo().generateUdtScript(tokenID, udtType) + if (!typeScript) { + return undefined + } const targetOutputLiveCell: LiveCell | null = await liveCellService.getOneByLockScriptAndTypeScript( lockScript, - new AssetAccountInfo().generateSudtScript(tokenID) + typeScript ) if (targetOutputLiveCell && new AssetAccountInfo().isAnyoneCanPayScript(lockScript)) { return undefined } - return AnyoneCanPayService.getSUDTAddCapacity(lockScript.args) + return helpers + .minimalCellCapacity({ + cellOutput: { + capacity: MIN_SUDT_CAPACITY.toString(), + lock: lockScript, + type: typeScript, + }, + data: BufferUtils.writeBigUInt128LE(BigInt(0)), + }) + .toString() } } diff --git a/packages/neuron-wallet/src/services/asset-account-service.ts b/packages/neuron-wallet/src/services/asset-account-service.ts index 991338a744..563fe68ae4 100644 --- a/packages/neuron-wallet/src/services/asset-account-service.ts +++ b/packages/neuron-wallet/src/services/asset-account-service.ts @@ -15,11 +15,11 @@ import WalletService from './wallets' import OutPoint from '../models/chain/out-point' import SystemScriptInfo from '../models/system-script-info' import Input from '../models/chain/input' -import { MIN_CELL_CAPACITY } from '../utils/const' +import { MIN_CELL_CAPACITY, UDTType } from '../utils/const' import SudtTokenInfoService from './sudt-token-info' export default class AssetAccountService { - private static async getACPCells(publicKeyHash: string, tokenId: string = 'CKBytes') { + private static async getACPCells(publicKeyHash: string, tokenId: string = 'CKBytes', udtType?: UDTType) { const assetAccountInfo = new AssetAccountInfo() const anyoneCanPayLockHash = assetAccountInfo.generateAnyoneCanPayScript(publicKeyHash).computeHash() const outputs = await getConnection() @@ -27,7 +27,10 @@ export default class AssetAccountService { .findBy({ status: In([OutputStatus.Live, OutputStatus.Sent]), lockHash: anyoneCanPayLockHash, - typeHash: tokenId !== 'CKBytes' ? assetAccountInfo.generateSudtScript(tokenId).computeHash() : IsNull(), + typeHash: + tokenId !== 'CKBytes' + ? assetAccountInfo.generateUdtScript(tokenId, udtType ?? UDTType.SUDT)!.computeHash() + : IsNull(), }) return outputs @@ -49,10 +52,13 @@ export default class AssetAccountService { return availableBalance >= 0 ? availableBalance.toString() : BigInt(0) } - private static async calculateUDTAccountBalance(publicKeyHash: string, tokenId: string) { + private static async calculateUDTAccountBalance(publicKeyHash: string, tokenId: string, udtType?: UDTType) { const assetAccountInfo = new AssetAccountInfo() const anyoneCanPayLockHash = assetAccountInfo.generateAnyoneCanPayScript(publicKeyHash).computeHash() - const typeHash = assetAccountInfo.generateSudtScript(tokenId).computeHash() + const typeHash = + udtType === UDTType.SUDT + ? assetAccountInfo.generateSudtScript(tokenId).computeHash() + : assetAccountInfo.generateXudtScript(tokenId).computeHash() const outputs = await getConnection() .getRepository(OutputEntity) .createQueryBuilder('output') @@ -71,7 +77,11 @@ export default class AssetAccountService { } public static async destroyAssetAccount(walletID: string, assetAccount: AssetAccount) { - const cells = await AssetAccountService.getACPCells(assetAccount?.blake160, assetAccount.tokenID) + const cells = await AssetAccountService.getACPCells( + assetAccount?.blake160, + assetAccount.tokenID, + assetAccount.udtType + ) const inputs = cells.map(cell => { return Input.fromObject({ previousOutput: cell.outPoint(), @@ -126,7 +136,7 @@ export default class AssetAccountService { const model = aa.toModel() const tokenID = determineTokenID(aa) - const cells = await this.getACPCells(aa.blake160, tokenID) + const cells = await this.getACPCells(aa.blake160, tokenID, aa.udtType) if (!cells.length) { return } @@ -135,7 +145,7 @@ export default class AssetAccountService { const bigIntAmount = await this.calculateAvailableCKBBalance(aa.blake160) model.balance = bigIntAmount.toString() } else { - const bigIntAmount = await this.calculateUDTAccountBalance(aa.blake160, aa.tokenID) + const bigIntAmount = await this.calculateUDTAccountBalance(aa.blake160, aa.tokenID, aa.udtType) model.balance = bigIntAmount.toString() } @@ -171,23 +181,38 @@ export default class AssetAccountService { const bitIntAmount = await this.calculateAvailableCKBBalance(assetAccount.blake160) assetAccount.balance = bitIntAmount.toString() } else { - const bigIntAmount = await this.calculateUDTAccountBalance(assetAccount.blake160, assetAccount.tokenID) + const bigIntAmount = await this.calculateUDTAccountBalance( + assetAccount.blake160, + assetAccount.tokenID, + assetAccount.udtType + ) assetAccount.balance = bigIntAmount.toString() } return assetAccount } - public static async generateCreateTx( - walletID: string, - tokenID: string, - symbol: string, - accountName: string, - tokenName: string, - decimal: string, - feeRate: string, + public static async generateCreateTx({ + walletID, + tokenID, + symbol, + accountName, + tokenName, + decimal, + feeRate, + fee, + udtType, + }: { + walletID: string + tokenID: string + symbol: string + accountName: string + tokenName: string + decimal: string + feeRate: string fee: string - ): Promise<{ + udtType?: UDTType + }): Promise<{ assetAccount: AssetAccount tx: Transaction }> { @@ -203,31 +228,42 @@ export default class AssetAccountService { const addrObj = !wallet.isHDWallet() ? addresses[0] : addresses.find(a => !usedBlake160s.has(a.blake160))! // 2. generate AssetAccount object - const assetAccount = new AssetAccount(tokenID, symbol, accountName, tokenName, decimal, '0', addrObj.blake160) + const assetAccount = AssetAccount.fromObject({ + tokenID, + symbol, + accountName, + tokenName, + decimal, + balance: '0', + blake160: addrObj.blake160, + udtType, + }) // 3. generate tx const changeAddrObj = await wallet.getNextChangeAddress() let tx: Transaction | undefined try { - tx = await TransactionGenerator.generateCreateAnyoneCanPayTx( + tx = await TransactionGenerator.generateCreateAnyoneCanPayTx({ tokenID, - walletID, - addrObj.blake160, - changeAddrObj!.blake160, + walletId: walletID, + blake160: addrObj.blake160, + changeBlake160: changeAddrObj!.blake160, feeRate, - fee - ) + fee, + udtType, + }) } catch (err) { if (!(err instanceof CapacityNotEnoughForChange)) { throw err } - tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance( + tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance({ tokenID, - walletID, - addrObj.blake160, + walletId: walletID, + blake160: addrObj.blake160, feeRate, - fee - ) + fee, + udtType, + }) } return { @@ -236,12 +272,12 @@ export default class AssetAccountService { } } - public static async checkAndSaveAssetAccountWhenSync(tokenID: string, blake160: string) { + public static async checkAndSaveAssetAccountWhenSync(tokenID: string, blake160: string, udtType?: UDTType) { const isCKB = tokenID === 'CKBytes' const decimal = isCKB ? '8' : '' const symbol = isCKB ? 'CKB' : '' const tokenName = isCKB ? 'CKBytes' : '' - const assetAccount = new AssetAccount(tokenID, symbol, '', tokenName, decimal, '0', blake160) + const assetAccount = new AssetAccount(tokenID, symbol, '', tokenName, decimal, '0', blake160, undefined, udtType) const assetAccountEntity = AssetAccountEntity.fromModel(assetAccount) await SudtTokenInfoService.insertSudtTokenInfo(assetAccountEntity.sudtTokenInfo) const existAccountAccount = await getConnection() @@ -322,6 +358,18 @@ export default class AssetAccountService { return assetAccounts.map(aa => aa.blake160) } + private static async getExistAssetAccount(assetAccount: AssetAccount) { + return getConnection() + .getRepository(AssetAccountEntity) + .createQueryBuilder() + .where({ + tokenID: assetAccount.tokenID, + blake160: assetAccount.blake160, + udtType: assetAccount.udtType ?? IsNull(), + }) + .getOne() + } + public static async sendTx( walletID: string, assetAccount: AssetAccount, @@ -331,12 +379,9 @@ export default class AssetAccountService { ): Promise { // 1. check AssetAccount exists const connection = getConnection() - const exists = await connection.manager.query( - `SELECT EXISTS (SELECT 1 FROM asset_account where tokenID = ? AND blake160 = ?) as exist`, - [assetAccount.tokenID, assetAccount.blake160] - ) + const exists = await AssetAccountService.getExistAssetAccount(assetAccount) - if (exists[0].exist === 1 && walletID) { + if (exists && walletID) { // For hardware wallet in ckb asset account: // 1. If a ckb account has been created, another one cannot be created; // 2. If a ckb account has been destroyed, ckb account can be created. @@ -344,7 +389,11 @@ export default class AssetAccountService { if (wallet.isHardware()) { const address = await wallet.getNextAddress() if (address) { - const acpCells = await AssetAccountService.getACPCells(address.blake160, assetAccount.tokenID) + const acpCells = await AssetAccountService.getACPCells( + address.blake160, + assetAccount.tokenID, + assetAccount.udtType + ) if (acpCells.length) { throw new Error(`Asset account already exists!`) } else { @@ -352,10 +401,14 @@ export default class AssetAccountService { .createQueryBuilder() .delete() .from(AssetAccountEntity) - .where('tokenID = :tokenID AND blake160 = :blake160', { - tokenID: assetAccount.tokenID, - blake160: assetAccount.blake160, - }) + .where( + `tokenID = :tokenID AND blake160 = :blake160 ${assetAccount.udtType ? 'AND udtType = :udtType' : ''}`, + { + tokenID: assetAccount.tokenID, + blake160: assetAccount.blake160, + udtType: assetAccount.udtType, + } + ) .execute() } } @@ -486,7 +539,12 @@ export default class AssetAccountService { } const tokenId = chequeLiveCell.type!.args - const assetAccount = new AssetAccount(tokenId, '', '', '', '', '0', receiverAcpScript.args) + const udtType = assetAccountInfo.isSudtScript(chequeLiveCell.type!) + ? UDTType.SUDT + : assetAccountInfo.isXudtScript(chequeLiveCell.type!) + ? UDTType.XUDT + : undefined + const assetAccount = new AssetAccount(tokenId, '', '', '', '', '0', receiverAcpScript.args, undefined, udtType) return { tx, assetAccount } } diff --git a/packages/neuron-wallet/src/services/cells.ts b/packages/neuron-wallet/src/services/cells.ts index 87fdde58fe..48c876e88d 100644 --- a/packages/neuron-wallet/src/services/cells.ts +++ b/packages/neuron-wallet/src/services/cells.ts @@ -39,6 +39,7 @@ import { LOCKTIME_ARGS_LENGTH, MIN_CELL_CAPACITY } from '../utils/const' import HdPublicKeyInfo from '../database/chain/entities/hd-public-key-info' import CellLocalInfoService from './cell-local-info' import CellLocalInfo from '../database/chain/entities/cell-local-info' +import { helpers } from '@ckb-lumos/lumos' export interface PaginationResult { totalCount: number @@ -57,6 +58,7 @@ export enum CustomizedType { NFTIssuer = 'NFTIssuer', SUDT = 'SUDT', + XUDT = 'XUDT', Spore = 'Spore', SporeCluster = 'SporeCluster', @@ -79,13 +81,13 @@ export enum TypeScriptCategory { NFTClass = CustomizedType.NFTClass, NFTIssuer = CustomizedType.NFTIssuer, SUDT = CustomizedType.SUDT, + XUDT = CustomizedType.XUDT, Spore = CustomizedType.Spore, Unknown = CustomizedType.Unknown, } export default class CellsService { private static ANYONE_CAN_PAY_CKB_CELL_MIN = BigInt(61 * 10 ** 8) - private static ANYONE_CAN_PAY_SUDT_CELL_MIN = BigInt(142 * 10 ** 8) public static async getBalancesByWalletId(walletId: string): Promise<{ liveBalances: Map @@ -296,6 +298,7 @@ export default class CellsService { const nftClassCodehash = assetAccountInfo.getNftClassInfo().codeHash const nftCodehash = assetAccountInfo.getNftInfo().codeHash const sudtCodehash = assetAccountInfo.getSudtCodeHash() + const xudtCodeHash = assetAccountInfo.infos.xudt.codeHash const sporeInfos = assetAccountInfo.getSporeInfos() const secp256k1LockHashes = [...blake160Hashes].map(blake160 => @@ -304,7 +307,7 @@ export default class CellsService { const skip = (pageNo - 1) * pageSize - const allMultiSignOutputs = await getConnection() + const allCustomizedOutputs = await getConnection() .getRepository(OutputEntity) .createQueryBuilder('output') .leftJoinAndSelect('output.transaction', 'tx') @@ -365,7 +368,7 @@ export default class CellsService { // to make the Spore NFT data available, // we need to fetch it from RPC instead of database const rpc = generateRPC(currentNetwork.remote, currentNetwork.type) - const sporeOutputs = allMultiSignOutputs.filter(item => + const sporeOutputs = allCustomizedOutputs.filter(item => sporeInfos.some(info => item.typeCodeHash && bytes.equal(info.codeHash, item.typeCodeHash)) ) @@ -393,7 +396,7 @@ export default class CellsService { }) ) - const matchedOutputs = allMultiSignOutputs.filter(o => { + const matchedOutputs = allCustomizedOutputs.filter(o => { if (o.multiSignBlake160) { return multiSignHashes.has(o.multiSignBlake160) } @@ -406,7 +409,11 @@ export default class CellsService { ) } - if (o.hasData && o.typeCodeHash === sudtCodehash && o.lockCodeHash === assetAccountInfo.anyoneCanPayCodeHash) { + if ( + o.hasData && + (o.typeCodeHash === sudtCodehash || o.typeCodeHash === xudtCodeHash) && + o.lockCodeHash === assetAccountInfo.anyoneCanPayCodeHash + ) { return false } @@ -466,10 +473,16 @@ export default class CellsService { }) } else if (o.typeCodeHash === sudtCodehash) { cell.setCustomizedAssetInfo({ - lock: CustomizedLock.SUDT, + lock: '', type: CustomizedType.SUDT, data: '', }) + } else if (o.typeCodeHash === xudtCodeHash) { + cell.setCustomizedAssetInfo({ + lock: '', + type: CustomizedType.XUDT, + data: '', + }) } else if (sporeInfos.some(info => o.typeCodeHash && bytes.equal(info.codeHash, o.typeCodeHash))) { const data = (() => { try { @@ -1113,8 +1126,14 @@ export default class CellsService { } else { totalSize += TransactionSize.secpLockWitness() inputOriginCells.push(cell) - // capacity - 142CKB, 142CKB remaining for change - inputCapacities -= this.ANYONE_CAN_PAY_SUDT_CELL_MIN + inputCapacities -= helpers.minimalCellCapacity({ + cellOutput: { + capacity: cell.capacity, + lock: cell.lock(), + type: cell.type(), + }, + data: cell.data, + }) totalSize += TransactionSize.sudtAnyoneCanPayOutput() + TransactionSize.sudtData() } inputs.push(input) @@ -1141,12 +1160,20 @@ export default class CellsService { .map(i => BigInt(i.capacity!)) .reduce((result, c) => result + c, BigInt(0)) let capacity: bigint = BigInt(0) - if (BigInt(cellCapacity) - this.ANYONE_CAN_PAY_SUDT_CELL_MIN >= extraPayCapacity) { + const curCellMinCapacity = helpers.minimalCellCapacity({ + cellOutput: { + capacity: cell.capacity, + lock: cell.lock(), + type: cell.type(), + }, + data: cell.data, + }) + if (BigInt(cellCapacity) - curCellMinCapacity >= extraPayCapacity) { capacity = BigInt(cellCapacity) - extraPayCapacity extraPayCapacity = BigInt(0) } else { - capacity = this.ANYONE_CAN_PAY_SUDT_CELL_MIN - extraPayCapacity = extraPayCapacity - (BigInt(cellCapacity) - this.ANYONE_CAN_PAY_SUDT_CELL_MIN) + capacity = curCellMinCapacity + extraPayCapacity = extraPayCapacity - (BigInt(cellCapacity) - curCellMinCapacity) } const output = Output.fromObject({ capacity: capacity.toString(), @@ -1365,6 +1392,8 @@ export default class CellsService { return TypeScriptCategory.NFTClass case assetAccountInfo.getSudtCodeHash(): return TypeScriptCategory.SUDT + case assetAccountInfo.infos.xudt.codeHash: + return TypeScriptCategory.XUDT case SystemScriptInfo.DAO_CODE_HASH: return TypeScriptCategory.DAO default: diff --git a/packages/neuron-wallet/src/services/sudt-token-info.ts b/packages/neuron-wallet/src/services/sudt-token-info.ts index 7195f2cdb2..f69f26c760 100644 --- a/packages/neuron-wallet/src/services/sudt-token-info.ts +++ b/packages/neuron-wallet/src/services/sudt-token-info.ts @@ -1,6 +1,7 @@ import { In, Not } from 'typeorm' import SudtTokenInfoEntity from '../database/chain/entities/sudt-token-info' import { getConnection } from '../database/chain/connection' +import { UDTType } from '../utils/const' export default class SudtTokenInfoService { static async findSudtTokenInfoByArgs(typeArgsList: string[]) { @@ -41,13 +42,14 @@ export default class SudtTokenInfoService { .execute() } - static getSudtTokenInfo(typeArgs: string): Promise { + static getSudtTokenInfo(typeArgs: string, udtType: UDTType): Promise { return getConnection() .getRepository(SudtTokenInfoEntity) .createQueryBuilder('info') .leftJoinAndSelect('info.assetAccounts', 'aa') - .where(`info.tokenID = :typeArgs`, { + .where(`info.tokenID = :typeArgs AND info.udtType = :udtType`, { typeArgs, + udtType, }) .getOne() } diff --git a/packages/neuron-wallet/src/services/tx/transaction-generator.ts b/packages/neuron-wallet/src/services/tx/transaction-generator.ts index 2c0214f958..ab8079ce94 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-generator.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-generator.ts @@ -31,7 +31,7 @@ import AddressService from '../../services/addresses' import { addressToScript } from '../../utils/scriptAndAddress' import MultisigConfigModel from '../../models/multisig-config' import WalletService from '../../services/wallets' -import { MIN_CELL_CAPACITY, MIN_SUDT_CAPACITY } from '../../utils/const' +import { MIN_CELL_CAPACITY, UDTType } from '../../utils/const' import AssetAccountService from '../../services/asset-account-service' import LiveCellService from '../../services/live-cell-service' import NetworksService from '../networks' @@ -651,37 +651,51 @@ export class TransactionGenerator { } // sUDT - public static async generateCreateAnyoneCanPayTx( - tokenID: string, - walletId: string, - blake160: string, - changeBlake160: string, - feeRate: string, + public static async generateCreateAnyoneCanPayTx({ + tokenID, + walletId, + blake160, + changeBlake160, + feeRate, + fee, + udtType, + }: { + tokenID: string + walletId: string + blake160: string + changeBlake160: string + feeRate: string fee: string - ): Promise { + udtType?: UDTType + }): Promise { // if tokenID === '' or 'CKBytes', create ckb cell const isCKB = tokenID === 'CKBytes' || tokenID === '' + if (!isCKB && !udtType) throw new Error('The udt token must has udt Type') const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() const assetAccountInfo = new AssetAccountInfo() const sudtCellDep = assetAccountInfo.sudtCellDep - const needCapacities = isCKB ? BigInt(61 * 10 ** 8) : BigInt(142 * 10 ** 8) + const xudtCellDep = assetAccountInfo.xudtCellDep const output = Output.fromObject({ - capacity: needCapacities.toString(), lock: assetAccountInfo.generateAnyoneCanPayScript(blake160), - type: isCKB ? null : assetAccountInfo.generateSudtScript(tokenID), + type: isCKB + ? null + : udtType === UDTType.SUDT + ? assetAccountInfo.generateSudtScript(tokenID) + : assetAccountInfo.generateXudtScript(tokenID), data: isCKB ? '0x' : BufferUtils.writeBigUInt128LE(BigInt(0)), }) const tx = Transaction.fromObject({ version: '0', headerDeps: [], - cellDeps: [secpCellDep, sudtCellDep], + cellDeps: [secpCellDep, sudtCellDep, xudtCellDep], inputs: [], outputs: [output], outputsData: [output.data], witnesses: [], }) const baseSize: number = TransactionSize.tx(tx) + const needCapacities = BigInt(output.capacity) const { inputs, capacities, finalFee, hasChangeOutput } = await CellsService.gatherInputs( needCapacities.toString(), walletId, @@ -713,13 +727,21 @@ export class TransactionGenerator { return tx } - public static async generateCreateAnyoneCanPayTxUseAllBalance( - tokenID: string, - walletId: string, - blake160: string, - feeRate: string, + public static async generateCreateAnyoneCanPayTxUseAllBalance({ + tokenID, + walletId, + blake160, + feeRate, + fee, + udtType, + }: { + tokenID: string + walletId: string + blake160: string + feeRate: string fee: string - ): Promise { + udtType?: UDTType + }): Promise { // if tokenID === '' or 'CKBytes', create ckb cell const isCKB = tokenID === 'CKBytes' || tokenID === '' @@ -729,6 +751,7 @@ export class TransactionGenerator { const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() const assetAccountInfo = new AssetAccountInfo() const sudtCellDep = assetAccountInfo.sudtCellDep + const xudtCellDep = assetAccountInfo.xudtCellDep const allInputs: Input[] = await CellsService.gatherAllInputs(walletId) @@ -741,14 +764,18 @@ export class TransactionGenerator { const output = Output.fromObject({ capacity: totalCapacity.toString(), lock: assetAccountInfo.generateAnyoneCanPayScript(blake160), - type: isCKB ? null : assetAccountInfo.generateSudtScript(tokenID), + type: isCKB + ? null + : udtType === UDTType.SUDT + ? assetAccountInfo.generateSudtScript(tokenID) + : assetAccountInfo.generateXudtScript(tokenID), data: isCKB ? '0x' : BufferUtils.writeBigUInt128LE(BigInt(0)), }) const tx = Transaction.fromObject({ version: '0', headerDeps: [], - cellDeps: [secpCellDep, sudtCellDep], + cellDeps: [secpCellDep, sudtCellDep, xudtCellDep], inputs: allInputs, outputs: [output], outputsData: [output.data], @@ -782,7 +809,7 @@ export class TransactionGenerator { throw new SudtAcpHaveDataError() } if (!isCKBAccount) { - cellDeps.push(assetAccountInfo.sudtCellDep) + cellDeps.push(assetAccountInfo.sudtCellDep, assetAccountInfo.xudtCellDep) } const output = Output.fromObject({ @@ -921,7 +948,6 @@ export class TransactionGenerator { ) { const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() const assetAccountInfo = new AssetAccountInfo() - const sudtCellDep = assetAccountInfo.sudtCellDep const anyoneCanPayDep = assetAccountInfo.anyoneCanPayCellDep const targetAmount: bigint = amount === 'all' ? BigInt(0) : BufferUtils.parseAmountFromSUDTData(targetOutput.data) + BigInt(amount) @@ -940,10 +966,14 @@ export class TransactionGenerator { data: targetOutput.data, }) : undefined + const udtCellDep = + targetOutput.type && assetAccountInfo.isSudtScript(targetOutput.type) + ? assetAccountInfo.sudtCellDep + : assetAccountInfo.xudtCellDep const tx = Transaction.fromObject({ version: '0', headerDeps: [], - cellDeps: [secpCellDep, sudtCellDep, anyoneCanPayDep], + cellDeps: [secpCellDep, udtCellDep, anyoneCanPayDep], inputs: targetInput ? [targetInput] : [], outputs: [output], outputsData: [output.data], @@ -987,7 +1017,7 @@ export class TransactionGenerator { // amount assertion TransactionGenerator.checkTxCapacity(tx, 'generateAnyoneCanPayToSudtTx capacity not match!') - TransactionGenerator.checkTxSudtAmount(tx, 'generateAnyoneCanPayToSudtTx sUDT amount not match!', assetAccountInfo) + TransactionGenerator.checkTxUdtAmount(tx, 'generateAnyoneCanPayToSudtTx sUDT amount not match!', assetAccountInfo) return tx } @@ -1075,22 +1105,23 @@ export class TransactionGenerator { const assetAccountInfo = new AssetAccountInfo() const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() - const sudtCellDep = assetAccountInfo.sudtCellDep + const udtCellDep = + assetAccount.udtType === UDTType.SUDT ? assetAccountInfo.sudtCellDep : assetAccountInfo.xudtCellDep const anyoneCanPayDep = assetAccountInfo.anyoneCanPayCellDep const senderAcpScript = assetAccountInfo.generateAnyoneCanPayScript(assetAccount.blake160) const receiverLockScript = AddressParser.parse(receiverAddress) const chequeCellTmp = Output.fromObject({ - capacity: BigInt(162 * 10 ** 8).toString(), lock: assetAccountInfo.generateChequeScript(bytes.hexify(Buffer.alloc(20)), bytes.hexify(Buffer.alloc(20))), - type: assetAccountInfo.generateSudtScript(assetAccount.tokenID), + type: assetAccountInfo.generateUdtScript(assetAccount.tokenID, assetAccount.udtType), + data: BufferUtils.writeBigUInt128LE(BigInt(0)), }) const tx = Transaction.fromObject({ version: '0', headerDeps: [], - cellDeps: [secpCellDep, sudtCellDep, anyoneCanPayDep], + cellDeps: [secpCellDep, udtCellDep, anyoneCanPayDep], inputs: [], outputs: [], outputsData: [], @@ -1173,7 +1204,7 @@ export class TransactionGenerator { tx.anyoneCanPaySendAmount = tx.sudtInfo.amount TransactionGenerator.checkTxCapacity(tx, 'generateCreateChequeTx capacity not match!') - TransactionGenerator.checkTxSudtAmount(tx, 'generateCreateChequeTx sUDT amount not match!', assetAccountInfo) + TransactionGenerator.checkTxUdtAmount(tx, 'generateCreateChequeTx sUDT amount not match!', assetAccountInfo) return tx } @@ -1204,11 +1235,13 @@ export class TransactionGenerator { } const chequeSenderLock = senderInputsByLockHash[0].lockScript() - const acpCellCapacity = BigInt(142 * 10 ** 8) const assetAccountInfo = new AssetAccountInfo() const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() - const sudtCellDep = assetAccountInfo.sudtCellDep + const udtCellDep = + chequeCell.type && assetAccountInfo.isSudtScript(chequeCell.type) + ? assetAccountInfo.sudtCellDep + : assetAccountInfo.xudtCellDep const anyoneCanPayDep = assetAccountInfo.anyoneCanPayCellDep const chequeDep = assetAccountInfo.getChequeInfo().cellDep @@ -1229,7 +1262,7 @@ export class TransactionGenerator { const tx = Transaction.fromObject({ version: '0', - cellDeps: [secpCellDep, sudtCellDep, anyoneCanPayDep, chequeDep], + cellDeps: [secpCellDep, udtCellDep, anyoneCanPayDep, chequeDep], headerDeps: [], inputs: [chequeInput], outputs: [senderOutput], @@ -1241,6 +1274,7 @@ export class TransactionGenerator { const receiverAcpCells = await CellsService.getACPCells(receiverAcpScript, chequeCell.type!) let requiredCapacity = BigInt(0) + let acpCellCapacity = BigInt(0) if (receiverAcpCells.length) { const originalReceiverAcpOutput = receiverAcpCells[0] @@ -1264,16 +1298,16 @@ export class TransactionGenerator { data: originalReceiverAcpOutput.data, }) tx.inputs.push(receiverAcpInput) + acpCellCapacity = BigInt(newReceiverAcpOutput.capacity) tx.outputs.push(newReceiverAcpOutput) } else { - requiredCapacity = acpCellCapacity - const receiverAcpOutput = Output.fromObject({ - capacity: acpCellCapacity.toString(), lock: receiverAcpScript, type: chequeCell.type, data: chequeCell.data, }) + acpCellCapacity = BigInt(receiverAcpOutput.capacity) + requiredCapacity = BigInt(receiverAcpOutput.capacity) tx.outputs.push(receiverAcpOutput) } @@ -1306,7 +1340,7 @@ export class TransactionGenerator { } TransactionGenerator.checkTxCapacity(tx, 'generateClaimChequeTx capacity not match!') - TransactionGenerator.checkTxSudtAmount(tx, 'generateClaimChequeTx sUDT amount not match!', assetAccountInfo) + TransactionGenerator.checkTxUdtAmount(tx, 'generateClaimChequeTx sUDT amount not match!', assetAccountInfo) return tx } @@ -1378,12 +1412,13 @@ export class TransactionGenerator { const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() const sudtCellDep = assetAccountInfo.sudtCellDep + const xudtCellDep = assetAccountInfo.xudtCellDep const anyoneCanPayDep = assetAccountInfo.anyoneCanPayCellDep const chequeDep = assetAccountInfo.getChequeInfo().cellDep const tx = Transaction.fromObject({ version: '0', - cellDeps: [secpCellDep, sudtCellDep, anyoneCanPayDep, chequeDep], + cellDeps: [secpCellDep, sudtCellDep, xudtCellDep, anyoneCanPayDep, chequeDep], headerDeps: [], inputs: [chequeSenderAcpInput, chequeInput], outputs: [senderAcpOutput], @@ -1410,7 +1445,7 @@ export class TransactionGenerator { tx.outputsData.push('0x') TransactionGenerator.checkTxCapacity(tx, 'generateWithdrawChequeTx capacity not match!') - TransactionGenerator.checkTxSudtAmount(tx, 'generateWithdrawChequeTx sUDT amount not match!', assetAccountInfo) + TransactionGenerator.checkTxUdtAmount(tx, 'generateWithdrawChequeTx sUDT amount not match!', assetAccountInfo) return tx } @@ -1456,7 +1491,6 @@ export class TransactionGenerator { ] const secpCellDep = await SystemScriptInfo.getInstance().getSecpCellDep() - const sudtCellDep = assetAccountInfo.sudtCellDep let outputs: Output[] = [] let acpInputCell: Input | null = null const acpCodeHashes = new Set([sudtCell.lock.codeHash]) @@ -1511,7 +1545,12 @@ export class TransactionGenerator { const tx = Transaction.fromObject({ version: '0', headerDeps: [], - cellDeps: [secpCellDep, sudtCellDep, ...acpCellDeps.filter((v): v is CellDep => !!v)], + cellDeps: [ + secpCellDep, + assetAccountInfo.sudtCellDep, + assetAccountInfo.xudtCellDep, + ...acpCellDeps.filter((v): v is CellDep => !!v), + ], inputs: sudtMigrateAcpInputs, outputs: outputs, outputsData: outputs.map(v => v.data || '0x'), @@ -1521,7 +1560,7 @@ export class TransactionGenerator { const txSize = TransactionSize.tx(tx) + TransactionSize.secpLockWitness() * tx.inputs.length tx.fee = TransactionFee.fee(txSize, BigInt(feeRate)).toString() const outputCapacity = BigInt(inputSudtCell.capacity) - BigInt(tx.fee) - if (outputCapacity >= BigInt(MIN_SUDT_CAPACITY)) { + if (outputCapacity >= tx.outputs[0].minimalCellCapacity()) { tx.outputs[0].capacity = outputCapacity.toString() return tx } @@ -1577,18 +1616,29 @@ export class TransactionGenerator { ) } - private static checkTxSudtAmount(tx: Transaction, msg: string, assetAccountInfo: AssetAccountInfo) { - const inputAmount = tx.inputs + private static checkTxUdtAmount(tx: Transaction, msg: string, assetAccountInfo: AssetAccountInfo) { + const sudtInputAmount = tx.inputs .filter(i => i.type && assetAccountInfo.isSudtScript(i.type)) .map(i => BufferUtils.parseAmountFromSUDTData(i.data!)) .reduce((result, c) => result + c, BigInt(0)) - const outputAmount = tx.outputs + const sudtOutputAmount = tx.outputs .filter(o => o.type && assetAccountInfo.isSudtScript(o.type)) .map(o => BufferUtils.parseAmountFromSUDTData(o.data!)) .reduce((result, c) => result + c, BigInt(0)) - assert.equal(inputAmount.toString(), outputAmount.toString(), `${msg}: ${JSON.stringify(tx)}`) + assert.equal(sudtInputAmount.toString(), sudtOutputAmount.toString(), `${msg}: ${JSON.stringify(tx)}`) + + const xudtInputAmount = tx.inputs + .filter(i => i.type && assetAccountInfo.isXudtScript(i.type)) + .map(i => BufferUtils.parseAmountFromSUDTData(i.data!)) + .reduce((result, c) => result + c, BigInt(0)) + + const xudtOutputAmount = tx.outputs + .filter(o => o.type && assetAccountInfo.isXudtScript(o.type)) + .map(o => BufferUtils.parseAmountFromSUDTData(o.data!)) + .reduce((result, c) => result + c, BigInt(0)) + assert.equal(xudtInputAmount.toString(), xudtOutputAmount.toString(), `${msg}: ${JSON.stringify(tx)}`) } } diff --git a/packages/neuron-wallet/src/services/tx/transaction-service.ts b/packages/neuron-wallet/src/services/tx/transaction-service.ts index 288fbfb8b7..18dbb29bf5 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-service.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-service.ts @@ -2,13 +2,7 @@ import { CKBRPC } from '@ckb-lumos/lumos/rpc' import TransactionEntity from '../../database/chain/entities/transaction' import OutputEntity from '../../database/chain/entities/output' import { getConnection } from '../../database/chain/connection' -import Transaction, { - TransactionStatus, - SudtInfo, - NFTType, - NFTInfo, - AssetAccountType, -} from '../../models/chain/transaction' +import Transaction, { TransactionStatus, NFTType, AssetAccountType } from '../../models/chain/transaction' import InputEntity from '../../database/chain/entities/input' import AddressParser from '../../models/address-parser' import AssetAccountInfo from '../../models/asset-account-info' @@ -22,6 +16,7 @@ import Input from '../../models/chain/input' import SudtTokenInfoService from '../sudt-token-info' import TransactionSize from '../../models/transaction-size' import Output from '../../models/chain/output' +import { UDTType } from '../../utils/const' export interface TransactionsByAddressesParam { pageNo: number @@ -93,6 +88,145 @@ export class TransactionsService { return 0 } + private static async getSudtInfo({ + txHash, + acpUdtInputs, + acpUdtOutputs, + udtType, + }: { + txHash: string + acpUdtInputs: InputEntity[] + acpUdtOutputs: OutputEntity[] + udtType: UDTType + }) { + const udtInput = acpUdtInputs.find(i => i.transactionHash === txHash) + const udtOutput = acpUdtOutputs.find(o => o.outPointTxHash === txHash) + const typeArgs: string | undefined | null = udtInput?.typeArgs ?? udtOutput?.typeArgs + const assetAccountType: AssetAccountType | undefined = + udtInput || udtOutput ? (udtType === UDTType.SUDT ? AssetAccountType.SUDT : AssetAccountType.XUDT) : undefined + if (!typeArgs) return undefined + const inputAmount = acpUdtInputs + .filter(i => i.transactionHash === txHash && i.typeArgs === typeArgs) + .map(i => BufferUtils.parseAmountFromSUDTData(i.data)) + .reduce((result, c) => result + c, BigInt(0)) + const outputAmount = acpUdtOutputs + .filter(o => o.outPointTxHash === txHash && o.typeArgs === typeArgs) + .map(o => BufferUtils.parseAmountFromSUDTData(o.data)) + .reduce((result, c) => result + c, BigInt(0)) + const amount = outputAmount - inputAmount + let txType = amount > 0 ? 'receive' : 'send' + if (udtInput && !udtOutput) { + txType = 'destroy' + } else if (!udtInput && udtOutput) { + txType = 'create' + } + + return { + sudtInfo: { + sUDT: (await SudtTokenInfoService.getSudtTokenInfo(typeArgs, udtType)) ?? undefined, + amount: amount.toString(), + }, + txType, + assetAccountType, + } + } + + private static async getAssetCKBInfo({ + txHash, + acpCKBInputs, + acpCKBOutputs, + }: { + txHash: string + acpCKBInputs: InputEntity[] + acpCKBOutputs: OutputEntity[] + }) { + const ckbAssetInput = acpCKBInputs.find(i => i.transactionHash === txHash) + const ckbAssetOutput = acpCKBOutputs.find(o => o.outPointTxHash === txHash) + if (!ckbAssetInput && !ckbAssetOutput) return + if (ckbAssetInput && !ckbAssetOutput) { + return { + txType: 'destroy', + assetAccountType: AssetAccountType.CKB, + } + } + if (!ckbAssetInput && ckbAssetOutput) { + return { + txType: 'create', + assetAccountType: AssetAccountType.CKB, + } + } + } + + private static getNtfInfo({ + txHash, + nftInputs, + nftOutputs, + }: { + txHash: string + nftInputs: InputEntity[] + nftOutputs: OutputEntity[] + }) { + const sendNFTCell = nftInputs.find(i => i.transactionHash === txHash) + const receiveNFTCell = nftOutputs.find(o => o.outPointTxHash === txHash) + + if (sendNFTCell) { + return { type: NFTType.Send, data: sendNFTCell.typeArgs! } + } + if (receiveNFTCell) { + return { type: NFTType.Receive, data: receiveNFTCell.typeArgs! } + } + } + + private static getDAOCapacity( + ckbChange: bigint, + txDaoInfo?: { + inputs: { + capacity: string + transactionHash: string + outPointTxHash: string + outPointIndex: string + data: string + }[] + outputs: { + capacity: string + transactionHash: string + daoData: string + }[] + } + ) { + if (!txDaoInfo) return + if (txDaoInfo.inputs.length && !txDaoInfo.outputs.length) { + return txDaoInfo.inputs.reduce((pre, cur) => BigInt(cur.capacity) + pre, ckbChange).toString() + } + if (!txDaoInfo.inputs.length && txDaoInfo.outputs.length) { + return `-${txDaoInfo.outputs.reduce((pre, cur) => BigInt(cur.capacity) + pre, BigInt(0)).toString()}` + } + } + + private static groupAssetCells(cells: T[]) { + const assetAccountInfo = new AssetAccountInfo() + const acpSudtCells: T[] = [] + const acpCKBCells: T[] = [] + const acpXudtCells: T[] = [] + cells.forEach(v => { + const typeScript = v.typeScript() + if (typeScript) { + if (assetAccountInfo.isSudtScript(typeScript)) { + acpSudtCells.push(v) + } else if (assetAccountInfo.isXudtScript(typeScript)) { + acpXudtCells.push(v) + } + } else { + acpCKBCells.push(v) + } + }) + return { + acpCKBCells, + acpSudtCells, + acpXudtCells, + } + } + public static async getAllByAddresses( params: TransactionsByAddressesParam, searchValue: string = '' @@ -359,14 +493,6 @@ export class TransactionsService { ) .getMany() - const anyoneCanPayInputs = assetAccountInputs.filter(i => i.typeScript()) - - const anyoneCanPayOutputs = assetAccountOutputs.filter(i => i.typeScript()) - - const ckbAssetInputs = assetAccountInputs.filter(i => !i.typeScript()) - - const ckbAssetOutputs = assetAccountOutputs.filter(i => !i.typeScript()) - const inputPreviousTxHashes: string[] = inputs.map(i => i.outPointTxHash).filter(h => !!h) as string[] const daoCellOutPoints: { txHash: string; index: string }[] = ( @@ -410,114 +536,53 @@ export class TransactionsService { } }) + const { + acpCKBCells: acpCKBInputs, + acpSudtCells: acpSudtInputs, + acpXudtCells: acpXudtInputs, + } = TransactionsService.groupAssetCells(assetAccountInputs) + const { + acpCKBCells: acpCKBOutputs, + acpSudtCells: acpSudtOutputs, + acpXudtCells: acpXudtOutputs, + } = TransactionsService.groupAssetCells(assetAccountOutputs) const txs = await Promise.all( transactions.map(async tx => { const value = sums.get(tx.hash!) || BigInt(0) - - let typeArgs: string | undefined | null - let txType: string = value > BigInt(0) ? 'receive' : 'send' - let assetAccountType: AssetAccountType | undefined - - const sudtInput = anyoneCanPayInputs.find( - i => i.transactionHash === tx.hash && assetAccountInfo.isSudtScript(i.typeScript()!) - ) - const sudtOutput = anyoneCanPayOutputs.find( - o => o.outPointTxHash === tx.hash && assetAccountInfo.isSudtScript(o.typeScript()!) - ) - if (sudtInput) { - typeArgs = sudtInput.typeArgs - if (!sudtOutput) { - txType = 'destroy' - assetAccountType = AssetAccountType.SUDT - } - } else { - if (sudtOutput) { - typeArgs = sudtOutput.typeArgs - txType = 'create' - assetAccountType = AssetAccountType.SUDT - } - } - - const ckbAssetInput = ckbAssetInputs.find(i => i.transactionHash === tx.hash) - const ckbAssetOutput = ckbAssetOutputs.find(o => o.outPointTxHash === tx.hash) - if (ckbAssetInput && !ckbAssetOutput) { - txType = 'destroy' - assetAccountType = AssetAccountType.CKB - } else if (!ckbAssetInput && ckbAssetOutput) { - txType = 'create' - assetAccountType = AssetAccountType.CKB - } - - let sudtInfo: SudtInfo | undefined - - if (typeArgs) { - // const typeArgs = sudtInput.typeArgs - const inputAmount = anyoneCanPayInputs - .filter( - i => - i.transactionHash === tx.hash && - assetAccountInfo.isSudtScript(i.typeScript()!) && - i.typeArgs === typeArgs - ) - .map(i => BufferUtils.parseAmountFromSUDTData(i.data)) - .reduce((result, c) => result + c, BigInt(0)) - const outputAmount = anyoneCanPayOutputs - .filter( - o => - o.outPointTxHash === tx.hash && - assetAccountInfo.isSudtScript(o.typeScript()!) && - o.typeArgs === typeArgs - ) - .map(o => BufferUtils.parseAmountFromSUDTData(o.data)) - .reduce((result, c) => result + c, BigInt(0)) - - const amount = outputAmount - inputAmount - const tokenInfo = await SudtTokenInfoService.getSudtTokenInfo(typeArgs) - - if (tokenInfo) { - sudtInfo = { - sUDT: tokenInfo, - amount: amount.toString(), - } - } - } - - const sendNFTCell = nftInputs.find(i => i.typeCodeHash === nftCodehash && i.transactionHash === tx.hash) - const receiveNFTCell = nftOutputs.find(o => o.typeCodeHash === nftCodehash && o.outPointTxHash === tx.hash) - - let nftInfo: NFTInfo | undefined - if (sendNFTCell) { - nftInfo = { type: NFTType.Send, data: sendNFTCell.typeArgs! } - } else if (receiveNFTCell) { - nftInfo = { type: NFTType.Receive, data: receiveNFTCell.typeArgs! } - } - - const txDaoInfo = daoInfo.get(tx.hash) - let daoCapacity: string | undefined - if (txDaoInfo) { - if (txDaoInfo.inputs.length && !txDaoInfo.outputs.length) { - daoCapacity = txDaoInfo.inputs.reduce((pre, cur) => BigInt(cur.capacity) + pre, BigInt(value)).toString() - } else if (!txDaoInfo.inputs.length && txDaoInfo.outputs.length) { - daoCapacity = `-${txDaoInfo.outputs.reduce((pre, cur) => BigInt(cur.capacity) + pre, BigInt(0)).toString()}` - } - } + const typeScriptInfo = + (await TransactionsService.getSudtInfo({ + txHash: tx.hash, + acpUdtInputs: acpSudtInputs, + acpUdtOutputs: acpSudtOutputs, + udtType: UDTType.SUDT, + })) || + (await TransactionsService.getSudtInfo({ + txHash: tx.hash, + acpUdtInputs: acpXudtInputs, + acpUdtOutputs: acpXudtOutputs, + udtType: UDTType.XUDT, + })) || + (await TransactionsService.getAssetCKBInfo({ + txHash: tx.hash, + acpCKBInputs, + acpCKBOutputs, + })) return Transaction.fromObject({ timestamp: tx.timestamp, value: value.toString(), hash: tx.hash, version: tx.version, - type: txType, - assetAccountType: assetAccountType, - nervosDao: !!txDaoInfo, status: tx.status, description: tx.description, createdAt: tx.createdAt, updatedAt: tx.updatedAt, blockNumber: tx.blockNumber, - sudtInfo: sudtInfo, - nftInfo: nftInfo, - daoCapacity, + ...typeScriptInfo, + type: typeScriptInfo?.txType ?? (value > BigInt(0) ? 'receive' : 'send'), + nftInfo: TransactionsService.getNtfInfo({ txHash: tx.hash, nftInputs, nftOutputs }), + nervosDao: !!daoInfo.get(tx.hash), + daoCapacity: TransactionsService.getDAOCapacity(value, daoInfo.get(tx.hash)), }) }) ) diff --git a/packages/neuron-wallet/src/utils/ckb-rpc.ts b/packages/neuron-wallet/src/utils/ckb-rpc.ts index 9994fa8f88..6a7d3a08d8 100644 --- a/packages/neuron-wallet/src/utils/ckb-rpc.ts +++ b/packages/neuron-wallet/src/utils/ckb-rpc.ts @@ -108,7 +108,7 @@ const lightRPCProperties: Record[0] paramsFormatters: [paramsFormatter.toHash], resultFormatters: (result: { status: 'fetched' | 'fetching' | 'added' | 'not_found' - data?: RPC.TransactionWithStatus + data?: Parameters[0] }) => { return { status: result.status, diff --git a/packages/neuron-wallet/src/utils/const.ts b/packages/neuron-wallet/src/utils/const.ts index d294db6b7f..9059715d05 100644 --- a/packages/neuron-wallet/src/utils/const.ts +++ b/packages/neuron-wallet/src/utils/const.ts @@ -24,6 +24,11 @@ export enum ResponseCode { Success, } +export enum UDTType { + SUDT = 'sUDT', + XUDT = 'xUDT', +} + export default { ResponseCode, } diff --git a/packages/neuron-wallet/tests/controllers/anyone-can-pay.test.ts b/packages/neuron-wallet/tests/controllers/anyone-can-pay.test.ts index dd906cea2d..119d08800a 100644 --- a/packages/neuron-wallet/tests/controllers/anyone-can-pay.test.ts +++ b/packages/neuron-wallet/tests/controllers/anyone-can-pay.test.ts @@ -1,7 +1,7 @@ import AnyoneCanPayController from '../../src/controllers/anyone-can-pay' import Transaction from '../../src/models/chain/transaction' import { ServiceHasNoResponse } from '../../src/exceptions' -import { ResponseCode } from '../../src/utils/const' +import { ResponseCode, UDTType } from '../../src/utils/const' import AssetAccountInfo from '../../src/models/asset-account-info' import { addressToScript } from '../../src/utils/scriptAndAddress' @@ -133,8 +133,8 @@ describe('anyone-can-pay-controller', () => { it('correct address', async () => { const address = 'ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqvyxgyfu4z8yq4t790um8jef7lpm40h2csv4cv7m' - await anyoneCanPayController.getHoldSudtCellCapacity(address, 'tokenID') - expect(getHoldSUDTCellCapacityMock).toHaveBeenCalledWith(addressToScript(address), 'tokenID') + await anyoneCanPayController.getHoldSudtCellCapacity(address, 'tokenID', UDTType.SUDT) + expect(getHoldSUDTCellCapacityMock).toHaveBeenCalledWith(addressToScript(address), 'tokenID', UDTType.SUDT) }) it('error address', async () => { const address = 'ct1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqvyxgyfu4z8yq4t790um8jef7lpm40h2csv4cv7m' diff --git a/packages/neuron-wallet/tests/models/asset-account-info.test.ts b/packages/neuron-wallet/tests/models/asset-account-info.test.ts index 3ade9fa201..7a21201fef 100644 --- a/packages/neuron-wallet/tests/models/asset-account-info.test.ts +++ b/packages/neuron-wallet/tests/models/asset-account-info.test.ts @@ -1,29 +1,27 @@ +import { predefined } from '@ckb-lumos/config-manager' import AssetAccountInfo from '../../src/models/asset-account-info' import CellDep, { DepType } from '../../src/models/chain/cell-dep' import OutPoint from '../../src/models/chain/out-point' -import { ScriptHashType } from '../../src/models/chain/script' import { hd } from '@ckb-lumos/lumos' import AddressMeta from '../../src/database/address/meta' const { AddressType } = hd describe('AssetAccountInfo', () => { + const { SUDT, ANYONE_CAN_PAY } = predefined.AGGRON4.SCRIPTS const testnetSudtInfo = { - cellDep: new CellDep( - new OutPoint('0xc1b2ae129fad7465aaa9acc9785f842ba3e6e8b8051d899defa89f5508a77958', '0'), - DepType.Code - ), - codeHash: '0x48dbf59b4c7ee1547238021b4869bceedf4eea6b43772e5d66ef8865b6ae7212', - hashType: ScriptHashType.Data, + cellDep: new CellDep(new OutPoint(SUDT.TX_HASH, SUDT.INDEX), SUDT.DEP_TYPE as DepType), + codeHash: SUDT.CODE_HASH, + hashType: SUDT.HASH_TYPE, } const testnetAnyoneCanPayInfo = { cellDep: new CellDep( - new OutPoint('0x4f32b3e39bd1b6350d326fdfafdfe05e5221865c3098ae323096f0bfc69e0a8c', '0'), - DepType.DepGroup + new OutPoint(ANYONE_CAN_PAY.TX_HASH, ANYONE_CAN_PAY.INDEX), + ANYONE_CAN_PAY.DEP_TYPE as DepType ), - codeHash: process.env.TESTNET_ACP_SCRIPT_CODEHASH, - hashType: process.env.TESTNET_ACP_SCRIPT_HASHTYPE, + codeHash: ANYONE_CAN_PAY.CODE_HASH, + hashType: ANYONE_CAN_PAY.HASH_TYPE, } describe('testnet', () => { diff --git a/packages/neuron-wallet/tests/services/anyone-can-pay.test.ts b/packages/neuron-wallet/tests/services/anyone-can-pay.test.ts index 4f259b2a8e..bb6955fb0f 100644 --- a/packages/neuron-wallet/tests/services/anyone-can-pay.test.ts +++ b/packages/neuron-wallet/tests/services/anyone-can-pay.test.ts @@ -5,7 +5,7 @@ import { initConnection, closeConnection, getConnection } from '../setupAndTeard import { AcpSendSameAccountError, TargetLockError, TargetOutputNotFoundError } from '../../src/exceptions' import AssetAccountInfo from '../../src/models/asset-account-info' import SystemScriptInfo from '../../src/models/system-script-info' -import { MIN_SUDT_CAPACITY } from '../../src/utils/const' +import { MIN_SUDT_CAPACITY, UDTType } from '../../src/utils/const' const addressParseMock = jest.fn() jest.mock('../../src/models/address-parser', () => ({ @@ -78,7 +78,9 @@ describe('anyone-can-pay-service', () => { 'tokenName', '8', '0', - '0x62260b4dd406bee8a021185edaa60b7a77f7e99a' + '0x62260b4dd406bee8a021185edaa60b7a77f7e99a', + undefined, + UDTType.SUDT ) ) const ckbAssetAccount = AssetAccountEntity.fromModel( @@ -202,9 +204,9 @@ describe('anyone-can-pay-service', () => { assetAccountEntity.id ) expect(fromObjectMock).toHaveBeenCalledWith({ - capacity: BigInt(MIN_SUDT_CAPACITY).toString(), lock: targetLockScript, type: new AssetAccountInfo().generateSudtScript(assetAccount.tokenID), + data: '0x00000000000000000000000000000000', }) expect(generateAnyoneCanPayToSudtTxMock).toHaveBeenCalledWith( 'walletId', @@ -292,12 +294,13 @@ describe('anyone-can-pay-service', () => { //@ts-ignore await AnyoneCanPayService.getSUDTTargetOutput( SystemScriptInfo.generateSecpScript(assetAccount.blake160), - 'tokenID' + 'tokenID', + UDTType.SUDT ) expect(fromObjectMock).toHaveBeenCalledWith({ - capacity: BigInt(MIN_SUDT_CAPACITY).toString(), lock: SystemScriptInfo.generateSecpScript(assetAccount.blake160), type: new AssetAccountInfo().generateSudtScript('tokenID'), + data: '0x00000000000000000000000000000000', }) }) it('send to anyone pay address not exist', async () => { @@ -305,12 +308,13 @@ describe('anyone-can-pay-service', () => { //@ts-ignore await AnyoneCanPayService.getSUDTTargetOutput( new AssetAccountInfo().generateAnyoneCanPayScript(assetAccount.blake160), - 'tokenID' + 'tokenID', + UDTType.SUDT ) expect(fromObjectMock).toHaveBeenCalledWith({ - capacity: BigInt(MIN_SUDT_CAPACITY).toString(), lock: new AssetAccountInfo().generateAnyoneCanPayScript(assetAccount.blake160), type: new AssetAccountInfo().generateSudtScript('tokenID'), + data: '0x00000000000000000000000000000000', }) }) it('send to anyone pay address exist', async () => { @@ -345,11 +349,15 @@ describe('anyone-can-pay-service', () => { getOneByLockScriptAndTypeScriptMock.mockResolvedValue(undefined) const args = `0x${'0'.repeat(40)}` //@ts-ignore - await AnyoneCanPayService.getSUDTTargetOutput(new AssetAccountInfo().generateChequeScript(args, args), 'tokenID') + await AnyoneCanPayService.getSUDTTargetOutput( + new AssetAccountInfo().generateChequeScript(args, args), + 'tokenID', + UDTType.SUDT + ) expect(fromObjectMock).toHaveBeenCalledWith({ - capacity: BigInt(162 * 10 ** 8).toString(), lock: new AssetAccountInfo().generateChequeScript(args, args), type: new AssetAccountInfo().generateSudtScript('tokenID'), + data: '0x00000000000000000000000000000000', }) }) }) @@ -358,7 +366,8 @@ describe('anyone-can-pay-service', () => { it('is secp256 address', async () => { const res = await AnyoneCanPayService.getHoldSUDTCellCapacity( SystemScriptInfo.generateSecpScript(assetAccount.blake160), - '0x00' + '0x00', + UDTType.SUDT ) expect(res).toBe(undefined) }) @@ -373,7 +382,8 @@ describe('anyone-can-pay-service', () => { getOneByLockScriptAndTypeScriptMock.mockResolvedValue({}) const res = await AnyoneCanPayService.getHoldSUDTCellCapacity( new AssetAccountInfo().generateAnyoneCanPayScript(assetAccount.blake160), - '0x00' + '0x00', + UDTType.SUDT ) expect(res).toBe(undefined) }) @@ -381,7 +391,8 @@ describe('anyone-can-pay-service', () => { getOneByLockScriptAndTypeScriptMock.mockResolvedValue(undefined) const res = await AnyoneCanPayService.getHoldSUDTCellCapacity( new AssetAccountInfo().generateAnyoneCanPayScript(assetAccount.blake160), - '0x00' + `0x${'00'.repeat(32)}`, + UDTType.SUDT ) expect(res).toBe(BigInt(MIN_SUDT_CAPACITY).toString()) }) @@ -389,7 +400,8 @@ describe('anyone-can-pay-service', () => { getOneByLockScriptAndTypeScriptMock.mockResolvedValue(undefined) const res = await AnyoneCanPayService.getHoldSUDTCellCapacity( new AssetAccountInfo().generateChequeScript(assetAccount.blake160, assetAccount.blake160), - '0x00' + `0x${'00'.repeat(32)}`, + UDTType.SUDT ) expect(res).toBe(BigInt(162 * 10 ** 8).toString()) }) diff --git a/packages/neuron-wallet/tests/services/asset-account-service.test.ts b/packages/neuron-wallet/tests/services/asset-account-service.test.ts index ae909fc8e3..b4fea15789 100644 --- a/packages/neuron-wallet/tests/services/asset-account-service.test.ts +++ b/packages/neuron-wallet/tests/services/asset-account-service.test.ts @@ -18,6 +18,7 @@ import SystemScriptInfo from '../../src/models/system-script-info' import Script from '../../src/models/chain/script' import Input from '../../src/models/chain/input' import { keyInfos } from '../setupAndTeardown/public-key-info.fixture' +import { UDTType } from '../../src/utils/const' const stubbedWalletServiceGet = jest.fn() const stubbedGenerateClaimChequeTx = jest.fn() @@ -59,7 +60,8 @@ const generateOutput = ( tokenAmount = '100', customData: string | undefined = undefined, status: OutputStatus = OutputStatus.Live, - lock?: Script + lock?: Script, + udtType?: UDTType ) => { const outputEntity = new OutputEntity() outputEntity.outPointTxHash = randomHex() @@ -76,7 +78,7 @@ const generateOutput = ( outputEntity.data = customData || '0x' outputEntity.hasData = customData ? true : false if (tokenID !== 'CKBytes') { - const type = assetAccountInfo.generateSudtScript(tokenID) + const type = assetAccountInfo.generateUdtScript(tokenID, udtType ?? UDTType.SUDT)! outputEntity.typeCodeHash = type.codeHash outputEntity.typeArgs = type.args outputEntity.typeHashType = type.hashType @@ -251,6 +253,7 @@ describe('AssetAccountService', () => { describe('#getAll', () => { const tokenID = '0x' + '0'.repeat(64) + const xUdtTokenID = '0x' + '1'.repeat(64) describe('with both sUDT and CKB accounts', () => { describe('with live cells', () => { @@ -264,6 +267,17 @@ describe('AssetAccountService', () => { balance: '0', accountName: 'sUDT', blake160, + udtType: UDTType.SUDT, + }), + AssetAccount.fromObject({ + tokenID: xUdtTokenID, + symbol: 'xUDT', + tokenName: 'xUDT', + decimal: '0', + balance: '0', + accountName: 'xUDT', + blake160, + udtType: UDTType.XUDT, }), AssetAccount.fromObject({ tokenID: 'CKBytes', @@ -280,6 +294,7 @@ describe('AssetAccountService', () => { generateOutput(undefined, undefined, undefined, toShannon(1000)), generateOutput(tokenID), generateOutput(tokenID), + generateOutput(xUdtTokenID), ] await createAccounts(assetAccounts, outputs) }) @@ -302,6 +317,17 @@ describe('AssetAccountService', () => { balance: '0', accountName: 'sUDT', blake160, + udtType: UDTType.SUDT, + }), + AssetAccount.fromObject({ + tokenID: xUdtTokenID, + symbol: 'xUDT', + tokenName: 'xUDT', + decimal: '0', + balance: '0', + accountName: 'xUDT', + blake160, + udtType: UDTType.XUDT, }), AssetAccount.fromObject({ tokenID: 'CKBytes', @@ -318,13 +344,35 @@ describe('AssetAccountService', () => { generateOutput(undefined, undefined, undefined, toShannon(1000), undefined, undefined, OutputStatus.Sent), generateOutput(tokenID), generateOutput(tokenID, undefined, undefined, undefined, undefined, undefined, OutputStatus.Sent), + generateOutput( + xUdtTokenID, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + UDTType.XUDT + ), + generateOutput( + xUdtTokenID, + undefined, + undefined, + undefined, + undefined, + undefined, + OutputStatus.Sent, + undefined, + UDTType.XUDT + ), ] await createAccounts(assetAccounts, outputs) }) it('includes balance calculations for both sUDT and CKB accounts', async () => { const result = await AssetAccountService.getAll(walletId) - expect(result.length).toEqual(2) + expect(result.length).toEqual(3) expect(result.find((a: any) => a.tokenID === tokenID)?.balance).toEqual('200') expect(result.find((a: any) => a.tokenID === 'CKBytes')?.balance).toEqual(toShannon(2000 - 61).toString()) }) @@ -507,6 +555,7 @@ describe('AssetAccountService', () => { balance: '0', accountName: 'sUDT', blake160, + udtType: UDTType.SUDT, }), AssetAccount.fromObject({ tokenID: 'CKBytes', @@ -962,7 +1011,9 @@ describe('AssetAccountService', () => { '', '', '0', - '0x0000000000000000000000000000000000000000' + '0x0000000000000000000000000000000000000000', + undefined, + UDTType.SUDT ) const tx = {} beforeEach(async () => { diff --git a/packages/neuron-wallet/tests/services/cells.test.ts b/packages/neuron-wallet/tests/services/cells.test.ts index 41997493ba..d2f7f90637 100644 --- a/packages/neuron-wallet/tests/services/cells.test.ts +++ b/packages/neuron-wallet/tests/services/cells.test.ts @@ -1270,6 +1270,10 @@ describe('CellsService', () => { generateCell('666', OutputStatus.Live, true, typeScript, { lockScript: bobDefaultLock }), // sudt asset generateCell('777', OutputStatus.Live, true, sudtType, { lockScript: bobDefaultLock }), + // xudt asset + generateCell('888', OutputStatus.Live, true, assetAccountInfo.generateXudtScript('0x'), { + lockScript: bobDefaultLock, + }), ] await getConnection().manager.save(cells) }) @@ -1280,10 +1284,10 @@ describe('CellsService', () => { result = await CellsService.getCustomizedAssetCells([publicKeyHash], 1, 10) }) it('returns all items', () => { - expect(result.totalCount).toEqual(7) - expect(result.items.length).toEqual(7) + expect(result.totalCount).toEqual(8) + expect(result.items.length).toEqual(8) const totalCapacity = result.items.reduce((total: number, cell: any) => total + parseInt(cell.capacity), 0) - expect(totalCapacity).toEqual(100 * 3 + 10000 * 2 + 666 + 777) + expect(totalCapacity).toEqual(100 * 3 + 10000 * 2 + 666 + 777 + 888) }) it('attaches setCustomizedAssetInfo to single multisign cells', async () => { const singleMultiSignCells = result.items.filter( @@ -1312,23 +1316,32 @@ describe('CellsService', () => { const cells = result.items.filter((item: any) => item.capacity === '777') expect(cells.length).toEqual(1) expect(cells[0].customizedAssetInfo).toEqual({ - lock: CustomizedLock.SUDT, + lock: '', type: CustomizedType.SUDT, data: '', }) }) + it('attaches setCustomizedAssetInfo to xudt cells', () => { + const cells = result.items.filter((item: any) => item.capacity === '888') + expect(cells.length).toEqual(1) + expect(cells[0].customizedAssetInfo).toEqual({ + lock: '', + type: CustomizedType.XUDT, + data: '', + }) + }) }) describe('within pagination scope', () => { it('returns first page result', async () => { const page = 1 const result = await CellsService.getCustomizedAssetCells([publicKeyHash], page, pageSize) - expect(result.totalCount).toEqual(7) + expect(result.totalCount).toEqual(8) expect(result.items.length).toEqual(pageSize) }) it('returns the remaining cells for the last page', async () => { const page = 2 const result = await CellsService.getCustomizedAssetCells([publicKeyHash], page, pageSize) - expect(result.totalCount).toEqual(7) + expect(result.totalCount).toEqual(8) expect(result.items.length).toEqual(2) }) }) @@ -1336,7 +1349,7 @@ describe('CellsService', () => { it('returns empty result', async () => { const page = 8 const result = await CellsService.getCustomizedAssetCells([publicKeyHash], page, pageSize) - expect(result.totalCount).toEqual(7) + expect(result.totalCount).toEqual(8) expect(result.items.length).toEqual(0) }) }) diff --git a/packages/neuron-wallet/tests/services/multisig.test.ts b/packages/neuron-wallet/tests/services/multisig.test.ts index 2b3ffe6f5d..155cfab9f0 100644 --- a/packages/neuron-wallet/tests/services/multisig.test.ts +++ b/packages/neuron-wallet/tests/services/multisig.test.ts @@ -1,3 +1,12 @@ +jest.doMock('../../src/models/subjects/multisig-config-db-changed-subject', () => ({ + getSubject() { + return { + next: jest.fn(), + subscribe: jest.fn(), + } + }, +})) + import MultisigConfig from '../../src/database/chain/entities/multisig-config' import MultisigConfigModel from '../../src/models/multisig-config' import MultisigService from '../../src/services/multisig' @@ -85,13 +94,14 @@ describe('multisig service', () => { multisigConfigModel.id = res?.id await getConnection().manager.save(multisigOutput) rpcBatchRequestMock.mockResolvedValue([]) - }) + }, 20_000) afterEach(async () => { const connection = getConnection() - await connection.synchronize(true) + await connection.getRepository(MultisigConfig).clear() + await connection.getRepository(MultisigOutput).clear() rpcBatchRequestMock.mockReset() multisigOutputChangedSubjectNextMock.mockReset() - }) + }, 20_000) describe('save multisig config', () => { it('has exist', async () => { diff --git a/packages/neuron-wallet/tests/services/networks.test.ts b/packages/neuron-wallet/tests/services/networks.test.ts index af3ad18a67..98b2b910ca 100644 --- a/packages/neuron-wallet/tests/services/networks.test.ts +++ b/packages/neuron-wallet/tests/services/networks.test.ts @@ -158,21 +158,27 @@ describe(`Unit tests of networks service`, () => { describe(`validation on parameters`, () => { describe(`validation on parameters`, () => { it(`service.create requires name, and remote`, async () => { - expect(service.create(undefined as any, undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) - expect(service.create('network name', undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) + await expect(service.create(undefined as any, undefined as any)).rejects.toThrowError( + t(ERROR_MESSAGE.MISSING_ARG) + ) + await expect(service.create('network name', undefined as any)).rejects.toThrowError( + t(ERROR_MESSAGE.MISSING_ARG) + ) }) - it(`service.update requires id, options`, () => { - expect(service.update(undefined as any, undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) - expect(service.update('', undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) + it(`service.update requires id, options`, async () => { + await expect(service.update(undefined as any, undefined as any)).rejects.toThrowError( + t(ERROR_MESSAGE.MISSING_ARG) + ) + await expect(service.update('', undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) }) - it(`service.delete requires id `, () => { - expect(service.delete(undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) + it(`service.delete requires id `, async () => { + await expect(service.delete(undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) }) - it(`service.activate requires id `, () => { - expect(service.activate(undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) + it(`service.activate requires id `, async () => { + await expect(service.activate(undefined as any)).rejects.toThrowError(t(ERROR_MESSAGE.MISSING_ARG)) }) }) }) @@ -182,18 +188,18 @@ describe(`Unit tests of networks service`, () => { await service.create('Default', 'http://127.0.0.1:8114') }) - it(`create network with existing name of Default`, () => { - expect(service.create('Default', 'http://127.0.0.1:8114')).rejects.toThrowError(t(ERROR_MESSAGE.NAME_USED)) + it(`create network with existing name of Default`, async () => { + await expect(service.create('Default', 'http://127.0.0.1:8114')).rejects.toThrowError(t(ERROR_MESSAGE.NAME_USED)) }) - it(`update network which is not existing`, () => { + it(`update network which is not existing`, async () => { const id = `not-existing-id` - expect(service.update(id, {})).rejects.toThrowError(t(ERROR_MESSAGE.NETWORK_ID_NOT_FOUND, { id })) + await expect(service.update(id, {})).rejects.toThrowError(t(ERROR_MESSAGE.NETWORK_ID_NOT_FOUND, { id })) }) - it(`activate network which is not existing`, () => { + it(`activate network which is not existing`, async () => { const id = `not-existing-id` - expect(service.activate(id)).rejects.toThrowError(t(ERROR_MESSAGE.NETWORK_ID_NOT_FOUND, { id })) + await expect(service.activate(id)).rejects.toThrowError(t(ERROR_MESSAGE.NETWORK_ID_NOT_FOUND, { id })) }) }) @@ -223,6 +229,7 @@ describe(`Unit tests of networks service`, () => { service.readSync = readSyncMock service.writeSync = writeSyncMock service.updateAll = updateAllMock + service.activate = jest.fn() }) afterEach(() => { readSyncMock.mockReset() diff --git a/packages/neuron-wallet/tests/services/sudt-token-info.test.ts b/packages/neuron-wallet/tests/services/sudt-token-info.test.ts index f89fd33088..9ce636567d 100644 --- a/packages/neuron-wallet/tests/services/sudt-token-info.test.ts +++ b/packages/neuron-wallet/tests/services/sudt-token-info.test.ts @@ -4,6 +4,7 @@ import { closeConnection, getConnection, initConnection } from '../setupAndTeard import HdPublicKeyInfo from '../../src/database/chain/entities/hd-public-key-info' import AssetAccountEntity from '../../src/database/chain/entities/asset-account' import accounts from '../setupAndTeardown/accounts.fixture' +import { UDTType } from '../../src/utils/const' const defaultTokenId = '0x' + '0'.repeat(64) @@ -162,19 +163,19 @@ describe('sudt token info service', () => { }) it('no token info', async () => { - await expect(SudtTokenInfoService.getSudtTokenInfo('0x')).resolves.toBeNull() + await expect(SudtTokenInfoService.getSudtTokenInfo('0x', UDTType.SUDT)).resolves.toBeNull() }) it('token info not match', async () => { const entity = AssetAccountEntity.fromModel(assetAccount) await getConnection().manager.save([entity.sudtTokenInfo, entity]) - await expect(SudtTokenInfoService.getSudtTokenInfo(`0x${'00'.repeat(20)}`)).resolves.toBeNull() + await expect(SudtTokenInfoService.getSudtTokenInfo(`0x${'00'.repeat(20)}`, UDTType.SUDT)).resolves.toBeNull() }) it('match token info', async () => { const entity = AssetAccountEntity.fromModel(assetAccount) await getConnection().manager.save([entity.sudtTokenInfo, entity]) - const result = await SudtTokenInfoService.getSudtTokenInfo(assetAccount.tokenID) + const result = await SudtTokenInfoService.getSudtTokenInfo(assetAccount.tokenID, UDTType.SUDT) expect(result).toBeDefined() expect(result?.assetAccounts).toHaveLength(1) }) diff --git a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts index 6e5af862d0..057a0c6018 100644 --- a/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts +++ b/packages/neuron-wallet/tests/services/tx/transaction-generator.test.ts @@ -87,6 +87,7 @@ import AssetAccount from '../../../src/models/asset-account' import MultisigConfigModel from '../../../src/models/multisig-config' import MultisigOutput from '../../../src/database/chain/entities/multisig-output' import { closeConnection, getConnection, initConnection } from '../../setupAndTeardown' +import { UDTType } from '../../../src/utils/const' describe('TransactionGenerator', () => { beforeAll(async () => { @@ -2094,14 +2095,14 @@ describe('TransactionGenerator', () => { describe('generateCreateAnyoneCanPayTx', () => { const feeRate = '1000' it('create ckb', async () => { - const tx = await TransactionGenerator.generateCreateAnyoneCanPayTx( - 'CKBytes', - walletId1, - alice.lockScript.args, - bob.lockScript.args, + const tx = await TransactionGenerator.generateCreateAnyoneCanPayTx({ + tokenID: 'CKBytes', + walletId: walletId1, + blake160: alice.lockScript.args, + changeBlake160: bob.lockScript.args, feeRate, - '0' - ) + fee: '0', + }) // check fee const inputCapacities = tx.inputs.map(i => BigInt(i.capacity ?? 0)).reduce((result, c) => result + c, BigInt(0)) @@ -2129,14 +2130,15 @@ describe('TransactionGenerator', () => { it('create sudt', async () => { const tokenID = '0x' + '0'.repeat(64) - const tx = await TransactionGenerator.generateCreateAnyoneCanPayTx( + const tx = await TransactionGenerator.generateCreateAnyoneCanPayTx({ tokenID, - walletId1, - alice.lockScript.args, - bob.lockScript.args, + walletId: walletId1, + blake160: alice.lockScript.args, + changeBlake160: bob.lockScript.args, feeRate, - '0' - ) + fee: '0', + udtType: UDTType.SUDT, + }) // check fee const inputCapacities = tx.inputs.map(i => BigInt(i.capacity ?? 0)).reduce((result, c) => result + c, BigInt(0)) @@ -2193,7 +2195,7 @@ describe('TransactionGenerator', () => { type: senderAcpLiveCell.type(), data: BufferUtils.writeBigUInt128LE(BigInt(110)), }) - assetAccount = new AssetAccount(tokenID, '', '', '', '', '', alice.lockScript.args) + assetAccount = new AssetAccount(tokenID, '', '', '', '', '', alice.lockScript.args, undefined, UDTType.SUDT) }) describe('with numeric send amount', () => { beforeEach(async () => { @@ -2561,11 +2563,11 @@ describe('TransactionGenerator', () => { }) const bobLockHash = scriptToAddress(bobAnyoneCanPayLockScript) const res = (await TransactionGenerator.generateSudtMigrateAcpTx(sudtCell, bobLockHash)) as Transaction - expect(res.outputs).toHaveLength(3) + expect(res.outputs).toHaveLength(2) expect(res.outputs[1].data).toEqual(BufferUtils.writeBigUInt128LE(BigInt(200))) - expect(res.outputs[2].capacity).toEqual((BigInt(secpCell.capacity) - BigInt(res.fee ?? 0)).toString()) - expect(res.inputs).toHaveLength(3) - expect(res.inputs[2].lockHash).toBe(bobAnyoneCanPayLockScript.computeHash()) + expect(res.outputs[0].capacity).toEqual((BigInt(sudtCell.capacity) - BigInt(res.fee ?? 0)).toString()) + expect(res.inputs).toHaveLength(2) + expect(res.inputs[1].lockHash).toBe(bobAnyoneCanPayLockScript.computeHash()) }) }) @@ -2633,13 +2635,13 @@ describe('TransactionGenerator', () => { const cells: OutputEntity[] = [generateCell(toShannon('100'), OutputStatus.Live, false, null)] await getConnection().manager.save(cells) - const tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance( - 'CKBytes', - walletId1, - alice.lockScript.args, + const tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance({ + tokenID: 'CKBytes', + walletId: walletId1, + blake160: alice.lockScript.args, feeRate, - '0' - ) + fee: '0', + }) // check fee const inputCapacities = tx.inputs.map(i => BigInt(i.capacity ?? 0)).reduce((result, c) => result + c, BigInt(0)) @@ -2667,13 +2669,14 @@ describe('TransactionGenerator', () => { await getConnection().manager.save(cells) const tokenID = '0x' + '0'.repeat(64) - const tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance( + const tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance({ tokenID, - walletId1, - alice.lockScript.args, + walletId: walletId1, + blake160: alice.lockScript.args, feeRate, - '0' - ) + fee: '0', + udtType: UDTType.SUDT, + }) // check fee const inputCapacities = tx.inputs.map(i => BigInt(i.capacity ?? 0)).reduce((result, c) => result + c, BigInt(0)) @@ -2689,6 +2692,35 @@ describe('TransactionGenerator', () => { expect(assetAccountInfo.isAnyoneCanPayScript(output.lock)).toBe(true) expect(output.data).toEqual('0x' + '0'.repeat(32)) }) + + it('create xudt', async () => { + const cells: OutputEntity[] = [generateCell(toShannon('143'), OutputStatus.Live, false, null)] + await getConnection().manager.save(cells) + + const tokenID = '0x' + '0'.repeat(64) + const tx = await TransactionGenerator.generateCreateAnyoneCanPayTxUseAllBalance({ + tokenID, + walletId: walletId1, + blake160: alice.lockScript.args, + feeRate, + fee: '0', + udtType: UDTType.XUDT, + }) + + // check fee + const inputCapacities = tx.inputs.map(i => BigInt(i.capacity ?? 0)).reduce((result, c) => result + c, BigInt(0)) + const outputCapacities = tx.outputs.map(o => BigInt(o.capacity)).reduce((result, c) => result + c, BigInt(0)) + expect(tx.fee).toEqual((inputCapacities - outputCapacities).toString()) + + // check output + expect(tx.outputs.length).toEqual(1) + + const output = tx.outputs[0] + expect(output.capacity).toEqual((BigInt(143 * 10 ** 8) - BigInt(tx.fee ?? 0)).toString()) + expect(assetAccountInfo.isXudtScript(output.type!)).toBe(true) + expect(assetAccountInfo.isAnyoneCanPayScript(output.lock)).toBe(true) + expect(output.data).toEqual('0x' + '0'.repeat(32)) + }) }) describe('#generateMigrateLegacyACPTx', () => { diff --git a/packages/neuron-wallet/tests/setupAndTeardown/accounts.fixture.ts b/packages/neuron-wallet/tests/setupAndTeardown/accounts.fixture.ts index 21d4c06b97..86d25a65fc 100644 --- a/packages/neuron-wallet/tests/setupAndTeardown/accounts.fixture.ts +++ b/packages/neuron-wallet/tests/setupAndTeardown/accounts.fixture.ts @@ -1,4 +1,5 @@ import AssetAccount from '../../src/models/asset-account' +import { UDTType } from '../../src/utils/const' import { DEPLOY_KEY } from './keys' const ASSET_ACCOUNT = { @@ -9,6 +10,7 @@ const ASSET_ACCOUNT = { balance: '0', accountName: 'SUDT Account', blake160: DEPLOY_KEY.blake160, + udtType: UDTType.SUDT, } const CKB_ASSET_ACCOUNT = {