setLockInfo(null)} />
+ }, [lockInfo, isMainnet])
+
const renderList = useCallback(
(cells: Readonly<(State.DetailedInput | State.DetailedOutput)[]>) =>
cells.map((cell, index) => {
@@ -146,8 +155,9 @@ const Transaction = () => {
{index} |
- {address}
+ {`${address.slice(0, 20)}...${address.slice(-20)}`}
+ setLockInfo(cell.lock)} />
|
@@ -211,6 +221,7 @@ const Transaction = () => {
{renderList(transaction.outputs)}
+ {renderLockInfoDialog()}
)
}
diff --git a/packages/neuron-ui/src/components/Transaction/transaction.module.scss b/packages/neuron-ui/src/components/Transaction/transaction.module.scss
index d929ab5a11..cafff3d8cf 100644
--- a/packages/neuron-ui/src/components/Transaction/transaction.module.scss
+++ b/packages/neuron-ui/src/components/Transaction/transaction.module.scss
@@ -34,6 +34,7 @@
height: 1.75rem;
word-wrap: none;
word-break: keep-all;
+
& > div {
text-overflow: ellipsis;
overflow: hidden;
@@ -63,8 +64,13 @@
}
.addressCell {
- max-width: calc(90vw - 416px);
- padding-right: 15px;
+ max-width: calc(90vw - 380px);
+ display: flex;
+ > div {
+ width: inherit;
+ height: 18px;
+ margin-top: 5px;
+ }
}
}
diff --git a/packages/neuron-ui/src/components/TransactionList/index.tsx b/packages/neuron-ui/src/components/TransactionList/index.tsx
index c3bf6fc36a..87b7ac9845 100644
--- a/packages/neuron-ui/src/components/TransactionList/index.tsx
+++ b/packages/neuron-ui/src/components/TransactionList/index.tsx
@@ -111,8 +111,7 @@ const TransactionList = ({
if (btn?.dataset?.hash && btn?.dataset?.action) {
switch (btn.dataset.action) {
case 'explorer': {
- const explorerUrl = isMainnet ? 'https://explorer.nervos.org' : 'https://explorer.nervos.org/aggron'
- openExternal(`${explorerUrl}/transaction/${btn.dataset.hash}`)
+ openExternal(`${getExplorerUrl(isMainnet)}/transaction/${btn.dataset.hash}`)
break
}
case 'detail': {
@@ -142,31 +141,45 @@ const TransactionList = ({
const confirmationsLabel = confirmations > 1000 ? '1,000+' : localNumberFormatter(confirmations)
let name = '--'
- let value = '--'
let amount = '--'
let typeLabel = '--'
let sudtAmount = ''
if (tx.nftInfo) {
+ // NFT
name = walletName
const { type, data } = tx.nftInfo
typeLabel = `${t(`history.${type}`)} m-NFT`
amount = `${type === 'receive' ? '+' : '-'}${nftFormatter(data)}`
} else if (tx.sudtInfo?.sUDT) {
+ // Asset Account
name = tx.sudtInfo.sUDT.tokenName || DEFAULT_SUDT_FIELDS.tokenName
- const type = +tx.sudtInfo.amount <= 0 ? 'send' : 'receive'
- typeLabel = `UDT ${t(`history.${type}`)}`
- value = tx.sudtInfo.amount
+ if (['create', 'destroy'].includes(tx.type)) {
+ // create/destroy an account
+ typeLabel = `${t(`history.${tx.type}`, { name })}`
+ } else {
+ // send/receive to/from an account
+ const type = +tx.sudtInfo.amount <= 0 ? 'send' : 'receive'
+ typeLabel = `UDT ${t(`history.${type}`)}`
+ }
if (tx.sudtInfo.sUDT.decimal) {
- sudtAmount = sudtValueToAmount(value, tx.sudtInfo.sUDT.decimal, true)
+ sudtAmount = sudtValueToAmount(tx.sudtInfo.amount, tx.sudtInfo.sUDT.decimal, true)
amount = `${sUDTAmountFormatter(sudtAmount)} ${tx.sudtInfo.sUDT.symbol}`
}
} else {
+ // normal tx
name = walletName
- value = `${tx.value} shannons`
amount = `${shannonToCKBFormatter(tx.value, true)} CKB`
- typeLabel = tx.nervosDao ? 'Nervos DAO' : t(`history.${tx.type}`)
+ if (tx.type === 'create' || tx.type === 'destroy') {
+ if (tx.assetAccountType === 'CKB') {
+ typeLabel = `${t(`history.${tx.type}`, { name: 'CKB' })}`
+ } else {
+ typeLabel = `${t(`overview.${tx.type}`, { name: 'Unknown' })}`
+ }
+ } else {
+ typeLabel = tx.nervosDao ? 'Nervos DAO' : t(`history.${tx.type}`)
+ }
}
let indicator =
diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts
index 17b4994cc3..25d96d94ee 100644
--- a/packages/neuron-ui/src/containers/Main/hooks.ts
+++ b/packages/neuron-ui/src/containers/Main/hooks.ts
@@ -180,7 +180,15 @@ export const useSubscription = ({
})
const syncStateSubscription = SyncStateSubject.subscribe(
- ({ cacheTipNumber, bestKnownBlockNumber, bestKnownBlockTimestamp, estimate, status }) => {
+ ({
+ cacheTipNumber,
+ bestKnownBlockNumber,
+ bestKnownBlockTimestamp,
+ estimate,
+ status,
+ isLookingValidTarget,
+ validTarget,
+ }) => {
dispatch({
type: NeuronWalletActions.UpdateSyncState,
payload: {
@@ -189,6 +197,8 @@ export const useSubscription = ({
bestKnownBlockTimestamp,
estimate,
status,
+ isLookingValidTarget,
+ validTarget,
},
})
}
diff --git a/packages/neuron-ui/src/containers/Navbar/index.tsx b/packages/neuron-ui/src/containers/Navbar/index.tsx
index 7c5095c4e4..2b6e4be622 100644
--- a/packages/neuron-ui/src/containers/Navbar/index.tsx
+++ b/packages/neuron-ui/src/containers/Navbar/index.tsx
@@ -1,10 +1,10 @@
-import React from 'react'
+import React, { useCallback } from 'react'
import { createPortal } from 'react-dom'
import { useHistory, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useState as useGlobalState } from 'states'
-import { showSettings } from 'services/remote'
+import { openExternal, showSettings } from 'services/remote'
import NetworkStatus from 'components/NetworkStatus'
import SyncStatus from 'components/SyncStatus'
@@ -18,6 +18,8 @@ import {
useOnLocaleChange,
SyncStatus as SyncStatusEnum,
ConnectionStatus,
+ getExplorerUrl,
+ isMainnet,
} from 'utils'
import styles from './navbar.module.scss'
@@ -60,7 +62,7 @@ const Navbar = () => {
chain: {
connectionStatus,
networkID,
- syncState: { cacheTipBlockNumber, bestKnownBlockNumber, estimate, status },
+ syncState: { cacheTipBlockNumber, bestKnownBlockNumber, estimate, status, isLookingValidTarget, validTarget },
},
settings: { wallets = [], networks = [] },
} = neuronWallet
@@ -73,6 +75,15 @@ const Navbar = () => {
const syncStatus = status
+ const onOpenValidTarget = useCallback(
+ (e: React.SyntheticEvent) => {
+ e.stopPropagation()
+ const explorerUrl = getExplorerUrl(isMainnet(networks, networkID))
+ openExternal(`${explorerUrl}/block/${validTarget}`)
+ },
+ [networks, networkID, validTarget]
+ )
+
if (!wallets.length || FULL_SCREENS.find(url => pathname.startsWith(url))) {
return null
}
@@ -146,6 +157,8 @@ const Navbar = () => {
{
toggleAllNotificationVisibility()(dispatch)
}}
+ className={styles.moreBtn}
>
more
diff --git a/packages/neuron-ui/src/exceptions/address.ts b/packages/neuron-ui/src/exceptions/address.ts
index 661595d19e..709e1d87dc 100644
--- a/packages/neuron-ui/src/exceptions/address.ts
+++ b/packages/neuron-ui/src/exceptions/address.ts
@@ -29,3 +29,17 @@ export class AddressDeprecatedException extends Error {
super(`${I18N_PATH}${ErrorCode.AddressIsDeprecated}`)
}
}
+
+export class AddressNotMatchException extends Error {
+ public code = ErrorCode.AddressTypeNotMatch
+ public i18n: {
+ tagName: string
+ }
+
+ constructor(tagName: string) {
+ super(`${I18N_PATH}${ErrorCode.AddressTypeNotMatch}`)
+ this.i18n = {
+ tagName,
+ }
+ }
+}
diff --git a/packages/neuron-ui/src/exceptions/amount.ts b/packages/neuron-ui/src/exceptions/amount.ts
index 5db9cee8e4..f24fd01d78 100644
--- a/packages/neuron-ui/src/exceptions/amount.ts
+++ b/packages/neuron-ui/src/exceptions/amount.ts
@@ -56,10 +56,3 @@ export class AmountNegativeException extends RangeError {
this.i18n.fieldValue = value
}
}
-
-export class BalanceNotEnoughException extends Error {
- public code = ErrorCode.BalanceNotEnough
- constructor() {
- super(`${I18N_PATH}${ErrorCode.BalanceNotEnough}`)
- }
-}
diff --git a/packages/neuron-ui/src/index.tsx b/packages/neuron-ui/src/index.tsx
index 2174051d11..e1bb50de65 100755
--- a/packages/neuron-ui/src/index.tsx
+++ b/packages/neuron-ui/src/index.tsx
@@ -13,6 +13,7 @@ import Main from 'containers/Main'
import Settings from 'containers/Settings'
import Transaction from 'components/Transaction'
import SignAndVerify from 'components/SignAndVerify'
+import MultiSignAddress from 'components/MultisigAddress'
import ErrorBoundary from 'components/ErrorBoundary'
import { withProvider } from 'states'
@@ -20,6 +21,13 @@ if (window.location.hash.startsWith('#/transaction/')) {
ReactDOM.render(, document.getElementById('root'))
} else if (window.location.hash.startsWith('#/sign-verify/')) {
ReactDOM.render(, document.getElementById('root'))
+} else if (window.location.hash.startsWith('#/multisig-address/')) {
+ ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+ )
} else {
const isSettings = window.location.hash.startsWith('#/settings/')
diff --git a/packages/neuron-ui/src/locales/en.json b/packages/neuron-ui/src/locales/en.json
index 9f5bf15788..93c41eb10d 100644
--- a/packages/neuron-ui/src/locales/en.json
+++ b/packages/neuron-ui/src/locales/en.json
@@ -25,7 +25,8 @@
},
"network-status": {
"tooltip": {
- "block-synced": "Block Synced"
+ "block-synced": "Block Synced",
+ "looking-valid-target": "Looking for assumed valid target"
}
},
"import-hardware": {
@@ -105,8 +106,8 @@
"wallet": "Wallet:",
"content": "Content:",
"export": "Export Tx",
- "sign-and-export": "Sign and export",
- "sign-and-broadcast": "Sign and broadcast",
+ "sign-and-export": "Sign & export",
+ "sign-and-broadcast": "Sign & broadcast",
"actions": {
"broadcast": "Broadcast",
"sign": "Sign and export",
@@ -137,7 +138,9 @@
"pending": "Pending",
"confirming": "Confirming"
},
- "copy-balance": "Copy Balance"
+ "copy-balance": "Copy Balance",
+ "create": "Create {{name}} Asset Account",
+ "destroy": "Destroy {{name}} Asset Account"
},
"wizard": {
"welcome-to-nervos-neuron": "Welcome to Neuron",
@@ -209,7 +212,8 @@
"scan-screen-qr-code": "Scan the QR Code on the screen",
"set-locktime": "Set Locktime",
"locktime-notice-content": "According to the actual running block height, there may be some time variances in locktime.",
- "release-on": "Release on"
+ "release-on": "Release on",
+ "locktime-warning": "Please ensure that receiver's wallet can support expiration unlocking. In general, exchanges do not support expiration unlocking, please use with caution!"
},
"receive": {
"title": "Receive",
@@ -262,7 +266,9 @@
"view-detail-button-title": "View Detail",
"copy-tx-hash": "Copy Transaction Hash",
"copy-balance": "Copy Balance",
- "copy-address": "Copy Address"
+ "copy-address": "Copy Address",
+ "create": "Create {{name}} Asset Account",
+ "destroy": "Destroy {{name}} Asset Account"
},
"transaction": {
"window-title": "Transaction: {{hash}}",
@@ -276,7 +282,9 @@
"amount": "Amount",
"inputs": "Inputs",
"outputs": "Outputs",
- "cell-from-cellbase": "From cellbase"
+ "cell-from-cellbase": "From cellbase",
+ "lock-script": "Lock Script",
+ "deprecated-address-format": "Address in deprecated format"
},
"addresses": {
"title": "Address Book",
@@ -391,6 +399,9 @@
"send-acp": {
"title": "Send CKB"
},
+ "send-acp-to-default": {
+ "title": "Send CKB"
+ },
"migrate-acp": {
"title": "Upgrade Asset Accounts"
},
@@ -406,9 +417,16 @@
"create-account-to-claim-cheque": {
"title": "Claim Cheque Asset with New Account"
},
- "destroy-ckb-account": {
- "title": "Destroy CKB Asset Account"
- }
+ "destroy-asset-account": {
+ "title": "Destroy Asset Account"
+ },
+ "send-from-multisig": {
+ "title": "Submit the transaction"
+ },
+ "send-from-multisig-need-one": {
+ "title": "Submit the transaction"
+ },
+ "xpub-notice": "For watch-only wallet, only exporting transactions are supported"
},
"qrcode": {
"copy": "Copy image",
@@ -464,6 +482,7 @@
"experimental-message-hardware": "This is an experimental feature. Please pay attention to the risk and use with caution.",
"experimental-message": "This is an experimental feature, it could change at any time. Please use with caution.",
"rebuild-sync": "For better user experience, Neuron has adopted a brand new indexing mechanism, which requires a rebuilding of cache database (estimated 30 ~ 60min).\nSorry for the inconvenience.",
+ "secp256k1/blake160-address-required": "Secp256k1/blake160 address is required",
"fields": {
"wallet": "Wallet",
"name": "Name",
@@ -523,6 +542,7 @@
"307": "Please enter a testnet address",
"308": "Amount is not enough",
"309": "The receiver needs to upgrade her account address to accept more transfer.",
+ "310": "Please enter a {{tagName}} address",
"402": "CKB App does not open. Please open the CKB App on your device.",
"403": "No device detected. Please connect your device.",
"404": "Multiple device detected. Only one device of the same model can be connected.",
@@ -571,6 +591,7 @@
"minimal-fee-required": "The minimum deposit capacity is {{minimal}} CKBytes",
"compensation-accumulated": "{{blockNumber}} blocks compensation accumulated",
"withdraw-alert": "Hint: there are only {{epochs}} epochs (~{{hours}} hours) until the end of your current lock period. If you wish to withdraw in current lock period, please send withdraw request in time. There are {{nextLeftEpochs}} epochs(~{{days}} days) until the end of your next lock period.",
+ "balance-not-reserved": "(Not recommended) Don't reserve any CKBytes for future DAO operations",
"deposit-record": {
"deposited-at": "Deposited",
"completed-at": "Completed",
@@ -621,25 +642,101 @@
"quit-and-install": "Install and relaunch"
},
"datetime": {
- "mon": { "full": "Monday", "short": "Mon.", "tag": "M" },
- "tue": { "full": "Tuesday", "short": "Tue.", "tag": "Tu" },
- "wed": { "full": "Wednesday", "short": "Wed.", "tag": "W" },
- "thur": { "full": "Thursday", "short": "Thur.", "tag": "T" },
- "fri": { "full": "Friday", "short": "Fri.", "tag": "F" },
- "sat": { "full": "Saturday", "short": "Sat.", "tag": "Sa" },
- "sun": { "full": "Sunday", "short": "Sun.", "tag": "Su" },
- "jan": { "full": "January", "short": "Jan.", "tag": "J" },
- "feb": { "full": "Febrary", "short": "Feb.", "tag": "F" },
- "mar": { "full": "March", "short": "Mar.", "tag": "M" },
- "apr": { "full": "April", "short": "Apr.", "tag": "A" },
- "may": { "full": "May", "short": "May.", "tag": "M" },
- "june": { "full": "June", "short": "June.", "tag": "J" },
- "july": { "full": "July", "short": "July.", "tag": "J" },
- "aug": { "full": "August", "short": "Aug.", "tag": "Aug" },
- "sept": { "full": "September", "short": "Sept.", "tag": "S" },
- "oct": { "full": "October", "short": "Oct.", "tag": "O" },
- "nov": { "full": "November", "short": "Nov.", "tag": "N" },
- "dec": { "full": "December", "short": "Dec.", "tag": "D" },
+ "mon": {
+ "full": "Monday",
+ "short": "Mon.",
+ "tag": "M"
+ },
+ "tue": {
+ "full": "Tuesday",
+ "short": "Tue.",
+ "tag": "Tu"
+ },
+ "wed": {
+ "full": "Wednesday",
+ "short": "Wed.",
+ "tag": "W"
+ },
+ "thur": {
+ "full": "Thursday",
+ "short": "Thur.",
+ "tag": "T"
+ },
+ "fri": {
+ "full": "Friday",
+ "short": "Fri.",
+ "tag": "F"
+ },
+ "sat": {
+ "full": "Saturday",
+ "short": "Sat.",
+ "tag": "Sa"
+ },
+ "sun": {
+ "full": "Sunday",
+ "short": "Sun.",
+ "tag": "Su"
+ },
+ "jan": {
+ "full": "January",
+ "short": "Jan.",
+ "tag": "J"
+ },
+ "feb": {
+ "full": "Febrary",
+ "short": "Feb.",
+ "tag": "F"
+ },
+ "mar": {
+ "full": "March",
+ "short": "Mar.",
+ "tag": "M"
+ },
+ "apr": {
+ "full": "April",
+ "short": "Apr.",
+ "tag": "A"
+ },
+ "may": {
+ "full": "May",
+ "short": "May.",
+ "tag": "M"
+ },
+ "june": {
+ "full": "June",
+ "short": "June.",
+ "tag": "J"
+ },
+ "july": {
+ "full": "July",
+ "short": "July.",
+ "tag": "J"
+ },
+ "aug": {
+ "full": "August",
+ "short": "Aug.",
+ "tag": "Aug"
+ },
+ "sept": {
+ "full": "September",
+ "short": "Sept.",
+ "tag": "S"
+ },
+ "oct": {
+ "full": "October",
+ "short": "Oct.",
+ "tag": "O"
+ },
+ "nov": {
+ "full": "November",
+ "short": "Nov.",
+ "tag": "N"
+ },
+ "dec": {
+ "full": "December",
+ "short": "Dec.",
+ "tag": "D"
+ },
"timezone": "Time Zone",
"start-tomorrow": "Selected time should start from tomorrow."
},
@@ -656,7 +753,10 @@
"confirm": "Confirm",
"verification-success": "Verification Succeeded",
"verification-failure": "Verification Failed",
- "address-not-found": "The given address does not belong to current wallet. Please check your wallet or wait for synchronizing complete."
+ "address-not-found": "The given address does not belong to current wallet. Please check your wallet or wait for synchronizing complete.",
+ "sign-with-magic-byte": "Message will be signed with magic bytes 'Nervos Message:'",
+ "verify-tip": "Could be verified by Neuron from v0.33.1",
+ "verify-old-sign-success": "Message was signed by Neuron before v0.33.1"
},
"special-assets": {
"title": "Customized Assets",
@@ -669,6 +769,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",
"claim-asset": "Claim",
"withdraw-asset": "Withdraw",
"view-details": "View Details",
@@ -676,7 +777,26 @@
"no-special-assets": "No customized assets",
"experimental": "Experimental",
"unknown-asset": "Unknown Asset",
- "transfer-nft": "Send"
+ "transfer-nft": "Send",
+ "user-defined-token": "Migrate"
+ },
+ "migrate-sudt": {
+ "title": "Migrate to a sUDT Asset Account",
+ "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",
+ "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"
+ },
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "balance": "Balance",
+ "amount": "Amount",
+ "address": "Address"
},
"customized-asset": {
"actions": {
@@ -699,6 +819,7 @@
"create-asset-account": "Create Asset Account",
"account-name": "Account Name",
"sudt-account": "sUDT Account",
+ "delete-failed": "Failed to delete the multisig config, reason for failure: {{reason}}",
"ckb-account": "CKB Account",
"set-token-info": "Set Token Info",
"token-id": "Token ID",
@@ -724,7 +845,8 @@
"click-to-edit": "Click to edit",
"cheque-address-hint": "162 CKBytes capacity is going to be temporarily locked to initialize the token transfer, and it will be automatically unlocked after the token being claimed by the receiver.",
"destroy": "Destroy",
- "destroy-desc": "Will returns all CKB of this CKB Asset account to your change address."
+ "destroy-ckb-desc": "Will return all CKB of this CKB Asset account to your change address.",
+ "destroy-sudt-desc": "Will return the asset account occupied CKB to your change address."
},
"receive": {
"notation": "Accept {{symbol}} only"
@@ -739,6 +861,93 @@
"confirm": "Confirm",
"cancel": "Cancel"
}
+ },
+ "multisig-address": {
+ "window-title": "Multisig Addresses",
+ "search": {
+ "placeholder": "search by multisig address,alias"
+ },
+ "add": {
+ "label": "Add"
+ },
+ "import": {
+ "label": "Import"
+ },
+ "export": {
+ "label": "Export"
+ },
+ "ok": "OK",
+ "no-data": "No multisig address",
+ "table": {
+ "address": "address",
+ "alias": "alias",
+ "type": "type",
+ "balance": "balance",
+ "copy-address": "Copy Address",
+ "actions": {
+ "info": "Info",
+ "send": "Send",
+ "approve": "Approve",
+ "delete": "Delete"
+ }
+ },
+ "import-dialog": {
+ "actions": {
+ "cancel": "Cancel",
+ "confirm": "Confirm"
+ },
+ "notice": "Only one configuration can be imported at a time. If the content of the file is an array, the first one is imported by default."
+ },
+ "delete-failed": "Delete multisig config failed, Reason for failure: {{reason}}",
+ "send-ckb": {
+ "title": "Send CKB from {{m}}-of-{{n}} multisig address: <0>0>",
+ "balance": "Balance",
+ "address": "Address",
+ "amount": "Amount",
+ "send": "Send",
+ "cancel": "Cancel",
+ "export": "Export Tx"
+ },
+ "create-dialog": {
+ "title": "Create multisig address",
+ "index": "index",
+ "required": "required",
+ "signer-address": "signer address",
+ "copy-address": "Copy Address",
+ "m-n": {
+ "title": "Please choose the multisig address configuration in the m-of-n type where any m private keys out of a possible n are required to move the asset.",
+ "error": "for m-of-n multisig address, m shouldn't be greater than n"
+ },
+ "multi-address-info": {
+ "title": "Please input the signers for the {{m}}-of-{{n}} multisig address",
+ "view-title": "The {{m}}-of-{{n}} multisig address is",
+ "ckb-address-placeholder": "ckb address"
+ },
+ "multi-list": "Signer list",
+ "actions": {
+ "cancel": "Cancel",
+ "back": "Back",
+ "next": "Next",
+ "confirm": "Confirm"
+ },
+ "duplicate-address-forbid": "Address can't be duplicated"
+ },
+ "approve-dialog": {
+ "title": "Approve with {{m}}-of-{{n}} multisig address <0>0>",
+ "transaction": "Transaction",
+ "cancel": "Cancel",
+ "signAndExport": "Sign & Export",
+ "export": "Export Tx",
+ "signAndBroadcast": "Sign & Broadcast",
+ "broadcast": "Broadcast",
+ "content": "Content:",
+ "status": "Status",
+ "signerApprove": "Lack of {{m}} signer approval(s), including {{r}} from specified signer(s)",
+ "noRSignerApprove": "Lack of {{m}} signer approval(s)",
+ "signed": "Signed",
+ "view-concise-data": "View concise data",
+ "view-raw-data": "View raw data"
+ }
}
}
}
diff --git a/packages/neuron-ui/src/locales/zh-tw.json b/packages/neuron-ui/src/locales/zh-tw.json
index 453acf4513..f6ef417d41 100644
--- a/packages/neuron-ui/src/locales/zh-tw.json
+++ b/packages/neuron-ui/src/locales/zh-tw.json
@@ -25,7 +25,8 @@
},
"network-status": {
"tooltip": {
- "block-synced": "已同步區塊"
+ "block-synced": "已同步區塊",
+ "looking-valid-target": "尋找預設可信區塊"
}
},
"import-hardware": {
@@ -130,7 +131,9 @@
"pending": "已提交",
"confirming": "確認中"
},
- "copy-balance": "複製餘額"
+ "copy-balance": "複製餘額",
+ "create": "創建 {{name}} 資產賬戶",
+ "destroy": "銷毀 {{name}} 資產賬戶"
},
"wizard": {
"welcome-to-nervos-neuron": "歡迎使用 Neuron",
@@ -202,7 +205,8 @@
"scan-screen-qr-code": "掃描螢幕上的二維碼",
"set-locktime": "設置鎖定時間",
"locktime-notice-content": "鎖定時間根據區塊鏈實際運行情況會有一定的誤差。",
- "release-on": "鎖定至"
+ "release-on": "鎖定至",
+ "locktime-warning": "請確保對方錢包能夠支持到期解鎖功能。一般情況下,交易所不支持到期解鎖功能,請謹慎使用!"
},
"receive": {
"title": "收款",
@@ -255,7 +259,9 @@
"view-detail-button-title": "查看詳情",
"copy-tx-hash": "複製交易哈希",
"copy-balance": "複製餘額",
- "copy-address": "複製地址"
+ "copy-address": "複製地址",
+ "create": "創建 {{name}} 資產賬戶",
+ "destroy": "銷毀 {{name}} 資產賬戶"
},
"transaction": {
"window-title": "交易: {{hash}}",
@@ -269,7 +275,9 @@
"amount": "數量",
"inputs": "輸入",
"outputs": "輸出",
- "cell-from-cellbase": "來自 Cellbase"
+ "cell-from-cellbase": "來自 Cellbase",
+ "lock-script": "鎖腳本",
+ "deprecated-address-format": "廢棄格式地址"
},
"addresses": {
"title": "地址簿",
@@ -384,6 +392,9 @@
"send-acp": {
"title": "發起 CKB 交易"
},
+ "send-acp-to-default": {
+ "title": "發起 CKB 交易"
+ },
"migrate-acp": {
"title": "升級資產賬戶"
},
@@ -399,9 +410,16 @@
"create-account-to-claim-cheque": {
"title": "創建賬戶並領取 Cheque 資產"
},
- "destroy-ckb-account": {
- "title": "銷毀 CKB 資產賬戶"
- }
+ "destroy-asset-account": {
+ "title": "銷毀資產賬戶"
+ },
+ "send-from-multisig": {
+ "title": "發起交易"
+ },
+ "send-from-multisig-need-one": {
+ "title": "發起交易"
+ },
+ "xpub-notice": "對於觀察錢包,只支持導出交易"
},
"qrcode": {
"copy": "複製圖片",
@@ -457,6 +475,7 @@
"experimental-message-hardware": "本功能為實驗性功能,請註意風險,謹慎使用。",
"experimental-message": "本頁面為實驗性功能,可能隨時變更。請謹慎使用。",
"rebuild-sync": "為了提供更好的用戶體驗,Neuron 採取了新的索引方案,該方案需要一次性重構緩存數據庫(預期同步時間為 30~60 分鐘)。\n抱歉給您帶來不便。",
+ "secp256k1/blake160-address-required": "請輸入 secp256k1/blake160 地址",
"fields": {
"wallet": "錢包",
"name": "名稱",
@@ -498,7 +517,7 @@
"115": "您需要有足夠的餘額來支付找零(大于 61 CKBytes),或者點擊 'Max' 按鈕發送全部餘額。",
"201": "缺少$t(messages.fields.{{fieldName}})。",
"202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 已被使用。",
- "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太長,其長度應不超過 {{length}}。",
+ "203": "$t(messages.fields.{{fieldName}})太長,其長度應不超過 {{length}}。",
"204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太短,其長度應不小於 {{length}}。",
"205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 無效。",
"206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 無效,其小數位應不超過 {{length}}比特。",
@@ -516,6 +535,7 @@
"307": "請輸入測試網地址",
"308": "餘額不足",
"309": "收款人需要升級資產賬戶才能繼續接收轉賬。",
+ "310": "請輸入 {{tagName}} 地址",
"402": "CKB 應用未打開。請在妳的設備打開 CKB 應用。",
"403": "未檢測到設備,請檢查妳的設備連接。",
"404": "檢測到多個設備,同一型號的設備只能同時連接一個。",
@@ -564,6 +584,7 @@
"minimal-fee-required": "存入數量應不少於 {{minimal}} CKBytes",
"compensation-accumulated": "已累計 {{blockNumber}} 個塊的鎖定補貼",
"withdraw-alert": "提示:本補貼申請距離 Nervos DAO 規則允許的最近一個鎖定週期僅剩下 {{epochs}} 個 epoch (約 {{hours}} 小時)。如果您希望在本鎖定週期取出,請及時提交取出申請,以確保取出申請能在本鎖定週期結束之前上鏈。下一個鎖定週期的結束時間預計為 {{nextLeftEpochs}} 個 epochs (約 {{days}} 天)。",
+ "balance-not-reserved": "(不建議)不預留未來操作手續費",
"deposit-record": {
"deposited-at": "存入於",
"completed-at": "已取出",
@@ -614,25 +635,101 @@
"quit-and-install": "安裝並重啓應用"
},
"datetime": {
- "mon": { "full": "周一", "short": "周一", "tag": "周一" },
- "tue": { "full": "周二", "short": "周二", "tag": "周二" },
- "wed": { "full": "周三", "short": "周三", "tag": "周三" },
- "thur": { "full": "周四", "short": "周四", "tag": "周四" },
- "fri": { "full": "周五", "short": "周五", "tag": "周五" },
- "sat": { "full": "周六", "short": "周六", "tag": "周六" },
- "sun": { "full": "周日", "short": "周日", "tag": "周日" },
- "jan": { "full": "一月", "short": "一月", "tag": "一月" },
- "feb": { "full": "二月", "short": "二月", "tag": "二月" },
- "mar": { "full": "三月", "short": "三月", "tag": "三月" },
- "apr": { "full": "四月", "short": "四月", "tag": "四月" },
- "may": { "full": "五月", "short": "五月", "tag": "五月" },
- "june": { "full": "六月", "short": "六月", "tag": "六月" },
- "july": { "full": "七月", "short": "七月", "tag": "七月" },
- "aug": { "full": "八月", "short": "八月", "tag": "八月" },
- "sept": { "full": "九月", "short": "九月", "tag": "九月" },
- "oct": { "full": "十月", "short": "十月", "tag": "十月" },
- "nov": { "full": "十一月", "short": "十一月", "tag": "十一月" },
- "dec": { "full": "十二月", "short": "十二月", "tag": "十二月" },
+ "mon": {
+ "full": "周一",
+ "short": "周一",
+ "tag": "周一"
+ },
+ "tue": {
+ "full": "周二",
+ "short": "周二",
+ "tag": "周二"
+ },
+ "wed": {
+ "full": "周三",
+ "short": "周三",
+ "tag": "周三"
+ },
+ "thur": {
+ "full": "周四",
+ "short": "周四",
+ "tag": "周四"
+ },
+ "fri": {
+ "full": "周五",
+ "short": "周五",
+ "tag": "周五"
+ },
+ "sat": {
+ "full": "周六",
+ "short": "周六",
+ "tag": "周六"
+ },
+ "sun": {
+ "full": "周日",
+ "short": "周日",
+ "tag": "周日"
+ },
+ "jan": {
+ "full": "一月",
+ "short": "一月",
+ "tag": "一月"
+ },
+ "feb": {
+ "full": "二月",
+ "short": "二月",
+ "tag": "二月"
+ },
+ "mar": {
+ "full": "三月",
+ "short": "三月",
+ "tag": "三月"
+ },
+ "apr": {
+ "full": "四月",
+ "short": "四月",
+ "tag": "四月"
+ },
+ "may": {
+ "full": "五月",
+ "short": "五月",
+ "tag": "五月"
+ },
+ "june": {
+ "full": "六月",
+ "short": "六月",
+ "tag": "六月"
+ },
+ "july": {
+ "full": "七月",
+ "short": "七月",
+ "tag": "七月"
+ },
+ "aug": {
+ "full": "八月",
+ "short": "八月",
+ "tag": "八月"
+ },
+ "sept": {
+ "full": "九月",
+ "short": "九月",
+ "tag": "九月"
+ },
+ "oct": {
+ "full": "十月",
+ "short": "十月",
+ "tag": "十月"
+ },
+ "nov": {
+ "full": "十一月",
+ "short": "十一月",
+ "tag": "十一月"
+ },
+ "dec": {
+ "full": "十二月",
+ "short": "十二月",
+ "tag": "十二月"
+ },
"timezone": "時區",
"start-tomorrow": "所選時間不能早於明天。"
},
@@ -649,7 +746,10 @@
"confirm": "確認",
"verification-success": "驗證成功",
"verification-failure": "驗證失敗",
- "address-not-found": "當前錢包地址列表中不包含輸入地址,請檢查錢包設置或等待同步完成。"
+ "address-not-found": "當前錢包地址列表中不包含輸入地址,請檢查錢包設置或等待同步完成。",
+ "sign-with-magic-byte": "信息會和 magic bytes 'Nervos Message:' 一起簽名",
+ "verify-tip": "可以通過 v0.33.1 及以後的 Neuron 進行驗證",
+ "verify-old-sign-success": "消息由 Neuron v0.33.1 之前的版本簽名"
},
"special-assets": {
"title": "自定義資產",
@@ -662,6 +762,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 賬戶",
"claim-asset": "領取",
"withdraw-asset": "撤回",
"view-details": "查看詳情",
@@ -669,7 +770,26 @@
"no-special-assets": "沒有自定義資產",
"experimental": "實驗室",
"unknown-asset": "未知資產",
- "transfer-nft": "轉讓"
+ "transfer-nft": "轉讓",
+ "user-defined-token": "遷移"
+ },
+ "migrate-sudt": {
+ "title": "遷移至 sUDT 資產賬戶",
+ "turn-into-new-account": {
+ "title": "轉換成新的 sUDT 資產賬戶",
+ "sub-title": "將 sUDT 資產轉換成 sUDT 資產賬戶, 至少占用 142 CKBytes",
+ "cancel": "取消",
+ "confirm": "確認"
+ },
+ "transfer-to-exist-account": {
+ "title": "轉入 sUDT 賬戶",
+ "sub-title": "將全部 sUDT 余額轉入已存在的 sUDT 資產賬戶, 請確保目標賬戶存在"
+ },
+ "cancel": "取消",
+ "confirm": "確認",
+ "balance": "余額",
+ "amount": "數量",
+ "address": "地址"
},
"customized-asset": {
"actions": {
@@ -717,7 +837,8 @@
"click-to-edit": "點擊編輯",
"cheque-address-hint": "向該地址轉賬需要臨時鎖定 162 CKB,對方領取後自動解鎖。",
"destroy": "銷毀",
- "destroy-desc": "將會返還所有該 CKB 資產賬戶的 CKB 到你的找零地址中"
+ "destroy-ckb-desc": "將會返還所有該 CKB 資產賬戶的 CKB 到你的找零地址中",
+ "destroy-sudt-desc": "將會返還該資產賬戶所占用的 CKB 到你的找零地址中"
},
"receive": {
"notation": "只接收 {{symbol}} 轉賬"
@@ -732,6 +853,93 @@
"confirm": "確認",
"cancel": "取消"
}
+ },
+ "multisig-address": {
+ "window-title": "多簽地址",
+ "search": {
+ "placeholder": "使用地址或者别名搜索"
+ },
+ "add": {
+ "label": "添加"
+ },
+ "import": {
+ "label": "導入"
+ },
+ "export": {
+ "label": "導出"
+ },
+ "ok": "好的",
+ "no-data": "沒有多簽地址",
+ "table": {
+ "address": "地址",
+ "alias": "别名",
+ "type": "類型",
+ "balance": "余额",
+ "copy-address": "復製地址",
+ "actions": {
+ "info": "詳情",
+ "send": "轉賬",
+ "approve": "確認",
+ "delete": "删除"
+ }
+ },
+ "import-dialog": {
+ "actions": {
+ "cancel": "取消",
+ "confirm": "確認"
+ },
+ "notice": "導入配置一次只能導入一個,如果是文件內容是數組默認取第一個導入"
+ },
+ "delete-failed": "刪除多簽配置失敗,失敗原因:{{reason}}",
+ "send-ckb": {
+ "title": "從 {{m}}-of-{{n}} 多簽地址 <0>0> 轉賬",
+ "balance": "余額",
+ "address": "地址",
+ "amount": "金額",
+ "send": "發送",
+ "cancel": "取消",
+ "export": "導出交易"
+ },
+ "create-dialog": {
+ "title": "創建多重簽名地址",
+ "index": "序號",
+ "required": "必選",
+ "signer-address": "簽名者地址",
+ "copy-address": "復製地址",
+ "m-n": {
+ "title": "請選擇 m-of-n 類型的多重簽名地址配置,從 n 個密鑰中通過 m 個確認來轉移資產。",
+ "error": "對於 m-of-n 多重簽名地址,m 不應大於 n"
+ },
+ "multi-address-info": {
+ "title": "對於 {{m}}-of-{{n}}, 請輸入 {{n}} 個簽名者地址",
+ "view-title": " {{m}}-of-{{n}} 的多簽地址是",
+ "ckb-address-placeholder": "ckb address"
+ },
+ "multi-list": "多簽地址列表",
+ "actions": {
+ "cancel": "取消",
+ "back": "上一步",
+ "next": "下一步",
+ "confirm": "確認"
+ },
+ "duplicate-address-forbid": "地址不能重復"
+ },
+ "approve-dialog": {
+ "title": "從{{m}}-of-{{n}}多簽地址<0>0>確認交易",
+ "transaction": "交易",
+ "cancel": "取消",
+ "signAndExport": "簽名並導出",
+ "export": "導出交易",
+ "signAndBroadcast": "簽名並廣播",
+ "broadcast": "廣播交易",
+ "content": "內容:",
+ "status": "狀態",
+ "signerApprove": "缺少 {{m}} 個簽名者同意, 其中包括 {{r}} 個指定簽名者",
+ "noRSignerApprove": "缺少 {{m}} 個簽名者同意",
+ "signed": "已簽名",
+ "view-concise-data": "查看概要",
+ "view-raw-data": "查看原始數據"
+ }
}
}
}
diff --git a/packages/neuron-ui/src/locales/zh.json b/packages/neuron-ui/src/locales/zh.json
index 42194cb660..102b5829d6 100644
--- a/packages/neuron-ui/src/locales/zh.json
+++ b/packages/neuron-ui/src/locales/zh.json
@@ -25,7 +25,8 @@
},
"network-status": {
"tooltip": {
- "block-synced": "已同步区块"
+ "block-synced": "已同步区块",
+ "looking-valid-target": "寻找预设可信区块"
}
},
"import-hardware": {
@@ -130,7 +131,9 @@
"pending": "已提交",
"confirming": "确认中"
},
- "copy-balance": "复制余额"
+ "copy-balance": "复制余额",
+ "create": "创建 {{name}} 资产账户",
+ "destroy": "销毁 {{name}} 资产账户"
},
"wizard": {
"welcome-to-nervos-neuron": "欢迎使用 Neuron",
@@ -202,7 +205,8 @@
"scan-screen-qr-code": "扫描屏幕上的二维码",
"set-locktime": "设置锁定时间",
"locktime-notice-content": "锁定时间根据区块链实际运行情况会有一定的误差。",
- "release-on": "锁定至"
+ "release-on": "锁定至",
+ "locktime-warning": "请确保对方钱包能够支持到期解锁功能。一般情况下,交易所不支持到期解锁功能,请谨慎使用!"
},
"receive": {
"title": "收款",
@@ -255,7 +259,9 @@
"view-detail-button-title": "查看详情",
"copy-tx-hash": "复制交易哈希",
"copy-balance": "复制余额",
- "copy-address": "复制地址"
+ "copy-address": "复制地址",
+ "create": "创建 {{name}} 资产账户",
+ "destroy": "销毁 {{name}} 资产账户"
},
"transaction": {
"window-title": "交易: {{hash}}",
@@ -269,7 +275,9 @@
"amount": "数量",
"inputs": "输入",
"outputs": "输出",
- "cell-from-cellbase": "来自 Cellbase"
+ "cell-from-cellbase": "来自 Cellbase",
+ "lock-script": "锁脚本",
+ "deprecated-address-format": "废弃格式地址"
},
"addresses": {
"title": "地址簿",
@@ -384,6 +392,9 @@
"send-acp": {
"title": "发起 CKB 交易"
},
+ "send-acp-to-default": {
+ "title": "发起 CKB 交易"
+ },
"migrate-acp": {
"title": "升级资产账户"
},
@@ -399,9 +410,16 @@
"create-account-to-claim-cheque": {
"title": "创建账户并领取 Cheque 资产"
},
- "destroy-ckb-account": {
- "title": "销毁 CKB 资产账户"
- }
+ "destroy-asset-account": {
+ "title": "销毁资产账户"
+ },
+ "send-from-multisig": {
+ "title": "发起交易"
+ },
+ "send-from-multisig-need-one": {
+ "title": "发起交易"
+ },
+ "xpub-notice": "对于观察钱包,只支持导出交易"
},
"qrcode": {
"copy": "复制图片",
@@ -457,6 +475,7 @@
"experimental-message-hardware": "本功能为实验性功能,请注意风险,谨慎使用。",
"experimental-message": "本页面为实验性功能,可能随时变更。请谨慎使用。",
"rebuild-sync": "为了提供更好的用户体验,Neuron 采取了新的索引方案,该方案需要一次性重构缓存数据库(预期同步时间为 30~60 分钟)。\n抱歉给您带来不便。",
+ "secp256k1/blake160-address-required": "请输入 secp256k1/blake160 地址",
"fields": {
"wallet": "钱包",
"name": "名称",
@@ -498,7 +517,7 @@
"115": "您需要有足够的余额来支付找零(大于 61 CKBytes),或者点击 'Max' 按钮发送全部余额。",
"201": "缺少$t(messages.fields.{{fieldName}})。",
"202": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 已被使用。",
- "203": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太长, 其长度应不超过 {{length}}。",
+ "203": "$t(messages.fields.{{fieldName}})太长, 其长度应不超过 {{length}}。",
"204": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 太短, 其长度应不小于 {{length}}。",
"205": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效。",
"206": "$t(messages.fields.{{fieldName}}) {{fieldValue}} 无效, 其小数位应不超过 {{length}} 位。",
@@ -516,6 +535,7 @@
"307": "请输入测试网地址",
"308": "余额不足",
"309": "收款人需要升级资产账户才能继续接收转账。",
+ "310": "请输入 {{tagName}} 地址",
"402": "CKB 应用未打开。请在你的设备打开 CKB 应用。",
"403": "未检测到设备,请检查你的设备连接。",
"404": "检查到多个设备,同一型号的设备只能同时连接一个。",
@@ -564,6 +584,7 @@
"minimal-fee-required": "存入数量应不少于 {{minimal}} CKBytes",
"compensation-accumulated": "已累计 {{blockNumber}} 个块的锁定补贴",
"withdraw-alert": "提示:本补贴申请距离 Nervos DAO 规则允许的最近一个锁定周期仅剩下 {{epochs}} 个 epoch (约 {{hours}} 小时)。 如果您希望在本锁定周期取出,请及时提交取出申请,以确保取出申请能在本锁定周期结束之前上链。下一个锁定周期的结束时间预计为 {{nextLeftEpochs}} 个 epochs (约 {{days}} 天)。",
+ "balance-not-reserved": "(不建议)不预留未来操作手续费",
"deposit-record": {
"deposited-at": "存入于",
"completed-at": "已取出",
@@ -614,25 +635,101 @@
"quit-and-install": "安装并重启应用"
},
"datetime": {
- "mon": { "full": "周一", "short": "周一", "tag": "周一" },
- "tue": { "full": "周二", "short": "周二", "tag": "周二" },
- "wed": { "full": "周三", "short": "周三", "tag": "周三" },
- "thur": { "full": "周四", "short": "周四", "tag": "周四" },
- "fri": { "full": "周五", "short": "周五", "tag": "周五" },
- "sat": { "full": "周六", "short": "周六", "tag": "周六" },
- "sun": { "full": "周日", "short": "周日", "tag": "周日" },
- "jan": { "full": "一月", "short": "一月", "tag": "一月" },
- "feb": { "full": "二月", "short": "二月", "tag": "二月" },
- "mar": { "full": "三月", "short": "三月", "tag": "三月" },
- "apr": { "full": "四月", "short": "四月", "tag": "四月" },
- "may": { "full": "五月", "short": "五月", "tag": "五月" },
- "june": { "full": "六月", "short": "六月", "tag": "六月" },
- "july": { "full": "七月", "short": "七月", "tag": "七月" },
- "aug": { "full": "八月", "short": "八月", "tag": "八月" },
- "sept": { "full": "九月", "short": "九月", "tag": "九月" },
- "oct": { "full": "十月", "short": "十月", "tag": "十月" },
- "nov": { "full": "十一月", "short": "十一月", "tag": "十一月" },
- "dec": { "full": "十二月", "short": "十二月", "tag": "十二月" },
+ "mon": {
+ "full": "周一",
+ "short": "周一",
+ "tag": "周一"
+ },
+ "tue": {
+ "full": "周二",
+ "short": "周二",
+ "tag": "周二"
+ },
+ "wed": {
+ "full": "周三",
+ "short": "周三",
+ "tag": "周三"
+ },
+ "thur": {
+ "full": "周四",
+ "short": "周四",
+ "tag": "周四"
+ },
+ "fri": {
+ "full": "周五",
+ "short": "周五",
+ "tag": "周五"
+ },
+ "sat": {
+ "full": "周六",
+ "short": "周六",
+ "tag": "周六"
+ },
+ "sun": {
+ "full": "周日",
+ "short": "周日",
+ "tag": "周日"
+ },
+ "jan": {
+ "full": "一月",
+ "short": "一月",
+ "tag": "一月"
+ },
+ "feb": {
+ "full": "二月",
+ "short": "二月",
+ "tag": "二月"
+ },
+ "mar": {
+ "full": "三月",
+ "short": "三月",
+ "tag": "三月"
+ },
+ "apr": {
+ "full": "四月",
+ "short": "四月",
+ "tag": "四月"
+ },
+ "may": {
+ "full": "五月",
+ "short": "五月",
+ "tag": "五月"
+ },
+ "june": {
+ "full": "六月",
+ "short": "六月",
+ "tag": "六月"
+ },
+ "july": {
+ "full": "七月",
+ "short": "七月",
+ "tag": "七月"
+ },
+ "aug": {
+ "full": "八月",
+ "short": "八月",
+ "tag": "八月"
+ },
+ "sept": {
+ "full": "九月",
+ "short": "九月",
+ "tag": "九月"
+ },
+ "oct": {
+ "full": "十月",
+ "short": "十月",
+ "tag": "十月"
+ },
+ "nov": {
+ "full": "十一月",
+ "short": "十一月",
+ "tag": "十一月"
+ },
+ "dec": {
+ "full": "十二月",
+ "short": "十二月",
+ "tag": "十二月"
+ },
"timezone": "时区",
"start-tomorrow": "所选时间不能早于明天。"
},
@@ -649,7 +746,10 @@
"confirm": "确认",
"verification-success": "验证成功",
"verification-failure": "验证失败",
- "address-not-found": "当前钱包地址列表中不包含输入地址,请检查钱包设置或等待同步完成。"
+ "address-not-found": "当前钱包地址列表中不包含输入地址,请检查钱包设置或等待同步完成。",
+ "sign-with-magic-byte": "信息会和 magic bytes 'Nervos Message:' 一起签名",
+ "verify-tip": "可以通过 v0.33.1 及以后的 Neuron 进行验证",
+ "verify-old-sign-success": "消息由 Neuron v0.33.1 之前的版本签名"
},
"special-assets": {
"title": "自定义资产",
@@ -662,6 +762,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 账户",
"claim-asset": "领取",
"withdraw-asset": "撤回",
"view-details": "查看详情",
@@ -669,7 +770,26 @@
"no-special-assets": "没有自定义资产",
"experimental": "实验室",
"unknown-asset": "未知资产",
- "transfer-nft": "转让"
+ "transfer-nft": "转让",
+ "user-defined-token": "迁移"
+ },
+ "migrate-sudt": {
+ "title": "迁移至 sUDT 资产账户",
+ "turn-into-new-account": {
+ "title": "转换成新的 sUDT 资产账户",
+ "sub-title": "将 sUDT 资产转换成 sUDT 资产账户, 至少占用 142 CKBytes",
+ "cancel": "取消",
+ "confirm": "确认"
+ },
+ "transfer-to-exist-account": {
+ "title": "转入 sUDT 账户",
+ "sub-title": "将全部 sUDT 余额转入已存在的 sUDT 资产账户, 请确保目标账户存在"
+ },
+ "cancel": "取消",
+ "confirm": "确认",
+ "balance": "余额",
+ "amount": "数量",
+ "address": "地址"
},
"customized-asset": {
"actions": {
@@ -717,7 +837,8 @@
"click-to-edit": "点击编辑",
"cheque-address-hint": "向该地址转账需要临时锁定 162 CKB,对方领取后自动解锁。",
"destroy": "销毁",
- "destroy-desc": "将会返还所有该 CKB 资产账户的 CKB 到你的找零地址中"
+ "destroy-ckb-desc": "将会返还所有该 CKB 资产账户的 CKB 到你的找零地址中",
+ "destroy-sudt-desc": "将会返还该资产账户所占用的 CKB 到你的找零地址中"
},
"receive": {
"notation": "只接收 {{symbol}} 转账"
@@ -732,6 +853,93 @@
"confirm": "确认",
"cancel": "取消"
}
+ },
+ "multisig-address": {
+ "window-title": "多签地址",
+ "search": {
+ "placeholder": "使用地址或者别名搜索"
+ },
+ "add": {
+ "label": "添加"
+ },
+ "import": {
+ "label": "导入"
+ },
+ "export": {
+ "label": "导出"
+ },
+ "ok": "好的",
+ "no-data": "没有多签地址",
+ "table": {
+ "address": "地址",
+ "alias": "别名",
+ "type": "类型",
+ "balance": "余额",
+ "copy-address": "复制地址",
+ "actions": {
+ "info": "详情",
+ "send": "转账",
+ "approve": "确认",
+ "delete": "删除"
+ }
+ },
+ "import-dialog": {
+ "actions": {
+ "cancel": "取消",
+ "confirm": "确认"
+ },
+ "notice": "导入配置一次只能导入一个,如果是文件内容是数组默认取第一个导入"
+ },
+ "delete-failed": "删除多签配置失败,失败原因:{{reason}}",
+ "send-ckb": {
+ "title": "从 {{m}}-of-{{n}} 多签地址 <0>0> 转账",
+ "balance": "余额",
+ "address": "地址",
+ "amount": "金额",
+ "send": "发送",
+ "cancel": "取消",
+ "export": "导出交易"
+ },
+ "create-dialog": {
+ "title": "创建多重签名地址",
+ "index": "序号",
+ "required": "必选",
+ "signer-address": "签名者地址",
+ "copy-address": "复制地址",
+ "m-n": {
+ "title": "请选择 m-of-n 类型的多重签名地址配置,从 n 个密钥中通过 m 个确认来转移资产。",
+ "error": "对于 m-of-n 多重签名地址,m 不应大于 n"
+ },
+ "multi-address-info": {
+ "title": "对于 {{m}}-of-{{n}}, 请输入 {{n}} 个签名者地址",
+ "view-title": " {{m}}-of-{{n}} 的多签地址是",
+ "ckb-address-placeholder": "ckb address"
+ },
+ "multi-list": "多签地址列表",
+ "actions": {
+ "cancel": "取消",
+ "back": "上一步",
+ "next": "下一步",
+ "confirm": "确认"
+ },
+ "duplicate-address-forbid": "地址不能重复"
+ },
+ "approve-dialog": {
+ "title": "从{{m}}-of-{{n}}多签地址<0>0>确认交易",
+ "transaction": "交易",
+ "cancel": "取消",
+ "signAndExport": "签名并导出",
+ "export": "导出交易",
+ "signAndBroadcast": "签名并广播",
+ "broadcast": "广播交易",
+ "content": "内容:",
+ "status": "状态",
+ "signerApprove": "缺少 {{m}} 个签名者同意, 其中包括 {{r}} 个指定签名者",
+ "noRSignerApprove": "缺少 {{m}} 个签名者同意",
+ "signed": "已签名",
+ "view-concise-data": "查看概要",
+ "view-raw-data": "查看原始数据"
+ }
}
}
}
diff --git a/packages/neuron-ui/src/services/chain.ts b/packages/neuron-ui/src/services/chain.ts
index 08b6ea6952..ecda9466ea 100644
--- a/packages/neuron-ui/src/services/chain.ts
+++ b/packages/neuron-ui/src/services/chain.ts
@@ -1,16 +1,11 @@
import CKBCore from '@nervosnetwork/ckb-sdk-core'
export const ckbCore = new CKBCore('')
-export const {
- getHeader,
- getBlock,
- getBlockchainInfo,
- getTipHeader,
- getHeaderByNumber,
- calculateDaoMaximumWithdraw,
-} = ckbCore.rpc
+export const { getHeader, getBlock, getBlockchainInfo, getTipHeader, getHeaderByNumber } = ckbCore.rpc
+
+export const { calculateDaoMaximumWithdraw } = ckbCore
-export const { toUint64Le } = ckbCore.utils
+export const { toUint64Le, parseEpoch } = ckbCore.utils
export default {
ckbCore,
getBlock,
diff --git a/packages/neuron-ui/src/services/remote/app.ts b/packages/neuron-ui/src/services/remote/app.ts
index e4bf19241c..6b0f8a050b 100644
--- a/packages/neuron-ui/src/services/remote/app.ts
+++ b/packages/neuron-ui/src/services/remote/app.ts
@@ -5,6 +5,7 @@ import { remoteApi } from './remoteApiWrapper'
export const getSystemCodeHash = remoteApi('get-system-codehash')
export const getNeuronWalletState = remoteApi('load-init-data')
export const openInWindow = remoteApi('open-in-window')
+export const requestOpenInExplorer = remoteApi('request-open-in-explorer')
export const handleViewError = remoteApi('handle-view-error')
export const showSettings = remoteApi('show-settings')
export const setLocale = remoteApi('set-locale')
diff --git a/packages/neuron-ui/src/services/remote/index.ts b/packages/neuron-ui/src/services/remote/index.ts
index 9376f26bc7..e98f79e525 100644
--- a/packages/neuron-ui/src/services/remote/index.ts
+++ b/packages/neuron-ui/src/services/remote/index.ts
@@ -19,6 +19,7 @@ export * from './cheque'
export * from './hardware'
export * from './offline'
export * from './nft'
+export * from './multisig'
const REMOTE_MODULE_NOT_FOUND =
'The remote module is not found, please make sure the UI is running inside the Electron App'
diff --git a/packages/neuron-ui/src/services/remote/multisig.ts b/packages/neuron-ui/src/services/remote/multisig.ts
new file mode 100644
index 0000000000..3e8f90d1ee
--- /dev/null
+++ b/packages/neuron-ui/src/services/remote/multisig.ts
@@ -0,0 +1,46 @@
+import { remoteApi } from './remoteApiWrapper'
+import { OfflineSignJSON } from './offline'
+
+interface MultisigParams {
+ r: number
+ m: number
+ n: number
+ addresses: string[]
+ isMainnet: boolean
+}
+
+export interface MultisigConfig {
+ id: number
+ walletId: string
+ r: number
+ m: number
+ n: number
+ addresses: string[]
+ alias?: string
+ fullPayload: string
+}
+
+export const createMultisigAddress = remoteApi('create-multisig-address')
+export const saveMultisigConfig = remoteApi, MultisigConfig>('save-multisig-config')
+export const getMultisigConfig = remoteApi<{ walletId: string }>('get-multisig-config')
+export const importMultisigConfig = remoteApi<{ isMainnet: boolean; walletId: string }, MultisigConfig[]>(
+ 'import-multisig-config'
+)
+export const exportMultisigConfig = remoteApi('export-multisig-config')
+export const updateMultisigConfig = remoteApi<{ id: number } & Omit, 'id'>, MultisigConfig>(
+ 'update-multisig-config'
+)
+export const deleteMultisigConfig = remoteApi('delete-multisig-config')
+export const getMultisigBalances = remoteApi<
+ { isMainnet: boolean; multisigAddresses: string[] },
+ Record
+>('get-multisig-balances')
+export const generateMultisigTx = remoteApi<{
+ items: { address: string; capacity: string }[]
+ multisigConfig: MultisigConfig
+}>('generate-multisig-tx')
+export const generateMultisigSendAllTx = remoteApi<{
+ items: { address: string; capacity: string }[]
+ multisigConfig: MultisigConfig
+}>('generate-multisig-send-all-tx')
+export const loadMultisigTxJson = remoteApi('load-multisig-tx-json')
diff --git a/packages/neuron-ui/src/services/remote/offline.ts b/packages/neuron-ui/src/services/remote/offline.ts
index 8a4c1e122a..c349a61757 100644
--- a/packages/neuron-ui/src/services/remote/offline.ts
+++ b/packages/neuron-ui/src/services/remote/offline.ts
@@ -1,5 +1,6 @@
/* eslint-disable camelcase */
import { remoteApi } from './remoteApiWrapper'
+import { MultisigConfig } from './multisig'
export enum OfflineSignStatus {
Signed = 'Signed',
@@ -12,6 +13,7 @@ export enum OfflineSignType {
UnlockDAO = 'UnlockDAO',
CreateSUDTAccount = 'CreateSUDTAccount',
SendSUDT = 'SendSUDT',
+ SendFromMultisigOnlySig = 'SendFromMultisigOnlySig',
Invalid = 'Invalid',
}
@@ -23,21 +25,23 @@ interface MultisigConfigs {
}
}
-interface Signatures {
+export interface Signatures {
[hash: string]: string[]
}
export interface OfflineSignJSON {
- transaction: any
+ transaction: {
+ signatures?: Signatures
+ [key: string]: any
+ }
status: OfflineSignStatus
type: OfflineSignType
description?: string
asset_account?: Pick
multisig_configs?: MultisigConfigs
- signatures?: Signatures
}
-export type SignProps = OfflineSignJSON & { walletID: string; password: string }
+export type SignProps = OfflineSignJSON & { walletID: string; password: string; multisigConfig?: MultisigConfig }
export type BroadcastProps = OfflineSignJSON & { walletID: string }
@@ -47,3 +51,4 @@ export const broadcastTransaction = remoteApi('broadcast-t
export const signAndExportTransaction = remoteApi(
'sign-and-export-transaction'
)
+export const signAndBroadcastTransaction = remoteApi('sign-and-broadcast-transaction')
diff --git a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
index 9d1ffd7dc3..496eaf02de 100644
--- a/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
+++ b/packages/neuron-ui/src/services/remote/remoteApiWrapper.ts
@@ -32,6 +32,7 @@ type Action =
| 'get-system-codehash'
| 'load-init-data'
| 'open-in-window'
+ | 'request-open-in-explorer'
| 'handle-view-error'
| 'show-settings'
| 'set-locale'
@@ -103,7 +104,9 @@ type Action =
| 'migrate-acp'
| 'check-migrate-acp'
| 'get-sudt-token-info'
- | 'generate-destroy-ckb-account-tx'
+ | 'generate-destroy-asset-account-tx'
+ | 'get-sudt-type-script-hash'
+ | 'generate-sudt-migrate-acp-tx'
// Cheque
| 'generate-create-cheque-tx'
| 'generate-withdraw-cheque-tx'
@@ -122,8 +125,21 @@ type Action =
| 'sign-transaction-only'
| 'broadcast-transaction-only'
| 'sign-and-export-transaction'
+ | 'sign-and-broadcast-transaction'
// nft
| 'generate-transfer-nft-tx'
+ // multisig
+ | 'create-multisig-address'
+ | 'save-multisig-config'
+ | 'get-multisig-config'
+ | 'import-multisig-config'
+ | 'export-multisig-config'
+ | 'update-multisig-config'
+ | 'delete-multisig-config'
+ | 'get-multisig-balances'
+ | 'generate-multisig-tx'
+ | 'generate-multisig-send-all-tx'
+ | 'load-multisig-tx-json'
export const remoteApi = (action: Action) => async (params: P): Promise> => {
const res: SuccessFromController | FailureFromController = await ipcRenderer.invoke(action, params).catch(() => ({
diff --git a/packages/neuron-ui/src/services/remote/specialAssets.ts b/packages/neuron-ui/src/services/remote/specialAssets.ts
index 6919869ef4..3baa18626d 100644
--- a/packages/neuron-ui/src/services/remote/specialAssets.ts
+++ b/packages/neuron-ui/src/services/remote/specialAssets.ts
@@ -2,4 +2,4 @@ import { remoteApi } from './remoteApiWrapper'
export const getSpecialAssets = remoteApi('get-customized-asset-cells')
export const unlockSpecialAsset = remoteApi('generate-withdraw-customized-cell-tx')
-export const destoryCKBAssetAccount = remoteApi('generate-destroy-ckb-account-tx')
+export const destoryAssetAccount = remoteApi('generate-destroy-asset-account-tx')
diff --git a/packages/neuron-ui/src/services/remote/sudt.ts b/packages/neuron-ui/src/services/remote/sudt.ts
index b8532d94b0..b1ef4b7398 100644
--- a/packages/neuron-ui/src/services/remote/sudt.ts
+++ b/packages/neuron-ui/src/services/remote/sudt.ts
@@ -37,3 +37,10 @@ export const migrateAcp = remoteApi('migrate-acp')
export const getSUDTTokenInfo = remoteApi(
'get-sudt-token-info'
)
+
+export const getSUDTTypeScriptHash = remoteApi<
+ Controller.GetSUDTTokenInfo.Params,
+ Controller.GetSUDTTokenInfo.Response
+>('get-sudt-type-script-hash')
+
+export const generateSudtMigrateAcpTx = remoteApi('generate-sudt-migrate-acp-tx')
diff --git a/packages/neuron-ui/src/services/remote/wallets.ts b/packages/neuron-ui/src/services/remote/wallets.ts
index b7d8ac97bc..b5e22f73d0 100644
--- a/packages/neuron-ui/src/services/remote/wallets.ts
+++ b/packages/neuron-ui/src/services/remote/wallets.ts
@@ -31,4 +31,4 @@ export const generateDaoClaimTx = remoteApi('withdraw-fr
// Sign and Verify
export const signMessage = remoteApi('sign-message')
-export const verifyMessage = remoteApi('verify-signature')
+export const verifyMessage = remoteApi('verify-signature')
diff --git a/packages/neuron-ui/src/services/subjects.ts b/packages/neuron-ui/src/services/subjects.ts
index 39fffdb8c8..57fb55333e 100644
--- a/packages/neuron-ui/src/services/subjects.ts
+++ b/packages/neuron-ui/src/services/subjects.ts
@@ -29,6 +29,7 @@ const SubjectConstructor = (
| 'navigation'
| 'set-locale'
| 'device-sign-index'
+ | 'multisig-output-update'
) => {
return ipcRenderer
? {
@@ -57,6 +58,7 @@ export const Command = SubjectConstructor('command')
export const Navigation = SubjectConstructor('navigation')
export const SetLocale = SubjectConstructor('set-locale')
export const DeviceSignIndex = SubjectConstructor('device-sign-index')
+export const MultisigOutputUpdate = SubjectConstructor('multisig-output-update')
export default {
DataUpdate,
@@ -71,4 +73,5 @@ export default {
Navigation,
SetLocale,
DeviceSignIndex,
+ MultisigOutputUpdate,
}
diff --git a/packages/neuron-ui/src/states/init/chain.ts b/packages/neuron-ui/src/states/init/chain.ts
index 1ae00ee80c..e0da056662 100644
--- a/packages/neuron-ui/src/states/init/chain.ts
+++ b/packages/neuron-ui/src/states/init/chain.ts
@@ -30,6 +30,7 @@ export const chainState: Readonly = {
bestKnownBlockTimestamp: 0,
estimate: undefined,
status: 0,
+ isLookingValidTarget: false,
},
transactions: {
pageNo: 1,
diff --git a/packages/neuron-ui/src/states/init/index.ts b/packages/neuron-ui/src/states/init/index.ts
index 9ca763e4ae..fda355db12 100644
--- a/packages/neuron-ui/src/states/init/index.ts
+++ b/packages/neuron-ui/src/states/init/index.ts
@@ -20,6 +20,7 @@ export const initStates = {
nervosDAO,
updater,
experimental: null,
+ sUDTAccounts: [],
}
export default initStates
diff --git a/packages/neuron-ui/src/states/stateProvider/reducer.ts b/packages/neuron-ui/src/states/stateProvider/reducer.ts
index d12a954e23..7b41290369 100644
--- a/packages/neuron-ui/src/states/stateProvider/reducer.ts
+++ b/packages/neuron-ui/src/states/stateProvider/reducer.ts
@@ -1,7 +1,7 @@
import produce, { Draft } from 'immer'
import { OfflineSignJSON } from 'services/remote'
import initStates from 'states/init'
-import { ConnectionStatus, ErrorCode } from 'utils'
+import { ConnectionStatus, ErrorCode, sortAccounts } from 'utils'
export enum NeuronWalletActions {
InitAppState = 'initAppState',
@@ -23,6 +23,8 @@ export enum NeuronWalletActions {
UpdateNervosDaoData = 'updateNervosDaoData',
// updater
UpdateAppUpdaterStatus = 'updateAppUpdaterStatus',
+ // sUDT
+ GetSUDTAccountList = 'getSUDTAccountList',
}
export enum AppActions {
AddSendOutput = 'addSendOutput',
@@ -100,6 +102,7 @@ export type StateAction =
| { type: NeuronWalletActions.UpdateSyncState; payload: State.SyncState }
| { type: NeuronWalletActions.UpdateNervosDaoData; payload: State.NervosDAO }
| { type: NeuronWalletActions.UpdateAppUpdaterStatus; payload: State.AppUpdater }
+ | { type: NeuronWalletActions.GetSUDTAccountList; payload: Controller.GetSUDTAccountList.Response }
export type StateDispatch = React.Dispatch // TODO: add type of payload
@@ -192,6 +195,22 @@ export const reducer = produce((state: Draft, action:
state.nervosDAO = action.payload as Draft
break
}
+ case NeuronWalletActions.GetSUDTAccountList: {
+ state.sUDTAccounts = action.payload
+ .filter(account => account.id !== undefined)
+ .sort(sortAccounts)
+ .map(({ id, accountName, tokenName, symbol, tokenID, balance: accountBalance, address, decimal }) => ({
+ accountId: id!.toString(),
+ accountName,
+ tokenName,
+ symbol,
+ balance: accountBalance,
+ address,
+ decimal,
+ tokenId: tokenID,
+ }))
+ break
+ }
// Actions of App
case AppActions.UpdateChainInfo: {
Object.assign(state.app, action.payload)
diff --git a/packages/neuron-ui/src/stories/SUDTUpdateDialog.stories.tsx b/packages/neuron-ui/src/stories/SUDTUpdateDialog.stories.tsx
index 2153d4626c..9ebcbdb526 100644
--- a/packages/neuron-ui/src/stories/SUDTUpdateDialog.stories.tsx
+++ b/packages/neuron-ui/src/stories/SUDTUpdateDialog.stories.tsx
@@ -9,6 +9,7 @@ stories.add('sUDT Token', () => {
return (
{
return (
= {
+ secp256k1_blake160: {
+ codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
+ hashType: 'type',
+ args: '0x2228dae340f587647362d31e3f04d7a51f8168dc',
+ },
+ anyone_can_pay: {
+ codeHash: '0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356',
+ hashType: 'type',
+ args: '0xb2b8101595fe0ddeb9f4e1acead6107119497fe6',
+ },
+ multisig: {
+ codeHash: '0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8',
+ hashType: 'type',
+ args: '0x3c12e68513a8731692607387fcfcfce4275b6ffa',
+ },
+ locktime: {
+ codeHash: '0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8',
+ hashType: 'type',
+ args: '0x822f72beaac01b956d4ce5ac00a5806a8be356100310008700f00020',
+ },
+}
+
+Object.entries(scripts).forEach(([title, script]) => {
+ stories.add(title, () => )
+})
diff --git a/packages/neuron-ui/src/stories/data/transactions.ts b/packages/neuron-ui/src/stories/data/transactions.ts
index ce55e5bb68..5d2af01e48 100644
--- a/packages/neuron-ui/src/stories/data/transactions.ts
+++ b/packages/neuron-ui/src/stories/data/transactions.ts
@@ -3,6 +3,50 @@ const transactions: {
} = {
'Empty List': [],
'Content List': [
+ {
+ type: 'destroy',
+ createdAt: (new Date(1565240655845).getTime() - 100000).toString(),
+ updatedAt: '',
+ timestamp: '',
+ value: '-10000',
+ hash: '0x70abeeaa2ed08b7d7659341a122b9a2f2ede99bb6bd0df7398d7ffe488beab61',
+ description: 'description of send transaction',
+ blockNumber: '120',
+ status: 'pending',
+ nervosDao: false,
+ sudtInfo: {
+ sUDT: {
+ tokenID: 'token id',
+ tokenName: 'Token Name',
+ symbol: 'Token Symbol',
+ decimal: '12',
+ },
+ amount: '0',
+ },
+ assetAccountType: 'sUDT',
+ },
+ {
+ type: 'create',
+ createdAt: (new Date(1565240655845).getTime() - 100000).toString(),
+ updatedAt: '',
+ timestamp: '',
+ value: '-10000',
+ hash: '0x70abeeaa2ed08b7d7659341a122b9a2f2ede99bb6bd0df7398d7ffe488beab61',
+ description: 'description of send transaction',
+ blockNumber: '120',
+ status: 'success',
+ nervosDao: false,
+ sudtInfo: {
+ sUDT: {
+ tokenID: 'token id',
+ tokenName: 'Token Name',
+ symbol: 'Token Symbol',
+ decimal: '12',
+ },
+ amount: '0',
+ },
+ assetAccountType: 'sUDT',
+ },
{
type: 'send',
createdAt: (new Date(1565240655845).getTime() - 100000).toString(),
diff --git a/packages/neuron-ui/src/stories/index.tsx b/packages/neuron-ui/src/stories/index.tsx
index 315e745689..5fc174fe8a 100644
--- a/packages/neuron-ui/src/stories/index.tsx
+++ b/packages/neuron-ui/src/stories/index.tsx
@@ -51,3 +51,4 @@ import './CopyZone.stories'
// Widgets
import './RingProgressBar.stories'
+import './ScriptTag.stories'
diff --git a/packages/neuron-ui/src/tests/getMultisigSignStatus/index.test.ts b/packages/neuron-ui/src/tests/getMultisigSignStatus/index.test.ts
new file mode 100644
index 0000000000..4dc96e5f3a
--- /dev/null
+++ b/packages/neuron-ui/src/tests/getMultisigSignStatus/index.test.ts
@@ -0,0 +1,132 @@
+import { MultisigConfig } from 'services/remote'
+import getMultisigSignStatus from 'utils/getMultisigSignStatus'
+import { addressToScript, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'
+
+const multisigConfig: MultisigConfig = {
+ id: 1,
+ walletId: 'walletId',
+ r: 1,
+ m: 2,
+ n: 3,
+ addresses: [
+ 'ckt1qyqwh5hmt8j59njztrfz6z0s9wug3nv5qysqrnfm2h',
+ 'ckt1qyqwjt5p4axvmx9tl4lrrnqd0ld9pr9wjqsqv87474',
+ 'ckt1qyqql0vgjyxjxjxknkj6nq8jxa485xsyl66sy7c5f6',
+ ],
+ fullPayload: 'ckt1qpw9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn32sq2f2scddm0lvmq36hmzx8nfhw8ucxzslhqussgky',
+}
+const fullPayloadHash = scriptToHash(addressToScript(multisigConfig.fullPayload))
+
+describe('Test getCompensationPeriod', () => {
+ // first sign but no match address
+ it('no address in fullPayload', () => {
+ const res = getMultisigSignStatus({ multisigConfig, signatures: {}, addresses: [] })
+ expect(res).toEqual({
+ lackOfRCount: 1,
+ lackOfMCount: 2,
+ canBroadcastAfterSign: false,
+ canSign: false,
+ })
+ })
+ // first sign
+ it('have signed address in fullPayload but not last', () => {
+ const res = getMultisigSignStatus({
+ multisigConfig,
+ signatures: {},
+ addresses: [
+ {
+ address: multisigConfig.addresses[1],
+ },
+ ] as any,
+ })
+ expect(res).toEqual({
+ lackOfRCount: 1,
+ lackOfMCount: 2,
+ canBroadcastAfterSign: false,
+ canSign: true,
+ })
+ })
+ // second sign but no match address
+ it('no signed address in fullPayload', () => {
+ const res = getMultisigSignStatus({
+ multisigConfig,
+ signatures: {
+ [fullPayloadHash]: [addressToScript(multisigConfig.addresses[1]).args],
+ },
+ addresses: [
+ {
+ address: multisigConfig.addresses[1],
+ },
+ ] as any,
+ })
+ expect(res).toEqual({
+ lackOfRCount: 1,
+ lackOfMCount: 1,
+ canBroadcastAfterSign: false,
+ canSign: false,
+ })
+ })
+ // second sign
+ it('need last unspecified signed address in fullPayload', () => {
+ const res = getMultisigSignStatus({
+ multisigConfig,
+ signatures: {
+ [fullPayloadHash]: [addressToScript(multisigConfig.addresses[0]).args],
+ },
+ addresses: [
+ {
+ address: multisigConfig.addresses[1],
+ },
+ ] as any,
+ })
+ expect(res).toEqual({
+ lackOfRCount: 0,
+ lackOfMCount: 1,
+ canBroadcastAfterSign: true,
+ canSign: true,
+ })
+ })
+ // second sign
+ it('need last specified signed address in fullPayload', () => {
+ const res = getMultisigSignStatus({
+ multisigConfig,
+ signatures: {
+ [fullPayloadHash]: [addressToScript(multisigConfig.addresses[1]).args],
+ },
+ addresses: [
+ {
+ address: multisigConfig.addresses[0],
+ },
+ ] as any,
+ })
+ expect(res).toEqual({
+ lackOfRCount: 1,
+ lackOfMCount: 1,
+ canBroadcastAfterSign: true,
+ canSign: true,
+ })
+ })
+ // sign success
+ it('has signed', () => {
+ const res = getMultisigSignStatus({
+ multisigConfig,
+ signatures: {
+ [fullPayloadHash]: [
+ addressToScript(multisigConfig.addresses[0]).args,
+ addressToScript(multisigConfig.addresses[1]).args,
+ ],
+ },
+ addresses: [
+ {
+ address: multisigConfig.addresses[0],
+ },
+ ] as any,
+ })
+ expect(res).toEqual({
+ lackOfRCount: 0,
+ lackOfMCount: 0,
+ canBroadcastAfterSign: false,
+ canSign: false,
+ })
+ })
+})
diff --git a/packages/neuron-ui/src/tests/parsers/index.test.ts b/packages/neuron-ui/src/tests/parsers/index.test.ts
new file mode 100644
index 0000000000..d9dc7e31db
--- /dev/null
+++ b/packages/neuron-ui/src/tests/parsers/index.test.ts
@@ -0,0 +1,111 @@
+import { listParams, epochParser, toUint128Le } from 'utils/parsers'
+
+describe('listParams', () => {
+ it('should have default pageNo = 1 and pageSize = 15', () => {
+ const fixture = {
+ params: '?keywords=foo',
+ expected: {
+ pageNo: 1,
+ pageSize: 15,
+ keywords: 'foo',
+ },
+ }
+ expect(listParams(fixture.params)).toEqual(fixture.expected)
+ })
+
+ it('should use passed pageNo and pageSize', () => {
+ const fixture = {
+ params: '?keywords=foo&pageNo=2&pageSize=30',
+ expected: {
+ pageNo: 2,
+ pageSize: 30,
+ keywords: 'foo',
+ },
+ }
+ expect(listParams(fixture.params)).toEqual(fixture.expected)
+ })
+})
+
+describe('epochParser', () => {
+ it('should have float number in value if length is not 0', () => {
+ const fixture = {
+ params: '0x1e00017000090',
+ expected: {
+ value: 144.04792,
+ index: BigInt('0x17'),
+ length: BigInt('0x1e0'),
+ number: BigInt('0x90'),
+ },
+ }
+ const res = epochParser(fixture.params)
+ expect(+res.value.toFixed(5)).toEqual(fixture.expected.value)
+ expect(res.index).toEqual(fixture.expected.index)
+ expect(res.length).toEqual(fixture.expected.length)
+ expect(res.number).toEqual(fixture.expected.number)
+ })
+
+ it('should not have float number in value if length is 0', () => {
+ const fixture = {
+ params: '0x2000000010000200',
+ expected: {
+ value: +'0x200',
+ index: BigInt('0x10'),
+ length: BigInt('0x0'),
+ number: BigInt('0x200'),
+ },
+ }
+
+ const res = epochParser(fixture.params)
+ expect(res.value).toEqual(fixture.expected.value)
+ expect(res.index).toEqual(fixture.expected.index)
+ expect(res.length).toEqual(fixture.expected.length)
+ expect(res.number).toEqual(fixture.expected.number)
+ })
+
+ it('should handle int string', () => {
+ const fixture = {
+ params: (+'0x1e00017000090').toString(),
+ expected: {
+ value: 144.04792,
+ index: BigInt('0x17'),
+ length: BigInt('0x1e0'),
+ number: BigInt('0x90'),
+ },
+ }
+ const res = epochParser(fixture.params)
+ expect(+res.value.toFixed(5)).toEqual(fixture.expected.value)
+ expect(res.index).toEqual(fixture.expected.index)
+ expect(res.length).toEqual(fixture.expected.length)
+ expect(res.number).toEqual(fixture.expected.number)
+ })
+})
+
+describe('toUint128Le', () => {
+ it('should parse 16 bytes', () => {
+ const fixture = {
+ params: '0x000010632d5ec76b0500000000000000',
+ expected: '0x00000000000000056bc75e2d63100000',
+ }
+ expect(toUint128Le(fixture.params)).toEqual(fixture.expected)
+ })
+
+ it('should pad 0 if bytes are not enough', () => {
+ const fixture = {
+ params: '0x000010632d5ec76b05',
+ expected: '0x00000000000000056bc75e2d63100000',
+ }
+ expect(toUint128Le(fixture.params)).toEqual(fixture.expected)
+ })
+
+ it('should ignore overflowed bytes', () => {
+ const fixture = {
+ params: '0x000010632d5ec76b0500000000000000b0',
+ expected: '0x00000000000000056bc75e2d63100000',
+ }
+ expect(toUint128Le(fixture.params)).toEqual(fixture.expected)
+ })
+
+ it('should throw an error if params is not a hex string', () => {
+ expect(() => toUint128Le('0')).toThrowError()
+ })
+})
diff --git a/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json b/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json
deleted file mode 100644
index 41a7f5a673..0000000000
--- a/packages/neuron-ui/src/tests/scriptToAddress/fixtures.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "secp256k1/blake160 address": {
- "params": [
- {
- "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
- "hashType": "type",
- "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64"
- },
- true
- ],
- "expected": "ckb1qyqt8xaupvm8837nv3gtc9x0ekkj64vud3jqfwyw5v"
- },
- "secp256k1/multisig address": {
- "params": [
- {
- "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8",
- "hashType": "type",
- "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a"
- },
- true
- ],
- "expected": "ckb1qyq5lv479ewscx3ms620sv34pgeuz6zagaaqklhtgg"
- },
- "anyone can pay address on lina": {
- "params": [
- {
- "codeHash": "0xd369597ff47f29fbc0d47d2e3775370d1250b85140c670e4718af712983a2354",
- "hashType": "type",
- "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a"
- },
- true
- ],
- "expected": "ckb1qypylv479ewscx3ms620sv34pgeuz6zagaaqvrugu7"
- },
- "anyone can pay address on aggron": {
- "params": [
- {
- "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356",
- "hashType": "type",
- "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a"
- },
- false
- ],
- "expected": "ckt1qypylv479ewscx3ms620sv34pgeuz6zagaaq3xzhsz"
- },
- "full version address of hashType = 'data'": {
- "params": [
- {
- "codeHash": "0xa656f172b6b45c245307aeb5a7a37a176f002f6f22e92582c58bf7ba362e4176",
- "hashType": "data",
- "args": "0x36c329ed630d6ce750712a477543672adab57f4c"
- },
- false
- ],
- "expected": "ckt1qzn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvqpkcv576ccddnn4quf2ga65xee2m26h7nqr2r97x"
- },
- "full version address of hashType = 'type'": {
- "params": [
- {
- "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2",
- "hashType": "type",
- "args": "0x36c329ed630d6ce750712a477543672adab57f4c"
- },
- false
- ],
- "expected": "ckt1qqvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlyqfkcv576ccddnn4quf2ga65xee2m26h7nq8t420m"
- }
-}
diff --git a/packages/neuron-ui/src/tests/scriptToAddress/index.test.ts b/packages/neuron-ui/src/tests/scriptToAddress/index.test.ts
deleted file mode 100644
index a9e755293b..0000000000
--- a/packages/neuron-ui/src/tests/scriptToAddress/index.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import scriptToAddress from '../../utils/scriptToAddress'
-import fixtures from './fixtures.json'
-
-describe('Test scriptToAddress', () => {
- const fixtureTable: [string, [CKBComponents.Script, boolean], string][] = Object.entries<{
- params: any
- expected: string
- }>(fixtures).map(([title, { params, expected }]) => [title, params, expected])
-
- test.each(fixtureTable)(`%s`, (_title, params, expected) => {
- expect.assertions(1)
- expect(scriptToAddress(...params)).toEqual(expected)
- })
-})
diff --git a/packages/neuron-ui/src/tests/validators/assetAccountAddress/fixtures.ts b/packages/neuron-ui/src/tests/validators/assetAccountAddress/fixtures.ts
index 4efa9c8365..9a38888a84 100644
--- a/packages/neuron-ui/src/tests/validators/assetAccountAddress/fixtures.ts
+++ b/packages/neuron-ui/src/tests/validators/assetAccountAddress/fixtures.ts
@@ -1,4 +1,4 @@
-import { ErrorCode, AccountType } from 'utils/enums'
+import { AccountType, ErrorCode } from 'utils/enums'
export default {
'Should throw an error when address is not a string': {
@@ -57,14 +57,14 @@ export default {
},
exception: null,
},
- "Should throw an error when account type is ckb and it's a short version address of secp256k1 lock": {
+ "Should pass when account type is ckb and it's a short version address of secp256k1 lock": {
params: {
address: 'ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83',
isMainnet: false,
required: false,
type: AccountType.CKB,
},
- exception: ErrorCode.FieldInvalid,
+ exception: null,
},
"Should throw an error when it's a short version address but its code hash index is not 0x00 or 0x02": {
params: {
@@ -74,12 +74,20 @@ export default {
},
exception: ErrorCode.FieldInvalid,
},
- "Should throw an error when it's a full version address but its code hash index is not 0x04": {
+ "Should throw an error when address's code hash is deprecated": {
params: {
address: 'ckt1q2r2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yy3d90uh',
isMainnet: false,
required: false,
},
+ exception: ErrorCode.AddressIsDeprecated,
+ },
+ "Should throw an error when it's a full version address but its code hash index is not 0x04": {
+ params: {
+ address: 'ckt1qkr2r35c0f9vhcdgslx2fjwa9tylevr5qka7mfgmscd33wlhfykyk7tvzu37rv87kyv59ltdece09usz9t9yy8tyx5j',
+ isMainnet: false,
+ required: false,
+ },
exception: ErrorCode.FieldInvalid,
},
'Should throw an error when minimum is malformed': {
diff --git a/packages/neuron-ui/src/tests/validators/totalAmount/fixtures.ts b/packages/neuron-ui/src/tests/validators/totalAmount/fixtures.ts
index 9b11fb2c15..c57ef18594 100644
--- a/packages/neuron-ui/src/tests/validators/totalAmount/fixtures.ts
+++ b/packages/neuron-ui/src/tests/validators/totalAmount/fixtures.ts
@@ -15,7 +15,7 @@ const fixtures = {
fee: '0',
balance: '10000000000000000000000',
},
- exception: ErrorCode.BalanceNotEnough,
+ exception: ErrorCode.AmountNotEnough,
},
'Should throw an error when total amount + fee is greater than balance': {
params: {
@@ -23,7 +23,7 @@ const fixtures = {
fee: '1',
balance: '10000000000000000000000',
},
- exception: ErrorCode.BalanceNotEnough,
+ exception: ErrorCode.AmountNotEnough,
},
'Should throw an error when balance is negative': {
params: {
@@ -31,7 +31,7 @@ const fixtures = {
fee: '10000000000',
balance: '-10000000000010000000000',
},
- exception: ErrorCode.BalanceNotEnough,
+ exception: ErrorCode.AmountNotEnough,
},
}
diff --git a/packages/neuron-ui/src/theme.tsx b/packages/neuron-ui/src/theme.tsx
index d0eb5b15ca..fcb3b4ff75 100644
--- a/packages/neuron-ui/src/theme.tsx
+++ b/packages/neuron-ui/src/theme.tsx
@@ -1,34 +1,30 @@
import React from 'react'
-import { loadTheme, getTheme } from 'office-ui-fabric-react'
-
+import { loadTheme } from 'office-ui-fabric-react'
import {
- Alert as AlertIcon,
- Checkmark as SuccessIcon,
- CircleInformation as InfoIcon,
- Close as DismissIcon,
- Close as FailIcon,
- Copy as CopyIcon,
- Domain as ExplorerIcon,
- Down as ArrowDownIcon,
- FormClose as ClearIcon,
- FormAdd as CreateIcon,
- FormPreviousLink as LeaveIcon,
- FormUp as ExpandIcon,
- FormUpload as ImportIcon,
- License as KeystoreIcon,
- LinkBottom as LinkBottomIcon,
- LinkDown as LinkDownIcon,
- LinkTop as LinkTopIcon,
- LinkUp as LinkUpIcon,
- More as MoreIcon,
- Nodes as ConnectedIcon,
- Scan as ScanIcon,
- Search as SearchIcon,
- SettingsOption as SettingsIcon,
- StatusGood as MatchedIcon,
- Update as UpdateIcon,
- Update as PendingIcon,
-} from 'grommet-icons'
+ Explorer,
+ Search,
+ FirstPage,
+ PreviousPage,
+ LastPage,
+ NextPage,
+ Matched,
+ InfoCircleOutlined,
+ Close,
+ More,
+ ArrowDown,
+ Alert,
+ Check,
+ CopyOutlined,
+ RightOutlined,
+ Plus,
+ ArrowLeftOutlined,
+ Connected,
+ PendingIcon,
+ Keystore,
+ Settings,
+ ScanOutlined,
+ UploadOutlined,
+} from 'widgets/Icons/icon'
import { registerIcons } from 'utils/icons'
@@ -49,41 +45,40 @@ loadTheme({
},
})
-const theme = getTheme()
-const { semanticColors } = theme
-
registerIcons({
icons: {
- info: ,
- errorbadge: ,
- completed: ,
- cancel: ,
- MiniCopy: ,
- Search: ,
- FirstPage: ,
- LastPage: ,
- PrevPage: ,
- NextPage: ,
- ArrowDown: ,
- ChevronRightMed: ,
- Scan: ,
- Import: ,
- Create: ,
- Clear: ,
- Dismiss: ,
- Leave: ,
- Connected: ,
- Disconnected: ,
- Updating: ,
- More: ,
- Matched: ,
- Unmatched: ,
- TransactionSuccess: ,
- TransactionFailure: ,
- TransactionPending: ,
- Keystore: ,
- Settings: ,
- Explorer: ,
+ info: ,
+ errorbadge: ,
+ completed: ,
+ cancel: ,
+ MiniCopy: ,
+ Search: ,
+ FirstPage: ,
+ LastPage: ,
+ PrevPage: ,
+ NextPage: ,
+ ArrowDown: ,
+ ChevronRightMed: ,
+ Scan: ,
+ Import: ,
+ Create: ,
+ Clear: ,
+ Dismiss: ,
+ Leave: ,
+ Connected: ,
+ Disconnected: ,
+ Updating: ,
+ More: ,
+ Matched: ,
+ Unmatched: ,
+ TransactionSuccess: ,
+ TransactionFailure: ,
+ TransactionPending: (
+
+ ),
+ Keystore: ,
+ Settings: ,
+ Explorer: ,
},
})
diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts
index 672c39bd57..59bd41fe4d 100644
--- a/packages/neuron-ui/src/types/App/index.d.ts
+++ b/packages/neuron-ui/src/types/App/index.d.ts
@@ -1,6 +1,6 @@
declare namespace State {
interface Transaction {
- type: 'send' | 'receive'
+ type: 'send' | 'receive' | 'create' | 'destroy'
createdAt: string
updatedAt: string
timestamp: string
@@ -18,6 +18,7 @@ declare namespace State {
type: 'send' | 'receive'
data: string
}
+ assetAccountType?: 'CKB' | 'sUDT' | string
}
interface DetailedInput {
@@ -82,15 +83,39 @@ declare namespace State {
| 'create-sudt-account'
| 'send-sudt'
| 'send-acp'
+ | 'send-acp-to-default'
| 'send-cheque'
| 'withdraw-cheque'
| 'claim-cheque'
| 'create-account-to-claim-cheque'
- | 'destroy-ckb-account'
+ | 'destroy-asset-account'
| 'migrate-acp'
| 'send-nft'
+ | 'send-from-multisig'
+ | 'send-from-multisig-need-one'
| null
walletID: string
+ multisigConfig?: {
+ id: number
+ walletId: string
+ r: number
+ m: number
+ n: number
+ addresses: string[]
+ alias?: string
+ fullPayload: string
+ }
+ }
+
+ interface SUDTAccount {
+ accountId: string
+ accountName?: string
+ tokenName?: string
+ symbol?: string
+ balance: string
+ tokenId: string
+ address: string
+ decimal: string
}
type AlertDialog = Record<'title' | 'message', string> | null
@@ -134,6 +159,7 @@ declare namespace State {
name: string
device?: DeviceInfo
isHD?: boolean
+ isWatchOnly?: boolean
}
enum Manufacturer {
@@ -176,6 +202,8 @@ declare namespace State {
bestKnownBlockTimestamp: number
estimate: number | undefined
status: number
+ isLookingValidTarget: boolean
+ validTarget?: string
}>
interface Chain {
@@ -237,6 +265,7 @@ declare namespace State {
wallet: Wallet
nervosDAO: NervosDAO
updater: AppUpdater
+ sUDTAccounts: SUDTAccount[]
experimental: { tx: any; assetAccount?: any } | null
}
}
diff --git a/packages/neuron-ui/src/types/Controller/index.d.ts b/packages/neuron-ui/src/types/Controller/index.d.ts
index a53650a7eb..19e0cbe14f 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 {
+ interface RequestOpenInExplorerParams {
+ key: string
+ type: 'transaction'
+ }
+
interface OpenInWindowParams {
url: string
title: string
@@ -54,6 +59,16 @@ declare namespace Controller {
tx: string
password?: string
description?: string
+ multisigConfig?: {
+ id: number
+ walletId: string
+ r: number
+ m: number
+ n: number
+ addresses: string[]
+ alias?: string
+ fullPayload: string
+ }
}
interface GenerateTransactionParams {
@@ -69,6 +84,7 @@ declare namespace Controller {
interface GenerateDepositAllTransactionParams {
walletID: string
+ isBalanceReserved: boolean
feeRate: string
}
@@ -273,6 +289,7 @@ declare namespace Controller {
walletID: string
tx: any
password?: string
+ skipLastInputs?: boolean
}
type Response = Hash
}
diff --git a/packages/neuron-ui/src/types/Subject/index.d.ts b/packages/neuron-ui/src/types/Subject/index.d.ts
index fc4a12f882..3bd4c77180 100644
--- a/packages/neuron-ui/src/types/Subject/index.d.ts
+++ b/packages/neuron-ui/src/types/Subject/index.d.ts
@@ -40,6 +40,8 @@ declare namespace Subject {
bestKnownBlockTimestamp: number
estimate: number
status: number
+ isLookingValidTarget: boolean
+ validTarget?: string
}
interface AppUpdater {
diff --git a/packages/neuron-ui/src/types/global/index.d.ts b/packages/neuron-ui/src/types/global/index.d.ts
index f19dee9d34..6b628019e7 100644
--- a/packages/neuron-ui/src/types/global/index.d.ts
+++ b/packages/neuron-ui/src/types/global/index.d.ts
@@ -25,7 +25,7 @@ declare module '*.json' {
declare module '*.svg' {
const value: string
- export const ReactComponent = value
+ export const ReactComponent: React.FC>
export default value
}
diff --git a/packages/neuron-ui/src/utils/const.ts b/packages/neuron-ui/src/utils/const.ts
index 454f9902e0..69145e688e 100644
--- a/packages/neuron-ui/src/utils/const.ts
+++ b/packages/neuron-ui/src/utils/const.ts
@@ -59,3 +59,8 @@ export const LONG_TYPE_PREFIX = '0x04'
// times
export const SYNC_REBUILD_SINCE_VERSION = '0.32'
+
+export const DEPRECATED_CODE_HASH: Record = {
+ AcpOnLina: '0x0fb343953ee78c9986b091defb6252154e0bb51044fd2879fde5b27314506111',
+ AcpOnAggron: '0x86a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b',
+}
diff --git a/packages/neuron-ui/src/utils/enums.ts b/packages/neuron-ui/src/utils/enums.ts
index 1109386eb3..b76108ce89 100644
--- a/packages/neuron-ui/src/utils/enums.ts
+++ b/packages/neuron-ui/src/utils/enums.ts
@@ -97,6 +97,7 @@ export enum ErrorCode {
TestnetAddressRequired = 307,
BalanceNotEnough = 308,
AddressIsDeprecated = 309,
+ AddressTypeNotMatch = 310,
// hardware
SignTransactionFailed = 400,
ConnectFailed = 401,
@@ -122,6 +123,7 @@ export enum SyncStatus {
export enum PresetScript {
Locktime = 'SingleMultiSign',
Cheque = 'Cheque',
+ SUDT = 'SUDT',
}
export enum CompensationPeriod {
@@ -141,6 +143,7 @@ export enum DefaultLockInfo {
HashType = 'type',
CodeHashIndex = '0x00',
ArgsLen = '20',
+ TagName = 'secp256k1/blake160',
}
export enum MultiSigLockInfo {
@@ -148,6 +151,15 @@ export enum MultiSigLockInfo {
HashType = 'type',
CodeHashIndex = '0x01',
ArgsLen = '20',
+ TagName = 'secp256k1/multisig',
+}
+
+export enum LocktimeLockInfo {
+ CodeHash = '0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8',
+ HashType = 'type',
+ CodeHashIndex = '0x01',
+ ArgsLen = '28',
+ TagName = 'secp256k1/multisig/locktime',
}
export enum AnyoneCanPayLockInfoOnAggron {
@@ -155,6 +167,7 @@ export enum AnyoneCanPayLockInfoOnAggron {
HashType = 'type',
CodeHashIndex = '0x02',
ArgsLen = '20,21,22',
+ TagName = 'secp256k1/anyone_can_pay',
}
export enum AnyoneCanPayLockInfoOnLina {
@@ -162,11 +175,23 @@ export enum AnyoneCanPayLockInfoOnLina {
HashType = 'type',
CodeHashIndex = '0x02',
ArgsLen = '20,21,22',
+ TagName = 'secp256k1/anyone_can_pay',
}
-export enum DeprecatedScript {
- AcpOnLina = '0x020fb343953ee78c9986b091defb6252154e0bb51044fd2879fde5b27314506111',
- AcpOnAggron = '0x0486a1c6987a4acbe1a887cca4c9dd2ac9fcb07405bbeda51b861b18bbf7492c4b',
+export enum ChequeLockInfoOnAggron {
+ CodeHash = '0x60d5f39efce409c587cb9ea359cefdead650ca128f0bd9cb3855348f98c70d5b',
+ HashType = 'type',
+ CodeHashIndex = '0x00',
+ ArgsLen = '40',
+ TagName = 'cheque',
+}
+
+export enum ChequeLockInfoOnLina {
+ CodeHash = '0xe4d4ecc6e5f9a059bf2f7a82cca292083aebc0c421566a52484fe2ec51a9fb0c',
+ HashType = 'type',
+ CodeHashIndex = '0x00',
+ ArgsLen = '40',
+ TagName = 'cheque',
}
export enum AccountType {
diff --git a/packages/neuron-ui/src/utils/getLockSupportShortAddress.ts b/packages/neuron-ui/src/utils/getLockSupportShortAddress.ts
new file mode 100644
index 0000000000..3954d1a23c
--- /dev/null
+++ b/packages/neuron-ui/src/utils/getLockSupportShortAddress.ts
@@ -0,0 +1,12 @@
+import { AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina, DefaultLockInfo, MultiSigLockInfo } from './enums'
+
+const getLockSupportShortAddress = (lock: CKBComponents.Script) => {
+ return [MultiSigLockInfo, DefaultLockInfo, AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina].find(
+ info =>
+ lock.codeHash === info.CodeHash &&
+ lock.hashType === info.HashType &&
+ info.ArgsLen.split(',').includes(`${(lock.args.length - 2) / 2}`)
+ )
+}
+
+export default getLockSupportShortAddress
diff --git a/packages/neuron-ui/src/utils/getMultisigSignStatus.ts b/packages/neuron-ui/src/utils/getMultisigSignStatus.ts
new file mode 100644
index 0000000000..dc78832d89
--- /dev/null
+++ b/packages/neuron-ui/src/utils/getMultisigSignStatus.ts
@@ -0,0 +1,54 @@
+import { addressToScript, scriptToHash } from '@nervosnetwork/ckb-sdk-utils'
+import { MultisigConfig, Signatures } from 'services/remote'
+
+export const getMultisigSignStatus = ({
+ multisigConfig,
+ signatures,
+ addresses,
+}: {
+ multisigConfig: MultisigConfig
+ signatures?: Signatures
+ addresses: State.Address[]
+}) => {
+ const multisigLockHash = scriptToHash(addressToScript(multisigConfig.fullPayload))
+ const multisigBlake160s = multisigConfig.addresses.map(v => addressToScript(v).args)
+ const addressBlake160s = addresses.map(v => addressToScript(v.address).args)
+ const notSpecifiedCount = multisigConfig.m - multisigConfig.r
+ const specifiedUnsignedAddresses: string[] = []
+ const unspecifiedSignedAddresses: string[] = []
+ const unspecifiedUnsignedAddresses: string[] = []
+ for (let i = 0; i < multisigBlake160s.length; i++) {
+ const hasSigned = signatures?.[multisigLockHash]?.includes(multisigBlake160s[i])
+ if (i < multisigConfig.r) {
+ if (!hasSigned) {
+ specifiedUnsignedAddresses.push(multisigBlake160s[i])
+ }
+ } else {
+ ;(hasSigned ? unspecifiedSignedAddresses : unspecifiedUnsignedAddresses).push(multisigBlake160s[i])
+ }
+ }
+ const lackOfUnspecifiedCount =
+ unspecifiedSignedAddresses.length < notSpecifiedCount ? notSpecifiedCount - unspecifiedSignedAddresses.length : 0
+ let canBroadcastAfterSign = false
+ let canSign = specifiedUnsignedAddresses.some(v => addressBlake160s.includes(v))
+ if (lackOfUnspecifiedCount + specifiedUnsignedAddresses.length === 1) {
+ if (specifiedUnsignedAddresses.length === 1) {
+ canBroadcastAfterSign = addressBlake160s.includes(specifiedUnsignedAddresses[0])
+ } else {
+ canBroadcastAfterSign = unspecifiedUnsignedAddresses.some(v => addressBlake160s.includes(v))
+ }
+ canSign = canBroadcastAfterSign
+ } else {
+ canSign =
+ specifiedUnsignedAddresses.some(v => addressBlake160s.includes(v)) ||
+ (!!lackOfUnspecifiedCount && unspecifiedUnsignedAddresses.some(v => addressBlake160s.includes(v)))
+ }
+ return {
+ lackOfRCount: specifiedUnsignedAddresses.length,
+ lackOfMCount: lackOfUnspecifiedCount + specifiedUnsignedAddresses.length,
+ canBroadcastAfterSign,
+ canSign,
+ }
+}
+
+export default getMultisigSignStatus
diff --git a/packages/neuron-ui/src/utils/getSUDTAmount.ts b/packages/neuron-ui/src/utils/getSUDTAmount.ts
new file mode 100644
index 0000000000..d8c66a1ce6
--- /dev/null
+++ b/packages/neuron-ui/src/utils/getSUDTAmount.ts
@@ -0,0 +1,22 @@
+import { sudtValueToAmount, toUint128Le } from 'utils'
+
+export const getSUDTAmount = ({
+ tokenInfo,
+ data,
+}: {
+ tokenInfo?: Controller.GetTokenInfoList.TokenInfo
+ data: string
+}) => {
+ let amount = BigInt(toUint128Le(data)).toString()
+ let amountToCopy = amount
+ if (tokenInfo) {
+ amount = `${sudtValueToAmount(amount, tokenInfo.decimal)} ${tokenInfo.symbol}`
+ amountToCopy = sudtValueToAmount(amountToCopy, tokenInfo.decimal, false, '')
+ }
+ return {
+ amount,
+ amountToCopy,
+ }
+}
+
+export default { getSUDTAmount }
diff --git a/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts b/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts
index 077a9a86ae..a4136869a1 100644
--- a/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts
+++ b/packages/neuron-ui/src/utils/hooks/createSUDTAccount.ts
@@ -1,7 +1,15 @@
+// TODO: update eslint to use 'import type' syntax
+import { TFunction } from 'i18next'
import { useEffect, useCallback } from 'react'
import { AccountType, TokenInfo } from 'components/SUDTCreateDialog'
import { AppActions, StateAction } from 'states'
-import { generateCreateSUDTAccountTransaction } from 'services/remote'
+import {
+ generateCreateSUDTAccountTransaction,
+ openExternal,
+ getSUDTTypeScriptHash,
+ invokeShowErrorMessage,
+} from 'services/remote'
+import { getExplorerUrl } from 'utils'
import { ErrorCode } from '../enums'
import { isSuccessResponse } from '../is'
import {
@@ -74,10 +82,12 @@ export const useOnGenerateNewAccountTransaction = ({
walletId,
dispatch,
onGenerated,
+ t,
}: {
walletId: string
dispatch: React.Dispatch
onGenerated: () => void
+ t: TFunction
}) =>
useCallback(
({ tokenId, tokenName, accountName, symbol, decimal }: TokenInfo) => {
@@ -94,7 +104,7 @@ export const useOnGenerateNewAccountTransaction = ({
if (isSuccessResponse(res)) {
return res.result
}
- throw new Error(res.message.toString())
+ throw new Error(typeof res.message === 'string' ? res.message : res.message.content)
})
.then((res: Controller.GenerateCreateSUDTAccountTransaction.Response) => {
dispatch({ type: AppActions.UpdateExperimentalParams, payload: res })
@@ -106,11 +116,22 @@ export const useOnGenerateNewAccountTransaction = ({
return true
})
.catch(err => {
- console.error(err)
+ invokeShowErrorMessage({ title: t('messages.error'), content: err.message })
return false
})
},
- [onGenerated, walletId, dispatch]
+ [onGenerated, walletId, dispatch, t]
)
-export default { useIsInsufficientToCreateSUDTAccount, useOnGenerateNewAccountTransaction }
+export const useOpenSUDTTokenUrl = (tokenID: string, isMainnet?: boolean) =>
+ useCallback(() => {
+ if (tokenID) {
+ getSUDTTypeScriptHash({ tokenID }).then(res => {
+ if (isSuccessResponse(res) && res.result) {
+ openExternal(`${getExplorerUrl(isMainnet)}/sudt/${res.result}`)
+ }
+ })
+ }
+ }, [isMainnet, tokenID])
+
+export default { useIsInsufficientToCreateSUDTAccount, useOnGenerateNewAccountTransaction, useOpenSUDTTokenUrl }
diff --git a/packages/neuron-ui/src/utils/hooks/index.ts b/packages/neuron-ui/src/utils/hooks/index.ts
index dd98366d2c..5729afffdf 100644
--- a/packages/neuron-ui/src/utils/hooks/index.ts
+++ b/packages/neuron-ui/src/utils/hooks/index.ts
@@ -1,4 +1,4 @@
-import { useState, useMemo, useCallback, useEffect } from 'react'
+import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
import { useHistory } from 'react-router-dom'
import { TFunction, i18n as i18nType } from 'i18next'
import { openContextMenu, requestPassword, deleteNetwork } from 'services/remote'
@@ -18,6 +18,9 @@ import {
validateSymbol,
validateTokenName,
validateDecimal,
+ validateAmount,
+ validateAddress,
+ validateAmountRange,
} from 'utils/validators'
import { MenuItemConstructorOptions } from 'electron'
@@ -173,6 +176,32 @@ export const useDialog = ({
}, [show, dialogRef, onClose])
}
+export const useDialogWrapper = ({
+ onClose,
+}: {
+ onClose?: () => void
+} = {}) => {
+ const dialogRef = useRef(null)
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
+ const openDialog = useCallback(() => {
+ setIsDialogOpen(true)
+ }, [setIsDialogOpen])
+ const closeDialog = useCallback(() => {
+ setIsDialogOpen(false)
+ }, [setIsDialogOpen])
+ useDialog({
+ show: isDialogOpen,
+ dialogRef,
+ onClose: onClose || closeDialog,
+ })
+ return {
+ isDialogOpen,
+ openDialog,
+ closeDialog,
+ dialogRef,
+ }
+}
+
export const useOnDefaultContextMenu = (t: TFunction) =>
useCallback(
(e: React.MouseEvent) => {
@@ -418,3 +447,49 @@ export const useGlobalNotifications = (
export const useDidMount = (cb: () => void) => {
useEffect(cb, [])
}
+
+export const useForceUpdate = (cb: T) => {
+ const [, update] = useState<{}>(Object.create(null))
+
+ const memoizedDispatch = useCallback(
+ (...args) => {
+ cb(...args)
+ update(Object.create(null))
+ },
+ [update, cb]
+ )
+ return memoizedDispatch
+}
+
+export const useOutputErrors = (
+ outputs: Partial>[],
+ isMainnet: boolean
+) => {
+ return useMemo(
+ () =>
+ outputs.map(({ address, amount, date }) => {
+ let amountError: (Error & { i18n: Record }) | undefined
+ if (amount !== undefined) {
+ try {
+ const extraSize = date ? CONSTANTS.SINCE_FIELD_SIZE : 0
+ validateAmount(amount)
+ validateAmountRange(amount, extraSize)
+ } catch (err) {
+ amountError = err
+ }
+ }
+
+ let addrError: (Error & { i18n: Record }) | undefined
+ if (address !== undefined) {
+ try {
+ validateAddress(address, isMainnet)
+ } catch (err) {
+ addrError = err
+ }
+ }
+
+ return { addrError, amountError }
+ }),
+ [outputs, isMainnet]
+ )
+}
diff --git a/packages/neuron-ui/src/utils/index.ts b/packages/neuron-ui/src/utils/index.ts
index dfc599927e..0c05d7633a 100644
--- a/packages/neuron-ui/src/utils/index.ts
+++ b/packages/neuron-ui/src/utils/index.ts
@@ -21,8 +21,8 @@ export * from './is'
export * from './parsers'
export * from './validators'
export * from './sortAccounts'
-export * from './scriptToAddress'
export * from './getSyncLeftTime'
export * from './baseActions'
+export * from './getSUDTAmount'
export { CONSTANTS }
diff --git a/packages/neuron-ui/src/utils/is.ts b/packages/neuron-ui/src/utils/is.ts
index 1651be1e5a..c7e742dd88 100644
--- a/packages/neuron-ui/src/utils/is.ts
+++ b/packages/neuron-ui/src/utils/is.ts
@@ -1,5 +1,6 @@
+import { addressToScript } from '@nervosnetwork/ckb-sdk-utils'
import { ControllerResponse, SuccessFromController } from 'services/remote/remoteApiWrapper'
-import { ResponseCode } from 'utils/enums'
+import { ResponseCode, DefaultLockInfo } from 'utils/enums'
import { MAINNET_TAG } from './const'
export const isMainnet = (networks: Readonly, networkID: string) => {
@@ -19,3 +20,16 @@ export const isReadyByVersion = (targetVersion: number, lastVersion: number | nu
}
return false
}
+
+export const isSecp256k1Address = (address: string) => {
+ try {
+ const script = addressToScript(address)
+ return (
+ script.codeHash === DefaultLockInfo.CodeHash &&
+ script.hashType === DefaultLockInfo.HashType &&
+ script.args.length === +DefaultLockInfo.ArgsLen * 2 + 2
+ )
+ } catch {
+ return false
+ }
+}
diff --git a/packages/neuron-ui/src/utils/parsers.ts b/packages/neuron-ui/src/utils/parsers.ts
index 4429dae7c4..cfad394fe1 100644
--- a/packages/neuron-ui/src/utils/parsers.ts
+++ b/packages/neuron-ui/src/utils/parsers.ts
@@ -1,4 +1,4 @@
-import { toUint64Le } from 'services/chain'
+import { toUint64Le, parseEpoch } from 'services/chain'
import { PAGE_SIZE } from './const'
export const listParams = (search: string) => {
@@ -13,25 +13,16 @@ export const listParams = (search: string) => {
return params
}
-export const prompt = (search: string) => {
- const query = new URLSearchParams(search)
- const params: { [index: string]: string | null } = {}
- const keys = [...query.keys()]
- keys.forEach((key: string) => {
- params[key] = query.get(key)
- })
- return params
-}
-export const queryParsers = { listParams, prompt }
-
export const epochParser = (epoch: string) => {
- const e = BigInt(epoch)
+ const e = epoch.startsWith('0x') ? epoch : `0x${BigInt(epoch).toString(16)}`
+ const parsed = parseEpoch(e)
const res = {
- length: (e >> BigInt(40)) & BigInt(0xffff),
- index: (e >> BigInt(24)) & BigInt(0xffff),
- number: e & BigInt(0xffffff),
+ length: BigInt(parsed.length),
+ index: BigInt(parsed.index),
+ number: BigInt(parsed.number),
}
+
return {
...res,
value: res.length > 0 ? Number(res.number) + Number(res.index) / Number(res.length) : Number(res.number),
@@ -39,5 +30,17 @@ export const epochParser = (epoch: string) => {
}
export const toUint128Le = (hexString: string) => {
- return `${toUint64Le(`0x${hexString.substr(34, 16)}`)}${toUint64Le(hexString.substr(0, 18)).slice(2)}`
+ if (!hexString.startsWith('0x')) {
+ throw new Error('Invalid hex string')
+ }
+
+ let s = hexString
+
+ if (s.length < 34) {
+ s = s.padEnd(34, '0')
+ } else if (s.length > 34) {
+ s = s.slice(0, 34)
+ }
+
+ return `${toUint64Le(`0x${s.substr(18, 16)}`)}${toUint64Le(s.substr(0, 18)).slice(2)}`
}
diff --git a/packages/neuron-ui/src/utils/scriptToAddress.ts b/packages/neuron-ui/src/utils/scriptToAddress.ts
deleted file mode 100644
index 7403b9f108..0000000000
--- a/packages/neuron-ui/src/utils/scriptToAddress.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ckbCore } from 'services/chain'
-import { MultiSigLockInfo, DefaultLockInfo, AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina } from './enums'
-
-export const scriptToAddress = (lock: CKBComponents.Script, isMainnet: boolean) => {
- const addressPrefix = isMainnet ? ckbCore.utils.AddressPrefix.Mainnet : ckbCore.utils.AddressPrefix.Testnet
-
- const foundLock = [MultiSigLockInfo, DefaultLockInfo, AnyoneCanPayLockInfoOnAggron, AnyoneCanPayLockInfoOnLina].find(
- info =>
- lock.codeHash === info.CodeHash &&
- lock.hashType === info.HashType &&
- info.ArgsLen.split(',').includes(`${(lock.args.length - 2) / 2}`)
- )
-
- if (foundLock) {
- return ckbCore.utils.bech32Address(lock.args, {
- prefix: addressPrefix,
- type: ckbCore.utils.AddressType.HashIdx,
- codeHashOrCodeHashIndex: foundLock.CodeHashIndex,
- })
- }
-
- return ckbCore.utils.scriptToAddress(lock, isMainnet)
-}
-
-export default scriptToAddress
diff --git a/packages/neuron-ui/src/utils/validators/address.ts b/packages/neuron-ui/src/utils/validators/address.ts
index 42c8fdc13e..cdebaeb3e3 100644
--- a/packages/neuron-ui/src/utils/validators/address.ts
+++ b/packages/neuron-ui/src/utils/validators/address.ts
@@ -4,6 +4,7 @@ import {
MainnetAddressRequiredException,
TestnetAddressRequiredException,
AddressEmptyException,
+ AddressNotMatchException,
} from 'exceptions'
import {
NEW_LONG_ADDR_PREFIX,
@@ -14,6 +15,15 @@ import {
SHORT_ADDR_MULTISIGN_LOCK_PREFIX,
SHORT_ADDR_SUDT_LOCK_PREFIX,
} from 'utils/const'
+import {
+ DefaultLockInfo,
+ MultiSigLockInfo,
+ LocktimeLockInfo,
+ AnyoneCanPayLockInfoOnAggron,
+ AnyoneCanPayLockInfoOnLina,
+ ChequeLockInfoOnAggron,
+ ChequeLockInfoOnLina,
+} from '../enums'
export const validateAddress = (address: string, isMainnet: boolean): boolean => {
const FIELD_NAME = 'address'
@@ -62,4 +72,22 @@ export const validateAddress = (address: string, isMainnet: boolean): boolean =>
return true
}
+const addressTagMap = {
+ [DefaultLockInfo.TagName]: [DefaultLockInfo, DefaultLockInfo],
+ [MultiSigLockInfo.TagName]: [MultiSigLockInfo, MultiSigLockInfo],
+ [LocktimeLockInfo.TagName]: [LocktimeLockInfo, LocktimeLockInfo],
+ [AnyoneCanPayLockInfoOnAggron.TagName]: [AnyoneCanPayLockInfoOnLina, AnyoneCanPayLockInfoOnAggron],
+ [ChequeLockInfoOnAggron.TagName]: [ChequeLockInfoOnLina, ChequeLockInfoOnAggron],
+}
+
+export function validateSpecificAddress(address: string, isMainnet: boolean, tagName: keyof typeof addressTagMap) {
+ validateAddress(address, isMainnet)
+ const script = ckbCore.utils.addressToScript(address)
+ const lockInfo = addressTagMap[tagName][isMainnet ? 0 : 1] // first is lock on Lina
+ if (script.codeHash !== lockInfo.CodeHash || script.hashType !== lockInfo.HashType) {
+ throw new AddressNotMatchException(tagName)
+ }
+ return true
+}
+
export default validateAddress
diff --git a/packages/neuron-ui/src/utils/validators/assetAccountAddress.ts b/packages/neuron-ui/src/utils/validators/assetAccountAddress.ts
index 91e4bf2bbe..6478e36b14 100644
--- a/packages/neuron-ui/src/utils/validators/assetAccountAddress.ts
+++ b/packages/neuron-ui/src/utils/validators/assetAccountAddress.ts
@@ -1,58 +1,43 @@
import { ckbCore } from 'services/chain'
import { FieldRequiredException, FieldInvalidException, AddressDeprecatedException } from 'exceptions'
-import {
- SHORT_ADDR_DEFAULT_LOCK_PREFIX,
- SHORT_ADDR_LENGTH,
- LONG_TYPE_PREFIX,
- SHORT_ADDR_SUDT_LOCK_PREFIX,
-} from 'utils/const'
-import { DeprecatedScript, AccountType } from 'utils/enums'
+import { DEPRECATED_CODE_HASH } from 'utils/const'
+import { isSecp256k1Address } from 'utils/is'
import { validateAddress } from './address'
+/**
+ * accept secp256k1 address for cheque txn and anyone-can-pay address(with locktime) for sudt/ckb asset txn
+ */
export const validateAssetAccountAddress = ({
address,
isMainnet,
required = false,
- type = AccountType.SUDT,
}: {
address: string
- codeHash?: string
isMainnet: boolean
required?: boolean
- type?: AccountType
}) => {
const FIELD_NAME = 'address'
if (address) {
validateAddress(address, isMainnet)
- const parsed = ckbCore.utils.parseAddress(address, 'hex')
- if (
- type === AccountType.SUDT &&
- parsed.startsWith(SHORT_ADDR_DEFAULT_LOCK_PREFIX) &&
- address.length === SHORT_ADDR_LENGTH
- ) {
+ if (isSecp256k1Address(address)) {
return true
}
- if ([DeprecatedScript.AcpOnAggron, DeprecatedScript.AcpOnLina].some(script => parsed.startsWith(script))) {
+ const lockScript = ckbCore.utils.addressToScript(address)
+
+ if ([DEPRECATED_CODE_HASH.AcpOnAggron, DEPRECATED_CODE_HASH.AcpOnLina].includes(lockScript.codeHash)) {
throw new AddressDeprecatedException()
}
- const ARGS_LENGTH = 40
- let minimums = ''
+ const ARGS_LENGTH = 42
- if (parsed.startsWith(LONG_TYPE_PREFIX)) {
- const CODE_HASH_LENGTH = 64
- if (parsed.length < LONG_TYPE_PREFIX.length + CODE_HASH_LENGTH + ARGS_LENGTH) {
- throw new FieldInvalidException(FIELD_NAME)
- }
- minimums = parsed.slice(LONG_TYPE_PREFIX.length + CODE_HASH_LENGTH + ARGS_LENGTH)
- } else if (parsed.startsWith(SHORT_ADDR_SUDT_LOCK_PREFIX)) {
- minimums = parsed.slice(SHORT_ADDR_SUDT_LOCK_PREFIX.length + ARGS_LENGTH)
- } else {
+ if (lockScript.args.length < ARGS_LENGTH) {
throw new FieldInvalidException(FIELD_NAME)
}
+ const minimums = lockScript.args.slice(ARGS_LENGTH)
+
if (minimums && ((minimums.length !== 2 && minimums.length !== 4) || Number.isNaN(+`0x${minimums}`))) {
throw new FieldInvalidException(FIELD_NAME)
}
diff --git a/packages/neuron-ui/src/utils/validators/totalAmount.ts b/packages/neuron-ui/src/utils/validators/totalAmount.ts
index 6cff28f98e..3c9cc68ede 100644
--- a/packages/neuron-ui/src/utils/validators/totalAmount.ts
+++ b/packages/neuron-ui/src/utils/validators/totalAmount.ts
@@ -1,13 +1,13 @@
-import { BalanceNotEnoughException } from 'exceptions/index'
+import { AmountNotEnoughException } from 'exceptions'
export const validateTotalAmount = (totalAmount: string, fee: string, balance: string) => {
if (BigInt(balance) < BigInt(0)) {
- throw new BalanceNotEnoughException()
+ throw new AmountNotEnoughException()
}
if (BigInt(totalAmount) + BigInt(fee) <= BigInt(balance)) {
return true
}
- throw new BalanceNotEnoughException()
+ throw new AmountNotEnoughException()
}
export default validateTotalAmount
diff --git a/packages/neuron-ui/src/widgets/CopyZoneAddress/copyZoneAddress.module.scss b/packages/neuron-ui/src/widgets/CopyZoneAddress/copyZoneAddress.module.scss
new file mode 100644
index 0000000000..c491ee320e
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/CopyZoneAddress/copyZoneAddress.module.scss
@@ -0,0 +1,17 @@
+.fullPayload {
+ min-width: 100px;
+ display: flex;
+ height: 1.625rem;
+ flex: 1;
+
+ .overflow {
+ word-break: break-all;
+ text-align: right;
+ line-height: 1.625rem;
+ overflow: hidden;
+ }
+
+ & > span {
+ line-height: 1.625rem;
+ }
+}
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/CopyZoneAddress/index.tsx b/packages/neuron-ui/src/widgets/CopyZoneAddress/index.tsx
new file mode 100644
index 0000000000..139cc86f45
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/CopyZoneAddress/index.tsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import CopyZone from 'widgets/CopyZone'
+import styles from './copyZoneAddress.module.scss'
+
+const Address = ({ fullPayload, className }: { fullPayload: string; className?: string }) => {
+ const [t] = useTranslation()
+ return (
+
+ {fullPayload.slice(0, -6)}
+ ...
+ {fullPayload.slice(-6)}
+
+ )
+}
+
+Address.displayName = 'MultisigAddress'
+
+export default React.memo(Address)
diff --git a/packages/neuron-ui/src/widgets/Dropdown/dropdown.module.scss b/packages/neuron-ui/src/widgets/Dropdown/dropdown.module.scss
new file mode 100644
index 0000000000..229c83769f
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Dropdown/dropdown.module.scss
@@ -0,0 +1,36 @@
+.arrowDown {
+ g[mask] {
+ fill: #666;
+ }
+}
+
+.customizableRoot {
+ position: relative;
+
+ .dropdownItems {
+ display: none;
+ position: absolute;
+ z-index: 100;
+ margin: 0;
+ background-color: #fff;
+ border-radius: 2px;
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12);
+ right: 0;
+
+ & > button {
+ border-radius: 0;
+ background-color: #fff;
+
+ &:hover {
+ background-color: #ddd;
+ font-weight: 500;
+ }
+ }
+ }
+
+ &:hover {
+ .dropdownItems {
+ display: block;
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Dropdown/index.tsx b/packages/neuron-ui/src/widgets/Dropdown/index.tsx
index ecd28a86ff..33fbf81a66 100644
--- a/packages/neuron-ui/src/widgets/Dropdown/index.tsx
+++ b/packages/neuron-ui/src/widgets/Dropdown/index.tsx
@@ -1,10 +1,12 @@
-import React from 'react'
+import React, { useCallback } from 'react'
import { Dropdown, IDropdownProps, Icon } from 'office-ui-fabric-react'
+import Button from 'widgets/Button'
+import styles from './dropdown.module.scss'
const CustomDropdown = (props: IDropdownProps) => (
{
- return
+ return
}}
styles={{
label: {
@@ -41,4 +43,44 @@ const CustomDropdown = (props: IDropdownProps) => (
/>
)
+interface DropdownItem {
+ key: string
+ label: string
+ disabled?: boolean
+}
+export const CustomizableDropdown = ({
+ onClickItem: onClickItemCallback,
+ className,
+ children,
+ options,
+}: {
+ options: DropdownItem[]
+ onClickItem?: (item: DropdownItem) => void
+} & React.HTMLProps) => {
+ const onClickItem = useCallback(
+ (option: DropdownItem) => (e: React.MouseEvent) => {
+ e.nativeEvent.stopImmediatePropagation()
+ if (onClickItemCallback) {
+ onClickItemCallback(option)
+ }
+ },
+ [onClickItemCallback]
+ )
+ return (
+
+ {children}
+
+ {options.map(option => (
+
+ ))}
+
+
+ )
+}
export default CustomDropdown
diff --git a/packages/neuron-ui/src/widgets/DropdownButton/dropdownButton.module.scss b/packages/neuron-ui/src/widgets/DropdownButton/dropdownButton.module.scss
index 571a9b9762..191c7dbac7 100644
--- a/packages/neuron-ui/src/widgets/DropdownButton/dropdownButton.module.scss
+++ b/packages/neuron-ui/src/widgets/DropdownButton/dropdownButton.module.scss
@@ -67,6 +67,10 @@
}
}
+.container {
+ line-height: 0;
+}
+
.container > button:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
diff --git a/packages/neuron-ui/src/widgets/Icons/Alert.svg b/packages/neuron-ui/src/widgets/Icons/Alert.svg
new file mode 100644
index 0000000000..50bec672db
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Alert.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/ArrowLeftOutlined.svg b/packages/neuron-ui/src/widgets/Icons/ArrowLeftOutlined.svg
new file mode 100644
index 0000000000..ec0da4a6f4
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/ArrowLeftOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Check.svg b/packages/neuron-ui/src/widgets/Icons/Check.svg
new file mode 100644
index 0000000000..ec964e9ad3
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Close.svg b/packages/neuron-ui/src/widgets/Icons/Close.svg
new file mode 100644
index 0000000000..544c0e0b77
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Connected.svg b/packages/neuron-ui/src/widgets/Icons/Connected.svg
new file mode 100644
index 0000000000..0edf231ce9
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Connected.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/CopyOutlined.svg b/packages/neuron-ui/src/widgets/Icons/CopyOutlined.svg
new file mode 100644
index 0000000000..e2f7c88f14
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/CopyOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Explorer.svg b/packages/neuron-ui/src/widgets/Icons/Explorer.svg
new file mode 100644
index 0000000000..61451eaaee
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Explorer.svg
@@ -0,0 +1 @@
+
diff --git a/packages/neuron-ui/src/widgets/Icons/InfoCircleOutlined.svg b/packages/neuron-ui/src/widgets/Icons/InfoCircleOutlined.svg
new file mode 100644
index 0000000000..f1471b4d2e
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/InfoCircleOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Keystore.svg b/packages/neuron-ui/src/widgets/Icons/Keystore.svg
new file mode 100644
index 0000000000..e306b65fb5
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Keystore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Matched.svg b/packages/neuron-ui/src/widgets/Icons/Matched.svg
new file mode 100644
index 0000000000..362a4b645d
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Matched.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/More.svg b/packages/neuron-ui/src/widgets/Icons/More.svg
new file mode 100644
index 0000000000..a52cac6f32
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/More.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/PendingIcon.svg b/packages/neuron-ui/src/widgets/Icons/PendingIcon.svg
new file mode 100644
index 0000000000..dab733bc34
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/PendingIcon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Plus.svg b/packages/neuron-ui/src/widgets/Icons/Plus.svg
new file mode 100644
index 0000000000..94f0a8894e
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/RightOutlined.svg b/packages/neuron-ui/src/widgets/Icons/RightOutlined.svg
new file mode 100644
index 0000000000..b27585dba2
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/RightOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/ScanOutlined.svg b/packages/neuron-ui/src/widgets/Icons/ScanOutlined.svg
new file mode 100644
index 0000000000..fb97bb956f
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/ScanOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Search.svg b/packages/neuron-ui/src/widgets/Icons/Search.svg
new file mode 100644
index 0000000000..9a0b02c23a
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/Settings.svg b/packages/neuron-ui/src/widgets/Icons/Settings.svg
new file mode 100644
index 0000000000..1526c2043c
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/Settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/UploadOutlined.svg b/packages/neuron-ui/src/widgets/Icons/UploadOutlined.svg
new file mode 100644
index 0000000000..1284d4b688
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/UploadOutlined.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/icon.module.scss b/packages/neuron-ui/src/widgets/Icons/icon.module.scss
new file mode 100644
index 0000000000..3d9a4ba0bd
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/icon.module.scss
@@ -0,0 +1,34 @@
+
+$error-color: #d50000;
+$success-color: #008000;
+$activity-color: #1b73b3;
+
+.error {
+ path {
+ fill: $error-color;
+ stroke: $error-color;
+ }
+ polygon {
+ fill: $error-color;
+ }
+}
+
+.success {
+ path {
+ fill: $success-color;
+ stroke: $success-color;
+ }
+ polygon {
+ fill: $success-color;
+ }
+}
+
+.activity {
+ path {
+ fill: $activity-color;
+ stroke: $activity-color;
+ }
+ polygon {
+ fill: $activity-color;
+ }
+}
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/Icons/icon.tsx b/packages/neuron-ui/src/widgets/Icons/icon.tsx
new file mode 100644
index 0000000000..356fd75c23
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/icon.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+import { ReactComponent as ExplorerSvg } from './Explorer.svg'
+import { ReactComponent as SearchSvg } from './Search.svg'
+import { ReactComponent as FirstPageSvg } from './FirstPage.svg'
+import { ReactComponent as PreviousPageSvg } from './PreviousPage.svg'
+import { ReactComponent as LastPageSvg } from './LastPage.svg'
+import { ReactComponent as NextPageSvg } from './NextPage.svg'
+import { ReactComponent as MatchedSvg } from './Matched.svg'
+import { ReactComponent as InfoCircleOutlinedSvg } from './InfoCircleOutlined.svg'
+import { ReactComponent as CheckSvg } from './Check.svg'
+import { ReactComponent as CloseSvg } from './Close.svg'
+import { ReactComponent as MoreSvg } from './More.svg'
+import { ReactComponent as ArrowDownSvg } from './ArrowToNext.svg'
+import { ReactComponent as AlertSvg } from './Alert.svg'
+import { ReactComponent as CopyOutlinedSvg } from './CopyOutlined.svg'
+import { ReactComponent as RightOutlinedSvg } from './RightOutlined.svg'
+import { ReactComponent as ScanOutlinedSvg } from './ScanOutlined.svg'
+import { ReactComponent as UploadOutlinedSvg } from './UploadOutlined.svg'
+import { ReactComponent as PlusSvg } from './Plus.svg'
+import { ReactComponent as ArrowLeftOutlinedSvg } from './ArrowLeftOutlined.svg'
+import { ReactComponent as ConnectedSvg } from './Connected.svg'
+import { ReactComponent as KeystoreSvg } from './Keystore.svg'
+import { ReactComponent as SettingsSvg } from './Settings.svg'
+import { ReactComponent as PendingIconSvg } from './PendingIcon.svg'
+import { ReactComponent as NewTabSvg } from './new_tab.svg'
+
+import styles from './icon.module.scss'
+
+function WrapSvg(SvgComponent: React.FC>) {
+ return ({
+ type,
+ className,
+ ...props
+ }: { type?: 'success' | 'activity' | 'error' } & React.SVGProps = {}) => {
+ return
+ }
+}
+
+export const Explorer = WrapSvg(ExplorerSvg)
+export const Search = WrapSvg(SearchSvg)
+export const FirstPage = WrapSvg(FirstPageSvg)
+export const PreviousPage = WrapSvg(PreviousPageSvg)
+export const LastPage = WrapSvg(LastPageSvg)
+export const NextPage = WrapSvg(NextPageSvg)
+export const Matched = WrapSvg(MatchedSvg)
+export const InfoCircleOutlined = WrapSvg(InfoCircleOutlinedSvg)
+export const Close = WrapSvg(CloseSvg)
+export const More = WrapSvg(MoreSvg)
+export const ArrowDown = WrapSvg(ArrowDownSvg)
+export const Alert = WrapSvg(AlertSvg)
+export const Check = WrapSvg(CheckSvg)
+export const CopyOutlined = WrapSvg(CopyOutlinedSvg)
+export const RightOutlined = WrapSvg(RightOutlinedSvg)
+export const ScanOutlined = WrapSvg(ScanOutlinedSvg)
+export const UploadOutlined = WrapSvg(UploadOutlinedSvg)
+export const Plus = WrapSvg(PlusSvg)
+export const ArrowLeftOutlined = WrapSvg(ArrowLeftOutlinedSvg)
+export const Connected = WrapSvg(ConnectedSvg)
+export const Keystore = WrapSvg(KeystoreSvg)
+export const Settings = WrapSvg(SettingsSvg)
+export const PendingIcon = WrapSvg(PendingIconSvg)
+export const NewTab = WrapSvg(NewTabSvg)
diff --git a/packages/neuron-ui/src/widgets/Icons/new_tab.svg b/packages/neuron-ui/src/widgets/Icons/new_tab.svg
new file mode 100644
index 0000000000..00c3eabeb6
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/Icons/new_tab.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/packages/neuron-ui/src/widgets/InputSelect/index.tsx b/packages/neuron-ui/src/widgets/InputSelect/index.tsx
new file mode 100644
index 0000000000..0a60ce5fa6
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/InputSelect/index.tsx
@@ -0,0 +1,138 @@
+import React, { useRef, useState, useEffect, useCallback } from 'react'
+import { useDidMount, useForceUpdate } from 'utils'
+import styles from './input-select.module.scss'
+
+export interface SelectOptions {
+ label: React.ReactNode
+ value: string
+ data?: any
+ className?: string
+}
+
+export interface InputSelectProps {
+ options: SelectOptions[]
+ className?: string
+ disabled?: boolean
+ onChange?: (value: string, arg?: SelectOptions) => void
+ value?: string
+ placeholder?: String
+}
+
+function parseValue(value: string, options: SelectOptions[]) {
+ const option = options.find(o => o.value === value)
+ return option?.value || value
+}
+
+const Select = ({ value, options, placeholder, disabled, onChange, className }: InputSelectProps) => {
+ const mounted = useRef(true)
+ const root = useRef(null)
+ const openRef = useRef(false)
+ const setOpen = useForceUpdate((isOpen: boolean) => {
+ openRef.current = isOpen
+ })
+ const [innerValue, setInnerValue] = useState('')
+
+ const onDocumentClick = useCallback(
+ e => {
+ if (mounted.current && !root.current!.contains(e.target) && openRef.current) {
+ setOpen(false)
+ }
+ },
+ [openRef, setOpen]
+ )
+
+ const onMouseDown = useCallback(() => {
+ if (!disabled) {
+ setOpen(!openRef.current)
+ }
+ }, [openRef, disabled, setOpen])
+
+ const setValue = useCallback(
+ (option: SelectOptions) => {
+ if (onChange) {
+ onChange(option.value, option)
+ }
+
+ setInnerValue(option.value)
+ setOpen(false)
+ },
+ [onChange, setInnerValue, setOpen]
+ )
+
+ const onInputChange = useCallback(
+ (e: React.ChangeEvent) => {
+ if (onChange) {
+ onChange(e.target.value)
+ }
+
+ setInnerValue(e.target.value)
+ setOpen(false)
+ },
+ [setInnerValue, setOpen, onChange]
+ )
+ const renderOption = useCallback(
+ (option: SelectOptions) => {
+ const { value: val } = option
+ const label: React.ReactNode = option.label || option.value
+ const isSelected = val === innerValue
+
+ return (
+
+ {label}
+
+ )
+ },
+ [innerValue, setValue]
+ )
+
+ useDidMount(() => {
+ document.addEventListener('click', onDocumentClick, false)
+ return () => document.removeEventListener('click', onDocumentClick, false)
+ })
+
+ useEffect(() => {
+ if (value !== undefined) {
+ const nextSelected = parseValue(value, options)
+ setInnerValue(nextSelected)
+ } else {
+ setInnerValue('')
+ }
+ }, [value, placeholder, options])
+
+ const disabledClass = disabled ? styles.disabled : ''
+ const dropdownClass = `${className || ''} ${styles.root} ${openRef.current ? styles.isOpen : ''}`
+ const controlClass = `${styles.control} ${disabledClass} ${openRef.current ? styles.isOpen : ''}`
+ const placeholderClass = `${styles.placeholder} ${innerValue !== '' ? styles.isSelected : ''}`
+
+ return (
+
+
+ {openRef.current ? (
+
+ {options.map(option => renderOption(option))}
+
+ ) : null}
+
+ )
+}
+
+Select.displayName = 'Select'
+
+export default Select
diff --git a/packages/neuron-ui/src/widgets/InputSelect/input-select.module.scss b/packages/neuron-ui/src/widgets/InputSelect/input-select.module.scss
new file mode 100644
index 0000000000..2703b909d8
--- /dev/null
+++ b/packages/neuron-ui/src/widgets/InputSelect/input-select.module.scss
@@ -0,0 +1,102 @@
+.root {
+ position: relative;
+ font-size: 0.75rem;
+}
+
+
+.control {
+ font-family: inherit;
+ grid-area: input;
+ color: #434343;
+ border: solid 1px #000000;
+ display: flex;
+ justify-content: flex-start;
+ align-items: stretch;
+ box-sizing: border-box;
+ height: 32px;
+
+ &[data-open="true"] {
+ border: 1px solid #000;
+ }
+
+ &:hover {
+ border: 1px solid #000;
+ }
+
+ & > input {
+ border: none;
+ width: 100%;
+ padding: 4px 24px 4px 12px;
+ }
+}
+
+.arrow {
+ border-color: #999 transparent transparent;
+ border-style: solid;
+ border-width: 5px 5px 0;
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.isOpen .arrow {
+ border-color: transparent transparent #999;
+ border-width: 0 5px 5px;
+}
+
+.menu {
+ background-color: white;
+ border: 1px solid #ccc;
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
+ box-sizing: border-box;
+ border-top: none;
+ max-height: 200px;
+ overflow-y: auto;
+ position: absolute;
+ top: 100%;
+ width: 100%;
+ z-index: 1000;
+ -webkit-overflow-scrolling: touch;
+}
+
+.menu .group > .title{
+ padding: 8px 10px;
+ color: rgba(51, 51, 51, 1);
+ font-weight: bold;
+ text-transform: capitalize;
+}
+
+.option {
+ box-sizing: border-box;
+ color: rgba(51, 51, 51, 0.8);
+ cursor: pointer;
+ display: block;
+ padding: 8px 10px;
+
+ &[aria-selected="true"] {
+ background-color: #ccc;
+ }
+
+ &.is-select {
+ background-color: #eee;
+ }
+
+ &:last-child {
+ border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 2px;
+ }
+
+ &:hover {
+ background-color: #eee;
+ color: #333;
+ }
+}
+
+.noresults {
+ box-sizing: border-box;
+ color: #ccc;
+ cursor: default;
+ display: block;
+ padding: 8px 10px;
+}
diff --git a/packages/neuron-ui/src/widgets/TextField/index.tsx b/packages/neuron-ui/src/widgets/TextField/index.tsx
index 11af4d6a36..dd15409bc0 100644
--- a/packages/neuron-ui/src/widgets/TextField/index.tsx
+++ b/packages/neuron-ui/src/widgets/TextField/index.tsx
@@ -1,79 +1,139 @@
-import React from 'react'
+import React, { useCallback, useRef, useState } from 'react'
import { ReactComponent as Attention } from 'widgets/Icons/Attention.svg'
+import { ReactComponent as Edit } from 'widgets/Icons/Edit.svg'
import styles from './textField.module.scss'
-const TextField = ({
- label,
- field,
+const TextField = React.forwardRef(
+ (
+ {
+ label,
+ field,
+ value,
+ hint,
+ error,
+ onChange,
+ onClick,
+ type = 'text',
+ className = '',
+ placeholder = '',
+ suffix,
+ stack = true,
+ required = false,
+ readOnly = false,
+ ...rest
+ }: {
+ field: string
+ label?: string
+ value?: string
+ hint?: string
+ error?: string
+ type?: 'text' | 'password' | 'file'
+ onChange?: (e: React.ChangeEvent) => void
+ onClick?: (e: React.SyntheticEvent) => void
+ className?: string
+ suffix?: string | React.ReactNode | undefined
+ stack?: boolean
+ required?: boolean
+ readOnly?: boolean
+ placeholder?: string
+
+ [key: string]: any
+ },
+ ref: React.LegacyRef
+ ) => {
+ return (
+
+ {label ? (
+
+ ) : null}
+
+
+ {suffix ? {suffix} : null}
+
+ {hint ? {hint} : null}
+ {error ? (
+
+
+ {error}
+
+ ) : null}
+
+ )
+ }
+)
+
+export const EditTextField = ({
value,
- hint,
- error,
onChange,
- onClick,
- type = 'text',
- className = '',
- placeholder = '',
- suffix,
- stack = true,
- required = false,
- readOnly = false,
...rest
}: {
- field: string
- label?: string
- value?: string
- hint?: string
- error?: string
- type?: 'text' | 'password' | 'file'
- onChange?: (e: React.SyntheticEvent) => void
- onClick?: (e: React.SyntheticEvent) => void
- className?: string
- suffix?: string | React.ReactNode | undefined
- stack?: boolean
- required?: boolean
- readOnly?: boolean
- placeholder?: string
+ value: string
+ onChange?: (value: string | undefined) => void
+} & React.ComponentPropsWithRef) => {
+ const rootRef = useRef()
+ const [editedValue, changeEditValue] = useState('')
+ const [isActive, changeActive] = useState(false)
+ const focusEdit = useCallback(() => {
+ if (rootRef.current && rootRef.current.querySelector('input')) {
+ changeEditValue(value)
+ rootRef.current.querySelector('input').focus()
+ }
+ changeActive(true)
+ }, [changeActive, value])
+ const onBlur = useCallback(() => {
+ changeActive(false)
+ if (onChange && editedValue !== value) {
+ onChange(editedValue)
+ }
+ }, [onChange, changeActive, editedValue, value])
+ const onChangeFocus = useCallback(
+ (e: React.ChangeEvent) => {
+ changeEditValue(e.target.value)
+ },
+ [changeEditValue]
+ )
- [key: string]: any
-}) => {
return (
-
- {label ? (
-
- ) : null}
-
-
- {suffix ? {suffix} : null}
-
- {hint ? {hint} : null}
- {error ? (
-
-
- {error}
-
- ) : null}
-
+
+
+
+ )
+ }
+ />
)
}
-
TextField.displayName = 'TextField'
export default TextField
diff --git a/packages/neuron-ui/src/widgets/TextField/textField.module.scss b/packages/neuron-ui/src/widgets/TextField/textField.module.scss
index 1d08dde5b1..53397f79d9 100644
--- a/packages/neuron-ui/src/widgets/TextField/textField.module.scss
+++ b/packages/neuron-ui/src/widgets/TextField/textField.module.scss
@@ -103,3 +103,41 @@ $secondary-font-size: 0.63rem;
margin-top: 0.125rem;
}
}
+
+.editTextField {
+ display: flex;
+ align-items: center;
+
+ .editBtn {
+ appearance: none;
+ border: none;
+ background-color: transparent;
+ width: 2rem;
+ height: 1rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ svg {
+ pointer-events: none;
+
+ g {
+ fill: #000;
+ }
+ }
+ }
+
+ input {
+ font-size: 0.875rem;
+ }
+
+ & > div {
+ border: none !important;
+ }
+
+ @media screen and (max-width: 1500px) {
+ input {
+ width: 100px;
+ }
+ }
+}
diff --git a/packages/neuron-wallet/.env b/packages/neuron-wallet/.env
index 09f9b917ab..b091d32d71 100644
--- a/packages/neuron-wallet/.env
+++ b/packages/neuron-wallet/.env
@@ -110,3 +110,11 @@ TESTNET_NFT_DEP_INDEX=2
TESTNET_NFT_DEP_TYPE=code
TESTNET_NFT_SCRIPT_CODEHASH=0xb1837b5ad01a88558731953062d1f5cb547adf89ece01e8934a9f0aeed2d959f
TESTNET_NFT_SCRIPT_HASH_TYPE=type
+
+# DEFAULT SCRIPT
+SECP256K1_CODE_HASH=0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8
+DAO_CODE_HASH=0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e
+MULTISIG_CODE_HASH=0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8
+
+# CKB NODE OPTIONS
+CKB_NODE_ASSUME_VALID_TARGET='0x6c914adcfd5289e8c4058e60e83555d5eb04ddc547b6184f1fae72d5f36929a3'
diff --git a/packages/neuron-wallet/.eslintrc.js b/packages/neuron-wallet/.eslintrc.js
index 7b3bdb3377..e3816554d8 100644
--- a/packages/neuron-wallet/.eslintrc.js
+++ b/packages/neuron-wallet/.eslintrc.js
@@ -1,11 +1,14 @@
module.exports = {
- "extends": "eslint:recommended",
+ "extends": ["eslint:recommended", "plugin:prettier/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
+ "prettier/prettier": [2, {
+ "printWidth": 120
+ }],
"no-console": 0,
"no-cond-assign": 0,
"no-extra-semi": "warn",
diff --git a/packages/neuron-wallet/.prettierrc b/packages/neuron-wallet/.prettierrc
new file mode 100644
index 0000000000..0e5c1dbe46
--- /dev/null
+++ b/packages/neuron-wallet/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "trailingComma": "none",
+ "semi": false,
+ "singleQuote": true
+}
diff --git a/packages/neuron-wallet/package.json b/packages/neuron-wallet/package.json
index 1b78931ba0..91009c198c 100644
--- a/packages/neuron-wallet/package.json
+++ b/packages/neuron-wallet/package.json
@@ -3,7 +3,7 @@
"productName": "Neuron",
"description": "CKB Neuron Wallet",
"homepage": "https://www.nervos.org/",
- "version": "0.101.3",
+ "version": "0.103.0",
"private": true,
"author": {
"name": "Nervos Core Dev",
@@ -41,10 +41,10 @@
"@iarna/toml": "2.2.5",
"@ledgerhq/hw-transport-node-hid": "6.20.0",
"@nervina-labs/ckb-indexer": "0.1.1",
- "@nervosnetwork/ckb-sdk-core": "0.101.0",
- "@nervosnetwork/ckb-sdk-utils": "0.101.0",
+ "@nervosnetwork/ckb-sdk-core": "0.102.2",
+ "@nervosnetwork/ckb-sdk-utils": "0.102.2",
"archiver": "5.3.0",
- "async": "3.2.0",
+ "async": "3.2.2",
"axios": "0.21.4",
"bn.js": "4.11.8",
"chalk": "3.0.0",
@@ -60,13 +60,13 @@
"reflect-metadata": "0.1.13",
"rxjs": "6.5.3",
"sha3": "2.0.7",
- "sqlite3": "5.0.2",
+ "sqlite3": "5.0.3",
"subleveldown": "^4.1.4",
"typeorm": "0.2.25",
"uuid": "8.3.2"
},
"devDependencies": {
- "@nervosnetwork/ckb-types": "0.101.0",
+ "@nervosnetwork/ckb-types": "0.102.2",
"@types/archiver": "3.1.0",
"@types/async": "3.2.3",
"@types/electron-devtools-installer": "2.2.0",
@@ -86,9 +86,12 @@
"electron-builder": "22.14.5",
"electron-devtools-installer": "2.2.4",
"electron-notarize": "0.2.1",
+ "eslint-config-prettier": "6.7.0",
+ "eslint-plugin-prettier": "3.1.1",
"jest-when": "2.7.2",
"lint-staged": "9.2.5",
- "neuron-ui": "0.101.3",
+ "neuron-ui": "0.103.0",
+ "prettier": "1.19.1",
"rimraf": "3.0.0",
"ttypescript": "1.5.10",
"typescript": "4.2.3"
diff --git a/packages/neuron-wallet/src/block-sync-renderer/index.ts b/packages/neuron-wallet/src/block-sync-renderer/index.ts
index 8122090d19..3f4142439f 100644
--- a/packages/neuron-wallet/src/block-sync-renderer/index.ts
+++ b/packages/neuron-wallet/src/block-sync-renderer/index.ts
@@ -24,34 +24,36 @@ let requests = new Map>()
export const killBlockSyncTask = async () => {
const _child = child
child = null
- if (!_child) { return }
+ if (!_child) {
+ return
+ }
logger.info('Sync:\tdrain requests')
await Promise.all(
- [...requests.values()].map(
- ({ reject }) =>
- typeof reject === 'function'
- ? reject()
- : logger.error(`Worker:\treject is not a function, get ${reject}`)
- ))
- .finally(() => requests = new Map())
+ [...requests.values()].map(({ reject }) =>
+ typeof reject === 'function' ? reject() : logger.error(`Worker:\treject is not a function, get ${reject}`)
+ )
+ ).finally(() => (requests = new Map()))
await waitForChildClose(_child)
await IndexerService.getInstance().stop()
}
-const waitForChildClose = (c: ChildProcess) => new Promise((resolve, reject) => {
- c.once('close', resolve)
- const msg: Required = {
- type: 'call',
- id: requestId++,
- channel: 'unmount',
- message: null
- }
- c.send(msg, err => {
- if (err) { reject(err) }
- })
-}).catch(() => 0)
+const waitForChildClose = (c: ChildProcess) =>
+ new Promise((resolve, reject) => {
+ c.once('close', resolve)
+ const msg: Required = {
+ type: 'call',
+ id: requestId++,
+ channel: 'unmount',
+ message: null
+ }
+ c.send(msg, err => {
+ if (err) {
+ reject(err)
+ }
+ })
+ }).catch(() => 0)
export const resetSyncTask = async (startTask = true) => {
await killBlockSyncTask()
@@ -64,7 +66,9 @@ export const resetSyncTask = async (startTask = true) => {
}
export const switchToNetwork = async (newNetwork: Network, reconnected = false, shouldSync = true) => {
- if (!reconnected && network?.id === newNetwork.id) { return }
+ if (!reconnected && network?.id === newNetwork.id && network?.genesisHash === newNetwork.genesisHash) {
+ return
+ }
network = newNetwork
@@ -79,8 +83,15 @@ export const switchToNetwork = async (newNetwork: Network, reconnected = false,
export const queryIndexer = async (query: LumosCellQuery): Promise => {
const _child = child
- if (!_child) { return [] }
- const msg: Required> = { type: 'call', id: requestId++, channel: 'queryIndexer', message: query }
+ if (!_child) {
+ return []
+ }
+ const msg: Required> = {
+ type: 'call',
+ id: requestId++,
+ channel: 'queryIndexer',
+ message: query
+ }
return registerRequest(_child, msg).catch(err => {
logger.error(`Sync:\tfailed to register query indexer task`, err)
return []
@@ -100,7 +111,9 @@ export const createBlockSyncTask = async () => {
child.on('message', ({ id, message, channel }: WorkerMessage) => {
if (id !== undefined) {
- if (!requests.has(id)) { return }
+ if (!requests.has(id)) {
+ return
+ }
const { resolve } = requests.get(id)!
requests.delete(id)
if (typeof resolve === 'function') {
@@ -112,7 +125,6 @@ export const createBlockSyncTask = async () => {
} else {
logger.error(`Sync:\tresolve expected, got ${resolve}`)
}
-
} else {
switch (channel) {
case 'cache-tip-block-updated':
@@ -132,7 +144,6 @@ export const createBlockSyncTask = async () => {
default:
break
}
-
}
})
@@ -140,21 +151,25 @@ export const createBlockSyncTask = async () => {
logger.error('Sync:ChildProcess:', data)
})
-
if (!network) {
network = NetworksService.getInstance().getCurrent()
}
DataUpdateSubject.next({
dataType: 'transaction',
- actionType: 'update',
+ actionType: 'update'
})
const _child = child
if (network.genesisHash !== EMPTY_GENESIS_HASH) {
const addressMetas = await AddressService.getAddressesByAllWallets()
- const message: StartParams = { genesisHash: network.genesisHash, url: network.remote, addressMetas, indexerUrl: IndexerService.LISTEN_URI }
+ const message: StartParams = {
+ genesisHash: network.genesisHash,
+ url: network.remote,
+ addressMetas,
+ indexerUrl: IndexerService.LISTEN_URI
+ }
const msg: Required> = { type: 'call', channel: 'start', id: requestId++, message }
return registerRequest(_child, msg).catch(err => {
logger.error(`Sync:\ffailed to register sync task`, err)
diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts
index b8759513ac..3ac3ab9c6d 100644
--- a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts
+++ b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts
@@ -1,6 +1,6 @@
import { getConnection } from 'typeorm'
import { queue } from 'async'
-import AddressMeta from "database/address/meta"
+import AddressMeta from 'database/address/meta'
import IndexerTxHashCache from 'database/chain/entities/indexer-tx-hash-cache'
import RpcService from 'services/rpc-service'
import TransactionWithStatus from 'models/chain/transaction-with-status'
@@ -12,12 +12,7 @@ export default class IndexerCacheService {
private walletId: string
private indexer: CkbIndexer
- constructor(
- walletId: string,
- addressMetas: AddressMeta[],
- rpcService: RpcService,
- indexer: CkbIndexer
- ) {
+ constructor(walletId: string, addressMetas: AddressMeta[], rpcService: RpcService, indexer: CkbIndexer) {
for (const addressMeta of addressMetas) {
if (addressMeta.walletId !== walletId) {
throw new Error(`address ${addressMeta.address} does not belong to wallet id ${walletId}`)
@@ -50,13 +45,13 @@ export default class IndexerCacheService {
.getMany()
}
- public static async nextUnprocessedBlock(walletIds: string[]): Promise<{blockNumber: string, blockHash: string} | undefined> {
+ public static async nextUnprocessedBlock(
+ walletIds: string[]
+ ): Promise<{ blockNumber: string; blockHash: string } | undefined> {
const result = await getConnection()
.getRepository(IndexerTxHashCache)
.createQueryBuilder()
- .where(
- 'walletId IN (:...walletIds) and isProcessed = false', {walletIds}
- )
+ .where('walletId IN (:...walletIds) and isProcessed = false', { walletIds })
.orderBy('blockNumber', 'ASC')
.getOne()
@@ -83,7 +78,7 @@ export default class IndexerCacheService {
.execute()
}
- private async fetchTxMapping(): Promise |