diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ff93fc11..c3f143bb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,107 +1,72 @@ -# Each PR will build preview site that help to check code is work as expect. - name: Build on: - pull_request: - types: [opened, synchronize, reopened] push: + branches: + # - develop + - feat/for_desktop + - feat/for_desktop* tags: - '*' +defaults: + run: + shell: bash -leo pipefail {0} + jobs: # Prepare node modules. Reuse cache if available setup: name: prepare build - runs-on: ubuntu-latest + runs-on: [self-hosted, X64, Linux, builder] env: NODE_OPTIONS: '--max_old_space_size=4096' steps: - name: checkout uses: actions/checkout@v3 + - name: Env Test + id: env-test + run: | + echo "whoami $(whoami)" + echo "shell is $(echo $0)" + echo "which node $(which node)" - name: Setup node uses: actions/setup-node@v3 with: node-version: '18.16.1' - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v3 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + # - uses: actions/cache@v3 + # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - - uses: actions/cache@v3 - id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: node_modules - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + # - uses: actions/cache@v3 + # id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: node_modules + # key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} - - name: Get Yarn Cache - if: steps.yarn-cache.outputs.cache-hit == 'true' - run: yarn --prefer-offline + # - name: Get Yarn Cache + # if: steps.yarn-cache.outputs.cache-hit == 'true' + # run: yarn --prefer-offline - name: Use NPM Token with organization read access uses: heisenberg-2077/use-npm-token-action@v1 with: token: '${{ secrets.NPM_AUTH_TOKEN }}' - - name: Install Dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' - run: yarn install --frozen-lockfile + # - name: Install Dependencies + # if: steps.yarn-cache.outputs.cache-hit != 'true' + # run: yarn install --frozen-lockfile build-pro: name: build pro - runs-on: ubuntu-latest - needs: setup - env: - NODE_OPTIONS: '--max_old_space_size=4096' - steps: - - name: checkout - uses: actions/checkout@v3 - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v3 - id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - - - uses: actions/cache@v3 - id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) - with: - path: node_modules - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} - - - name: build - run: yarn build:pro - - - name: Upload artifact - uses: actions/upload-artifact@v3 - if: ${{ github.event_name == 'pull_request' }} - with: - name: Rabby_${{github.sha}} - path: dist - retention-days: 7 - - - name: Upload artifact - uses: actions/upload-artifact@v3 - if: ${{ github.event_name == 'push' }} - with: - name: Rabby_${{github.ref_name}} - path: dist - retention-days: 7 - - build-debug: - name: build debug - runs-on: ubuntu-latest + runs-on: [self-hosted, X64, Linux, builder] needs: setup env: NODE_OPTIONS: '--max_old_space_size=4096' @@ -123,23 +88,79 @@ jobs: id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: node_modules - key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('patches/*.patch') }} - name: build - run: yarn build:debug - - - name: Upload artifact - uses: actions/upload-artifact@v3 - if: ${{ github.event_name == 'pull_request' }} - with: - name: Rabby_${{github.sha}}_debug - path: dist - retention-days: 7 - - - name: Upload artifact - uses: actions/upload-artifact@v3 - if: ${{ github.event_name == 'push' }} - with: - name: Rabby_${{github.ref_name}}_debug - path: dist - retention-days: 7 + run: | + sh ./scripts/pack.sh; + env: + RABBY_BUILD_BUCKET: ${{ secrets.RABBY_BUILD_BUCKET }} + LARK_CHAT_URL: ${{ secrets.LARK_CHAT_URL }} + LARK_CHAT_SECRET: ${{ secrets.LARK_CHAT_SECRET }} + # see more details on https://docs.github.com/en/actions/learn-github-actions/contexts#github-context + ACTIONS_JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GIT_COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + GIT_REF_NAME: ${{ github.ref_name }} + GIT_REF_URL: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TRIGGERING_ACTOR: ${{ github.triggering_actor }} + # - name: Upload artifact + # uses: actions/upload-artifact@v3 + # if: ${{ github.event_name == 'pull_request' }} + # with: + # name: Rabby_${{github.sha}} + # path: dist + # retention-days: 7 + + # - name: Upload artifact + # uses: actions/upload-artifact@v3 + # if: ${{ github.event_name == 'push' }} + # with: + # name: Rabby_${{github.ref_name}} + # path: dist + # retention-days: 7 + + # build-debug: + # name: build debug + # runs-on: [self-hosted, X64, Linux, builder] + # needs: setup + # env: + # NODE_OPTIONS: '--max_old_space_size=4096' + # steps: + # - name: checkout + # uses: actions/checkout@v3 + + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" + + # - uses: actions/cache@v3 + # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + + # - uses: actions/cache@v3 + # id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: node_modules + # key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + + # - name: build + # run: yarn build:debug + + # - name: Upload artifact + # uses: actions/upload-artifact@v3 + # if: ${{ github.event_name == 'pull_request' }} + # with: + # name: Rabby_${{github.sha}}_debug + # path: dist + # retention-days: 7 + + # - name: Upload artifact + # uses: actions/upload-artifact@v3 + # if: ${{ github.event_name == 'push' }} + # with: + # name: Rabby_${{github.ref_name}}_debug + # path: dist + # retention-days: 7 diff --git a/.github/workflows/build_debug.yml b/.github/workflows/build_debug.yml new file mode 100644 index 000000000..f693c02f3 --- /dev/null +++ b/.github/workflows/build_debug.yml @@ -0,0 +1,95 @@ +# Each PR will build debug that help to check if build works as expect. + +name: Test Build + +on: + pull_request: + types: [opened, synchronize, reopened] + +defaults: + run: + shell: bash -leo pipefail {0} + +jobs: + # Prepare node modules. Reuse cache if available + setup: + name: prepare build + runs-on: [self-hosted, X64, Linux, builder] + env: + NODE_OPTIONS: '--max_old_space_size=4096' + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Env Test + id: env-test + run: | + echo "whoami $(whoami)" + echo "shell is $(echo $0)" + echo "which node $(which node)" + + # - name: Setup node + # uses: actions/setup-node@v3 + # with: + # node-version: '16.14' + # cache: 'yarn' + + # - name: Get yarn cache directory path + # id: yarn-cache-dir-path + # run: echo "::set-output name=dir::$(yarn cache dir)" + + # - uses: actions/cache@v3 + # id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + + # - uses: actions/cache@v3 + # id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + # with: + # path: node_modules + # key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + + # - name: Get Yarn Cache + # if: steps.yarn-cache.outputs.cache-hit == 'true' + # run: yarn --prefer-offline + + - name: Use NPM Token with organization read access + uses: heisenberg-2077/use-npm-token-action@v1 + with: + token: '${{ secrets.NPM_AUTH_TOKEN }}' + + # - name: Install Dependencies + # if: steps.yarn-cache.outputs.cache-hit != 'true' + # run: yarn install --frozen-lockfile + + build-debug: + name: build debug + runs-on: [self-hosted, X64, Linux, builder] + needs: setup + env: + NODE_OPTIONS: '--max_old_space_size=4096' + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v3 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + + - uses: actions/cache@v3 + id: yarn-node_modules # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: node_modules + key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }} + + - name: build + run: | + export NO_UPLOAD=1; + sh ./scripts/pack.sh; \ No newline at end of file diff --git a/.github/workflows/private.yaml b/.github/workflows/private.yaml index 1d6602111..60283e779 100644 --- a/.github/workflows/private.yaml +++ b/.github/workflows/private.yaml @@ -1,13 +1,16 @@ name: Triger Private Build on: - pull_request: - types: [opened, synchronize, reopened] + # pull_request: + # types: [opened, synchronize, reopened] push: branches: - - develop - tags: - - v* + - dont_trigger + # - develop + # - feat/for_desktop + # - feat/for_desktop* + # tags: + # - v* jobs: create-new-private-build: diff --git a/.gitignore b/.gitignore index cf63bf973..a046ec912 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dist-* .DS_Store yarn-error -tmp/*.zip \ No newline at end of file +tmp/*.zip +tmp diff --git a/_raw/blank.html b/_raw/blank.html new file mode 100644 index 000000000..162208cae --- /dev/null +++ b/_raw/blank.html @@ -0,0 +1,7 @@ + + + RabbyX Blank Page + + + + diff --git a/_raw/vendor/matomo.js b/_raw/vendor/matomo.js index 4a9e210ab..d5e1e98c9 100644 --- a/_raw/vendor/matomo.js +++ b/_raw/vendor/matomo.js @@ -7,7 +7,7 @@ _paq.push(['enableLinkTracking']); chrome.storage.local.get('extensionId', function (result) { var u = 'https://matomo.debank.com/'; _paq.push(['setTrackerUrl', u + 'matomo.php']); - _paq.push(['setSiteId', '2']); + _paq.push(['setSiteId', '4']); if (result.extensionId) { _paq.push(['setVisitorId', result.extensionId]); } diff --git a/build/paths.js b/build/paths.js index c3c56b9d6..19db50f7c 100644 --- a/build/paths.js +++ b/build/paths.js @@ -1,18 +1,26 @@ const path = require('path'); const fs = require('fs'); -const appRoot = fs.realpathSync(process.cwd()); +const appRoot = fs.realpathSync(path.resolve(__dirname, '..')); + +let desktopRepo = process.env.RABBY_DESKTOP_REPO || path.join(appRoot, '../RabbyDesktop'); +if (!fs.existsSync(desktopRepo)) { + console.log('RabbyDesktop repo not found at ' + desktopRepo); + desktopRepo = null; +} else { + console.log('Using RabbyDesktop repo at ' + desktopRepo); +}; const rootResolve = path.resolve.bind(path, appRoot); module.exports = { root: appRoot, src: rootResolve('src'), - popupHtml: rootResolve('src/ui/popup.html'), + popupHtml: rootResolve('src/ui/popup.ejs'), notificationHtml: rootResolve('src/ui/notification.html'), indexHtml: rootResolve('src/ui/index.html'), backgroundHtml: rootResolve('src/background/background.html'), - dist: rootResolve('dist'), + dist: desktopRepo ? path.resolve(desktopRepo, './assets/chrome_exts/rabby') : rootResolve('dist'), rootResolve, } diff --git a/build/webpack.common.config.js b/build/webpack.common.config.js index 1f0abe33f..46d79542e 100644 --- a/build/webpack.common.config.js +++ b/build/webpack.common.config.js @@ -40,6 +40,11 @@ const config = { sideEffects: true, test: /[\\/]pageProvider[\\/]index.ts/, loader: 'ts-loader', + options: { + compilerOptions: { + outDir: paths.dist, + }, + } }, { test: /[\\/]ui[\\/]index.tsx/, @@ -59,6 +64,7 @@ const config = { }), compilerOptions: { module: 'es2015', + outDir: paths.dist, }, }, }, @@ -97,6 +103,9 @@ const config = { }), ], }), + compilerOptions: { + outDir: paths.dist, + } }, }, ], @@ -179,9 +188,9 @@ const config = { ], }, plugins: [ - new ESLintWebpackPlugin({ - extensions: ['ts', 'tsx', 'js', 'jsx'], - }), + // new ESLintWebpackPlugin({ + // extensions: ['ts', 'tsx', 'js', 'jsx'], + // }), // new AntdDayjsWebpackPlugin(), new HtmlWebpackPlugin({ inject: true, diff --git a/package.json b/package.json index 1d4aad4e2..66f4d0a35 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.92.46", "description": "A browser plugin for DeFi users", "scripts": { - "clean": "mkdir -p dist && rm -rf dist/*", - "make-theme": "node ./scripts/make-theme.js", + "clean": "sh ./scripts/clean-assets.sh", + "make-theme": "node scripts/make-theme.js", "build:dev": "npm run clean && npm run make-theme && TAILWIND_MODE=watch webpack --progress --env config=dev", "build:pro": "npm run clean && npm run make-theme && webpack --progress --env config=pro", "build:debug": "npm run clean && npm run make-theme && webpack --progress --env config=debug", @@ -17,7 +17,7 @@ "dependencies": { "@coinbase/wallet-sdk": "3.8.0-beta.3", "@debank/common": "0.3.51", - "@debank/festats": "1.0.0", + "@debank/festats": "1.0.3", "@dnd-kit/core": "5.0.1", "@dnd-kit/sortable": "6.0.0", "@dnd-kit/utilities": "3.0.2", @@ -91,6 +91,7 @@ "human-standard-token-abi": "2.0.0", "immer": "9.0.21", "interval-promise": "1.4.0", + "jotai": "^2.1.0", "koa-compose": "4.1.0", "lodash": "4.17.21", "loglevel": "1.7.1", diff --git a/patches/@rabby-wallet+eth-lattice-keyring+1.0.5.patch b/patches/@rabby-wallet+eth-lattice-keyring+1.0.5.patch new file mode 100644 index 000000000..609d397fb --- /dev/null +++ b/patches/@rabby-wallet+eth-lattice-keyring+1.0.5.patch @@ -0,0 +1,86 @@ +diff --git a/node_modules/@rabby-wallet/eth-lattice-keyring/index.js b/node_modules/@rabby-wallet/eth-lattice-keyring/index.js +index 9e934aa..1fc8d5c 100644 +--- a/node_modules/@rabby-wallet/eth-lattice-keyring/index.js ++++ b/node_modules/@rabby-wallet/eth-lattice-keyring/index.js +@@ -446,7 +446,13 @@ class LatticeKeyring extends EventEmitter { + + async _openConnectorTab(url) { + try { ++ if (chrome && chrome.tabs) { ++ const browserTab = await chrome.tabs.create({ url: url }); ++ return { desktopChromium: browserTab }; ++ } ++ + const browserTab = window.open(url); ++ + // Preferred option for Chromium browsers. This extension runs in a window + // for Chromium so we can do window-based communication very easily. + if (browserTab) { +@@ -469,7 +475,7 @@ class LatticeKeyring extends EventEmitter { + } + + async _findTabById(id) { +- const tabs = await browser.tabs.query({}); ++ const tabs = await (chrome || browser).tabs.query({}); + return tabs.find((tab) => tab.id === id); + } + +@@ -516,7 +522,21 @@ class LatticeKeyring extends EventEmitter { + return reject(new Error('Lattice connector closed.')); + } + }, 500); +- } else if (conn.firefox) { ++ // } else if (conn.desktopChromium) { ++ // // On a Chromium browser we can just listen for a window message ++ // window.addEventListener('message', receiveMessage, false); ++ // // Watch for the open window closing before creds are sent back ++ // listenInterval = setInterval(() => { ++ // this._findTabById(conn.desktopChromium.id).then(tab => { ++ // if (!tab || !tab.url) { ++ // clearInterval(listenInterval); ++ // return reject(new Error('Lattice connector closed.')); ++ // } ++ // }) ++ // }, 500); ++ } else if (conn.desktopChromium || conn.firefox) { ++ const firefoxLikeTab = conn.desktopChromium || conn.firefox; ++ const browserHost = conn.desktopChromium ? chrome : browser; + // For Firefox we cannot use `window` in the extension and can't + // directly communicate with the tabs very easily so we use a + // workaround: listen for changes to the URL, which will contain +@@ -525,16 +545,21 @@ class LatticeKeyring extends EventEmitter { + // host permissions in your manifest file (and also `activeTab` permission) + const loginUrlParam = '&loginCache='; + listenInterval = setInterval(() => { +- this._findTabById(conn.firefox.id).then((tab) => { +- if (!tab || !tab.url) { ++ this._findTabById(firefoxLikeTab.id).then((tab) => { ++ if ( ++ (conn.firefox && (!tab || !tab.url)) ++ || (conn.desktopChromium && !tab) ++ ) { ++ clearInterval(listenInterval); + return reject(new Error('Lattice connector closed.')); + } + // If the tab we opened contains a new URL param +- const paramLoc = tab.url.indexOf(loginUrlParam); ++ const paramLoc = !tab ? -1 : tab.url.indexOf(loginUrlParam); + if (paramLoc < 0) return; + const dataLoc = paramLoc + loginUrlParam.length; + // Stop this interval + clearInterval(listenInterval); ++ + try { + // Parse the login data. It is a stringified JSON object + // encoded as a base64 string. +@@ -542,8 +567,9 @@ class LatticeKeyring extends EventEmitter { + tab.url.slice(dataLoc), + 'base64' + ).toString(); ++ + // Close the tab and return the credentials +- browser.tabs.remove(tab.id).then(() => { ++ browserHost.tabs.remove(tab.id).then(() => { + const creds = JSON.parse(_creds); + if (!creds.deviceID || !creds.password) + return reject( diff --git a/patches/@rabby-wallet+eth-trezor-keyring+2.3.0.patch b/patches/@rabby-wallet+eth-trezor-keyring+2.3.0.patch new file mode 100644 index 000000000..cd7fa8baf --- /dev/null +++ b/patches/@rabby-wallet+eth-trezor-keyring+2.3.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@rabby-wallet/eth-trezor-keyring/dist/index.js b/node_modules/@rabby-wallet/eth-trezor-keyring/dist/index.js +index 4fdd53f..27ce1aa 100644 +--- a/node_modules/@rabby-wallet/eth-trezor-keyring/dist/index.js ++++ b/node_modules/@rabby-wallet/eth-trezor-keyring/dist/index.js +@@ -134,7 +134,7 @@ class TrezorKeyring extends events_1.EventEmitter { + } + }); + if (!this.trezorConnectInitiated) { +- connect_web_1.default.init({ manifest: TREZOR_CONNECT_MANIFEST, lazyLoad: true }); ++ connect_web_1.default.init({ manifest: TREZOR_CONNECT_MANIFEST, webusb: false, lazyLoad: true }); + this.trezorConnectInitiated = true; + } + } diff --git a/patches/@rabby-wallet+page-provider+0.1.25.patch b/patches/@rabby-wallet+page-provider+0.1.25.patch new file mode 100644 index 000000000..e8a24e36e --- /dev/null +++ b/patches/@rabby-wallet+page-provider+0.1.25.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/@rabby-wallet/page-provider/dist/index.js b/node_modules/@rabby-wallet/page-provider/dist/index.js +index 5d0e448..da8eed8 100644 +--- a/node_modules/@rabby-wallet/page-provider/dist/index.js ++++ b/node_modules/@rabby-wallet/page-provider/dist/index.js +@@ -662,6 +662,14 @@ class EthereumProvider extends events.EventEmitter { + }; + // TODO: support multi request! + this.request = async (data) => { ++ if (typeof window.__rbCheckRequestable !== 'function') { ++ throw new Error(ethRpcErrors.ethErrors.rpc.invalidRequest()); ++ } ++ ++ const checked = await window.__rbCheckRequestable(data); ++ if (!checked) { ++ return ; ++ } + if (!this._isReady) { + const promise = new Promise((resolve, reject) => { + this._cacheRequestsBeforeReady.push({ diff --git a/patches/@trezor+connect-web+9.0.6.patch b/patches/@trezor+connect-web+9.0.6.patch new file mode 100644 index 000000000..5f3065f78 --- /dev/null +++ b/patches/@trezor+connect-web+9.0.6.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/@trezor/connect-web/lib/popup/index.js b/node_modules/@trezor/connect-web/lib/popup/index.js +index a323aaa..0bc361f 100644 +--- a/node_modules/@trezor/connect-web/lib/popup/index.js ++++ b/node_modules/@trezor/connect-web/lib/popup/index.js +@@ -108,11 +108,15 @@ class PopupManager extends events_1.default { + currentWindow: true, + active: true, + }, tabs => { +- this.extensionTabId = tabs[0].id; +- chrome.tabs.create({ +- url, +- index: tabs[0].index + 1, +- }, tab => { ++ var params = { ++ url: url ++ }; ++ if (Array.isArray(tabs) && tabs.length > 0) { ++ this.extensionTabId = tabs[0].id; ++ params.index = tabs[0].index + 1; ++ } ++ ++ chrome.tabs.create(params, (tab) => { + this._window = tab; + }); + }); diff --git a/scripts/clean-assets.sh b/scripts/clean-assets.sh new file mode 100644 index 000000000..c7c1e2151 --- /dev/null +++ b/scripts/clean-assets.sh @@ -0,0 +1,18 @@ +script_dir="$( cd "$( dirname "$0" )" && pwd )" +project_dir=$(dirname "$script_dir") + +if [ -z $RABBY_DESKTOP_REPO ]; then + if [ -d $project_dir/../RabbyDesktop ]; then + export RABBY_DESKTOP_REPO=$( cd "$project_dir/../RabbyDesktop" && pwd ) + echo "[clean-assets] RABBY_DESKTOP_REPO is not set, use default: $RABBY_DESKTOP_REPO" + fi +else + echo "[clean-assets] RABBY_DESKTOP_REPO is set to: $RABBY_DESKTOP_REPO" +fi + +if [ ! -z $RABBY_DESKTOP_REPO ]; then + mkdir -p $RABBY_DESKTOP_REPO/assets/chrome_exts/rabby && rm -rf $RABBY_DESKTOP_REPO/assets/chrome_exts/rabby/* && cp -r _raw/* $RABBY_DESKTOP_REPO/assets/chrome_exts/rabby +else + echo "[clean-assets] RABBY_DESKTOP_REPO is not set, skip clean assets." + rm -rf $project_dir/dist && mkdir -p $project_dir/dist && cp -r _raw/* $project_dir/dist +fi \ No newline at end of file diff --git a/scripts/notify-lark.js b/scripts/notify-lark.js new file mode 100644 index 000000000..90019084c --- /dev/null +++ b/scripts/notify-lark.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +// curl -X POST -H "Content-Type: application/json" \ +// -d '{"msg_type":"text","content":{"text":"request example"}}' +const { createHmac } = require('crypto'); +const Axios = require('axios'); + +function makeSign(secret) { + const timestamp = Date.now(); + const timeSec = Math.floor(timestamp / 1000); + const stringToSign = `${timeSec}\n${secret}`; + const hash = createHmac('sha256', stringToSign).digest(); + + const Signature = hash.toString('base64'); + + return { + timeSec, + Signature, + }; +} + +const chatURL = process.env.LARK_CHAT_URL; +const secret = process.env.LARK_CHAT_SECRET; + +if (!chatURL) { + throw new Error('LARK_CHAT_URL is not set'); +} + +if (!secret) { + throw new Error('LARK_CHAT_SECRET is not set'); +} + +// sendMessage with axios +async function sendMessage({ + downloadURL = '', + actionsJobUrl = '', + gitCommitURL = '', + gitRefURL = '', + triggers = [], +}) { + const { timeSec, Signature } = makeSign(secret); + + // dedupe + triggers = [...new Set(triggers)]; + + const headers = { + 'Content-Type': 'application/json', + 'Signature': Signature, + }; + + const body = { + timestamp: timeSec, + sign: Signature, + // msg_type: 'text', + // content: { + // text: message, + // }, + msg_type: 'post', + content: { + post: { + "zh_cn": { + "title": "πŸš€ ζ–°ηš„ Rabbyx εŒ…ζ‰“ε₯½δΊ† 🌟", + "content": [ + [ + { "tag": "text", "text": `δΈ‹θ½½ι“ΎζŽ₯: ` }, + { "tag": "a", "href": downloadURL, "text": downloadURL } + ], + [ + { "tag": "text", "text": `---------` }, + ], + [ + { "tag": "text", "text": `Actions Job: ` }, + { "tag": "a", "href": actionsJobUrl, "text": actionsJobUrl } + ], + [ + { "tag": "text", "text": `Git Commit: ` }, + { "tag": "a", "href": gitCommitURL, "text": gitCommitURL } + ], + gitRefURL && [ + { "tag": "text", "text": `Git Ref: ` }, + { "tag": "text", "text": gitRefURL } + ], + triggers.length && [ + { "tag": "text", "text": `Triggers: ` }, + { "tag": "text", "text": triggers.join(', ') } + ], + ].filter(Boolean) + } + } + } + }; + + const res = await Axios.post(chatURL, body, { headers }); + console.log(res.data); +} + +const args = process.argv.slice(2); + +if (args[0]) { + sendMessage({ + downloadURL: args[0], + actionsJobUrl: args[1] || process.env.ACTIONS_JOB_URL, + gitCommitURL: args[2] || process.env.GIT_COMMIT_URL, + gitRefURL: process.env.GIT_REF_URL, + triggers: [ + process.env.GITHUB_TRIGGERING_ACTOR, + process.env.GITHUB_ACTOR, + ].filter(Boolean) + }) +} else { + console.log('[notify-lark] no message'); +} \ No newline at end of file diff --git a/scripts/pack.sh b/scripts/pack.sh new file mode 100644 index 000000000..412bb626b --- /dev/null +++ b/scripts/pack.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env sh +set -e + +script_dir="$( cd "$( dirname "$0" )" && pwd )" +project_dir=$(dirname "$script_dir") + +if [ -d $project_dir/../RabbyDesktop ]; then + RABBY_DESKTOP_REPO=$( cd "$project_dir/../RabbyDesktop" && pwd ) +fi + +export VERSION=$(node --eval="process.stdout.write(require('./package.json').version)"); +export RABBYX_GIT_HASH=$(git rev-parse --short HEAD); +export CURRENT_TIME=$(date +%Y%m%d%H%M%S); + +TARGET_FILE=$project_dir/tmp/RabbyX-v${VERSION}-${RABBYX_GIT_HASH}.zip; + +echo "[pack] VERSION is $VERSION"; +echo "[pack] RABBY_DESKTOP_REPO is $RABBY_DESKTOP_REPO"; + +# for windows, download zip.exe from http://stahlworks.com/dev/index.php?tool=zipunzip and add to your path + +# rm -rf $RABBY_DESKTOP_REPO/assets/chrome_exts/rabby; +if [ -z $NO_BUILD ]; then + yarn; + yarn build:pro; +fi +echo "[pack] built finished"; + +DIST_DIR=$project_dir/dist; +if [ ! -z $RABBY_DESKTOP_REPO ]; then + DIST_DIR=$RABBY_DESKTOP_REPO/assets/chrome_exts/rabby; +fi + +rm -rf $project_dir/tmp/ && mkdir -p $project_dir/tmp/; +if [ -d $DIST_DIR ]; then + cd $DIST_DIR; + zip -r $TARGET_FILE ./* +else + echo "[pack] dist dir not found: $DIST_DIR"; +fi + +cd $project_dir; +cp $TARGET_FILE $project_dir/tmp/RabbyX-latest.zip + +DOWNLOAD_URL="https://download.rabby.io/_tools/RabbyX-v${VERSION}-${RABBYX_GIT_HASH}.zip" + +# upload to storage +if [ -z $NO_UPLOAD ]; then + INVALIDATION_BASE="/_tools/RabbyX-v${VERSION}-${RABBYX_GIT_HASH}*" + JSON="{'Paths': {'Quantity': 2,'Items': ['$INVALIDATION_BASE', '/_tools/RabbyX-latest.zip']}, 'CallerReference': 'cli-rabbyx-${VERSION}-${RABBYX_GIT_HASH}-${CURRENT_TIME}'}" + echo $(node -e "console.log(JSON.stringify($JSON, null, 2))") > "$project_dir/tmp/inv-batch.json" + aws s3 cp $project_dir/tmp/ s3://$RABBY_BUILD_BUCKET/rabby/_tools/ --recursive --exclude="*" --include "*.zip" --acl public-read + echo "[pack] uploaded. DOWNLOAD_URL is $DOWNLOAD_URL"; + + if [ ! -z $CI ]; then + node ./scripts/notify-lark.js "$DOWNLOAD_URL" + else + aws cloudfront create-invalidation --distribution-id E1F7UQCCQWLXXZ --invalidation-batch file://./tmp/inv-batch.json + echo "[pack] invalidation finished."; + fi +fi + +if [ ! -z $RABBY_DESKTOP_REPO ]; then + # cp to RabbyDesktop + rm -rf $RABBY_DESKTOP_REPO/release/rabbyx/ && mkdir -p $RABBY_DESKTOP_REPO/release/rabbyx/; + cp $TARGET_FILE $RABBY_DESKTOP_REPO/release/rabbyx/ + cp $TARGET_FILE $RABBY_DESKTOP_REPO/release/rabbyx/RabbyX-latest.zip +fi + +echo "[pack] finished."; diff --git a/src/background/controller/ens.ts b/src/background/controller/ens.ts new file mode 100644 index 000000000..67de8d2ed --- /dev/null +++ b/src/background/controller/ens.ts @@ -0,0 +1,24 @@ +import { CHAINS } from '@debank/common'; +import { ethers } from 'ethers'; +import { preferenceService } from '../service'; +import buildinProvider from '../utils/buildinProvider'; + +export const getResolver = async (name: string) => { + const account = await preferenceService.getCurrentAccount(); + if (!account) throw new Error('no current account'); + buildinProvider.currentProvider.currentAccount = account.address; + buildinProvider.currentProvider.currentAccountType = account.type; + buildinProvider.currentProvider.currentAccountBrand = account.brandName; + buildinProvider.currentProvider.chainId = CHAINS['ETH'].network; + + const provider = new ethers.providers.Web3Provider( + buildinProvider.currentProvider + ); + return provider.getResolver(name); +}; + +export const getEnsContentHash = async (name: string) => { + return getResolver(name).then((resolver) => { + return resolver.getContentHash(); + }); +}; diff --git a/src/background/controller/mint-rabby.ts b/src/background/controller/mint-rabby.ts new file mode 100644 index 000000000..e3d3a8d62 --- /dev/null +++ b/src/background/controller/mint-rabby.ts @@ -0,0 +1,26 @@ +import { + getMintRabbyContractAddress, + MintRabbyAbi, +} from '@/constant/mint-rabby/mint-rabby-abi'; +import { CHAINS } from '@debank/common'; +import { ethers, Contract } from 'ethers'; +import { preferenceService } from '../service'; +import buildinProvider from '../utils/buildinProvider'; + +export const initMintRabbyContract = async () => { + const account = await preferenceService.getCurrentAccount(); + if (!account) throw new Error('no current account'); + buildinProvider.currentProvider.currentAccount = account.address; + buildinProvider.currentProvider.currentAccountType = account.type; + buildinProvider.currentProvider.currentAccountBrand = account.brandName; + buildinProvider.currentProvider.chainId = CHAINS['ETH'].network; + + const contractAddress = getMintRabbyContractAddress(); + const provider = new ethers.providers.Web3Provider( + buildinProvider.currentProvider + ); + const signer = provider.getSigner(); + const contract = new Contract(contractAddress, MintRabbyAbi, signer); + + return contract; +}; diff --git a/src/background/controller/provider/controller.ts b/src/background/controller/provider/controller.ts index 8528d81ae..5353e56a3 100644 --- a/src/background/controller/provider/controller.ts +++ b/src/background/controller/provider/controller.ts @@ -480,6 +480,13 @@ class ProviderController extends BaseController { if (hash) { swapService.postSwap(chain, hash, other); } + sessionService.broadcastToDesktopOnly('transactionChanged', { + type: 'push-tx', + ...other, + value: approvalRes.value || '0x0', + hash: hash, + chain, + }); statsData.submit = true; statsData.submitSuccess = true; @@ -689,7 +696,19 @@ class ProviderController extends BaseController { if (notificationService.statsData?.signMethod) { statsData.signMethod = notificationService.statsData?.signMethod; } + // transactionHistoryService.removeSigningTx(signingTxId!); notificationService.setStatsData(statsData); + + const errMsg = e.message || JSON.stringify(e); + // notification.create( + // undefined, + // i18n.t('background.error.txPushFailed'), + // errMsg + // ); + sessionService.broadcastToDesktopOnly('transactionChanged', { + type: 'push-failed', + errMsg, + }); throw typeof e === 'object' ? e : new Error(e); } }; @@ -703,7 +722,7 @@ class ProviderController extends BaseController { @Reflect.metadata('SAFE', true) web3ClientVersion = () => { - return `Rabby/${process.env.release}`; + return `RabbyX/${globalThis.rabbyDesktop.appVersion}`; }; @Reflect.metadata('APPROVAL', ['ETHSign', () => null, { height: 390 }]) diff --git a/src/background/controller/provider/rpcFlow.ts b/src/background/controller/provider/rpcFlow.ts index da38586af..f798bb70a 100644 --- a/src/background/controller/provider/rpcFlow.ts +++ b/src/background/controller/provider/rpcFlow.ts @@ -118,7 +118,7 @@ const flowContext = flow params: { origin, name, icon }, approvalComponent: 'Connect', }, - { height: 800 } + { height: 390 } ); connectOrigins.delete(origin); permissionService.addConnectedSiteV2({ diff --git a/src/background/controller/safe.ts b/src/background/controller/safe.ts new file mode 100644 index 000000000..0345cc61a --- /dev/null +++ b/src/background/controller/safe.ts @@ -0,0 +1,40 @@ +import { validateEOASign, validateETHSign } from '@/ui/utils/gnosis'; +import { SafeTransactionDataPartial } from '@gnosis.pm/safe-core-sdk-types'; +import { isSameAddress } from '../utils'; + +export const validateConfirmation = ( + txHash: string, + signature: string, + ownerAddress: string, + type: string, + version: string, + safeAddress: string, + tx: SafeTransactionDataPartial, + networkId: number, + owners: string[] +) => { + if (!owners.find((owner) => isSameAddress(owner, ownerAddress))) return false; + switch (type) { + case 'EOA': + try { + return validateEOASign( + signature, + ownerAddress, + tx, + version, + safeAddress, + networkId + ); + } catch (e) { + return false; + } + case 'ETH_SIGN': + try { + return validateETHSign(signature, txHash, ownerAddress); + } catch (e) { + return false; + } + default: + return false; + } +}; diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index a4271e964..ee15e311b 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -97,6 +97,10 @@ import { GET_WALLETCONNECT_CONFIG } from '@/utils/walletconnect'; import { estimateL1Fee } from '@/utils/l2'; import HdKeyring from '@rabby-wallet/eth-hd-keyring'; import CoinbaseKeyring from '@rabby-wallet/eth-coinbase-keyring/dist/coinbase-keyring'; +import { getMintRabbyContractAddress } from '@/constant/mint-rabby/mint-rabby-abi'; +import { initMintRabbyContract } from './mint-rabby'; +import { validateConfirmation } from './safe'; +import { getEnsContentHash } from './ens'; const stashKeyrings: Record = {}; @@ -111,24 +115,34 @@ export class WalletController extends BaseController { isBooted = () => keyringService.isBooted(); verifyPassword = (password: string) => keyringService.verifyPassword(password); + safeVerifyPassword = async (password: string) => { + const result = { success: false, error: null as null | Error }; + try { + await keyringService.verifyPassword(password); + result.success = true; + } catch (error) { + result.success = false; + result.error = error?.message; + } - setWhitelist = async (password: string, addresses: string[]) => { - await this.verifyPassword(password); + return result; + }; + updatePassword = (oldPassword: string, newPassword: string) => + keyringService.updatePassword(oldPassword, newPassword); + + setWhitelist = async (addresses: string[]) => { whitelistService.setWhitelist(addresses); }; - addWhitelist = async (password: string, address: string) => { - await this.verifyPassword(password); + addWhitelist = async (address: string) => { whitelistService.addWhitelist(address); }; - removeWhitelist = async (password: string, address: string) => { - await this.verifyPassword(password); + removeWhitelist = async (address: string) => { whitelistService.removeWhitelist(address); }; - toggleWhitelist = async (password: string, enable: boolean) => { - await this.verifyPassword(password); + toggleWhitelist = async (enable: boolean) => { if (enable) { whitelistService.enableWhitelist(); } else { @@ -428,9 +442,10 @@ export class WalletController extends BaseController { if (!chainObj) throw new Error(t('background.error.notFindChain', { chain })); try { + let approvalTxHash: string | undefined; if (shouldTwoStepApprove) { unTriggerTxCounter.increase(3); - await this.approveToken( + approvalTxHash = await this.approveToken( chainObj.serverId, pay_token_id, spender, @@ -451,7 +466,7 @@ export class WalletController extends BaseController { if (!shouldTwoStepApprove) { unTriggerTxCounter.increase(2); } - await this.approveToken( + approvalTxHash = await this.approveToken( chainObj.serverId, pay_token_id, spender, @@ -468,10 +483,15 @@ export class WalletController extends BaseController { unTriggerTxCounter.decrease(); } + if (approvalTxHash) { + return approvalTxHash; + } + if (postSwapParams) { swapService.addTx(chain, quote.tx.data, postSwapParams); } - await this.sendRequest({ + + const tx: string = await this.sendRequest({ $ctx: needApprove && pay_token_id !== chainObj.nativeTokenAddress ? { @@ -497,7 +517,9 @@ export class WalletController extends BaseController { }, ], }); + unTriggerTxCounter.decrease(); + return tx; } catch (e) { unTriggerTxCounter.reset(); } @@ -609,11 +631,12 @@ export class WalletController extends BaseController { ...extra, }; } - await this.sendRequest({ + const txHash: string = await this.sendRequest({ $ctx, method: 'eth_sendTransaction', params: [tx], }); + return txHash; }; fetchEstimatedL1Fee = async ( @@ -1050,6 +1073,9 @@ export class WalletController extends BaseController { return this.getTotalBalanceCached([address], address, force); }; + updateAddressBalanceCache = (address: string, balance: string) => { + preferenceService.updateAddressUSDValueCache(address, Number(balance)); + }; getAddressCacheBalance = (address: string | undefined, isTestnet = false) => { if (!address) return null; if (isTestnet) { @@ -2007,6 +2033,8 @@ export class WalletController extends BaseController { transactionHistoryService.clearPendingTransactions(address); transactionWatcher.clearPendingTx(address); transactionBroadcastWatchService.clearPendingTx(address); + sessionService.broadcastToDesktopOnly('clearPendingTransactions', null); + return; }; @@ -2811,6 +2839,9 @@ export class WalletController extends BaseController { // getTxExplainCacheByApprovalId = (id: string) => // transactionHistoryService.getExplainCacheByApprovalId(id); + markTransactionAsIndexed = (address: string, chainId: number, hash: string) => + transactionHistoryService.markTransactionAsIndexed(address, chainId, hash); + getTransactionHistory = (address: string) => transactionHistoryService.getList(address); @@ -3572,6 +3603,88 @@ export class WalletController extends BaseController { await keyringService.addNewAccount(keyring); return this._setCurrentAccountFromKeyring(keyring, -1); }; + mintedRabbyTotal = async () => { + const contract = await initMintRabbyContract(); + const result = await contract.totalSupply(); + + return result.toString(); + }; + + mintedRabbyEndDateTime = async () => { + const contract = await initMintRabbyContract(); + const { publicSaleEnd } = await contract.saleDetails(); + + try { + return new Date(publicSaleEnd.toNumber() * 1000).getTime(); + } catch (e) { + return 0; + } + }; + + getMintedRabby = async () => { + const account = await preferenceService.getCurrentAccount(); + const contract = await initMintRabbyContract(); + const accountAddress = account!.address; + const result = await contract.mintedPerAddress(accountAddress); + const isMinted = (result.totalMints as BigNumber).eq(1); + + if (!isMinted) { + return false; + } + + const nfts = await openapiService.listNFT(accountAddress, true); + const contractAddress = getMintRabbyContractAddress(); + // only one token, so just return the first one + const nft = nfts.find((item) => + isSameAddress(item.contract_id, contractAddress) + ); + + if (!nft) { + return { + contractAddress, + }; + } + + return { + tokenId: nft?.inner_id, + contractAddress: nft?.contract_id, + detailUrl: nft?.detail_url, + }; + }; + + mintRabbyFee = async () => { + const contract = await initMintRabbyContract(); + const feeAmount = (await contract.zoraFeeForAmount(1)).fee; + + return feeAmount.toString(); + }; + + mintRabby = async () => { + const account = await preferenceService.getCurrentAccount(); + const contract = await initMintRabbyContract(); + const feeAmount = await this.mintRabbyFee(); + const value = `0x${new BigNumber(feeAmount).toString(16)}`; + const contractAddress = getMintRabbyContractAddress(); + + const result = await this.sendRequest({ + method: 'eth_sendTransaction', + params: [ + { + chainId: CHAINS['ETH'].id, + value, + from: account!.address, + to: contractAddress, + data: contract.interface.encodeFunctionData('purchase', [1]), + }, + ], + }); + + return result; + }; + + getEnsContentHash = getEnsContentHash; + + validateSafeConfirmation = validateConfirmation; } const wallet = new WalletController(); diff --git a/src/background/desktop-inject/bridge.ts b/src/background/desktop-inject/bridge.ts new file mode 100644 index 000000000..48a30dbe7 --- /dev/null +++ b/src/background/desktop-inject/bridge.ts @@ -0,0 +1,128 @@ +import { walletController } from "../controller"; +import { openapiService, permissionService, sessionService } from "../service"; +import { testnetOpenapiService } from "../service/openapi"; + +import { BridgePayload, runAndCatchErr } from "./utils"; + +async function onRabbyxRpcQuery (payload: BridgePayload) { + if (!payload.rpcId) { + throw new Error('[rabbyx-rpc-query] rpcId is required'); + } + + let retPayload = { + result: null, + error: null + } as any; + + switch (payload.method) { + case 'walletController.boot': { + const [password] = payload.params; + retPayload = await runAndCatchErr(() => { + return walletController.boot(password); + }, payload.method) + break; + } + case 'walletController.isBooted': { + retPayload = await runAndCatchErr(() => { + return walletController.isBooted.apply(walletController); + }, payload.method) + break; + } + case 'walletController.isUnlocked': { + retPayload = await runAndCatchErr(() => { + return walletController.isUnlocked.apply(walletController); + }, payload.method) + break; + } + case 'walletController.lockWallet': { + retPayload = await runAndCatchErr(() => { + return walletController.lockWallet.apply(walletController); + }, payload.method) + break; + } + case 'walletController.unlock': { + retPayload = await runAndCatchErr(() => { + return walletController.unlock.apply(walletController, payload.params as any); + }, payload.method) + break; + } + case 'walletController.getConnectedSites': { + retPayload = await runAndCatchErr(() => { + return walletController.getConnectedSites.apply(walletController); + }, payload.method) + break; + } + case 'walletController.importPrivateKey': { + retPayload = await runAndCatchErr(() => { + const [password] = payload.params || []; + return walletController.importPrivateKey.apply(walletController, [password]); + }, payload.method) + break; + } + case 'walletController.getAlianName': { + retPayload = await runAndCatchErr(() => { + const [address] = payload.params || []; + return walletController.getAlianName.apply(walletController, [address]); + }, payload.method) + break; + } + case 'walletController.updateAlianName': { + retPayload = await runAndCatchErr(() => { + const [address, name] = payload.params || []; + return walletController.updateAlianName.apply(walletController, [address, name]); + }, payload.method) + break; + } + default: { + const [ns, method] = payload.method.split('.'); + + if (ns === 'walletController' && typeof walletController[method] === 'function') { + retPayload = await runAndCatchErr(() => { + return walletController[method].apply(walletController, payload.params); + }, payload.method) + } else if (ns === 'permissionService' && typeof permissionService[method] === 'function') { + retPayload = await runAndCatchErr(() => { + return permissionService[method].apply(permissionService, payload.params); + }, payload.method) + } else if (ns === 'sessionService' && typeof sessionService[method] === 'function') { + retPayload = await runAndCatchErr(() => { + return sessionService[method].apply(sessionService, payload.params); + }, payload.method) + } else if (ns === 'openapi' && typeof openapiService[method] === 'function') { + retPayload = await runAndCatchErr(() => { + return openapiService[method].apply(openapiService, payload.params); + }, payload.method) + } else if (ns === 'testnetOpenapi' && typeof testnetOpenapiService[method] === 'function') { + retPayload = await runAndCatchErr(() => { + return testnetOpenapiService[method].apply(testnetOpenapiService, payload.params); + }, payload.method) + } else { + retPayload.error = { + message: `[rabbyx-rpc-query] method ${payload.method} is not supported` + } + } + } + } + + // console.debug('[debug] retPayload', retPayload); + + window.rabbyDesktop.ipcRenderer.sendMessage('rabbyx-rpc-respond', JSON.stringify({ + rpcId: payload.rpcId, + result: retPayload?.result, + error: retPayload?.error, + })); +} + +if (window.rabbyDesktop?.ipcRenderer.on) { + console.warn('[debug] window.rabbyDesktop?.ipcRenderer.on', window.rabbyDesktop?.ipcRenderer.on); + window.rabbyDesktop?.ipcRenderer.on('rabbyx-rpc-query', onRabbyxRpcQuery); +} else { + document.addEventListener('rabbyx-rpc-query', (e: any) => { + onRabbyxRpcQuery(e.detail); + }); +} + +(window as any)._walletController = walletController; +(window as any)._permissionService = permissionService; +(window as any)._sessionService = sessionService; +(window as any)._openApi = openapiService; diff --git a/src/background/desktop-inject/type.d.ts b/src/background/desktop-inject/type.d.ts new file mode 100644 index 000000000..a67a7bcca --- /dev/null +++ b/src/background/desktop-inject/type.d.ts @@ -0,0 +1,40 @@ +interface Window { + rabbyDesktop: { + ipcRenderer: { + sendMessage( + channel: T, + ...args: any[] + ): void; + invoke( + channel: U, + ...args: any[] + ): Promise; + on: { + ( + channel: T, + func: (...args: any[]) => void + ): (() => void) | undefined; + ( + channel: T, + func: (event: any) => void + ): (() => void) | undefined; + }; + once: { + ( + channel: T, + func: (...args: any[]) => void + ): (() => void) | undefined; + ( + channel: T, + func: (event: any) => void + ): (() => void) | undefined; + }; + }; + rendererHelpers: { + b64ToObjLink: (b64: string) => string; + bufToObjLink: (buf: Buffer | Uint8Array) => string; + + formatDappURLToShow: (dappURL: string) => string; + }; + }; +} \ No newline at end of file diff --git a/src/background/desktop-inject/utils.ts b/src/background/desktop-inject/utils.ts new file mode 100644 index 000000000..418be3daa --- /dev/null +++ b/src/background/desktop-inject/utils.ts @@ -0,0 +1,36 @@ +export type BridgePayload = { + rpcId: string + method: string + params: any[] +} + +type LooseErrorObj = { + code?: string; + message?: string; + stack?: string; +} +export async function runAndCatchErr(proc: Function, mark?: string): Promise<{ + result: T | null + error?: LooseErrorObj +}> { + try { + console.debug('[debug] runAndCatchErr:: mark', mark); + const result = await proc(); + // console.debug('[debug] runAndCatchErr:: result', result); + + return { + result: result + }; + } catch (err) { + console.error('runAndCatchErr:: err occured', err) + + return { + result: null, + error: { + code: (err as any).code, + message: err.message, + stack: err.stack, + } + }; + } +} \ No newline at end of file diff --git a/src/background/index.ts b/src/background/index.ts index 90825283a..5ac608ded 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,7 +1,9 @@ +/// + import { groupBy } from 'lodash'; import 'reflect-metadata'; import * as Sentry from '@sentry/browser'; -import browser from 'webextension-polyfill'; +import browser, { Runtime } from 'webextension-polyfill'; import { ethErrors } from 'eth-rpc-errors'; import { WalletController } from 'background/controller/wallet'; import { Message } from '@/utils/message'; @@ -40,6 +42,8 @@ import { getSentryEnv } from '@/utils/env'; import { matomoRequestEvent } from '@/utils/matomo-request'; import { testnetOpenapiService } from './service/openapi'; +import './desktop-inject/bridge'; + dayjs.extend(utc); setPopupIcon('default'); @@ -51,7 +55,11 @@ let appStoreLoaded = false; Sentry.init({ dsn: 'https://e871ee64a51b4e8c91ea5fa50b67be6b@o460488.ingest.sentry.io/5831390', - release: process.env.release, + release: globalThis.rabbyDesktop.appVersion, + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + // We recommend adjusting this value in production + tracesSampleRate: 1.0, environment: getSentryEnv(), ignoreErrors: [ 'Transport error: {"event":"transport_error","params":["Websocket connection failed"]}', @@ -78,6 +86,8 @@ function initAppMeta() { async function restoreAppState() { const keyringState = await storage.get('keyringState'); + if (!await storage.get('keyringStateBackup_')) storage.set('keyringStateBackup_', keyringState); + keyringService.loadStore(keyringState); keyringService.store.subscribe((value) => storage.set('keyringState', value)); await openapiService.init(); @@ -107,6 +117,8 @@ async function restoreAppState() { transactionBroadcastWatchService.roll(); initAppMeta(); startEnableUser(); + + window.rabbyDesktop.ipcRenderer.sendMessage('rabbyx-initialized', Date.now()); } restoreAppState(); @@ -175,21 +187,25 @@ restoreAppState(); interval = null; } }); + + keyringService.on('beforeUpdatePassword', () => { + storage.set('keyringStateBackup_', keyringService.store.getState()); + }); } -// for page provider -browser.runtime.onConnect.addListener((port) => { +const onConnectListner = async (port: Runtime.Port) => { if ( port.name === 'popup' || port.name === 'notification' || - port.name === 'tab' + port.name === 'tab' || + port.name === 'rabbyDesktop' ) { const pm = new PortMessage(port); pm.listen((data) => { if (data?.type) { switch (data.type) { case 'broadcast': - eventBus.emit(data.method, data.params); + eventBus.emit(data.method, data.params); break; case 'openapi': if (walletController.openapi[data.method]) { @@ -197,6 +213,8 @@ browser.runtime.onConnect.addListener((port) => { null, data.params ); + } else { + console.error(`[onConnectListner][port:${port.name}] ${data.method} not found in walletController.openapi, check if you have implemented it.`) } break; case 'testnetOpenapi': @@ -209,8 +227,10 @@ browser.runtime.onConnect.addListener((port) => { break; case 'controller': default: - if (data.method) { + if (walletController[data.method]) { return walletController[data.method].apply(null, data.params); + } else { + console.error(`[onConnectListner][port:${port.name}] ${data.method} not found in walletController, check if you have implemented it.`) } } } @@ -294,6 +314,16 @@ browser.runtime.onConnect.addListener((port) => { port.onDisconnect.addListener((port) => { subscriptionManager.destroy(); }); +} + +// for other extension's such as rabby desktop's shell +browser.runtime.onConnectExternal.addListener(function(port) { + onConnectListner(port); +}); + +// for page provider +browser.runtime.onConnect.addListener((port) => { + onConnectListner(port); }); declare global { diff --git a/src/background/service/contactBook.ts b/src/background/service/contactBook.ts index 5a1fe152e..d1acd0fd9 100644 --- a/src/background/service/contactBook.ts +++ b/src/background/service/contactBook.ts @@ -1,4 +1,6 @@ import { createPersistStore } from 'background/utils'; +import { BROADCAST_TO_UI_EVENTS } from '@/utils/broadcastToUI'; +import { syncStateToUI } from '../utils/broadcastToUI'; export interface ContactBookItem { name: string; diff --git a/src/background/service/keyring/eth-ledger-bridge-keyring.ts b/src/background/service/keyring/eth-ledger-bridge-keyring.ts index ae89a580f..f67bf19df 100644 --- a/src/background/service/keyring/eth-ledger-bridge-keyring.ts +++ b/src/background/service/keyring/eth-ledger-bridge-keyring.ts @@ -481,16 +481,10 @@ class LedgerBridgeKeyring extends EventEmitter { async _reconnect() { if (this.isWebHID) { await this.cleanUp(); - - let count = 0; // wait connect the WebHID - while (!this.app) { - await this.makeApp(); - await wait(() => { - if (count++ > 50) { - throw new Error('Ledger: Failed to connect to Ledger'); - } - }, 100); + await this.makeApp(); + if (!this.app) { + throw new Error('Ledger: Failed to connect to Ledger'); } } } diff --git a/src/background/service/keyring/index.ts b/src/background/service/keyring/index.ts index bb357d001..c29e506a0 100644 --- a/src/background/service/keyring/index.ts +++ b/src/background/service/keyring/index.ts @@ -39,6 +39,8 @@ import { generateAliasName } from '@/utils/account'; import * as Sentry from '@sentry/browser'; import { GET_WALLETCONNECT_CONFIG } from '@/utils/walletconnect'; +import './patch'; + export const KEYRING_SDK_TYPES = { SimpleKeyring, HdKeyring, @@ -122,13 +124,42 @@ export class KeyringService extends EventEmitter { this.store = new ObservableStore(initState); } - async boot(password: string) { + // /** + // * @description reset lock and clear all keyrings + // */ + // async resetKeyringState() { + // this.password = null; + // this.store.updateState({ booted: '', vault: '' }); + + // this.memStore.updateState({ isUnlocked: false, keyrings: [] }); + // this.keyrings = []; + + // this.emit('resetKeyringState'); + // } + + async _setupBoot(password: string) { this.password = password; const encryptBooted = await this.encryptor.encrypt(password, 'true'); this.store.updateState({ booted: encryptBooted }); + } + + async boot(password: string) { + this._setupBoot(password); this.memStore.updateState({ isUnlocked: true }); } + async updatePassword(oldPassword: string, newPassword: string) { + await this.verifyPassword(oldPassword); + + this.emit('beforeUpdatePassword', { + keyringState: this.store.getState() + }); + + // reboot it + this._setupBoot(newPassword); + this.persistAllKeyrings(); + } + isBooted() { return !!this.store.getState().booted; } diff --git a/src/background/service/keyring/patch.ts b/src/background/service/keyring/patch.ts new file mode 100644 index 000000000..995cd1508 --- /dev/null +++ b/src/background/service/keyring/patch.ts @@ -0,0 +1,6 @@ +// patch trezor-like +import TrezorConnect from '@trezor/connect-web'; +// import OneKeyConnect from '@onekeyfe/connect'; + +(globalThis as any)._TrezorConnect = TrezorConnect; +// (globalThis as any)._OnekeyConnect = OneKeyConnect; diff --git a/src/background/service/notification.ts b/src/background/service/notification.ts index 421bb0cfc..91a2e7c12 100644 --- a/src/background/service/notification.ts +++ b/src/background/service/notification.ts @@ -336,13 +336,16 @@ class NotificationService extends Events { return; } } - if (this.notifiWindowId !== null) { browser.windows.update(this.notifiWindowId, { focused: true, }); } else { - this.openNotification(approval.winProps); + this.openNotification( + approval.winProps, + false, + approval.data.approvalComponent + ); } }); }; @@ -381,7 +384,7 @@ class NotificationService extends Events { this.isLocked = true; }; - openNotification = (winProps, ignoreLock = false) => { + openNotification = (winProps, ignoreLock = false, approvalType?: string) => { // Only use ignoreLock flag when approval exist but no notification window exist if (!ignoreLock) { if (this.isLocked) return; @@ -391,6 +394,10 @@ class NotificationService extends Events { winMgr.remove(this.notifiWindowId); this.notifiWindowId = null; } + if (approvalType) { + winProps.query = `type=${approvalType}`; + } + winMgr.openNotification(winProps).then((winId) => { this.notifiWindowId = winId!; }); diff --git a/src/background/service/preference.ts b/src/background/service/preference.ts index 399a903f0..baaa941ee 100644 --- a/src/background/service/preference.ts +++ b/src/background/service/preference.ts @@ -21,7 +21,7 @@ import { syncStateToUI } from '../utils/broadcastToUI'; import { BROADCAST_TO_UI_EVENTS } from '@/utils/broadcastToUI'; import dayjs from 'dayjs'; -const version = process.env.release || '0'; +const version = globalThis.rabbyDesktop.appVersion || '0'; export interface Account { type: string; @@ -418,6 +418,18 @@ class PreferenceService { getPopupOpen = () => this.popupOpen; + updateAddressUSDValueCache = (address: string, balance: number) => { + const balanceMap = this.store.balanceMap || {}; + const before = this.store.balanceMap[address.toLowerCase()]; + this.store.balanceMap = { + ...balanceMap, + [address.toLowerCase()]: { + total_usd_value: balance, + chain_list: before.chain_list || [], + }, + }; + }; + updateTestnetAddressBalance = ( address: string, data: TotalBalanceResponse diff --git a/src/background/service/session.ts b/src/background/service/session.ts index 79dc39502..06ee0739c 100644 --- a/src/background/service/session.ts +++ b/src/background/service/session.ts @@ -61,6 +61,7 @@ const getOrCreateSession = (id: number, origin: string) => { const createSession = (key: string, data?: null | SessionProp) => { const session = new Session(data); sessionMap.set(key, session); + broadcastToDesktopOnly('createSession', key); return session; }; @@ -78,7 +79,19 @@ const deleteSession = (key: string) => { sessionMap.delete(key); }; -const broadcastEvent = (ev, data?, origin?: string) => { +const broadcastToDesktopOnly = (ev: string, data?: any, origin?: string) => { + window.rabbyDesktop?.ipcRenderer.sendMessage( + '__internal_rpc:rabbyx:on-session-broadcast', + { + event: ev, + data, + origin, + } + ); +}; + +const broadcastEvent = (ev: string, data?: any, origin?: string) => { + broadcastToDesktopOnly(ev, data, origin); let sessions: { key: string; data: Session }[] = []; sessionMap.forEach((session, key) => { if (session && permissionService.hasPermission(session.origin)) { @@ -112,4 +125,5 @@ export default { deleteSession, deleteSessionsByTabId, broadcastEvent, + broadcastToDesktopOnly, }; diff --git a/src/background/service/transactionBroadcastWatcher.ts b/src/background/service/transactionBroadcastWatcher.ts index 1bac9e747..16ff044e6 100644 --- a/src/background/service/transactionBroadcastWatcher.ts +++ b/src/background/service/transactionBroadcastWatcher.ts @@ -2,6 +2,7 @@ import { findChainByID } from '@/utils/chain'; import { TxRequest } from '@rabby-wallet/rabby-api/dist/types'; import { openapiService, + sessionService, swapService, transactionWatchService, } from 'background/service'; @@ -42,6 +43,13 @@ class TransactionBroadcastWatcher { ...this.store.pendingTx, [reqId]: data, }; + + sessionService.broadcastToDesktopOnly('transactionChanged', { + type: 'submitted', + url: null, + hash: null, + chain: findChainByID(data.chainId)?.enum, + }); }; updateTx = (id: string, data: Partial) => { diff --git a/src/background/service/transactionHistory.ts b/src/background/service/transactionHistory.ts index a2e1cd366..79dbab82f 100644 --- a/src/background/service/transactionHistory.ts +++ b/src/background/service/transactionHistory.ts @@ -60,6 +60,8 @@ export interface TransactionGroup { txs: TransactionHistoryItem[]; isPending: boolean; createdAt: number; + completedAt?: number; + dbIndexed: boolean; explain: ObjectType.Merge< ExplainTxResponse, { approvalId: string; calcSuccess: boolean } @@ -73,7 +75,7 @@ export interface TransactionGroup { $ctx?: any; } -interface TxHistoryStore { +export interface TxHistoryStore { transactions: { [addr: string]: Record; }; @@ -282,6 +284,7 @@ class TxHistory { explain: explain, isFailed: false, isSubmitFailed: true, + dbIndexed: true, }, }, }); @@ -348,6 +351,7 @@ class TxHistory { explain, action: actionData, isFailed: false, + dbIndexed: false, $ctx, }, }, @@ -682,6 +686,7 @@ class TxHistory { if (!target.isPending) { return; } + target.completedAt = Date.now(); target.isPending = false; target.isFailed = !success; const index = target.txs.findIndex( @@ -971,6 +976,25 @@ class TxHistory { if (!target) return null; return target; }; + markTransactionAsIndexed(address: string, chainId: number, hash: string) { + const list = Object.values( + this.store.transactions[address.toLowerCase()] || {} + ); + const target = list.find((item) => { + return item.chainId === chainId && item.txs.find((i) => i.hash === hash); + }); + if (!target) return; + this.store.transactions = { + ...this.store.transactions, + [address.toLowerCase()]: { + ...this.store.transactions[address.toLowerCase()], + [`${chainId}-${target.nonce}`]: { + ...target, + dbIndexed: true, + }, + }, + }; + } } export default new TxHistory(); diff --git a/src/background/service/transactionWatcher.ts b/src/background/service/transactionWatcher.ts index d8eff8ab2..d5bb9e3ff 100644 --- a/src/background/service/transactionWatcher.ts +++ b/src/background/service/transactionWatcher.ts @@ -2,6 +2,7 @@ import { openapiService, i18n, transactionHistoryService, + sessionService, } from 'background/service'; import { createPersistStore, isSameAddress } from 'background/utils'; import { notification } from 'background/webapi'; @@ -61,6 +62,12 @@ class TransactionWatcher { // i18n.t('background.transactionWatcher.submitted'), // i18n.t('background.transactionWatcher.more') // ); + sessionService.broadcastToDesktopOnly('transactionChanged', { + type: 'submitted', + url, + hash, + chain, + }); }; checkStatus = async (id: string) => { @@ -121,6 +128,13 @@ class TransactionWatcher { notification.create(url, title, content, 2); + sessionService.broadcastToDesktopOnly('transactionChanged', { + type: 'finished', + success: txReceipt.status === '0x1', + hash, + chain, + }); + eventBus.emit(EVENTS.broadcastToUI, { method: EVENTS.TX_COMPLETED, params: { address, hash }, diff --git a/src/background/webapi/window.ts b/src/background/webapi/window.ts index d4a83bdc4..ffcf30393 100644 --- a/src/background/webapi/window.ts +++ b/src/background/webapi/window.ts @@ -93,10 +93,10 @@ const remove = async (winId) => { return browser.windows.remove(winId); }; -const openNotification = ({ route = '', ...rest } = {}): Promise< +const openNotification = ({ route = '', query = '', ...rest } = {}): Promise< number | undefined > => { - const url = `notification.html${route && `#${route}`}`; + const url = `notification.html${query && `?${query}`}${route && `#${route}`}`; return create({ url, ...rest }); }; diff --git a/src/constant/index.ts b/src/constant/index.ts index 3ca1ccdb4..d83faef1f 100644 --- a/src/constant/index.ts +++ b/src/constant/index.ts @@ -480,6 +480,7 @@ export enum WALLET_BRAND_TYPES { COOLWALLET = 'CoolWallet', DEFIANT = 'Defiant', WALLETCONNECT = 'WALLETCONNECT', + WalletConnect = 'WalletConnect', AIRGAP = 'AirGap', IMTOKENOFFLINE = 'imTokenOffline', Rainbow = 'Rainbow', @@ -596,6 +597,18 @@ export const WALLET_BRAND_CONTENT: { connectType: BRAND_WALLET_CONNECT_TYPE.WalletConnect, category: WALLET_BRAND_CATEGORY.MOBILE, }, + [WALLET_BRAND_TYPES.WalletConnect]: { + id: 100, + name: 'Wallet Connect', + brand: WALLET_BRAND_TYPES.WalletConnect, + icon: LogoWalletConnect, + lightIcon: LogoWalletConnect, + image: LogoWalletConnect, + rcSvg: RcLogoWalletConnect, + maybeSvg: LogoWalletConnectWhite, + connectType: BRAND_WALLET_CONNECT_TYPE.WalletConnect, + category: WALLET_BRAND_CATEGORY.MOBILE, + }, [WALLET_BRAND_TYPES.FIREBLOCKS]: { id: 11, name: 'Fireblocks', diff --git a/src/constant/mint-rabby/gen-tx-detail.ts b/src/constant/mint-rabby/gen-tx-detail.ts new file mode 100644 index 000000000..3aaf7cfa5 --- /dev/null +++ b/src/constant/mint-rabby/gen-tx-detail.ts @@ -0,0 +1,43 @@ +import { ExplainTxResponse } from '@rabby-wallet/rabby-api/dist/types'; +import { getMintRabbyContractAddress } from './mint-rabby-abi'; +import IconRabbySVG from 'src/ui/assets/dashboard/rabby.svg'; +import RabbyNFTSVG from './nft.svg'; +import { isSameAddress } from '@/ui/utils'; + +export const genMintRabbyTxDetail = ( + txDetail: ExplainTxResponse +): ExplainTxResponse => { + if (!txDetail.type_call) { + return txDetail; + } + + const { contract } = txDetail.type_call; + if (!isSameAddress(contract, getMintRabbyContractAddress())) { + return txDetail; + } + const nftList = txDetail.balance_change.receive_nft_list; + return { + ...txDetail, + balance_change: { + ...txDetail.balance_change, + receive_nft_list: nftList?.length + ? [ + { + ...nftList[0], + name: 'Rabby Desktop Genesis #' + nftList[0].inner_id, + content: RabbyNFTSVG, + content_type: 'image_url', + collection: { + name: 'Rabby Desktop Genesis', + } as any, + }, + ] + : [], + }, + type_call: { + ...txDetail.type_call, + contract_protocol_name: 'Rabby Desktop', + contract_protocol_logo_url: IconRabbySVG, + }, + }; +}; diff --git a/src/constant/mint-rabby/mint-rabby-abi.tsx b/src/constant/mint-rabby/mint-rabby-abi.tsx new file mode 100644 index 000000000..246c13635 --- /dev/null +++ b/src/constant/mint-rabby/mint-rabby-abi.tsx @@ -0,0 +1,96 @@ +export const TEST_ADDRESS = '0xe473A20617f20f4A7B4fBDD39490380B78430141'; +export const PROD_ADDRESS = '0x1645787ddcb380932130f0d8c22e6bf53a38e725'; + +const { appChannel } = (await window.rabbyDesktop.ipcRenderer.invoke( + 'rabbyx:get-app-version' +)) as { + appChannel: 'reg' | 'prod'; +}; + +export const getMintRabbyContractAddress = () => { + return PROD_ADDRESS; + // if (appChannel === 'prod') { + // return PROD_ADDRESS; + // } + // return TEST_ADDRESS; +}; + +export const MintRabbyAbi = [ + { + inputs: [{ internalType: 'address', name: 'minter', type: 'address' }], + name: 'mintedPerAddress', + outputs: [ + { + components: [ + { internalType: 'uint256', name: 'totalMints', type: 'uint256' }, + { internalType: 'uint256', name: 'presaleMints', type: 'uint256' }, + { internalType: 'uint256', name: 'publicMints', type: 'uint256' }, + ], + internalType: 'struct IERC721Drop.AddressMintDetails', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'quantity', type: 'uint256' }], + name: 'purchase', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'payable', + type: 'function', + }, + + { + inputs: [{ internalType: 'uint256', name: 'quantity', type: 'uint256' }], + name: 'zoraFeeForAmount', + outputs: [ + { internalType: 'address payable', name: 'recipient', type: 'address' }, + { internalType: 'uint256', name: 'fee', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'saleDetails', + outputs: [ + { + components: [ + { internalType: 'bool', name: 'publicSaleActive', type: 'bool' }, + { internalType: 'bool', name: 'presaleActive', type: 'bool' }, + { internalType: 'uint256', name: 'publicSalePrice', type: 'uint256' }, + { internalType: 'uint64', name: 'publicSaleStart', type: 'uint64' }, + { internalType: 'uint64', name: 'publicSaleEnd', type: 'uint64' }, + { internalType: 'uint64', name: 'presaleStart', type: 'uint64' }, + { internalType: 'uint64', name: 'presaleEnd', type: 'uint64' }, + { + internalType: 'bytes32', + name: 'presaleMerkleRoot', + type: 'bytes32', + }, + { + internalType: 'uint256', + name: 'maxSalePurchasePerAddress', + type: 'uint256', + }, + { internalType: 'uint256', name: 'totalMinted', type: 'uint256' }, + { internalType: 'uint256', name: 'maxSupply', type: 'uint256' }, + ], + internalType: 'struct IERC721Drop.SaleDetails', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, +]; diff --git a/src/constant/mint-rabby/nft.svg b/src/constant/mint-rabby/nft.svg new file mode 100644 index 000000000..9cf348a95 --- /dev/null +++ b/src/constant/mint-rabby/nft.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/content-script/index.ts b/src/content-script/index.ts index c7ef70618..a235ac191 100644 --- a/src/content-script/index.ts +++ b/src/content-script/index.ts @@ -19,7 +19,11 @@ const injectProviderScript = (isDefaultWallet: boolean) => { content += `var __rabby__isDefaultWallet = ${isDefaultWallet};`; content += `var __rabby__uuid = '${uuid()}';`; content += `var __rabby__isOpera = ${isOpera};`; + content += `if (!window.__RD_isDappSafeView && window.__RD_isDappView) { + ;`; content += '#PAGEPROVIDER#'; + content += ` + };`; content += '\n})();'; ele.textContent = content; container.insertBefore(ele, container.children[0]); diff --git a/src/migrations/migrations.ts b/src/migrations/migrations.ts index 5007bbd98..e51ff7dc2 100644 --- a/src/migrations/migrations.ts +++ b/src/migrations/migrations.ts @@ -4,3 +4,4 @@ export { default as contactBookMigration } from './contactBookMigration'; export { default as connectedSiteMigration } from './connectedSiteMigration'; export { default as customRPCMigration } from './customRPCMigration'; export { default as customizedTokenMigration } from './customizedTokenMigration'; +export { default as transactionHistoryMigration } from './transactionHistoryMigration'; diff --git a/src/migrations/transactionHistoryMigration.ts b/src/migrations/transactionHistoryMigration.ts new file mode 100644 index 000000000..61ee94a25 --- /dev/null +++ b/src/migrations/transactionHistoryMigration.ts @@ -0,0 +1,25 @@ +import { TxHistoryStore } from 'background/service/transactionHistory'; + +export default { + version: 5, + async migrator(data: { txHistory: TxHistoryStore | undefined }) { + try { + if (!data.txHistory) return undefined; + for (const addr in data.txHistory.transactions) { + const txs = data.txHistory.transactions[addr]; + for (const key in txs) { + txs[key] = { + ...txs[key], + dbIndexed: true, + }; + } + } + return { + txHistory: data.txHistory, + }; + } catch (e) { + // drop custom tokens if migrate failed + return data; + } + }, +}; diff --git a/src/stats.ts b/src/stats.ts index b80546f3d..7168d66f8 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -1,3 +1,3 @@ import StatsReport, { SITE } from '@debank/festats'; -export default new StatsReport(SITE.rabby); +export default new StatsReport(SITE.rabbyDesktop); diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 461a94318..935fdc756 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -11,7 +11,7 @@ import { Integrations } from '@sentry/tracing'; import i18n, { addResourceBundle } from 'src/i18n'; import { EVENTS } from 'consts'; -import type { WalletControllerType } from 'ui/utils/WalletContext'; +import { WalletControllerType } from 'ui/utils/WalletContext'; import store from './store'; @@ -21,7 +21,12 @@ import { getSentryEnv } from '@/utils/env'; Sentry.init({ dsn: 'https://e871ee64a51b4e8c91ea5fa50b67be6b@o460488.ingest.sentry.io/5831390', - release: process.env.release, + release: globalThis.rabbyDesktop.appVersion, + + // Set tracesSampleRate to 1.0 to capture 100% + // of transactions for performance monitoring. + // We recommend adjusting this value in production + tracesSampleRate: 1.0, environment: getSentryEnv(), ignoreErrors: [ 'ResizeObserver loop limit exceeded', diff --git a/src/ui/assets-const.ts b/src/ui/assets-const.ts new file mode 100644 index 000000000..1f7448b5d --- /dev/null +++ b/src/ui/assets-const.ts @@ -0,0 +1,97 @@ +import { + HARDWARE_KEYRING_TYPES, + IWalletBrandContent, + KEYRING_CLASS, + WALLET_BRAND_CONTENT, + WALLET_BRAND_TYPES, +} from '../constant'; + +import IconEN from 'ui/assets/langs/en.svg'; +import IconAmber from 'ui/assets/walletlogo/amber.svg'; +import LogoAmber from 'ui/assets/walletlogo/amber.svg'; +import { + default as IconBitBox02, + default as IconBitBox02WithBorder, +} from 'ui/assets/walletlogo/bitbox.svg'; +import IconCobo from 'ui/assets/walletlogo/cobo.svg'; +import LogoCobo from 'ui/assets/walletlogo/cobo.svg'; +import IconFireblocksWithBorder from 'ui/assets/walletlogo/fireblocks.svg'; +import IconFireblocks from 'ui/assets/walletlogo/fireblocks.svg'; +import IconGnosis from 'ui/assets/walletlogo/gnosis.svg'; +import IconGridPlus from 'ui/assets/walletlogo/gridplus.svg'; +import IconImtoken from 'ui/assets/walletlogo/imtoken.svg'; +import LogoImtoken from 'ui/assets/walletlogo/imtoken.svg'; +import IconJade from 'ui/assets/walletlogo/jade.svg'; +import LogoJade from 'ui/assets/walletlogo/jade.svg'; +import LogoKeystone from 'ui/assets/walletlogo/keystone.svg'; +import LogoAirGap from 'ui/assets/walletlogo/airgap.svg'; +import LogoLedgerDark from 'ui/assets/walletlogo/ledger.svg'; +import LogoLedgerWhite from 'ui/assets/walletlogo/ledger.svg'; +import IconMath from 'ui/assets/walletlogo/math.svg'; +import LogoMath from 'ui/assets/walletlogo/math.svg'; +import IconMetaMask from 'ui/assets/walletlogo/metamask.svg'; +import IconMnemonicInk from 'ui/assets/walletlogo/mnemonic-ink.svg'; +import IconMnemonicWhite from 'ui/assets/walletlogo/IconMnemonic-white.svg'; +import LogoMnemonic from 'ui/assets/walletlogo/mnemoniclogo.svg'; +import IconOnekey from 'ui/assets/walletlogo/onekey.svg'; +import IconOneKey18 from 'ui/assets/walletlogo/onekey.svg'; +import LogoOnekey from 'ui/assets/walletlogo/onekey.svg'; +import IconPrivateKeyWhite from 'ui/assets/walletlogo/private-key-white.svg'; +import IconPrivateKeyInk from 'ui/assets/walletlogo/privatekey-ink.svg'; +import LogoPrivateKey from 'ui/assets/walletlogo/privatekeylogo.svg'; +import LogoTp from 'ui/assets/walletlogo/tp.svg'; +import IconTokenpocket from 'ui/assets/walletlogo/tp.svg'; +import IconTrezor from 'ui/assets/walletlogo/trezor.svg'; +import IconTrezor24Border from 'ui/assets/walletlogo/trezor.svg'; +import IconTrezor24 from 'ui/assets/walletlogo/trezor.svg'; +import LogoTrezor from 'ui/assets/walletlogo/trezor.svg'; +import LogoTrust from 'ui/assets/walletlogo/trust.svg'; +import IconTrust from 'ui/assets/walletlogo/trust.svg'; +import LogoCoolWallet from 'ui/assets/walletlogo/coolwallet.svg'; +import IconWatchPurple from 'ui/assets/walletlogo/watch-purple.svg'; +import IconWatchWhite from 'ui/assets/walletlogo/IconWatch-white.svg'; +import LogoDefiant from 'ui/assets/walletlogo/defiant.svg'; +import LogoDefiantWhite from 'ui/assets/walletlogo/defiant.svg'; +import LogoWalletConnect from 'ui/assets/walletlogo/walletconnect28.svg'; +import IconWalletConnect from 'ui/assets/walletlogo/walletconnect28.svg'; + +export const KEYRING_ICONS = { + [KEYRING_CLASS.MNEMONIC]: IconMnemonicInk, + [KEYRING_CLASS.PRIVATE_KEY]: IconPrivateKeyInk, + [KEYRING_CLASS.WATCH]: IconWatchPurple, + [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02, + [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, + [HARDWARE_KEYRING_TYPES.Onekey.type]: LogoOnekey, + [HARDWARE_KEYRING_TYPES.Trezor.type]: IconTrezor24, + [HARDWARE_KEYRING_TYPES.GridPlus.type]: IconGridPlus, + [HARDWARE_KEYRING_TYPES.Keystone.type]: LogoKeystone, +}; + +export const KEYRING_ICONS_WHITE = { + [KEYRING_CLASS.MNEMONIC]: IconMnemonicWhite, + [KEYRING_CLASS.PRIVATE_KEY]: IconPrivateKeyWhite, + [KEYRING_CLASS.WATCH]: IconWatchWhite, + [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02, + [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, + [HARDWARE_KEYRING_TYPES.Onekey.type]: LogoOnekey, + [HARDWARE_KEYRING_TYPES.Trezor.type]: IconTrezor24, + [HARDWARE_KEYRING_TYPES.GridPlus.type]: IconGridPlus, + [HARDWARE_KEYRING_TYPES.Keystone.type]: LogoKeystone, +}; +export const KEYRING_PURPLE_LOGOS = { + [KEYRING_CLASS.MNEMONIC]: IconMnemonicInk, + [KEYRING_CLASS.PRIVATE_KEY]: IconPrivateKeyInk, + [KEYRING_CLASS.WATCH]: IconWatchPurple, +}; + +export const KEYRINGS_LOGOS = { + [KEYRING_CLASS.MNEMONIC]: LogoMnemonic, + [KEYRING_CLASS.PRIVATE_KEY]: LogoPrivateKey, + [KEYRING_CLASS.WATCH]: IconWatchWhite, + [HARDWARE_KEYRING_TYPES.BitBox02.type]: IconBitBox02WithBorder, + [HARDWARE_KEYRING_TYPES.Ledger.type]: LogoLedgerWhite, + [HARDWARE_KEYRING_TYPES.Onekey.type]: IconOneKey18, + [HARDWARE_KEYRING_TYPES.Trezor.type]: IconTrezor24Border, + [HARDWARE_KEYRING_TYPES.GridPlus.type]: IconGridPlus, + [HARDWARE_KEYRING_TYPES.Keystone.type]: LogoKeystone, +}; diff --git a/src/ui/assets/walletlogo/gnosis.svg b/src/ui/assets/walletlogo/gnosis.svg index 7f5ac7506..e10a65199 100644 --- a/src/ui/assets/walletlogo/gnosis.svg +++ b/src/ui/assets/walletlogo/gnosis.svg @@ -1,9 +1,18 @@ - - + + + + + + + - + - + + + + + diff --git a/src/ui/assets/walletlogo/walletconnect28.svg b/src/ui/assets/walletlogo/walletconnect28.svg new file mode 100644 index 000000000..ef4b309ab --- /dev/null +++ b/src/ui/assets/walletlogo/walletconnect28.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/ui/component/Contact/ListModal.tsx b/src/ui/component/Contact/ListModal.tsx index bb841948b..31a4d88f8 100644 --- a/src/ui/component/Contact/ListModal.tsx +++ b/src/ui/component/Contact/ListModal.tsx @@ -92,7 +92,7 @@ const ListModal = ({ visible, onOk, onCancel }: ListModalProps) => { cancelText: t('global.Cancel'), title: t('component.Contact.ListModal.authModal.title'), validationHandler: async (password: string) => - wallet.setWhitelist(password, list), + wallet.setWhitelist(list), onFinished() { setEditWhitelistVisible(false); message.success({ diff --git a/src/ui/component/QRCodeReader/index.tsx b/src/ui/component/QRCodeReader/index.tsx index bedca9ba8..48e264c2e 100644 --- a/src/ui/component/QRCodeReader/index.tsx +++ b/src/ui/component/QRCodeReader/index.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { BrowserQRCodeReader } from '@zxing/browser'; import './style.less'; import { openInternalPageInTab } from 'ui/utils'; @@ -28,27 +34,37 @@ const QRCodeReader = ({ }); }, []); const videoEl = useRef(null); - const checkCameraPermission = async () => { + const [deviceId, setDeviceId] = useState(); + + const findDevices = useCallback(async () => { const devices = await window.navigator.mediaDevices.enumerateDevices(); - const webcams = devices.filter((device) => device.kind === 'videoinput'); - // const hasWebcamPermissions = webcams.some( - // (webcam) => webcam.label && webcam.label.length > 0 - // ); - // if (!hasWebcamPermissions) { - // openInternalPageInTab('request-permission?type=camera'); - // } - }; + const videoDevices = devices.filter( + (device) => device.kind === 'videoinput' + ); + const { constrains } = (await window.rabbyDesktop.ipcRenderer.invoke( + 'rabbyx:get-selected-camera' + )) as any; + + if (constrains?.label) { + const device = videoDevices.find((d) => d.label === constrains.label); + if (device) { + setDeviceId(device.deviceId); + } + } + }, [onError]); + useEffect(() => { - checkCameraPermission(); - }, []); + findDevices(); + }, [findDevices]); useEffect(() => { + if (!deviceId) return; const videoElem = document.getElementById('video'); const canplayListener = () => { setCanplay(true); }; videoElem!.addEventListener('canplay', canplayListener); const promise = codeReader.decodeFromVideoDevice( - undefined, + deviceId, 'video', (result) => { if (result) { @@ -66,7 +82,7 @@ const QRCodeReader = ({ }) .catch(console.log); }; - }, []); + }, [deviceId]); return ( diff --git a/src/ui/style/antd-overwrite.less b/src/ui/style/antd-overwrite.less index c8c6df521..b0d634d7d 100644 --- a/src/ui/style/antd-overwrite.less +++ b/src/ui/style/antd-overwrite.less @@ -41,13 +41,16 @@ right: 0; bottom: 0; left: 0; - background-color: rgba(45, 48, 51, 0.1); opacity: 0; transition: opacity 0.2s ease-in-out; } + + &:not(.ant-btn-background-ghost)::before { + background-color: rgba(45, 48, 51, 0.1); + } } -.ant-btn-primary { +.ant-btn-primary:not(.ant-btn-background-ghost) { &:hover, &:focus { background-color: var(--r-blue-default, #7084ff); @@ -62,6 +65,8 @@ color: var(--r-neutral-title-2, #FFF); background-color: var(--r-blue-disable, rgba(219, 224, 255, 1)); border-color: var(--r-blue-disable, rgba(219, 224, 255, 1)); + text-shadow: none; + box-shadow: none; } } /*--------------------- diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index e278020d8..30581eeb8 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -3,9 +3,11 @@ import { CHECK_METAMASK_INSTALLED_URL, WALLET_BRAND_CONTENT, KEYRING_CLASS, +} from 'consts'; +import { KEYRINGS_LOGOS, KEYRING_PURPLE_LOGOS, -} from 'consts'; +} from 'ui/assets-const'; import { Account } from 'background/service/preference'; // eslint-disable-next-line @typescript-eslint/no-empty-function export const noop = () => {}; diff --git a/src/ui/utils/url.ts b/src/ui/utils/url.ts index 536de8793..1b0139ba8 100644 --- a/src/ui/utils/url.ts +++ b/src/ui/utils/url.ts @@ -18,3 +18,7 @@ export const obj2query = (obj: Record) => { export const isValidateUrl = (url: string) => { return /^(https?|http?):\/\/(localhost|\S)+/.test(url); }; + +export const formatDappURLToShow = (url: string) => { + return window.rabbyDesktop?.rendererHelpers?.formatDappURLToShow?.(url) || url; +} \ No newline at end of file diff --git a/src/ui/views/AddressDetail/index.tsx b/src/ui/views/AddressDetail/index.tsx index 49b26df49..006cf88bc 100644 --- a/src/ui/views/AddressDetail/index.tsx +++ b/src/ui/views/AddressDetail/index.tsx @@ -49,9 +49,9 @@ const AddressDetail = () => { wallet, validationHandler: async (password) => { if (checked) { - await wallet.addWhitelist(password, address); + await wallet.addWhitelist(address); } else { - await wallet.removeWhitelist(password, address); + await wallet.removeWhitelist(address); } }, onFinished() { diff --git a/src/ui/views/Approval/components/Connect/index.tsx b/src/ui/views/Approval/components/Connect/index.tsx index 089f7977b..af59cd13c 100644 --- a/src/ui/views/Approval/components/Connect/index.tsx +++ b/src/ui/views/Approval/components/Connect/index.tsx @@ -28,6 +28,7 @@ import IconSuccess from 'ui/assets/success.svg'; import PQueue from 'p-queue'; import { SignTestnetPermission } from './SignTestnetPermission'; import { ReactComponent as ArrowDownSVG } from '@/ui/assets/approval/arrow-down-blue.svg'; +import { formatDappURLToShow } from '@/ui/utils/url'; interface ConnectProps { params: any; @@ -84,6 +85,11 @@ const ConnectWrapper = styled.div` line-height: 26px; text-align: center; color: var(--r-neutral-title-1, #192945); + + white-space: pre-wrap; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; } } } @@ -608,6 +614,9 @@ const Connect = ({ params: { icon, origin } }: ConnectProps) => { }); activePopup('CancelConnect'); }; + const originToShow = useMemo(() => { + return formatDappURLToShow(origin); + }, [origin]); return ( @@ -621,7 +630,7 @@ const Connect = ({ params: { icon, origin } }: ConnectProps) => {
{t('page.connect.selectChainToConnect')}
-
{origin}
+
{originToShow}
} value={defaultChain} @@ -633,7 +642,7 @@ const Connect = ({ params: { icon, origin } }: ConnectProps) => {
-

{origin}

+

{originToShow}

@@ -760,7 +769,7 @@ const Connect = ({ params: { icon, origin } }: ConnectProps) => { onClose={handleRuleDrawerClose} /> { {canProcess ? (