diff --git a/.ckb-light-version b/.ckb-light-version
index d379b578ae..5aff472ddf 100644
--- a/.ckb-light-version
+++ b/.ckb-light-version
@@ -1 +1 @@
-v0.3.6
+v0.4.1
diff --git a/.ckb-version b/.ckb-version
index cff1fcdc66..b56633ee63 100644
--- a/.ckb-version
+++ b/.ckb-version
@@ -1 +1 @@
-v0.113.1
+v0.121.0
diff --git a/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml
index 87de0e3831..292eca11e9 100644
--- a/.github/workflows/check-code-style.yml
+++ b/.github/workflows/check-code-style.yml
@@ -18,7 +18,7 @@ jobs:
cache: "yarn"
- name: Restore
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
node_modules
@@ -31,7 +31,7 @@ jobs:
- name: Changed Files
id: changed-files
- uses: tj-actions/changed-files@v41
+ uses: tj-actions/changed-files@v44
with:
files: "packages/**/*.{js,cjs,mjs,jsx,ts,tsx,css,scss}"
diff --git a/.github/workflows/check_checksums.yml b/.github/workflows/check_checksums.yml
index 4c047f4754..087175598d 100644
--- a/.github/workflows/check_checksums.yml
+++ b/.github/workflows/check_checksums.yml
@@ -8,13 +8,16 @@ jobs:
compare:
name: Compare checksums
runs-on: macos-latest
+ environment: Release
+ permissions:
+ contents: write # to append checksum for each commit
+ actions: read # to read artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate checksums from artifacts
- run:
- ruby ./scripts/release-checksums.rb ${{ github.event.release.tag_name }} | tee generated_checksums.txt
+ run: ruby ./scripts/release-checksums.rb ${{ github.event.release.tag_name }} | tee generated_checksums.txt
- name: Fetch checksums from release note
run: |
@@ -36,4 +39,3 @@ jobs:
- uses: peter-evans/commit-comment@v3
with:
body: ${{ steps.comment_body.outputs.body }}
-
diff --git a/.github/workflows/check_storybook.yml b/.github/workflows/check_storybook.yml
index 1f351e7fb1..26cf157f13 100644
--- a/.github/workflows/check_storybook.yml
+++ b/.github/workflows/check_storybook.yml
@@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
node:
- - 18.12.0
+ - 20.15.1
os:
- macos-latest
- ubuntu-20.04
@@ -35,7 +35,7 @@ jobs:
cache: "yarn"
- name: Restore
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
node_modules
diff --git a/.github/workflows/checksums-of-release-draft.yml b/.github/workflows/checksums-of-release-draft.yml
index e0881a8db9..7f46c4933a 100644
--- a/.github/workflows/checksums-of-release-draft.yml
+++ b/.github/workflows/checksums-of-release-draft.yml
@@ -10,6 +10,9 @@ on:
jobs:
checksums:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ actions: read
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout
diff --git a/.github/workflows/merge_released_into_develop.yml b/.github/workflows/merge_released_into_develop.yml
index aaffa45742..fc1af620da 100644
--- a/.github/workflows/merge_released_into_develop.yml
+++ b/.github/workflows/merge_released_into_develop.yml
@@ -9,8 +9,11 @@ jobs:
merge-to-dev:
name: Merge into develop
runs-on: ubuntu-latest
+ environment: Release
+ permissions:
+ pull-requests: write
steps:
- - uses: actions/checkout@master
+ - uses: actions/checkout@v4
- name: Request
uses: repo-sync/pull-request@v2
with:
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index c22478947d..126b544948 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
node:
- - 18.12.0
+ - 20.15.1
os:
- macos-latest
- ubuntu-20.04
@@ -20,6 +20,10 @@ jobs:
runs-on: ${{ matrix.os }}
+ environment: Release
+ permissions:
+ contents: write
+
name: ${{ matrix.os }}(Node.js ${{ matrix.node }})
steps:
@@ -38,7 +42,7 @@ jobs:
cache: "yarn"
- name: Restore
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
node_modules
@@ -47,7 +51,7 @@ jobs:
- name: Add msbuild to PATH
if: matrix.os == 'windows-2019'
- uses: microsoft/setup-msbuild@v1.3.1
+ uses: microsoft/setup-msbuild@v2
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: "true"
@@ -99,6 +103,11 @@ jobs:
env:
CI: false
+ # Inject LOG_ENCRYPTION_PUBLIC_KEY to encrypt sensitive log
+ - name: Inject environment variables
+ run: |
+ echo "LOG_ENCRYPTION_PUBLIC_KEY=${{ secrets.LOG_ENCRYPTION_PUBLIC_KEY }}" >> packages/neuron-wallet/.env
+
- name: Package for MacOS
if: matrix.os == 'macos-latest'
run: |
@@ -134,28 +143,28 @@ jobs:
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
- name: Neuron-Mac
+ name: Neuron-Mac-x64
path: release/Neuron-*-mac-x64.zip
- name: Upload Neuron App Zip(arm64)
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
- name: Neuron-Mac
+ name: Neuron-Mac-arm64
path: release/Neuron-*-mac-arm64.zip
- name: Upload Neuron Dmg
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
- name: Neuron-Dmg
+ name: Neuron-Dmg-x64
path: release/Neuron-*-x64.dmg
- name: Upload Neuron Dmg(arm64)
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
- name: Neuron-Dmg
+ name: Neuron-Dmg-arm64
path: release/Neuron-*-arm64.dmg
- name: Upload Neuron Win
diff --git a/.github/workflows/package_for_test.yml b/.github/workflows/package_for_test.yml
index d73e821810..6527d3f7f2 100644
--- a/.github/workflows/package_for_test.yml
+++ b/.github/workflows/package_for_test.yml
@@ -2,17 +2,17 @@ name: Package Neuron for Test
on:
issue_comment:
- types: [created, edited]
+ types: [created]
push:
jobs:
packaging:
- if: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/package')) || github.event_name == 'push' }}
+ if: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/package') && contains(fromJSON('["OWNER", "COLLABORATOR"]'), github.event.comment.author_association)) || github.event_name == 'push' }}
strategy:
matrix:
node:
- - 18.12.0
+ - 20.15.1
os:
- macos-latest
- ubuntu-20.04
@@ -20,6 +20,10 @@ jobs:
runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+ pull-requests: read
+
name: ${{ matrix.os }}(Node.js ${{ matrix.node }})
env:
@@ -42,6 +46,37 @@ jobs:
with:
ref: refs/pull/${{ github.event.issue.number }}/merge
+ - name: Ensure no more commits after the triggering comment
+ uses: actions/github-script@v7
+ if: ${{ github.event_name == 'issue_comment' }}
+ env:
+ ISSUE_NUMBER: ${{github.event.issue.number}}
+ COMMENT_ID: ${{ github.event.comment.id }}
+ with:
+ script: |
+ const { ISSUE_NUMBER, COMMENT_ID } = process.env
+ let page = 1
+ let hasFoundComment = false
+ while(true) {
+ const { data: timelines } = await github.rest.issues.listEventsForTimeline({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: ISSUE_NUMBER,
+ page,
+ per_page: 100,
+ })
+ if (timelines.some(v => {
+ hasFoundComment = hasFoundComment || (v.event === 'commented' && `${v.id}` === `${COMMENT_ID}`)
+ return hasFoundComment && v.event === 'committed'
+ })) {
+ throw new Error('The last commit comes after the comment, please comment and package after last commit')
+ }
+ if (timelines.length === 0) {
+ return
+ }
+ page += 1
+ }
+
- name: Setup Node
uses: actions/setup-node@v4
with:
@@ -49,7 +84,7 @@ jobs:
cache: "yarn"
- name: Restore
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
node_modules
@@ -58,7 +93,7 @@ jobs:
- name: Add msbuild to PATH
if: matrix.os == 'windows-2019'
- uses: microsoft/setup-msbuild@v1.3.1
+ uses: microsoft/setup-msbuild@v2
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: "true"
@@ -208,6 +243,9 @@ jobs:
needs: [packaging]
name: Append links to the Pull Request
runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ contents: write
steps:
- name: Comment by push event
if: ${{ github.event_name == 'push' }}
@@ -218,7 +256,7 @@ jobs:
- name: Comment by pull request comment event
if: ${{ github.event_name == 'issue_comment' }}
- uses: peter-evans/create-or-update-comment@v3
+ uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ github.event.comment.id }}
body: |
@@ -230,10 +268,12 @@ jobs:
if: ${{ always() && needs.packaging.result == 'failure' }}
name: Append failed comment to the comment
runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
steps:
- name: Comment by pull request comment event when package failed
if: ${{ github.event_name == 'issue_comment' }}
- uses: peter-evans/create-or-update-comment@v3
+ uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ github.event.comment.id }}
body: Packageing failed in [${{ github.run_id }}](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). @${{ github.event.comment.user.login }}
diff --git a/.github/workflows/spam-comment-detection.yaml b/.github/workflows/spam-comment-detection.yaml
new file mode 100644
index 0000000000..a6d46d855d
--- /dev/null
+++ b/.github/workflows/spam-comment-detection.yaml
@@ -0,0 +1,49 @@
+name: Spam detection
+
+on:
+ issue_comment:
+ types: [created, edited]
+
+permissions:
+ issues: write
+
+jobs:
+ detect-spam:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up Node.js
+ uses: actions/setup-node@main
+
+ - name: Check for Spam
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const comment = process.env.COMMENT_BODY.toLowerCase()
+ const spam_words = process.env.SPAM_WORDS.split(',').map(w => w.toLowerCase())
+ const comment_id = process.env.COMMENT_ID
+ const issue_number = process.env.ISSUE_NUMBER
+ const owner = process.env.REPO_OWNER
+ const repo = process.env.REPO_NAME
+ const EXTERNAL_LINK_REGEXT = /https?:\/\/(?!((\w+\.)?github\.com|github\.com|(\w+\.)?magickbase\.com|(\w+\.)?nervos\.org))/gi
+ if (spam_words.some(w => comment.includes(w))) {
+ console.info(`Spam comment: ${comment}`)
+ github.rest.issues.deleteComment({ owner, repo, comment_id })
+ } else if (EXTERNAL_LINK_REGEXT.test(comment)) {
+ console.info(`External link detected, append an annotation`)
+ github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body: `An external link is mentioned in the comment above. Please verify the link's safety before visiting.`
+ })
+ } else {
+ console.info("No spam detected")
+ }
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COMMENT_BODY: ${{ github.event.comment.body }}
+ COMMENT_ID: ${{ github.event.comment.id }}
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
+ REPO_OWNER: ${{github.repository_owner }}
+ REPO_NAME: ${{ github.event.repository.name }}
+ SPAM_WORDS: ${{ secrets.SPAM_WORDS }}
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 4ca3d472ad..0fb3c58f1d 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
node:
- - 18.12.0
+ - 20.15.1
os:
- macos-latest
- ubuntu-20.04
@@ -35,7 +35,7 @@ jobs:
cache: "yarn"
- name: Restore
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
node_modules
diff --git a/.github/workflows/update_ckb_client_versions.yml b/.github/workflows/update_ckb_client_versions.yml
index 2719acf71d..8a2ba945d9 100644
--- a/.github/workflows/update_ckb_client_versions.yml
+++ b/.github/workflows/update_ckb_client_versions.yml
@@ -8,6 +8,7 @@ jobs:
default:
name: Update CKB client versions
runs-on: ubuntu-latest
+ environment: neuron-bot
permissions:
pull-requests: write # open PR
contents: write # update version files
@@ -19,7 +20,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 20.15.1
- name: Update versions
id: update_versions
@@ -36,7 +37,7 @@ jobs:
git_commit_gpgsign: true
- name: Open PR to develop branch
- uses: peter-evans/create-pull-request@v5
+ uses: peter-evans/create-pull-request@v6
with:
title: Update ckb client versions
commit-message: 'feat: update ckb client versions'
diff --git a/.github/workflows/update_neuron_compatible.yml b/.github/workflows/update_neuron_compatible.yml
index ac4764a899..2f2e1d5d71 100644
--- a/.github/workflows/update_neuron_compatible.yml
+++ b/.github/workflows/update_neuron_compatible.yml
@@ -11,6 +11,7 @@ jobs:
update-neuron-compatible:
name: Update Neuron compatibility table
runs-on: ubuntu-latest
+ environment: neuron-bot
permissions:
pull-requests: write # open PR
contents: write # update version files
@@ -22,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version: 20.15.1
- name: Update versions
id: update_versions
@@ -39,7 +40,7 @@ jobs:
git_commit_gpgsign: true
- name: Open PR to RC branch
- uses: peter-evans/create-pull-request@v5
+ uses: peter-evans/create-pull-request@v6
with:
title: Update Neuron compatibility table
commit-message: 'feat: Update Neuron compatibility table'
diff --git a/.github/workflows/update_wallet_env.yml b/.github/workflows/update_wallet_env.yml
index bda340a1ba..62fd3985fe 100644
--- a/.github/workflows/update_wallet_env.yml
+++ b/.github/workflows/update_wallet_env.yml
@@ -7,25 +7,29 @@ jobs:
ready-for-release:
name: Update ckb node assume valid target
runs-on: ubuntu-latest
+ environment: neuron-bot
+ permissions:
+ pull-requests: write # open PR
+ contents: write # update version files
if: ${{ startsWith(github.ref_name, 'rc/') }}
steps:
- name: Create Branch
- uses: peterjgrainger/action-create-branch@v2.4.0
+ uses: peterjgrainger/action-create-branch@v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
branch: 'chore-update-wallet-env/${{github.ref_name}}'
- sha: '${{ github.event.create.head.sha }}'
+ sha: '${{ github.sha }}'
- name: Checkout
uses: actions/checkout@v4
with:
ref: 'chore-update-wallet-env/${{github.ref_name}}'
-
+
- name: Setup Node
uses: actions/setup-node@v4
with:
- node-version: 18.12.0
+ node-version: 20.15.1
- name: Write env file
run: |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd475928af..5dc17f3ad4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,328 @@
+# 0.121.1 (2025-02-13)
+
+### CKB Node & Light Client
+
+- [CKB@v0.121.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.121.0) was released on Jan. 23rd, 2025. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x53dd03a0420b68e082d660cb2e86f167aa8f1bd95c637228285bf36f5caa9e1d`(at height `15,361,857`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3310)
+
+---
+
+## New features
+
+- #3291: Support complex transaction with multisig account.(@devchenyan)
+- #3298: Support Nervos DAO with multisig account.(@devchenyan)
+
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.121.0...v0.121.1
+
+
+# 0.121.0 (2025-01-16)
+
+### CKB Node & Light Client
+
+- [CKB@v0.121.0-rc1](https://github.com/nervosnetwork/ckb/releases/tag/v0.121.0-rc1) was released on Jan. 9th, 2025. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x1381f9e4f70ce521256c4095fa536d11165488171a8a2cbac687f8cf53907afa`(at height `15,119,157`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3300)
+
+---
+
+## New features
+
+- #3290: Support inspecting and exporting private key for specific addresses.(@devchenyan)
+- #3293: Refine interaction of Sign and Verify Message.(@devchenyan)
+
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.120.0...v0.121.0
+
+
+# 0.120.0 (2024-12-13)
+
+### CKB Node & Light Client
+
+- [CKB@v0.120.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.120.0) was released on Dec. 12th, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0xe1085c7ce8f4e8461ea75afe63ef21d2c1ce6a5d0bf0f0170042bebdd2fbde04`(at height `14,817,366`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3282)
+
+---
+
+## New features
+
+- #3271: Support Arabic, FrCanadian/Belgian in User Interface.(@Natixe)
+
+## New Contributors
+
+- @Natixe made their first contribution in https://github.com/nervosnetwork/neuron/pull/3271
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.119.0...v0.120.0
+
+
+# 0.119.0 (2024-12-02)
+
+### CKB Node & Light Client
+
+- [CKB@v0.119.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.119.0) was released on Oct. 25th, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.4.1](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.4.1) was released on Nov. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x7488acf2280ebf5b83c805a517f766eab77f45cd51f61476811d1ce96a60ea71`(at height `14,687,217`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3268)
+
+---
+
+## Bug fixes
+
+- #3239: Fix abnormal display of remaining time of Nervos DAO.(@devchenyan)
+- #3246: Use the median fee rate instead of the average as a more appropriate reference for fee rate.(@yanguoyu)
+
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.117.0...v0.119.0
+
+
+# 0.117.0 (2024-08-12)
+
+### CKB Node & Light Client
+
+- [CKB@v0.117.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.117.0) was released on Jul. 29th, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.7](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.7) was released on Apr. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0xca44ae8f7bc12ba8eab3224cbe3156c913e2284693e36dc1d01e4d30f362f3c2`(at height `13,705,152`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3227)
+
+---
+
+[](https://youtu.be/zf78Y094m60)
+
+YouTube: https://youtu.be/zf78Y094m60
+
+---
+
+## New features
+
+- #3206: Support XUDT asset management.(@yanguoyu)
+- #3207: Support connecting to an external light client.(@devchenyan)
+- #3167: Support cells consolidation.(@devchenyan)
+- #3199: Validate pending transactions periodically.(@devchenyan)
+- #3200: Optimize the process of generating a wallet.(@devchenyan)
+- #3176: Support setting start block numbers of multisig addresses.(@yanguoyu)
+- #3160: Optimize synchronization in light client mode for multiple wallets.(@yanguoyu)
+- #3169: Be compatible with multisig transaction JSON file exported from CKB CLI.(@devchenyan)
+- #3197: Support resetting pin code for window lock.(@yanguoyu)
+- #3194: Add a tip for multisig addresses.(@yanguoyu)
+
+## Bug fixes
+
+- #3195: Fix the synchronization status check.(@yanguoyu)
+
+## New Contributors
+
+- @tcpdumppy made their first contribution in https://github.com/nervosnetwork/neuron/pull/3182
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.116.2...v0.117.0
+
+# 0.116.2 (2024-05-29)
+
+### CKB Node & Light Client
+
+- [CKB@v0.116.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.116.1) was released on May. 11st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.7](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.7) was released on Apr. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x6dd077b407d019a0bce0cbad8c34e69a524ae4b2599b9feda2c7491f3559d32c`(at height `13,007,704`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3157)
+
+---
+
+## Bug fixes
+
+- 3179: Remove the display of the ledger firmware version because it causes the Nervos app to crash on the ledger device.(@yanguoyu)
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.116.1...v0.116.2
+
+# 0.116.1 (2024-05-28)
+
+### CKB Node & Light Client
+
+- [CKB@v0.116.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.116.1) was released on May. 11st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.7](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.7) was released on Apr. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x6dd077b407d019a0bce0cbad8c34e69a524ae4b2599b9feda2c7491f3559d32c`(at height `13,007,704`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3157)
+
+---
+
+## Bug fixes
+
+- 3173: Fix importing an account from a hardware wallet.(@yanguoyu)
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.116.0...v0.116.1
+
+# 0.116.0 (2024-05-24)
+
+### CKB Node & Light Client
+
+- [CKB@v0.116.1](https://github.com/nervosnetwork/ckb/releases/tag/v0.116.1) was released on May. 11st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.7](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.7) was released on Apr. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x6dd077b407d019a0bce0cbad8c34e69a524ae4b2599b9feda2c7491f3559d32c`(at height `13,007,704`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3157)
+
+---
+
+[](https://youtu.be/QXv8by2C8zU)
+
+YouTube: https://youtu.be/QXv8by2C8zU
+
+---
+
+## New features
+
+- #3134: Support 'replace-by-fee' nervos dao transactions and sudt transactions.(@devchenyan)
+- #3144: Reduce size of light client log in debug information and reveal start-block-number in log.(@yanguoyu)
+- #3064: Support locking window by pin code.(@yanguoyu)
+- #3131: Add detailed result for nervos dao transaction.(@devchenyan)
+
+## Bug fixes
+
+- #3121: Locate the first transaction on Explorer directly when users want to set the start-block-number for light client.(@yanguoyu)
+- #3101: Show migration instruction properly.(@devchenyan)
+- #3062: Migrate legacy ACP to active ACP account(@yanguoyu)
+- #3141: Fix some issues about light client synchronizaiton.(@yanguoyu)
+- #3120: Remove all sync data when start-block-number is set less than before.(@yanguoyu)
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.114.3...v0.116.0
+
+# 0.114.3 (2024-04-16)
+
+### CKB Node & Light Client
+
+- [CKB@v0.115.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.115.0) was released on Apr. 1st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.7](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.7) was released on Apr. 13th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x9443ad8da9172d484367bc5467988cba7a0c46028398309edfdda7d2d79be897`(at height `12,703,957`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3123)
+
+---
+
+## New features
+
+- #3054: Displaying DAO rewards.(@devchenyan)
+- #3066: Support keeping screen awake.(@yanguoyu)
+
+## Bug fixes
+
+- #3055: Fix sending sudt to a new acp cell with extra 142 CKB by offline sign.(@yanguoyu)
+- #3103: Handle MacOS crash properly on quitting.(@devchenyan)
+
+## New Contributors
+
+- @twhy made their first contribution in https://github.com/nervosnetwork/neuron/pull/3110
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.114.2...v0.114.3
+
+# 0.114.2 (2024-03-15)
+
+### CKB Node & Light Client
+
+- [CKB@v0.114.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.114.0) was released on Feb. 29st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.6](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.6) was released on Feb. 8th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x397d7d41167051cab2bf1610e334ad3aa5cf612e2cd442f71b91422e0361141e`(at height `12,450,084`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3077)
+
+---
+
+Good day, good Neuron, packed with exciting new features and enhancements to elevate your transaction experience!
+
+With support for Cell Management, Chained Transactions, and detailed viewing options, managing your transactions has never been easier.
+
+We're also thrilled to announce multilingual support, with French and Spanish interfaces now available, thanks to the contributions of our dedicated community members.
+
+Plus, enjoy enhanced guidance features like remaining time estimation and the ability to amend pending transactions for added flexibility.
+
+With optimizations to settings and transaction overviews, Neuron continues to prioritize efficiency and user satisfaction.
+
+Upgrade now and experience seamless transaction management with Neuron.
+
+[](https://youtu.be/df29-EUZG0Y)
+
+---
+
+## Bug fixes
+
+- #3081: fix start-sync confirmation in light client mode.(@yanguoyu)
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.114.0...v0.114.2
+
+# 0.114.0 (2024-03-15)
+
+### CKB Node & Light Client
+
+- [CKB@v0.114.0](https://github.com/nervosnetwork/ckb/releases/tag/v0.114.0) was released on Feb. 29st, 2024. This version of CKB node is now bundled and preconfigured in Neuron.
+- [CKB Light Client@v0.3.6](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.3.6) was released on Feb. 8th, 2024. This version of CKB Light Client is now bundled and preconfigured in Neuron
+
+### Assumed valid target
+
+Block before `0x397d7d41167051cab2bf1610e334ad3aa5cf612e2cd442f71b91422e0361141e`(at height `12,450,084`) will be skipped in validation.(https://github.com/nervosnetwork/neuron/pull/3077)
+
+---
+
+Good day, good Neuron, packed with exciting new features and enhancements to elevate your transaction experience!
+
+With support for Cell Management, Chained Transactions, and detailed viewing options, managing your transactions has never been easier.
+
+We're also thrilled to announce multilingual support, with French and Spanish interfaces now available, thanks to the contributions of our dedicated community members.
+
+Plus, enjoy enhanced guidance features like remaining time estimation and the ability to amend pending transactions for added flexibility.
+
+With optimizations to settings and transaction overviews, Neuron continues to prioritize efficiency and user satisfaction.
+
+Upgrade now and experience seamless transaction management with Neuron.
+
+[](https://youtu.be/df29-EUZG0Y)
+
+---
+
+## New features
+
+- #2859: Support Cell Management.(@yanguoyu)
+- #2963: Support chained transactions.(@yanguoyu)
+- #2986: Support viewing chained transaction detail.(@yanguoyu)
+- #3012: Support French in User Interface.(Special thanks to @Natixe)
+- #3024: Support Spanish in User Interface.(Special thanks to @Natixe)
+- #3014: Support remaining time estimation.(@yanguoyu)
+- #3045: Support "amend a pending transaction".(@devchenyan)
+- #3005: Show tip for first sync and show warning when disk is not enough for full-node mode.(@yanguoyu)
+- #2994: Optimize setting of light client.(@yanguoyu)
+- #3040: Optimize transaction overview.(@yanguoyu)
+
+## Bug fixes
+
+- #2951: Fix error on creating multisig address.(@devchenyan)
+- #2992: Fix transaction lost in light client mode.(@yanguoyu)
+- #3010: Fix storage of multisig configuration.(@yanguoyu)
+- #3020: Disable nervos dao unlock button when balance is not enough as transaction fee.(@yanguoyu)
+
+## New Contributors
+
+- @Natixe made their first contribution in https://github.com/nervosnetwork/neuron/pull/3012
+
+**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.112.0...v0.114.0
+
# 0.112.0 (2023-12-07)
### CKB Node & Light Client
@@ -25,10 +350,8 @@ Block before `0x1d46fe5bb62d19a004eadd7ba40564c45620905ab260d8a546a9e4b9d7cc0f85
- #2944: Fix requests jam when a synced transaction includes cellbase cells.(@yanguoyu)
- #2965: Fix transaction type recognition on light client mode.(@yanguoyu)
-
**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.111.1...v0.112.0
-
# 0.111.1 (2023-11-08)
### CKB Node & Light Client
@@ -57,10 +380,8 @@ Block before `0x79cecdd6f41361e2474290224751284312a018528d1d92f4e18dd6d542feddfe
- #2913: Use KeyLocker to sign Neuron for Windows, conforming to the new industry standards effective since June 1, 2023.(@keith-cy)
- #2921: Add network option of "Light Client(Mainnet)", and reserve "Internal Node" for built-in CKB Node only.(@yanguoyu)
-
**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.111.0...v0.111.1
-
# 0.111.0 (2023-10-19)
### CKB Node & Light Client
@@ -84,10 +405,8 @@ Block before `0xd5e25ad24400f237aa5f72f3738a9ae77fe082a89937e75143fcc8ef5b009383
- #2870: Fix width of navbar.(@yanguoyu)
- #2873: Fix fallback font on Linux.(@yanguoyu)
-
**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.110.3...v0.111.0
-
# 0.110.3 (2023-10-11)
### CKB Node & Light Client
@@ -138,7 +457,6 @@ Curious to see the magic of the new Neuron in action? Checkout our demo video on
**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.110.2...v0.110.3
-
# 0.110.2 (2023-07-07)
### CKB Node & Light Client
@@ -161,10 +479,8 @@ Block before `0xc0c532e10c708852d9586be46a5ed8317b2aa0835c721aa691abffb9bf4a26cd
- #2760: Avoid generating deposit DAO when the dialog is not visible.(@yanguoyu)
-
**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.110.1...v0.110.2
-
# 0.110.1 (2023-05-31)
### CKB Node & Light Client
diff --git a/README.md b/README.md
index 8732315577..e953803473 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,18 @@ $ yarn start:ui
$ yarn start:wallet
```
+If you want to develop with devnet, replace the testnet script with the dev script in the `packages/neuron-wallet/.env` file.
+
+Here is a script env example, it is constructed by network、script name and filed name.
+
+```
+TESTNET_SUDT_DEP_TXHASH=0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769
+TESTNET_SUDT_DEP_INDEX=0
+TESTNET_SUDT_DEP_TYPE=code
+TESTNET_SUDT_SCRIPT_CODEHASH=0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4
+TESTNET_SUDT_SCRIPT_HASHTYPE=type
+```
+
### Test
```shell
diff --git a/_typos.toml b/_typos.toml
index ae82742a0c..7a852c75b4 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -1,6 +1,9 @@
[default.extend-words]
thur = "thur"
numer = "numer"
+HD = "HD"
+hd = "hd"
+passin = 'passin'
# defined in database schema
lastest = "lastest"
diff --git a/compatible.json b/compatible.json
index a28669ae51..8bae07b50f 100644
--- a/compatible.json
+++ b/compatible.json
@@ -1,5 +1,13 @@
{
"fullVersions": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
"0.113",
"0.112",
"0.111",
@@ -13,12 +21,21 @@
"0.103"
],
"lightVersions": [
+ "0.4",
"0.3",
"0.2"
],
"compatible": {
"0.111": {
"full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
"0.113",
"0.112",
"0.111",
@@ -26,12 +43,21 @@
"0.109"
],
"light": [
+ "0.4",
"0.3",
"0.2"
]
},
"0.110": {
"full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
"0.113",
"0.112",
"0.111",
@@ -39,6 +65,7 @@
"0.109"
],
"light": [
+ "0.4",
"0.3",
"0.2"
]
@@ -61,6 +88,14 @@
},
"0.112": {
"full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
"0.113",
"0.112",
"0.111",
@@ -68,6 +103,139 @@
"0.109"
],
"light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.114": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.116": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.117": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.119": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.120": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
+ "0.3",
+ "0.2"
+ ]
+ },
+ "0.121": {
+ "full": [
+ "0.121",
+ "0.120",
+ "0.119",
+ "0.118",
+ "0.117",
+ "0.116",
+ "0.115",
+ "0.114",
+ "0.113",
+ "0.112",
+ "0.111",
+ "0.110",
+ "0.109"
+ ],
+ "light": [
+ "0.4",
"0.3",
"0.2"
]
diff --git a/lerna.json b/lerna.json
index 8fd1da0d48..27ee37d2b0 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,8 @@
{
- "packages": ["packages/*"],
- "version": "0.112.0",
+ "packages": [
+ "packages/*"
+ ],
+ "version": "0.121.1",
"npmClient": "yarn",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
diff --git a/package.json b/package.json
index 1318cbb1a7..f4dc78585b 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "neuron",
"productName": "Neuron",
"description": "CKB Neuron Wallet",
- "version": "0.112.0",
+ "version": "0.121.1",
"private": true,
"author": {
"name": "Nervos Core Dev",
@@ -53,18 +53,30 @@
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "4.2.1",
"husky": "8.0.3",
- "lerna": "8.0.1",
- "lint-staged": "15.2.0",
+ "jest": "27.5.1",
+ "lerna": "8.1.5",
+ "lint-staged": "15.2.7",
"ncp": "2.0.0",
"prettier": "2.8.8",
- "ts-jest": "29.1.1",
+ "ts-jest": "29.2.0",
+ "ts-node": "^10.9.2",
"typescript": "5.3.3",
"wait-on": "7.0.1"
},
"dependencies": {},
"resolutions": {
"@types/react": "18.2.45",
- "react-i18next": ">=11.16.4"
+ "react-i18next": ">=11.16.4",
+ "react-refresh": "0.14.0",
+ "node-fetch": "2.6.13",
+ "@ckb-lumos/ckb-indexer": "0.23.0",
+ "@ckb-lumos/base": "0.23.0",
+ "@ckb-lumos/bi": "0.23.0",
+ "@ckb-lumos/codec": "0.23.0",
+ "@ckb-lumos/common-scripts": "0.23.0",
+ "@ckb-lumos/config-manager": "0.23.0",
+ "@ckb-lumos/lumos": "0.23.0",
+ "@ckb-lumos/rpc": "0.23.0"
},
"volta": {
"node": "20.10.0"
diff --git a/packages/neuron-ui/.eslintrc.js b/packages/neuron-ui/.eslintrc.js
index b83c885ab3..c2b6beada2 100644
--- a/packages/neuron-ui/.eslintrc.js
+++ b/packages/neuron-ui/.eslintrc.js
@@ -65,7 +65,7 @@ module.exports = {
'import/no-extraneous-dependencies': [
'error',
{
- devDependencies: ['**/*.stories.*', 'src/setupTests.ts', '**/*.test.ts'],
+ devDependencies: ['**/*.stories.*', 'src/setupTests.ts', '**/*.test.ts', './vitest-setup.js'],
},
],
'import/extensions': [
@@ -96,5 +96,7 @@ module.exports = {
allowExpressions: true,
},
],
+ '@typescript-eslint/ban-types': 'off',
+ 'import/no-unresolved': [2, { ignore: ['.svg'] }],
},
}
diff --git a/packages/neuron-ui/.storybook/electron.js b/packages/neuron-ui/.storybook/electron.js
index 492a24c539..18164a437d 100644
--- a/packages/neuron-ui/.storybook/electron.js
+++ b/packages/neuron-ui/.storybook/electron.js
@@ -1,21 +1,19 @@
const sendSyncValues = {
- 'get-locale': ('zh', 'fr', 'es'),
+ 'get-locale': ('zh', 'fr', 'es', 'ar'),
'get-version': '0.103.1',
}
-module.exports = {
- ipcRenderer: {
- sendSync(key) {
- return sendSyncValues[key]
- },
- invoke() {
- return Promise.resolve({})
- },
- on() {},
- removeAllListeners() {},
+export const ipcRenderer = {
+ sendSync(key) {
+ return sendSyncValues[key]
},
- clipboard() {},
- nativeImage() {},
- shell() {},
- desktopCapturer() {},
+ invoke() {
+ return Promise.resolve({})
+ },
+ on() {},
+ removeAllListeners() {},
}
+export const clipboard = () => {}
+export const nativeImage = () => {}
+export const shell = () => {}
+export const desktopCapturer = () => {}
diff --git a/packages/neuron-ui/.storybook/main.ts b/packages/neuron-ui/.storybook/main.ts
index 3cf0dbf366..863f6ebf84 100644
--- a/packages/neuron-ui/.storybook/main.ts
+++ b/packages/neuron-ui/.storybook/main.ts
@@ -1,24 +1,30 @@
-export default {
- stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+import { StorybookConfig } from '@storybook/react-vite'
+const { mergeConfig } = require('vite')
+import path from 'path'
+
+const config: StorybookConfig = {
+ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
- '@storybook/preset-create-react-app',
'storybook-addon-react-router-v6',
],
- framework: {
- name: '@storybook/react-webpack5',
- options: {},
+ framework: '@storybook/react-vite',
+ core: {
+ builder: '@storybook/builder-vite',
},
- webpackFinal: config => {
- config.resolve.alias = {
- ...config.resolve.alias,
- electron: require.resolve('./electron'),
- }
- return config
+ async viteFinal(config) {
+ return mergeConfig(config, {
+ resolve: {
+ alias: { electron: path.resolve(__dirname, '../.storybook/electron') },
+ },
+ })
},
docs: {
autodocs: true,
},
+ staticDirs: ['../public'],
}
+
+export default config
diff --git a/packages/neuron-ui/config-overrides.js b/packages/neuron-ui/config-overrides.js
index aa4d60f53b..a4f768b8ac 100644
--- a/packages/neuron-ui/config-overrides.js
+++ b/packages/neuron-ui/config-overrides.js
@@ -4,5 +4,24 @@ const path = require('path')
module.exports = function override(config) {
const webpackConfig = { ...config }
webpackConfig.resolve.alias.electron = path.join(__dirname, 'src/electron-modules')
+ webpackConfig.resolve.fallback = {
+ fs: false,
+ crypto: false,
+ buffer: false,
+ }
+ /* disable autoprefixer */
+ webpackConfig.module.rules
+ .find(r => r.oneOf)
+ .oneOf.forEach(rule => {
+ if (rule.use) {
+ rule.use.forEach(loader => {
+ if (loader.options && loader.options.postcssOptions) {
+ loader.options.postcssOptions.plugins = loader.options.postcssOptions.plugins.filter(
+ plugin => !plugin.toString().includes('autoprefixer')
+ )
+ }
+ })
+ }
+ })
return webpackConfig
}
diff --git a/packages/neuron-ui/index.html b/packages/neuron-ui/index.html
new file mode 100644
index 0000000000..8108b98d45
--- /dev/null
+++ b/packages/neuron-ui/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+ Neuron
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json
index 512984e473..813bfa0f17 100644
--- a/packages/neuron-ui/package.json
+++ b/packages/neuron-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "neuron-ui",
- "version": "0.112.0",
+ "version": "0.121.1",
"private": true,
"author": {
"name": "Nervos Core Dev",
@@ -15,14 +15,14 @@
"main": "./build",
"license": "MIT",
"scripts": {
- "start": "cross-env DISABLE_ESLINT_PLUGIN=true GENERATE_SOURCEMAP=false react-app-rewired start",
+ "start": "cross-env DISABLE_ESLINT_PLUGIN=true vite",
"lint": "eslint --fix --ext .tsx,.ts,.js src",
- "test": "react-app-rewired test --env=jsdom --watchAll=false",
- "build": "cross-env DISABLE_ESLINT_PLUGIN=true GENERATE_SOURCEMAP=false react-app-rewired build",
+ "test": "vitest --environment=jsdom --run --watch=false",
+ "build": "cross-env DISABLE_ESLINT_PLUGIN=true tsc && vite build",
"clean": "npx rimraf build",
"precommit": "lint-staged",
- "storybook": "storybook dev -p 9009 -s public",
- "build-storybook": "storybook build -s public"
+ "storybook": "storybook dev -p 9009",
+ "build-storybook": "storybook build"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/"
@@ -46,14 +46,16 @@
"jest": {
"displayName": "UI Tests"
},
- "browserslist": [
- "last 2 chrome versions"
- ],
"dependencies": {
- "@ckb-lumos/base": "0.21.1",
- "@ckb-lumos/codec": "0.21.1",
- "@nervosnetwork/ckb-sdk-core": "0.109.0",
- "@nervosnetwork/ckb-sdk-utils": "0.109.0",
+ "@ckb-lumos/base": "0.23.0",
+ "@ckb-lumos/bi": "0.23.0",
+ "@ckb-lumos/codec": "0.23.0",
+ "@ckb-lumos/common-scripts": "0.23.0",
+ "@ckb-lumos/config-manager": "0.23.0",
+ "@ckb-lumos/hd": "0.23.0",
+ "@ckb-lumos/helpers": "0.23.0",
+ "@ckb-lumos/lumos": "0.23.0",
+ "@ckb-lumos/rpc": "0.23.0",
"canvg": "2.0.0",
"i18next": "23.7.11",
"immer": "9.0.21",
@@ -66,21 +68,23 @@
"react-router-dom": "6.21.1",
"react-transition-group": "4.4.5",
"sass": "1.69.5",
- "tslib": "2.6.2"
+ "tslib": "2.6.3"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
+ "@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.23.3",
- "@jest/globals": "27.5.1",
- "@storybook/addon-actions": "7.6.6",
- "@storybook/addon-essentials": "7.6.6",
- "@storybook/addon-interactions": "7.6.6",
- "@storybook/addon-links": "7.6.6",
+ "@storybook/addon-actions": "8.6.4",
+ "@storybook/addon-essentials": "8.6.4",
+ "@storybook/addon-interactions": "8.6.4",
+ "@storybook/addon-links": "8.6.4",
+ "@storybook/builder-vite": "8.6.4",
"@storybook/node-logger": "7.6.6",
- "@storybook/preset-create-react-app": "7.6.6",
- "@storybook/react": "7.6.6",
- "@storybook/react-webpack5": "7.6.6",
+ "@storybook/react": "8.6.4",
+ "@storybook/react-vite": "8.6.4",
"@storybook/testing-library": "0.2.2",
+ "@testing-library/jest-dom": "6.6.3",
+ "@testing-library/react": "16.2.0",
"@types/enzyme": "3.10.18",
"@types/enzyme-adapter-react-16": "1.0.9",
"@types/node": "20.10.5",
@@ -88,22 +92,28 @@
"@types/react-dom": "18.2.18",
"@types/react-router-dom": "5.3.3",
"@types/styled-components": "5.1.34",
+ "@vitejs/plugin-react": "4.3.4",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"babel-jest": "25.5.1",
- "electron": "28.1.0",
+ "electron": "30.0.0",
"enzyme": "3.11.0",
- "enzyme-adapter-react-16": "1.15.7",
+ "enzyme-adapter-react-16": "1.15.8",
"eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"jest-styled-components": "7.2.0",
- "react-app-rewired": "2.2.1",
- "react-scripts": "5.0.1",
- "react-test-renderer": "18.2.0",
- "storybook": "7.6.6",
+ "jsdom": "26.0.0",
+ "postcss-preset-env": "10.1.5",
+ "storybook": "8.6.4",
"storybook-addon-react-router-v6": "2.0.10",
- "webpack": "5.89.0"
+ "terser": "5.39.0",
+ "vite": "6.2.1",
+ "vite-plugin-commonjs": "0.10.4",
+ "vite-plugin-eslint": "1.8.1",
+ "vite-plugin-node-polyfills": "0.23.0",
+ "vite-plugin-svgr": "4.3.0",
+ "vitest": "3.0.8"
},
"resolutions": {
"react-i18next": "14.0.0"
diff --git a/packages/neuron-ui/public/index.html b/packages/neuron-ui/public/index.html
deleted file mode 100755
index bbfb71d586..0000000000
--- a/packages/neuron-ui/public/index.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
- Neuron
-
-
-
-
-
-
-
-
-
diff --git a/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss
index 118ab0d4b2..325e953d3e 100644
--- a/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss
+++ b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss
@@ -168,6 +168,20 @@
}
}
+.privateKey {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ &:hover {
+ svg {
+ g,
+ path {
+ stroke: var(--primary-color);
+ }
+ }
+ }
+}
+
@media screen and (max-width: 1330px) {
.container {
.balance {
diff --git a/packages/neuron-ui/src/components/AddressBook/index.tsx b/packages/neuron-ui/src/components/AddressBook/index.tsx
index 5bcd15bd13..947c8675cb 100644
--- a/packages/neuron-ui/src/components/AddressBook/index.tsx
+++ b/packages/neuron-ui/src/components/AddressBook/index.tsx
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'
import { useState as useGlobalState, useDispatch } from 'states'
import Dialog from 'widgets/Dialog'
import CopyZone from 'widgets/CopyZone'
-import { Copy } from 'widgets/Icons/icon'
+import ViewPrivateKey from 'components/ViewPrivateKey'
+import { Copy, PrivateKey } from 'widgets/Icons/icon'
import Table, { TableProps, SortType } from 'widgets/Table'
import { shannonToCKBFormatter, useLocalDescription } from 'utils'
import { HIDE_BALANCE } from 'utils/const'
@@ -44,6 +45,7 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
const dispatch = useDispatch()
const { onChangeEditStatus, onSubmitDescription } = useLocalDescription('address', walletId, dispatch)
+ const [viewPrivateKeyAddress, setViewPrivateKeyAddress] = useState('')
const columns = useMemo['columns']>(
() => [
@@ -118,7 +120,7 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
return `${HIDE_BALANCE} CKB`
}
return (
-
+
{`${shannonToCKBFormatter(balance)} CKB`}
)
@@ -149,6 +151,21 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
return 0
},
},
+ {
+ title: '',
+ dataIndex: 'key',
+ align: 'left',
+ width: '40px',
+ render(_, __, { address }) {
+ return (
+
+
+
+ )
+ },
+ },
],
[t]
)
@@ -179,6 +196,10 @@ const AddressBook = ({ onClose }: { onClose?: () => void }) => {
}
/>
+
+ {!!viewPrivateKeyAddress && (
+ setViewPrivateKeyAddress('')} />
+ )}
)
diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss
new file mode 100644
index 0000000000..15c6aeef62
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/amendPendingTransactionDialog.module.scss
@@ -0,0 +1,5 @@
+@import '../../styles/mixin.scss';
+
+.content {
+ width: 680px;
+}
diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts
new file mode 100644
index 0000000000..dfb4afc26a
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/hooks.ts
@@ -0,0 +1,234 @@
+import React, { useState, useCallback, useEffect, useMemo } from 'react'
+import { PasswordIncorrectException } from 'exceptions'
+import { TFunction } from 'i18next'
+import { getTransaction as getOnChainTransaction } from 'services/chain'
+import { getTransaction as getSentTransaction, sendTx, invokeShowErrorMessage } from 'services/remote'
+import { isSuccessResponse, ErrorCode, shannonToCKBFormatter, scriptToAddress } from 'utils'
+import { FEE_RATIO } from 'utils/const'
+
+export const useInitialize = ({
+ tx,
+ walletID,
+ t,
+ onClose,
+}: {
+ tx: State.Transaction
+ walletID: string
+ t: TFunction
+ onClose: () => void
+}) => {
+ const [transaction, setTransaction] = useState(null)
+ const [generatedTx, setGeneratedTx] = useState(null)
+ const [size, setSize] = useState(0)
+ const [minPrice, setMinPrice] = useState('0')
+ const [price, setPrice] = useState('0')
+ const [password, setPassword] = useState('')
+ const [pwdError, setPwdError] = useState('')
+ const [isSending, setIsSending] = useState(false)
+
+ const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false)
+
+ const fee = useMemo(() => {
+ const ratio = BigInt(FEE_RATIO)
+ const base = BigInt(size) * BigInt(price)
+ const curFee = base / ratio
+ if (curFee * ratio < base) {
+ return curFee + BigInt(1)
+ }
+ return curFee
+ }, [price, size])
+
+ const fetchInitData = useCallback(async () => {
+ const res = await getOnChainTransaction(tx.hash)
+ const {
+ minReplaceFee,
+ transaction: { outputsData },
+ } = res
+
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ }
+
+ const txRes = await getSentTransaction({ hash: tx.hash, walletID })
+
+ if (isSuccessResponse(txRes)) {
+ const txResult = txRes.result
+ setTransaction({
+ ...txResult,
+ outputsData,
+ })
+
+ setSize(txResult.size)
+ if (minReplaceFee) {
+ const mPrice = ((BigInt(minReplaceFee) * BigInt(FEE_RATIO)) / BigInt(txResult.size)).toString()
+ setMinPrice(mPrice)
+ setPrice(mPrice)
+ }
+ }
+ }, [tx, setIsConfirmedAlertShown, setPrice, setTransaction, setSize, setMinPrice])
+
+ useEffect(() => {
+ fetchInitData()
+ }, [])
+
+ const onPwdChange = useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value } = e.target as HTMLInputElement
+ setPassword(value)
+ setPwdError('')
+ },
+ [setPassword, setPwdError]
+ )
+
+ const onSubmit = useCallback(async () => {
+ try {
+ const { minReplaceFee } = await getOnChainTransaction(tx.hash)
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ return
+ }
+
+ if (!generatedTx) {
+ return
+ }
+ setIsSending(true)
+
+ try {
+ const skipLastInputs = generatedTx.inputs.length > generatedTx.witnesses.length
+
+ const res = await sendTx({ walletID, tx: generatedTx, password, skipLastInputs, amendHash: tx.hash })
+
+ if (isSuccessResponse(res)) {
+ onClose()
+ } else if (res.status === ErrorCode.PasswordIncorrect) {
+ setPwdError(t(new PasswordIncorrectException().message))
+ } else {
+ invokeShowErrorMessage({
+ title: t('messages.error'),
+ content: typeof res.message === 'string' ? res.message : res.message.content!,
+ })
+ }
+ } catch (err) {
+ console.warn(err)
+ } finally {
+ setIsSending(false)
+ }
+ } catch {
+ // ignore
+ }
+ }, [walletID, tx, setIsConfirmedAlertShown, setPwdError, password, generatedTx, setIsSending])
+
+ return {
+ fee,
+ price,
+ setPrice,
+ generatedTx,
+ setGeneratedTx,
+ transaction,
+ setTransaction,
+ minPrice,
+ isConfirmedAlertShown,
+ onSubmit,
+ password,
+ onPwdChange,
+ pwdError,
+ isSending,
+ setIsSending,
+ }
+}
+
+export const useOutputs = ({
+ transaction,
+ isMainnet,
+ addresses,
+ sUDTAccounts,
+ fee,
+}: {
+ transaction: State.GeneratedTx | null
+ isMainnet: boolean
+ addresses: State.Address[]
+ sUDTAccounts: State.SUDTAccount[]
+ fee: bigint
+}) => {
+ const getLastOutputAddress = (outputs: State.DetailedOutput[]) => {
+ if (outputs.length === 1) {
+ return scriptToAddress(outputs[0].lock, { isMainnet })
+ }
+
+ const change = outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ return !!addresses.find(item => item.address === address && item.type === 1)
+ })
+
+ if (change) {
+ return scriptToAddress(change.lock, { isMainnet })
+ }
+
+ const receive = outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ return !!addresses.find(item => item.address === address && item.type === 0)
+ })
+ if (receive) {
+ return scriptToAddress(receive.lock, { isMainnet })
+ }
+
+ const sudt = outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ return !!sUDTAccounts.find(item => item.address === address)
+ })
+ if (sudt) {
+ return scriptToAddress(sudt.lock, { isMainnet })
+ }
+ return ''
+ }
+
+ const items: {
+ address: string
+ amount: string
+ capacity: string
+ isLastOutput: boolean
+ output: State.DetailedOutput
+ }[] = useMemo(() => {
+ if (transaction && transaction.outputs.length) {
+ const lastOutputAddress = getLastOutputAddress(transaction.outputs)
+ return transaction.outputs.map(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ return {
+ capacity: output.capacity,
+ address,
+ output,
+ amount: shannonToCKBFormatter(output.capacity || '0'),
+ isLastOutput: address === lastOutputAddress,
+ }
+ })
+ }
+ return []
+ }, [transaction?.outputs])
+
+ const outputsCapacity = useMemo(() => {
+ const outputList = items.filter(item => !item.isLastOutput)
+ return outputList.reduce((total, cur) => {
+ return total + BigInt(cur.capacity || '0')
+ }, BigInt(0))
+ }, [items])
+
+ const lastOutputsCapacity = useMemo(() => {
+ if (transaction) {
+ const inputsCapacity = transaction.inputs.reduce((total, cur) => {
+ return total + BigInt(cur.capacity || '0')
+ }, BigInt(0))
+
+ return inputsCapacity - outputsCapacity - fee
+ }
+ return undefined
+ }, [transaction, fee, outputsCapacity])
+
+ return {
+ items,
+ lastOutputsCapacity,
+ }
+}
+
+export default {
+ useInitialize,
+}
diff --git a/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx
new file mode 100644
index 0000000000..49a52289b7
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendPendingTransactionDialog/index.tsx
@@ -0,0 +1,157 @@
+import React, { useEffect, useMemo, useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useState as useGlobalState } from 'states'
+import TextField from 'widgets/TextField'
+import Dialog from 'widgets/Dialog'
+import { MIN_AMOUNT, DAO_DATA } from 'utils/const'
+import { isMainnet as isMainnetUtil, localNumberFormatter, shannonToCKBFormatter } from 'utils'
+import AlertDialog from 'widgets/AlertDialog'
+import styles from './amendPendingTransactionDialog.module.scss'
+import { useInitialize, useOutputs } from './hooks'
+
+const AmendPendingTransactionDialog = ({ tx, onClose }: { tx: State.Transaction; onClose: () => void }) => {
+ const {
+ wallet: { id: walletID = '', addresses },
+ chain: { networkID },
+ settings: { networks = [] },
+ sUDTAccounts,
+ } = useGlobalState()
+ const { t } = useTranslation()
+
+ const isMainnet = isMainnetUtil(networks, networkID)
+
+ const {
+ fee,
+ price,
+ setPrice,
+ transaction,
+ onSubmit,
+ minPrice,
+ isConfirmedAlertShown,
+ password,
+ onPwdChange,
+ pwdError,
+ generatedTx,
+ setGeneratedTx,
+ isSending,
+ } = useInitialize({
+ tx,
+ walletID,
+ t,
+ onClose,
+ })
+
+ const { items, lastOutputsCapacity } = useOutputs({
+ transaction,
+ isMainnet,
+ addresses,
+ sUDTAccounts,
+ fee,
+ })
+
+ const priceError = useMemo(() => {
+ return Number(price) < Number(minPrice) ? t('price-switch.errorTip', { minPrice }) : null
+ }, [price, minPrice])
+
+ const inputHint = t('price-switch.hintTip', { suggestFeeRate: minPrice })
+
+ const handlePriceChange = useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value: inputValue } = e.currentTarget
+
+ const value = inputValue.split('.')[0].replace(/[^\d]/g, '')
+ setPrice(value)
+ },
+ [setPrice]
+ )
+
+ useEffect(() => {
+ if (transaction && lastOutputsCapacity !== undefined) {
+ const outputs = items.map(item => {
+ const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity
+ if (item.output.data === DAO_DATA) {
+ // eslint-disable-next-line no-param-reassign
+ item.output.daoData = DAO_DATA
+ }
+ return {
+ ...item.output,
+ capacity,
+ }
+ })
+
+ setGeneratedTx({
+ ...transaction,
+ outputs,
+ })
+ }
+ }, [lastOutputsCapacity, transaction, items, setGeneratedTx])
+
+ const disabled = !!(
+ isSending ||
+ !generatedTx ||
+ priceError ||
+ lastOutputsCapacity === undefined ||
+ lastOutputsCapacity < MIN_AMOUNT
+ )
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+AmendPendingTransactionDialog.displayName = 'AmendPendingTransactionDialog'
+
+export default AmendPendingTransactionDialog
diff --git a/packages/neuron-ui/src/components/AmendSUDTSend/amendSUDTSend.module.scss b/packages/neuron-ui/src/components/AmendSUDTSend/amendSUDTSend.module.scss
new file mode 100644
index 0000000000..7b971044b1
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSUDTSend/amendSUDTSend.module.scss
@@ -0,0 +1,158 @@
+@import '../../styles/mixin.scss';
+$bottomHeight: 24px;
+$headerHeight: 72px;
+$noticeHeight: 60px;
+
+.layout {
+ display: flex;
+
+ .left {
+ flex: 1;
+ position: relative;
+ min-width: 480px;
+
+ .addressCell {
+ margin-bottom: 10px;
+ .label {
+ font-size: 14px;
+ line-height: 20px;
+ color: var(--secondary-text-color);
+ margin-bottom: 8px;
+ }
+ .content {
+ font-family: 'JetBrains Mono';
+ display: flex;
+ align-items: center;
+ color: var(--main-text-color);
+ border: 1px solid var(--divide-line-color);
+ background-color: var(--input-disabled-color);
+ border-radius: 8px;
+ padding: 10px 16px;
+ word-break: break-word;
+ white-space: break-spaces;
+ }
+ }
+
+ .inputCell {
+ background: var(--secondary-background-color);
+ padding: 20px 16px 18px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+ .textFieldClass {
+ margin-bottom: 10px;
+ }
+ }
+
+ .leftFooter {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ left: 0;
+ height: 80px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .addButton {
+ min-width: 216px;
+ padding: 20px;
+ box-shadow: 0 0 10px var(--primary-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ &_icon {
+ margin-right: 8px;
+ }
+ svg {
+ g,
+ path {
+ fill: var(--primary-text-color);
+ }
+ }
+ }
+ }
+ }
+
+ &.withFullySynced {
+ .right {
+ height: calc(100vh - $headerHeight - $bottomHeight - $noticeHeight);
+ }
+ }
+
+ .right {
+ margin-left: 16px;
+ flex: 1;
+ position: relative;
+ border-radius: 16px;
+ background: var(--secondary-background-color);
+ height: calc(100vh - $headerHeight - $bottomHeight);
+ $actionHeight: 108px;
+
+ .content {
+ $contentPadding: 16px;
+ padding: 16px;
+ /**
+ These styles are for showing the full tips, because of content needs to scroll on the y-axis.
+ Then the overflow property that is set as scroll will truncate the tips overflow of the content.
+ So I need to expand the content's left to show the full tips.
+ */
+ position: relative;
+ left: -48px;
+ padding-left: 64px;
+ width: calc(100% - 32px);
+ overflow-y: scroll;
+ height: calc(100% - $actionHeight - 2 * $contentPadding);
+ }
+
+ .rightFooter {
+ position: absolute;
+ width: 100%;
+ bottom: 16px;
+ left: 0;
+ text-align: center;
+ .actions {
+ display: flex;
+ justify-content: center;
+
+ button {
+ width: 216px;
+ &:last-child {
+ margin-left: 12px;
+ }
+ }
+ }
+ }
+ }
+}
+
+.headerContainer {
+ display: flex;
+ align-items: center;
+
+ .goBack {
+ margin-right: 8px;
+ cursor: pointer;
+ g,
+ path {
+ stroke: var(--main-text-color);
+ }
+ }
+ .btn {
+ margin-left: 12px;
+ min-width: 30px;
+ height: 16px;
+ padding: 0 8px;
+ border-radius: 0;
+ border-left: 1px solid var(--button-cancel-color);
+ svg {
+ g,
+ path {
+ fill: var(--main-text-color);
+ }
+ }
+ }
+
+ .balance {
+ font-size: 14px;
+ }
+}
diff --git a/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts b/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts
new file mode 100644
index 0000000000..b82355cec2
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSUDTSend/hooks.ts
@@ -0,0 +1,142 @@
+import React, { useState, useCallback, useEffect, useMemo } from 'react'
+import { TFunction } from 'i18next'
+import { AppActions } from 'states/stateProvider/reducer'
+import { getTransaction as getOnChainTransaction } from 'services/chain'
+import { getTransaction as getSentTransaction, getTransactionList } from 'services/remote'
+import { isSuccessResponse } from 'utils'
+import { FEE_RATIO } from 'utils/const'
+
+export const useInitialize = ({
+ hash,
+ walletID,
+ dispatch,
+}: {
+ hash: string
+ walletID: string
+ isMainnet: boolean
+ dispatch: React.Dispatch
+ t: TFunction
+}) => {
+ const [transaction, setTransaction] = useState(null)
+ const [size, setSize] = useState(0)
+ const [minPrice, setMinPrice] = useState('0')
+ const [price, setPrice] = useState('0')
+ const [description, setDescription] = useState('')
+ const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false)
+ const [sudtInfo, setSudtInfo] = useState(null)
+ const [txValue, setTxValue] = useState('0')
+
+ const onDescriptionChange = useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value } = e.target as HTMLInputElement
+ setDescription(value)
+ },
+ [dispatch]
+ )
+
+ const fee = useMemo(() => {
+ const ratio = BigInt(FEE_RATIO)
+ const base = BigInt(size) * BigInt(price)
+ const curFee = base / ratio
+ if (curFee * ratio < base) {
+ return curFee + BigInt(1)
+ }
+ return curFee
+ }, [price, size])
+
+ const fetchInitData = useCallback(async () => {
+ const {
+ minReplaceFee,
+ transaction: { outputsData },
+ } = await getOnChainTransaction(hash)
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ }
+
+ const listRes = await getTransactionList({
+ walletID,
+ pageNo: 1,
+ pageSize: 10,
+ keywords: hash,
+ })
+ if (isSuccessResponse(listRes)) {
+ const list = listRes.result.items
+ if (list.length) {
+ const { sudtInfo: info, value } = list[0]
+ setSudtInfo(info)
+ setTxValue(value)
+ }
+ }
+
+ const txRes = await getSentTransaction({ hash, walletID })
+ if (isSuccessResponse(txRes)) {
+ const tx = txRes.result
+
+ setTransaction({ ...tx, outputsData })
+
+ setSize(tx.size)
+ if (minReplaceFee) {
+ const mPrice = ((BigInt(minReplaceFee) * BigInt(FEE_RATIO)) / BigInt(tx.size)).toString()
+ setMinPrice(mPrice)
+ setPrice(mPrice)
+ }
+ }
+ }, [hash, setIsConfirmedAlertShown, setPrice, setTransaction, setSize, setMinPrice])
+
+ useEffect(() => {
+ fetchInitData()
+ }, [])
+
+ const onSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ const {
+ dataset: { walletId, status },
+ } = e.target as HTMLFormElement
+ e.preventDefault()
+ if (status !== 'ready' || !transaction) {
+ return
+ }
+ try {
+ const { minReplaceFee } = await getOnChainTransaction(hash)
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ return
+ }
+
+ const actionType =
+ transaction.inputs.length > transaction.witnesses.length ? 'send-sudt' : 'send-acp-sudt-to-new-cell'
+
+ dispatch({
+ type: AppActions.RequestPassword,
+ payload: {
+ walletID: walletId as string,
+ amendHash: hash,
+ actionType,
+ },
+ })
+ } catch {
+ // ignore
+ }
+ },
+ [dispatch, walletID, hash, setIsConfirmedAlertShown, transaction]
+ )
+
+ return {
+ setPrice,
+ price,
+ description,
+ onDescriptionChange,
+ fee,
+ transaction,
+ setTransaction,
+ minPrice,
+ isConfirmedAlertShown,
+ onSubmit,
+ sudtInfo,
+ txValue,
+ }
+}
+
+export default {
+ useInitialize,
+}
diff --git a/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx b/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx
new file mode 100644
index 0000000000..66250d03b6
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSUDTSend/index.tsx
@@ -0,0 +1,223 @@
+import React, { useEffect, useMemo, useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useParams } from 'react-router-dom'
+import { useState as useGlobalState, useDispatch, AppActions } from 'states'
+import TextField from 'widgets/TextField'
+import PageContainer from 'components/PageContainer'
+import Button from 'widgets/Button'
+import Spinner from 'widgets/Spinner'
+import { GoBack } from 'widgets/Icons/icon'
+import {
+ isMainnet as isMainnetUtil,
+ localNumberFormatter,
+ useGoBack,
+ scriptToAddress,
+ shannonToCKBFormatter,
+ sudtValueToAmount,
+ sUDTAmountFormatter,
+} from 'utils'
+import { DEFAULT_SUDT_FIELDS } from 'utils/const'
+import AlertDialog from 'widgets/AlertDialog'
+import { useOutputs } from 'components/AmendPendingTransactionDialog/hooks'
+import styles from './amendSUDTSend.module.scss'
+import { useInitialize } from './hooks'
+
+const AmendSUDTSend = () => {
+ const {
+ app: {
+ loadings: { sending = false },
+ showWaitForFullySynced,
+ },
+ wallet: { id: walletID = '', addresses },
+ chain: { networkID },
+ experimental,
+ settings: { networks = [] },
+ sUDTAccounts,
+ } = useGlobalState()
+ const dispatch = useDispatch()
+ const { t } = useTranslation()
+ const { hash = '' } = useParams()
+
+ const onBack = useGoBack()
+
+ const isMainnet = isMainnetUtil(networks, networkID)
+
+ const {
+ fee,
+ price,
+ setPrice,
+ transaction,
+ onSubmit,
+ minPrice,
+ isConfirmedAlertShown,
+ sudtInfo,
+ description,
+ onDescriptionChange,
+ txValue,
+ } = useInitialize({
+ hash,
+ walletID,
+ isMainnet,
+ dispatch,
+ t,
+ })
+
+ const { items, lastOutputsCapacity } = useOutputs({
+ transaction,
+ isMainnet,
+ addresses,
+ sUDTAccounts,
+ fee,
+ })
+
+ const priceError = useMemo(() => {
+ return Number(price || '0') < Number(minPrice) ? t('price-switch.errorTip', { minPrice }) : null
+ }, [price, minPrice])
+
+ const inputHint = t('price-switch.hintTip', { suggestFeeRate: minPrice })
+
+ const handlePriceChange = useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value: inputValue } = e.currentTarget
+
+ const value = inputValue.split('.')[0].replace(/[^\d]/g, '')
+ setPrice(value)
+ },
+ [setPrice]
+ )
+
+ const toAddress = useMemo(() => {
+ if (!transaction?.outputs) return ''
+
+ const list = sUDTAccounts.map(item => item.address)
+
+ const to = transaction?.outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ if (list.includes(address) || (sudtInfo && !output.type)) {
+ return false
+ }
+ return true
+ })
+ if (to) {
+ return scriptToAddress(to.lock, { isMainnet })
+ }
+ return scriptToAddress(transaction?.outputs[0].lock, { isMainnet })
+ }, [transaction?.outputs])
+
+ useEffect(() => {
+ if (transaction && lastOutputsCapacity !== undefined) {
+ const outputs = items.map(item => {
+ const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity
+ return {
+ ...item.output,
+ capacity,
+ }
+ })
+ dispatch({
+ type: AppActions.UpdateExperimentalParams,
+ payload: {
+ tx: {
+ ...transaction,
+ description: experimental?.params?.description || description || '',
+ outputs,
+ },
+ },
+ })
+ }
+ }, [lastOutputsCapacity, transaction, items, dispatch, experimental?.params?.description, description])
+
+ const disabled =
+ sending || !experimental?.tx || priceError || lastOutputsCapacity === undefined || lastOutputsCapacity < 0
+
+ return (
+
+
+ {t('navbar.send')}
+
+ }
+ >
+
+
+
+ )
+}
+
+AmendSUDTSend.displayName = 'AmendSUDTSend'
+
+export default AmendSUDTSend
diff --git a/packages/neuron-ui/src/components/AmendSend/amendSend.module.scss b/packages/neuron-ui/src/components/AmendSend/amendSend.module.scss
new file mode 100644
index 0000000000..10302555ae
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSend/amendSend.module.scss
@@ -0,0 +1,199 @@
+@import '../../styles/mixin.scss';
+$bottomHeight: 24px;
+$headerHeight: 72px;
+$noticeHeight: 60px;
+
+.layout {
+ display: flex;
+
+ .left {
+ flex: 1;
+ position: relative;
+ min-width: 480px;
+
+ .addressCell {
+ margin-bottom: 10px;
+ .label {
+ font-size: 14px;
+ line-height: 20px;
+ color: var(--secondary-text-color);
+ margin-bottom: 8px;
+ }
+ .content {
+ font-family: 'JetBrains Mono';
+ display: flex;
+ align-items: center;
+ color: var(--main-text-color);
+ border: 1px solid var(--divide-line-color);
+ background-color: var(--input-disabled-color);
+ border-radius: 8px;
+ padding: 10px 16px;
+ word-break: break-word;
+ white-space: break-spaces;
+ }
+ }
+
+ .inputCell {
+ background: var(--secondary-background-color);
+ padding: 20px 16px 18px;
+ border-radius: 16px;
+ margin-bottom: 16px;
+ .textFieldClass {
+ margin-bottom: 10px;
+ }
+ }
+
+ .leftFooter {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ left: 0;
+ height: 80px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .addButton {
+ min-width: 216px;
+ padding: 20px;
+ box-shadow: 0 0 10px var(--primary-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ &_icon {
+ margin-right: 8px;
+ }
+ svg {
+ g,
+ path {
+ fill: var(--primary-text-color);
+ }
+ }
+ }
+ }
+ }
+
+ &.withFullySynced {
+ .right {
+ height: calc(100vh - $headerHeight - $bottomHeight - $noticeHeight);
+ }
+ }
+
+ .right {
+ margin-left: 16px;
+ flex: 1;
+ position: relative;
+ border-radius: 16px;
+ background: var(--secondary-background-color);
+ height: calc(100vh - $headerHeight - $bottomHeight);
+ $actionHeight: 108px;
+
+ .content {
+ $contentPadding: 16px;
+ padding: 16px;
+ /**
+ These styles are for showing the full tips, because of content needs to scroll on the y-axis.
+ Then the overflow property that is set as scroll will truncate the tips overflow of the content.
+ So I need to expand the content's left to show the full tips.
+ */
+ position: relative;
+ left: -48px;
+ padding-left: 64px;
+ width: calc(100% - 32px);
+ overflow-y: scroll;
+ height: calc(100% - $actionHeight - 2 * $contentPadding);
+ }
+
+ .totalAmountField {
+ padding-bottom: 34px;
+ p {
+ margin: 0;
+ }
+ .title {
+ color: var(--secondary-text-color);
+ font-size: 14px;
+ }
+ .value {
+ margin-top: 22px;
+ color: var(--main-text-color);
+ font-size: 32px;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ font-family: 'D-DIN-PRO';
+ }
+ .errorMessage {
+ margin-top: 8px;
+ color: var(--error-color);
+ }
+ }
+
+ .rightFooter {
+ position: absolute;
+ width: 100%;
+ bottom: 16px;
+ left: 0;
+ text-align: center;
+ .actions {
+ display: flex;
+ justify-content: center;
+
+ button {
+ width: 216px;
+ &:last-child {
+ margin-left: 12px;
+ }
+ }
+ }
+ .allowUseSent {
+ display: inline-block;
+ margin-bottom: 16px;
+ color: var(--secondary-text-color);
+ input[type='checkbox'] {
+ display: none;
+ margin-right: 8px;
+ }
+ input[type='checkbox'] + span {
+ display: inline-block;
+ padding-left: 26px;
+ background: url('../../widgets/Icons/Radio.svg') no-repeat left top;
+ user-select: none;
+ }
+ input[type='checkbox']:checked + span {
+ background: url('../../widgets/Icons/RadioSelected.svg') no-repeat left top;
+ }
+ }
+ }
+ }
+}
+
+.headerContainer {
+ display: flex;
+ align-items: center;
+
+ .goBack {
+ margin-right: 8px;
+ cursor: pointer;
+ g,
+ path {
+ stroke: var(--main-text-color);
+ }
+ }
+ .btn {
+ margin-left: 12px;
+ min-width: 30px;
+ height: 16px;
+ padding: 0 8px;
+ border-radius: 0;
+ border-left: 1px solid var(--button-cancel-color);
+ svg {
+ g,
+ path {
+ fill: var(--main-text-color);
+ }
+ }
+ }
+
+ .balance {
+ font-size: 14px;
+ }
+}
diff --git a/packages/neuron-ui/src/components/AmendSend/hooks.ts b/packages/neuron-ui/src/components/AmendSend/hooks.ts
new file mode 100644
index 0000000000..5e82ccdc02
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSend/hooks.ts
@@ -0,0 +1,149 @@
+import React, { useState, useCallback, useEffect, useMemo } from 'react'
+import { TFunction } from 'i18next'
+import { AppActions, StateAction, StateDispatch } from 'states/stateProvider/reducer'
+import { getTransaction as getOnChainTransaction } from 'services/chain'
+import { getTransaction as getSentTransaction } from 'services/remote'
+import { isSuccessResponse } from 'utils'
+import { FEE_RATIO } from 'utils/const'
+
+const clear = (dispatch: StateDispatch) => {
+ dispatch({
+ type: AppActions.ClearSendState,
+ })
+}
+
+const useUpdateTransactionPrice = (dispatch: StateDispatch) =>
+ useCallback(
+ (value: string) => {
+ const price = value.split('.')[0].replace(/[^\d]/g, '')
+ dispatch({
+ type: AppActions.UpdateSendPrice,
+ payload: price,
+ })
+ },
+ [dispatch]
+ )
+
+const useSendDescriptionChange = (dispatch: StateDispatch) =>
+ useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value } = e.target as HTMLInputElement
+ dispatch({
+ type: AppActions.UpdateSendDescription,
+ payload: value,
+ })
+ },
+ [dispatch]
+ )
+
+export const useInitialize = ({
+ hash,
+ walletID,
+ price,
+ dispatch,
+}: {
+ hash: string
+ walletID: string
+ price: string
+ isMainnet: boolean
+ dispatch: React.Dispatch
+ t: TFunction
+}) => {
+ const [transaction, setTransaction] = useState(null)
+ const [size, setSize] = useState(0)
+ const [minPrice, setMinPrice] = useState('0')
+ const [isConfirmedAlertShown, setIsConfirmedAlertShown] = useState(false)
+
+ const updateTransactionPrice = useUpdateTransactionPrice(dispatch)
+ const onDescriptionChange = useSendDescriptionChange(dispatch)
+
+ const fee = useMemo(() => {
+ const ratio = BigInt(FEE_RATIO)
+ const base = BigInt(size) * BigInt(price)
+ const curFee = base / ratio
+ if (curFee * ratio < base) {
+ return curFee + BigInt(1)
+ }
+ return curFee
+ }, [price, size])
+
+ const fetchInitData = useCallback(async () => {
+ const res = await getOnChainTransaction(hash)
+ const {
+ minReplaceFee,
+ transaction: { outputsData },
+ } = res
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ }
+
+ const txRes = await getSentTransaction({ hash, walletID })
+ if (isSuccessResponse(txRes)) {
+ const tx = txRes.result
+ setTransaction({
+ ...tx,
+ outputsData,
+ })
+
+ setSize(tx.size)
+ if (minReplaceFee) {
+ const mPrice = ((BigInt(minReplaceFee) * BigInt(FEE_RATIO)) / BigInt(tx.size)).toString()
+ setMinPrice(mPrice)
+ updateTransactionPrice(mPrice)
+ }
+ }
+ }, [hash, setIsConfirmedAlertShown, updateTransactionPrice, setTransaction, setSize, setMinPrice])
+
+ useEffect(() => {
+ fetchInitData()
+ }, [])
+
+ useEffect(() => {
+ clear(dispatch)
+ }, [walletID, dispatch])
+
+ const onSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ const {
+ dataset: { walletId, status },
+ } = e.target as HTMLFormElement
+ e.preventDefault()
+ if (status !== 'ready') {
+ return
+ }
+ try {
+ const { minReplaceFee } = await getOnChainTransaction(hash)
+ if (!minReplaceFee) {
+ setIsConfirmedAlertShown(true)
+ return
+ }
+ dispatch({
+ type: AppActions.RequestPassword,
+ payload: {
+ walletID: walletId as string,
+ amendHash: hash,
+ actionType: 'send',
+ },
+ })
+ } catch {
+ // ignore
+ }
+ },
+ [dispatch, walletID, hash, setIsConfirmedAlertShown]
+ )
+
+ return {
+ updateTransactionPrice,
+ onDescriptionChange,
+ fee,
+ transaction,
+ setTransaction,
+ minPrice,
+ isConfirmedAlertShown,
+ onSubmit,
+ }
+}
+
+export default {
+ useInitialize,
+}
diff --git a/packages/neuron-ui/src/components/AmendSend/index.tsx b/packages/neuron-ui/src/components/AmendSend/index.tsx
new file mode 100644
index 0000000000..453eef097b
--- /dev/null
+++ b/packages/neuron-ui/src/components/AmendSend/index.tsx
@@ -0,0 +1,276 @@
+import React, { useEffect, useMemo, useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useParams, useNavigate } from 'react-router-dom'
+import { useState as useGlobalState, useDispatch, appState, AppActions } from 'states'
+import TextField from 'widgets/TextField'
+import PageContainer from 'components/PageContainer'
+import Button from 'widgets/Button'
+import Spinner from 'widgets/Spinner'
+import { GoBack } from 'widgets/Icons/icon'
+import { MIN_AMOUNT } from 'utils/const'
+import {
+ isMainnet as isMainnetUtil,
+ localNumberFormatter,
+ useGoBack,
+ scriptToAddress,
+ shannonToCKBFormatter,
+ RoutePath,
+ isSecp256k1Address,
+} from 'utils'
+import AlertDialog from 'widgets/AlertDialog'
+import styles from './amendSend.module.scss'
+import { useInitialize } from './hooks'
+
+const AmendSend = () => {
+ const {
+ app: {
+ send = appState.send,
+ loadings: { sending = false },
+ showWaitForFullySynced,
+ },
+ wallet: { id: walletID = '', addresses },
+ chain: { networkID },
+ settings: { networks = [] },
+ } = useGlobalState()
+ const dispatch = useDispatch()
+ const navigate = useNavigate()
+ const { t } = useTranslation()
+ const { hash = '' } = useParams()
+
+ const onBack = useGoBack()
+
+ const isMainnet = isMainnetUtil(networks, networkID)
+
+ const { fee, updateTransactionPrice, onDescriptionChange, transaction, onSubmit, minPrice, isConfirmedAlertShown } =
+ useInitialize({
+ hash,
+ walletID,
+ price: send.price,
+ isMainnet,
+ dispatch,
+ t,
+ })
+
+ const priceError = useMemo(() => {
+ return Number(send.price) < Number(minPrice) ? t('price-switch.errorTip', { minPrice }) : null
+ }, [send.price, minPrice])
+
+ const inputHint = t('price-switch.hintTip', { suggestFeeRate: minPrice })
+
+ const handlePriceChange = useCallback(
+ (e: React.SyntheticEvent) => {
+ const { value: inputValue } = e.currentTarget
+
+ updateTransactionPrice(inputValue.replace(/,/g, ''))
+ },
+ [updateTransactionPrice]
+ )
+
+ const getLastOutputAddress = (outputs: State.DetailedOutput[]) => {
+ if (outputs.length === 1) {
+ return scriptToAddress(outputs[0].lock, { isMainnet })
+ }
+
+ const change = outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ if (!isSecp256k1Address(address)) {
+ navigate(`${RoutePath.History}/amendSUDTSend/${hash}`, {
+ replace: true,
+ })
+ }
+
+ return !!addresses.find(item => item.address === address && item.type === 1)
+ })
+ if (change) {
+ return scriptToAddress(change.lock, { isMainnet })
+ }
+
+ const receive = outputs.find(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ return !!addresses.find(item => item.address === address && item.type === 0)
+ })
+ if (receive) {
+ return scriptToAddress(receive.lock, { isMainnet })
+ }
+
+ return ''
+ }
+
+ const inputsCapacity = useMemo(() => {
+ if (transaction) {
+ return transaction.inputs.reduce((total, cur) => {
+ return total + BigInt(cur.capacity || '0')
+ }, BigInt(0))
+ }
+ return undefined
+ }, [transaction])
+
+ const items: {
+ address: string
+ amount: string
+ capacity: string
+ isLastOutput: boolean
+ output: State.DetailedOutput
+ }[] = useMemo(() => {
+ if (transaction && transaction.outputs.length && inputsCapacity) {
+ const lastOutputAddress = getLastOutputAddress(transaction.outputs)
+ return transaction.outputs.map(output => {
+ const address = scriptToAddress(output.lock, { isMainnet })
+ const capacity =
+ transaction.outputs.length === 1 && address === lastOutputAddress
+ ? (inputsCapacity - fee).toString()
+ : output.capacity
+ return {
+ capacity,
+ address,
+ output,
+ amount: shannonToCKBFormatter(capacity || '0'),
+ isLastOutput: address === lastOutputAddress,
+ }
+ })
+ }
+ return []
+ }, [transaction?.outputs, inputsCapacity, fee])
+
+ const outputsCapacity = useMemo(() => {
+ const outputList = items.length === 1 ? items : items.filter(item => !item.isLastOutput)
+ return outputList.reduce((total, cur) => {
+ return total + BigInt(cur.capacity || '0')
+ }, BigInt(0))
+ }, [items])
+
+ const totalAmount = shannonToCKBFormatter(outputsCapacity.toString())
+
+ const lastOutputsCapacity = useMemo(() => {
+ if (inputsCapacity) {
+ if (items.length === 1) {
+ return BigInt(items[0].capacity || '0')
+ }
+
+ return inputsCapacity - outputsCapacity - fee
+ }
+ return undefined
+ }, [inputsCapacity, fee, outputsCapacity, items])
+
+ useEffect(() => {
+ if (transaction && lastOutputsCapacity !== undefined) {
+ const outputs = items.map(item => {
+ const capacity = item.isLastOutput ? lastOutputsCapacity.toString() : item.capacity
+ return {
+ ...item.output,
+ capacity,
+ }
+ })
+ dispatch({
+ type: AppActions.UpdateGeneratedTx,
+ payload: {
+ ...transaction,
+ outputs,
+ },
+ })
+ }
+ }, [lastOutputsCapacity, transaction, items, dispatch])
+
+ const disabled =
+ sending || !send.generatedTx || priceError || lastOutputsCapacity === undefined || lastOutputsCapacity < MIN_AMOUNT
+
+ return (
+
+
+ {t('navbar.send')}
+
+ }
+ >
+
+
+
+
+ )
+}
+
+AmendSend.displayName = 'AmendSend'
+
+export default AmendSend
diff --git a/packages/neuron-ui/src/components/ApproveMultisigTxDialog/index.tsx b/packages/neuron-ui/src/components/ApproveMultisigTxDialog/index.tsx
index 11580fe6f6..9e06a70ac9 100644
--- a/packages/neuron-ui/src/components/ApproveMultisigTxDialog/index.tsx
+++ b/packages/neuron-ui/src/components/ApproveMultisigTxDialog/index.tsx
@@ -2,11 +2,10 @@ import React, { useMemo } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { MultisigConfig, OfflineSignJSON } from 'services/remote'
import { useState as useGlobalState } from 'states'
-import { ckbCore } from 'services/chain'
import Tooltip from 'widgets/Tooltip'
import { Copy } from 'widgets/Icons/icon'
import CopyZone from 'widgets/CopyZone'
-import { shannonToCKBFormatter } from 'utils'
+import { scriptToAddress, shannonToCKBFormatter } from 'utils'
import ScriptTag from 'components/ScriptTag'
import Dialog from 'widgets/Dialog'
import getMultisigSignStatus from 'utils/getMultisigSignStatus'
@@ -15,10 +14,7 @@ import styles from './approveMultisigTx.module.scss'
const Cell = React.memo(
({ cell, isMainnet }: { cell: State.DetailedInput | State.DetailedOutput; isMainnet: boolean }) => {
- const address = useMemo(
- () => (cell.lock ? ckbCore.utils.scriptToAddress(cell.lock, isMainnet) : ''),
- [cell, isMainnet]
- )
+ const address = useMemo(() => (cell.lock ? scriptToAddress(cell.lock, { isMainnet }) : ''), [cell, isMainnet])
return (
diff --git a/packages/neuron-ui/src/components/Balance/index.tsx b/packages/neuron-ui/src/components/Balance/index.tsx
index dec27a4b9d..6a2b073e35 100644
--- a/packages/neuron-ui/src/components/Balance/index.tsx
+++ b/packages/neuron-ui/src/components/Balance/index.tsx
@@ -17,7 +17,7 @@ const Balance = ({ balance, connectionStatus, syncStatus }: BalanceProps) => {
return (
<>
{`${t('overview.balance')}:`}
-
+
{shannonToCKBFormatter(balance)}
diff --git a/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss b/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss
new file mode 100644
index 0000000000..1b6d32159d
--- /dev/null
+++ b/packages/neuron-ui/src/components/CellInfoDialog/cellInfoDialog.module.scss
@@ -0,0 +1,278 @@
+@import '../../styles/mixin.scss';
+
+.cellInfoDialog {
+ min-width: 650px;
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 14px;
+
+ .outPoint {
+ display: flex;
+ padding: 5px 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ border-radius: 16px;
+ border: 1px solid var(--divide-line-color);
+ background: var(--input-disabled-color);
+ font-weight: 400;
+ color: var(--main-text-color);
+
+ .hash {
+ font-family: 'JetBrains Mono';
+ }
+
+ & > svg {
+ path {
+ fill: var(--secondary-text-color);
+ }
+ }
+ }
+}
+
+.head {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ margin-bottom: 12px;
+
+ & > svg {
+ transform: rotate(-90deg);
+ path {
+ fill: var(--border-color);
+ }
+ }
+
+ [data-type='hash'] {
+ color: var(--third-text-color);
+ font-family: 'JetBrains Mono';
+ }
+
+ .liveIcon {
+ width: 8px;
+ height: 8px;
+ border-radius: 8px;
+ background-color: var(--primary-color);
+ }
+}
+
+.tabsClassName {
+ .tabsWrapClassName {
+ position: relative;
+ display: flex;
+ margin-bottom: 16px;
+ border-bottom: 1px solid var(--divide-line-color);
+
+ .tabsColumnClassName {
+ position: relative;
+ padding: 10px 20px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--tabs-default-color);
+ background-color: transparent;
+ border: none;
+ margin-right: 10px;
+
+ &:hover {
+ font-weight: bold;
+ color: var(--tabs-active);
+ }
+
+ &.active {
+ color: var(--tabs-active);
+ border-bottom: 2px solid var(--tabs-active);
+ }
+ }
+ }
+}
+
+.scriptTable {
+ border-collapse: separate;
+ border-spacing: 0px;
+ width: 100%;
+ color: var(--main-text-color);
+
+ td {
+ padding: 8px;
+ border: 1px solid var(--divide-line-color);
+ text-align: center;
+ }
+
+ thead {
+ td {
+ border-bottom: 0;
+ &:nth-child(1) {
+ border-top-left-radius: 8px;
+ border-right: 0;
+ }
+ &:nth-last-child(1) {
+ border-top-right-radius: 8px;
+ }
+ }
+ }
+
+ tbody {
+ tr {
+ &:not(:nth-last-child(1)) {
+ td {
+ border-bottom: 0;
+ }
+ }
+ &:nth-last-child(1) {
+ td {
+ &:nth-child(1) {
+ border-bottom-left-radius: 8px;
+ }
+ &:nth-last-child(1) {
+ border-bottom-right-radius: 8px;
+ }
+ }
+ }
+ }
+ td {
+ &:not(:nth-last-child(1)) {
+ border-right: 0;
+ }
+ }
+ }
+}
+
+.content {
+ position: relative;
+
+ .switchFormat {
+ position: absolute;
+ right: 0;
+ top: 10px;
+ color: var(--main-text-color);
+ display: flex;
+ align-items: center;
+ }
+}
+
+.preStyle {
+ border: 1px solid var(--lock-info-title-border);
+ border-radius: 12px;
+ padding: 16px;
+ overflow-y: auto;
+ margin: 0;
+ color: var(--main-text-color);
+ font-family: 'JetBrains Mono';
+ display: block;
+ width: calc(100% - 32px);
+
+ &::selection {
+ background-color: var(--primary-color);
+ color: var(--primary-text-color);
+ }
+}
+
+.capacityUsed {
+ padding: 0 16px 16px;
+ .slider {
+ width: 616px;
+ height: 16px;
+ border-radius: 8px;
+ border: 1px solid var(--divide-line-color);
+ background-color: var(--secondary-background-color);
+ position: relative;
+ overflow: hidden;
+
+ & > div {
+ position: absolute;
+ top: -1px;
+ height: 100%;
+ border-radius: 8px;
+ border: 1px solid var(--primary-color);
+ background-color: var(--primary-color);
+ }
+ }
+ .capacityDetail {
+ color: var(--main-text-color);
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ margin-top: 12px;
+ }
+}
+
+.dataAction {
+ position: fixed;
+ width: 496px;
+ border-radius: 8px;
+ background: var(--secondary-background-color);
+ box-shadow: 0px 2px 10px 0px var(--main-shadow-color);
+
+ .dataView {
+ color: var(--main-text-color);
+ font-weight: 500;
+ padding: 12px;
+ border-bottom: 1px solid var(--divide-line-color);
+ display: flex;
+ align-items: center;
+
+ .formatTypeBtn {
+ border: none;
+ cursor: pointer;
+ background-color: transparent;
+
+ &:hover,
+ &[data-activity='true'] {
+ color: var(--primary-color);
+ }
+
+ &[data-open='true'] {
+ & > svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ & > svg {
+ margin-left: 4px;
+ path {
+ fill: var(--primary-color);
+ }
+ }
+ }
+
+ .dropdown {
+ position: relative;
+ margin-left: 12px;
+ .formatTypes {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background-color: var(--secondary-background-color);
+ border-radius: 4px;
+ box-shadow: 0px 2px 10px 0px var(--main-shadow-color);
+ display: flex;
+ flex-direction: column;
+ padding: 12px 8px;
+ margin: 0;
+ gap: 16px;
+ white-space: nowrap;
+ z-index: 10;
+ }
+ }
+ }
+
+ .dataFormat {
+ padding: 12px;
+ word-wrap: break-word;
+ }
+}
+
+.copyAddress {
+ margin-left: 4px;
+ position: relative;
+ top: 4px;
+ cursor: pointer;
+}
+
+.notice {
+ @include dialog-copy-animation;
+}
diff --git a/packages/neuron-ui/src/components/CellInfoDialog/index.tsx b/packages/neuron-ui/src/components/CellInfoDialog/index.tsx
new file mode 100644
index 0000000000..df5cdad484
--- /dev/null
+++ b/packages/neuron-ui/src/components/CellInfoDialog/index.tsx
@@ -0,0 +1,186 @@
+import React, { useCallback, useMemo, useState } from 'react'
+import Dialog from 'widgets/Dialog'
+import { calculateUsedCapacity, getExplorerUrl, shannonToCKBFormatter, truncateMiddle, useCopy } from 'utils'
+import { useTranslation } from 'react-i18next'
+import Tabs from 'widgets/Tabs'
+import { type TFunction } from 'i18next'
+import { Script } from '@ckb-lumos/lumos'
+import Switch from 'widgets/Switch'
+import { Copy, ExplorerIcon } from 'widgets/Icons/icon'
+import Alert from 'widgets/Alert'
+import { openExternal } from 'services/remote'
+import styles from './cellInfoDialog.module.scss'
+
+type ScriptRenderType = 'table' | 'raw'
+
+const ScriptRender = ({ script, renderType }: { script?: Script; renderType: ScriptRenderType }) => {
+ if (renderType === 'raw') {
+ const scriptRaw = script
+ ? `{
+ "code_hash": "${script.codeHash}"
+ "hash_type": "${script.hashType}"
+ "args": "${script.args}"
+}`
+ : `{
+ "null"
+}`
+ return {scriptRaw}
+ }
+ return (
+
+
+
+ | KEY |
+ VALUE |
+
+
+
+
+ | code_hash |
+ {script?.codeHash ?? '--'} |
+
+
+ | hash_type |
+ {script?.hashType ?? '--'} |
+
+
+ | args |
+ {script?.args ?? '--'} |
+
+
+
+ )
+}
+
+const tabIds = {
+ lock: 'lock',
+ type: 'type',
+ data: 'data',
+ capacityUsage: 'capacityUsage',
+}
+
+const useTabs = ({ t, output }: { t: TFunction; output?: State.DetailedOutput }) => {
+ const [scriptRenderType, setScriptRenderType] = useState('table')
+ const usedCapacity = useMemo(() => (output ? calculateUsedCapacity(output) : 0), [output])
+ const tabs = [
+ {
+ id: tabIds.lock,
+ label: 'Lock Script',
+ render() {
+ return
+ },
+ },
+ {
+ id: tabIds.type,
+ label: 'Type Script',
+ render() {
+ return
+ },
+ },
+ {
+ id: tabIds.data,
+ label: 'Data',
+ render() {
+ return {output?.data}
+ },
+ },
+ {
+ id: tabIds.capacityUsage,
+ label: t('cell-manage.cell-detail-dialog.capacity-used'),
+ render() {
+ return (
+
+
+
+ {`Occupied ${usedCapacity} CKB, Declared ${shannonToCKBFormatter(output?.capacity ?? '')} CKB`}
+
+
+ )
+ },
+ },
+ ]
+ const [currentTab, setCurrentTab] = useState(tabs[0])
+ return {
+ currentTab,
+ setCurrentTab,
+ tabs,
+ setScriptRenderType,
+ scriptRenderType,
+ }
+}
+
+const CellInfoDialog = ({
+ onCancel,
+ output,
+ isMainnet,
+}: {
+ onCancel: () => void
+ output?: State.DetailedOutput
+ isMainnet: boolean
+}) => {
+ const [t] = useTranslation()
+
+ const { tabs, currentTab, setCurrentTab, scriptRenderType, setScriptRenderType } = useTabs({
+ t,
+ output,
+ })
+ const { copied, copyTimes, onCopy } = useCopy()
+ const onOpenTx = useCallback(() => {
+ const explorerUrl = getExplorerUrl(isMainnet)
+ openExternal(`${explorerUrl}/transaction/${output?.outPoint.txHash}`)
+ }, [isMainnet, output?.outPoint.txHash])
+ if (!output) {
+ return null
+ }
+ return (
+
+ }
+ onCancel={onCancel}
+ showFooter={false}
+ className={styles.cellInfoDialog}
+ >
+
+
+ {[tabIds.lock, tabIds.type].includes(currentTab.id) ? (
+
+ Raw Data
+ setScriptRenderType(checked ? 'raw' : 'table')}
+ />
+
+ ) : null}
+
+ {copied ? (
+
+ {t('common.copied')}
+
+ ) : null}
+
+ )
+}
+
+CellInfoDialog.displayName = 'CellInfoDialog'
+
+export default CellInfoDialog
diff --git a/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss b/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss
index 14ed375b5a..a36bad6e1e 100644
--- a/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss
+++ b/packages/neuron-ui/src/components/CellManagement/cellManagement.module.scss
@@ -51,13 +51,14 @@
}
.actions {
- & > svg {
+ display: flex;
+ gap: 16px;
+ & svg {
cursor: pointer;
&[data-disabled='true'] {
cursor: not-allowed;
opacity: 50%;
}
- margin-right: 16px;
}
}
@@ -163,9 +164,9 @@
@include checkbox;
.multiActions {
- position: absolute;
+ position: fixed;
bottom: 24px;
- left: 50%;
+ left: calc(50% + 80px);
transform: translateX(-50%);
padding: 12px 40px 12px 40px;
border-radius: 40px;
@@ -235,4 +236,22 @@
margin-right: 4px;
}
}
+
+ .hardWalletImg {
+ width: 88px;
+ height: 88px;
+ margin: 16px 0 24px 0;
+ }
+
+ .lockActions {
+ margin-top: 24px;
+ display: flex;
+ justify-content: center;
+ gap: 16px;
+ }
+
+ .hardwalletErr {
+ justify-content: center;
+ margin-top: 12px;
+ }
}
diff --git a/packages/neuron-ui/src/components/CellManagement/hooks.ts b/packages/neuron-ui/src/components/CellManagement/hooks.ts
index 6dca24e92f..8f1eecc658 100644
--- a/packages/neuron-ui/src/components/CellManagement/hooks.ts
+++ b/packages/neuron-ui/src/components/CellManagement/hooks.ts
@@ -1,25 +1,25 @@
+import { CkbAppNotFoundException, DeviceNotFoundException } from 'exceptions'
+import { TFunction } from 'i18next'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
- openExternal,
+ connectDevice,
+ getDeviceCkbAppVersion,
+ getDevices,
getLiveCells,
+ getPlatform,
updateLiveCellsLocalInfo,
updateLiveCellsLockStatus as updateLiveCellsLockStatusAPI,
+ updateWallet,
} from 'services/remote'
+import { ControllerResponse } from 'services/remote/remoteApiWrapper'
import { AppActions, useDispatch } from 'states'
-import {
- LockScriptCategory,
- RoutePath,
- TypeScriptCategory,
- calculateUsedCapacity,
- getExplorerUrl,
- isSuccessResponse,
- outPointToStr,
-} from 'utils'
+import { ErrorCode, LockScriptCategory, RoutePath, TypeScriptCategory, isSuccessResponse, outPointToStr } from 'utils'
import { SortType } from 'widgets/Table'
const cellTypeOrder: Record
= {
- [TypeScriptCategory.SUDT]: 1,
+ [TypeScriptCategory.SUDT]: 0,
+ [TypeScriptCategory.XUDT]: 1,
[TypeScriptCategory.NFT]: 2,
[TypeScriptCategory.Spore]: 3,
[TypeScriptCategory.Unknown]: 4,
@@ -48,6 +48,9 @@ const getLockStatusAndReason = (item: State.LiveCellWithLocalInfo) => {
case TypeScriptCategory.DAO:
lockedReason = { key: 'cell-manage.locked-reason.NFT-SUDT-DAO', params: { type: 'Nervos DAO' } }
break
+ case TypeScriptCategory.XUDT:
+ lockedReason = { key: 'cell-manage.locked-reason.NFT-SUDT-DAO', params: { type: 'XUDT' } }
+ break
case TypeScriptCategory.Unknown:
lockedReason = { key: 'cell-manage.locked-reason.Unknown' }
break
@@ -83,6 +86,7 @@ const getCellType = (item: State.LiveCellWithLocalInfo) => {
switch (item.typeScriptType) {
case TypeScriptCategory.NFT:
case TypeScriptCategory.SUDT:
+ case TypeScriptCategory.XUDT:
case TypeScriptCategory.Spore:
case TypeScriptCategory.Unknown:
return item.typeScriptType
@@ -141,12 +145,17 @@ export const useLiveCells = ({
}
return liveCells
}, [sortInfo, liveCells])
- useEffect(() => {
+
+ const fetchLiveCells = useCallback(() => {
getLiveCells().then(res => {
if (isSuccessResponse(res) && res.result) {
setLiveCells(res.result.map(v => ({ ...v, ...getLockStatusAndReason(v), cellType: getCellType(v) })))
}
})
+ }, [setLiveCells])
+
+ useEffect(() => {
+ fetchLiveCells()
}, [])
const updateLiveCell = useCallback((params: State.UpdateLiveCellLocalInfo) => {
@@ -195,6 +204,7 @@ export const useLiveCells = ({
onSorted,
updateLiveCellsLockStatus,
sortInfo,
+ fetchLiveCells,
}
}
@@ -203,6 +213,8 @@ export enum Actions {
Lock = 'lock',
Unlock = 'unlock',
Consume = 'consume',
+ Consolidate = 'consolidate',
+ Recycle = 'recycle',
}
export const useAction = ({
@@ -213,6 +225,8 @@ export const useAction = ({
resetPassword,
setError,
password,
+ verifyDeviceStatus,
+ wallet,
}: {
liveCells: State.LiveCellWithLocalInfo[]
currentPageLiveCells: State.LiveCellWithLocalInfo[]
@@ -221,6 +235,8 @@ export const useAction = ({
resetPassword: () => void
setError: (error: string) => void
password: string
+ verifyDeviceStatus: () => Promise
+ wallet: State.Wallet
}) => {
const dispatch = useDispatch()
const navigate = useNavigate()
@@ -228,7 +244,7 @@ export const useAction = ({
const [operateCells, setOperateCells] = useState([])
const [loading, setLoading] = useState(false)
const onOpenActionDialog = useCallback(
- (e: React.SyntheticEvent) => {
+ async (e: React.SyntheticEvent) => {
e.stopPropagation()
const { action: curAction, index } = e.currentTarget.dataset as { action: Actions; index: string }
if (!curAction || index === undefined || !currentPageLiveCells[+index]) return
@@ -236,24 +252,38 @@ export const useAction = ({
setOperateCells([operateCell])
setAction(curAction)
resetPassword()
+ await verifyDeviceStatus()
},
[currentPageLiveCells, setOperateCells, dispatch, navigate]
)
const onMultiAction = useCallback(
- (e: React.SyntheticEvent) => {
+ async (e: React.SyntheticEvent) => {
e.stopPropagation()
const { action: curAction } = e.currentTarget.dataset as { action: Actions }
if (!curAction || !selectedOutPoints.size) return
setOperateCells(liveCells.filter(v => selectedOutPoints.has(outPointToStr(v.outPoint))))
setAction(curAction)
resetPassword()
+ await verifyDeviceStatus()
},
[liveCells, selectedOutPoints, setOperateCells, dispatch, navigate]
)
- const onActionConfirm = useCallback(() => {
+
+ const getConsolidateAddress = useCallback(() => {
+ const { addresses } = wallet
+ if (addresses.length === 1) {
+ return addresses[0].address
+ }
+ const unusedReceiveAddress = addresses.find(a => a.type === 0 && a.txCount === 0)?.address ?? ''
+
+ return unusedReceiveAddress
+ }, [wallet])
+
+ const onActionConfirm = useCallback(async () => {
switch (action) {
- case 'lock':
- case 'unlock':
+ case Actions.Lock:
+ case Actions.Unlock:
+ if (!(await verifyDeviceStatus())) return
setLoading(true)
updateLiveCellsLockStatus({
outPoints: operateCells.map(v => v.outPoint),
@@ -272,17 +302,24 @@ export const useAction = ({
setLoading(false)
})
break
- case 'consume':
+ case Actions.Consume:
dispatch({
type: AppActions.UpdateConsumeCells,
payload: operateCells.map(v => ({ outPoint: v.outPoint, capacity: v.capacity })),
})
navigate(`${RoutePath.Send}?isSendMax=true`)
break
+ case Actions.Consolidate:
+ dispatch({
+ type: AppActions.UpdateConsumeCells,
+ payload: operateCells.map(v => ({ outPoint: v.outPoint, capacity: v.capacity })),
+ })
+ navigate(`${RoutePath.Send}?isSendMax=true&toAddress=${getConsolidateAddress()}`)
+ break
default:
break
}
- }, [action, operateCells, dispatch, navigate, password])
+ }, [action, operateCells, dispatch, navigate, password, getConsolidateAddress])
const onActionCancel = useCallback(() => {
setAction(undefined)
setOperateCells([])
@@ -351,52 +388,6 @@ export const useSelect = (liveCells: State.LiveCellWithLocalInfo[]) => {
}
}
-export const useViewCell = ({ isMainnet, viewCell }: { isMainnet: boolean; viewCell: State.LiveCellWithLocalInfo }) => {
- const onViewDetail = useCallback(
- (e: React.SyntheticEvent) => {
- const {
- dataset: { txHash },
- } = e.currentTarget
- if (!txHash) {
- return
- }
- const explorerUrl = getExplorerUrl(isMainnet)
- openExternal(`${explorerUrl}/transaction/${txHash}`)
- },
- [isMainnet]
- )
- const rawLock = `{
- "code_hash": "${viewCell?.lock.codeHash}"
- "hash_type": "${viewCell?.lock.hashType}"
- "args": "${viewCell?.lock.args}"
-}`
- const rawType = viewCell?.type
- ? `{
- "code_hash": "${viewCell.type.codeHash}"
- "hash_type": "${viewCell.type.hashType}"
- "args": "${viewCell.type.args}"
-}`
- : `{
- "null"
-}`
- const rawData = `{
- "data": "${viewCell?.data ?? '0x'}"
-}`
- const usedCapacity = useMemo(() => {
- if (!viewCell) {
- return 0
- }
- return calculateUsedCapacity(viewCell)
- }, [viewCell])
- return {
- onViewDetail,
- rawData,
- rawLock,
- rawType,
- usedCapacity,
- }
-}
-
export const usePassword = () => {
const [password, setPassword] = useState('')
const [error, setError] = useState('')
@@ -417,3 +408,124 @@ export const usePassword = () => {
resetPassword,
}
}
+
+export const useHardWallet = ({ wallet, t }: { wallet: State.WalletIdentity; t: TFunction }) => {
+ const isWin32 = useMemo(() => {
+ return getPlatform() === 'win32'
+ }, [])
+ const [error, setError] = useState()
+ const isNotAvailable = useMemo(() => {
+ return error === ErrorCode.DeviceNotFound || error === ErrorCode.CkbAppNotFound
+ }, [error])
+
+ const [deviceInfo, setDeviceInfo] = useState(wallet.device)
+ const [isReconnecting, setIsReconnecting] = useState(false)
+
+ const ensureDeviceAvailable = useCallback(
+ async (device: State.DeviceInfo) => {
+ try {
+ const connectionRes = await connectDevice(device)
+ let { descriptor } = device
+ if (!isSuccessResponse(connectionRes)) {
+ // for win32, opening or closing the ckb app changes the HID descriptor(deviceInfo),
+ // so if we can't connect to the device, we need to re-search device automatically.
+ // for unix, the descriptor never changes unless user plugs the device into another USB port,
+ // in that case, mannauly re-search device one time will do.
+ if (isWin32) {
+ setIsReconnecting(true)
+ const devicesRes = await getDevices(device)
+ setIsReconnecting(false)
+ if (isSuccessResponse(devicesRes) && Array.isArray(devicesRes.result) && devicesRes.result.length > 0) {
+ const [updatedDeviceInfo] = devicesRes.result
+ descriptor = updatedDeviceInfo.descriptor
+ setDeviceInfo(updatedDeviceInfo)
+ } else {
+ throw new DeviceNotFoundException()
+ }
+ } else {
+ throw new DeviceNotFoundException()
+ }
+ }
+
+ // getDeviceCkbAppVersion will halt forever while in win32 sleep mode.
+ const ckbVersionRes = await Promise.race([
+ getDeviceCkbAppVersion(descriptor),
+ new Promise((_, reject) => {
+ setTimeout(() => reject(), 1000)
+ }),
+ ]).catch(() => {
+ return { status: ErrorCode.DeviceInSleep }
+ })
+
+ if (!isSuccessResponse(ckbVersionRes)) {
+ if (ckbVersionRes.status !== ErrorCode.DeviceInSleep) {
+ throw new CkbAppNotFoundException()
+ } else {
+ throw new DeviceNotFoundException()
+ }
+ }
+ setError(undefined)
+ return true
+ } catch (err) {
+ if (err instanceof CkbAppNotFoundException || err instanceof DeviceNotFoundException) {
+ setError(err.code)
+ }
+ return false
+ }
+ },
+ [isWin32]
+ )
+
+ const reconnect = useCallback(async () => {
+ if (!deviceInfo) return
+ setError(undefined)
+ setIsReconnecting(true)
+ try {
+ const res = await getDevices(deviceInfo)
+ if (isSuccessResponse(res) && Array.isArray(res.result) && res.result.length > 0) {
+ const [device] = res.result
+ setDeviceInfo(device)
+ if (device.descriptor !== deviceInfo.descriptor) {
+ await updateWallet({
+ id: wallet.id,
+ device,
+ })
+ }
+ await ensureDeviceAvailable(device)
+ } else {
+ setError(ErrorCode.DeviceNotFound)
+ }
+ } catch (err) {
+ setError(ErrorCode.DeviceNotFound)
+ } finally {
+ setIsReconnecting(false)
+ }
+ }, [deviceInfo, ensureDeviceAvailable, wallet.id])
+
+ const verifyDeviceStatus = useCallback(async () => {
+ if (deviceInfo) {
+ return ensureDeviceAvailable(deviceInfo)
+ }
+ return true
+ }, [ensureDeviceAvailable, deviceInfo])
+
+ const errorMessage = useMemo(() => {
+ switch (error) {
+ case ErrorCode.DeviceNotFound:
+ return t('hardware-verify-address.status.disconnect')
+ case ErrorCode.CkbAppNotFound:
+ return t(CkbAppNotFoundException.message)
+ default:
+ return error
+ }
+ }, [error, t])
+ return {
+ deviceInfo,
+ isReconnecting,
+ isNotAvailable,
+ reconnect,
+ verifyDeviceStatus,
+ errorMessage,
+ setError,
+ }
+}
diff --git a/packages/neuron-ui/src/components/CellManagement/index.tsx b/packages/neuron-ui/src/components/CellManagement/index.tsx
index 62769ac99a..bec2ca7096 100644
--- a/packages/neuron-ui/src/components/CellManagement/index.tsx
+++ b/packages/neuron-ui/src/components/CellManagement/index.tsx
@@ -1,5 +1,15 @@
import React, { useCallback, useMemo, useState } from 'react'
-import { Attention, Consume, Copy, DetailIcon, EyesClose, EyesOpen, LockCell, NewTab, UnLock } from 'widgets/Icons/icon'
+import {
+ Attention,
+ Consume,
+ DetailIcon,
+ EyesClose,
+ EyesOpen,
+ LockCell,
+ UnLock,
+ Consolidate,
+ Recycle,
+} from 'widgets/Icons/icon'
import PageContainer from 'components/PageContainer'
import { useTranslation } from 'react-i18next'
import Breadcrum from 'widgets/Breadcrum'
@@ -10,22 +20,26 @@ import {
shannonToCKBFormatter,
uniformTimeFormatter,
usePagination,
- isMainnet as isMainnetUtil,
- useCopy,
- clsx,
outPointToStr,
LockScriptCategory,
getLockTimestamp,
+ isMainnet as isMainnetUtil,
+ TypeScriptCategory,
} from 'utils'
import { HIDE_BALANCE } from 'utils/const'
import Tooltip from 'widgets/Tooltip'
import Dialog from 'widgets/Dialog'
-import Alert from 'widgets/Alert'
import ShowOrEditDesc from 'widgets/ShowOrEditDesc'
import { TFunction } from 'i18next'
import TextField from 'widgets/TextField'
import { useSearchParams } from 'react-router-dom'
-import { Actions, useAction, useLiveCells, usePassword, useSelect, useViewCell } from './hooks'
+import CellInfoDialog from 'components/CellInfoDialog'
+import { computeScriptHash } from '@ckb-lumos/lumos/utils'
+import Hardware from 'widgets/Icons/Hardware.png'
+import Button from 'widgets/Button'
+import Alert from 'widgets/Alert'
+import RecycleUDTCellDialog from 'components/RecycleUDTCellDialog'
+import { Actions, useAction, useHardWallet, useLiveCells, usePassword, useSelect } from './hooks'
import styles from './cellManagement.module.scss'
const getColumns = ({
@@ -181,31 +195,51 @@ const getColumns = ({
dataIndex: 'action',
title: t('cell-manage.table.head.action'),
render(_, index, item) {
- const { locked, lockedReason } = item
+ const { locked, lockedReason, lockScriptType, typeScriptType } = item
+ const showRecycleAction =
+ lockScriptType === LockScriptCategory.ANYONE_CAN_PAY &&
+ typeScriptType &&
+ [TypeScriptCategory.SUDT, TypeScriptCategory.XUDT].includes(typeScriptType)
return (
-
- {locked ? (
-
+
+
+
+ {showRecycleAction ? (
+
+
+
) : (
-
+
+ {locked ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
)}
-
)
},
@@ -216,14 +250,14 @@ const getColumns = ({
const CellManagement = () => {
const {
app: { epoch },
- wallet: { balance = '' },
+ wallet,
chain: {
- networkID,
syncState: { bestKnownBlockTimestamp },
+ networkID,
},
settings: { networks },
} = useGlobalState()
- const isMainnet = isMainnetUtil(networks, networkID)
+ const isMainnet = useMemo(() => isMainnetUtil(networks, networkID), [networks, networkID])
const [t] = useTranslation()
const [searchParams] = useSearchParams()
const breadPages = useMemo(() => [{ label: t('cell-manage.title') }], [t])
@@ -235,12 +269,25 @@ const CellManagement = () => {
direction: SortType.Decrease,
}
: undefined
- const { liveCells, updateLiveCell, onSorted, updateLiveCellsLockStatus } = useLiveCells({ initSortInfo })
+ const { liveCells, updateLiveCell, onSorted, updateLiveCellsLockStatus, fetchLiveCells } = useLiveCells({
+ initSortInfo,
+ })
const { pageNo, pageSize, onPageChange } = usePagination()
const currentPageLiveCells = useMemo(() => {
return liveCells.slice(pageSize * (pageNo - 1), pageSize * pageNo)
}, [pageNo, pageSize, liveCells])
const { onSelect, onSelectAll, isAllSelected, selectedOutPoints, hasSelectLocked, isAllLocked } = useSelect(liveCells)
+ const {
+ isReconnecting,
+ isNotAvailable,
+ reconnect,
+ verifyDeviceStatus,
+ errorMessage: hardwalletError,
+ setError: setHardwalletError,
+ } = useHardWallet({
+ wallet,
+ t,
+ })
const { password, error, onPasswordChange, setError, resetPassword } = usePassword()
const { action, operateCells, onActionCancel, onActionConfirm, onOpenActionDialog, onMultiAction, loading } =
useAction({
@@ -248,9 +295,11 @@ const CellManagement = () => {
currentPageLiveCells,
updateLiveCellsLockStatus,
selectedOutPoints,
- setError,
+ setError: wallet.device ? setHardwalletError : setError,
resetPassword,
password,
+ verifyDeviceStatus,
+ wallet,
})
const columns = useMemo(
() =>
@@ -277,11 +326,6 @@ const CellManagement = () => {
bestKnownBlockTimestamp,
]
)
- const { copied, onCopy, copyTimes } = useCopy()
- const { onViewDetail, rawData, rawLock, rawType, usedCapacity } = useViewCell({
- viewCell: operateCells[0],
- isMainnet,
- })
const totalCapacity = useMemo(
() => shannonToCKBFormatter(operateCells.reduce((pre, cur) => pre + BigInt(cur.capacity), BigInt(0)).toString()),
[operateCells]
@@ -295,7 +339,7 @@ const CellManagement = () => {
{showBalance ? : }
{t('cell-manage.wallet-balance')}
- {`${showBalance ? shannonToCKBFormatter(balance) : HIDE_BALANCE} CKB`}
+ {`${showBalance ? shannonToCKBFormatter(wallet.balance) : HIDE_BALANCE} CKB`}
}
@@ -322,6 +366,10 @@ const CellManagement = () => {
{t('cell-manage.consume')}
+
) : null}
@@ -332,96 +380,116 @@ const CellManagement = () => {
pageNo={pageNo}
onChange={onPageChange}
/>
-