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) + +--- + +[![Neuron@v0.117.0](https://github.com/user-attachments/assets/7d2eba67-e33e-4fca-a714-7ba1709d8bd3)](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) + +--- + +[![Neuron@v0.116.0](https://github.com/Magickbase/neuron-public-issues/assets/7271329/ec10aa01-47fe-47a3-9636-3d4e86fc6c9b)](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. + +[![Neuron@v0.114.2](https://github.com/nervosnetwork/neuron/assets/7271329/883aba6e-ceb0-402d-9de0-d46609c528b7)](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. + +[![Neuron@v0.114.0](https://github.com/nervosnetwork/neuron/assets/7271329/883aba6e-ceb0-402d-9de0-d46609c528b7)](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')}

+ + } + > +
+
+
+
+
+
+
{t('send.address')}
+
{toAddress}
+
+ + +
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+
+
+ +
+ ) +} + +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')}

+ + } + > +
+
+
+
+ {items + .filter(item => items.length === 1 || !item.isLastOutput) + .map(item => ( +
+
+
{t('send.address')}
+
{item.address}
+
+ + +
+ ))} +
+
+ +
+
+
+

{t('send.total-amount')}

+

{totalAmount}

+
+ + + +
+
+ +
+ +
+
+
+
+
+ + +
+ ) +} + +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 ( + + + + + + + + + + + + + + + + + + + + + +
KEYVALUE
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 ( + + {t('cell-manage.cell-detail-dialog.title')} +
+ {t('cell-manage.cell-detail-dialog.transaction-hash')} + :   + {truncateMiddle(output.outPoint.txHash, 10, 10)} + onCopy(output.outPoint.txHash)} /> + +
+
+ } + 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} /> - -
-
OutPoint.TxHash
-
-

{operateCells[0]?.outPoint.txHash}

-
- onCopy(operateCells[0]!.outPoint.txHash)} /> - -
+ isMainnet={isMainnet} + /> + {action === Actions.Recycle && operateCells[0] && ( + + )} + {wallet.device ? ( + +

+ {t(`cell-manage.cell-${action}-dialog.capacity`, { capacity: totalCapacity })} +

+
+ hard-wallet
-
-
-
Lock Script
-
{rawLock}
-
-
-
Type Script
-
{rawType}
-
-
-
{t('cell-manage.cell-detail-dialog.data')}
-
{rawData}
-
- {copied ? ( - - {t('common.copied')} - - ) : null} -
-
- {t('cell-manage.cell-detail-dialog.capacity-used')} -
- {t('cell-manage.cell-detail-dialog.total')} -  {shannonToCKBFormatter(operateCells[0]?.capacity ?? '')} CKB , - {t('cell-manage.cell-detail-dialog.used')} -  {usedCapacity} CKB -
-
-
-
+ {action === Actions.Lock ? ( + + + {t('cell-manage.cell-lock-dialog.locked-cell-can-not-use')} + + ) : null} +
+ +
-
-
+ {hardwalletError ? ( + + {hardwalletError} + + ) : null} + + ) : ( + +

+ {t(`cell-manage.cell-${action}-dialog.capacity`, { capacity: totalCapacity })} +

+ + {action === Actions.Lock ? ( + + + {t('cell-manage.cell-lock-dialog.locked-cell-can-not-use')} + + ) : null} +
+ )} -

- {t(`cell-manage.cell-${action}-dialog.capacity`, { capacity: totalCapacity })} -

- - {action === Actions.Lock ? ( - - - {t('cell-manage.cell-lock-dialog.locked-cell-can-not-use')} - - ) : null} + {t('cell-manage.cell-consume-dialog.warn-consume')}
- {t('cell-manage.cell-consume-dialog.warn-consume')} + {t('cell-manage.cell-consolidate-dialog.warn-consume')} ) diff --git a/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx b/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx index c719dabc31..b6a7ee707e 100644 --- a/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx +++ b/packages/neuron-ui/src/components/CompensationPeriodTooltip/index.tsx @@ -1,10 +1,9 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { getCompensatedTime, getCompensationPeriod, CONSTANTS, CompensationPeriod, uniformTimeFormatter } from 'utils' +import { getCompensatedTime, getCompensationPeriod, CompensationPeriod, uniformTimeFormatter } from 'utils' +import { WITHDRAW_EPOCHS } from 'utils/const' import styles from './compensationPeriodTooltip.module.scss' -const { WITHDRAW_EPOCHS } = CONSTANTS - const HOUR = 3_600_000 const HOURS_PER_EPOCH = 4 * HOUR const SECS_PER_DAY = 24 * HOUR diff --git a/packages/neuron-ui/src/components/DataSetting/dataSetting.module.scss b/packages/neuron-ui/src/components/DataSetting/dataSetting.module.scss index e260687f30..e90dea89ae 100644 --- a/packages/neuron-ui/src/components/DataSetting/dataSetting.module.scss +++ b/packages/neuron-ui/src/components/DataSetting/dataSetting.module.scss @@ -50,14 +50,16 @@ .itemBtn { color: var(--secondary-text-color); - padding: 0 16px; - flex-shrink: 0; + padding: 0 8px; border-radius: 8px; height: 100%; background: var(--input-disabled-color); border: 1px solid var(--divide-line-color); &:hover:not([disabled]) { - color: var(--primary-color); + g, + path { + stroke: var(--primary-color); + } } &[disabled] { cursor: not-allowed; @@ -160,3 +162,29 @@ word-break: normal; white-space: normal; } + +.moreTooltip { + height: 100%; +} + +.moreTip { + margin-top: 4px; + margin-left: 60px; +} + +.actions { + button { + background: none; + font-size: 14px; + line-height: 30px; + color: var(--main-text-color); + border: none; + text-align: left; + display: flex; + align-items: center; + cursor: pointer; + &:hover { + color: var(--primary-color); + } + } +} diff --git a/packages/neuron-ui/src/components/DataSetting/hooks.ts b/packages/neuron-ui/src/components/DataSetting/hooks.ts index e945e4823d..1df113502a 100644 --- a/packages/neuron-ui/src/components/DataSetting/hooks.ts +++ b/packages/neuron-ui/src/components/DataSetting/hooks.ts @@ -18,21 +18,27 @@ export const useDataPath = (network?: State.Network) => { } }) }, [network?.id]) - const onSetting = useCallback(() => { - invokeShowOpenDialog({ - buttonLabel: t('settings.data.set', { lng: navigator.language }), - properties: ['openDirectory', 'createDirectory', 'promptToCreate', 'treatPackageAsDirectory'], - }).then(res => { - if (isSuccessResponse(res) && !res.result?.canceled && res.result?.filePaths?.length) { - setCurrentPath(res.result?.filePaths?.[0]) - stopProcessMonitor(type).then(stopRes => { - if (isSuccessResponse(stopRes)) { - setIsDialogOpen(true) - } - }) - } - }) - }, [t, type]) + + const onSetting = useCallback( + (onSuccess?: (path: string) => void) => { + invokeShowOpenDialog({ + buttonLabel: t('settings.data.set', { lng: navigator.language }), + properties: ['openDirectory', 'createDirectory', 'promptToCreate', 'treatPackageAsDirectory'], + }).then(res => { + if (isSuccessResponse(res) && !res.result?.canceled && res.result?.filePaths?.length) { + const path = res.result?.filePaths?.[0] + setCurrentPath(path) + stopProcessMonitor(type).then(stopRes => { + if (isSuccessResponse(stopRes)) { + onSuccess?.(path) + } + }) + } + }) + }, + [t, type] + ) + const onCancel = useCallback(() => { startProcessMonitor(type).then(res => { if (isSuccessResponse(res)) { @@ -40,6 +46,12 @@ export const useDataPath = (network?: State.Network) => { } }) }, [setIsDialogOpen, type]) + + const onResync = useCallback(async () => { + await stopProcessMonitor(type) + return startProcessMonitor(type) + }, [type]) + const onConfirm = useCallback( (dataPath: string) => { setPrevPath(dataPath) @@ -47,6 +59,12 @@ export const useDataPath = (network?: State.Network) => { }, [setIsDialogOpen, setPrevPath] ) + + const openDialog = useCallback(() => { + setCurrentPath('') + setIsDialogOpen(true) + }, [setIsDialogOpen, setCurrentPath]) + return { prevPath, currentPath, @@ -54,6 +72,8 @@ export const useDataPath = (network?: State.Network) => { onCancel, onConfirm, isDialogOpen, + openDialog, + onResync, } } diff --git a/packages/neuron-ui/src/components/DataSetting/index.tsx b/packages/neuron-ui/src/components/DataSetting/index.tsx index 6c284eff8c..c9cbb8ecea 100644 --- a/packages/neuron-ui/src/components/DataSetting/index.tsx +++ b/packages/neuron-ui/src/components/DataSetting/index.tsx @@ -1,12 +1,15 @@ -import React, { useCallback, useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ClearCache from 'components/ClearCache' import { useDispatch, useState as useGlobalState } from 'states' import { shell } from 'electron' import Tooltip from 'widgets/Tooltip' import { NetworkType } from 'utils/const' -import { Attention } from 'widgets/Icons/icon' -import MigrateCkbDataDialog from 'widgets/MigrateCkbDataDialog' +import { Attention, More } from 'widgets/Icons/icon' +import Toast from 'widgets/Toast' +import ModifyPathDialog from 'components/ModifyPathDialog' +import AlertDialog from 'widgets/AlertDialog' +import { isSuccessResponse } from 'utils' import { useDataPath } from './hooks' import styles from './dataSetting.module.scss' @@ -28,9 +31,33 @@ const PathItem = ({ - + + {[ + { + label: 'settings.data.browse-local-files', + onClick: openPath, + }, + { + label: 'settings.data.modify-path', + onClick: handleClick, + }, + ].map(({ label, onClick }) => ( + + ))} + + } + trigger="click" + > + + ) } @@ -42,16 +69,31 @@ const DataSetting = () => { chain: { networkID }, settings: { networks = [] }, } = useGlobalState() + const [notice, setNotice] = useState('') + const [showLostDialog, setShowLostDialog] = useState(false) const network = useMemo(() => networks.find(n => n.id === networkID), [networkID, networks]) - const { onSetting, prevPath, currentPath, isDialogOpen, onCancel, onConfirm } = useDataPath(network) + const { isDialogOpen, openDialog, onSetting, prevPath, currentPath, onCancel, onConfirm, onResync } = + useDataPath(network) const openPath = useCallback(() => { - if (prevPath) { - shell.openPath(prevPath!) - } - }, [prevPath]) + shell.openPath(prevPath).then(res => { + if (res) { + setShowLostDialog(true) + } + }) + }, [prevPath, onResync]) const isLightClient = network?.type === NetworkType.Light const hiddenDataPath = isLightClient || !network?.readonly + + const handleResync = useCallback(() => { + setShowLostDialog(false) + onResync().then(res => { + if (isSuccessResponse(res)) { + openPath() + } + }) + }, [openPath]) + return ( <>
@@ -82,17 +124,36 @@ const DataSetting = () => {
- {hiddenDataPath ? null : } + {hiddenDataPath ? null : }
- + + {isDialogOpen && ( + + )} + + {showLostDialog && ( + setShowLostDialog(false)} + /> + )} + + setNotice('')} /> ) } diff --git a/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss b/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss index 821546c681..a410263179 100644 --- a/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss +++ b/packages/neuron-ui/src/components/DepositDialog/depositDialog.module.scss @@ -1,7 +1,11 @@ @import '../../styles/mixin.scss'; +.container { + width: 680px; +} + .slider { - margin-top: 16px; + margin-top: 10px; :global(.ms-Slider-thumb) { top: -8px; @@ -32,57 +36,72 @@ margin-top: 16px; } -.depositValueLabel { - line-height: 20px; - color: var(--secondary-text-color); - font-size: 14px; +.depositValueLabelWrap { + display: flex; + + .depositValueLabel { + line-height: 20px; + color: var(--secondary-text-color); + font-size: 14px; + } + .tooltip { + margin-left: 120px; + } + .tip { + width: 400px; + word-break: normal; + white-space: normal; + line-height: 180%; + } + + svg { + margin: 2px 4px 0; + cursor: pointer; + path { + fill: var(--primary-color); + } + } } .fee { - display: flex; - justify-content: space-between; font-size: 14px; line-height: 20px; + margin-top: 4px; color: var(--main-text-color); +} - & > div { - &:nth-last-child(1) { - width: 230px; - text-align: right; +.rewards { + padding: 6px 16px; + background: var(--tag-green-bg-color); + border-radius: 8px; + margin-top: 14px; + div { + display: flex; + justify-content: space-between; + color: var(--primary-color); + p { + margin: 0; + padding: 4px 0; + font-size: 14px; + line-height: 20px; } } } -.notice { - $lineHeight: 24px; +.acpContent { display: flex; - box-sizing: border-box; - margin-top: 22px; - border: 1px solid rgba(252, 136, 0, 0.2); - padding: 7px 54px; - border-radius: 4px; - background: #fff6eb; - color: #f68c2a; - font-weight: 500; - font-size: 12px; - line-height: $lineHeight; - text-align: center; - - & > svg { - $size: 14px; - flex-shrink: 0; - margin: calc(($lineHeight - $size) / 2) 4px 0 0; - width: $size; - height: $size; - - g[fill='#D50000'] { - fill: currentColor; - } + .tip { + min-width: 190px; + word-break: normal; + white-space: normal; + line-height: 180%; } - - @media (prefers-color-scheme: dark) { - border-color: #4b391c; - background: #4b391c; + svg { + margin: 2px 4px 0; + cursor: pointer; + path { + fill: var(--primary-color); + } } } @@ -90,7 +109,7 @@ font-weight: 500; font-size: 12px; background: inherit; - color: #f68c2a; + color: var(--primary-color); border: none; padding: 0; margin: 0; @@ -100,6 +119,7 @@ .isBalanceReserved { display: flex; align-items: center; + margin-top: 12px; & > input { display: none; @@ -111,6 +131,7 @@ gap: 8px; & > svg { + flex-shrink: 0; path { fill: #e5e5e5; @@ -133,3 +154,22 @@ } } } + +.notification { + color: var(--notice-text-color); + display: flex; + gap: 6px; + justify-content: center; + align-items: center; + word-break: break-word; + margin-top: 12px; + background-color: var(--warn-background-color); + font-size: 12px; + line-height: 24px; + padding: 8px 14px; + border: 1px solid rgba(252, 136, 0, 0.2); + border-radius: 4px; + span { + font-weight: 500; + } +} diff --git a/packages/neuron-ui/src/components/DepositDialog/hooks.ts b/packages/neuron-ui/src/components/DepositDialog/hooks.ts index 2a28144db6..82b93000aa 100644 --- a/packages/neuron-ui/src/components/DepositDialog/hooks.ts +++ b/packages/neuron-ui/src/components/DepositDialog/hooks.ts @@ -3,8 +3,11 @@ import { TFunction } from 'i18next' import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { + MultisigConfig, generateDaoDepositAllTx as generateDaoDepositAllTxAPI, generateDaoDepositTx as generateDaoDepositTxAPI, + generateMultisigDaoDepositTx as generateMultisigDaoDepositTxAPI, + generateMultisigDaoDepositAllTx as generateMultisigDaoDepositAllTxAPI, } from 'services/remote' import { AppActions, useDispatch } from 'states' import { @@ -17,6 +20,7 @@ import { useClearGeneratedTx, validateAmount, } from 'utils' +import getMultisigSignStatus from 'utils/getMultisigSignStatus' import { MAX_DECIMAL_DIGITS, MIN_DEPOSIT_AMOUNT, SHANNON_CKB_RATIO } from 'utils/const' const PERCENT_100 = 100 @@ -45,17 +49,26 @@ function generateDaoDepositTx({ capacity, suggestFeeRate, t, + multisigConfig, }: { walletID: string capacity: string suggestFeeRate: number t: TFunction + multisigConfig?: MultisigConfig }): Promise { - return generateDaoDepositTxAPI({ - feeRate: `${suggestFeeRate}`, - capacity, - walletID, - }).then(res => { + const generateCall = multisigConfig + ? generateMultisigDaoDepositTxAPI({ + feeRate: `${suggestFeeRate}`, + capacity, + multisigConfig, + }) + : generateDaoDepositTxAPI({ + walletID, + feeRate: `${suggestFeeRate}`, + capacity, + }) + return generateCall.then(res => { if (isSuccessResponse(res)) { return res.result } @@ -73,16 +86,25 @@ function generateDaoDepositAllTx({ suggestFeeRate, isBalanceReserved, walletID, + multisigConfig, }: { suggestFeeRate: number isBalanceReserved: boolean walletID: string + multisigConfig?: MultisigConfig }): Promise { - return generateDaoDepositAllTxAPI({ - walletID, - feeRate: `${suggestFeeRate}`, - isBalanceReserved, - }).then(res => { + const generateAllCall = multisigConfig + ? generateMultisigDaoDepositAllTxAPI({ + feeRate: `${suggestFeeRate}`, + isBalanceReserved, + multisigConfig, + }) + : generateDaoDepositAllTxAPI({ + walletID, + feeRate: `${suggestFeeRate}`, + isBalanceReserved, + }) + return generateAllCall.then(res => { if (isSuccessResponse(res)) { return res.result } @@ -97,6 +119,7 @@ export const useGenerateDaoDepositTx = ({ suggestFeeRate, showDepositDialog, slidePercent, + multisigConfig, }: { walletID: string isBalanceReserved: boolean @@ -104,6 +127,7 @@ export const useGenerateDaoDepositTx = ({ suggestFeeRate: number showDepositDialog: boolean slidePercent: number + multisigConfig?: MultisigConfig }) => { const timer = useRef>() const [errorMessage, setErrorMessage] = useState('') @@ -127,8 +151,14 @@ export const useGenerateDaoDepositTx = ({ } const generateDaoDepositResult: Promise = isDepositAll - ? generateDaoDepositAllTx({ walletID, isBalanceReserved, suggestFeeRate }) - : generateDaoDepositTx({ walletID, capacity: CKBToShannonFormatter(depositValue), suggestFeeRate, t }) + ? generateDaoDepositAllTx({ walletID, isBalanceReserved, suggestFeeRate, multisigConfig }) + : generateDaoDepositTx({ + walletID, + capacity: CKBToShannonFormatter(depositValue), + suggestFeeRate, + t, + multisigConfig, + }) generateDaoDepositResult .then(res => { dispatch({ @@ -136,7 +166,7 @@ export const useGenerateDaoDepositTx = ({ payload: res, }) if (isDepositAll) { - setMaxDepositValue(shannonToCKBFormatter(res?.outputs[0]?.capacity ?? '0', false, '')) + setMaxDepositValue(shannonToCKBFormatter(res?.outputs[0]?.capacity ?? '0', false, false)) if (!isBalanceReserved) { setErrorMessage(t('messages.remain-ckb-for-withdraw')) } @@ -181,7 +211,7 @@ export const useDepositValue = (balance: string, showDepositDialog: boolean) => const amount = shannonToCKBFormatter( ((BigInt(percent) * BigInt(balance)) / BigInt(PERCENT_100)).toString(), false, - '' + false ) setDepositValue(padFractionDigitsIfDecimal(amount, 8)) }, @@ -238,23 +268,39 @@ export const useBalanceReserved = () => { } export const useOnDepositDialogSubmit = ({ - onCloseDepositDialog, - walletID, + onDepositSuccess, + wallet, + multisigConfig, }: { - onCloseDepositDialog: () => void - walletID: string + onDepositSuccess: () => void + wallet: State.Wallet + multisigConfig?: MultisigConfig }) => { const dispatch = useDispatch() return useCallback(() => { - dispatch({ - type: AppActions.RequestPassword, - payload: { - walletID, - actionType: 'send', - }, - }) - onCloseDepositDialog() - }, [dispatch, walletID, onCloseDepositDialog]) + if (multisigConfig) { + const { canBroadcastAfterSign } = getMultisigSignStatus({ multisigConfig, addresses: wallet.addresses }) + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: wallet.id, + actionType: canBroadcastAfterSign ? 'send-from-multisig-need-one' : 'send-from-multisig', + multisigConfig, + onSuccess: onDepositSuccess, + title: 'password-request.verify-password', + }, + }) + } else { + dispatch({ + type: AppActions.RequestPassword, + payload: { + walletID: wallet.id, + actionType: 'send', + onSuccess: onDepositSuccess, + }, + }) + } + }, [dispatch, wallet.id, onDepositSuccess, multisigConfig]) } export const useOnDepositDialogCancel = ({ @@ -275,3 +321,36 @@ export const useOnDepositDialogCancel = ({ clearGeneratedTx() }, [dispatch, onCloseDepositDialog, resetDepositValue, clearGeneratedTx]) } + +export const useDepositRewards = ({ + depositValue, + maxDepositValue, + disabled, + globalAPC, +}: { + depositValue: string + maxDepositValue: string | null + disabled: boolean + globalAPC: number +}) => { + const [annualRewards, monthRewards] = useMemo(() => { + if (disabled) return ['0', '0'] + + const value = CKBToShannonFormatter( + (Number(maxDepositValue || depositValue) - MIN_DEPOSIT_AMOUNT).toFixed(MAX_DECIMAL_DIGITS).toString() + ) + + const dpc = globalAPC / 365 / 100 + + const mRewards = (Number(value) * dpc * 30).toFixed(0).toString() + + const rewerds = (Number(value) * dpc * 360).toFixed(0).toString() + + return [rewerds, mRewards] + }, [depositValue, maxDepositValue, disabled, globalAPC]) + + return { + annualRewards, + monthRewards, + } +} diff --git a/packages/neuron-ui/src/components/DepositDialog/index.tsx b/packages/neuron-ui/src/components/DepositDialog/index.tsx index fd4125033f..44305ee785 100644 --- a/packages/neuron-ui/src/components/DepositDialog/index.tsx +++ b/packages/neuron-ui/src/components/DepositDialog/index.tsx @@ -1,12 +1,15 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { Slider } from 'office-ui-fabric-react' import { Trans, useTranslation } from 'react-i18next' import TextField from 'widgets/TextField' import Spinner, { SpinnerSize } from 'widgets/Spinner' -import { openExternal } from 'services/remote' +import { openExternal, MultisigConfig } from 'services/remote' import { localNumberFormatter, shannonToCKBFormatter } from 'utils' +import getMultisigSignStatus from 'utils/getMultisigSignStatus' import { Attention, Success } from 'widgets/Icons/icon' import Dialog from 'widgets/Dialog' +import Tooltip from 'widgets/Tooltip' +import Alert from 'widgets/Alert' import styles from './depositDialog.module.scss' import { useBalanceReserved, @@ -14,6 +17,7 @@ import { useGenerateDaoDepositTx, useOnDepositDialogCancel, useOnDepositDialogSubmit, + useDepositRewards, } from './hooks' const NERVOS_DAO_RFC_URL = @@ -27,7 +31,10 @@ interface DepositDialogProps { isDepositing: boolean isTxGenerated: boolean suggestFeeRate: number - walletID: string + wallet: State.Wallet + globalAPC: number + onDepositSuccess: () => void + multisigConfig?: MultisigConfig } const RfcLink = React.memo(() => ( @@ -45,7 +52,7 @@ const RfcLink = React.memo(() => ( )) const DepositDialog = ({ - walletID, + wallet, balance, show, fee, @@ -53,23 +60,35 @@ const DepositDialog = ({ isDepositing, isTxGenerated, suggestFeeRate, + globalAPC, + onDepositSuccess, + multisigConfig, }: DepositDialogProps) => { - const [t] = useTranslation() + const [t, { language }] = useTranslation() const disabled = !isTxGenerated + const [isTyping, setIsTyping] = useState(false) const { isBalanceReserved, onIsBalanceReservedChange, setIsBalanceReserved } = useBalanceReserved() const { depositValue, onChangeDepositValue, slidePercent, onSliderChange, resetDepositValue } = useDepositValue( balance, show ) const { errorMessage, maxDepositValue } = useGenerateDaoDepositTx({ - walletID, + walletID: wallet.id, isBalanceReserved, depositValue, suggestFeeRate, showDepositDialog: show, slidePercent, + multisigConfig, }) - const onConfirm = useOnDepositDialogSubmit({ onCloseDepositDialog, walletID }) + + const canSign = useMemo(() => { + if (!multisigConfig) return true + const multisigSignStatus = getMultisigSignStatus({ multisigConfig, addresses: wallet.addresses }) + return multisigSignStatus.canSign + }, [multisigConfig, wallet.addresses]) + + const onConfirm = useOnDepositDialogSubmit({ onDepositSuccess, wallet, multisigConfig }) const onCancel = useOnDepositDialogCancel({ onCloseDepositDialog, resetDepositValue, setIsBalanceReserved }) const onSubmit = useCallback( (e: React.FormEvent) => { @@ -81,6 +100,24 @@ const DepositDialog = ({ }, [disabled, onConfirm] ) + const { annualRewards, monthRewards } = useDepositRewards({ + depositValue, + maxDepositValue, + disabled, + globalAPC, + }) + + const isChinese = language === 'zh' || language.startsWith('zh-') + + const handleBlur = useCallback(() => { + setIsTyping(false) + }, [setIsTyping]) + + const handleFocus = useCallback(() => { + setIsTyping(true) + }, [setIsTyping]) + + const inputValue = useMemo(() => maxDepositValue ?? depositValue, [maxDepositValue, depositValue]) return ( {isDepositing ? ( ) : (
- +
+ + + ]} /> +
+ } + > + + +
+
+ {t('nervos-dao.fee')} + {`${shannonToCKBFormatter(fee)}`} +
+
+
+
- {t('nervos-dao.fee')} - {`${shannonToCKBFormatter(fee)}`} +

{t(`nervos-dao.estimated-rewards`, { days: 30 })}

+

{shannonToCKBFormatter(monthRewards)} CKB

+
+
+

{t(`nervos-dao.estimated-rewards`, { days: 360 })}

+

{shannonToCKBFormatter(annualRewards)} CKB

-
- -
-
- ]} /> +
+ {t(`nervos-dao.estimated-apc`)} + {isChinese ? null : ( + {t(`nervos-dao.estimated-apc-tooltip`)}

} + > + +
+ )} +
+

{globalAPC}%

+ + {t('nervos-dao.attention')} + )}
diff --git a/packages/neuron-ui/src/components/DepositRulesDialog/depositRulesDialog.module.scss b/packages/neuron-ui/src/components/DepositRulesDialog/depositRulesDialog.module.scss new file mode 100644 index 0000000000..22455d7c0d --- /dev/null +++ b/packages/neuron-ui/src/components/DepositRulesDialog/depositRulesDialog.module.scss @@ -0,0 +1,28 @@ +@import '../../styles/mixin.scss'; + +.container { + width: 680px; + + .content { + background: var(--table-head-background-color); + border-radius: 8px; + + .item { + padding: 16px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--divide-line-color); + font-size: 14px; + color: var(--main-text-color); + &:last-child { + border-bottom: none; + } + p { + margin: 0; + } + .description { + font-weight: 500; + } + } + } +} diff --git a/packages/neuron-ui/src/components/DepositRulesDialog/index.tsx b/packages/neuron-ui/src/components/DepositRulesDialog/index.tsx new file mode 100644 index 0000000000..b934091fbe --- /dev/null +++ b/packages/neuron-ui/src/components/DepositRulesDialog/index.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import Dialog from 'widgets/Dialog' +import styles from './depositRulesDialog.module.scss' + +const DepositRulesDialog = ({ show, onClose }: { show: boolean; onClose: () => void }) => { + const [t] = useTranslation() + + return ( + +
+ {[ + // TODO: calculating by the capacity of the lock + ['deposit-rules.minimum-deposit', '102 CKB'], + ['deposit-rules.single-compensation-cycle', 'deposit-rules.single-compensation-cycle-description'], + ['deposit-rules.withdraw', 'deposit-rules.withdraw-description'], + ['deposit-rules.unlock', 'deposit-rules.unlock-description'], + ].map(([title, description]) => ( +
+

{t(title)}

+

{t(description)}

+
+ ))} +
+
+ ) +} + +DepositRulesDialog.displayName = 'DepositRulesDialog' + +export default DepositRulesDialog diff --git a/packages/neuron-ui/src/components/ErrorBoundary/index.tsx b/packages/neuron-ui/src/components/ErrorBoundary/index.tsx index bed246d94c..e707dc65b0 100644 --- a/packages/neuron-ui/src/components/ErrorBoundary/index.tsx +++ b/packages/neuron-ui/src/components/ErrorBoundary/index.tsx @@ -5,7 +5,7 @@ import { handleViewError } from 'services/remote' const handleError = (error: Error) => { handleViewError(error.toString()) - if (process.env.NODE_ENV !== 'development') { + if (import.meta.env.MODE !== 'development') { window.location.reload() } return { hasError: true } diff --git a/packages/neuron-ui/src/components/FormattedTokenAmount/formattedTokenAmount.module.scss b/packages/neuron-ui/src/components/FormattedTokenAmount/formattedTokenAmount.module.scss index d82b115329..74cd2d5637 100644 --- a/packages/neuron-ui/src/components/FormattedTokenAmount/formattedTokenAmount.module.scss +++ b/packages/neuron-ui/src/components/FormattedTokenAmount/formattedTokenAmount.module.scss @@ -1,3 +1,8 @@ -.isReceive { - color: var(--primary-color); +.amount { + &[data-direction='receive'] { + color: var(--primary-color); + } + &[data-direction='send'] { + color: var(--error-color); + } } diff --git a/packages/neuron-ui/src/components/FormattedTokenAmount/index.tsx b/packages/neuron-ui/src/components/FormattedTokenAmount/index.tsx index 63d82b449b..97580d8da1 100644 --- a/packages/neuron-ui/src/components/FormattedTokenAmount/index.tsx +++ b/packages/neuron-ui/src/components/FormattedTokenAmount/index.tsx @@ -8,18 +8,20 @@ import { HIDE_BALANCE } from 'utils/const' import styles from './formattedTokenAmount.module.scss' -type FormattedTokenAmountProps = { item: State.Transaction; show: boolean; symbolClassName?: string } +type FormattedTokenAmountProps = { item: State.Transaction; show: boolean; symbolClassName?: string; symbol?: string } type AmountProps = Omit & { - sudtAmount: string + sudtAmount?: string isReceive: boolean - amount: string + amount?: string symbolClassName?: string } -const Amount = ({ sudtAmount, show, item, isReceive, amount, symbolClassName }: AmountProps) => { +const Amount = ({ sudtAmount, show, item, isReceive, amount, symbolClassName, symbol }: AmountProps) => { return sudtAmount ? ( -
- {show ? `${!sudtAmount.includes('-') ? '+' : ''}${sudtAmount}` : HIDE_BALANCE}  +
+ + {show ? `${isReceive ? '+' : ''}${sudtAmount}` : HIDE_BALANCE}  +
) : ( - {amount} +
+ + {amount ?? '--'} + +  {amount ? symbol : ''} +
) } export const FormattedTokenAmount = ({ item, show, symbolClassName }: FormattedTokenAmountProps) => { - let amount = '--' + let amount: string | undefined let sudtAmount = '' - let copyText = amount + let copyText: string | undefined = amount let isReceive = false + let symbol = '' if (item.blockNumber !== undefined) { if (item.nftInfo) { @@ -43,20 +51,55 @@ export const FormattedTokenAmount = ({ item, show, symbolClassName }: FormattedT const { type, data } = item.nftInfo amount = show ? `${type === 'receive' ? '+' : '-'}${nftFormatter(data)}` : `${HIDE_BALANCE}mNFT` copyText = amount + symbol = amount.includes('mNFT') ? 'mNFT' : '' + amount = amount.replace('mNFT', '') isReceive = type === 'receive' } else if (item.sudtInfo?.sUDT) { if (item.sudtInfo.sUDT.decimal) { sudtAmount = sUDTAmountFormatter(sudtValueToAmount(item.sudtInfo.amount, item.sudtInfo.sUDT.decimal)) copyText = `${sudtValueToAmount(item.sudtInfo.amount, item.sudtInfo.sUDT.decimal)} ${item.sudtInfo.sUDT.symbol}` + if (!sudtAmount.includes('-') && ['destroy', 'send'].includes(item.type)) { + sudtAmount = `-${sudtAmount}` + } + isReceive = !sudtAmount.includes('-') } } else { - amount = show ? `${shannonToCKBFormatter(item.value, true)} CKB` : `${HIDE_BALANCE} CKB` - isReceive = !amount.includes('-') - copyText = amount + amount = show + ? `${shannonToCKBFormatter(item.nervosDao ? item.daoCapacity ?? '--' : item.value, true)}` + : `${HIDE_BALANCE}` + isReceive = !amount?.includes('-') + copyText = `${amount} CKB` + symbol = 'CKB' + if (item.nervosDao && item.daoCapacity === undefined) { + amount = undefined + copyText = undefined + } } } - const props = { sudtAmount, show, item, isReceive, amount, symbolClassName } + const props = { sudtAmount, show, item, isReceive, amount, symbolClassName, symbol } + + return show && copyText ? ( + + + + ) : ( + + ) +} + +export const FormattedCKBBalanceChange = ({ item, show, symbolClassName }: FormattedTokenAmountProps) => { + let amount = '--' + let copyText = amount + let isReceive = false + + if (item.blockNumber !== undefined) { + amount = show ? `${shannonToCKBFormatter(item.value, true)}` : `${HIDE_BALANCE}` + isReceive = !amount.includes('-') + copyText = amount + } + + const props = { show, item, isReceive, amount, symbolClassName } return show ? ( diff --git a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts new file mode 100644 index 0000000000..fe2e933eec --- /dev/null +++ b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/hooks.ts @@ -0,0 +1,113 @@ +import { TFunction } from 'i18next' +import { useCallback, useState } from 'react' +import { verifyLockWindowPassword } from 'services/remote' +import { updateLockWindowInfo, useDispatch } from 'states' +import { isSuccessResponse } from 'utils' + +export const passwordLength = 4 + +export const useOldPassword = ({ t }: { t: TFunction }) => { + const [oldPasswordErr, setOldPasswordErr] = useState('') + const [oldPassword, setOldPassword] = useState(new Array(passwordLength).fill('')) + const [verifySuccess, setVerifySuccess] = useState(false) + const onUpdateOldPassword = useCallback( + (v: string, idx: number) => { + const updatedOldPassword = oldPassword.toSpliced(idx, 1, v).join('') + if (updatedOldPassword.length === passwordLength) { + verifyLockWindowPassword(updatedOldPassword) + .then(res => { + if (isSuccessResponse(res)) { + // verify success + setVerifySuccess(true) + } + throw new Error('verify failed') + }) + .catch(() => { + setOldPassword(new Array(passwordLength).fill('')) + setOldPasswordErr(t('settings.general.lock-window.password-error')) + }) + } else { + setOldPassword(value => value.toSpliced(idx, 1, v)) + } + }, + [oldPassword] + ) + const resetOldPassword = useCallback(() => { + setOldPasswordErr('') + setOldPassword(new Array(passwordLength).fill('')) + setVerifySuccess(false) + }, []) + return { + oldPasswordErr, + oldPassword, + onUpdateOldPassword, + verifySuccess, + setVerifySuccess, + resetOldPassword, + } +} + +export const usePassword = () => { + const [password, setPassword] = useState(new Array(passwordLength).fill('')) + const onUpdatePassword = useCallback((v: string, idx: number) => { + setPassword(value => value.toSpliced(idx, 1, v)) + }, []) + const resetPassword = useCallback(() => { + setPassword(new Array(passwordLength).fill('')) + }, []) + return { + password, + onUpdatePassword, + resetPassword, + } +} + +export const useRepeatPassword = ({ + password, + t, + encryptedPassword, + onCancel, +}: { + password: string + t: TFunction + encryptedPassword?: string + onCancel: (success: boolean) => void +}) => { + const dispatch = useDispatch() + const [errMsg, setErrMsg] = useState('') + const [repeatPassword, setRepeatPassword] = useState(new Array(passwordLength).fill('')) + const [isSuccess, setIsSuccess] = useState(false) + const onUpdateRepeatPassword = useCallback( + (v: string, idx: number) => { + const updatedRepeatPassword = repeatPassword.toSpliced(idx, 1, v).join('') + if (updatedRepeatPassword.length === passwordLength) { + if (updatedRepeatPassword !== password) { + setErrMsg(t('settings.general.lock-window.different-password')) + setRepeatPassword(new Array(passwordLength).fill('')) + } else { + setIsSuccess(true) + updateLockWindowInfo( + encryptedPassword ? { password: updatedRepeatPassword } : { password: updatedRepeatPassword, locked: true } + )(dispatch) + onCancel(true) + } + } else { + setErrMsg('') + setRepeatPassword(value => value.toSpliced(idx, 1, v)) + } + }, + [password, t, repeatPassword] + ) + const resetRepeatPassword = useCallback(() => { + setErrMsg('') + setRepeatPassword(new Array(passwordLength).fill('')) + setIsSuccess(false) + }, []) + return { + errMsg, + repeatPassword, + onUpdateRepeatPassword, + resetRepeatPassword, + isSuccess, + } +} diff --git a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx new file mode 100644 index 0000000000..c9f0226413 --- /dev/null +++ b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/index.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useEffect, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import Dialog from 'widgets/Dialog' +import SplitPasswordInput from 'widgets/SplitPasswordInput' +import Alert from 'widgets/Alert' +import Button from 'widgets/Button' +import { passwordLength, useOldPassword, usePassword, useRepeatPassword } from './hooks' +import styles from './lockWindowDialog.module.scss' + +const LockWindowDialog = ({ + show, + onCancel, + encryptedPassword, +}: { + show: boolean + onCancel: (success?: boolean) => void + encryptedPassword?: string +}) => { + const [t] = useTranslation() + const { oldPasswordErr, verifySuccess, oldPassword, onUpdateOldPassword, resetOldPassword } = useOldPassword({ t }) + const { password, onUpdatePassword, resetPassword } = usePassword() + const joinedPassword = useMemo(() => password.join(''), [password]) + const { errMsg, repeatPassword, onUpdateRepeatPassword, resetRepeatPassword, isSuccess } = useRepeatPassword({ + t, + password: joinedPassword, + encryptedPassword, + onCancel, + }) + useEffect(() => { + // when dialog open, reset all status + if (show) { + resetOldPassword() + resetPassword() + resetRepeatPassword() + } + }, [show, resetOldPassword, resetPassword, resetRepeatPassword]) + const onReset = useCallback(() => { + resetPassword() + resetRepeatPassword() + }, [resetPassword, resetRepeatPassword]) + const content = () => { + if (encryptedPassword && !verifySuccess) { + // is verify old password + return ( + <> + {t('settings.general.lock-window.enter-current-password')} +
+ +
{oldPasswordErr}
+
+ + ) + } + if (joinedPassword.length !== passwordLength) { + // enter password + return ( + <> + {t(`settings.general.lock-window.set-password`)} +
+ +
+ + ) + } + // is verify repeat password + return ( + <> + {t(`settings.general.lock-window.confirm-password`)} +
+ +
{errMsg}
+ {errMsg ? ( + + ) : null} +
+ + ) + } + + return ( + <> + +
{content()}
+
+ {isSuccess ? ( + + {t(`settings.general.lock-window.${encryptedPassword ? 'change-password-success' : 'set-password-success'}`)} + + ) : null} + + ) +} + +LockWindowDialog.displayName = 'LockWindowDialog' +export default LockWindowDialog diff --git a/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/lockWindowDialog.module.scss b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/lockWindowDialog.module.scss new file mode 100644 index 0000000000..f4c099a097 --- /dev/null +++ b/packages/neuron-ui/src/components/GeneralSetting/LockWindowDialog/lockWindowDialog.module.scss @@ -0,0 +1,30 @@ +@import '../../../styles/mixin.scss'; + +.content { + text-align: center; + + .secTitle { + color: var(--secondary-text-color); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + } + + .password { + margin-top: 24px; + } + + .err { + color: var(--error-color); + margin: 16px 0 24px 0; + } +} + +.dialog { + width: 680px; +} + +.notice { + @include dialog-copy-animation; +} diff --git a/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss b/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss index e55142e967..39881944b7 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss +++ b/packages/neuron-ui/src/components/GeneralSetting/generalSetting.module.scss @@ -4,14 +4,19 @@ $action-button-width: 11.25rem; .container { display: flex; + flex-wrap: wrap; width: 100%; + gap: 16px; .content { - width: 176px; + min-width: 176px; padding: 16px; background: var(--input-disabled-color); border-radius: 8px; - margin-right: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + flex-shrink: 0; p { font-size: 14px; @@ -19,7 +24,7 @@ $action-button-width: 11.25rem; margin: 0 0 12px; color: var(--main-text-color); } - button { + button[data-button-type='text'] { font-size: 14px; line-height: 20px; border: none; @@ -33,6 +38,11 @@ $action-button-width: 11.25rem; margin-right: 4px; } } + + &.lockWindow { + width: auto; + min-width: 176px; + } } .showVersion { position: relative; diff --git a/packages/neuron-ui/src/components/GeneralSetting/index.tsx b/packages/neuron-ui/src/components/GeneralSetting/index.tsx index 7866c4eaaa..8f77e68c4d 100644 --- a/packages/neuron-ui/src/components/GeneralSetting/index.tsx +++ b/packages/neuron-ui/src/components/GeneralSetting/index.tsx @@ -4,14 +4,17 @@ import { useSearchParams } from 'react-router-dom' import Dialog from 'widgets/Dialog' import LanguageDialog from 'components/LanguageDialog' import AlertDialog from 'widgets/AlertDialog' -import { ReactComponent as VersionLogo } from 'widgets/Icons/VersionLogo.svg' -import { ReactComponent as ArrowNext } from 'widgets/Icons/ArrowNext.svg' -import { ReactComponent as Update } from 'widgets/Icons/Update.svg' +import VersionLogo from 'widgets/Icons/VersionLogo.svg?react' +import ArrowNext from 'widgets/Icons/ArrowNext.svg?react' +import Update from 'widgets/Icons/Update.svg?react' import { cancelCheckUpdates, downloadUpdate, installUpdate, getVersion } from 'services/remote' -import { uniformTimeFormatter, bytesFormatter, clsx } from 'utils' -import { LanguageSelect } from 'widgets/Icons/icon' +import { uniformTimeFormatter, bytesFormatter, clsx, wakeScreen, releaseWakeLock } from 'utils' +import Switch from 'widgets/Switch' +import { keepScreenAwake } from 'services/localCache' +import { LanguageSelect, UnLock } from 'widgets/Icons/icon' import styles from './generalSetting.module.scss' import { useCheckUpdate, useUpdateDownloadStatus } from './hooks' +import LockWindowDialog from './LockWindowDialog' interface UpdateDownloadStatusProps { show: boolean @@ -94,6 +97,7 @@ const UpdateDownloadStatus = ({ } if (downloaded) { + const isWin = process.platform === 'win32' return ( { +const GeneralSetting = ({ updater, app }: GeneralSettingProps) => { const [t, i18n] = useTranslation() const [showLangDialog, setShowLangDialog] = useState(false) + const [isLockDialogShow, setIsLockDialogShow] = useState(false) const [searchParams] = useSearchParams() const [errorMsg, setErrorMsg] = useState('') const { showCheckDialog, setShowCheckDialog, onCancelCheckUpdates } = useCheckUpdate() @@ -173,13 +179,29 @@ const GeneralSetting = ({ updater }: GeneralSettingProps) => { } }, [updater.errorMsg, setErrorMsg]) + const [isScreenKeepAwake, setIsScreenKeepAwake] = useState(keepScreenAwake.get()) + const onChangeScreenKeepAwake = useCallback((v: boolean) => { + keepScreenAwake.save(v) + setIsScreenKeepAwake(v) + if (v) { + wakeScreen() + } else { + releaseWakeLock() + } + }, []) + return (

{t('settings.general.version')} v{newVersion || currentVersion}

- @@ -189,6 +211,7 @@ const GeneralSetting = ({ updater }: GeneralSettingProps) => {

{t('settings.general.language')}

+
+

{t('settings.general.keep-awake')}

+ +
+ +
+

{t('settings.general.lock-password')}

+ +
{ setShowLangDialog(false) }} /> + + { + setIsLockDialogShow(false) + }} + />
) } diff --git a/packages/neuron-ui/src/components/HDWalletSign/index.tsx b/packages/neuron-ui/src/components/HDWalletSign/index.tsx index 9b8a70c99b..feb81838d0 100644 --- a/packages/neuron-ui/src/components/HDWalletSign/index.tsx +++ b/packages/neuron-ui/src/components/HDWalletSign/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { ckbCore } from 'services/chain' -import { shannonToCKBFormatter, useDidMount, isSuccessResponse, CONSTANTS } from 'utils' +import { scriptToAddress, shannonToCKBFormatter, useDidMount, isSuccessResponse, CONSTANTS } from 'utils' import { getSystemCodeHash, getAllNetworks, getCurrentNetworkID } from 'services/remote' import CopyZone from 'widgets/CopyZone' import { DeviceSignIndex as DeviceSignIndexSubject } from 'services/subjects' @@ -13,7 +12,6 @@ const { MAINNET_CLIENT_LIST } = CONSTANTS const HDWalletSign = ({ tx }: { tx: State.DetailedTransaction }) => { const [t] = useTranslation() const [isMainnet, setIsMainnet] = useState(false) - const addressPrefix = isMainnet ? ckbCore.utils.AddressPrefix.Mainnet : ckbCore.utils.AddressPrefix.Testnet const [systemCodeHash, setSystemCodeHash] = useState('') const [inputVisible, setInputVisible] = useState(true) const [activeInputIndex, setActiveInputIndex] = useState(0) @@ -24,7 +22,6 @@ const HDWalletSign = ({ tx }: { tx: State.DetailedTransaction }) => { setSystemCodeHash(res.result) } }) - Promise.all([getAllNetworks(), getCurrentNetworkID()]) .then(([networksRes, idRes]) => { if (isSuccessResponse(networksRes) && isSuccessResponse(idRes)) { @@ -55,23 +52,10 @@ const HDWalletSign = ({ tx }: { tx: State.DetailedTransaction }) => { address = t('transaction.cell-from-cellbase') } else { try { - if (cell.lock.codeHash === systemCodeHash && cell.lock.hashType === 'type') { - address = ckbCore.utils.bech32Address(cell.lock.args, { - prefix: addressPrefix, - type: ckbCore.utils.AddressType.HashIdx, - codeHashOrCodeHashIndex: '0x00', - }) - } else { - address = ckbCore.utils.fullPayloadToAddress({ - args: cell.lock.args, - prefix: addressPrefix, - type: - cell.lock.hashType === 'data' - ? ckbCore.utils.AddressType.DataCodeHash - : ckbCore.utils.AddressType.TypeCodeHash, - codeHash: cell.lock.codeHash, - }) - } + address = scriptToAddress(cell.lock, { + isMainnet, + deprecated: cell.lock.codeHash === systemCodeHash && cell.lock.hashType === 'type', + }) } catch (err) { console.error(err) } @@ -106,7 +90,7 @@ const HDWalletSign = ({ tx }: { tx: State.DetailedTransaction }) => { ) }), - [t, addressPrefix, systemCodeHash] + [t, isMainnet, systemCodeHash] ) const inputBody = useMemo(() => { diff --git a/packages/neuron-ui/src/components/HardwareSign/hooks.ts b/packages/neuron-ui/src/components/HardwareSign/hooks.ts index cec9567a50..2ccc38400b 100644 --- a/packages/neuron-ui/src/components/HardwareSign/hooks.ts +++ b/packages/neuron-ui/src/components/HardwareSign/hooks.ts @@ -85,6 +85,8 @@ export default ({ switch (offlineSignJSON?.type) { case OfflineSignType.CreateSUDTAccount: return 'create-sudt-account' + case OfflineSignType.SendAcpSUDTToNewCell: + return 'send-acp-sudt-to-new-cell' case OfflineSignType.SendSUDT: return 'send-sudt' case OfflineSignType.UnlockDAO: diff --git a/packages/neuron-ui/src/components/HardwareSign/success.tsx b/packages/neuron-ui/src/components/HardwareSign/success.tsx index f971cd973f..5d30993eff 100644 --- a/packages/neuron-ui/src/components/HardwareSign/success.tsx +++ b/packages/neuron-ui/src/components/HardwareSign/success.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { RoutePath } from 'utils' import Button from 'widgets/Button' -import { ReactComponent as CompleteIcon } from 'widgets/Icons/Complete.svg' +import CompleteIcon from 'widgets/Icons/Complete.svg?react' import styles from './hardwareSign.module.scss' diff --git a/packages/neuron-ui/src/components/History/RowExtend.tsx b/packages/neuron-ui/src/components/History/RowExtend.tsx index 800557d238..4a6c995223 100644 --- a/packages/neuron-ui/src/components/History/RowExtend.tsx +++ b/packages/neuron-ui/src/components/History/RowExtend.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { showPageNotice, useDispatch } from 'states' import { openExternal } from 'services/remote' @@ -9,6 +9,10 @@ import { ExplorerIcon, Copy, DetailIcon } from 'widgets/Icons/icon' import { useTranslation } from 'react-i18next' import ShowOrEditDesc from 'widgets/ShowOrEditDesc' import Tooltip from 'widgets/Tooltip' +import AmendPendingTransactionDialog from 'components/AmendPendingTransactionDialog' +import { getTransaction as getOnChainTransaction } from 'services/chain' + +import Button from 'widgets/Button' import styles from './history.module.scss' type RowExtendProps = { @@ -17,12 +21,15 @@ type RowExtendProps = { isMainnet: boolean bestBlockNumber: number id: string + isWatchOnly?: boolean } -const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber }: RowExtendProps) => { +const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber, isWatchOnly }: RowExtendProps) => { const dispatch = useDispatch() const navigate = useNavigate() const [t] = useTranslation() + const [amendabled, setAmendabled] = useState(false) + const [amendPendingTx, setAmendPendingTx] = useState() const { onChangeEditStatus, onSubmitDescription } = useLocalDescription('transaction', id, dispatch) @@ -39,6 +46,19 @@ const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber }: RowExten navigate(`${RoutePath.History}/${btn.dataset.hash}`) break } + case 'amend': { + if (column.type === 'send' && !column.nftInfo && !column.nervosDao) { + if (column?.sudtInfo) { + navigate(`${RoutePath.History}/amendSUDTSend/${btn.dataset.hash}`) + } else { + navigate(`${RoutePath.History}/amend/${btn.dataset.hash}`) + } + } else { + setAmendPendingTx(column) + } + + break + } default: { // ignore } @@ -48,76 +68,112 @@ const RowExtend = ({ column, columns, isMainnet, id, bestBlockNumber }: RowExten [isMainnet] ) - const { blockNumber, hash, description } = column - const confirmations = blockNumber ? 1 + bestBlockNumber - +blockNumber : 0 - const confirmationsLabel = confirmations > 1000 ? '1,000+' : localNumberFormatter(confirmations) + const { blockNumber, hash, description, status } = column + const confirmations = bestBlockNumber && blockNumber ? 1 + bestBlockNumber - +blockNumber : null + const confirmationsLabel = + // eslint-disable-next-line no-nested-ternary + confirmations === null || confirmations < 0 + ? '--' + : confirmations > 1000 + ? '1,000+' + : localNumberFormatter(confirmations) const onCopy = useCallback(() => { window.navigator.clipboard.writeText(hash) showPageNotice('common.copied')(dispatch) }, [hash, dispatch]) + useEffect(() => { + setAmendabled(false) + if (status !== 'success' && column.type !== 'receive' && !isWatchOnly) { + getOnChainTransaction(hash).then(tx => { + const { minReplaceFee } = tx + if (minReplaceFee) { + setAmendabled(true) + } + }) + } + }, [status, hash, setAmendabled]) + + const onCloseAmendDialog = useCallback(() => { + setAmendPendingTx(undefined) + }, [setAmendPendingTx]) + return ( - - -
-
-
-
{t('history.confirmationTimes')}
-
{confirmationsLabel}
+ <> + + +
+
+
+
{t('history.confirmationTimes')}
+
{confirmationsLabel}
+
+
+
{t('history.description')}
+ + } + showTriangle + isTriggerNextToChild + > +
{description || t('addresses.default-description')}
+
+
-
{t('history.description')}
- - } - showTriangle - isTriggerNextToChild - > -
{description || t('addresses.default-description')}
-
+
{t('history.transaction-hash')}
+
+ {hash} + +
-
+
+
+ + +
-
-
{t('history.transaction-hash')}
-
- {hash} - + {amendabled ? ( + + ) : null}
-
- - -
-
- - + + + {amendPendingTx ? : null} + ) } export default RowExtend diff --git a/packages/neuron-ui/src/components/History/history.module.scss b/packages/neuron-ui/src/components/History/history.module.scss index c389de4ac8..70fa69db33 100644 --- a/packages/neuron-ui/src/components/History/history.module.scss +++ b/packages/neuron-ui/src/components/History/history.module.scss @@ -1,5 +1,4 @@ @import '../../styles/mixin.scss'; -@import '../../styles/theme.scss'; @import '../../styles/animation.scss'; $history-info-title-color: #999; @@ -88,11 +87,16 @@ body { .infoBox { display: flex; + .infoBlock { + margin-bottom: 0; + } } .infoBlock { + display: flex; + margin-bottom: 20px; &Title { - margin-bottom: 8px; + margin-right: 12px; color: $history-info-title-color; } @@ -104,7 +108,7 @@ body { } .descText { - padding-bottom: 16px; + padding-bottom: 20px; word-break: break-all; cursor: pointer; &:hover { @@ -130,7 +134,14 @@ body { .infoOperationBox { display: flex; + justify-content: space-between; margin-top: 20px; + + div { + display: flex; + gap: 20px; + flex: 1; + } gap: 20px; .explorerNavButton, @@ -152,6 +163,16 @@ body { } } } + + .amendButton { + float: rights; + padding: 4px 16px; + font-size: 14px; + height: auto; + width: auto; + min-width: 0; + border-radius: 4px; + } } } } @@ -202,7 +223,7 @@ body { } .isReceive { - color: $main-color; + color: var(--primary-color); } .tokenName { diff --git a/packages/neuron-ui/src/components/History/hooks.ts b/packages/neuron-ui/src/components/History/hooks.ts index 94ba80e0e7..326b8ab8b5 100644 --- a/packages/neuron-ui/src/components/History/hooks.ts +++ b/packages/neuron-ui/src/components/History/hooks.ts @@ -1,9 +1,12 @@ import { useState, useEffect } from 'react' import { updateTransactionList } from 'states/stateProvider/actionCreators/transactions' -import { listParams, backToTop } from 'utils' +import { NeuronWalletActions } from 'states/stateProvider/reducer' +import { listParams, backToTop, isSuccessResponse } from 'utils' +import { getSUDTAccountList } from 'services/remote' export const useSearch = (search: string, walletID: string, dispatch: React.Dispatch) => { const [keywords, setKeywords] = useState('') + const [sortInfo, setSortInfo] = useState({ sort: '', direction: '' }) const onKeywordsChange = (_e?: React.FormEvent, newValue?: string) => { if (undefined !== newValue) { @@ -15,9 +18,25 @@ export const useSearch = (search: string, walletID: string, dispatch: React.Disp backToTop() const params = listParams(search) setKeywords(params.keywords) + setSortInfo({ sort: params.sort, direction: params.direction }) updateTransactionList({ ...params, keywords: params.keywords, walletID })(dispatch) + + getSUDTAccountList({ walletID }) + .then(res => { + if (isSuccessResponse(res)) { + return res.result + } + throw new Error(res.message.toString()) + }) + .then((list: Controller.GetSUDTAccountList.Response) => { + dispatch({ + type: NeuronWalletActions.GetSUDTAccountList, + payload: list, + }) + }) + .catch((err: Error) => console.error(err)) }, [search, walletID, dispatch]) - return { keywords, onKeywordsChange, setKeywords } + return { keywords, onKeywordsChange, setKeywords, sortInfo } } export default { diff --git a/packages/neuron-ui/src/components/History/index.tsx b/packages/neuron-ui/src/components/History/index.tsx index 38c96c5429..5000029bcd 100644 --- a/packages/neuron-ui/src/components/History/index.tsx +++ b/packages/neuron-ui/src/components/History/index.tsx @@ -10,11 +10,11 @@ import { Download, Search, ArrowNext, Clean } from 'widgets/Icons/icon' import PageContainer from 'components/PageContainer' import TransactionStatusWrap from 'components/TransactionStatusWrap' -import FormattedTokenAmount from 'components/FormattedTokenAmount' +import FormattedTokenAmount, { FormattedCKBBalanceChange } from 'components/FormattedTokenAmount' import { useState as useGlobalState, useDispatch } from 'states' import { exportTransactions } from 'services/remote' -import { ReactComponent as CKBAvatar } from 'widgets/Icons/Nervos.svg' +import CKBAvatar from 'widgets/Icons/Nervos.svg?react' import { RoutePath, isMainnet as isMainnetUtil, uniformTimeFormatter } from 'utils' import { onEnter } from 'utils/inputDevice' @@ -30,7 +30,7 @@ import styles from './history.module.scss' const History = () => { const { app: { pageNotice }, - wallet: { id, name: walletName }, + wallet: { id, name: walletName, isWatchOnly }, chain: { networkID, syncState: { cacheTipBlockNumber, bestKnownBlockNumber }, @@ -45,8 +45,11 @@ const History = () => { const [isExporting, setIsExporting] = useState(false) const isMainnet = isMainnetUtil(networks, networkID) - const { keywords, onKeywordsChange } = useSearch(search, id, dispatch) - const onSearch = useCallback(() => navigate(`${RoutePath.History}?keywords=${keywords}`), [navigate, keywords]) + const { keywords, onKeywordsChange, sortInfo } = useSearch(search, id, dispatch) + const onSearch = useCallback( + () => navigate(`${RoutePath.History}?keywords=${keywords}&sort=${sortInfo.sort}&direction=${sortInfo.direction}`), + [navigate, keywords] + ) const onClean = useCallback(() => onKeywordsChange(undefined, ''), [onKeywordsChange]) const onExport = useCallback(() => { setIsExporting(true) @@ -109,7 +112,7 @@ const History = () => { title: t('history.table.type'), dataIndex: 'type', align: 'left', - minWidth: '120px', + minWidth: '100px', render: (_, __, item) => { return ( { /> ) }, + sortable: true, }, { - title: t('history.table.amount'), + title: t('history.table.asset'), dataIndex: 'amount', align: 'left', isBalance: true, - minWidth: '200px', + minWidth: '140px', render(_, __, item, show) { return }, }, + { + title: t('history.table.balance'), + dataIndex: 'value', + align: 'left', + isBalance: true, + minWidth: '140px', + render(_, __, item, show) { + return + }, + sortable: true, + }, { title: t('history.table.timestamp'), dataIndex: 'timestamp', align: 'left', minWidth: '150px', render: (_, __, item) => uniformTimeFormatter(item.timestamp), + sortable: true, }, { title: t('history.table.status'), @@ -212,12 +228,15 @@ const History = () => { isMainnet={isMainnet} id={id} bestBlockNumber={bestBlockNumber} + isWatchOnly={isWatchOnly} /> )} expandedRow={expandedRow} onRowClick={(_, __, idx) => handleExpandClick(idx)} + onSorted={(key, type) => { + navigate(`${RoutePath.History}?pageNo=${pageNo}&keywords=${keywords}&sort=${key}&direction=${type}`) + }} /> -
{ pageSize={pageSize} pageNo={pageNo} onChange={(no: number) => { - navigate(`${RoutePath.History}?pageNo=${no}&keywords=${keywords}`) + navigate( + `${RoutePath.History}?pageNo=${no}&keywords=${keywords}&sort=${sortInfo.sort}&direction=${sortInfo.direction}` + ) }} />
diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss b/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss index c3242948b3..066467f75b 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss +++ b/packages/neuron-ui/src/components/HistoryDetailPage/historyDetailPage.module.scss @@ -1,26 +1,36 @@ @import '../../styles/mixin.scss'; -.basicInfoWrap { +.tx { + @include card; margin-bottom: 16px; +} - .basicInfoTitle { - font-weight: 500; - font-size: 16px; +.basicInfoWrap { + .basicInfoItemBox { + display: flex; + font-size: 14px; padding: 16px; - } - .basicInfoItemWrap { - .basicInfoItemBox { - display: flex; - font-size: 14px; - padding: 16px; - - .infoItemLabel { - min-width: 125px; - } + .infoItemLabel { + min-width: 125px; } - .basicInfoMiddleWrap { - display: flex; + } + .twoColumns { + display: flex; + } + .txHash { + flex: 1; + border: 1px solid var(--table-head-border-color); + border-left: none; + border-right: none; + } + .flexItem { + display: flex; + align-items: center; + + & > svg { + margin-left: 8px; + cursor: pointer; } } } @@ -81,22 +91,6 @@ } } -.income { - display: flex; - align-items: center; - - .incomeCopy { - height: 20px; - line-height: 20px; - cursor: pointer; - } - - & > svg { - margin-left: 8px; - cursor: pointer; - } -} - .amount { height: 32px; line-height: 32px; @@ -115,3 +109,20 @@ .scriptTag:hover { color: var(--primary-color); } + +.cellInfo { + display: flex; + align-items: center; + border: none; + background: transparent; + cursor: pointer; + &:hover { + & > span { + color: var(--primary-color); + } + } + & > span { + color: var(--main-text-color); + margin-right: 4px; + } +} diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts b/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts new file mode 100644 index 0000000000..f8c8d65832 --- /dev/null +++ b/packages/neuron-ui/src/components/HistoryDetailPage/hooks.ts @@ -0,0 +1,32 @@ +import { TFunction } from 'i18next' +import { useCallback, useState } from 'react' + +export const TabId = { + Basic: 'Basic', + Topology: 'Topology', +} + +export const useTxTabs = ({ t }: { t: TFunction }) => { + const tabs = [ + { id: TabId.Basic, label: t('send-tx-detail.basic-info') }, + { id: TabId.Topology, label: t('send-tx-detail.topology') }, + ] + const [currentTab, setCurrentTab] = useState(tabs[0]) + return { + currentTab, + setCurrentTab, + tabs, + } +} + +export const useCellInfoDialog = () => { + const [outputCell, setOutputCell] = useState() + const onCancel = useCallback(() => { + setOutputCell(undefined) + }, []) + return { + outputCell, + setOutputCell, + onCancel, + } +} diff --git a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx index 5dbc8515e4..8db87ebf76 100644 --- a/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx +++ b/packages/neuron-ui/src/components/HistoryDetailPage/index.tsx @@ -1,30 +1,38 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { scriptToAddress } from '@nervosnetwork/ckb-sdk-utils' -import { getTransaction } from 'services/remote' +import { calculateUnlockDaoMaximumWithdraw, getTransaction } from 'services/remote' import { showPageNotice, transactionState, useDispatch, useState as useGlobalState } from 'states' +import { type CKBComponents } from '@ckb-lumos/lumos/rpc' import PageContainer from 'components/PageContainer' import LockInfoDialog from 'components/LockInfoDialog' import ScriptTag from 'components/ScriptTag' import AlertDialog from 'widgets/AlertDialog' import Tabs from 'widgets/Tabs' -import Table from 'widgets/Table' +import Table, { TableProps } from 'widgets/Table' import CopyZone from 'widgets/CopyZone' -import { BalanceHide, BalanceShow, Copy } from 'widgets/Icons/icon' +import { ArrowNext, BalanceHide, BalanceShow, Copy } from 'widgets/Icons/icon' import Tooltip from 'widgets/Tooltip' import Breadcrum from 'widgets/Breadcrum' import { ErrorCode, + scriptToAddress, localNumberFormatter, uniformTimeFormatter, shannonToCKBFormatter, isSuccessResponse, isMainnet as isMainnetUtil, + calculateFee, } from 'utils' -import { HIDE_BALANCE } from 'utils/const' +import { CONFIRMATION_THRESHOLD, HIDE_BALANCE } from 'utils/const' +import TxTopology from 'components/SendTxDetail/TxTopology' +import FormattedTokenAmount, { FormattedCKBBalanceChange } from 'components/FormattedTokenAmount' +import TransactionStatusWrap from 'components/TransactionStatusWrap' +import CellInfoDialog from 'components/CellInfoDialog' +import TransactionType from 'components/TransactionType' +import { TabId, useCellInfoDialog, useTxTabs } from './hooks' import styles from './historyDetailPage.module.scss' type InputOrOutputType = (State.DetailedInput | State.DetailedOutput) & { idx: number } @@ -36,18 +44,147 @@ const InfoItem = ({ label, value, className }: { label: string; value: React.Rea
) +const BasicInfo = ({ + transaction, + cacheTipBlockNumber, + bestKnownBlockNumber, + txFee, +}: { + transaction: State.DetailedTransaction + cacheTipBlockNumber: number + bestKnownBlockNumber: number + txFee: string +}) => { + const [t] = useTranslation() + const dispatch = useDispatch() + const onCopy = useCallback(() => { + window.navigator.clipboard.writeText(transaction.hash) + showPageNotice('common.copied')(dispatch) + }, [transaction.hash, dispatch]) + const [isAmountShow, setIsAmountShow] = useState(true) + const onChangeAmountShow = useCallback(() => { + setIsAmountShow(v => !v) + }, []) + const [isBalanceShow, setIsBalanceShow] = useState(true) + const onChangeBalanceShow = useCallback(() => { + setIsBalanceShow(v => !v) + }, []) + const bestBlockNumber = Math.max(cacheTipBlockNumber, bestKnownBlockNumber) + const confirmationCount = 1 + bestBlockNumber - +transaction.blockNumber + const status = + transaction.status === 'success' && confirmationCount < CONFIRMATION_THRESHOLD ? 'confirming' : transaction.status + + const infos = { + hash: { + label: t('transaction.transaction-hash'), + value: ( +
+ {transaction.hash} + +
+ ), + }, + blockNumber: { + label: t('transaction.block-number'), + value: transaction.blockNumber ? localNumberFormatter(transaction.blockNumber) : 'none', + }, + time: { + label: t('transaction.date'), + value: +(transaction.timestamp || transaction.createdAt) + ? uniformTimeFormatter(+(transaction.timestamp || transaction.createdAt)) + : 'none', + }, + type: { + label: t('transaction.type'), + value: ( + + ), + }, + fee: { + label: t('transaction.fee'), + value: `${shannonToCKBFormatter(txFee)} CKB`, + }, + amount: { + label: t('transaction.assets'), + value: ( +
+ + {isAmountShow ? : } +
+ ), + }, + balance: { + label: t('transaction.balance'), + value: ( +
+ + {isBalanceShow ? ( + + ) : ( + + )} +
+ ), + }, + status: { + label: t('transaction.status'), + value: , + }, + size: { + label: t('transaction.size'), + value: `${transaction.size} Bytes`, + }, + cycles: { + label: t('transaction.cycles'), + value: transaction.cycles ? +transaction.cycles : '--', + }, + } + return ( +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ) +} + const HistoryDetailPage = () => { const { hash } = useParams() const navigate = useNavigate() const { app: { pageNotice }, - chain: { networkID }, + chain: { + networkID, + syncState: { cacheTipBlockNumber, bestKnownBlockNumber }, + transactions: { items = [] }, + }, settings: { networks }, wallet: currentWallet, } = useGlobalState() const isMainnet = isMainnetUtil(networks, networkID) const [t] = useTranslation() const [transaction, setTransaction] = useState(transactionState) + const [daoMaximumWithdraw, setDaoMaximumWithdraw] = useState() const [error, setError] = useState({ code: '', message: '' }) const [failedMessage, setFailedMessage] = useState('') const [lockInfo, setLockInfo] = useState(null) @@ -61,7 +198,8 @@ const HistoryDetailPage = () => { getTransaction({ hash, walletID: currentWallet.id }) .then(res => { if (isSuccessResponse(res)) { - setTransaction(res.result) + const tx = items.find(v => v.hash === hash) + setTransaction({ ...tx, ...res.result, nervosDao: tx?.nervosDao ?? res.result.nervosDao }) } else { setFailedMessage(t(`messages.codes.${ErrorCode.FieldNotFound}`, { fieldName: 'transaction' })) } @@ -75,57 +213,15 @@ const HistoryDetailPage = () => { } }, [t, hash, currentWallet]) - const dispatch = useDispatch() - const onCopy = useCallback(() => { - window.navigator.clipboard.writeText(transaction.hash) - showPageNotice('common.copied')(dispatch) - }, [transaction.hash, dispatch]) - const [isIncomeShow, setIsIncomeShow] = useState(true) - const onChangeIncomeShow = useCallback(() => { - setIsIncomeShow(v => !v) - }, []) - - const infos = [ - { - label: t('transaction.transaction-hash'), - value: ( -
- {transaction.hash} - -
- ), - }, - { - label: t('transaction.block-number'), - value: transaction.blockNumber ? localNumberFormatter(transaction.blockNumber) : 'none', - }, - { - label: t('transaction.date'), - value: +(transaction.timestamp || transaction.createdAt) - ? uniformTimeFormatter(+(transaction.timestamp || transaction.createdAt)) - : 'none', - }, - { - label: t('transaction.income'), - value: isIncomeShow ? ( -
- - {`${shannonToCKBFormatter(transaction.value)} CKB`} - - -
- ) : ( -
- {`${HIDE_BALANCE} CKB`} - -
- ), - }, - ] + useEffect(() => { + if (hash) { + calculateUnlockDaoMaximumWithdraw(hash).then(res => { + if (isSuccessResponse(res) && res.result) { + setDaoMaximumWithdraw(res.result) + } + }) + } + }, [hash]) const inputsTitle = useMemo( () => `${t('transaction.inputs')} (${transaction.inputs.length}/${localNumberFormatter(transaction.inputsCount)})`, @@ -159,7 +255,7 @@ const HistoryDetailPage = () => { address = t('transaction.cell-from-cellbase') } else { try { - address = scriptToAddress(cell.lock, isMainnet) + address = scriptToAddress(cell.lock, { isMainnet }) } catch (err) { console.error(err) } @@ -180,19 +276,11 @@ const HistoryDetailPage = () => { ) } - const columns: { - title: string - dataIndex: string - isBalance?: boolean - render?: (v: any, idx: number, item: InputOrOutputType, showBalance: boolean) => React.ReactNode - width?: string - align?: 'left' | 'right' | 'center' - className?: string - }[] = [ + const columns: TableProps['columns'] = [ { title: t('transaction.index'), dataIndex: 'idx', - width: '90px', + width: '60px', render(_, __, item) { return <>{item.idx} }, @@ -201,7 +289,7 @@ const HistoryDetailPage = () => { title: t('transaction.address'), dataIndex: 'type', align: 'left', - width: '580px', + width: '560px', render: (_, __, item) => { const { address } = handleListData(item) return ( @@ -245,8 +333,39 @@ const HistoryDetailPage = () => { }, ] + const { setOutputCell, outputCell, onCancel } = useCellInfoDialog() + + const cellInfoColumn: TableProps['columns'][number] = { + title: '', + dataIndex: 'cellInfo', + align: 'left', + width: '100px', + render(_, __, item) { + return ( + + ) + }, + } + const breadPages = useMemo(() => [{ label: t('history.title-detail') }], [t]) + const { tabs: txTabs, setCurrentTab: setTxCurrentTab, currentTab: currentTxTab } = useTxTabs({ t }) + const txFee = useMemo(() => { + if (daoMaximumWithdraw) { + return ( + BigInt(daoMaximumWithdraw) - + transaction.outputs.reduce( + (result: bigint, output: { capacity: string }) => result + BigInt(output.capacity), + BigInt(0) + ) + ).toString() + } + return calculateFee(transaction) + }, [transaction, daoMaximumWithdraw]) + return ( { @@ -256,16 +375,25 @@ const HistoryDetailPage = () => { head={} notice={pageNotice} > -
-
{t('history.basic-information')}
-
- -
- - -
- -
+
+ + {currentTxTab.id === TabId.Basic ? ( + + ) : ( + + )}
{ activeColumnClassName={styles.active} /> } - columns={columns} + columns={currentTab.id === tabs[0].id ? columns : [...columns, cellInfoColumn]} dataSource={currentTab.id === tabs[0].id ? inputsData : outputsData} noDataContent={t('overview.no-recent-activities')} hasHoverTrBg={false} @@ -295,6 +423,7 @@ const HistoryDetailPage = () => { type="failed" onCancel={() => navigate(-1)} /> + ) } diff --git a/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss b/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss new file mode 100644 index 0000000000..1162951ae5 --- /dev/null +++ b/packages/neuron-ui/src/components/ImportFailureDialog/importFailureDialog.module.scss @@ -0,0 +1,23 @@ +@import '../../styles/mixin.scss'; + +.container { + width: 680px; + + .content { + max-width: 60%; + text-align: center; + margin: 0 auto; + font-size: 14px; + line-height: 24px; + color: var(--main-text-color); + + button { + border: none; + background: none; + color: var(--primary-color); + text-decoration: underline; + text-underline-offset: 4px; + cursor: pointer; + } + } +} diff --git a/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx b/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx new file mode 100644 index 0000000000..b67b984db5 --- /dev/null +++ b/packages/neuron-ui/src/components/ImportFailureDialog/index.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Dialog from 'widgets/Dialog' +import { openExternal } from 'services/remote' +import styles from './importFailureDialog.module.scss' + +const ImportFailureDialog = ({ show, onClose }: { show: boolean; onClose: () => void }) => { + const [t] = useTranslation() + + const onBtnClick = useCallback(() => { + openExternal( + 'https://github.com/nervosnetwork/ckb-cli/wiki/Import-ckb-cli-keystore-from%26to-Neuron-wallet#ckb-cli-and-neuron-use-the-keystore-in-different-way' + ) + }, []) + + return ( + +
+

+ {t('import-keystore.import-failure-msg')} + +

+
+
+ ) +} + +ImportFailureDialog.displayName = 'ImportFailureDialog' + +export default ImportFailureDialog diff --git a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx index 8cc8d31190..afee339b6d 100644 --- a/packages/neuron-ui/src/components/ImportHardware/confirming.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/confirming.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' -import { ReactComponent as PendingIcon } from 'widgets/Icons/Pending.svg' +import PendingIcon from 'widgets/Icons/Pending.svg?react' import { getDeviceExtendedPublickey } from 'services/remote' import { isSuccessResponse } from 'utils' import { ImportStep, ActionType } from './common' diff --git a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx index 008b1ba8cf..5998c2dc1d 100644 --- a/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/detect-device.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' -import { getDevices, getDeviceFirmwareVersion, getDeviceCkbAppVersion, connectDevice } from 'services/remote' +import { getDevices, getDeviceCkbAppVersion, connectDevice } from 'services/remote' import { isSuccessResponse, errorFormatter, useDidMount } from 'utils' -import { ReactComponent as SuccessInfo } from 'widgets/Icons/SuccessInfo.svg' +import SuccessInfo from 'widgets/Icons/SuccessInfo.svg?react' import { Error as ErrorIcon } from 'widgets/Icons/icon' import Spinner from 'widgets/Spinner' import { @@ -50,7 +50,6 @@ const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch { setError('') @@ -76,10 +75,6 @@ const DetectDevice = ({ dispatch, model }: { dispatch: React.Dispatch{productName} {errorMsg ? : null} {scanning ? : null} - {firmwareVersion && !errorMsg && !scanning ? ( - - ) : null} {appVersion ? : null}
diff --git a/packages/neuron-ui/src/components/ImportHardware/import-error.tsx b/packages/neuron-ui/src/components/ImportHardware/import-error.tsx index 6363f4a4ca..9f11eb7ffa 100644 --- a/packages/neuron-ui/src/components/ImportHardware/import-error.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/import-error.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' import CopyZone from 'widgets/CopyZone' -import { ReactComponent as FailedInfo } from 'widgets/Icons/FailedInfo.svg' +import FailedInfo from 'widgets/Icons/FailedInfo.svg?react' import { errorFormatter } from 'utils' import { ActionType, ImportHardwareState, ImportStep } from './common' diff --git a/packages/neuron-ui/src/components/ImportHardware/import-success.tsx b/packages/neuron-ui/src/components/ImportHardware/import-success.tsx index b34d0781c6..84a55ca6e6 100644 --- a/packages/neuron-ui/src/components/ImportHardware/import-success.tsx +++ b/packages/neuron-ui/src/components/ImportHardware/import-success.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Button from 'widgets/Button' -import { ReactComponent as CompleteIcon } from 'widgets/Icons/Complete.svg' +import CompleteIcon from 'widgets/Icons/Complete.svg?react' import { ImportStep, ActionType } from './common' import styles from './findDevice.module.scss' diff --git a/packages/neuron-ui/src/components/ImportKeystore/index.tsx b/packages/neuron-ui/src/components/ImportKeystore/index.tsx index e3b71e74ac..3e7f211fd1 100644 --- a/packages/neuron-ui/src/components/ImportKeystore/index.tsx +++ b/packages/neuron-ui/src/components/ImportKeystore/index.tsx @@ -19,6 +19,7 @@ import ReplaceDuplicateWalletDialog, { useReplaceDuplicateWallet } from 'compone import { FinishCreateLoading, CreateFirstWalletNav } from 'components/WalletWizard' import TextField from 'widgets/TextField' import { importedWalletDialogShown } from 'services/localCache' +import ImportFailureDialog from '../ImportFailureDialog' import styles from './importKeystore.module.scss' const { MAX_WALLET_NAME_LENGTH, MAX_PASSWORD_LENGTH } = CONSTANTS @@ -49,6 +50,7 @@ const ImportKeystore = () => { const navigate = useNavigate() const [fields, setFields] = useState(defaultFields) const [openingFile, setOpeningFile] = useState(false) + const [isImportFailureDialogShow, setIsImportFailureDialogShow] = useState(false) const { onImportingExitingWalletError, dialogProps } = useReplaceDuplicateWallet() const goBack = useGoBack() @@ -122,6 +124,11 @@ const ImportKeystore = () => { return } + if (res.status === ErrorCode.UnsupportedCkbCliKeystore) { + setIsImportFailureDialogShow(true) + return + } + if (res.message) { const msg = typeof res.message === 'string' ? res.message : res.message.content || '' if (msg) { @@ -140,7 +147,18 @@ const ImportKeystore = () => { closeDialog() }) }, - [fields.name, fields.password, fields.path, navigate, openDialog, closeDialog, disabled, setFields, t] + [ + fields.name, + fields.password, + fields.path, + navigate, + openDialog, + closeDialog, + disabled, + setFields, + t, + setIsImportFailureDialogShow, + ] ) const handleChange = useCallback( @@ -199,6 +217,10 @@ const ImportKeystore = () => { [setFields, wallets, t] ) + const onCloseImportFailureDialog = useCallback(() => { + setIsImportFailureDialogShow(false) + }, [setIsImportFailureDialogShow]) + return ( <>
@@ -248,6 +270,8 @@ const ImportKeystore = () => { + + ) } diff --git a/packages/neuron-ui/src/components/LanguageDialog/index.tsx b/packages/neuron-ui/src/components/LanguageDialog/index.tsx index d8d61f6266..d11fa901fa 100644 --- a/packages/neuron-ui/src/components/LanguageDialog/index.tsx +++ b/packages/neuron-ui/src/components/LanguageDialog/index.tsx @@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next' import Dialog from 'widgets/Dialog' import { setLocale } from 'services/remote' import { CONSTANTS } from 'utils' -import { ReactComponent as Arrow } from 'widgets/Icons/Arrow.svg' -import { ReactComponent as Select } from 'widgets/Icons/Select.svg' +import Arrow from 'widgets/Icons/Arrow.svg?react' +import Select from 'widgets/Icons/Select.svg?react' import Button from 'widgets/Button' import styles from './languageDialog.module.scss' diff --git a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx index 3f21882da3..2242042484 100644 --- a/packages/neuron-ui/src/components/LockInfoDialog/index.tsx +++ b/packages/neuron-ui/src/components/LockInfoDialog/index.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next' import React, { useRef } from 'react' -import { bech32Address, AddressPrefix } from '@nervosnetwork/ckb-sdk-utils' +import { type CKBComponents } from '@ckb-lumos/lumos/rpc' import Dialog from 'widgets/Dialog' -import { useCopy, useDialog } from 'utils' +import { useCopy, useDialog, scriptToAddress } from 'utils' import { Copy } from 'widgets/Icons/icon' import Alert from 'widgets/Alert' import getLockSupportShortAddress from '../../utils/getLockSupportShortAddress' @@ -35,10 +35,7 @@ const ShortAddr = ({ return null } - const shortAddr = bech32Address(lockScript.args, { - prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, - codeHashOrCodeHashIndex: lock.CodeHashIndex, - }) + const shortAddr = scriptToAddress(lockScript, { isMainnet, deprecated: true }) return ( <> diff --git a/packages/neuron-ui/src/components/ModifyPathDialog/index.tsx b/packages/neuron-ui/src/components/ModifyPathDialog/index.tsx new file mode 100644 index 0000000000..f8c5f5a87a --- /dev/null +++ b/packages/neuron-ui/src/components/ModifyPathDialog/index.tsx @@ -0,0 +1,135 @@ +import React, { useState, useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import Dialog from 'widgets/Dialog' +import AlertDialog from 'widgets/AlertDialog' +import MigrateCkbDataDialog from 'widgets/MigrateCkbDataDialog' +import { setCkbNodeDataPath, getCkbNodeDataNeedSize } from 'services/remote' +import { Attention } from 'widgets/Icons/icon' +import { isSuccessResponse } from 'utils' +import styles from './modifyPathDialog.module.scss' + +const ModifyPathDialog = ({ + prevPath, + currentPath, + onCancel, + onConfirm, + onSetting, + setNotice, +}: { + onCancel?: () => void + prevPath: string + currentPath: string + onConfirm: (dataPath: string) => void + onSetting: (onSuccess?: (path: string) => void) => void + setNotice: (notice: string) => void +}) => { + const [t] = useTranslation() + const [isMigrateOpen, setIsMigrateOpen] = useState(false) + const [failureMessage, setFailureMessage] = useState('') + const [isRetainPreviousData, setIsRetainPreviousData] = useState(false) + const [needSize, setNeedSize] = useState(0) + + useEffect(() => { + getCkbNodeDataNeedSize().then(res => { + if (isSuccessResponse(res)) { + setNeedSize(res.result!) + } + }) + }, []) + + const handleResync = useCallback(async () => { + setFailureMessage('') + onSetting(path => { + setCkbNodeDataPath({ + dataPath: path, + clearCache: true, + }).then(res => { + if (isSuccessResponse(res)) { + onConfirm(path) + } else { + setFailureMessage(typeof res.message === 'string' ? res.message : res.message.content!) + } + }) + }) + }, [onSetting, onConfirm, setFailureMessage]) + + if (isMigrateOpen) { + return ( + setIsMigrateOpen(false)} + onConfirm={onConfirm} + onClose={onCancel} + setNotice={setNotice} + /> + ) + } + + if (isRetainPreviousData) { + return ( + setIsRetainPreviousData(false)} + onConfirm={() => setIsMigrateOpen(true)} + confirmText={t('wizard.next')} + className={styles.dialog} + disabled={!currentPath} + onClose={onCancel} + > +
+
+ + {t('settings.data.modify-path-notice', { needSize })} +
+ +
+

{currentPath || 'path/to/ckb_node_data'}

+ +
+
+
+ ) + } + + return ( + <> + setIsRetainPreviousData(true)} + confirmProps={{ type: 'dashed' }} + cancelText={t('settings.data.resync')} + onCancel={handleResync} + cancelProps={{ type: 'dashed' }} + > +
+
+ + {t('settings.data.modify-path-notice', { needSize })} +
+

{t('settings.data.modify-path-content')}

+
+
+ setFailureMessage('')} + /> + + ) +} + +ModifyPathDialog.displayName = 'ModifyPathDialog' + +export default ModifyPathDialog diff --git a/packages/neuron-ui/src/components/ModifyPathDialog/modifyPathDialog.module.scss b/packages/neuron-ui/src/components/ModifyPathDialog/modifyPathDialog.module.scss new file mode 100644 index 0000000000..5c75df38ce --- /dev/null +++ b/packages/neuron-ui/src/components/ModifyPathDialog/modifyPathDialog.module.scss @@ -0,0 +1,85 @@ +@import '../../styles/mixin.scss'; + +.passwordInput { + margin-top: 16px; +} + +.dialog { + width: 700px; +} + +.tip { + color: var(--warn-text-color); + background: var(--warn-background-color); + margin: -20px -16px 0; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + font-size: 12px; + gap: 4px; + font-weight: 500; + border-bottom: 1px solid var(--warn-border-color); +} + +.label { + font-weight: 500; + color: var(--main-text-color); + font-size: 14px; +} + +.copy { + display: flex; + align-items: center; + margin-left: 6px; +} + +.notice { + @include dialog-copy-animation; +} + +.modifyTip { + max-width: 450px; + margin: 0 auto; + text-align: center; + margin-top: 16px; + font-size: 14px; + line-height: 24px; + color: var(--main-text-color); +} + +.pathItem { + display: flex; + align-items: center; + justify-content: space-between; + width: 488px; + height: 40px; + margin: 24px auto 0; + background: var(--secondary-background-color); + border: 1px solid var(--divide-line-color); + border-radius: 8px; + button { + height: 100%; + border-radius: 0 8px 8px 0; + min-width: 60px; + border: none; + border-left: 1px solid var(--divide-line-color); + background: var(--input-disabled-color); + cursor: pointer; + color: var(--secondary-text-color); + &:hover { + color: var(--primary-color); + } + } + .path { + color: var(--input-hint-color); + } + p { + padding: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding: 0 16px; + color: var(--disable-button-text-color); + } +} diff --git a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts index a672164efd..55dcb2b2d6 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/hooks.ts +++ b/packages/neuron-ui/src/components/MultisigAddress/hooks.ts @@ -1,5 +1,5 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react' -import { isSuccessResponse, getMultisigAddress, DefaultLockInfo } from 'utils' +import { isSuccessResponse, getMultisigAddress, DefaultLockInfo, addressToScript, scriptToAddress } from 'utils' import { MultisigOutputUpdate } from 'services/subjects' import { MultisigConfig, @@ -15,7 +15,7 @@ import { OfflineSignJSON, getMultisigSyncProgress, } from 'services/remote' -import { addressToScript, scriptToAddress, scriptToHash } from '@nervosnetwork/ckb-sdk-utils' +import { computeScriptHash } from '@ckb-lumos/lumos/utils' export const useSearch = (clearSelected: () => void, onFilterConfig: (searchKey: string) => void) => { const [keywords, setKeywords] = useState('') @@ -79,16 +79,21 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM } }) }, [setEntities]) - const updateConfig = useCallback( + const onUpdateConfig = useCallback((values: Partial & { id: number }) => { + return updateMultisigConfig(values).then(res => { + if (isSuccessResponse(res)) { + setEntities(v => v.map(config => (res.result && config.id === res.result?.id ? res.result : config))) + } else { + throw new Error(typeof res.message === 'string' ? res.message : res.message.content!) + } + }) + }, []) + const onUpdateConfigAlias = useCallback( (id: number) => (e: React.SyntheticEvent) => { const { value } = e.target as HTMLInputElement - updateMultisigConfig({ id, alias: value || '' }).then(res => { - if (isSuccessResponse(res)) { - setEntities(v => v.map(config => (res.result && config.id === res.result?.id ? res.result : config))) - } - }) + onUpdateConfig({ id, alias: value || '' }) }, - [setEntities] + [onUpdateConfig] ) const deleteConfigById = useCallback( (id: number) => { @@ -124,7 +129,7 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM codeHash: DefaultLockInfo.CodeHash, hashType: DefaultLockInfo.HashType, }, - isMainnet + { isMainnet } ) ), fullPayload: getMultisigAddress(entity.blake160s, entity.r, entity.m, entity.n, isMainnet), @@ -141,7 +146,8 @@ export const useConfigManage = ({ walletId, isMainnet }: { walletId: string; isM return { saveConfig, allConfigs, - updateConfig, + onUpdateConfigAlias, + onUpdateConfig, deleteConfigById, onImportConfig, configs, @@ -292,12 +298,58 @@ const useApproveAction = () => { } } +const useDaoDepositAction = () => { + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [depositFromMultisig, setDepositFromMultisig] = useState() + const onOpenDialog = useCallback( + (option: MultisigConfig) => { + setIsDialogOpen(true) + setDepositFromMultisig(option) + }, + [setIsDialogOpen, setDepositFromMultisig] + ) + const closeDialog = useCallback(() => { + setIsDialogOpen(false) + }, [setIsDialogOpen]) + + return { + action: onOpenDialog, + closeDialog, + depositFromMultisig, + isDialogOpen, + } +} + +const useDaoWithdrawAction = () => { + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [withdrawFromMultisig, setWithdrawFromMultisig] = useState() + const onOpenDialog = useCallback( + (option: MultisigConfig) => { + setIsDialogOpen(true) + setWithdrawFromMultisig(option) + }, + [setIsDialogOpen, setWithdrawFromMultisig] + ) + const closeDialog = useCallback(() => { + setIsDialogOpen(false) + }, [setIsDialogOpen]) + + return { + action: onOpenDialog, + closeDialog, + withdrawFromMultisig, + isDialogOpen, + } +} + export const useActions = ({ deleteConfigById }: { deleteConfigById: (id: number) => void }) => { return { deleteAction: useDeleteAction(deleteConfigById), infoAction: useInfoAction(), sendAction: useSendAction(), approveAction: useApproveAction(), + daoDepositAction: useDaoDepositAction(), + daoWithdrawAction: useDaoWithdrawAction(), } } @@ -324,7 +376,7 @@ export const useSubscription = ({ const hashToPayload = useMemo( () => configs.reduce>( - (pre, cur) => ({ ...pre, [scriptToHash(addressToScript(cur.fullPayload))]: cur.fullPayload }), + (pre, cur) => ({ ...pre, [computeScriptHash(addressToScript(cur.fullPayload))]: cur.fullPayload }), {} ), [configs] @@ -365,3 +417,61 @@ export const useSubscription = ({ }, [isLightClient, getAndSaveMultisigSyncProgress]) return { multisigBanlances, multisigSyncProgress } } + +export const useCancelWithLightClient = () => { + const [isCloseWarningDialogShow, setIsCloseWarningDialogShow] = useState(false) + const onCancel = useCallback(() => { + setIsCloseWarningDialogShow(true) + }, [setIsCloseWarningDialogShow]) + const onCancelCloseMultisigDialog = useCallback(() => { + setIsCloseWarningDialogShow(false) + }, [setIsCloseWarningDialogShow]) + return { + isCloseWarningDialogShow, + onCancel, + onCancelCloseMultisigDialog, + } +} + +export const useSetStartBlockNumber = ({ + onUpdateConfig, +}: { + onUpdateConfig: (v: Partial & { id: number }) => Promise +}) => { + const [isSetStartBlockShown, setIsSetStartBlockShown] = useState(false) + const [editId, setEditId] = useState() + const [address, setAddress] = useState() + const [lastStartBlockNumber, setLastStartBlockNumber] = useState() + const onConfirm = useCallback( + (startBlockNumber: number) => { + if (editId) { + return onUpdateConfig({ + id: editId, + startBlockNumber, + }).then(() => { + setIsSetStartBlockShown(false) + }) + } + return Promise.reject(new Error('The Edit multisig config is empty')) + }, + [editId] + ) + const openDialog = useCallback>(e => { + const { id, address: editAddress, startBlockNumber } = e.currentTarget.dataset + if (id) { + setEditId(+id) + } + setAddress(editAddress) + setLastStartBlockNumber(startBlockNumber ? +startBlockNumber : undefined) + setIsSetStartBlockShown(true) + }, []) + return { + openDialog, + closeDialog: useCallback(() => setIsSetStartBlockShown(false), []), + isSetStartBlockShown, + onConfirm, + address, + lastStartBlockNumber, + onCancel: useCallback(() => setIsSetStartBlockShown(false), []), + } +} diff --git a/packages/neuron-ui/src/components/MultisigAddress/index.tsx b/packages/neuron-ui/src/components/MultisigAddress/index.tsx index 262fbe8596..c586003db0 100644 --- a/packages/neuron-ui/src/components/MultisigAddress/index.tsx +++ b/packages/neuron-ui/src/components/MultisigAddress/index.tsx @@ -1,34 +1,62 @@ -import React, { useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' import { - useOnLocaleChange, isMainnet as isMainnetUtil, shannonToCKBFormatter, useExitOnWalletChange, useGoBack, + useOnWindowResize, + calculateFee, } from 'utils' +import appState from 'states/init/app' import { useState as useGlobalState } from 'states' import MultisigAddressCreateDialog from 'components/MultisigAddressCreateDialog' import MultisigAddressInfo from 'components/MultisigAddressInfo' import SendFromMultisigDialog from 'components/SendFromMultisigDialog' -import { MultisigConfig } from 'services/remote' +import { MultisigConfig, changeMultisigSyncStatus, openExternal } from 'services/remote' import ApproveMultisigTxDialog from 'components/ApproveMultisigTxDialog' +import DepositDialog from 'components/DepositDialog' +import MultisigAddressNervosDAODialog from 'components/MultisigAddressNervosDAODialog' import Dialog from 'widgets/Dialog' import Table from 'widgets/Table' import Tooltip from 'widgets/Tooltip' +import Toast from 'widgets/Toast' import AlertDialog from 'widgets/AlertDialog' -import { ReactComponent as AddSimple } from 'widgets/Icons/AddSimple.svg' -import { ReactComponent as Details } from 'widgets/Icons/Details.svg' -import { ReactComponent as Delete } from 'widgets/Icons/Delete.svg' -import { ReactComponent as Confirm } from 'widgets/Icons/Confirm.svg' -import { ReactComponent as Transfer } from 'widgets/Icons/Transfer.svg' -import { ReactComponent as Upload } from 'widgets/Icons/Upload.svg' -import { ReactComponent as Edit } from 'widgets/Icons/Edit.svg' -import { Download, Search } from 'widgets/Icons/icon' +import { + Download, + Search, + AddSimple, + Details, + Delete, + Confirm, + Transfer, + Upload, + Edit, + Confirming, + Question, + LineDownArrow, + DAODeposit, + DAOWithdrawal, +} from 'widgets/Icons/icon' +import { getHeader } from 'services/chain' +import AttentionCloseDialog from 'widgets/Icons/Attention.png' import { HIDE_BALANCE, NetworkType } from 'utils/const' import { onEnter } from 'utils/inputDevice' import getMultisigSignStatus from 'utils/getMultisigSignStatus' -import { useSearch, useConfigManage, useExportConfig, useActions, useSubscription } from './hooks' +import useGetCountDownAndFeeRateStats from 'utils/hooks/useGetCountDownAndFeeRateStats' +import Button from 'widgets/Button' +import SetStartBlockNumberDialog from 'components/SetStartBlockNumberDialog' +import { type TFunction } from 'i18next' +import hooks from 'components/NervosDAO/hooks' +import { + useSearch, + useConfigManage, + useExportConfig, + useActions, + useSubscription, + useCancelWithLightClient, + useSetStartBlockNumber, +} from './hooks' import styles from './multisigAddress.module.scss' @@ -50,41 +78,80 @@ const tableActions = [ key: ApproveKey, icon: , }, + { + key: 'daoDeposit', + icon: , + }, + { + key: 'daoWithdraw', + icon: , + }, ] +const LearnMore = React.memo(({ t }: { t: TFunction }) => ( + +)) + const MultisigAddress = () => { - const [t, i18n] = useTranslation() - useOnLocaleChange(i18n) + const [t] = useTranslation() useExitOnWalletChange() const { - wallet: { id: walletId, addresses }, - chain: { networkID }, + app: { + send = appState.send, + loadings: { sending = false }, + }, + wallet, + chain: { + syncState: { bestKnownBlockNumber, bestKnownBlockTimestamp }, + networkID, + connectionStatus, + }, settings: { networks = [] }, } = useGlobalState() + const { id: walletId, addresses } = wallet const isMainnet = isMainnetUtil(networks, networkID) const isLightClient = useMemo( () => networks.find(n => n.id === networkID)?.type === NetworkType.Light, [networks, networkID] ) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) - const { allConfigs, saveConfig, updateConfig, deleteConfigById, onImportConfig, configs, onFilterConfig } = - useConfigManage({ - walletId, - isMainnet, - }) + const { + allConfigs, + saveConfig, + onUpdateConfig, + onUpdateConfigAlias, + deleteConfigById, + onImportConfig, + configs, + onFilterConfig, + } = useConfigManage({ + walletId, + isMainnet, + }) const { multisigBanlances, multisigSyncProgress } = useSubscription({ walletId, isMainnet, configs: allConfigs, isLightClient, }) - const { deleteAction, infoAction, sendAction, approveAction } = useActions({ deleteConfigById }) + const { deleteAction, infoAction, sendAction, approveAction, daoDepositAction, daoWithdrawAction } = useActions({ + deleteConfigById, + }) const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const { suggestFeeRate } = useGetCountDownAndFeeRateStats() + const [globalAPC, setGlobalAPC] = useState(0) + const [genesisBlockTimestamp, setGenesisBlockTimestamp] = useState(undefined) + const [notice, setNotice] = useState('') - const showMainDialog = useMemo( - () => !(infoAction.isDialogOpen || sendAction.isDialogOpen || approveAction.isDialogOpen || isCreateDialogOpen), - [infoAction.isDialogOpen, sendAction.isDialogOpen, approveAction.isDialogOpen, isCreateDialogOpen] - ) const onClickItem = useCallback( (multisigConfig: MultisigConfig) => (e: React.SyntheticEvent) => { const { @@ -104,6 +171,12 @@ const MultisigAddress = () => { case 'approve': approveAction.action(multisigConfig) break + case 'daoDeposit': + daoDepositAction.action(multisigConfig) + break + case 'daoWithdraw': + daoWithdrawAction.action(multisigConfig) + break default: break } @@ -141,10 +214,92 @@ const MultisigAddress = () => { }, [multisigBanlances, sendAction.sendFromMultisig]) const onBack = useGoBack() + const { + onCancel: onCancelWithLight, + isCloseWarningDialogShow, + onCancelCloseMultisigDialog, + } = useCancelWithLightClient() + const { + isSetStartBlockShown, + openDialog: openSetStartBlockNumber, + lastStartBlockNumber, + address, + onConfirm, + onCancel, + } = useSetStartBlockNumber({ onUpdateConfig }) + + useEffect(() => { + if (isLightClient) { + changeMultisigSyncStatus(true) + } + return () => { + if (isLightClient) { + changeMultisigSyncStatus(false) + } + } + }, [isLightClient]) + const titleRef = useRef(null) + const [tipPosition, setTipPosition] = useState<{ left?: number; top?: number }>({}) + const updateTipPosition = useCallback(() => { + if (titleRef.current) { + const boundingClientRect = titleRef.current.getBoundingClientRect() + setTipPosition({ + left: boundingClientRect.left - 18, + top: boundingClientRect.top - boundingClientRect.height, + }) + } + }, [titleRef.current, setTipPosition]) + useEffect(() => { + updateTipPosition() + }, [updateTipPosition]) + useOnWindowResize(updateTipPosition) + + const genesisBlockHash = useMemo(() => networks.find(v => v.id === networkID)?.genesisHash, [networkID, networks]) + + useEffect(() => { + if (genesisBlockHash) { + getHeader(genesisBlockHash) + .then(header => setGenesisBlockTimestamp(+header.timestamp)) + .catch(err => console.error(err)) + } + }, []) + + hooks.useUpdateGlobalAPC({ bestKnownBlockTimestamp, genesisBlockTimestamp, setGlobalAPC }) + + const fee = `${shannonToCKBFormatter( + send.generatedTx ? send.generatedTx.fee || calculateFee(send.generatedTx) : '0' + )} CKB` + + const onDepositSuccess = useCallback(() => { + daoDepositAction.closeDialog() + setNotice(t('nervos-dao.deposit-submitted')) + if (daoDepositAction.depositFromMultisig) { + daoWithdrawAction.action(daoDepositAction.depositFromMultisig) + } + }, [t, setNotice, daoDepositAction, daoWithdrawAction]) return (
- + + {t('multisig-address.window-title')} + ]} />} + placement="top" + showTriangle + tipClassName={styles.multiGuide} + tipStyles={tipPosition} + > + + +
+ } + onCancel={isLightClient ? onCancelWithLight : onBack} + showFooter={false} + >
@@ -221,8 +376,8 @@ const MultisigAddress = () => {