diff --git a/.ckb-version b/.ckb-version index 135af485bb..e4f95f2ae6 100644 --- a/.ckb-version +++ b/.ckb-version @@ -1 +1 @@ -v0.110.0 +v0.110.2 diff --git a/.github/workflows/add-replied-label.yml b/.github/workflows/add-replied-label.yml index 5e596dd725..0e7b35c48f 100644 --- a/.github/workflows/add-replied-label.yml +++ b/.github/workflows/add-replied-label.yml @@ -12,11 +12,11 @@ jobs: steps: - id: check-access name: Check if the commenter is a collaborator - uses: actions/github-script@v4 + uses: actions/github-script@v6 with: script: | try{ - const response = await github.repos.checkCollaborator({ + const response = await github.rest.repos.checkCollaborator({ owner: context.repo.owner, repo: context.repo.repo, username: context.payload.comment.user.login, @@ -30,10 +30,10 @@ jobs: - id: check-issue name: Check if the comment is replied in an issue - uses: actions/github-script@v4 + uses: actions/github-script@v6 with: script: | - const response = await github.issues.get({ + const response = await github.rest.issues.get({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, @@ -45,10 +45,10 @@ jobs: - id: add-label name: Add 'replied' label if: ${{ steps.check-access.outputs.result == 'true' && steps.check-issue.outputs.result == 'true' }} - uses: actions/github-script@v4 + uses: actions/github-script@v6 with: script: | - await github.issues.addLabels({ + await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, diff --git a/.github/workflows/check-code-style.yml b/.github/workflows/check-code-style.yml new file mode 100644 index 0000000000..61aa93474a --- /dev/null +++ b/.github/workflows/check-code-style.yml @@ -0,0 +1,41 @@ +name: Check Code Style + +on: + push: + pull_request: + +jobs: + default: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + cache: "yarn" + + - name: Restore + uses: actions/cache@v3 + with: + path: | + node_modules + */*/node_modules + key: 2022-05-07-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} + + - name: Bootstrap + run: | + yarn + + - name: Changed Files + id: changed-files + uses: tj-actions/changed-files@v37 + with: + files: "packages/**/*.{js,cjs,mjs,jsx,ts,tsx,css,scss}" + + - name: Prettier Check + if: steps.changed-files.outputs.any_changed == 'true' + run: | + yarn prettier --check ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/check-spell.yml b/.github/workflows/check-spell.yml new file mode 100644 index 0000000000..0830c528e8 --- /dev/null +++ b/.github/workflows/check-spell.yml @@ -0,0 +1,12 @@ +name: Check spell +on: + - pull_request + - push + +jobs: + run: + name: Check spell + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: crate-ci/typos@master diff --git a/.github/workflows/check_storybook.yml b/.github/workflows/check_storybook.yml new file mode 100644 index 0000000000..c63ff69d39 --- /dev/null +++ b/.github/workflows/check_storybook.yml @@ -0,0 +1,66 @@ +name: Check storybook + +on: + push: + pull_request: + +jobs: + default: + strategy: + matrix: + node: + - 18.12.0 + os: + - macos-latest + - ubuntu-20.04 + - windows-latest + + runs-on: ${{ matrix.os }} + + name: ${{ matrix.os }}(Node.js ${{ matrix.node }}) + + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: "yarn" + + - name: Restore + uses: actions/cache@v3 + with: + path: | + node_modules + */*/node_modules + key: 2022-10-11-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} + + - name: Install libudev + if: matrix.os == 'ubuntu-20.04' + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev + + - name: Install Lerna + run: yarn global add lerna + + - name: Bootstrap + run: | + yarn + yarn build + env: + CI: false + + - name: Build storybook + run: | + cd packages/neuron-ui + yarn build-storybook + env: + CI: true diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index a0a60034ab..04fdd175e4 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -4,8 +4,8 @@ on: push: branches: - master - - 'rc/**' - - 'hotfix/**' + - "rc/**" + - "hotfix/**" jobs: default: @@ -35,7 +35,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - cache: 'yarn' + cache: "yarn" - name: Restore uses: actions/cache@v3 @@ -49,7 +49,7 @@ jobs: if: matrix.os == 'windows-2019' uses: microsoft/setup-msbuild@v1.3.1 env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" - name: Install libudev if: matrix.os == 'ubuntu-20.04' @@ -60,7 +60,7 @@ jobs: - name: Install Lerna run: yarn global add lerna - - name: Boostrap + - name: Bootstrap run: | yarn env: @@ -77,6 +77,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} CSC_LINK: ${{ secrets.MAC_CERTIFICATE_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + TEAM_ID: ${{ secrets.TEAM_ID }} - name: Package for Windows if: matrix.os == 'windows-2019' diff --git a/.github/workflows/package_for_test.yml b/.github/workflows/package_for_test.yml index 6a057cdebf..1a014d36c8 100644 --- a/.github/workflows/package_for_test.yml +++ b/.github/workflows/package_for_test.yml @@ -1,9 +1,14 @@ name: Package Neuron for Test -on: [push] +on: + issue_comment: + types: [created, edited] + push: jobs: packaging: + if: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/package')) || github.event_name == 'push' }} + strategy: matrix: node: @@ -17,20 +22,31 @@ jobs: name: ${{ matrix.os }}(Node.js ${{ matrix.node }}) + env: + MAC_SHOULD_CODE_SIGN: ${{ github.event_name != 'pull_request' && secrets.APPLE_ID != '' }} + WIN_CERTIFICATE_BASE64: ${{ secrets.WIN_CERTIFICATE_BASE64 }} + steps: - name: Set git to use LF run: | git config --global core.autocrlf false git config --global core.eol lf - - name: Checkout + - name: Checkout for push + uses: actions/checkout@v3 + if: ${{ github.event_name == 'push' }} + + - name: Checkout for PR uses: actions/checkout@v3 + if: ${{ github.event_name == 'issue_comment' }} + with: + ref: refs/pull/${{ github.event.issue.number }}/merge - name: Setup Node uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} - cache: 'yarn' + cache: "yarn" - name: Restore uses: actions/cache@v3 @@ -44,7 +60,7 @@ jobs: if: matrix.os == 'windows-2019' uses: microsoft/setup-msbuild@v1.3.1 env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" - name: Install libudev if: matrix.os == 'ubuntu-20.04' @@ -55,14 +71,14 @@ jobs: - name: Install Lerna run: yarn global add lerna - - name: Boostrap + - name: Bootstrap run: | yarn env: CI: false - name: Package for MacOS - if: matrix.os == 'macos-latest' + if: ${{ matrix.os == 'macos-latest' && env.MAC_SHOULD_CODE_SIGN == 'true' }} run: | ./scripts/download-ckb.sh mac yarn package:test mac @@ -72,9 +88,20 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} CSC_LINK: ${{ secrets.MAC_CERTIFICATE_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + TEAM_ID: ${{ secrets.TEAM_ID }} + + - name: Package for MacOS for skip code sign + if: ${{ matrix.os == 'macos-latest' && env.MAC_SHOULD_CODE_SIGN == 'false' }} + run: | + export CSC_IDENTITY_AUTO_DISCOVERY=false + ./scripts/download-ckb.sh mac + yarn package:test mac + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SKIP_NOTARIZE: true - name: Package for Windows - if: matrix.os == 'windows-2019' + if: ${{ matrix.os == 'windows-2019' && env.WIN_CERTIFICATE_BASE64 != '' }} run: | bash ./scripts/download-ckb.sh win yarn build @@ -85,6 +112,16 @@ jobs: CSC_LINK: ${{ secrets.WIN_CERTIFICATE_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PASSWORD }} + - name: Package for Windows for skip code sign + if: ${{ matrix.os == 'windows-2019' && env.WIN_CERTIFICATE_BASE64 == '' }} + run: | + bash ./scripts/download-ckb.sh win + yarn build + bash ./scripts/copy-ui-files.sh + bash ./scripts/package-for-test.sh win + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Package for Linux if: matrix.os == 'ubuntu-20.04' run: | @@ -135,12 +172,37 @@ jobs: name: Neuron-Linux path: release/Neuron-*.AppImage - comment: + comment_when_package_success: needs: [packaging] name: Append links to the Pull Request runs-on: ubuntu-latest steps: - - uses: peter-evans/commit-comment@v2 + - name: Comment by push event + if: ${{ github.event_name == 'push' }} + uses: peter-evans/commit-comment@v2 with: body: | Packaging for test is done in [${{ github.run_id }}](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}) + + - name: Comment by pull request comment event + if: ${{ github.event_name == 'issue_comment' }} + uses: peter-evans/create-or-update-comment@v3 + with: + comment-id: ${{ github.event.comment.id }} + body: | + Packaging for test is done in [${{ github.run_id }}](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). @${{ github.event.comment.user.login }} + edit-mode: append + + comment_when_package_failed: + needs: [packaging] + if: ${{ always() && needs.packaging.result == 'failure' }} + name: Append failed comment to the comment + runs-on: ubuntu-latest + 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 + 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 }} + edit-mode: append diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index c9b32645b5..ba643391b2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -40,7 +40,7 @@ jobs: path: | node_modules */*/node_modules - key: 2022-05-07-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} + key: 2022-10-11-${{ runner.os }}-${{ hashFiles('**/yarn.lock')}} - name: Install libudev if: matrix.os == 'ubuntu-20.04' @@ -51,7 +51,7 @@ jobs: - name: Install Lerna run: yarn global add lerna - - name: Boostrap + - name: Bootstrap run: | yarn yarn build diff --git a/.github/workflows/update_ckb_client_versions.yml b/.github/workflows/update_ckb_client_versions.yml new file mode 100644 index 0000000000..9f978ded31 --- /dev/null +++ b/.github/workflows/update_ckb_client_versions.yml @@ -0,0 +1,45 @@ +name: Update CKB client versions + +on: + schedule: + - cron: '0 0 * * *' + +jobs: + default: + name: Update CKB client versions + runs-on: ubuntu-latest + permissions: + pull-requests: write # open PR + contents: write # update version files + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Update versions + id: update_versions + run: | + npm run update:client-versions + git add .ckb-version .ckb-light-version + + - name: Set GPG + uses: crazy-max/ghaction-import-gpg@v5 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Open PR to develop branch + uses: peter-evans/create-pull-request@v5 + with: + title: Update ckb client versions + commit-message: 'feat: update ckb client versions' + body: 'Update versions of builtin CKB node and light client' + committer: Chen Yu + branch: update-ckb-client-versions diff --git a/.github/workflows/update_valid_target.yml b/.github/workflows/update_valid_target.yml new file mode 100644 index 0000000000..4b2cd85462 --- /dev/null +++ b/.github/workflows/update_valid_target.yml @@ -0,0 +1,87 @@ +name: Update ckb node assume valid target + +on: + pull_request: + types: [ready_for_review] + branches: + - master + +jobs: + ready-for-release: + name: Update ckb node assume valid target + runs-on: ubuntu-latest + steps: + - name: Create Branch + uses: peterjgrainger/action-create-branch@v2.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + branch: 'chore-update-valid-target/${{github.head_ref}}' + sha: '${{ github.event.pull_request.head.sha }}' + + - name: Checkout + uses: actions/checkout@v3 + with: + ref: 'chore-update-valid-target/${{github.head_ref}}' + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 18.12.0 + + - name: Write env file + run: | + npm run update:valid-target + + - name: Commit env file + uses: actions/github-script@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE: ${{ github.head_ref }} + with: + script: | + const fs = require('node:fs') + const { BASE, HEAD } = process.env + const envFilePath = 'packages/neuron-wallet/.env' + const destinationBranch = `chore-update-valid-target/${BASE}` + const { data } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: envFilePath, + ref: destinationBranch, + }) + await github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: envFilePath, + message: `chore: Update ckb node assume valid target for ${BASE}.`, + content: fs.readFileSync(envFilePath).toString('base64'), + sha: data.sha, + branch: destinationBranch, + }) + + - name: Create PR + uses: actions/github-script@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE: ${{github.head_ref}} + HEAD: chore-update-valid-target/${{github.head_ref}} + REPO: ${{github.repository}} + with: + script: | + const { BASE, HEAD, REPO } = process.env + const { data: pulls } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + if (pulls.some(pull => pull.head.ref === HEAD)) { + return + } + github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + head: HEAD, + base: BASE, + title: 'chore: Update ckb node assume valid target', + body: `This PR uses to update ckb node assume valid target for PR https://github.com/${REPO}/pull/${context.issue.number}`, + }) diff --git a/.gitignore b/.gitignore index e752ec8fce..7e8a5d078a 100755 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ build dist pages release +/packages/neuron-ui/storybook-static /packages/neuron-wallet/bin/mac /packages/neuron-wallet/bin/linux /packages/neuron-wallet/bin/win diff --git a/.vscode/settings.json b/.vscode/settings.json index ae55f121aa..f3253b0eca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,4 +17,4 @@ "typescriptreact" ], "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c632f20b0b..377c2a82ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +# 0.110.3 (2023-10-11) + +### CKB Node & Light Client + +- [CKB@v0.110.2](https://github.com/nervosnetwork/ckb/releases/tag/v0.110.2) was released on Sep. 12nd, 2023. This version of CKB node is now bundled and preconfigured in Neuron. +- [CKB Light Client@v0.2.4](https://github.com/nervosnetwork/ckb-light-client/releases/tag/v0.2.4) was released on May. 28th, 2023. This version of CKB Light Client is now bundled and preconfigured in Neuron + +#### Caveat + +◆ **CKB Light Client** is only activated on testnet, thus only `light testnet` is enabled in Neuron. **CKB Light Client on Mainnet** requires an activation on the mainnet, the timetable can be found at https://github.com/nervosnetwork/ckb/releases/tag/v0.110.1. +◆ **default node** has been renamed to **Internal Node** for clarity. + +### Assumed valid target + +Block before `0x6b6db6bb23e6e98f63b88e6cd38fa49f46980e5b816f620c71c6c9c74633ee54`(at height `10,985,048`) will be skipped in validation.(257c06eb95062e8ef3bf53179668965fc743b10f) + +--- + +We are so excited to announce this new release of Neuron that has been completely revamped with modernizaed user interface and also enhanced your overall user experience. + +Say goodbye to clunky interface and hello to a smoother, more intuitive user experience. Neuron's new version is designed to make managing your assets on CKB effortless. + +Curious to see the magic of the new Neuron in action? Checkout our demo video on YouTube: https://youtu.be/MRuXmTLcXFo + +[![Brand-new Neuron](https://github.com/nervosnetwork/neuron/assets/7271329/77618118-4524-46c8-bf56-789b3e9d3206)](https://youtu.be/MRuXmTLcXFo) + +--- + +## New features + +- Adopt brand new User Interface.(@yanguoyu, @devchenyan, @WhiteMinds, @jeffreyma597, @zhangyouxin, @homura) +- #2672: Optimize storage of transactions to boost searching and reduce disk usage.(@yanguoyu) +- #2783: Check new versions actively.(@yanguoyu) +- #2786: Support synchronization from a specified block number in light client mode.(@yanguoyu) +- #2808: Optimize compatibility checking of CKB NODE.(@yanguoyu) +- #2813: Rename `default node` to `Internal node` for clarity.(@yanguoyu) +- #2839: Update Ckb client versions(@github-actions) + +## Bug fixes + +- #2711: Fix exit of synchronization with exception and multisig balance.(@yanguoyu) +- #2722: Automatically pad decimal places when the deposit dialog automatically provides a maximum value with decimals by(@WhiteMinds) +- #2772: Fix generating transaction when deposit all without balance.(@yanguoyu) + +## New Contributors + +- @zhangyouxin made their first contribution in https://github.com/nervosnetwork/neuron/pull/2775 + +**Full Changelog**: https://github.com/nervosnetwork/neuron/compare/v0.110.2...v0.110.3 + + # 0.110.2 (2023-07-07) ### CKB Node & Light Client diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000000..09deee0241 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,11 @@ +[default.extend-words] +thur = "thur" + +# defined in database schema +lastest = "lastest" + +[files] +extend-exclude = ["CHANGELOG.md", "**/migrations/*.ts"] + + + diff --git a/compatible.csv b/compatible.csv new file mode 100644 index 0000000000..98e98ab39d --- /dev/null +++ b/compatible.csv @@ -0,0 +1,5 @@ +CKB,0.110,0.109,0.108,0.107,0.106,0.105,0.104,0.103 +Neuron,,,,,,,, +0.110,yes,yes,no,no,no,no,no,no +0.106,no,no,yes,yes,yes,yes,no,no +0.103,no,no,no,no,no,no,yes,yes diff --git a/lerna.json b/lerna.json index 2940faa5dd..ce766d7238 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "0.110.2", + "version": "0.110.3", "npmClient": "yarn", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package.json b/package.json index 251284c3e1..88dc5ef815 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "neuron", "productName": "Neuron", "description": "CKB Neuron Wallet", - "version": "0.110.2", + "version": "0.110.3", "private": true, "author": { "name": "Nervos Core Dev", @@ -34,33 +34,39 @@ "test": "cross-env NODE_OPTIONS=--openssl-legacy-provider lerna run --parallel --load-env-files=false test", "test:ci": "yarn build:main && yarn test", "lint": "lerna run --stream lint", - "postinstall": "husky install && lerna run build --scope=@nervina-labs/ckb-indexer", - "db:chain": "node ./node_modules/.bin/typeorm" + "postinstall": "husky install", + "db:chain": "node ./node_modules/.bin/typeorm", + "update:valid-target": "node ./scripts/update-valid-target.js", + "update:client-versions": "node ./scripts/update-ckb-client-versions.js" }, "devDependencies": { - "@babel/core": "7.21.4", + "@babel/core": "7.22.5", "@types/jest": "27.5.2", - "@types/node": "18.15.11", + "@types/node": "18.16.18", "@types/npmlog": "4.1.4", - "@typescript-eslint/eslint-plugin": "5.58.0", - "@typescript-eslint/parser": "5.58.0", - "concurrently": "8.0.1", + "@typescript-eslint/eslint-plugin": "5.60.1", + "@typescript-eslint/parser": "5.60.1", + "concurrently": "8.2.0", "cross-env": "7.0.3", "eslint": "8.38.0", "eslint-config-prettier": "8.8.0", + "eslint-plugin-prettier": "4.2.1", "husky": "8.0.3", - "lerna": "7.0.0", - "lint-staged": "13.2.1", + "lerna": "7.1.0", + "lint-staged": "13.2.2", "ncp": "2.0.0", - "prettier": "2.8.7", + "prettier": "2.8.8", "ts-jest": "27.1.5", "typescript": "5.0.4", - "wait-on": "6.0.1" + "wait-on": "7.0.1" }, "dependencies": {}, "resolutions": { - "@types/react": "17.0.58", + "@types/react": "17.0.62", "react-i18next": ">=11.16.4", "usb": "1.8.8" + }, + "volta": { + "node": "18.16.1" } } diff --git a/packages/ckb-indexer/.gitignore b/packages/ckb-indexer/.gitignore deleted file mode 100644 index 502167fa0b..0000000000 --- a/packages/ckb-indexer/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/lib diff --git a/packages/ckb-indexer/package.json b/packages/ckb-indexer/package.json deleted file mode 100644 index 44351435cc..0000000000 --- a/packages/ckb-indexer/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@nervina-labs/ckb-indexer", - "version": "0.1.1", - "description": "CKB indexer binding for Node.js", - "author": { - "name": "Nervos Core Dev", - "email": "dev@nervos.org", - "url": "https://github.com/nervosnetwork/neuron" - }, - "homepage": "https://github.com/nervosnetwork/neuron#readme", - "license": "MIT", - "main": "lib/index.js", - "directories": { - "lib": "lib", - "test": "__tests__" - }, - "dependencies": { - "@ckb-lumos/base": "0.18.0", - "@ckb-lumos/rpc": "0.18.0", - "ckb-js-toolkit": "0.100.0-rc1", - "cross-fetch": "3.1.5" - }, - "devDependencies": { - "events": "3.3.0", - "eslint": "8.38.0" - }, - "scripts": { - "build": "tsc", - "clean": "npx rimraf lib", - "prepublishOnly": "yarn run clean && yarn run build", - "release": "npm publish" - }, - "files": [ - "lib" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/nervosnetwork/neuron.git" - }, - "bugs": { - "url": "https://github.com/nervosnetwork/neuron/issues" - } -} diff --git a/packages/ckb-indexer/src/collector.ts b/packages/ckb-indexer/src/collector.ts deleted file mode 100644 index 184ff25e80..0000000000 --- a/packages/ckb-indexer/src/collector.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { utils, Cell, BaseCellCollector } from "@ckb-lumos/base"; -import { validators } from "ckb-js-toolkit"; -import { - SearchKeyFilter, - CKBIndexerQueryOptions, - GetCellsResults, - Order, - OtherQueryOptions, -} from "./type"; - -import { CkbIndexer } from "./indexer"; -import { - generateSearchKey, - getHexStringBytes, - instanceOfScriptWrapper, -} from "./services"; -import fetch from "cross-fetch"; - -interface GetBlockHashRPCResult { - jsonrpc: string; - id: number; - result: string; -} - -/** CellCollector will not get cell with block_hash by default, please use OtherQueryOptions.withBlockHash and OtherQueryOptions.CKBRpcUrl to get block_hash if you need. */ -export class CKBCellCollector implements BaseCellCollector { - constructor( - public indexer: CkbIndexer, - public queries: CKBIndexerQueryOptions, - public otherQueryOptions?: OtherQueryOptions - ) { - const defaultQuery: CKBIndexerQueryOptions = { - lock: undefined, - type: undefined, - argsLen: -1, - data: "any", - fromBlock: undefined, - toBlock: undefined, - order: Order.asc, - skip: undefined, - outputDataLenRange: undefined, - outputCapacityRange: undefined, - bufferSize: undefined, - }; - this.queries = { ...defaultQuery, ...this.queries }; - if ( - !this.queries.lock && - (!this.queries.type || this.queries.type === "empty") - ) { - throw new Error("Either lock or type script must be provided!"); - } - - // unWrap `ScriptWrapper` into `Script`. - if (this.queries.lock) { - if (!instanceOfScriptWrapper(this.queries.lock)) { - validators.ValidateScript(this.queries.lock); - } else if (instanceOfScriptWrapper(this.queries.lock)) { - validators.ValidateScript(this.queries.lock.script); - this.queries.lock = this.queries.lock.script; - } - } - - // unWrap `ScriptWrapper` into `Script`. - if (this.queries.type && this.queries.type !== "empty") { - if ( - typeof this.queries.type === "object" && - !instanceOfScriptWrapper(this.queries.type) - ) { - validators.ValidateScript(this.queries.type); - } else if ( - typeof this.queries.type === "object" && - instanceOfScriptWrapper(this.queries.type) - ) { - validators.ValidateScript(this.queries.type.script); - this.queries.type = this.queries.type.script; - } - } - - if (this.queries.fromBlock) { - utils.assertHexadecimal("fromBlock", this.queries.fromBlock); - } - if (this.queries.toBlock) { - utils.assertHexadecimal("toBlock", this.queries.toBlock); - } - if (this.queries.order !== Order.asc && this.queries.order !== Order.desc) { - throw new Error("Order must be either asc or desc!"); - } - if (this.queries.outputCapacityRange) { - utils.assertHexadecimal( - "outputCapacityRange[0]", - this.queries.outputCapacityRange[0] - ); - utils.assertHexadecimal( - "outputCapacityRange[1]", - this.queries.outputCapacityRange[1] - ); - } - - if (this.queries.outputDataLenRange) { - utils.assertHexadecimal( - "outputDataLenRange[0]", - this.queries.outputDataLenRange[0] - ); - utils.assertHexadecimal( - "outputDataLenRange[1]", - this.queries.outputDataLenRange[1] - ); - } - - if (this.queries.skip && typeof this.queries.skip !== "number") { - throw new Error("skip must be a number!"); - } - - if ( - this.queries.bufferSize && - typeof this.queries.bufferSize !== "number" - ) { - throw new Error("bufferSize must be a number!"); - } - } - - private async getLiveCell(lastCursor?: string): Promise { - const searchKeyFilter: SearchKeyFilter = { - sizeLimit: this.queries.bufferSize, - order: this.queries.order as Order, - }; - if (lastCursor) { - searchKeyFilter.lastCursor = lastCursor; - } - const result: GetCellsResults = await this.indexer.getCells( - generateSearchKey(this.queries), - undefined, - searchKeyFilter - ); - return result; - } - - private shouldSkipped(cell: Cell, index: number) { - if (this.queries.skip && index < this.queries.skip) { - return true; - } - if (cell && this.queries.type === "empty" && cell.cell_output.type) { - return true; - } - if (this.queries.data !== "any" && cell.data !== this.queries.data) { - return true; - } - if ( - this.queries.argsLen !== -1 && - this.queries.argsLen !== "any" && - getHexStringBytes(cell.cell_output.lock.args) !== this.queries.argsLen - ) { - return true; - } - } - - async count() { - let lastCursor: undefined | string = undefined; - const getCellWithCursor = async (): Promise => { - const result: GetCellsResults = await this.getLiveCell(lastCursor); - lastCursor = result.lastCursor; - return result.objects; - }; - let counter = 0; - let cells: Cell[] = await getCellWithCursor(); - if (cells.length === 0) { - return 0; - } - let buffer: Promise = getCellWithCursor(); - let index: number = 0; - while (true) { - if (!this.shouldSkipped(cells[index], index)) { - counter += 1; - } - index++; - //reset index and exchange `cells` and `buffer` after count last cell - if (index === cells.length) { - index = 0; - cells = await buffer; - // break if can not get more cells - if (cells.length === 0) { - break; - } - buffer = getCellWithCursor(); - } - } - return counter; - } - - private async request(rpcUrl: string, data: unknown): Promise { - const res: Response = await fetch(rpcUrl, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, - }); - if (res.status !== 200) { - throw new Error(`indexer request failed with HTTP code ${res.status}`); - } - const result = await res.json(); - if (result.error !== undefined) { - throw new Error( - `indexer request rpc failed with error: ${JSON.stringify(result.error)}` - ); - } - return result; - } - - private async getLiveCellWithBlockHash(lastCursor?: string) { - if (!this.otherQueryOptions) { - throw new Error("CKB Rpc URL must provide"); - } - let result: GetCellsResults = await this.getLiveCell(lastCursor); - if (result.objects.length === 0) { - return result; - } - const requestData = result.objects.map((cell, index) => { - return { - id: index, - jsonrpc: "2.0", - method: "get_block_hash", - params: [cell.block_number], - }; - }); - const blockHashList: GetBlockHashRPCResult[] = await this.request( - this.otherQueryOptions.ckbRpcUrl, - requestData - ); - result.objects = result.objects.map((item, index) => { - const rpcResponse = blockHashList.find( - (responseItem: GetBlockHashRPCResult) => responseItem.id === index - ); - const block_hash = rpcResponse && rpcResponse.result; - return { ...item, block_hash }; - }); - return result; - } - - /** collect cells without block_hash by default.if you need block_hash, please add OtherQueryOptions.withBlockHash and OtherQueryOptions.ckbRpcUrl when constructor CellCollect. - * don't use OtherQueryOption if you don't need block_hash,cause it will slowly your collect. - */ - async *collect() { - const withBlockHash = - this.otherQueryOptions && - "withBlockHash" in this.otherQueryOptions && - this.otherQueryOptions.withBlockHash; - let lastCursor: undefined | string = undefined; - const getCellWithCursor = async (): Promise => { - const result: GetCellsResults = await (withBlockHash - ? this.getLiveCellWithBlockHash(lastCursor) - : this.getLiveCell(lastCursor)); - lastCursor = result.lastCursor; - return result.objects; - }; - let cells: Cell[] = await getCellWithCursor(); - if (cells.length === 0) { - return; - } - let buffer: Promise = getCellWithCursor(); - let index: number = 0; - while (true) { - if (!this.shouldSkipped(cells[index], index)) { - yield cells[index]; - } - index++; - //reset index and exchange `cells` and `buffer` after yield last cell - if (index === cells.length) { - index = 0; - cells = await buffer; - // break if can not get more cells - if (cells.length === 0) { - break; - } - buffer = getCellWithCursor(); - } - } - } -} diff --git a/packages/ckb-indexer/src/index.ts b/packages/ckb-indexer/src/index.ts deleted file mode 100644 index aa018f543c..0000000000 --- a/packages/ckb-indexer/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** CkbIndexer.collector will not get cell with block_hash by default, please use OtherQueryOptions.withBlockHash and OtherQueryOptions.CKBRpcUrl to get block_hash if you need. */ -export { CkbIndexer } from './indexer' -/** CellCollector will not get cell with block_hash by default, please use OtherQueryOptions.withBlockHash and OtherQueryOptions.CKBRpcUrl to get block_hash if you need. */ -export { CKBCellCollector as CellCollector } from './collector' -export { CKBIndexerTransactionCollector as TransactionCollector } from './transaction_collector' diff --git a/packages/ckb-indexer/src/indexer.ts b/packages/ckb-indexer/src/indexer.ts deleted file mode 100644 index 91907cf584..0000000000 --- a/packages/ckb-indexer/src/indexer.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { - Cell, - CellCollector, - HexString, - Indexer, - Script, - Tip, - Output, - utils, - Block, -} from "@ckb-lumos/base"; -import { validators } from "ckb-js-toolkit"; -import { RPC } from "@ckb-lumos/rpc"; -import { request, requestBatch } from "./services"; -import { CKBCellCollector } from "./collector"; -import { EventEmitter } from "events"; -import { - GetTransactionRPCResult, - CKBIndexerQueryOptions, - GetCellsResults, - GetLiveCellsResult, - GetTransactionsResult, - GetTransactionsResults, - IndexerEmitter, - Order, - OutputToVerify, - SearchKey, - SearchKeyFilter, - Terminator, - OtherQueryOptions, -} from "./type"; - -const DefaultTerminator: Terminator = () => { - return { stop: false, push: true }; -}; - -function defaultLogger(level: string, message: string) { - console.log(`[${level}] ${message}`); -} - -/** CkbIndexer.collector will not get cell with block_hash by default, please use OtherQueryOptions.withBlockHash and OtherQueryOptions.CKBRpcUrl to get block_hash if you need. */ -export class CkbIndexer implements Indexer { - uri: string; - medianTimeEmitters: EventEmitter[] = []; - emitters: IndexerEmitter[] = []; - isSubscribeRunning: boolean = false; - constructor(public ckbRpcUrl: string, public ckbIndexerUrl: string) { - this.uri = ckbRpcUrl; - this.ckbIndexerUrl = ckbIndexerUrl; - } - - private getCkbRpc(): RPC { - return new RPC(this.ckbRpcUrl); - } - - async tip(): Promise { - const res = await request(this.ckbIndexerUrl, "get_indexer_tip"); - return res as Tip; - } - - asyncSleep(timeout: number): Promise { - return new Promise((resolve) => setTimeout(resolve, timeout)); - } - - async waitForSync(blockDifference = 0): Promise { - const rpcTipNumber = parseInt( - (await this.getCkbRpc().get_tip_header()).number, - 16 - ); - while (true) { - const indexerTipNumber = parseInt((await this.tip()).block_number, 16); - if (indexerTipNumber + blockDifference >= rpcTipNumber) { - return; - } - await this.asyncSleep(1000); - } - } - - /** collector cells without block_hash by default.if you need block_hash, please add OtherQueryOptions.withBlockHash and OtherQueryOptions.ckbRpcUrl. - * don't use OtherQueryOption if you don't need block_hash,cause it will slowly your collect. - */ - collector( - queries: CKBIndexerQueryOptions, - otherQueryOptions?: OtherQueryOptions - ): CellCollector { - return new CKBCellCollector(this, queries, otherQueryOptions); - } - - private async request( - method: string, - params?: any, - ckbIndexerUrl: string = this.ckbIndexerUrl - ): Promise { - return request(ckbIndexerUrl, method, params); - } - - public async getCells( - searchKey: SearchKey, - terminator: Terminator = DefaultTerminator, - searchKeyFilter: SearchKeyFilter = {} - ): Promise { - const infos: Cell[] = []; - let cursor: string | undefined = searchKeyFilter.lastCursor; - let sizeLimit = searchKeyFilter.sizeLimit || 100; - let order = searchKeyFilter.order || Order.asc; - const index = 0; - while (true) { - let params = [searchKey, order, `0x${sizeLimit.toString(16)}`, cursor]; - const res: GetLiveCellsResult = await this.request("get_cells", params); - const liveCells = res.objects; - cursor = res.last_cursor; - for (const liveCell of liveCells) { - const cell: Cell = { - cell_output: liveCell.output, - data: liveCell.output_data, - out_point: liveCell.out_point, - block_number: liveCell.block_number, - }; - const { stop, push } = terminator(index, cell); - if (push) { - infos.push(cell); - } - if (stop) { - return { - objects: infos, - lastCursor: cursor, - }; - } - } - if (liveCells.length < sizeLimit) { - break; - } - } - return { - objects: infos, - lastCursor: cursor, - }; - } - - public async getTransactions( - searchKey: SearchKey, - searchKeyFilter: SearchKeyFilter = {} - ): Promise { - let infos: GetTransactionsResult[] = []; - let cursor: string | undefined = searchKeyFilter.lastCursor; - let sizeLimit = searchKeyFilter.sizeLimit || 100; - let order = searchKeyFilter.order || Order.asc; - for (;;) { - const params = [searchKey, order, `0x${sizeLimit.toString(16)}`, cursor]; - const res = await this.request("get_transactions", params); - const txs = res.objects; - cursor = res.last_cursor as string; - infos = infos.concat(txs); - if (txs.length < sizeLimit) { - break; - } - } - return { - objects: infos, - lastCursor: cursor, - }; - } - - running(): boolean { - return true; - } - - start(): void { - defaultLogger( - "warn", - "deprecated: no need to start the ckb-indexer manually" - ); - } - - startForever(): void { - defaultLogger( - "warn", - "deprecated: no need to startForever the ckb-indexer manually" - ); - } - - stop(): void { - defaultLogger( - "warn", - "deprecated: no need to stop the ckb-indexer manually" - ); - } - - subscribe(queries: CKBIndexerQueryOptions): EventEmitter { - this.isSubscribeRunning = true; - this.scheduleLoop(); - if (queries.lock && queries.type) { - throw new Error( - "The notification machanism only supports you subscribing for one script once so far!" - ); - } - if (queries.toBlock !== null || queries.skip !== null) { - defaultLogger( - "warn", - "The passing fields such as toBlock and skip are ignored in subscribe() method." - ); - } - let emitter = new IndexerEmitter(); - emitter.argsLen = queries.argsLen; - emitter.outputData = queries.data; - if (queries.fromBlock) { - utils.assertHexadecimal("fromBlock", queries.fromBlock); - } - emitter.fromBlock = !queries.fromBlock ? BigInt(0) : BigInt(queries.fromBlock); - if (queries.lock) { - validators.ValidateScript(queries.lock); - emitter.lock = queries.lock as Script; - } else if (queries.type && queries.type !== "empty") { - validators.ValidateScript(queries.type); - emitter.type = queries.type as Script; - } else { - throw new Error("Either lock or type script must be provided!"); - } - this.emitters.push(emitter); - return emitter; - } - - private loop() { - if (!this.isSubscribeRunning) { - return; - } - this.poll() - .then((timeout) => { - this.scheduleLoop(timeout); - }) - .catch((e) => { - defaultLogger( - "error", - `Error occurs: ${e} ${e.stack}, stopping indexer!` - ); - this.isSubscribeRunning = false; - }); - } - - private scheduleLoop(timeout = 1) { - setTimeout(() => { - this.loop(); - }, timeout); - } - - private async poll() { - let timeout = 1; - const tip = await this.tip(); - const { block_number, block_hash } = tip; - if (block_number === "0x0") { - const block: Block = await this.request( - "get_block_by_number", - [block_number], - this.ckbRpcUrl - ); - await this.publishAppendBlockEvents(block); - } - const nextBlockNumber = BigInt(block_number) + BigInt(1); - const block = await this.request( - "get_block_by_number", - [`0x${nextBlockNumber.toString(16)}`], - this.ckbRpcUrl - ); - if (block) { - if (block.header.parent_hash === block_hash) { - await this.publishAppendBlockEvents(block); - } else { - const block: Block = await this.request( - "get_block_by_number", - [block_number], - this.ckbRpcUrl - ); - await this.publishAppendBlockEvents(block); - } - } else { - const block = await this.request( - "get_block_by_number", - [block_number], - this.ckbRpcUrl - ); - await this.publishAppendBlockEvents(block); - timeout = 3 * 1000; - } - return timeout; - } - - private async publishAppendBlockEvents(block: Block) { - for (const [txIndex, tx] of block.transactions.entries()) { - const blockNumber = block.header.number; - // publish changed events if subscribed script exists in previous output cells , skip the cellbase. - if (txIndex > 0) { - const requestData = tx.inputs.map((input, index) => { - return { - id: index, - jsonrpc: "2.0", - method: "get_transaction", - params: [input.previous_output.tx_hash], - }; - }); - - // batch request by block - const transactionResponse: OutputToVerify[] = await requestBatch( - this.ckbRpcUrl, - requestData - ).then((response: GetTransactionRPCResult[]) => { - return response.map( - (item: GetTransactionRPCResult, index: number) => { - const cellIndex = tx.inputs[index].previous_output.index; - const outputCell = - item.result.transaction.outputs[parseInt(cellIndex)]; - const outputData = - item.result.transaction.outputs_data[parseInt(cellIndex)]; - return { output: outputCell, outputData } as OutputToVerify; - } - ); - }); - transactionResponse.forEach(({ output, outputData }) => { - this.filterEvents(output, blockNumber, outputData); - }); - } - // publish changed events if subscribed script exists in output cells. - for (const [outputIndex, output] of tx.outputs.entries()) { - const outputData = tx.outputs_data[outputIndex]; - this.filterEvents(output, blockNumber, outputData); - } - } - await this.emitMedianTimeEvents(); - } - - private filterEvents( - output: Output, - blockNumber: string, - outputData: HexString - ) { - for (const emitter of this.emitters) { - if ( - emitter.lock !== undefined && - this.checkFilterOptions( - emitter, - blockNumber, - outputData, - emitter.lock, - output.lock - ) - ) { - emitter.emit("changed"); - } - } - if (output.type !== null) { - for (const emitter of this.emitters) { - if ( - emitter.type !== undefined && - this.checkFilterOptions( - emitter, - blockNumber, - outputData, - emitter.type, - output.type - ) - ) { - emitter.emit("changed"); - } - } - } - } - - private checkFilterOptions( - emitter: IndexerEmitter, - blockNumber: string, - outputData: string, - emitterScript: Script, - script: Script | undefined - ) { - const checkBlockNumber = emitter.fromBlock - ? emitter.fromBlock <= BigInt(blockNumber) - : true; - const checkOutputData = - emitter.outputData === "any" || !emitter.outputData - ? true - : emitter.outputData === outputData; - const checkScript = !script - ? true - : emitterScript.code_hash === script.code_hash && - emitterScript.hash_type === script.hash_type && - this.checkArgs(emitter.argsLen, emitterScript.args, script.args); - return checkBlockNumber && checkOutputData && checkScript; - } - - private checkArgs( - argsLen: number | "any" | undefined, - emitterArgs: HexString, - args: HexString - ) { - if (argsLen === -1 || (!argsLen && argsLen !== 0)) { - return emitterArgs === args; - } else if (typeof argsLen === "number" && args.length === argsLen * 2 + 2) { - return args.substring(0, emitterArgs.length) === emitterArgs; - } else if (argsLen === "any") { - return args.substring(0, emitterArgs.length) === emitterArgs; - } else { - return false; - } - } - - private async emitMedianTimeEvents() { - if (this.medianTimeEmitters.length === 0) { - return; - } - const info = await request(this.ckbRpcUrl, "get_blockchain_info"); - const medianTime = info.median_time; - for (const medianTimeEmitter of this.medianTimeEmitters) { - medianTimeEmitter.emit("changed", medianTime); - } - } - - subscribeMedianTime(): EventEmitter { - this.isSubscribeRunning = true; - this.scheduleLoop(); - const medianTimeEmitter = new EventEmitter(); - this.medianTimeEmitters.push(medianTimeEmitter); - return medianTimeEmitter; - } -} diff --git a/packages/ckb-indexer/src/services.ts b/packages/ckb-indexer/src/services.ts deleted file mode 100644 index e2edad46ee..0000000000 --- a/packages/ckb-indexer/src/services.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { utils, Script, ScriptWrapper, HexString } from "@ckb-lumos/base"; -import { - CKBIndexerQueryOptions, - HexadecimalRange, - SearchFilter, - ScriptType, - SearchKey, -} from "./type"; -import fetch from "cross-fetch"; - -function instanceOfScriptWrapper(object: unknown): object is ScriptWrapper { - return typeof object === "object" && object != null && "script" in object; -} -const UnwrapScriptWrapper = (inputScript: ScriptWrapper | Script): Script => { - if (instanceOfScriptWrapper(inputScript)) { - return inputScript.script; - } - return inputScript; -}; -const generateSearchKey = (queries: CKBIndexerQueryOptions): SearchKey => { - let script: Script | undefined = undefined; - const filter: SearchFilter = {}; - let script_type: ScriptType | undefined = undefined; - if (queries.lock) { - const lock = UnwrapScriptWrapper(queries.lock); - script = lock as Script; - script_type = ScriptType.lock; - if (queries.type && typeof queries.type !== "string") { - const type = UnwrapScriptWrapper(queries.type); - filter.script = type as Script; - } - } else if (queries.type && typeof queries.type !== "string") { - const type = UnwrapScriptWrapper(queries.type); - script = type as Script; - script_type = ScriptType.type; - } - let block_range: HexadecimalRange | null = null; - if (queries.fromBlock && queries.toBlock) { - //toBlock+1 cause toBlock need to be included - block_range = [ - queries.fromBlock, - `0x${(BigInt(queries.toBlock) + BigInt(1)).toString(16)}`, - ]; - } - if (block_range) { - filter.block_range = block_range; - } - if (queries.outputDataLenRange) { - filter.output_data_len_range = queries.outputDataLenRange; - } - if (queries.outputCapacityRange) { - filter.output_capacity_range = queries.outputCapacityRange; - } - if (!script) { - throw new Error("Either lock or type script must be provided!"); - } - if (!script_type) { - throw new Error("script_type must be provided"); - } - return { - script, - script_type, - filter, - }; -}; - -const getHexStringBytes = (hexString: HexString) => { - utils.assertHexString("", hexString); - return Math.ceil(hexString.substr(2).length / 2); -}; - -const requestBatch = async (rpcUrl: string, data: unknown): Promise => { - const res: Response = await fetch(rpcUrl, { - method: "POST", - body: JSON.stringify(data), - headers: { - "Content-Type": "application/json", - }, - }); - if (res.status !== 200) { - throw new Error(`indexer request failed with HTTP code ${res.status}`); - } - const result = await res.json(); - if (result.error !== undefined) { - throw new Error( - `indexer request rpc failed with error: ${JSON.stringify(result.error)}` - ); - } - return result; -}; - -const request = async ( - ckbIndexerUrl: string, - method: string, - params?: any -): Promise => { - const res = await fetch(ckbIndexerUrl, { - method: "POST", - body: JSON.stringify({ - id: 0, - jsonrpc: "2.0", - method, - params, - }), - headers: { - "Content-Type": "application/json", - }, - }); - if (res.status !== 200) { - throw new Error(`indexer request failed with HTTP code ${res.status}`); - } - const data = await res.json(); - if (data.error !== undefined) { - throw new Error( - `indexer request rpc failed with error: ${JSON.stringify(data.error)}` - ); - } - return data.result; -}; - -export { - generateSearchKey, - getHexStringBytes, - instanceOfScriptWrapper, - requestBatch, - request, -}; diff --git a/packages/ckb-indexer/src/transaction_collector.ts b/packages/ckb-indexer/src/transaction_collector.ts deleted file mode 100644 index 5d882042d9..0000000000 --- a/packages/ckb-indexer/src/transaction_collector.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { - TransactionCollectorOptions, - indexer as BaseIndexerModule, - Output, - OutPoint, - TransactionWithStatus, -} from "@ckb-lumos/base"; -import { - SearchKeyFilter, - CKBIndexerQueryOptions, - GetTransactionsResult, - GetTransactionsResults, - IOType, - Order, - TransactionWithIOType, - GetTransactionRPCResult, -} from "./type"; -import { CkbIndexer } from "./indexer"; -import { - generateSearchKey, - getHexStringBytes, - instanceOfScriptWrapper, - requestBatch, - request, -} from "./services"; - -interface GetTransactionDetailResult { - objects: TransactionWithStatus[]; - lastCursor: string | undefined; -} - -export class CKBIndexerTransactionCollector extends BaseIndexerModule.TransactionCollector { - filterOptions: TransactionCollectorOptions; - constructor( - public indexer: CkbIndexer, - public queries: CKBIndexerQueryOptions, - public CKBRpcUrl: string, - public options?: TransactionCollectorOptions - ) { - super(indexer, queries, options); - const defaultOptions: TransactionCollectorOptions = { - skipMissing: false, - includeStatus: true, - }; - this.filterOptions = { ...defaultOptions, ...this.options }; - } - - /* - *lock?: ScriptWrapper.script query by ckb-indexer,ScriptWrapper.ioType filter after get transaction from indexer, ScriptWrapper.argsLen filter after get transaction from rpc; - *type?: ScriptWrapper.script query by ckb-indexer,ScriptWrapper.ioType filter after get transaction from indexer, ScriptWrapper.argsLen filter after get transaction from rpc; - *data?: will not filter - *argsLen?: filter after get transaction detail; - *fromBlock?: query by ckb-indexer; - *toBlock?: query by ckb-indexer; - *skip?: filter after get transaction from ckb-indexer;; - *order?: query by ckb-indexer; - */ - private async getTransactions( - lastCursor?: string, - ): Promise { - const searchKeyFilter: SearchKeyFilter = { - sizeLimit: this.queries.bufferSize, - order: this.queries.order as Order, - }; - if (lastCursor) { - searchKeyFilter.lastCursor = lastCursor; - } - let transactionHashList: GetTransactionsResults = { - objects: [], - lastCursor: "", - }; - /* - * if both lock and type exist,we need search them in independent and then get intersection - * cause ckb-indexer use searchKey script on each cell but native indexer use lock and type on transaction, - * and one transaction may have many cells both in input and output, more detail in test 'Test query transaction by both input lock and output type script' - */ - - //if both lock and type, search search them in independent and then get intersection, GetTransactionsResults.lastCursor change to `${lockLastCursor}-${typeLastCursor}` - if ( - instanceOfScriptWrapper(this.queries.lock) && - instanceOfScriptWrapper(this.queries.type) - ) { - transactionHashList = await this.getTransactionByLockAndTypeIndependent( - searchKeyFilter - ); - lastCursor = transactionHashList.lastCursor; - } else { - //query by ScriptWrapper.script,block_range,order - transactionHashList = await this.indexer.getTransactions( - generateSearchKey(this.queries), - searchKeyFilter - ); - lastCursor = transactionHashList.lastCursor; - } - - // filter by ScriptWrapper.io_type - transactionHashList.objects = this.filterByTypeIoTypeAndLockIoType( - transactionHashList.objects, - this.queries - ); - // return if transaction hash list if empty - if (transactionHashList.objects.length === 0) { - return { - objects: [], - lastCursor: lastCursor, - }; - } - - let transactionList: TransactionWithIOType[] = await this.getTransactionListFromRpc( - transactionHashList - ); - - for (const transactionWrapper of transactionList) { - if (transactionWrapper.ioType === "input") { - const targetOutPoint: OutPoint = - transactionWrapper.transaction.inputs[ - parseInt(transactionWrapper.ioIndex) - ].previous_output; - const targetCell = await this.getCellByOutPoint(targetOutPoint); - transactionWrapper.inputCell = targetCell; - } - } - - //filter by ScriptWrapper.argsLen - transactionList = transactionList.filter( - (transactionWrapper: TransactionWithIOType) => { - if ( - transactionWrapper.ioType === "input" && - transactionWrapper.inputCell - ) { - return this.isCellScriptArgsValid(transactionWrapper.inputCell); - } else { - const targetCell: Output = - transactionWrapper.transaction.outputs[ - parseInt(transactionWrapper.ioIndex) - ]; - return this.isCellScriptArgsValid(targetCell); - } - } - ); - const objects = transactionList.map((tx) => ({ - transaction: tx.transaction, - tx_status: tx.tx_status, - })); - return { - objects: objects, - lastCursor: lastCursor, - }; - } - - private async getTxHashesWithCursor(lastCursor?: string) { - const searchKeyFilter: SearchKeyFilter = { - sizeLimit: this.queries.bufferSize, - order: this.queries.order as Order, - }; - if (lastCursor) { - searchKeyFilter.lastCursor = lastCursor; - } - let transactionHashList: GetTransactionsResults = { - objects: [], - lastCursor: "", - }; - /* - * if both lock and type exist,we need search them in independent and then get intersection - * cause ckb-indexer use searchKey script on each cell but native indexer use lock and type on transaction, - * and one transaction may have many cells both in input and output, more detail in test 'Test query transaction by both input lock and output type script' - */ - - //if both lock and type, search search them in independent and then get intersection, GetTransactionsResults.lastCursor change to `${lockLastCursor}-${typeLastCursor}` - if ( - instanceOfScriptWrapper(this.queries.lock) && - instanceOfScriptWrapper(this.queries.type) - ) { - transactionHashList = await this.getTransactionByLockAndTypeIndependent( - searchKeyFilter - ); - lastCursor = transactionHashList.lastCursor; - } else { - //query by ScriptWrapper.script,block_range,order - transactionHashList = await this.indexer.getTransactions( - generateSearchKey(this.queries), - searchKeyFilter - ); - lastCursor = transactionHashList.lastCursor; - } - - // filter by ScriptWrapper.io_type - transactionHashList.objects = this.filterByTypeIoTypeAndLockIoType( - transactionHashList.objects, - this.queries - ); - - return transactionHashList; - } - - private async getTransactionByLockAndTypeIndependent( - searchKeyFilter: SearchKeyFilter - ): Promise { - const queryWithTypeAdditionOptions = { ...searchKeyFilter }; - const queryWithLockAdditionOptions = { ...searchKeyFilter }; - if (searchKeyFilter.lastCursor) { - const [lockLastCursor, typeLastCursor] = searchKeyFilter.lastCursor.split( - "-" - ); - queryWithLockAdditionOptions.lastCursor = lockLastCursor; - queryWithTypeAdditionOptions.lastCursor = typeLastCursor; - } - const queriesWithoutType = { ...this.queries, type: undefined }; - const transactionByLock = await this.indexer.getTransactions( - generateSearchKey(queriesWithoutType), - queryWithTypeAdditionOptions - ); - const queriesWithoutLock = { ...this.queries, lock: undefined }; - const transactionByType = await this.indexer.getTransactions( - generateSearchKey(queriesWithoutLock), - queryWithLockAdditionOptions - ); - - const intersection = ( - transactionList1: GetTransactionsResult[], - transactionList2: GetTransactionsResult[] - ) => { - const result: GetTransactionsResult[] = []; - transactionList1.forEach((tx1) => { - const tx2 = transactionList2.find( - (item) => item.tx_hash === tx1.tx_hash - ); - if (tx2) { - // put the output io_type to intersection result, cause output have cells - const targetTx = tx1.io_type === "output" ? tx1 : tx2; - // change io_type to both cause targetTx exist both input and output - result.push({ ...targetTx, io_type: "both" }); - } - }); - return result; - }; - let hashList = intersection( - transactionByType.objects, - transactionByLock.objects - ); - const lastCursor = - transactionByLock.lastCursor + "-" + transactionByType.lastCursor; - const objects = hashList; - return { objects, lastCursor }; - } - - private getTransactionListFromRpc = async ( - transactionHashList: GetTransactionsResults - ) => { - const getDetailRequestData = transactionHashList.objects.map( - (hashItem: GetTransactionsResult, index: number) => { - return { - id: index, - jsonrpc: "2.0", - method: "get_transaction", - params: [hashItem.tx_hash], - }; - } - ); - const transactionList: TransactionWithIOType[] = await requestBatch( - this.CKBRpcUrl, - getDetailRequestData - ).then((response: GetTransactionRPCResult[]) => { - return response.map( - (item: GetTransactionRPCResult): TransactionWithIOType => { - if (!this.filterOptions.skipMissing && !item.result) { - throw new Error( - `Transaction ${ - transactionHashList.objects[item.id].tx_hash - } is missing!` - ); - } - const ioType = transactionHashList.objects[item.id].io_type; - const ioIndex = transactionHashList.objects[item.id].io_index; - return { ioType, ioIndex, ...item.result }; - } - ); - }); - return transactionList; - }; - - private getCellByOutPoint = async (output: OutPoint) => { - const res = await request( - this.CKBRpcUrl, - "get_transaction", - [output.tx_hash] - ); - return res.transaction.outputs[parseInt(output.index)]; - }; - - private isLockArgsLenMatched = ( - args: string | undefined, - argsLen?: number | "any" - ) => { - if (!argsLen) return true; - if (argsLen === "any") return true; - if (argsLen === -1) return true; - return getHexStringBytes(args as string) === argsLen; - }; - - // only valid after pass flow three validate - private isCellScriptArgsValid = (targetCell: Output) => { - if (this.queries.lock) { - let lockArgsLen = instanceOfScriptWrapper(this.queries.lock) - ? this.queries.lock.argsLen - : this.queries.argsLen; - if (!this.isLockArgsLenMatched(targetCell.lock.args, lockArgsLen)) { - return false; - } - } - - if (this.queries.type && this.queries.type !== "empty") { - let typeArgsLen = instanceOfScriptWrapper(this.queries.type) - ? this.queries.type.argsLen - : this.queries.argsLen; - if (!this.isLockArgsLenMatched(targetCell.type?.args, typeArgsLen)) { - return false; - } - } - - if (this.queries.type && this.queries.type === "empty") { - if (targetCell.type) { - return false; - } - } - - return true; - }; - - private filterByIoType = ( - inputResult: GetTransactionsResult[], - ioType: IOType - ) => { - if (ioType === "both") { - return inputResult; - } - if (ioType === "input" || ioType === "output") { - return inputResult.filter( - (item: GetTransactionsResult) => - item.io_type === ioType || item.io_type === "both" - ); - } - return inputResult; - }; - - private filterByTypeIoTypeAndLockIoType = ( - inputResult: GetTransactionsResult[], - queries: CKBIndexerQueryOptions - ) => { - let result = inputResult; - if (instanceOfScriptWrapper(queries.lock) && queries.lock.ioType) { - result = this.filterByIoType(result, queries.lock.ioType); - } - if (instanceOfScriptWrapper(queries.type) && queries.type.ioType) { - result = this.filterByIoType(result, queries.type.ioType); - } - return result; - }; - - async count(): Promise { - let lastCursor: undefined | string = undefined; - const getTxWithCursor = async (): Promise => { - const result: GetTransactionDetailResult = await this.getTransactions( - lastCursor - ); - lastCursor = result.lastCursor; - return result.objects; - }; - let counter = 0; - //skip query result in first query - let txs: TransactionWithStatus[] = await getTxWithCursor(); - if (txs.length === 0) { - return 0; - } - let buffer: Promise = getTxWithCursor(); - let index: number = 0; - while (true) { - if (this.queries.skip && index < this.queries.skip) { - index++; - continue; - } - counter += 1; - index++; - //reset index and exchange `txs` and `buffer` after count last tx - if (index === txs.length) { - index = 0; - txs = await buffer; - // break if can not get more txs - if (txs.length === 0) { - break; - } - buffer = getTxWithCursor(); - } - } - return counter; - } - async getTransactionHashes(): Promise { - let lastCursor: undefined | string = undefined; - const getTxWithCursor = async (): Promise => { - const result = await this.getTxHashesWithCursor( - lastCursor - ); - lastCursor = result.lastCursor; - return result.objects; - }; - - let transactionHashes: string[] = []; - //skip query result in first query - let txs = await getTxWithCursor(); - if (txs.length === 0) { - return []; - } - let buffer = getTxWithCursor(); - let index: number = 0; - while (true) { - if (this.queries.skip && index < this.queries.skip) { - index++; - continue; - } - if (txs[index]?.tx_hash) { - transactionHashes.push(txs[index].tx_hash as string); - } - index++; - //reset index and exchange `txs` and `buffer` after count last tx - if (index === txs.length) { - index = 0; - txs = await buffer; - // break if can not get more txs - if (txs.length === 0) { - break; - } - buffer = getTxWithCursor(); - } - } - return transactionHashes; - } - async *collect() { - let lastCursor: undefined | string = undefined; - const getTxWithCursor = async (): Promise => { - const result: GetTransactionDetailResult = await this.getTransactions( - lastCursor - ); - lastCursor = result.lastCursor; - return result.objects; - }; - //skip query result in first query - let txs: TransactionWithStatus[] = await getTxWithCursor(); - if (txs.length === 0) { - return 0; - } - let buffer: Promise = getTxWithCursor(); - let index: number = 0; - while (true) { - if (this.queries.skip && index < this.queries.skip) { - index++; - continue; - } - if (this.filterOptions.includeStatus) { - yield txs[index]; - } else { - yield txs[index].transaction; - } - index++; - //reset index and exchange `txs` and `buffer` after count last tx - if (index === txs.length) { - index = 0; - txs = await buffer; - // break if can not get more txs - if (txs.length === 0) { - break; - } - buffer = getTxWithCursor(); - } - } - } -} diff --git a/packages/ckb-indexer/src/type.ts b/packages/ckb-indexer/src/type.ts deleted file mode 100644 index 5823faf280..0000000000 --- a/packages/ckb-indexer/src/type.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - Cell, - Hexadecimal, - HexString, - QueryOptions, - Script, - OutPoint, - HexNumber, - Output, - TransactionWithStatus, -} from "@ckb-lumos/base"; -import { EventEmitter } from "events"; - -export enum ScriptType { - type = "type", - lock = "lock", -} - -export enum Order { - asc = "asc", - desc = "desc", -} - -export interface CKBIndexerQueryOptions extends QueryOptions { - outputDataLenRange?: HexadecimalRange; - outputCapacityRange?: HexadecimalRange; - bufferSize?: number; -} - -export type HexadecimalRange = [Hexadecimal, Hexadecimal]; -export interface SearchFilter { - script?: Script; - output_data_len_range?: HexadecimalRange; //empty - output_capacity_range?: HexadecimalRange; //empty - block_range?: HexadecimalRange; //fromBlock-toBlock -} -export interface SearchKey { - script: Script; - script_type: ScriptType; - filter?: SearchFilter; -} - -export interface GetLiveCellsResult { - last_cursor: string; - objects: IndexerCell[]; -} - -export interface rpcResponse { - status: number; - data: rpcResponseData; -} - -export interface rpcResponseData { - result: string; - error: string; -} - -export interface IndexerCell { - block_number: Hexadecimal; - out_point: OutPoint; - output: { - capacity: HexNumber; - lock: Script; - type?: Script; - }; - output_data: HexString; - tx_index: Hexadecimal; -} - -export interface TerminatorResult { - stop: boolean; - push: boolean; -} - -export declare type Terminator = ( - index: number, - cell: Cell -) => TerminatorResult; - -export type HexNum = string; -export type IOType = "input" | "output" | "both"; -export type Bytes32 = string; -export type GetTransactionsResult = { - block_number: HexNum; - io_index: HexNum; - io_type: IOType; - tx_hash: Bytes32; - tx_index: HexNum; -}; -export interface GetTransactionsResults { - lastCursor: string | undefined; - objects: GetTransactionsResult[]; -} - -export interface GetCellsResults { - lastCursor: string; - objects: Cell[]; -} - -export interface SearchKeyFilter { - sizeLimit?: number; - order?: Order; - lastCursor?: string; -} - -export interface OutputToVerify { - output: Output; - outputData: string; -} - -export class IndexerEmitter extends EventEmitter { - lock?: Script; - type?: Script; - outputData?: HexString | "any"; - argsLen?: number | "any"; - fromBlock?: bigint; -} - -export interface OtherQueryOptions { - withBlockHash: true; - ckbRpcUrl: string; -} - -export interface GetTransactionRPCResult { - jsonrpc: string; - id: number; - result: TransactionWithStatus; -} - -export interface TransactionWithIOType extends TransactionWithStatus { - inputCell?: Output; - ioType: IOType; - ioIndex: string; -} diff --git a/packages/ckb-indexer/tsconfig.json b/packages/ckb-indexer/tsconfig.json deleted file mode 100644 index ec78ef7742..0000000000 --- a/packages/ckb-indexer/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "lib", - "lib": ["es2020", "dom"], - "module": "commonjs", - "declaration": true, - "sourceMap": true - }, - "include": ["src"] -} diff --git a/packages/neuron-ui/.eslintrc.js b/packages/neuron-ui/.eslintrc.js index 318286afee..b8001f8237 100644 --- a/packages/neuron-ui/.eslintrc.js +++ b/packages/neuron-ui/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'prettier'], + extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], settings: { // https://github.com/SimulatedGREG/electron-vue/issues/423#issuecomment-464807973 'import/core-modules': ['electron'], diff --git a/packages/neuron-ui/.storybook/.babelrc b/packages/neuron-ui/.storybook/.babelrc new file mode 100644 index 0000000000..3313ff9ef0 --- /dev/null +++ b/packages/neuron-ui/.storybook/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-typescript"] +} diff --git a/packages/neuron-ui/.storybook/addons.js b/packages/neuron-ui/.storybook/addons.js deleted file mode 100644 index 194f46a605..0000000000 --- a/packages/neuron-ui/.storybook/addons.js +++ /dev/null @@ -1,4 +0,0 @@ -import '@storybook/addon-actions/register' -import '@storybook/addon-links/register' -import '@storybook/addon-viewport/register' -import '@storybook/addon-knobs/register'; diff --git a/packages/neuron-ui/.storybook/config.js b/packages/neuron-ui/.storybook/config.js deleted file mode 100644 index e74164e568..0000000000 --- a/packages/neuron-ui/.storybook/config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { - configure -} from '@storybook/react' - -function loadStories() { - require('../src/stories') -} - -configure(loadStories, module) diff --git a/packages/neuron-ui/.storybook/electron.js b/packages/neuron-ui/.storybook/electron.js new file mode 100644 index 0000000000..78003d8100 --- /dev/null +++ b/packages/neuron-ui/.storybook/electron.js @@ -0,0 +1,21 @@ +const sendSyncValues = { + 'get-locale': 'zh', + 'get-version': '0.103.1', +} + +module.exports = { + ipcRenderer: { + sendSync(key) { + return sendSyncValues[key] + }, + invoke() { + return Promise.resolve({}) + }, + on() {}, + removeAllListeners() {}, + }, + clipboard() {}, + nativeImage() {}, + shell() {}, + desktopCapturer() {}, +} diff --git a/packages/neuron-ui/.storybook/main.js b/packages/neuron-ui/.storybook/main.js deleted file mode 100644 index e4e5a1a3fe..0000000000 --- a/packages/neuron-ui/.storybook/main.js +++ /dev/null @@ -1,11 +0,0 @@ -const path = require('path') - -module.exports = { - webpackFinal: config => { - config.resolve.alias = { - ...config.resolve.alias, - electron: path.join(__dirname, '..', 'src', 'electron-modules'), - } - return config - }, -} diff --git a/packages/neuron-ui/.storybook/main.ts b/packages/neuron-ui/.storybook/main.ts new file mode 100644 index 0000000000..3cf0dbf366 --- /dev/null +++ b/packages/neuron-ui/.storybook/main.ts @@ -0,0 +1,24 @@ +export default { + stories: ['../src/**/*.stories.mdx', '../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: {}, + }, + webpackFinal: config => { + config.resolve.alias = { + ...config.resolve.alias, + electron: require.resolve('./electron'), + } + return config + }, + docs: { + autodocs: true, + }, +} diff --git a/packages/neuron-ui/.storybook/preview.tsx b/packages/neuron-ui/.storybook/preview.tsx new file mode 100644 index 0000000000..e77da948c7 --- /dev/null +++ b/packages/neuron-ui/.storybook/preview.tsx @@ -0,0 +1,46 @@ +import 'theme' +import 'styles/index.scss' +import 'styles/layout.scss' +import 'styles/theme.scss' +import 'utils/i18n' +import 'stories/styles.scss' +import React from 'react' +import { action } from '@storybook/addon-actions' +import { NeuronWalletContext, initStates } from '../src/states' + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +} + +const dispatch = action('Dispatch') + +BigInt.prototype['toJSON'] = function () { + return this.toString() +} + +export const decorators = [ + (Story, { argTypes, args }) => { + const globalArgTypes = Object.keys(argTypes).filter(v => argTypes[v]?.isGlobal) + const globalStates = globalArgTypes.reduce( + (pre, cur) => + args[cur] + ? { + ...pre, + [cur]: args[cur], + } + : pre, + initStates + ) + return ( + + + + ) + }, +] diff --git a/packages/neuron-ui/package.json b/packages/neuron-ui/package.json index 7d29bfbc47..93ad46e5ec 100644 --- a/packages/neuron-ui/package.json +++ b/packages/neuron-ui/package.json @@ -1,6 +1,6 @@ { "name": "neuron-ui", - "version": "0.110.2", + "version": "0.110.3", "private": true, "author": { "name": "Nervos Core Dev", @@ -19,18 +19,28 @@ "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", - "clean": "npx rimraf build/*", + "clean": "npx rimraf build", "precommit": "lint-staged", - "storybook": "start-storybook -p 9009 -s public", - "build-storybook": "build-storybook -s public" + "storybook": "storybook dev -p 9009 -s public", + "build-storybook": "storybook build -s public" }, "publishConfig": { "registry": "https://registry.npmjs.org/" }, "lint-staged": { - "src/**/*.{ts,tsx}": [ + ".storybook/**/*.{js,cjs,mjs,jsx,ts,tsx}": [ + "prettier --ignore-path ../../.prettierignore --write", "eslint --fix", "git add" + ], + "src/**/*.{js,cjs,mjs,jsx,ts,tsx}": [ + "prettier --ignore-path ../../.prettierignore --write", + "eslint --fix", + "git add" + ], + "src/**/*.{css,scss}": [ + "prettier --ignore-path ../../.prettierignore --write", + "git add" ] }, "jest": { @@ -40,10 +50,8 @@ "last 2 chrome versions" ], "dependencies": { - "@nervosnetwork/ckb-sdk-core": "0.107.0", - "@nervosnetwork/ckb-sdk-utils": "0.107.0", - "@uifabric/experiments": "7.45.14", - "@uifabric/styling": "7.25.1", + "@nervosnetwork/ckb-sdk-core": "0.109.0", + "@nervosnetwork/ckb-sdk-utils": "0.109.0", "canvg": "2.0.0", "i18next": "21.10.0", "immer": "9.0.21", @@ -53,31 +61,32 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-i18next": "12.1.5", - "react-router-dom": "5.1.2", - "sass": "1.62.0" + "react-router-dom": "6.14.0", + "react-transition-group": "4.4.5", + "sass": "1.63.6" }, "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "7.21.0", - "@storybook/addon-actions": "5.3.18", - "@storybook/addon-knobs": "5.3.21", - "@storybook/addon-links": "5.3.18", - "@storybook/addon-storyshots": "5.3.18", - "@storybook/addon-viewport": "5.3.18", - "@storybook/addons": "5.3.18", - "@storybook/cli": "5.3.18", - "@storybook/react": "5.3.18", - "@types/enzyme": "3.10.12", + "@babel/plugin-proposal-private-property-in-object": "7.21.11", + "@babel/preset-typescript": "7.22.5", + "@storybook/addon-actions": "7.0.24", + "@storybook/addon-essentials": "7.0.24", + "@storybook/addon-interactions": "7.0.24", + "@storybook/addon-links": "7.0.24", + "@storybook/node-logger": "7.0.24", + "@storybook/preset-create-react-app": "7.0.24", + "@storybook/react": "7.0.24", + "@storybook/react-webpack5": "7.0.24", + "@storybook/testing-library": "0.2.0", + "@types/enzyme": "3.10.13", "@types/enzyme-adapter-react-16": "1.0.6", - "@types/node": "18.15.11", - "@types/react": "17.0.58", - "@types/react-dom": "17.0.19", + "@types/node": "18.16.18", + "@types/react": "17.0.62", + "@types/react-dom": "17.0.20", "@types/react-router-dom": "5.3.3", - "@types/storybook-react-router": "1.0.1", - "@types/storybook__addon-storyshots": "5.1.2", "@types/styled-components": "5.1.26", "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "babel-jest": "25.5.1", - "electron": "24.1.1", + "electron": "24.7.1", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.7", "eslint-config-airbnb": "19.0.4", @@ -88,7 +97,9 @@ "react-app-rewired": "2.2.1", "react-scripts": "5.0.1", "react-test-renderer": "16.14.0", - "storybook-react-router": "1.0.8" + "storybook": "7.0.24", + "storybook-addon-react-router-v6": "1.0.2", + "webpack": "5.88.0" }, "resolutions": { "react-i18next": "12.1.5" diff --git a/packages/neuron-ui/public/css/fonts.css b/packages/neuron-ui/public/css/fonts.css index ad28a36c7f..85b52a469f 100644 --- a/packages/neuron-ui/public/css/fonts.css +++ b/packages/neuron-ui/public/css/fonts.css @@ -7,3 +7,33 @@ font-family: 'SourceCodePro-Regular'; src: url('../fonts/SourceCodePro-Regular.ttf') format('truetype'); } + +@font-face { + font-family: 'ProximaNova-Regular'; + src: url('../fonts/ProximaNova-Regular.otf') format('opentype'), + url('../fonts//Proximanova-Regular.ttf') format('opentype'); +} + +@font-face { + font-family: 'ProximaNova-Semibold'; + src: url('../fonts/ProximaNova-Semibold.otf') format('opentype'), + url('../fonts/ProximaNova-Semibold.ttf') format('opentype'); +} + +@font-face { + font-family: 'D-DIN-PRO'; + src: url('../fonts/D-DIN-PRO-500-Medium.otf') format('opentype'), + url('../fonts/D-DIN-PRO-500-Medium.ttf') format('opentype'); +} + +@font-face { + font-family: 'JetBrains Mono Medium'; + src: url('../fonts/JetBrainsMonoNL-Medium.otf') format('opentype'), + url('../fonts/JetBrainsMonoNL-Medium.ttf') format('opentype'); +} + +@font-face { + font-family: 'JetBrains Mono'; + src: url('../fonts/JetBrainsMonoNL-Regular.otf') format('opentype'), + url('../fonts/JetBrainsMonoNL-Regular.ttf') format('opentype'); +} diff --git a/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.otf b/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.otf new file mode 100755 index 0000000000..d72d454eb9 Binary files /dev/null and b/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.otf differ diff --git a/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.ttf b/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.ttf new file mode 100755 index 0000000000..ca0abecb19 Binary files /dev/null and b/packages/neuron-ui/public/fonts/D-DIN-PRO-500-Medium.ttf differ diff --git a/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.otf b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.otf new file mode 100644 index 0000000000..ec04820464 Binary files /dev/null and b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.otf differ diff --git a/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.ttf b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.ttf new file mode 100644 index 0000000000..3d866abd42 Binary files /dev/null and b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Medium.ttf differ diff --git a/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.otf b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.otf new file mode 100644 index 0000000000..6aca7701fd Binary files /dev/null and b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.otf differ diff --git a/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.ttf b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.ttf new file mode 100644 index 0000000000..1aa91ec265 Binary files /dev/null and b/packages/neuron-ui/public/fonts/JetBrainsMonoNL-Regular.ttf differ diff --git a/packages/neuron-ui/public/fonts/ProximaNova-Regular.otf b/packages/neuron-ui/public/fonts/ProximaNova-Regular.otf new file mode 100644 index 0000000000..27c8d8f7bf Binary files /dev/null and b/packages/neuron-ui/public/fonts/ProximaNova-Regular.otf differ diff --git a/packages/neuron-ui/public/fonts/ProximaNova-Semibold.otf b/packages/neuron-ui/public/fonts/ProximaNova-Semibold.otf new file mode 100644 index 0000000000..11a950a079 Binary files /dev/null and b/packages/neuron-ui/public/fonts/ProximaNova-Semibold.otf differ diff --git a/packages/neuron-ui/public/fonts/ProximaNova-Semibold.ttf b/packages/neuron-ui/public/fonts/ProximaNova-Semibold.ttf new file mode 100644 index 0000000000..f406ffebce Binary files /dev/null and b/packages/neuron-ui/public/fonts/ProximaNova-Semibold.ttf differ diff --git a/packages/neuron-ui/public/fonts/Proximanova-Regular.ttf b/packages/neuron-ui/public/fonts/Proximanova-Regular.ttf new file mode 100644 index 0000000000..01c2697c7d Binary files /dev/null and b/packages/neuron-ui/public/fonts/Proximanova-Regular.ttf differ diff --git a/packages/neuron-ui/public/index.html b/packages/neuron-ui/public/index.html index e0afb78b63..bbfb71d586 100755 --- a/packages/neuron-ui/public/index.html +++ b/packages/neuron-ui/public/index.html @@ -1,13 +1,12 @@ - - - - - - - - - Neuron - - - - -
-
-
- + Neuron + + + +
+
+
+ diff --git a/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss new file mode 100644 index 0000000000..c6f6a9d5d6 --- /dev/null +++ b/packages/neuron-ui/src/components/AddressBook/addressBook.module.scss @@ -0,0 +1,220 @@ +@import '../../styles/mixin.scss'; + +.content { + background: var(--secondary-background-color); + border: 1px solid var(--divide-line-color); + border-radius: 8px; +} + +.textarea { + width: calc(100% - 32px); + padding: 16px 16px 0; + background: transparent; + border: none; + height: 206px; + resize: none; + color: var(--main-text-color); +} + +.recordTab { + position: relative; + display: flex; + box-sizing: border-box; + align-items: center; + color: var(--secondary-text-color); + border-bottom: 1px solid var(--divide-line-color); + + button { + appearance: none; + flex: 1; + height: 50px; + font-size: 14px; + font-weight: 500; + background-color: transparent; + margin: 0; + border: none; + color: var(--tabs-default-color); + cursor: pointer; + } + + .underline { + display: block; + position: absolute; + bottom: 2px; + left: 0; + width: 33%; + box-sizing: border-box; + transition: transform 0.1s ease-in-out; + &::after { + content: ''; + width: 100px; + height: 2px; + position: absolute; + top: 0; + left: calc(50% - 46px); + background: var(--primary-color); + border-radius: 8px; + } + } + + &[data-idx='1'] { + .underline { + transform: translateX(100%); + } + } + &[data-idx='2'] { + .underline { + transform: translateX(200%); + } + } +} + +.addressTable { + height: 440px; + color: var(--main-text-color); +} + +.container { + width: 680px; + + .qrCodeContainer { + width: 354px; + @include card; + flex-shrink: 0; + .qrCode { + margin-top: 48px; + } + } + + .copyBalance { + height: 32px; + line-height: 32px; + min-width: 80px; + } + + .balance { + padding-left: 26px; + } + + .address { + height: 56px; + display: flex; + cursor: pointer; + + & > span { + line-height: 56px; + font-family: 'JetBrains Mono'; + } + + .overflow { + word-break: break-all; + line-height: 56px; + overflow: hidden; + cursor: pointer; + } + + &:hover { + color: var(--activity-color); + } + } + + .copyTableAddress { + @include copyAddress; + } + + .addresses { + flex-grow: 1; + height: 100%; + overflow-y: hidden; + } + + .description { + height: 56px; + line-height: 56px; + min-width: 100px; + .descTips { + @media screen and (max-width: 1400px) { + transform: translateX(-70%) !important; + + & > div:nth-last-child(1) { + right: 25%; + } + } + } + + &:hover { + color: var(--activity-color); + } + svg { + cursor: pointer; + margin-left: 4px; + g { + fill: var(--primary-color); + } + } + .descText { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + cursor: pointer; + } + } + + .addressTip { + @media screen and (max-width: 1400px) { + transform: translateX(-40%) !important; + } + } + + .descTipRoot { + position: relative; + width: 208px; + .autoHeight { + width: 100%; + height: 100%; + position: absolute; + + & > svg { + position: absolute; + right: 0; + bottom: 8px; + } + } + .descInput { + border: none; + word-break: break-word; + resize: none; + width: 100%; + height: 100%; + line-height: 24px; + background-color: transparent; + color: var(--main-text-color); + caret-color: #000000; + pointer-events: none; + + @media (prefers-color-scheme: dark) { + caret-color: #fff; + } + } + & > .hidden { + word-break: break-word; + white-space: break-spaces; + visibility: hidden; + min-width: 100px; + min-height: 24px; + padding: 2px; + line-height: 24px; + } + } +} + +@media screen and (max-width: 1330px) { + .container { + .balance { + padding-left: 18px; + } + .txCount { + padding-left: 64px; + } + } +} diff --git a/packages/neuron-ui/src/components/AddressBook/index.tsx b/packages/neuron-ui/src/components/AddressBook/index.tsx new file mode 100644 index 0000000000..02a9c597ba --- /dev/null +++ b/packages/neuron-ui/src/components/AddressBook/index.tsx @@ -0,0 +1,206 @@ +import React, { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useState as useGlobalState, useDispatch } from 'states' +import Dialog from 'widgets/Dialog' +import CopyZone from 'widgets/CopyZone' +import { Copy, Edit } from 'widgets/Icons/icon' +import Table, { TableProps, SortType } from 'widgets/Table' +import { shannonToCKBFormatter, useLocalDescription } from 'utils' +import { HIDE_BALANCE } from 'utils/const' +import Tooltip from 'widgets/Tooltip' +import styles from './addressBook.module.scss' + +enum TabIdx { + All = '0', + Receive = '1', + Change = '2', +} + +const AddressBook = ({ onClose }: { onClose?: () => void }) => { + const { wallet } = useGlobalState() + const [t] = useTranslation() + const { addresses, id: walletId } = wallet + + const [tabIdx, setTabIdx] = useState(TabIdx.All) + const onTabClick = (e: React.SyntheticEvent) => { + const { + dataset: { idx }, + } = e.target as HTMLDivElement + if (idx) { + setTabIdx(idx as TabIdx) + } + } + + const tableData = useMemo(() => { + if (tabIdx === TabIdx.Receive) { + return addresses.filter(item => item.type === 0) + } + if (tabIdx === TabIdx.Change) { + return addresses.filter(item => item.type !== 0) + } + return addresses + }, [tabIdx, addresses]) + + const dispatch = useDispatch() + const { localDescription, onDescriptionPress, onDescriptionChange, onDescriptionFieldBlur, onDescriptionSelected } = + useLocalDescription('address', walletId, dispatch, 'textarea') + + const columns = useMemo['columns']>( + () => [ + { + title: t('addresses.type'), + dataIndex: 'type', + align: 'left', + width: '80px', + render(type) { + return type === 0 ? t('addresses.receiving-address') : t('addresses.change-address') + }, + }, + { + title: t('addresses.address'), + dataIndex: 'address', + align: 'left', + render(itemAddress: string) { + return ( + + {itemAddress} + + + } + showTriangle + isTriggerNextToChild + tipClassName={styles.addressTip} + > +
+ {itemAddress.slice(0, 6)} + ... + {itemAddress.slice(-6)} +
+
+ ) + }, + }, + { + title: t('addresses.description'), + dataIndex: 'description', + align: 'center', + render(description: string, _idx, item) { + const isSelected = localDescription.key === item.address + return ( + +
+