diff --git a/.env.example b/.env.example index 8d65d2944..3e46d77c8 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,8 @@ MOON_PAY_API_KEY= # Analytics MIX_PANEL_TOKEN= -MIX_PANEL_EXPLORE_APP_TOKEN= \ No newline at end of file +MIX_PANEL_EXPLORE_APP_TOKEN= + +# only needed for E2E test +# SEED_WORDS1= [] +# SEED_WORDS2= [] diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index d7ae94a6c..dff30f980 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -50,7 +50,7 @@ jobs: if-no-files-found: error UItest: needs: [build] - name: UI Test ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} + name: E2E Test ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} timeout-minutes: 10 runs-on: ubuntu-20.04 strategy: @@ -79,8 +79,11 @@ jobs: run: npm install playwright - name: Install Playwright Browsers run: npx playwright install chromium --with-deps - - name: Run UI test suite - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Run E2E test suite + env: + SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} + SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep-invert "#localexecution" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v3 @@ -122,7 +125,7 @@ jobs: with: name: html-report--attempt-${{ github.run_attempt }} path: playwright-report - retention-days: 5 + retention-days: 4 publish-rc: # TODO also keep the develop PR description up to date diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 462e274c0..f10252803 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ on: - develop jobs: build: - if: ${{ !startsWith(github.head_ref, 'release/') && !startsWith(github.head_ref, 'e2etest/')}} + if: ${{ !startsWith(github.head_ref, 'release/') && !startsWith(github.head_ref, 'e2etest/') && !github.event.pull_request.draft == true }} runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 @@ -40,15 +40,18 @@ jobs: run: npm run build --if-present - name: Install Playwright Browsers run: npx playwright install chromium --with-deps - - name: Run UI test suite - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep "#smoketest" --reporter=html + - name: Run E2E test suite + env: + SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} + SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npm run e2etest:smoketest --reporter=html - name: Upload Playwright report if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/ - retention-days: 30 + retention-days: 4 - name: Save Filename run: | BRANCH_NAME=$(echo ${{ github.head_ref }} | sed 's/\//-/g') diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index b0d85aed2..187ed9642 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -32,6 +32,7 @@ jobs: - id: run-create-release-pr-sh env: BUMP: ${{ inputs.bump }} + SOURCE_BRANCH: ${{ github.ref_name }} GH_TOKEN: ${{ github.token }} run: | # git config diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 71d08e62f..c4b1853b2 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -7,7 +7,7 @@ on: jobs: build: - if: ${{ startsWith(github.head_ref, 'e2etest/') || github.event_name == 'workflow_dispatch' }} + if: ${{ (startsWith(github.head_ref, 'e2etest/') && github.event.pull_request && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -40,11 +40,11 @@ jobs: with: name: web-extension1 path: ./build - retention-days: 5 + retention-days: 4 if-no-files-found: error UItest: needs: [build] - name: UI Test ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} + name: E2E Test ${{ matrix.shardIndex }} of ${{ matrix.shardTotal }} timeout-minutes: 10 runs-on: ubuntu-20.04 strategy: @@ -73,8 +73,11 @@ jobs: run: npm install playwright - name: Install Playwright Browsers run: npx playwright install chromium --with-deps - - name: Run UI test suite - run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Run E2E test suite + env: + SEED_WORDS1: ${{ secrets.SEED_WORDS1 }} + SEED_WORDS2: ${{ secrets.SEED_WORDS2 }} + run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npx playwright test --grep-invert "#localexecution" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v3 @@ -116,4 +119,4 @@ jobs: with: name: html-report--attempt-${{ github.run_attempt }} path: playwright-report - retention-days: 5 + retention-days: 4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a062aa7d6..1527eac78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: PR_ID: ${{ github.event.pull_request.number }} run: | # update PR description - cat release.json | jq -r .body > body.md + cat scripts/release.json | jq -r .body > body.md echo -e "\n\nPublished latest release: $(cat release.json | jq -r .html_url)" >> body.md gh api \ --method PATCH \ @@ -71,7 +71,7 @@ jobs: - id: download-latest-asset name: Download latest asset from rc run: | - ASSET_ID=$(cat releases.json | jq -r ".[] | select(.tag_name==\"$TAG_RC\") | .assets[0].id") + ASSET_ID=$(cat scripts/releases.json | jq -r ".[] | select(.tag_name==\"$TAG_RC\") | .assets[0].id") gh api \ -H "Accept: application/octet-stream" \ -H "X-GitHub-Api-Version: 2022-11-28" \ @@ -96,6 +96,7 @@ jobs: git config user.name "GitHub Actions Bot" git config user.email "<>" # run shell script + cd scripts ./merge-to-remote.sh - id: copy-release-to-public name: Copy release to public remote @@ -103,7 +104,7 @@ jobs: REMOTE_REPO: xverse-web-extension run: | # publish the latest release on remote - cat release.json | jq -r .body > public-body.md + cat scripts/release.json | jq -r .body > public-body.md gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ diff --git a/README.md b/README.md index b1aef1bb4..1bca9fcc6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npm run e2etest If you only want to run the smoke test suite, run ``` -npm run smoketest +npm run e2etest:smoketest ``` If you want to run the e2e test in UI Mode: diff --git a/package-lock.json b/package-lock.json index a9b5b020f..6bc9ccc21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { "name": "xverse-web-extension", - "version": "0.35.1", + "version": "0.38.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.35.1", + "version": "0.38.1", "dependencies": { + "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", + "@sats-connect/core": "0.0.15", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "13.6.7", + "@secretkeylabs/xverse-core": "17.1.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -26,7 +28,8 @@ "@testing-library/user-event": "^13.5.0", "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", - "axios": "^1.1.3", + "async-mutex": "^0.5.0", + "axios": "1.7.0", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", "buffer": "6.0.3", @@ -67,13 +70,13 @@ "redux": "^4.0.5", "redux-persist": "^6.0.0", "redux-state-sync": "^3.1.4", - "sats-connect": "2.1.0", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", "superjson": "2.2.1", "swiper": "11.0.6", "ts-transformer-keys": "0.4.4", + "valibot": "0.33.2", "valid-url": "^1.0.9", "webextension-polyfill": "^0.10.0", "zod": "3.22.4", @@ -108,7 +111,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-inline-styles": "^1.0.5", - "eslint-plugin-playwright": "^1.5.4", + "eslint-plugin-playwright": "^1.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -128,11 +131,13 @@ "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", "ts-loader": "^9.5.1", + "ts-prune": "0.10.3", "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", "typescript": "^5.0.0", "typescript-plugin-styled-components": "^3.0.0", + "vite-tsconfig-paths": "4.3.2", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", @@ -144,7 +149,7 @@ }, "../xverse-core": { "name": "@secretkeylabs/xverse-core", - "version": "11.2.0", + "version": "12.0.1", "extraneous": true, "license": "ISC", "dependencies": { @@ -178,6 +183,7 @@ "c32check": "^2.0.0", "ecdsa-sig-formatter": "^1.0.11", "ecpair": "^2.1.0", + "json-bigint": "^1.0.0", "jsontokens": "^4.0.1", "ledger-bitcoin": "^0.2.1", "process": "^0.11.10", @@ -186,6 +192,7 @@ "varuint-bitcoin": "^1.1.2" }, "devDependencies": { + "@types/json-bigint": "^1.0.4", "@types/react": "^18.2.18", "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", @@ -215,7 +222,8 @@ }, "peerDependencies": { "bignumber.js": "^9.0.0", - "react": ">18.0.0" + "react": ">18.0.0", + "react-dom": ">18.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -245,6 +253,14 @@ "node": ">=6.0.0" } }, + "node_modules/@aryzing/superqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@aryzing/superqs/-/superqs-0.0.6.tgz", + "integrity": "sha512-MQR7BysZv1BqEmiFohs967UvMJMnWBipn2EFTG9AotyYXGgY67g7iofVOq8K83LAiOLVImr5kLhlzsmMTKLjFA==", + "dependencies": { + "superjson": "2.2.1" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -1307,10 +1323,35 @@ "linux" ] }, + "node_modules/@sats-connect/core": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15.tgz", + "integrity": "sha512-f+s+uM4BnjLs+I8AHp4Tf4GG8kMLGn44RalNZqlo/OkTeZj+llQEsMEwfKkIM8f45gA3jSWqmn4q/LsjMpWlVw==", + "dependencies": { + "axios": "1.6.8", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "lodash.omit": "4.5.0" + }, + "peerDependencies": { + "valibot": "0.33.2" + } + }, + "node_modules/@sats-connect/core/node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1383,15 +1424,16 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "13.6.7", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/13.6.7/0f39eead8e8603e7a0fb1b0740881064b7885b6c", - "integrity": "sha512-q9IrwImnEHURBBfNCsBuuZM2lQ7zuNPXMA9ShYbHKhjJKfUv/SGBJOpUOE2KdvE5NyJqmaDaWp9fEZU9aFbc8w==", - "license": "ISC", + "version": "17.1.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1/8824e8522bcad7fc01e0cf36d8f323ddcecc77fa", + "integrity": "sha512-RwLeyPvlPfr08DCuSpc5O/YLphkz00mMfiDzpgvLGF7KzSJ6QDLJhZcMl90hC2yRQN3egXC3a9rNkXOJlVcxeQ==", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.13.1", "@stacks/connect": "7.7.1", @@ -1404,7 +1446,7 @@ "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", - "axios": "1.6.2", + "axios": "1.7.0", "base64url": "^3.0.1", "bip32": "^4.0.0", "bip39": "3.0.3", @@ -1431,10 +1473,58 @@ }, "peerDependencies": { "bignumber.js": "^9.0.0", + "micro-packed": ">0.5.0", "react": ">18.0.0", "react-dom": ">18.0.0" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/connect": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", @@ -1461,6 +1551,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, + "node_modules/@secretkeylabs/xverse-core/node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@secretkeylabs/xverse-core/node_modules/bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -2117,6 +2215,18 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@ts-morph/common": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", + "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, "node_modules/@types/argon2-browser": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz", @@ -3239,9 +3349,9 @@ } }, "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", "dependencies": { "undici-types": "~5.26.4" } @@ -3699,9 +3809,9 @@ } }, "node_modules/async-mutex": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", - "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "dependencies": { "tslib": "^2.4.0" } @@ -3732,11 +3842,11 @@ } }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.0.tgz", + "integrity": "sha512-IiB0wQeKyPRdsFVhBgIo31FbzOyf2M6wYl7/NVutFwFBRMiAbjNiydJIHKeLmPugF4kJLfA1uWZ82Is2QzqqFA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -3959,9 +4069,9 @@ } }, "node_modules/bitcoinjs-lib": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", - "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", "dependencies": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -4355,12 +4465,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4673,6 +4789,12 @@ "node": ">=6" } }, + "node_modules/code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5325,6 +5447,22 @@ "node": ">= 10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -5873,6 +6011,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -6260,9 +6417,9 @@ } }, "node_modules/eslint-plugin-playwright": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.5.4.tgz", - "integrity": "sha512-J38Wy3Vc2f9y73J+KRmgXgbYI8TZ3zbz6qBbTj3PhpFndUS572jZ7kqQ3rJ9si5BaMHT7lmZzraO+3UjwIDV4Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", + "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", "dev": true, "dependencies": { "globals": "^13.23.0" @@ -6947,9 +7104,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -7175,15 +7332,6 @@ "node": ">=12" } }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fs-monkey": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", @@ -7196,9 +7344,12 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -7253,14 +7404,18 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7374,6 +7529,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -7445,11 +7606,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7513,6 +7674,17 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -8772,15 +8944,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/jsonp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", @@ -9270,6 +9433,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "node_modules/lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", @@ -9655,6 +9823,18 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -12783,6 +12963,12 @@ "util": "^0.10.3" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -14258,16 +14444,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "node_modules/sats-connect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-2.1.0.tgz", - "integrity": "sha512-HCPG3L0GModbC2g4uk+ezpSUr8kfCw1y5kv6LC9N8ZNNSRFTTK/CX9eSStV7bBGk5po+euaLsRy4YlLihRcnnw==", - "dependencies": { - "jsontokens": "^4.0.1", - "process": "^0.11.10", - "util": "^0.12.4" - } - }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -14499,6 +14675,22 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -15236,6 +15428,15 @@ "uglify-js": "^3.1.9" } }, + "node_modules/true-myth": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", + "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", + "dev": true, + "engines": { + "node": "10.* || >= 12.*" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -15359,6 +15560,42 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/ts-morph": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", + "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.12.3", + "code-block-writer": "^11.0.0" + } + }, + "node_modules/ts-prune": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz", + "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", + "dev": true, + "dependencies": { + "commander": "^6.2.1", + "cosmiconfig": "^7.0.1", + "json5": "^2.1.3", + "lodash": "^4.17.21", + "true-myth": "^4.1.0", + "ts-morph": "^13.0.1" + }, + "bin": { + "ts-prune": "lib/index.js" + } + }, + "node_modules/ts-prune/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/ts-transformer-keys": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/ts-transformer-keys/-/ts-transformer-keys-0.4.4.tgz", @@ -15379,6 +15616,26 @@ "typescript": ">=3" } }, + "node_modules/tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -15689,9 +15946,9 @@ "dev": true }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -15719,6 +15976,15 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unload": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", @@ -15845,6 +16111,11 @@ "uuid": "8.3.2" } }, + "node_modules/valibot": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.33.2.tgz", + "integrity": "sha512-ZpFWuI+bs5+PP66q4zVFn4e4t/s5jmMw5iPBZmGUoi8iQqXyU9YY/BLCAyk62Z/bNS8qdUNBEyx52952qdqW3w==" + }, "node_modules/valid-url": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", @@ -15945,6 +16216,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", @@ -16752,6 +17042,14 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@aryzing/superqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@aryzing/superqs/-/superqs-0.0.6.tgz", + "integrity": "sha512-MQR7BysZv1BqEmiFohs967UvMJMnWBipn2EFTG9AotyYXGgY67g7iofVOq8K83LAiOLVImr5kLhlzsmMTKLjFA==", + "requires": { + "superjson": "2.2.1" + } + }, "@babel/code-frame": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", @@ -17528,10 +17826,34 @@ "dev": true, "optional": true }, + "@sats-connect/core": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15.tgz", + "integrity": "sha512-f+s+uM4BnjLs+I8AHp4Tf4GG8kMLGn44RalNZqlo/OkTeZj+llQEsMEwfKkIM8f45gA3jSWqmn4q/LsjMpWlVw==", + "requires": { + "axios": "1.6.8", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "lodash.omit": "4.5.0" + }, + "dependencies": { + "axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } + } + }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", + "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==" }, "@scure/bip32": { "version": "1.1.3", @@ -17578,14 +17900,16 @@ } }, "@secretkeylabs/xverse-core": { - "version": "13.6.7", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/13.6.7/0f39eead8e8603e7a0fb1b0740881064b7885b6c", - "integrity": "sha512-q9IrwImnEHURBBfNCsBuuZM2lQ7zuNPXMA9ShYbHKhjJKfUv/SGBJOpUOE2KdvE5NyJqmaDaWp9fEZU9aFbc8w==", + "version": "17.1.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1/8824e8522bcad7fc01e0cf36d8f323ddcecc77fa", + "integrity": "sha512-RwLeyPvlPfr08DCuSpc5O/YLphkz00mMfiDzpgvLGF7KzSJ6QDLJhZcMl90hC2yRQN3egXC3a9rNkXOJlVcxeQ==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", "@stacks/auth": "6.13.1", "@stacks/connect": "7.7.1", @@ -17598,7 +17922,7 @@ "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", - "axios": "1.6.2", + "axios": "1.7.0", "base64url": "^3.0.1", "bip32": "^4.0.0", "bip39": "3.0.3", @@ -17621,6 +17945,38 @@ "varuint-bitcoin": "^1.1.2" }, "dependencies": { + "@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "requires": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, + "@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "requires": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, "@stacks/connect": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", @@ -17647,6 +18003,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" }, + "async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "requires": { + "tslib": "^2.4.0" + } + }, "bip39": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", @@ -18146,6 +18510,18 @@ "@babel/runtime": "^7.12.5" } }, + "@ts-morph/common": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", + "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", + "dev": true, + "requires": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, "@types/argon2-browser": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.1.tgz", @@ -19103,9 +19479,9 @@ } }, "@types/node": { - "version": "18.19.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.31.tgz", - "integrity": "sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==", + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", "requires": { "undici-types": "~5.26.4" } @@ -19452,9 +19828,9 @@ } }, "async-mutex": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", - "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", "requires": { "tslib": "^2.4.0" } @@ -19476,11 +19852,11 @@ "dev": true }, "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.0.tgz", + "integrity": "sha512-IiB0wQeKyPRdsFVhBgIo31FbzOyf2M6wYl7/NVutFwFBRMiAbjNiydJIHKeLmPugF4kJLfA1uWZ82Is2QzqqFA==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -19657,9 +20033,9 @@ } }, "bitcoinjs-lib": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", - "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", "requires": { "@noble/hashes": "^1.2.0", "bech32": "^2.0.0", @@ -19982,12 +20358,15 @@ "dev": true }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -20214,6 +20593,12 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" }, + "code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -20727,6 +21112,16 @@ "execa": "^5.0.0" } }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -21169,6 +21564,19 @@ "which-typed-array": "^1.1.10" } }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -21565,9 +21973,9 @@ } }, "eslint-plugin-playwright": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.5.4.tgz", - "integrity": "sha512-J38Wy3Vc2f9y73J+KRmgXgbYI8TZ3zbz6qBbTj3PhpFndUS572jZ7kqQ3rJ9si5BaMHT7lmZzraO+3UjwIDV4Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", + "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", "dev": true, "requires": { "globals": "^13.23.0" @@ -21993,9 +22401,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -22143,14 +22551,6 @@ "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true - } } }, "fs-monkey": { @@ -22165,9 +22565,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -22204,14 +22604,15 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-stream": { @@ -22286,6 +22687,12 @@ "slash": "^3.0.0" } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "goober": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", @@ -22343,11 +22750,11 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" } }, "has-proto": { @@ -22387,6 +22794,14 @@ "minimalistic-assert": "^1.0.1" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -23253,14 +23668,6 @@ "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true - } } }, "jsonp": { @@ -23624,6 +24031,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", @@ -23929,6 +24341,12 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, "mlly": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", @@ -26018,6 +26436,12 @@ } } }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -27048,16 +27472,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sats-connect": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sats-connect/-/sats-connect-2.1.0.tgz", - "integrity": "sha512-HCPG3L0GModbC2g4uk+ezpSUr8kfCw1y5kv6LC9N8ZNNSRFTTK/CX9eSStV7bBGk5po+euaLsRy4YlLihRcnnw==", - "requires": { - "jsontokens": "^4.0.1", - "process": "^0.11.10", - "util": "^0.12.4" - } - }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -27262,6 +27676,19 @@ "send": "0.18.0" } }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -27797,6 +28224,12 @@ "uglify-js": "^3.1.9" } }, + "true-myth": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", + "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", + "dev": true + }, "ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -27885,6 +28318,38 @@ } } }, + "ts-morph": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", + "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", + "dev": true, + "requires": { + "@ts-morph/common": "~0.12.3", + "code-block-writer": "^11.0.0" + } + }, + "ts-prune": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz", + "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", + "dev": true, + "requires": { + "commander": "^6.2.1", + "cosmiconfig": "^7.0.1", + "json5": "^2.1.3", + "lodash": "^4.17.21", + "true-myth": "^4.1.0", + "ts-morph": "^13.0.1" + }, + "dependencies": { + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true + } + } + }, "ts-transformer-keys": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/ts-transformer-keys/-/ts-transformer-keys-0.4.4.tgz", @@ -27898,6 +28363,13 @@ "dev": true, "requires": {} }, + "tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "requires": {} + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -28130,9 +28602,9 @@ "dev": true }, "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==" + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==" }, "unbox-primitive": { "version": "1.0.2", @@ -28151,6 +28623,12 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, "unload": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", @@ -28239,6 +28717,11 @@ "uuid": "8.3.2" } }, + "valibot": { + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.33.2.tgz", + "integrity": "sha512-ZpFWuI+bs5+PP66q4zVFn4e4t/s5jmMw5iPBZmGUoi8iQqXyU9YY/BLCAyk62Z/bNS8qdUNBEyx52952qdqW3w==" + }, "valid-url": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", @@ -28284,6 +28767,17 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, + "vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + } + }, "vitest": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", diff --git a/package.json b/package.json index 344130c3c..5c0b4b16b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.35.1", + "version": "0.38.1", "private": true, "engines": { "node": "^18.18.2" @@ -14,18 +14,38 @@ "test": "vitest ./src", "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install", - "e2etest": "npx playwright test", + "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", "e2etest:ui": "npx playwright test --ui", - "smoketest": "npx playwright test --grep \"#smoketest\"", - "e2etest:report": "playwright show-report" + "e2etest:smoketest": "npx playwright test --grep \"#smoketest\"", + "e2etest:skipped": "npx playwright test --grep \"#localexecution\"", + "e2etest:report": "playwright show-report", + "find-deadcode": "ts-prune -p tsconfig.tsPrune.json", + "ts-check": "tsc --noEmit", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet" + }, + "overrides": { + "buffer": "6.0.3" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "node scripts/ts-prune-staged.js", + "prettier --write", + "eslint", + "tsc-files --noEmit src/styled.d.ts src/react-app-env.d.ts" + ], + "*.json": [ + "prettier --write" + ] }, "dependencies": { + "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", + "@sats-connect/core": "0.0.15", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "13.6.7", + "@secretkeylabs/xverse-core": "17.1.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -38,7 +58,8 @@ "@testing-library/user-event": "^13.5.0", "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", - "axios": "^1.1.3", + "async-mutex": "^0.5.0", + "axios": "1.7.0", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", "buffer": "6.0.3", @@ -79,31 +100,18 @@ "redux": "^4.0.5", "redux-persist": "^6.0.0", "redux-state-sync": "^3.1.4", - "sats-connect": "2.1.0", "stream-browserify": "^3.0.0", "string-to-color": "^2.2.2", "styled-components": "^5.3.5", "superjson": "2.2.1", "swiper": "11.0.6", "ts-transformer-keys": "0.4.4", + "valibot": "0.33.2", "valid-url": "^1.0.9", "webextension-polyfill": "^0.10.0", "zod": "3.22.4", "zxcvbn": "^4.4.2" }, - "overrides": { - "buffer": "6.0.3" - }, - "lint-staged": { - "*.{ts,tsx}": [ - "prettier --write", - "eslint", - "tsc-files --noEmit src/styled.d.ts src/react-app-env.d.ts" - ], - "*.json": [ - "prettier --write" - ] - }, "devDependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@tanstack/eslint-plugin-query": "^4.29.4", @@ -133,7 +141,7 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-no-inline-styles": "^1.0.5", - "eslint-plugin-playwright": "^1.5.4", + "eslint-plugin-playwright": "^1.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -153,11 +161,13 @@ "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", "ts-loader": "^9.5.1", + "ts-prune": "0.10.3", "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", "typescript": "^5.0.0", "typescript-plugin-styled-components": "^3.0.0", + "vite-tsconfig-paths": "4.3.2", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index 36533c558..288391240 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,8 +18,11 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 1 : 0, - /* Opt out of 2 tests parallel on CI. */ - workers: process.env.CI ? 2 : 1, + /* Opt to only 2 tests parallel on CI. + Note that you may want to change the non-ci amount to a lower number depending on + the specs of your computer or if the number of tests increases over time. + */ + workers: process.env.CI ? 2 : 5, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'blob' : 'html', snapshotDir: './playwright-snapshots', diff --git a/scripts/create-release-pr.sh b/scripts/create-release-pr.sh index c9d7a9b07..6daa86c50 100755 --- a/scripts/create-release-pr.sh +++ b/scripts/create-release-pr.sh @@ -14,10 +14,13 @@ if [[ -z "$BUMP" ]]; then exit 1 fi -echo -e "\n--- Prepare for $BUMP release branch ---" +# Check for an optional SOURCE_BRANCH variable, default to 'develop' if not provided +SOURCE_BRANCH="${SOURCE_BRANCH:-develop}" + +echo -e "\n--- Prepare for $BUMP release branch from $SOURCE_BRANCH ---" git fetch --all -git checkout develop +git checkout $SOURCE_BRANCH git pull npm version $BUMP --git-tag-version=false diff --git a/scripts/ts-prune-staged.js b/scripts/ts-prune-staged.js new file mode 100644 index 000000000..c82efb947 --- /dev/null +++ b/scripts/ts-prune-staged.js @@ -0,0 +1,16 @@ +const {exec} = require('child_process'); + +const includedFiles = process.argv + .slice(2) + .map((path) => path.replace(process.cwd() + '/', '')); + +exec('npm run --silent find-deadcode', (_, stdout) => { + const result = stdout + .split('\n') + .filter((line) => includedFiles.some((file) => line.startsWith(file))) + .join('\n'); + + // eslint-disable-next-line no-console + console.log(result); + process.exit(result ? 1 : 0); +}); diff --git a/scripts/ts-prune.sh b/scripts/ts-prune.sh new file mode 100755 index 000000000..e45980370 --- /dev/null +++ b/scripts/ts-prune.sh @@ -0,0 +1,14 @@ +OUTPUT=$(npm run --silent find-deadcode) +if [ $(echo "$OUTPUT" | wc -w) -gt 0 ]; then + echo -e "\033[0;31mYou're trying to push unused code, please check these files:\033[0m" + IFS=$'\n' # Set the input field separator to newline + lines=($OUTPUT) # Split the output into an array of lines + + for ((i=0; i<${#lines[@]}; i++)); do + echo "$(($i+1)). ${lines[$i]}" # Print each line with numbering + done + + exit 1 +else + echo "No dead code was found, keep it clean 🌱" +fi \ No newline at end of file diff --git a/src/app/App.tsx b/src/app/App.tsx index 41443c348..a4b9336c3 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,4 +1,5 @@ -import LoadingScreen from '@components/loadingScreen'; +import { PermissionsProvider } from '@components/permissionsManager'; +import StartupLoadingScreen from '@components/startupLoadingScreen'; import { CheckCircle, XCircle } from '@phosphor-icons/react'; import { setXClientVersion } from '@secretkeylabs/xverse-core'; import rootStore from '@stores/index'; @@ -28,66 +29,68 @@ const StyledIcon = styled.div` justify-content: center; `; -function App(): JSX.Element { +function App(): React.ReactNode { return ( <> - - - - }> - - - - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.success_medium, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + + + + + }> + + + + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.success_medium, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - }, - error: { - icon: ( - - - - ), - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.danger_dark, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.white_0, + error: { + icon: ( + + + + ), + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.danger_dark, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.white_0, + }, }, - }, - blank: { - style: { - ...Theme.typography.body_medium_m, - backgroundColor: Theme.colors.white_0, - borderRadius: Theme.radius(2), - padding: Theme.space.s, - color: Theme.colors.elevation0, + blank: { + style: { + ...Theme.typography.body_medium_m, + backgroundColor: Theme.colors.white_0, + borderRadius: Theme.radius(2), + padding: Theme.space.s, + color: Theme.colors.elevation0, + }, }, - duration: 2000, - }, - }} - /> - - - - - + }} + /> + + + + + + ); } diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index 5143b8a3b..477418fee 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -7,10 +7,11 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import OptionsDialog, { OPTIONS_DIALOG_WIDTH } from '@components/optionsDialog/optionsDialog'; +import OptionsDialog from '@components/optionsDialog/optionsDialog'; import useSeedVault from '@hooks/useSeedVault'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { DotsThreeVertical } from '@phosphor-icons/react'; +import { OPTIONS_DIALOG_WIDTH } from '@utils/constants'; const SelectedAccountContainer = styled.div<{ showBorderBottom?: boolean }>((props) => ({ display: 'flex', @@ -51,10 +52,10 @@ const ButtonRow = styled.button` align-items: center; background-color: transparent; justify-content: flex-start; - padding-left: 24px; - padding-right: 24px; - padding-top: 11px; - padding-bottom: 11px; + padding-top: ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.s}; + padding-left: ${(props) => props.theme.space.l}; + padding-right: ${(props) => props.theme.space.l}; font: ${(props) => props.theme.body_medium_m}; color: ${(props) => props.theme.colors.white_0}; transition: background-color 0.2s ease; @@ -70,19 +71,19 @@ const WarningButton = styled(ButtonRow)` color: ${(props) => props.theme.colors.feedback.error}; `; -interface AccountHeaderComponentProps { +type Props = { disableMenuOption?: boolean; disableAccountSwitch?: boolean; showBorderBottom?: boolean; -} +}; function AccountHeaderComponent({ disableMenuOption = false, disableAccountSwitch = false, showBorderBottom = true, -}: AccountHeaderComponentProps) { +}: Props) { const navigate = useNavigate(); - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { t: optionsDialogTranslation } = useTranslation('translation', { @@ -138,9 +139,7 @@ function AccountHeaderComponent({ setOptionsDialogIndents({ top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, - left: `calc(${ - (event.target as HTMLElement).parentElement?.getBoundingClientRect().right - }px - ${OPTIONS_DIALOG_WIDTH}px)`, + left: `calc(100% - ${OPTIONS_DIALOG_WIDTH}px)`, }); }; diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index ea4072125..a24fd49d8 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -1,15 +1,15 @@ import LedgerBadge from '@assets/img/ledger/ledger_badge.svg'; import BarLoader from '@components/barLoader'; -import BottomModal from '@components/bottomModal'; -import OptionsDialog, { OPTIONS_DIALOG_WIDTH } from '@components/optionsDialog/optionsDialog'; +import OptionsDialog from '@components/optionsDialog/optionsDialog'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { CaretDown, DotsThreeVertical } from '@phosphor-icons/react'; import { Account, currencySymbolMap } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import Input from '@ui-library/input'; +import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; -import { EMPTY_LABEL, LoaderSize, MAX_ACC_NAME_LENGTH } from '@utils/constants'; +import { EMPTY_LABEL, LoaderSize, OPTIONS_DIALOG_WIDTH } from '@utils/constants'; import { getAccountGradient } from '@utils/gradient'; import { isLedgerAccount, validateAccountName } from '@utils/helper'; import { useEffect, useRef, useState } from 'react'; @@ -58,10 +58,14 @@ const CurrentAccountTextContainer = styled.div((props) => ({ gap: props.theme.space.xs, })); -const AccountName = styled.h1<{ isSelected: boolean }>((props) => ({ +const AccountName = styled.span<{ isSelected: boolean }>((props) => ({ ...props.theme.typography.body_bold_m, color: props.isSelected ? props.theme.colors.white_0 : props.theme.colors.white_400, textAlign: 'start', + maxWidth: 160, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', })); const BarLoaderContainer = styled.div((props) => ({ @@ -82,8 +86,6 @@ const OptionsButton = styled.button({ }); const ModalContent = styled.form((props) => ({ - padding: props.theme.space.m, - paddingTop: props.theme.space.m, paddingBottom: props.theme.space.xxl, })); @@ -212,15 +214,6 @@ function AccountRow({ } }, [accountName]); - const getName = () => { - const name = - account?.accountName ?? - account?.bnsName ?? - `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`; - - return name.length > MAX_ACC_NAME_LENGTH ? `${name.slice(0, MAX_ACC_NAME_LENGTH)}...` : name; - }; - const handleClick = () => { onAccountSelected(account!); }; @@ -336,7 +329,9 @@ function AccountRow({ - {getName()} + {account?.accountName ?? + account?.bnsName ?? + `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`} {isLedgerAccount(account) && Ledger icon} {isSelected && !disabledAccountSelect && !isAccountListView && ( @@ -394,9 +389,9 @@ function AccountRow({ )} {showRemoveAccountModal && ( - @@ -418,13 +413,13 @@ function AccountRow({ - + )} {showRenameAccountModal && ( - @@ -462,7 +457,7 @@ function AccountRow({ - + )} ); diff --git a/src/app/components/alertMessage/index.tsx b/src/app/components/alertMessage/index.tsx index 6715936e9..6287416aa 100644 --- a/src/app/components/alertMessage/index.tsx +++ b/src/app/components/alertMessage/index.tsx @@ -86,6 +86,7 @@ interface Props { onButtonClick?: () => void; onSecondButtonClick?: () => void; tickMarkButtonClick?: (e: React.ChangeEvent) => void; + tickMarkButtonChecked?: boolean; } function AlertMessage({ @@ -99,6 +100,7 @@ function AlertMessage({ onButtonClick, onSecondButtonClick, tickMarkButtonClick, + tickMarkButtonChecked, }: Props) { return ( <> @@ -134,6 +136,7 @@ function AlertMessage({ checkboxId={`${title}-ticker`} text={tickMarkButtonText} onChange={tickMarkButtonClick} + checked={tickMarkButtonChecked} /> )} diff --git a/src/app/components/bottomModal/index.tsx b/src/app/components/bottomModal/index.tsx index c022155d2..4e27454c1 100644 --- a/src/app/components/bottomModal/index.tsx +++ b/src/app/components/bottomModal/index.tsx @@ -28,7 +28,7 @@ const CustomisedModal = styled(Modal)` position: absolute; `; -interface Props { +type Props = { header: string; visible: boolean; children: React.ReactNode; @@ -36,7 +36,7 @@ interface Props { overlayStylesOverriding?: {}; contentStylesOverriding?: {}; className?: string; -} +}; function BottomModal({ header, diff --git a/src/app/components/collectibleCollectionGridItem/index.tsx b/src/app/components/collectibleCollectionGridItem/index.tsx index d2a876aa1..7adbf116d 100644 --- a/src/app/components/collectibleCollectionGridItem/index.tsx +++ b/src/app/components/collectibleCollectionGridItem/index.tsx @@ -48,15 +48,16 @@ const GridItemContainer = styled.button` width: 100%; `; -interface Props { +type Props = { item?: Inscription | NonFungibleToken; itemId?: string; itemSubText?: string; itemSubTextColor?: Color; children: ReactNode; onClick?: (item: Inscription | NonFungibleToken) => void; -} -export function CollectibleCollectionGridItem({ +}; + +function CollectibleCollectionGridItem({ item, itemId, itemSubText, @@ -72,7 +73,7 @@ export function CollectibleCollectionGridItem({ : undefined; return ( - + {children} diff --git a/src/app/screens/createInscription/ContentLabel/common.ts b/src/app/components/confirmBtcTransaction/ContentLabel/common.ts similarity index 100% rename from src/app/screens/createInscription/ContentLabel/common.ts rename to src/app/components/confirmBtcTransaction/ContentLabel/common.ts diff --git a/src/app/screens/createInscription/ContentLabel/index.tsx b/src/app/components/confirmBtcTransaction/ContentLabel/index.tsx similarity index 97% rename from src/app/screens/createInscription/ContentLabel/index.tsx rename to src/app/components/confirmBtcTransaction/ContentLabel/index.tsx index b2fe5d1f2..2cd79c82d 100644 --- a/src/app/screens/createInscription/ContentLabel/index.tsx +++ b/src/app/components/confirmBtcTransaction/ContentLabel/index.tsx @@ -101,9 +101,16 @@ type Props = { contentType: string; content: string; repeat?: number; + inscriptionId?: string; }; -function ContentIcon({ type, content, contentType: inputContentType, repeat = 1 }: Props) { +function ContentIcon({ + type, + content, + contentType: inputContentType, + repeat = 1, + inscriptionId, +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'INSCRIPTION_REQUEST.PREVIEW' }); const [showPreview, setShowPreview] = useState(false); const [showMenu, setShowMenu] = useState(false); @@ -245,6 +252,7 @@ function ContentIcon({ type, content, contentType: inputContentType, repeat = 1 contentTypeRaw={inputContentType} type={type} visible={showPreview} + inscriptionId={inscriptionId} onClick={() => setShowPreview(false)} /> diff --git a/src/app/screens/createInscription/ContentLabel/preview.tsx b/src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx similarity index 84% rename from src/app/screens/createInscription/ContentLabel/preview.tsx rename to src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx index cedd7e592..ca6847dd0 100644 --- a/src/app/screens/createInscription/ContentLabel/preview.tsx +++ b/src/app/components/confirmBtcTransaction/ContentLabel/preview.tsx @@ -2,6 +2,8 @@ import { X } from '@phosphor-icons/react'; import { useLayoutEffect, useRef, useState } from 'react'; import styled from 'styled-components'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; import { ContentType } from './common'; const MIN_FONT_SIZE = 9; @@ -73,12 +75,22 @@ type Props = { contentType: ContentType; contentTypeRaw: string; visible: boolean; + inscriptionId?: string; }; -function Preview({ onClick, type, content, contentType, contentTypeRaw, visible }: Props) { +function Preview({ + onClick, + type, + content, + contentType, + contentTypeRaw, + visible, + inscriptionId, +}: Props) { const containerRef = useRef(null); const textRef = useRef(null); const [fontSize, setFontSize] = useState(24); + const { network } = useWalletSelector(); useLayoutEffect(() => { // this decreases the font size until the preview text fits in the container @@ -121,7 +133,17 @@ function Preview({ onClick, type, content, contentType, contentTypeRaw, visible ); } else if (contentType === ContentType.IMAGE) { - preview = ; + // workaround to show the inscription preview for an already inscribed inscription + if (inscriptionId) { + preview = ( + + ); + } else { + preview = ; + } } else if (contentType === ContentType.VIDEO) { preview = ( diff --git a/src/app/screens/createInscription/ContentLabel/utils.ts b/src/app/components/confirmBtcTransaction/ContentLabel/utils.ts similarity index 100% rename from src/app/screens/createInscription/ContentLabel/utils.ts rename to src/app/components/confirmBtcTransaction/ContentLabel/utils.ts diff --git a/src/app/components/confirmBtcTransaction/burnSection.tsx b/src/app/components/confirmBtcTransaction/burnSection.tsx index 70ac3437f..a56f46c4c 100644 --- a/src/app/components/confirmBtcTransaction/burnSection.tsx +++ b/src/app/components/confirmBtcTransaction/burnSection.tsx @@ -46,11 +46,7 @@ function BurnSection({ burns }: Props) { {burns.map((burn) => ( - + ))} diff --git a/src/app/components/confirmBtcTransaction/delegateSection.tsx b/src/app/components/confirmBtcTransaction/delegateSection.tsx index c5703b771..c2b29e9f3 100644 --- a/src/app/components/confirmBtcTransaction/delegateSection.tsx +++ b/src/app/components/confirmBtcTransaction/delegateSection.tsx @@ -86,11 +86,7 @@ function DelegateSection({ delegations }: Props) { {delegations.map((delegation) => ( - + ))} setShowDelegationInfo((prevState) => !prevState)}> diff --git a/src/app/components/confirmBtcTransaction/etchSection.tsx b/src/app/components/confirmBtcTransaction/etchSection.tsx new file mode 100644 index 000000000..0b1f25230 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/etchSection.tsx @@ -0,0 +1,236 @@ +import useOrdinalsApi from '@hooks/apiClients/useOrdinalsApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { ArrowRight } from '@phosphor-icons/react'; +import { EtchActionDetails, Inscription, RUNE_DISPLAY_DEFAULTS } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import Theme from '../../../theme'; +import InscribeSection from './inscribeSection'; +import { + AddressLabel, + Container, + Header, + RowCenter, + RuneAmount, + RuneData, + RuneImage, + RuneSymbol, + RuneValue, +} from './runes'; + +type Props = { + etch?: EtchActionDetails; +}; + +/** + * only used for ordinals service etches + */ +function EtchSection({ etch }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { ordinalsAddress } = useSelectedAccount(); + const [delegateInscriptionDetails, setDelegateInscriptionDetails] = useState( + null, + ); + const ordinalsApi = useOrdinalsApi(); + + const fetchInscriptionDetails = useCallback(async (inscriptionId: string) => { + const inscriptionDetails = await ordinalsApi.getInscription(inscriptionId); + if (inscriptionDetails) { + setDelegateInscriptionDetails(inscriptionDetails); + } + }, []); + + useEffect(() => { + if (etch?.delegateInscriptionId) { + fetchInscriptionDetails(etch.delegateInscriptionId); + } + }, [fetchInscriptionDetails, etch?.delegateInscriptionId]); + + if (!etch) return null; + + return ( + <> + +
+ + {t('YOU_WILL_ISSUE')} + +
+
+ + {t('NAME')} + + + {etch.runeName} + +
+
+ + {t('SYMBOL')} + + + {etch.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol} + +
+
+ + {t('DIVISIBILITY')} + + + {etch.divisibility || RUNE_DISPLAY_DEFAULTS.divisibility} + +
+
+ + {t('MINTABLE')} + + + {etch.isMintable ? t('YES') : t('NO')} + +
+
+ + {t('MINT_AMOUNT')} + + + ( + + {value} + + )} + /> + +
+
+ + {t('MINTING_LIMIT')} + + ( + + {value} + + )} + /> +
+ {etch.terms?.heightStart || + (etch.terms?.heightEnd && ( +
+ + {t('RUNE_BLOCK_HEIGHT_TERM')} + + + {`${etch.terms.heightStart}/${etch.terms.heightEnd}`} + +
+ ))} + {etch.terms?.offsetStart || + (etch.terms?.offsetEnd && ( +
+ + {t('RUNE_BLOCK_OFFSET_TERM')} + + + {`${etch.terms.offsetStart ? etch.terms.offsetStart : '-'}/${etch.terms.offsetEnd}`} + +
+ ))} +
+ {etch.premine && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + + {' '} + {etch.destinationAddress && etch.destinationAddress !== ordinalsAddress + ? getShortTruncatedAddress(etch.destinationAddress) + : t('YOUR_ORDINAL_ADDRESS')} + + +
+
+ + {etch.runeName} + +
+
+ + + + {etch.symbol || '¤'} + + + + + {t('AMOUNT')} + + {etch && ( + + {t('RUNE_SIZE')}: {RUNE_DISPLAY_DEFAULTS.size} Sats + + )} + + + ( + + + {value} + + + {etch?.symbol ?? '¤'} + + + )} + /> +
+
+ )} + {etch.inscriptionDetails && ( + + )} + {etch.delegateInscriptionId && delegateInscriptionDetails && ( + + )} + + ); +} + +export default EtchSection; diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index b14384ab3..e9c4bcf76 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -1,12 +1,17 @@ import { delay } from '@common/utils/ledger'; -import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; import { Tab } from '@components/tabBar'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import TransportFactory from '@ledgerhq/hw-transport-webusb'; -import { RuneSummary, Transport, btcTransaction } from '@secretkeylabs/xverse-core'; +import { + RuneSummary, + RuneSummaryActions, + Transport, + btcTransaction, +} from '@secretkeylabs/xverse-core'; import Callout from '@ui-library/callout'; import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; +import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; import { isLedgerAccount } from '@utils/helper'; import { useState } from 'react'; @@ -47,7 +52,7 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - runeSummary?: RuneSummary; + runeSummary?: RuneSummaryActions | RuneSummary; showCenotaphCallout: boolean; isLoading: boolean; isSubmitting: boolean; @@ -113,7 +118,7 @@ function ConfirmBtcTransaction({ const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const hideBackButton = !onBackClick; const hasInsufficientRunes = @@ -228,7 +233,7 @@ function ConfirmBtcTransaction({ )} - setIsModalVisible(false)}> + setIsModalVisible(false)}> )} - + ); } diff --git a/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx b/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx new file mode 100644 index 000000000..3a5c3586e --- /dev/null +++ b/src/app/components/confirmBtcTransaction/inscribeSection/index.tsx @@ -0,0 +1,78 @@ +import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; +import { ArrowDown } from '@phosphor-icons/react'; +import { StyledP } from '@ui-library/common.styled'; +import { getShortTruncatedAddress } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import ContentLabel from '../ContentLabel'; +import { Pill, StyledPillLabel } from '../runes'; +import { + ButtonIcon, + CardContainer, + CardRow, + IconLabel, + InfoIconContainer, + YourAddress, +} from './styles'; + +type InscribeSectionProps = { + contentType: string; + content: string; + payloadType: 'BASE_64' | 'PLAIN_TEXT'; + ordinalsAddress: string; + repeat?: number; + inscriptionId?: string; +}; + +function InscribeSection({ + repeat, + content, + contentType, + ordinalsAddress, + payloadType, + inscriptionId, +}: InscribeSectionProps) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + return ( + + + + {t('INSCRIBE.TITLE')} + {repeat && {`x${repeat}`}} + + + + +
+ +
+
{t('INSCRIBE.ORDINAL')}
+
+ +
+ + + + + + {t('INSCRIBE.TO')} + + + + {getShortTruncatedAddress(ordinalsAddress)} + + + {t('INSCRIBE.YOUR_ADDRESS')} + + + +
+ ); +} + +export default InscribeSection; diff --git a/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx b/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx new file mode 100644 index 000000000..b4475f295 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/inscribeSection/styles.tsx @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +export const CardContainer = styled.div<{ bottomPadding?: boolean }>((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.m, + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: props.theme.spacing(8), + paddingBottom: props.bottomPadding ? props.theme.spacing(12) : props.theme.spacing(8), + justifyContent: 'center', + marginBottom: props.theme.spacing(6), + fontSize: 14, +})); + +type CardRowProps = { + topMargin?: boolean; + center?: boolean; +}; +export const CardRow = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: props.center ? 'center' : 'flex-start', + justifyContent: 'space-between', + marginTop: props.topMargin ? props.theme.spacing(8) : 0, +})); + +export const IconLabel = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', +})); + +export const ButtonIcon = styled.img((props) => ({ + width: 32, + height: 32, + marginRight: props.theme.spacing(4), +})); + +export const InfoIconContainer = styled.div((props) => ({ + background: props.theme.colors.white_0, + color: props.theme.colors.elevation0, + width: 32, + height: 32, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '50%', + marginRight: props.theme.spacing(5), +})); + +export const YourAddress = styled.div` + text-align: right; +`; diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx index 5c296c50f..0a2f98d04 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -1,7 +1,8 @@ +import FiatAmountText from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { currencySymbolMap, getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; +import { getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; import Avatar from '@ui-library/avatar'; import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; @@ -9,10 +10,6 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -type Props = { - amount: number; -}; - const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ display: 'flex', flex: 1, @@ -29,36 +26,15 @@ const AvatarContainer = styled.div` margin-right: ${(props) => props.theme.space.xs}; `; +type Props = { + amount: number; +}; + export default function Amount({ amount }: Props) { const { fiatCurrency } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const { t } = useTranslation('translation'); - const getFiatAmountString = (amountParam: number, btcFiatRateParam: string) => { - const fiatAmount = getBtcFiatEquivalent( - new BigNumber(amountParam), - BigNumber(btcFiatRateParam), - ); - if (!fiatAmount) { - return ''; - } - - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - - return ( - `~ ${value}`} - /> - ); - }; - return ( @@ -76,11 +52,17 @@ export default function Amount({ amount }: Props) { displayType="text" thousandSeparator suffix=" BTC" - renderText={(value: string) => {value}} + renderText={(value: string) => ( + + {value} + + )} + /> + - - {getFiatAmountString(amount, btcFiatRate)} - diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 75e315477..ff9c391b8 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -1,5 +1,6 @@ import { mapRuneNameToPlaceholder } from '@components/confirmBtcTransaction/utils'; import TokenImage from '@components/tokenImage'; +import { RuneBase } from '@secretkeylabs/xverse-core'; import Avatar from '@ui-library/avatar'; import { StyledP } from '@ui-library/common.styled'; import { ftDecimals, getTicker } from '@utils/helper'; @@ -40,20 +41,14 @@ const StyledPRight = styled(StyledP)` `; type Props = { - tokenName: string; - amount: string; - divisibility: number; + rune: RuneBase; hasSufficientBalance?: boolean; }; -export default function RuneAmount({ - tokenName, - amount, - divisibility, - hasSufficientBalance = true, -}: Props) { +export default function RuneAmount({ rune, hasSufficientBalance = true }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const amountWithDecimals = ftDecimals(amount, divisibility); + const { runeName, amount, divisibility, symbol, inscriptionId } = rune; + const amountWithDecimals = ftDecimals(String(amount), divisibility); return ( @@ -61,7 +56,7 @@ export default function RuneAmount({ src={ ( - {tokenName} + {runeName} diff --git a/src/app/components/confirmBtcTransaction/mintSection.tsx b/src/app/components/confirmBtcTransaction/mintSection.tsx index 1edcedcc5..cd3c9b7aa 100644 --- a/src/app/components/confirmBtcTransaction/mintSection.tsx +++ b/src/app/components/confirmBtcTransaction/mintSection.tsx @@ -1,45 +1,32 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import { ArrowRight } from '@phosphor-icons/react'; -import { RuneSummary } from '@secretkeylabs/xverse-core'; +import { MintActionDetails, RuneSummary } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import { ftDecimals } from '@utils/helper'; +import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; import Theme from '../../../theme'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - paddingTop: props.theme.space.m, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -const RowCenter = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', -}); - -const AddressLabel = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -const Header = styled(RowCenter)((props) => ({ - marginBottom: props.theme.space.m, - padding: `0 ${props.theme.space.m}`, -})); +import { + AddressLabel, + Container, + Header, + Pill, + RowCenter, + RuneAmount, + RuneData, + RuneImage, + RuneSymbol, + RuneValue, + StyledPillLabel, +} from './runes'; type Props = { - mints?: RuneSummary['mint'][]; + mints?: RuneSummary['mint'][] | MintActionDetails[]; }; function MintSection({ mints }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - + const { ordinalsAddress } = useSelectedAccount(); if (!mints) return null; return ( @@ -47,46 +34,57 @@ function MintSection({ mints }: Props) { {mints.map( (mint) => mint && ( - +
- + {t('YOU_WILL_MINT')} - + {mint.repeats && {`x${mint.repeats}`}} + - {t('YOUR_ORDINAL_ADDRESS')} + {mint.destinationAddress && mint.destinationAddress !== ordinalsAddress + ? getShortTruncatedAddress(mint.destinationAddress) + : t('YOUR_ORDINAL_ADDRESS')}
- - {t('NAME')} - {mint?.runeName}
- - {t('SYMBOL')} - - - {mint?.symbol} - -
-
- - {t('AMOUNT')} - + + + + {mint?.symbol} + + + + + {t('AMOUNT')} + + {mint.runeSize && ( // This is the only place where runeSize is used + + {t('RUNE_SIZE')}: {mint.runeSize} Sats + + )} + + ( - - {value} - + + + {value} + + + {mint?.symbol} + + )} />
diff --git a/src/app/components/confirmBtcTransaction/receiveSection.tsx b/src/app/components/confirmBtcTransaction/receiveSection.tsx index 4eb37245a..bd364dfc4 100644 --- a/src/app/components/confirmBtcTransaction/receiveSection.tsx +++ b/src/app/components/confirmBtcTransaction/receiveSection.tsx @@ -1,4 +1,5 @@ import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight } from '@phosphor-icons/react'; import { btcTransaction, RuneSummary } from '@secretkeylabs/xverse-core'; @@ -54,7 +55,8 @@ function ReceiveSection({ runeReceipts, transactionIsFinal, }: Props) { - const { btcAddress, ordinalsAddress, hasActivatedRareSatsKey } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { hasActivatedRareSatsKey } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ @@ -116,11 +118,7 @@ function ReceiveSection({ {showOrdinalRunes && ordinalRuneReceipts.map((receipt) => ( - + ))} {areInscriptionRareSatsInOrdinal && ( @@ -157,11 +155,7 @@ function ReceiveSection({ {showPaymentRunes && paymentRuneReceipts.map((receipt) => ( - + ))} {amountIsBiggerThanZero && ( diff --git a/src/app/components/confirmBtcTransaction/runes.tsx b/src/app/components/confirmBtcTransaction/runes.tsx new file mode 100644 index 000000000..13fbcb731 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/runes.tsx @@ -0,0 +1,74 @@ +import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + paddingTop: props.theme.space.m, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +export const RowCenter = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', +}); + +export const AddressLabel = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +export const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +export const StyledPillLabel = styled.p` + display: flex; + align-items: center; + gap: ${(props) => props.theme.space.s}; +`; + +export const Pill = styled.span` + ${(props) => props.theme.typography.body_bold_s} + color: ${(props) => props.theme.colors.elevation0}; + background-color: ${(props) => props.theme.colors.white_0}; + padding: 3px 6px; + border-radius: 40px; +`; + +export const RuneValue = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +export const RuneSymbol = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +export const RuneData = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', +}); + +export const RuneImage = styled.div((props) => ({ + height: 32, + width: 32, + borderRadius: '50%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: props.theme.colors.white_850, +})); + +export const RuneAmount = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginLeft: props.theme.space.xs, +})); diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 1bda0424a..54c9157b7 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -7,7 +7,13 @@ import MintSection from '@components/confirmBtcTransaction/mintSection'; import TransferFeeView from '@components/transferFeeView'; import useCoinRates from '@hooks/queries/useCoinRates'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import { btcTransaction, getBtcFiatEquivalent, RuneSummary } from '@secretkeylabs/xverse-core'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { + btcTransaction, + getBtcFiatEquivalent, + RuneSummary, + RuneSummaryActions, +} from '@secretkeylabs/xverse-core'; import SelectFeeRate from '@ui-components/selectFeeRate'; import Callout from '@ui-library/callout'; import BigNumber from 'bignumber.js'; @@ -15,11 +21,12 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import DelegateSection from './delegateSection'; +import EtchSection from './etchSection'; import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; import ReceiveSection from './receiveSection'; import TransferSection from './transferSection'; import TxInOutput from './txInOutput/txInOutput'; -import { getNetAmount, isScriptOutput, isSpendOutput } from './utils'; +import { getNetAmount, isScriptOutput } from './utils'; const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, @@ -38,7 +45,7 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - runeSummary?: RuneSummary; + runeSummary?: RuneSummaryActions | RuneSummary; getFeeForFeeRate?: ( feeRate: number, useEffectiveFeeRate?: boolean, @@ -69,7 +76,7 @@ function TransactionSummary({ const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { t: tUnits } = useTranslation('translation', { keyPrefix: 'UNITS' }); - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { data: recommendedFees } = useBtcFeeRate(); const hasOutputScript = outputs.some((output) => isScriptOutput(output)); @@ -81,10 +88,12 @@ function TransactionSummary({ ordinalsAddress, }); - const isUnConfirmedInput = inputs.some((input) => !input.extendedUtxo.utxo.status.confirmed); + const isUnConfirmedInput = inputs.some( + (input) => !input.extendedUtxo.utxo.status.confirmed && input.walletWillSign, + ); const satsToFiat = (sats: string) => - getBtcFiatEquivalent(new BigNumber(sats), new BigNumber(btcFiatRate)).toNumber().toFixed(2); + getBtcFiatEquivalent(new BigNumber(sats), new BigNumber(btcFiatRate)).toString(); const showFeeSelector = !!(feeRate && getFeeForFeeRate && onFeeRateSet); @@ -132,6 +141,7 @@ function TransactionSummary({ /> {!hasRuneDelegation && } + {hasOutputScript && !runeSummary && } diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx index cac937ee8..155c83d67 100644 --- a/src/app/components/confirmBtcTransaction/transferSection.tsx +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -1,5 +1,5 @@ import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, RuneSummary } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { useTranslation } from 'react-i18next'; @@ -53,7 +53,7 @@ function TransferSection({ netAmount, onShowInscription, }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { inputFromPayment, inputFromOrdinal } = getInputsWitAssetsFromUserAddress({ @@ -96,12 +96,7 @@ function TransferSection({ transactionIsFinal && runeTransfers?.map((transfer) => ( - + )) } diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index 20e9469c1..58ab5b3c4 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -1,6 +1,6 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; import TransferDetailView from '@components/transferDetailView'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { getTruncatedAddress } from '@utils/helper'; @@ -35,7 +35,7 @@ type Props = { }; function TransactionInput({ input }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isPaymentsAddress = input.extendedUtxo.address === btcAddress; @@ -50,7 +50,9 @@ function TransactionInput({ input }: Props) { addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( ({t('YOUR_ADDRESS')}) - {getTruncatedAddress(addressToBeDisplayed)} + + {getTruncatedAddress(addressToBeDisplayed)} + ) : ( {getTruncatedAddress(addressToBeDisplayed)} @@ -61,6 +63,7 @@ function TransactionInput({ input }: Props) { ({ paddingBottom: props.theme.spacing(8), @@ -19,6 +19,10 @@ const SubValueText = styled.h1((props) => ({ color: props.theme.colors.white_400, })); +const HighlightText = styled.h1((props) => ({ + color: props.theme.colors.white_0, +})); + const YourAddressText = styled.h1((props) => ({ ...props.theme.typography.body_m, fontSize: 12, @@ -37,24 +41,51 @@ type Props = { }; function TransactionOutput({ output, scriptOutputCount }: Props) { - const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const outputWithScript = isScriptOutput(output); + const isOutputWithScript = isScriptOutput(output); + const isOutputWithPubKey = isPubKeyOutput(output); - const detailViewIcon = outputWithScript ? ScriptIcon : OutputIcon; - const detailViewHideCopyButton = outputWithScript - ? true - : btcAddress === output.address || ordinalsAddress === output.address; - const detailViewValue = outputWithScript ? ( - {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} - ) : output.address === btcAddress || output.address === ordinalsAddress ? ( - - ({t('YOUR_ADDRESS')}) - {getTruncatedAddress(output.address)} - - ) : ( - {getTruncatedAddress(output.address)} - ); + const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; + const detailViewHideCopyButton = + isOutputWithScript || isOutputWithPubKey + ? true + : btcAddress === output.address || ordinalsAddress === output.address; + + const detailView = () => { + if (isOutputWithScript) { + return {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`}; + } + if (isOutputWithPubKey) { + const outputType = output.type === 'pk' ? t('PUBLIC_KEY') : t('MULTISIG'); + const toOwnKey = + output.pubKeys?.includes(btcPublicKey) || output.pubKeys?.includes(ordinalsPublicKey); + const toOwnString = toOwnKey ? ` (${t('YOUR_PUBLIC_KEY')})` : ''; + return ( + + {outputType} + {toOwnString} + + ); + } + + if (output.address === btcAddress || output.address === ordinalsAddress) { + return ( + + ({t('YOUR_ADDRESS')}) + + {getTruncatedAddress(output.address)} + + + ); + } + + return ( + + {getTruncatedAddress(output.address)} + + ); + }; return ( @@ -62,14 +93,15 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { icon={detailViewIcon} hideAddress hideCopyButton={detailViewHideCopyButton} + dataTestID="confirm-amount" amount={`${satsToBtc( - new BigNumber(isSpendOutput(output) ? output.amount.toString() : '0'), + new BigNumber(isAddressOutput(output) ? output.amount.toString() : '0'), ).toFixed()} BTC`} - address={outputWithScript ? '' : output.address} - outputScript={outputWithScript ? output.script : undefined} - outputScriptIndex={outputWithScript ? scriptOutputCount : undefined} + address={isOutputWithScript || isOutputWithPubKey ? '' : output.address} + outputScript={isOutputWithScript ? output.script : undefined} + outputScriptIndex={isOutputWithScript ? scriptOutputCount : undefined} > - {detailViewValue} + {detailView()} ); diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts index 0e6668a74..1e8b89ca3 100644 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -18,7 +18,12 @@ export const isScriptOutput = ( ): output is btcTransaction.TransactionScriptOutput => (output as btcTransaction.TransactionScriptOutput).script !== undefined; -export const isSpendOutput = ( +export const isPubKeyOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionPubKeyOutput => + !!(output as btcTransaction.TransactionPubKeyOutput).pubKeys?.length; + +export const isAddressOutput = ( output: btcTransaction.EnhancedOutput, ): output is btcTransaction.TransactionOutput => (output as btcTransaction.TransactionOutput).address !== undefined; @@ -48,7 +53,7 @@ export const getNetAmount = ({ const totalUserReceive = outputs.reduce((accumulator: number, output) => { const isToUserAddress = - isSpendOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); + isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); if (isToUserAddress) { return accumulator + output.amount; } @@ -64,8 +69,14 @@ export const getOutputsWithAssetsFromUserAddress = ({ outputs, }: Omit) => { // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes - const outputsFromPayment: btcTransaction.TransactionOutput[] = []; - const outputsFromOrdinal: btcTransaction.TransactionOutput[] = []; + const outputsFromPayment: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; + const outputsFromOrdinal: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; outputs.forEach((output) => { if (isScriptOutput(output)) { return; @@ -126,7 +137,11 @@ export const getOutputsWithAssetsToUserAddress = ({ const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; outputs.forEach((output) => { // we want to discard outputs that are not spendable or are not to user address - if (isScriptOutput(output) || ![btcAddress, ordinalsAddress].includes(output.address)) { + if ( + isScriptOutput(output) || + isPubKeyOutput(output) || + ![btcAddress, ordinalsAddress].includes(output.address) + ) { return; } @@ -268,7 +283,11 @@ export const getSatRangesWithInscriptions = ({ return { satRanges: satRangesArray, totalExoticSats }; }; -export const mapRuneNameToPlaceholder = (runeName: string): FungibleToken => ({ +export const mapRuneNameToPlaceholder = ( + runeName: string, + symbol: string, + inscriptionId: string, +): FungibleToken => ({ protocol: 'runes', name: runeName, assetName: '', @@ -276,4 +295,6 @@ export const mapRuneNameToPlaceholder = (runeName: string): FungibleToken => ({ principal: '', total_received: '', total_sent: '', + runeSymbol: symbol, + runeInscriptionId: inscriptionId, }); diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 7ff9bb9e0..5fee38893 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -4,11 +4,12 @@ import ActionButton from '@components/button'; import RecipientComponent from '@components/recipientComponent'; import TransactionSettingAlert from '@components/transactionSetting'; import TransferFeeView from '@components/transferFeeView'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useCoinRates from '@hooks/queries/useCoinRates'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; -import useBtcClient from '@hooks/useBtcClient'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Bundle, @@ -35,11 +36,7 @@ import styled from 'styled-components'; import TransactionDetailComponent from '../transactionDetailComponent'; import SatsBundle from './bundle'; -interface MainContainerProps { - isGalleryOpen: boolean; -} - -const OuterContainer = styled.div` +const OuterContainer = styled.div` display: flex; flex-direction: column; `; @@ -73,15 +70,14 @@ const ErrorContainer = styled.div((props) => ({ marginRight: props.theme.spacing(8), })); -const ErrorText = styled.h1((props) => ({ +const ErrorText = styled.p((props) => ({ ...props.theme.typography.body_s, color: props.theme.colors.danger_medium, })); -interface ReviewTransactionTitleProps { +const ReviewTransactionText = styled.h1<{ centerAligned: boolean; -} -const ReviewTransactionText = styled.h1((props) => ({ +}>((props) => ({ ...props.theme.typography.headline_s, color: props.theme.colors.white_0, marginBottom: props.theme.spacing(16), @@ -93,7 +89,7 @@ const CalloutContainer = styled.div((props) => ({ marginhorizontal: props.theme.spacing(8), })); -interface Props { +type Props = { currentFee: BigNumber; feePerVByte: BigNumber; // TODO tim: is this the same as currentFeeRate? refactor to be clear loadingBroadcastedTx: boolean; @@ -114,7 +110,7 @@ interface Props { setCurrentFeeRate: (feeRate: BigNumber) => void; onConfirmClick: (signedTxHex: string) => void; onCancelClick: () => void; -} +}; function ConfirmBtcTransactionComponent({ currentFee, @@ -139,9 +135,10 @@ function ConfirmBtcTransactionComponent({ onCancelClick, }: Props) { const { t } = useTranslation('translation'); - const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const [loading, setLoading] = useState(false); - const { btcAddress, selectedAccount, network, feeMultipliers } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { btcAddress } = selectedAccount; + const { network, feeMultipliers } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const { selectedSatBundle } = useNftDataSelector(); const { getSeed } = useSeedVault(); @@ -353,7 +350,7 @@ function ConfirmBtcTransactionComponent({ return ( <> - + {showFeeWarning && ( @@ -377,6 +374,7 @@ function ConfirmBtcTransactionComponent({ {currencyType !== 'BTC' && bundle && } {ordinalTxUtxo ? ( ( ({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - background: props.theme.colors.elevation1, - borderRadius: 12, - overflowY: 'auto', - padding: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(6), -})); - -const TransferDetailContainer = styled.div((props) => ({ - paddingBottom: props.theme.spacing(8), -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const OutputTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(6), -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const TxIdText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(2), -})); - -const YourAddressText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_0, - marginRight: props.theme.spacing(2), -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - width: '100%', - justifyContent: 'flex-end', -}); - -const DropDownContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - flex: 1, - alignItems: 'center', - justifyContent: 'flex-end', -}); - -const TxIdContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', -}); - -const ExpandedContainer = styled(animated.div)({ - display: 'flex', - flexDirection: 'column', - marginTop: 16, -}); - -const Button = styled.button((props) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - background: 'transparent', - marginLeft: props.theme.spacing(4), -})); - -interface Props { - address: string[]; - parsedPsbt: ParsedPSBT | undefined; - isExpanded: boolean; - onArrowClick: () => void; -} - -function InputOutputComponent({ address, parsedPsbt, isExpanded, onArrowClick }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { btcAddress, ordinalsAddress } = useSelector((state: StoreState) => state.walletState); - let scriptOutputCount = 1; - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 400 }, - from: { opacity: 0, height: 0 }, - to: { - opacity: isExpanded ? 1 : 0, - height: isExpanded ? 'auto' : 0, - }, - }); - - const arrowRotation = useSpring({ - transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - const renderAddress = (addressToBeDisplayed: string) => - addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( - - (Your Address) - {getTruncatedAddress(addressToBeDisplayed)} - - ) : ( - {getTruncatedAddress(addressToBeDisplayed)} - ); - const renderSubValue = (input: PSBTInput, signedAddress: string) => - input.userSigns ? ( - renderAddress(signedAddress) - ) : ( - - {getTruncatedAddress(input.txid)} - (txid) - - ); - - function showPsbtOutput(output: PSBTOutput) { - const detailViewIcon = output.outputScript ? ScriptIcon : OutputIcon; - const detailViewHideCopyButton = output.outputScript - ? true - : btcAddress === output.address || ordinalsAddress === output.address; - const showAddress = - output.address === btcAddress || output.address === ordinalsAddress ? ( - - (Your Address) - {getTruncatedAddress(output.address)} - - ) : ( - {getTruncatedAddress(output.address)} - ); - const detailViewValue = output.outputScript ? ( - {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} - ) : ( - showAddress - ); - return ( - - {detailViewValue} - - ); - } - - return ( - - - {isExpanded ? t('INPUT') : t('INPUT_AND_OUTPUT')} - - - - - - {isExpanded && ( - - {parsedPsbt?.inputs.map((input, index) => ( - - - {renderSubValue(input, address[index])} - - - ))} - - {t('OUTPUT')} - {parsedPsbt?.outputs.map((output) => ( - {showPsbtOutput(output)} - ))} - - )} - - ); -} - -export default InputOutputComponent; diff --git a/src/app/components/confirmStxTransactionComponent/index.styled.ts b/src/app/components/confirmStxTransactionComponent/index.styled.ts new file mode 100644 index 000000000..8de1152e3 --- /dev/null +++ b/src/app/components/confirmStxTransactionComponent/index.styled.ts @@ -0,0 +1,90 @@ +import Button from '@ui-library/button'; +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + flex: 1; + padding-top: 22px; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + overflow-y: auto; + + &::-webkit-scrollbar { + display: none; + } +`; + +export const ButtonsContainer = styled.div((props) => ({ + display: 'flex', + padding: `${props.theme.space.l} ${props.theme.space.m}`, + columnGap: props.theme.space.s, +})); + +export const EditNonceButton = styled(Button)((props) => ({ + justifyContent: 'flex-start', + padding: props.theme.space.xxs, + '&.tertiary': { + color: props.theme.colors.tangerine, + '&:focus-visible': { + color: props.theme.colors.tangerine, + opacity: 0.8, + }, + '&:hover:enabled': { + color: props.theme.colors.tangerine, + opacity: 0.8, + }, + '&:active:enabled': { + color: props.theme.colors.tangerine, + opacity: 0.6, + }, + '&:disabled': { + color: props.theme.colors.tangerine, + opacity: 0.6, + }, + }, +})); + +export const SponsoredInfoText = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, +})); + +export const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); + +export const ReviewTransactionText = styled.p((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + textAlign: 'left', +})); + +export const RequestedByText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + marginTop: props.theme.space.xs, + textAlign: 'left', +})); + +export const TitleContainer = styled.div((props) => ({ + marginBottom: props.theme.space.l, +})); + +export const WarningWrapper = styled.div((props) => ({ + marginBottom: props.theme.space.m, +})); + +export const FeeRateContainer = styled.div` + margin-bottom: ${(props) => props.theme.space.m}; + background-color: ${(props) => props.theme.colors.background.elevation1}; + border-radius: ${(props) => props.theme.space.s}; + padding: ${(props) => props.theme.space.m}; +`; diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 2b5de848e..166543f2d 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -1,124 +1,53 @@ -import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg'; import { delay } from '@common/utils/ledger'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import TransactionSettingAlert from '@components/transactionSetting'; -import TransferFeeView from '@components/transferFeeView'; +import useCoinRates from '@hooks/queries/useCoinRates'; +import useStxWalletData from '@hooks/queries/useStxWalletData'; import useNetworkSelector from '@hooks/useNetwork'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; +import { FadersHorizontal } from '@phosphor-icons/react'; import type { StacksTransaction } from '@secretkeylabs/xverse-core'; import { getNonce, + getStxFiatEquivalent, microstacksToStx, - setFee, - setNonce, signLedgerStxTransaction, signMultiStxTransactions, signTransaction, stxToMicrostacks, } from '@secretkeylabs/xverse-core'; +import { estimateTransaction } from '@stacks/transactions'; +import SelectFeeRate from '@ui-components/selectFeeRate'; +import Button from '@ui-library/button'; +import Callout from '@ui-library/callout'; import { isHardwareAccount } from '@utils/helper'; +import { modifyRecommendedStxFees } from '@utils/transactions/transactions'; import BigNumber from 'bignumber.js'; import { ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div` - display: flex; - flex-direction: column; - flex: 1; - margin-top: 22px; - margin-left: 16px; - margin-right: 16px; - overflow-y: auto; - - &::-webkit-scrollbar { - display: none; - } -`; - -export const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(12), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - backgroundColor: props.theme.colors.background.elevation0, -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), - width: '100%', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - width: '100%', - marginTop: props.theme.spacing(10), -})); - -const ButtonText = styled.div((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const SponsoredInfoText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_400, -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - textAlign: 'left', -})); - -const RequestedByText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - marginTop: props.theme.spacing(4), - textAlign: 'left', -})); - -const TitleContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(16), -})); - -const WarningWrapper = styled.div((props) => ({ - marginBottom: props.theme.spacing(8), -})); - -interface Props { +import Theme from 'theme'; +import { + ButtonsContainer, + Container, + EditNonceButton, + FeeRateContainer, + RequestedByText, + ReviewTransactionText, + SponsoredInfoText, + SuccessActionsContainer, + TitleContainer, + WarningWrapper, +} from './index.styled'; + +// todo: make fee non option - that'll require change in all components using it +type Props = { initialStxTransactions: StacksTransaction[]; loading: boolean; onCancelClick: () => void; @@ -130,8 +59,9 @@ interface Props { title?: string; subTitle?: string; hasSignatures?: boolean; - onFeeChange?: (fee: BigNumber) => void; -} + fee?: string | undefined; + setFeeRate?: (feeRate: string) => void; +}; function ConfirmStxTransactionComponent({ initialStxTransactions, @@ -145,16 +75,23 @@ function ConfirmStxTransactionComponent({ onCancelClick, skipModal = false, hasSignatures = false, - onFeeChange, + fee, + setFeeRate, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); + const { t: settingsTranslate } = useTranslation('translation', { + keyPrefix: 'TRANSACTION_SETTING', + }); const selectedNetwork = useNetworkSelector(); + const { stxBtcRate, btcFiatRate } = useCoinRates(); + const { data: stxData } = useStxWalletData(); const { getSeed } = useSeedVault(); const [showFeeSettings, setShowFeeSettings] = useState(false); - const { selectedAccount, feeMultipliers } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { feeMultipliers, fiatCurrency } = useWalletSelector(); const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); const [buttonLoading, setButtonLoading] = useState(loading); const [isModalVisible, setIsModalVisible] = useState(false); @@ -165,30 +102,82 @@ function ConfirmStxTransactionComponent({ const [isTxApproved, setIsTxApproved] = useState(false); const [isTxRejected, setIsTxRejected] = useState(false); const [showFeeWarning, setShowFeeWarning] = useState(false); + const [feesLoading, setFeesLoading] = useState(false); + + const [feeRates, setFeeRates] = useState({ low: 0, medium: 0, high: 0 }); + + const stxBalance = stxData?.availableBalance.toString() ?? '0'; + // TODO: fix type error as any + const amount = (initialStxTransactions[0]?.payload as any)?.amount; useEffect(() => { setButtonLoading(loading); }, [loading]); + // Reactively estimate fees + useEffect(() => { + const fetchStxFees = async () => { + try { + setFeesLoading(true); + const [low, medium, high] = await estimateTransaction( + initialStxTransactions[0].payload, + undefined, + selectedNetwork, + ); + + const modifiedFees = modifyRecommendedStxFees( + { + low: low.fee, + medium: medium.fee, + high: high.fee, + }, + feeMultipliers, + ); + + setFeeRates({ + low: microstacksToStx(BigNumber(modifiedFees.low)).toNumber(), + medium: microstacksToStx(BigNumber(modifiedFees.medium)).toNumber(), + high: microstacksToStx(BigNumber(modifiedFees.high)).toNumber(), + }); + if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(medium.fee))).toString()); + } catch (e) { + console.error(e); + } finally { + setFeesLoading(false); + } + }; + + fetchStxFees(); + }, [selectedNetwork, initialStxTransactions]); + useEffect(() => { - const fee = new BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); + const stxTxFee = BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); - if (feeMultipliers && fee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighStacksFee))) { + if ( + feeMultipliers && + stxTxFee.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) + ) { setShowFeeWarning(true); } else if (showFeeWarning) { setShowFeeWarning(false); } }, [initialStxTransactions, feeMultipliers]); - const getFee = () => - isSponsored - ? new BigNumber(0) - : new BigNumber( - initialStxTransactions - .map((tx) => tx?.auth?.spendingCondition?.fee ?? BigInt(0)) - .reduce((prev, curr) => prev + curr, BigInt(0)) - .toString(10), - ); + const stxToFiat = (stx: string) => + getStxFiatEquivalent( + stxToMicrostacks(BigNumber(stx)), + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + ).toString(); + + const getFee = () => { + const defaultFee = isSponsored + ? BigNumber(0) + : fee + ? BigNumber(fee) + : BigNumber(feeRates.medium); + return defaultFee; + }; const getTxNonce = (): string => { const nonce = getNonce(initialStxTransactions[0]); @@ -215,6 +204,13 @@ function ConfirmStxTransactionComponent({ const seed = await getSeed(); let signedTxs: StacksTransaction[] = []; + + if (fee) { + for (let i = 0; i < initialStxTransactions.length; i++) { + initialStxTransactions[i].setFee(stxToMicrostacks(BigNumber(fee)).toString()); + } + } + if (initialStxTransactions.length === 1) { const signedContractCall = await signTransaction( initialStxTransactions[0], @@ -242,18 +238,8 @@ function ConfirmStxTransactionComponent({ feeRate?: string; nonce?: string; }) => { - const fee = stxToMicrostacks(new BigNumber(settingFee)); - - if (feeMultipliers && fee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighStacksFee))) { - setShowFeeWarning(true); - } else if (showFeeWarning) { - setShowFeeWarning(false); - } - - setFee(initialStxTransactions[0], BigInt(fee.toString())); - onFeeChange?.(fee); if (nonce && nonce !== '') { - setNonce(initialStxTransactions[0], BigInt(nonce)); + initialStxTransactions[0].setNonce(BigInt(nonce)); } setOpenTransactionSettingModal(false); }; @@ -269,7 +255,6 @@ function ConfirmStxTransactionComponent({ return; } setIsButtonDisabled(true); - const transport = await Transport.create(); if (!transport) { @@ -283,6 +268,12 @@ function ConfirmStxTransactionComponent({ await delay(1500); setCurrentStepIndex(1); try { + if (fee) { + for (let i = 0; i < initialStxTransactions.length; i++) { + initialStxTransactions[i].setFee(stxToMicrostacks(BigNumber(fee)).toString()); + } + } + const signedTxs = await signLedgerStxTransaction({ transport, transactionBuffer: Buffer.from(initialStxTransactions[0].serialize()), @@ -306,6 +297,34 @@ function ConfirmStxTransactionComponent({ setCurrentStepIndex(0); }; + const setTxFee = (stxFee: string) => { + const feeToSet = stxToMicrostacks(BigNumber(stxFee)); + + if ( + feeMultipliers && + feeToSet.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) + ) { + setShowFeeWarning(true); + } else if (showFeeWarning) { + setShowFeeWarning(false); + } + setFeeRate?.(stxFee); + }; + + const checkIfEnoughBalance = (totalFee: number) => { + const hasInsufficientFunds = + amount && + BigNumber(stxBalance).isLessThan( + BigNumber(amount).plus(stxToMicrostacks(BigNumber(totalFee ?? 0))), + ); + + if (hasInsufficientFunds) { + return Promise.resolve(undefined); + } + + return Promise.resolve(totalFee); + }; + return ( <> @@ -318,34 +337,38 @@ function ConfirmStxTransactionComponent({ {showFeeWarning && ( - + )} {children} - - {/* TODO fix type error as any */} - {(initialStxTransactions[0]?.payload as any)?.amount && ( - + - )} + + {isSponsored ? ( {t('SPONSORED_TX_INFO')} ) : ( !hasSignatures && ( - + } + title={settingsTranslate('ADVANCED_SETTING_NONCE_OPTION')} + variant="tertiary" + onClick={onAdvancedSettingClick} + /> ) )} - - - - - + {text} diff --git a/src/app/components/startupLoadingScreen/index.tsx b/src/app/components/startupLoadingScreen/index.tsx new file mode 100644 index 000000000..28f1a3ab3 --- /dev/null +++ b/src/app/components/startupLoadingScreen/index.tsx @@ -0,0 +1,45 @@ +import logo from '@assets/img/xverse_logo.svg'; +import ErrorDisplay from '@components/errorDisplay'; +import rootStore from '@stores/index'; +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + backgroundColor: props.theme.colors.elevation0, +})); + +function StartupLoadingScreen(): React.ReactNode { + const [error, setError] = useState(''); + + useEffect(() => { + let currentError = error; + const intervalId = setInterval(() => { + if (!currentError && rootStore.rehydrateError.current) { + setError(rootStore.rehydrateError.current); + currentError = rootStore.rehydrateError.current; + } + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, []); + + if (error) { + return ; + } + + return ( + + logo + + ); +} + +export default StartupLoadingScreen; diff --git a/src/app/components/steps/index.tsx b/src/app/components/steps/index.tsx deleted file mode 100644 index f8a24959a..000000000 --- a/src/app/components/steps/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { nanoid } from 'nanoid'; -import styled from 'styled-components'; - -const StepsContainer = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}); - -const StepsDot = styled.div((props) => ({ - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: props.active ? props.theme.colors.action.classic : props.theme.colors.elevation3, - marginRight: props.theme.spacing(4), -})); - -interface StepsProps { - data: any[]; - activeIndex: number; - dotStrategy?: 'completion' | 'selection'; -} - -interface StepDotProps { - active: boolean; -} - -export default function Steps(props: StepsProps): JSX.Element { - const { data, activeIndex, dotStrategy } = props; - const getStrategy = (index: number) => { - if (dotStrategy === 'selection') { - return index === activeIndex; - } - return index <= activeIndex; - }; - return ( - - {data.map((view, index) => ( - - ))} - - ); -} diff --git a/src/app/components/tabs/index.tsx b/src/app/components/tabs/index.tsx new file mode 100644 index 000000000..dac97d295 --- /dev/null +++ b/src/app/components/tabs/index.tsx @@ -0,0 +1,54 @@ +import styled, { css } from 'styled-components'; + +const TabContainer = styled.div` + display: flex; + gap: ${(props) => props.theme.space.xxs}; +`; + +const TabItem = styled.div<{ $active?: boolean }>` + padding: 7px 12px 8px; + border-radius: 12px; + ${(props) => props.theme.typography.body_bold_s}; + color: ${(props) => props.theme.colors.white_200}; + text-transform: uppercase; + cursor: pointer; + user-select: none; + transition: color 0.1s ease; + + ${({ $active }) => + $active + ? css` + color: ${(props) => props.theme.colors.white_0}; + background-color: ${({ theme }) => theme.colors.elevation3}; + cursor: default; + ` + : css` + &:hover { + color: ${(props) => props.theme.colors.white_0}; + } + `} +`; + +interface TabProps { + tabs: { label: string; value: T }[]; + activeTab: T; + onTabClick: (value: T) => void; + className?: string; +} +function Tabs({ tabs, activeTab, onTabClick, className }: TabProps) { + return ( + + {tabs.map((tab) => ( + onTabClick(tab.value)} + > + {tab.label} + + ))} + + ); +} + +export default Tabs; diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index e8a60d92a..6348230e8 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -5,10 +5,9 @@ import RunesIcon from '@assets/img/transactions/runes.svg'; import { StyledBarLoader } from '@components/tilesSkeletonLoader'; import useWalletSelector from '@hooks/useWalletSelector'; import { FungibleToken } from '@secretkeylabs/xverse-core'; -import { ORDINALS_URL } from '@secretkeylabs/xverse-core/constant'; -import { CurrencyTypes } from '@utils/constants'; +import { CurrencyTypes, XVERSE_ORDIVIEW_URL } from '@utils/constants'; import { getTicker } from '@utils/helper'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import styled from 'styled-components'; const DEFAULT_SIZE = 40; @@ -32,12 +31,12 @@ const TickerIconContainer = styled.div<{ size?: number; round?: boolean }>((prop height: props.size ?? DEFAULT_SIZE, width: props.size ?? DEFAULT_SIZE, borderRadius: '50%', - backgroundColor: props.theme.colors.white_400, + backgroundColor: props.theme.colors.white_850, })); const TickerIconText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, - color: props.theme.colors.elevation0, + color: props.theme.colors.white_0, textAlign: 'center', wordBreak: 'break-all', fontSize: 11, @@ -56,7 +55,7 @@ const ProtocolIcon = styled.div<{ isSquare?: boolean }>((props) => ({ position: 'absolute', right: props.isSquare ? -9 : -11, bottom: -2, - backgroundColor: props.theme.colors.elevation1, + backgroundColor: props.theme.colors.elevation0, padding: 2, })); @@ -84,6 +83,7 @@ export default function TokenImage({ }: TokenImageProps) { const { network } = useWalletSelector(); const ftProtocol = fungibleToken?.protocol; + const [imageError, setImageError] = useState(false); const getCurrencyIcon = useCallback(() => { if (currency === 'STX') { @@ -111,24 +111,47 @@ export default function TokenImage({ }; const renderIcon = () => { + const ticker = + fungibleToken?.ticker || + (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); + + if (imageError) { + return ( + + {ticker.substring(0, 4)} + + ); + } + if (!fungibleToken) { - return ; + return ( + setImageError(true)} + /> + ); } if (fungibleToken?.image) { - return ; + return ( + setImageError(true)} + /> + ); } if (fungibleToken.runeInscriptionId) { - const img = new Image(); // determine if valid image - img.src = ORDINALS_URL(network.type, fungibleToken.runeInscriptionId); - if (img.complete) { - return ( - - ); - } + return ( + setImageError(true)} + /> + ); } if (fungibleToken.runeSymbol) { return ( @@ -138,10 +161,6 @@ export default function TokenImage({ ); } - const ticker = fungibleToken?.name - ? getTicker(fungibleToken.name) - : fungibleToken?.ticker || fungibleToken?.assetName || ''; - return ( {ticker.substring(0, 4)} diff --git a/src/app/components/tokenTile/index.tsx b/src/app/components/tokenTile/index.tsx index 82df3330c..c16990094 100644 --- a/src/app/components/tokenTile/index.tsx +++ b/src/app/components/tokenTile/index.tsx @@ -4,11 +4,10 @@ import TokenImage from '@components/tokenImage'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; import useStxWalletData from '@hooks/queries/useStxWalletData'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; -import { microstacksToStx, satsToBtc } from '@secretkeylabs/xverse-core'; +import { FungibleToken, getFiatEquivalent } from '@secretkeylabs/xverse-core'; import { StoreState } from '@stores/index'; import { CurrencyTypes } from '@utils/constants'; -import { getFtBalance, getFtTicker } from '@utils/tokens'; +import { getBalanceAmount, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { NumericFormat } from 'react-number-format'; import { useSelector } from 'react-redux'; @@ -105,7 +104,7 @@ interface Props { title: string; loading: boolean; currency: CurrencyTypes; - onPress: (coin: CurrencyTypes, ftKey: string | undefined) => void; + onPress: (coin: CurrencyTypes, fungibleToken: FungibleToken | undefined) => void; fungibleToken?: FungibleToken; enlargeTicker?: boolean; className?: string; @@ -127,41 +126,26 @@ function TokenTile({ const { data: stxData } = useStxWalletData(); const { data: btcBalance } = useBtcWalletData(); - function getTickerTitle() { + const getTickerTitle = () => { if (currency === 'STX' || currency === 'BTC') return `${currency}`; return `${getFtTicker(fungibleToken as FungibleToken)}`; - } - - function getBalanceAmount() { - switch (currency) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)).toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).toString(); - case 'FT': - return fungibleToken ? getFtBalance(fungibleToken) : ''; - default: + }; + + const handleTokenPressed = () => onPress(currency, fungibleToken); + + const getFiatAmount = () => { + const fiatAmount = getFiatEquivalent( + Number(getBalanceAmount(currency, fungibleToken, stxData, btcBalance)), + currency, + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + fungibleToken, + ); + if (fiatAmount) { + return BigNumber(fiatAmount); } - } - - function getFiatEquivalent(): BigNumber | undefined { - switch (currency) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)) - .multipliedBy(stxBtcRate) - .multipliedBy(btcFiatRate); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).multipliedBy(btcFiatRate); - case 'FT': - return fungibleToken?.tokenFiatRate - ? new BigNumber(getFtBalance(fungibleToken)).multipliedBy(fungibleToken.tokenFiatRate) - : undefined; - default: - return undefined; - } - } - - const handleTokenPressed = () => onPress(currency, fungibleToken?.principal); + return undefined; + }; return ( @@ -185,12 +169,12 @@ function TokenTile({ ) : ( {value}} /> - + )} diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx index 6d4e55fce..4d2621063 100644 --- a/src/app/components/topRow/index.tsx +++ b/src/app/components/topRow/index.tsx @@ -1,4 +1,4 @@ -import { ArrowLeft } from '@phosphor-icons/react'; +import { ArrowLeft, DotsThreeVertical } from '@phosphor-icons/react'; import styled from 'styled-components'; import Theme from 'theme'; @@ -30,19 +30,26 @@ const BackButton = styled.button((props) => ({ '&:hover': { backgroundColor: props.theme.colors.white_900, }, - '&:focus': { - backgroundColor: props.theme.colors.white_850, - }, })); -interface Props { +const MenuButton = styled.button((props) => ({ + display: 'flex', + justifyContent: 'flex-end', + backgroundColor: 'transparent', + padding: props.theme.space.xxs, + position: 'absolute', + right: props.theme.space.xxs, +})); + +type Props = { title?: string; onClick: (e: React.MouseEvent) => void; showBackButton?: boolean; className?: string; -} + onMenuClick?: (e: React.MouseEvent) => void; +}; -function TopRow({ title, onClick, showBackButton = true, className }: Props) { +function TopRow({ title, onClick, showBackButton = true, className, onMenuClick }: Props) { return ( {showBackButton && ( @@ -51,6 +58,11 @@ function TopRow({ title, onClick, showBackButton = true, className }: Props) { )} {title && {title}} + {onMenuClick && ( + + + + )} ); } diff --git a/src/app/components/transactionDetailComponent/index.tsx b/src/app/components/transactionDetailComponent/index.tsx index f3fc866ce..5b03b91e2 100644 --- a/src/app/components/transactionDetailComponent/index.tsx +++ b/src/app/components/transactionDetailComponent/index.tsx @@ -1,34 +1,31 @@ -import { currencySymbolMap } from '@secretkeylabs/xverse-core'; -import { StoreState } from '@stores/index'; +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; import BigNumber from 'bignumber.js'; -import { NumericFormat } from 'react-number-format'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'row', background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '12px 16px', + borderRadius: props.theme.radius(2), + padding: props.theme.space.m, justifyContent: 'center', alignItems: 'center', - marginBottom: 12, + marginBottom: props.theme.space.s, })); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const TitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_200, })); -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const ValueText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_0, })); -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, +const SubValueText = styled.p((props) => ({ + ...props.theme.typography.body_s, textAlign: 'right', color: props.theme.colors.white_400, })); @@ -45,34 +42,17 @@ const TitleContainer = styled.div({ flexDirection: 'column', }); -interface Props { +type Props = { title: string; subTitle?: string; value?: string | React.ReactNode; description?: string; subValue?: BigNumber; -} +}; function TransactionDetailComponent({ title, subTitle, value, subValue, description }: Props) { - const { fiatCurrency } = useSelector((state: StoreState) => state.walletState); + const { fiatCurrency } = useWalletSelector(); - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - - ); - } - return ''; - }; return ( @@ -82,7 +62,7 @@ function TransactionDetailComponent({ title, subTitle, value, subValue, descript {value && {value}} {description && {description}} - {subValue && {getFiatAmountString(subValue)}} + {subValue && } ); diff --git a/src/app/components/transactionSetting/editBtcFee.tsx b/src/app/components/transactionSetting/editBtcFee.tsx index fc63a7616..f16745e6a 100644 --- a/src/app/components/transactionSetting/editBtcFee.tsx +++ b/src/app/components/transactionSetting/editBtcFee.tsx @@ -1,12 +1,13 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useCoinRates from '@hooks/queries/useCoinRates'; -import useBtcClient from '@hooks/useBtcClient'; import useBtcFees from '@hooks/useBtcFees'; import useDebounce from '@hooks/useDebounce'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Faders } from '@phosphor-icons/react'; import { - currencySymbolMap, ErrorCodes, getBtcFees, getBtcFeesForNonOrdinalBtcSend, @@ -27,8 +28,6 @@ import FeeItem from './feeItem'; const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.space.m, - marginRight: props.theme.space.m, paddingBottom: props.theme.space.m, })); @@ -37,10 +36,9 @@ const DetailText = styled.h1((props) => ({ color: props.theme.colors.white_200, })); -interface InputContainerProps { +const InputContainer = styled.div<{ withError?: boolean; -} -const InputContainer = styled.div((props) => ({ +}>((props) => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -74,7 +72,7 @@ const InputField = styled.input((props) => ({ }, })); -const FeeText = styled.h1((props) => ({ +const FeeText = styled.span((props) => ({ ...props.theme.typography.body_m, color: props.theme.colors.white_0, })); @@ -96,11 +94,9 @@ const FeePrioritiesContainer = styled.div` flex-direction: column; `; -interface FeeContainerProps { +const FeeItemContainer = styled.button<{ isSelected: boolean; -} - -const FeeItemContainer = styled.button` +}>` display: flex; padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; align-items: center; @@ -138,7 +134,7 @@ const TotalFeeText = styled(StyledP)` margin-right: ${(props) => props.theme.space.xxs}; `; -interface Props { +type Props = { type?: string; fee: string; feeRate?: BigNumber | string; @@ -157,7 +153,8 @@ interface Props { setError: (error: string) => void; setCustomFeeSelected: (selected: boolean) => void; feeOptionSelected: (feeRate: string, totalFee: string) => void; -} +}; + function EditBtcFee({ type, fee, @@ -180,8 +177,9 @@ function EditBtcFee({ }: Props) { const { t } = useTranslation('translation'); - const { network, btcAddress, fiatCurrency, selectedAccount, ordinalsAddress } = - useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { btcAddress, ordinalsAddress } = selectedAccount; + const { network, fiatCurrency } = useWalletSelector(); const { btcFiatRate } = useCoinRates(); const [totalFee, setTotalFee] = useState(fee); const [feeRateInput, setFeeRateInput] = useState(feeRate?.toString() ?? ''); @@ -281,33 +279,6 @@ function EditBtcFee({ } }, [debouncedFeeRateInput]); - function getFiatEquivalent() { - return getBtcFiatEquivalent(new BigNumber(totalFee), BigNumber(btcFiatRate)); - } - - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - ( - - {value} - - )} - /> - ); - } - return ''; - }; - const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { if (error) { setError(''); @@ -336,8 +307,9 @@ function EditBtcFee({ time="~10 mins" feeRate={feeData?.highFeeRate} totalFee={feeData?.highTotalFee} - fiat={getFiatAmountString( - getBtcFiatEquivalent(new BigNumber(feeData.highTotalFee), BigNumber(btcFiatRate)), + fiatAmount={getBtcFiatEquivalent( + new BigNumber(feeData.highTotalFee), + BigNumber(btcFiatRate), )} onClick={() => { feeOptionSelected(feeData?.highFeeRate?.toString() || '', feeData?.highTotalFee); @@ -351,8 +323,9 @@ function EditBtcFee({ time="~30 mins" feeRate={feeData?.standardFeeRate} totalFee={feeData?.standardTotalFee} - fiat={getFiatAmountString( - getBtcFiatEquivalent(new BigNumber(feeData.standardTotalFee), BigNumber(btcFiatRate)), + fiatAmount={getBtcFiatEquivalent( + new BigNumber(feeData.standardTotalFee), + BigNumber(btcFiatRate), )} onClick={() => { feeOptionSelected( @@ -411,9 +384,10 @@ function EditBtcFee({ )} /> - - {getFiatAmountString(getFiatEquivalent())} - + {error && {error}} diff --git a/src/app/components/transactionSetting/editNonce.tsx b/src/app/components/transactionSetting/editNonce.tsx index 5f28bf038..2b4bbbaa2 100644 --- a/src/app/components/transactionSetting/editNonce.tsx +++ b/src/app/components/transactionSetting/editNonce.tsx @@ -1,58 +1,31 @@ -import InfoContainer from '@components/infoContainer'; +import Callout from '@ui-library/callout'; +import Input from '@ui-library/input'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import Theme from 'theme'; -const NonceContainer = styled.div((props) => ({ +const Container = styled.div({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); +}); -const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, +const Description = styled.p((props) => ({ + ...props.theme.typography.body_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), -})); - -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(8), -})); - -const InputContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(6), - border: `1px solid ${props.theme.colors.elevation6}`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: 8, - paddingLeft: props.theme.spacing(5), - paddingRight: props.theme.spacing(5), - paddingTop: props.theme.spacing(5), - paddingBottom: props.theme.spacing(5), -})); - -const InputField = styled.input((props) => ({ - ...props.theme.body_m, - backgroundColor: props.theme.colors.elevation1, - color: props.theme.colors.white_400, - width: '100%', - border: 'transparent', + marginBottom: props.theme.space.l, })); -interface Props { +type Props = { nonce: string; setNonce: (nonce: string) => void; -} +}; + function EditNonce({ nonce, setNonce }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'TRANSACTION_SETTING' }); const [nonceInput, setNonceInput] = useState(nonce); - const onInputEditNonceChange = (e: React.ChangeEvent) => { + const handleOnChange = (e: React.ChangeEvent) => { setNonceInput(e.target.value); }; @@ -61,14 +34,20 @@ function EditNonce({ nonce, setNonce }: Props) { }, [nonceInput, setNonce]); return ( - - {t('NONCE_INFO')} - {t('NONCE')} - - - - - + + {t('NONCE_INFO')} + +
+ +
); } diff --git a/src/app/components/transactionSetting/editStxFee.tsx b/src/app/components/transactionSetting/editStxFee.tsx index 203b22121..117c68e9e 100644 --- a/src/app/components/transactionSetting/editStxFee.tsx +++ b/src/app/components/transactionSetting/editStxFee.tsx @@ -1,14 +1,10 @@ +import FiatAmountText from '@components/fiatAmountText'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { - currencySymbolMap, - getStxFiatEquivalent, - stxToMicrostacks, -} from '@secretkeylabs/xverse-core'; +import { getStxFiatEquivalent, stxToMicrostacks } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; const Container = styled.div((props) => ({ @@ -19,25 +15,19 @@ const Container = styled.div((props) => ({ marginBottom: props.theme.spacing(2), })); -const FiatAmountText = styled.h1((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, color: props.theme.colors.white_200, })); const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, marginTop: props.theme.spacing(8), })); -interface InputContainerProps { +const InputContainer = styled.div<{ withError?: boolean; -} -const InputContainer = styled.div((props) => ({ +}>((props) => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', @@ -52,7 +42,7 @@ const InputContainer = styled.div((props) => ({ })); const InputField = styled.input((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, backgroundColor: 'transparent', color: props.theme.colors.white_0, border: 'transparent', @@ -70,17 +60,11 @@ const InputField = styled.input((props) => ({ }, })); -const SubText = styled.h1((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - -interface ButtonProps { +const FeeButton = styled.button<{ isSelected: boolean; isLastInRow?: boolean; -} -const FeeButton = styled.button((props) => ({ - ...props.theme.body_medium_m, +}>((props) => ({ + ...props.theme.typography.body_medium_m, color: `${props.isSelected ? props.theme.colors.elevation2 : props.theme.colors.white_400}`, background: `${props.isSelected ? props.theme.colors.white : 'transparent'}`, border: `1px solid ${props.isSelected ? 'transparent' : props.theme.colors.elevation6}`, @@ -113,14 +97,14 @@ const TickerContainer = styled.div({ flex: 1, }); -const ErrorText = styled.h1((props) => ({ +const ErrorText = styled.p((props) => ({ ...props.theme.body_xs, color: props.theme.colors.feedback.error, marginBottom: props.theme.spacing(2), })); // TODO tim: this component needs refactoring. separate business logic from presentation -interface Props { +type Props = { type?: string; fee: string; feeRate?: BigNumber | string; @@ -130,7 +114,8 @@ interface Props { setFeeRate: (feeRate: string) => void; setFeeMode: (feeMode: string) => void; setError: (error: string) => void; -} +}; + function EditStxFee({ type, fee, @@ -191,33 +176,6 @@ function EditStxFee({ } }, [feeRateInput]); - function getFiatEquivalent() { - return getStxFiatEquivalent( - stxToMicrostacks(new BigNumber(totalFee)), - BigNumber(stxBtcRate), - BigNumber(btcFiatRate), - ); - } - - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (fiatAmount) { - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - return ( - {value}} - /> - ); - } - return ''; - }; - const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { if (error) { setError(''); @@ -244,7 +202,14 @@ function EditStxFee({ onChange={onInputEditFeesChange} /> - {getFiatAmountString(getFiatEquivalent())} + {error && {error}} diff --git a/src/app/components/transactionSetting/feeItem.tsx b/src/app/components/transactionSetting/feeItem.tsx index 1a0d9184a..b24541cd3 100644 --- a/src/app/components/transactionSetting/feeItem.tsx +++ b/src/app/components/transactionSetting/feeItem.tsx @@ -1,16 +1,17 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; import { Bicycle, CarProfile, RocketLaunch } from '@phosphor-icons/react'; import { ErrorCodes } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Spinner from '@ui-library/spinner'; +import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Theme from 'theme'; -interface FeeContainer { +const FeeItemContainer = styled.button<{ isSelected: boolean; -} - -const FeeItemContainer = styled.button` +}>` display: flex; padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; align-items: center; @@ -65,30 +66,37 @@ const LoaderContainer = styled.div` flex: 1; `; +const StyledFiatAmountText = styled(FiatAmountText)` + ${(props) => props.theme.typography.body_medium_s} + color: ${(props) => props.theme.colors.white_200}; +`; + type FeePriority = 'high' | 'medium' | 'low'; -interface FeeItemProps { +type Props = { priority: FeePriority; time: string; feeRate: string; totalFee: string; - fiat: string | JSX.Element; + fiatAmount: BigNumber; selected: boolean; onClick?: () => void; error?: string; -} +}; function FeeItem({ priority, time, feeRate, totalFee, - fiat, + fiatAmount, selected, error, onClick, -}: FeeItemProps) { +}: Props) { const { t } = useTranslation('translation'); + const { fiatCurrency } = useWalletSelector(); + const getIcon = () => { switch (priority) { case 'high': @@ -148,9 +156,7 @@ function FeeItem({ {`${totalFee} Sats`} )} - - {fiat} - + {error && ( {getErrorMessage(error)} diff --git a/src/app/components/transactionSetting/index.tsx b/src/app/components/transactionSetting/index.tsx index 40273a0dc..5d8e1ecd7 100644 --- a/src/app/components/transactionSetting/index.tsx +++ b/src/app/components/transactionSetting/index.tsx @@ -1,10 +1,10 @@ import ArrowIcon from '@assets/img/settings/arrow.svg'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useWalletSelector from '@hooks/useWalletSelector'; import { isCustomFeesAllowed, Recipient, stxToMicrostacks, UTXO } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import Sheet from '@ui-library/sheet'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,37 +14,15 @@ import EditBtcFee from './editBtcFee'; import EditNonce from './editNonce'; import EditStxFee from './editStxFee'; -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(10), - marginBottom: props.theme.spacing(20), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - const ButtonsContainer = styled.div` display: flex; - flex-direction: row; - margin-left: ${(props) => props.theme.space.m}; - margin-right: ${(props) => props.theme.space.m}; - margin-bottom: ${(props) => props.theme.space.m}; -`; - -const LeftButton = styled.div` - display: flex; - margin-right: ${(props) => props.theme.space.xs}; - flex: 1; -`; - -const RightButton = styled.div` - display: flex; - margin-left: ${(props) => props.theme.space.xs}; - flex: 1; + column-gap: ${(props) => props.theme.space.s}; + margin-top: ${(props) => props.theme.space.l}; + margin-bottom: ${(props) => props.theme.space.xxl}; `; const TransactionSettingOptionText = styled.h1((props) => ({ - ...props.theme.body_medium_l, + ...props.theme.typography.body_medium_l, color: props.theme.colors.white_200, })); @@ -55,8 +33,6 @@ const TransactionSettingOptionButton = styled.button((props) => ({ width: '100%', marginTop: props.theme.spacing(16), marginBottom: props.theme.spacing(16), - paddingLeft: props.theme.spacing(12), - paddingRight: props.theme.spacing(12), justifyContent: 'space-between', })); @@ -66,14 +42,12 @@ const TransactionSettingNonceOptionButton = styled.button((props) => ({ flexDirection: 'row', width: '100%', marginBottom: props.theme.spacing(20), - paddingLeft: props.theme.spacing(12), - paddingRight: props.theme.spacing(12), justifyContent: 'space-between', })); type TxType = 'STX' | 'BTC' | 'Ordinals'; -interface Props { +type Props = { visible: boolean; fee: string; feePerVByte?: BigNumber; @@ -87,8 +61,9 @@ interface Props { isRestoreFlow?: boolean; nonOrdinalUtxos?: UTXO[]; showFeeSettings: boolean; + nonceSettings?: boolean; setShowFeeSettings: (value: boolean) => void; -} +}; function TransactionSettingAlert({ visible, @@ -104,6 +79,7 @@ function TransactionSettingAlert({ isRestoreFlow, nonOrdinalUtxos, showFeeSettings, + nonceSettings = false, setShowFeeSettings, }: Props) { const { t } = useTranslation('translation'); @@ -139,12 +115,17 @@ function TransactionSettingAlert({ return; } } - setShowNonceSettings(false); setShowFeeSettings(false); setError(''); onApplyClick({ fee: feeInput.toString(), nonce: nonceInput }); }; + const applyClickForNonceStx = () => { + setShowNonceSettings(false); + setError(''); + onApplyClick({ fee: feeInput.toString(), nonce: nonceInput }); + }; + const applyClickForBtc = async () => { const currentFee = new BigNumber(feeInput); if (currentFee.gt(btcBalance ?? 0)) { @@ -202,7 +183,7 @@ function TransactionSettingAlert({ }; const renderContent = () => { - if (showNonceSettings) { + if (showNonceSettings || nonceSettings) { return ; } @@ -254,14 +235,14 @@ function TransactionSettingAlert({ {t('TRANSACTION_SETTING.ADVANCED_SETTING_FEE_OPTION')} - Arrow + Arrow {type === 'STX' && ( {t('TRANSACTION_SETTING.ADVANCED_SETTING_NONCE_OPTION')} - Arrow + Arrow )} @@ -269,58 +250,47 @@ function TransactionSettingAlert({ }; return ( - {renderContent()} - {type === 'STX' && (showFeeSettings || showNonceSettings) && ( - - + )} - {!hideAddress && {getTruncatedAddress(address)}} + {!hideAddress && ( + {getTruncatedAddress(address)} + )} {!hideCopyButton && } diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 149992b7e..7c2e0c2ba 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -1,25 +1,25 @@ import AmountWithInscriptionSatribute from '@components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; +import FiatAmountText from '@components/fiatAmountText'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; +import { PencilSimple } from '@phosphor-icons/react'; import { btcTransaction, - currencySymbolMap, getBtcFiatEquivalent, getFiatEquivalent, } from '@secretkeylabs/xverse-core'; -import { StoreState } from '@stores/index'; import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import Theme from 'theme'; const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '12px 16px', - marginBottom: 12, + borderRadius: props.theme.radius(2), + padding: props.theme.space.m, + marginBottom: props.theme.space.s, })); const Row = styled.div({ @@ -40,15 +40,35 @@ const FeeContainer = styled.div({ alignItems: 'flex-end', }); -interface Props { +const CustomRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const EditButton = styled.button` + display: flex; + flex-direction: row; + background: transparent; + align-items: center; + gap: ${(props) => props.theme.space.xxs}; + cursor: ${(props) => (props.onClick ? 'pointer' : 'initial')}; + width: 100%; + margin-left: ${(props) => props.theme.space.xs}; +`; + +type Props = { feePerVByte?: BigNumber; fee: BigNumber; currency: string; title?: string; inscriptions?: btcTransaction.IOInscription[]; satributes?: btcTransaction.IOSatribute[]; + customFeeClick?: () => void; + subtitle?: string; onShowInscription?: (inscription: btcTransaction.IOInscription) => void; -} +}; + function TransferFeeView({ feePerVByte, fee, @@ -56,6 +76,8 @@ function TransferFeeView({ title, inscriptions = [], satributes = [], + customFeeClick, + subtitle, onShowInscription = () => {}, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); @@ -64,27 +86,6 @@ function TransferFeeView({ const { fiatCurrency } = useWalletSelector(); const { btcFiatRate, stxBtcRate } = useCoinRates(); - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (!fiatAmount) { - return ''; - } - - if (fiatAmount.isLessThan(0.01)) { - return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - - return ( - `~ ${value}`} - /> - ); - }; - return ( @@ -92,6 +93,24 @@ function TransferFeeView({ {title ?? t('FEES')} + {customFeeClick && ( + + + Custom + + {}}> + + Edit + + + + + )} + {subtitle && ( + + {subtitle} + + )} ( - + {value} )} @@ -119,18 +138,21 @@ function TransferFeeView({ /> )} - {getFiatAmountString( - currency === 'sats' - ? getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)) - : new BigNumber( - getFiatEquivalent( - Number(fee), - 'STX', - BigNumber(stxBtcRate), - BigNumber(btcFiatRate), - )!, - ), - )} + diff --git a/src/app/hooks/useBtcClient.ts b/src/app/hooks/apiClients/useBtcClient.ts similarity index 90% rename from src/app/hooks/useBtcClient.ts rename to src/app/hooks/apiClients/useBtcClient.ts index 18320c299..c5f3466d4 100644 --- a/src/app/hooks/useBtcClient.ts +++ b/src/app/hooks/apiClients/useBtcClient.ts @@ -1,6 +1,6 @@ import { BitcoinEsploraApiProvider } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import useWalletSelector from './useWalletSelector'; +import useWalletSelector from '../useWalletSelector'; const useBtcClient = () => { const { network } = useWalletSelector(); diff --git a/src/app/hooks/useOrdinalsApi.ts b/src/app/hooks/apiClients/useOrdinalsApi.ts similarity index 85% rename from src/app/hooks/useOrdinalsApi.ts rename to src/app/hooks/apiClients/useOrdinalsApi.ts index d455897ce..7d30c72ba 100644 --- a/src/app/hooks/useOrdinalsApi.ts +++ b/src/app/hooks/apiClients/useOrdinalsApi.ts @@ -1,6 +1,6 @@ import { OrdinalsApi } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import useWalletSelector from './useWalletSelector'; +import useWalletSelector from '../useWalletSelector'; const useOrdinalsApi = () => { const { network } = useWalletSelector(); diff --git a/src/app/hooks/apiClients/useOrdinalsServiceApi.ts b/src/app/hooks/apiClients/useOrdinalsServiceApi.ts new file mode 100644 index 000000000..38e84483e --- /dev/null +++ b/src/app/hooks/apiClients/useOrdinalsServiceApi.ts @@ -0,0 +1,10 @@ +import { getOrdinalsServiceApiClient } from '@secretkeylabs/xverse-core'; +import { useMemo } from 'react'; +import useWalletSelector from '../useWalletSelector'; + +const useOrdinalsServiceApi = () => { + const { network } = useWalletSelector(); + return useMemo(() => getOrdinalsServiceApiClient(network.type), [network.type]); +}; + +export default useOrdinalsServiceApi; diff --git a/src/app/hooks/useRunesApi.ts b/src/app/hooks/apiClients/useRunesApi.ts similarity index 83% rename from src/app/hooks/useRunesApi.ts rename to src/app/hooks/apiClients/useRunesApi.ts index 5b2f925ec..a68dfe562 100644 --- a/src/app/hooks/useRunesApi.ts +++ b/src/app/hooks/apiClients/useRunesApi.ts @@ -1,6 +1,6 @@ import { getRunesClient } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import useWalletSelector from './useWalletSelector'; +import useWalletSelector from '../useWalletSelector'; const useRunesApi = () => { const { network } = useWalletSelector(); diff --git a/src/app/hooks/queries/ordinals/useAddressInscription.ts b/src/app/hooks/queries/ordinals/useAddressInscription.ts index 07d39f9a4..adfb06012 100644 --- a/src/app/hooks/queries/ordinals/useAddressInscription.ts +++ b/src/app/hooks/queries/ordinals/useAddressInscription.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { getInscription, Inscription } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; @@ -7,7 +8,8 @@ import { handleRetries, InvalidParamsError } from '@utils/query'; * Get inscription details belonging to an address by ordinalId */ const useAddressInscription = (ordinalId: string, ordinal?: Inscription | null) => { - const { ordinalsAddress, network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const fetchOrdinals = async (): Promise => { if (ordinal && ordinal.id === ordinalId) return ordinal; if (!ordinalsAddress || !ordinalId) { diff --git a/src/app/hooks/queries/ordinals/useAddressInscriptionCollections.ts b/src/app/hooks/queries/ordinals/useAddressInscriptionCollections.ts index 7a82aff39..a403d5dbc 100644 --- a/src/app/hooks/queries/ordinals/useAddressInscriptionCollections.ts +++ b/src/app/hooks/queries/ordinals/useAddressInscriptionCollections.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { getCollections } from '@secretkeylabs/xverse-core'; import { useInfiniteQuery } from '@tanstack/react-query'; @@ -9,7 +10,8 @@ const PAGE_SIZE = 30; * Get collections belonging to an address */ const useAddressInscriptionCollections = () => { - const { ordinalsAddress, network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const getCollectionsByAddress = async ({ pageParam = 0 }) => { if (!ordinalsAddress) { diff --git a/src/app/hooks/queries/ordinals/useAddressInscriptions.ts b/src/app/hooks/queries/ordinals/useAddressInscriptions.ts index a2fcce9c6..a2392a6de 100644 --- a/src/app/hooks/queries/ordinals/useAddressInscriptions.ts +++ b/src/app/hooks/queries/ordinals/useAddressInscriptions.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { getCollectionSpecificInscriptions } from '@secretkeylabs/xverse-core'; import { useInfiniteQuery } from '@tanstack/react-query'; @@ -9,7 +10,8 @@ const PAGE_SIZE = 30; * Get inscriptions belonging to an address, filtered by collection id */ const useAddressInscriptions = (collectionId?: string) => { - const { ordinalsAddress, network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const getInscriptionsByAddress = async ({ pageParam = 0 }) => { if (!ordinalsAddress || !collectionId) { diff --git a/src/app/hooks/queries/ordinals/useAddressRareSats.ts b/src/app/hooks/queries/ordinals/useAddressRareSats.ts index 3e21ff647..a318c2e8c 100644 --- a/src/app/hooks/queries/ordinals/useAddressRareSats.ts +++ b/src/app/hooks/queries/ordinals/useAddressRareSats.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { getAddressUtxoOrdinalBundles, @@ -5,12 +6,13 @@ import { mapRareSatsAPIResponseToBundle, } from '@secretkeylabs/xverse-core'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { handleRetries, InvalidParamsError } from '@utils/query'; +import { InvalidParamsError, handleRetries } from '@utils/query'; const PAGE_SIZE = 30; export const useAddressRareSats = () => { - const { ordinalsAddress, network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const getRareSatsByAddress = async ({ pageParam = 0 }) => { if (!ordinalsAddress) { @@ -24,7 +26,7 @@ export const useAddressRareSats = () => { PAGE_SIZE, { hideUnconfirmed: true, - hideInscriptionOnly: true, + hideSpecialWithoutSatributes: true, }, ); return bundleResponse; diff --git a/src/app/hooks/queries/ordinals/useCollectionMarketData.ts b/src/app/hooks/queries/ordinals/useCollectionMarketData.ts index ab5a9e8b1..425bb3588 100644 --- a/src/app/hooks/queries/ordinals/useCollectionMarketData.ts +++ b/src/app/hooks/queries/ordinals/useCollectionMarketData.ts @@ -1,7 +1,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { CollectionMarketDataResponse, getCollectionMarketData } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -import { handleRetries, InvalidParamsError } from '@utils/query'; +import { InvalidParamsError, handleRetries } from '@utils/query'; /** * Get inscription collection market data diff --git a/src/app/hooks/queries/ordinals/useGetBrc20FungibleTokens.ts b/src/app/hooks/queries/ordinals/useGetBrc20FungibleTokens.ts index 8e7aefd6e..b032cdf09 100644 --- a/src/app/hooks/queries/ordinals/useGetBrc20FungibleTokens.ts +++ b/src/app/hooks/queries/ordinals/useGetBrc20FungibleTokens.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Brc20Token, @@ -9,7 +10,7 @@ import { import { useQuery } from '@tanstack/react-query'; import BigNumber from 'bignumber.js'; -export const brc20TokenToFungibleToken = (coin: Brc20Token): FungibleToken => ({ +const brc20TokenToFungibleToken = (coin: Brc20Token): FungibleToken => ({ name: coin.name, principal: coin.ticker ?? coin.name, balance: '0', @@ -54,14 +55,27 @@ export const fetchBrc20FungibleTokens = }; export const useGetBrc20FungibleTokens = () => { - const { ordinalsAddress, fiatCurrency, network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const { fiatCurrency, network, spamTokens, showSpamTokens } = useWalletSelector(); const queryFn = fetchBrc20FungibleTokens(ordinalsAddress, fiatCurrency, network); - return useQuery({ + const query = useQuery({ queryKey: ['brc20-fungible-tokens', ordinalsAddress, network.type, fiatCurrency], queryFn, enabled: Boolean(network && ordinalsAddress), }); + + return { + ...query, + unfilteredData: query.data, + data: query.data?.filter((ft) => { + let passedSpamCheck = true; + if (spamTokens?.length) { + passedSpamCheck = showSpamTokens || !spamTokens.includes(ft.principal); + } + return passedSpamCheck; + }), + }; }; export const useVisibleBrc20FungibleTokens = (): ReturnType & { diff --git a/src/app/hooks/queries/ordinals/useInscriptionDetails.ts b/src/app/hooks/queries/ordinals/useInscriptionDetails.ts index adbc5566f..029de57d9 100644 --- a/src/app/hooks/queries/ordinals/useInscriptionDetails.ts +++ b/src/app/hooks/queries/ordinals/useInscriptionDetails.ts @@ -1,4 +1,4 @@ -import useOrdinalsApi from '@hooks/useOrdinalsApi'; +import useOrdinalsApi from '@hooks/apiClients/useOrdinalsApi'; import { useQuery } from '@tanstack/react-query'; const useInscriptionDetails = (id: string) => { diff --git a/src/app/hooks/queries/runes/useGetRuneFungibleTokens.ts b/src/app/hooks/queries/runes/useGetRuneFungibleTokens.ts deleted file mode 100644 index 051182d01..000000000 --- a/src/app/hooks/queries/runes/useGetRuneFungibleTokens.ts +++ /dev/null @@ -1,56 +0,0 @@ -import useHasFeature from '@hooks/useHasFeature'; -import useRunesApi from '@hooks/useRunesApi'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { FungibleToken, getXverseApiClient, NetworkType } from '@secretkeylabs/xverse-core'; -import { useQuery } from '@tanstack/react-query'; -import BigNumber from 'bignumber.js'; - -export const fetchRuneBalances = - ( - runesApi, - network: NetworkType, - ordinalsAddress: string, - fiatCurrency: string, - ): (() => Promise) => - async () => { - const runeBalances = await runesApi.getRuneFungibleTokens(ordinalsAddress); - const runeNames = runeBalances.map((runeBalanceFt: FungibleToken) => runeBalanceFt.name); - return getXverseApiClient(network) - .getRuneFiatRates(runeNames, fiatCurrency) - .then((runeFiatRates) => - runeBalances.map((runeBalanceFt: FungibleToken) => ({ - ...runeBalanceFt, - tokenFiatRate: runeFiatRates[runeBalanceFt.name]?.[fiatCurrency], - })), - ) - .catch(() => runeBalances); - }; - -export const useGetRuneFungibleTokens = () => { - const { ordinalsAddress, network, fiatCurrency } = useWalletSelector(); - const showRunes = useHasFeature('RUNES_SUPPORT'); - const runesApi = useRunesApi(); - const queryFn = fetchRuneBalances(runesApi, network.type, ordinalsAddress, fiatCurrency); - return useQuery({ - queryKey: ['get-rune-fungible-tokens', network.type, ordinalsAddress, fiatCurrency], - enabled: Boolean(network && ordinalsAddress && showRunes), - queryFn, - }); -}; - -/* - * This hook is used to get the list of runes which the user has not hidden - */ -export const useVisibleRuneFungibleTokens = (): ReturnType & { - visible: FungibleToken[]; -} => { - const { runesManageTokens } = useWalletSelector(); - const runesQuery = useGetRuneFungibleTokens(); - return { - ...runesQuery, - visible: (runesQuery.data ?? []).filter((ft) => { - const userSetting = runesManageTokens[ft.principal]; - return userSetting === true || (userSetting === undefined && new BigNumber(ft.balance).gt(0)); - }), - }; -}; diff --git a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts new file mode 100644 index 000000000..0423f1941 --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts @@ -0,0 +1,23 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export default function useRuneFloorPriceQuery(runeName: string, backgroundRefetch = true) { + const { network } = useWalletSelector(); + if (network.type !== 'Mainnet') { + throw new Error('Only available on Mainnet'); + } + const runesApi = useRunesApi(); + const queryFn = useCallback(async () => { + const res = await runesApi.getRuneMarketData(runeName); + return Number(res.floorUnitPrice.formatted); + }, [runeName, runesApi]); + return useQuery({ + refetchOnWindowFocus: backgroundRefetch, + refetchOnReconnect: backgroundRefetch, + queryKey: ['get-rune-floor-price', runeName], + enabled: Boolean(runeName), + queryFn, + }); +} diff --git a/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts b/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts new file mode 100644 index 000000000..70a1cb011 --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneFungibleTokensQuery.ts @@ -0,0 +1,78 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useHasFeature from '@hooks/useHasFeature'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { FeatureId, FungibleToken } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import BigNumber from 'bignumber.js'; + +export const fetchRuneBalances = + ( + runesApi: ReturnType, + ordinalsAddress: string, + fiatCurrency: string, + ): (() => Promise) => + async () => { + const runeBalances = await runesApi.getRuneFungibleTokens(ordinalsAddress, true); + + if (!Array.isArray(runeBalances) || runeBalances.length === 0) { + return []; + } + + const runeIds = runeBalances.map((runeBalanceFt: FungibleToken) => runeBalanceFt.principal); + try { + const runeFiatRates = await runesApi.getRuneFiatRatesByRuneIds(runeIds, fiatCurrency); + return runeBalances.map((runeBalanceFt: FungibleToken) => ({ + ...runeBalanceFt, + tokenFiatRate: runeFiatRates[runeBalanceFt.principal]?.[fiatCurrency], + })); + } catch (error) { + return runeBalances; + } + }; + +export const useRuneFungibleTokensQuery = (backgroundRefetch = true) => { + const { ordinalsAddress } = useSelectedAccount(); + const { network, fiatCurrency, spamTokens, showSpamTokens } = useWalletSelector(); + const showRunes = useHasFeature(FeatureId.RUNES_SUPPORT); + const runesApi = useRunesApi(); + const queryFn = fetchRuneBalances(runesApi, ordinalsAddress, fiatCurrency); + const query = useQuery({ + queryKey: ['get-rune-fungible-tokens', network.type, ordinalsAddress, fiatCurrency], + enabled: Boolean(network && ordinalsAddress && showRunes), + refetchOnWindowFocus: backgroundRefetch, + refetchOnReconnect: backgroundRefetch, + queryFn, + }); + + return { + ...query, + unfilteredData: query.data, + data: query.data?.filter((ft) => { + let passedSpamCheck = true; + if (spamTokens?.length) { + passedSpamCheck = showSpamTokens || !spamTokens.includes(ft.principal); + } + return passedSpamCheck; + }), + }; +}; + +/* + * This hook is used to get the list of runes which the user has not hidden + */ +export const useVisibleRuneFungibleTokens = ( + backgroundRefetch = true, +): ReturnType & { + visible: FungibleToken[]; +} => { + const { runesManageTokens } = useWalletSelector(); + const runesQuery = useRuneFungibleTokensQuery(backgroundRefetch); + return { + ...runesQuery, + visible: (runesQuery.data ?? []).filter((ft) => { + const userSetting = runesManageTokens[ft.principal]; + return userSetting === true || (userSetting === undefined && new BigNumber(ft.balance).gt(0)); + }), + }; +}; diff --git a/src/app/hooks/queries/runes/useRuneSellPsbt.ts b/src/app/hooks/queries/runes/useRuneSellPsbt.ts new file mode 100644 index 000000000..1ab6a95e8 --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneSellPsbt.ts @@ -0,0 +1,70 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { BitcoinNetworkType } from '@sats-connect/core'; +import { RuneSellRequest } from '@secretkeylabs/xverse-core'; +import { RuneItem } from '@utils/runes'; +import { useCallback, useState } from 'react'; + +/* + Currently only supports MagicEden. + This hook returns a function where we call ME Sell Runes API to return a psbtBase64 for PSBT construction + */ +const useRuneSellPsbt = (runeName: string, listingUtxos: Record) => { + const { network } = useWalletSelector(); + const { ordinalsAddress, ordinalsPublicKey, btcAddress } = useSelectedAccount(); + + const [loading, setLoading] = useState(false); + const [signPsbtPayload, setSignPsbtPayload] = useState(null); + const [error, setError] = useState(null); + const runesApi = useRunesApi(); + + const getRuneSellPsbt = useCallback(async () => { + const sanitizedRuneName = runeName.replace(/[^A-Za-z]+/g, '').toUpperCase(); + const utxosToList = Object.entries(listingUtxos) + .filter((item) => item[1].selected) + .map(([key, item]) => ({ + location: key, + priceSats: Math.round(item.amount * item.priceSats), + })); + const expiresAt = new Date(); + // setting the expiration date to 10 days from now + expiresAt.setDate(expiresAt.getDate() + 10); + + const args: RuneSellRequest = { + side: 'sell', + rune: sanitizedRuneName, + makerRunesPublicKey: ordinalsPublicKey, + makerRunesAddress: ordinalsAddress, + makerReceiveAddress: btcAddress, + utxos: utxosToList, + expiresAt: expiresAt.toISOString(), + }; + + try { + setLoading(true); + setError(null); + const sellOrder = await runesApi.getRunesSellOrder(args); + const psbtBase64 = sellOrder.orderPsbtBase64; + const payload = { + network: { + type: + network.type === 'Mainnet' ? BitcoinNetworkType.Mainnet : BitcoinNetworkType.Testnet, + }, + broadcast: false, + inputsToSign: undefined, + psbtBase64, + message: 'Sign Transaction', + }; + setSignPsbtPayload(payload); + } catch (err) { + setError('Something went wrong. Please try again with a lower sat price.'); + } finally { + setLoading(false); + } + }, [runeName, listingUtxos, btcAddress, network, ordinalsAddress, ordinalsPublicKey, runesApi]); + + return { getRuneSellPsbt, loading, signPsbtPayload, error }; +}; + +export default useRuneSellPsbt; diff --git a/src/app/hooks/queries/runes/useRuneUtxosQuery.ts b/src/app/hooks/queries/runes/useRuneUtxosQuery.ts new file mode 100644 index 000000000..ba18c7b7c --- /dev/null +++ b/src/app/hooks/queries/runes/useRuneUtxosQuery.ts @@ -0,0 +1,39 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export default function useRuneUtxosQuery( + runeName: string, + filter?: 'listed' | 'unlisted', + backgroundRefetch = true, +) { + const { network } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + if (network.type !== 'Mainnet') { + throw new Error('Only available on Mainnet'); + } + const runesApi = useRunesApi(); + const queryFn = useCallback(async () => { + const res = await runesApi.getRunesUtxos({ address: ordinalsAddress, rune: runeName }); + const sortedRes = res.sort((a, b) => { + const amountA = Number(a.runes?.[0][1].amount); + const amountB = Number(b.runes?.[0][1].amount); + return amountB - amountA; + }); + if (filter === 'unlisted') { + return sortedRes.filter((item) => item.listing[0] === null); + } + if (filter === 'listed') { + return sortedRes.filter((item) => item.listing[0] !== null); + } + }, [runesApi, ordinalsAddress, filter, runeName]); + return useQuery({ + refetchOnWindowFocus: backgroundRefetch, + refetchOnReconnect: backgroundRefetch, + queryKey: ['get-rune-utxos', ordinalsAddress, runeName, filter], + enabled: Boolean(ordinalsAddress && runeName), + queryFn, + }); +} diff --git a/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts b/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts new file mode 100644 index 000000000..8225c17c6 --- /dev/null +++ b/src/app/hooks/queries/runes/useSubmitRuneSellPsbt.ts @@ -0,0 +1,33 @@ +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { SubmitRuneSellRequest } from '@secretkeylabs/xverse-core'; +import { useCallback } from 'react'; + +const useSubmitRuneSellPsbt = () => { + const { ordinalsAddress, ordinalsPublicKey, btcAddress } = useSelectedAccount(); + const runesApi = useRunesApi(); + + const submitRuneSellPsbt = useCallback( + async (signedPsbtBase64: string, runeName: string) => { + const expiresAt = new Date(); + const sanitizedRuneName = runeName.replace(/[^A-Za-z]+/g, '').toUpperCase(); + // setting the expiration date to 10 days from now + expiresAt.setDate(expiresAt.getDate() + 10); + const args: SubmitRuneSellRequest = { + side: 'sell', + symbol: sanitizedRuneName, + signedPsbtBase64, + makerRunesPublicKey: ordinalsPublicKey, + makerRunesAddress: ordinalsAddress, + makerReceiveAddress: btcAddress, + expiresAt: expiresAt.toISOString(), + }; + return runesApi.submitRunesSellOrder(args); + }, + [btcAddress, ordinalsAddress, ordinalsPublicKey, runesApi], + ); + + return { submitRuneSellPsbt }; +}; + +export default useSubmitRuneSellPsbt; diff --git a/src/app/hooks/queries/stx/useGetSip10FungibleTokens.ts b/src/app/hooks/queries/stx/useGetSip10FungibleTokens.ts index 0281be321..f38b423fa 100644 --- a/src/app/hooks/queries/stx/useGetSip10FungibleTokens.ts +++ b/src/app/hooks/queries/stx/useGetSip10FungibleTokens.ts @@ -1,4 +1,5 @@ import useNetworkSelector from '@hooks/useNetwork'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { FungibleToken, @@ -56,7 +57,8 @@ export const fetchSip10FungibleTokens = }; export const useGetSip10FungibleTokens = () => { - const { stxAddress, fiatCurrency, network } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); + const { fiatCurrency, network, spamTokens, showSpamTokens } = useWalletSelector(); const currentNetworkInstance = useNetworkSelector(); const queryFn = fetchSip10FungibleTokens( @@ -66,11 +68,23 @@ export const useGetSip10FungibleTokens = () => { currentNetworkInstance, ); - return useQuery({ + const query = useQuery({ queryKey: ['sip10-fungible-tokens', network.type, stxAddress, fiatCurrency], queryFn, enabled: Boolean(network && stxAddress), }); + + return { + ...query, + unfilteredData: query.data, + data: query.data?.filter((ft) => { + let passedSpamCheck = true; + if (spamTokens?.length) { + passedSpamCheck = showSpamTokens || !spamTokens.includes(ft.principal); + } + return passedSpamCheck; + }), + }; }; export const useVisibleSip10FungibleTokens = (): ReturnType & { @@ -78,11 +92,21 @@ export const useVisibleSip10FungibleTokens = (): ReturnType { const { sip10ManageTokens } = useWalletSelector(); const sip10Query = useGetSip10FungibleTokens(); + // set visible false for unsupported tokens + const sip10FTList = sip10Query.data || []; + sip10FTList.forEach((ft) => { + ft.visible = !!ft.supported; + }); + return { ...sip10Query, - visible: (sip10Query.data ?? []).filter((ft) => { + visible: sip10FTList.filter((ft) => { const userSetting = sip10ManageTokens[ft.principal]; - return userSetting === true || (userSetting === undefined && new BigNumber(ft.balance).gt(0)); + + return ( + userSetting === true || + (userSetting === undefined && ft.supported && new BigNumber(ft.balance).gt(0)) + ); }), }; }; diff --git a/src/app/hooks/queries/useAccountBalance.ts b/src/app/hooks/queries/useAccountBalance.ts index dc0efbd47..3b9f66a3c 100644 --- a/src/app/hooks/queries/useAccountBalance.ts +++ b/src/app/hooks/queries/useAccountBalance.ts @@ -1,7 +1,7 @@ +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useRunesApi from '@hooks/apiClients/useRunesApi'; import useCoinRates from '@hooks/queries/useCoinRates'; -import useBtcClient from '@hooks/useBtcClient'; import useNetworkSelector from '@hooks/useNetwork'; -import useRunesApi from '@hooks/useRunesApi'; import useWalletSelector from '@hooks/useWalletSelector'; import { Account, @@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js'; import { useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { fetchBrc20FungibleTokens } from './ordinals/useGetBrc20FungibleTokens'; -import { fetchRuneBalances } from './runes/useGetRuneFungibleTokens'; +import { fetchRuneBalances } from './runes/useRuneFungibleTokensQuery'; import { fetchSip10FungibleTokens } from './stx/useGetSip10FungibleTokens'; const useAccountBalance = () => { @@ -50,56 +50,56 @@ const useAccountBalance = () => { let finalBrcCoinsList: FungibleToken[] = []; let finalRunesCoinsList: FungibleToken[] = []; - if (account.btcAddress) { - const btcData: BtcAddressData = await btcClient.getBalance(account.btcAddress); - btcBalance = btcData.finalBalance.toString(); + try { + if (account.btcAddress) { + const btcData: BtcAddressData = await btcClient.getBalance(account.btcAddress); + btcBalance = btcData.finalBalance.toString(); + } + + if (account.ordinalsAddress) { + const fetchBrc20Balances = fetchBrc20FungibleTokens( + account.ordinalsAddress, + fiatCurrency, + network, + ); + finalBrcCoinsList = (await fetchBrc20Balances()).filter((ft) => { + const setting = brc20ManageTokens[ft.principal]; + return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); + }); + const runeBalances = fetchRuneBalances(runesApi, account.ordinalsAddress, fiatCurrency); + finalRunesCoinsList = (await runeBalances()).filter((ft) => { + const setting = runesManageTokens[ft.principal]; + return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); + }); + } + if (account.stxAddress) { + const apiUrl = `${getNetworkURL(stacksNetwork)}/extended/v1/address/${ + account.stxAddress + }/balances`; + + const response = await axios.get(apiUrl, { + timeout: API_TIMEOUT_MILLI, + }); + + const availableBalance = new BigNumber(response.data.stx.balance); + const lockedBalance = new BigNumber(response.data.stx.locked); + stxBalance = availableBalance.plus(lockedBalance).toString(); + + const fetchSip10Balances = fetchSip10FungibleTokens( + account.stxAddress, + fiatCurrency, + network, + stacksNetwork, + ); + finalSipCoinsList = (await fetchSip10Balances()).filter((ft) => { + const setting = sip10ManageTokens[ft.principal]; + return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); + }); + } + } catch (error) { + console.error('Failed to fetch balances:', error); } - if (account.ordinalsAddress) { - const fetchBrc20Balances = fetchBrc20FungibleTokens( - account.ordinalsAddress, - fiatCurrency, - network, - ); - finalBrcCoinsList = (await fetchBrc20Balances()).filter((ft) => { - const setting = brc20ManageTokens[ft.principal]; - return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); - }); - const runeBalances = fetchRuneBalances( - runesApi, - network.type, - account.ordinalsAddress, - fiatCurrency, - ); - finalRunesCoinsList = (await runeBalances()).filter((ft) => { - const setting = runesManageTokens[ft.principal]; - return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); - }); - } - if (account.stxAddress) { - const apiUrl = `${getNetworkURL(stacksNetwork)}/extended/v1/address/${ - account.stxAddress - }/balances`; - - const response = await axios.get(apiUrl, { - timeout: API_TIMEOUT_MILLI, - }); - - const availableBalance = new BigNumber(response.data.stx.balance); - const lockedBalance = new BigNumber(response.data.stx.locked); - stxBalance = availableBalance.plus(lockedBalance).toString(); - - const fetchSip10Balances = fetchSip10FungibleTokens( - account.stxAddress, - fiatCurrency, - network, - stacksNetwork, - ); - finalSipCoinsList = (await fetchSip10Balances()).filter((ft) => { - const setting = sip10ManageTokens[ft.principal]; - return setting === true || (setting === undefined && new BigNumber(ft.balance).gt(0)); - }); - } const totalBalance = calculateTotalBalance({ stxBalance, btcBalance, diff --git a/src/app/hooks/queries/useBnsName.ts b/src/app/hooks/queries/useBnsName.ts index 6f02fe79a..0ba665e3d 100644 --- a/src/app/hooks/queries/useBnsName.ts +++ b/src/app/hooks/queries/useBnsName.ts @@ -9,8 +9,12 @@ export const useBnsName = (walletAddress: string) => { useEffect(() => { (async () => { - const name = await getBnsName(walletAddress, network); - setBnsName(name ?? ''); + if (walletAddress) { + const name = await getBnsName(walletAddress, network); + setBnsName(name ?? ''); + } else { + setBnsName(''); + } })(); }, [walletAddress, network]); diff --git a/src/app/hooks/queries/useBtcWalletData.ts b/src/app/hooks/queries/useBtcWalletData.ts index eae77c8fe..49e7cfce0 100644 --- a/src/app/hooks/queries/useBtcWalletData.ts +++ b/src/app/hooks/queries/useBtcWalletData.ts @@ -1,10 +1,10 @@ -import useBtcClient from '@hooks/useBtcClient'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import type { BtcAddressData } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -import useWalletSelector from '../useWalletSelector'; -export const useBtcWalletData = () => { - const { btcAddress } = useWalletSelector(); +const useBtcWalletData = () => { + const { btcAddress } = useSelectedAccount(); const btcClient = useBtcClient(); const fetchBtcWalletData = async () => { diff --git a/src/app/hooks/queries/useConfirmedBtcBalance.ts b/src/app/hooks/queries/useConfirmedBtcBalance.ts index 34c869813..69ef06048 100644 --- a/src/app/hooks/queries/useConfirmedBtcBalance.ts +++ b/src/app/hooks/queries/useConfirmedBtcBalance.ts @@ -1,10 +1,10 @@ -import useBtcClient from '@hooks/useBtcClient'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; -import useWalletSelector from '../useWalletSelector'; const useConfirmBtcBalance = () => { - const { btcAddress } = useWalletSelector(); + const { btcAddress } = useSelectedAccount(); const btcClient = useBtcClient(); const fetchBtcAddressData = async () => btcClient.getAddressData(btcAddress); diff --git a/src/app/hooks/queries/useDelegationState.ts b/src/app/hooks/queries/useDelegationState.ts index 91fa3e559..bdc52a08e 100644 --- a/src/app/hooks/queries/useDelegationState.ts +++ b/src/app/hooks/queries/useDelegationState.ts @@ -1,10 +1,10 @@ import useNetworkSelector from '@hooks/useNetwork'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { fetchDelegationState } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; const useDelegationState = () => { - const { stxAddress } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); const selectedNetwork = useNetworkSelector(); const networkType = selectedNetwork.isMainnet() ? 'Mainnet' : 'Testnet'; diff --git a/src/app/hooks/queries/usePendingOrdinalTx.ts b/src/app/hooks/queries/usePendingOrdinalTx.ts index bc2c5382b..ca6be9aa8 100644 --- a/src/app/hooks/queries/usePendingOrdinalTx.ts +++ b/src/app/hooks/queries/usePendingOrdinalTx.ts @@ -1,10 +1,10 @@ -import useBtcClient from '@hooks/useBtcClient'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import type { BtcAddressMempool } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -import useWalletSelector from '../useWalletSelector'; const usePendingOrdinalTxs = (ordinalUtxoHash: string | undefined) => { - const { ordinalsAddress } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); const btcClient = useBtcClient(); const fetchOrdinalsMempoolTxs = async (): Promise => diff --git a/src/app/hooks/queries/useSpamTokens.ts b/src/app/hooks/queries/useSpamTokens.ts new file mode 100644 index 000000000..efe631122 --- /dev/null +++ b/src/app/hooks/queries/useSpamTokens.ts @@ -0,0 +1,54 @@ +import { getSpamTokensList } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; + +import useWalletSelector from '@hooks/useWalletSelector'; +import { setSpamTokensAction } from '@stores/wallet/actions/actionCreators'; +import { useDispatch } from 'react-redux'; + +const useSpamTokens = () => { + const { network, spamTokens: localSpamTokens } = useWalletSelector(); + + const dispatch = useDispatch(); + + const getSpamTokens = async (): Promise => { + try { + const spamTokens: string[] = await getSpamTokensList(network.type); + const updatedSpamTokensList = new Set([...localSpamTokens, ...spamTokens]); + const uniqueSpamTokens = Array.from(updatedSpamTokensList); + dispatch(setSpamTokensAction(uniqueSpamTokens)); + return uniqueSpamTokens; + } catch (error) { + return []; + } + }; + + const addToSpamTokens = async (token: string) => { + try { + const updatedSpamTokensList = new Set([...localSpamTokens, token]); + const uniqueSpamTokens = Array.from(updatedSpamTokensList); + dispatch(setSpamTokensAction(uniqueSpamTokens)); + } catch (error) { + console.error(error); + } + }; + + const removeFromSpamTokens = async (token: string) => { + try { + const updatedSpamTokensList = new Set(localSpamTokens); + updatedSpamTokensList.delete(token); + const uniqueSpamTokens = Array.from(updatedSpamTokensList); + dispatch(setSpamTokensAction(uniqueSpamTokens)); + } catch (error) { + console.error(error); + } + }; + + useQuery({ + queryKey: ['spamTokens', network.type], + queryFn: getSpamTokens, + }); + + return { addToSpamTokens, removeFromSpamTokens }; +}; + +export default useSpamTokens; diff --git a/src/app/hooks/queries/useStackingData.ts b/src/app/hooks/queries/useStackingData.ts index 33aae114f..e91ad8f9d 100644 --- a/src/app/hooks/queries/useStackingData.ts +++ b/src/app/hooks/queries/useStackingData.ts @@ -1,3 +1,4 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import { getStacksInfo, getXverseApiClient, StackingData } from '@secretkeylabs/xverse-core'; import { useQueries } from '@tanstack/react-query'; import { useCallback } from 'react'; @@ -5,7 +6,8 @@ import useWalletSelector from '../useWalletSelector'; import useDelegationState from './useDelegationState'; const useStackingData = () => { - const { stxAddress, network } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const { data: delegationStateData, isLoading: delegateStateIsLoading } = useDelegationState(); const xverseApiClient = getXverseApiClient(network.type); diff --git a/src/app/hooks/queries/useStacksCollectibles.ts b/src/app/hooks/queries/useStacksCollectibles.ts index 3ad6c3c0a..59074fd47 100644 --- a/src/app/hooks/queries/useStacksCollectibles.ts +++ b/src/app/hooks/queries/useStacksCollectibles.ts @@ -1,11 +1,11 @@ import useNetworkSelector from '@hooks/useNetwork'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { getNftCollections, StacksCollectionList } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; import { handleRetries } from '@utils/query'; const useStacksCollectibles = () => { - const { stxAddress } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); const selectedNetwork = useNetworkSelector(); const fetchNftCollections = (): Promise => diff --git a/src/app/hooks/queries/useStxPendingTxData.ts b/src/app/hooks/queries/useStxPendingTxData.ts index b37b87936..e9c0f7fa7 100644 --- a/src/app/hooks/queries/useStxPendingTxData.ts +++ b/src/app/hooks/queries/useStxPendingTxData.ts @@ -1,11 +1,10 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import { fetchStxPendingTxData, StxPendingTxData } from '@secretkeylabs/xverse-core'; -import { StoreState } from '@stores/index'; import { useQuery } from '@tanstack/react-query'; -import { useSelector } from 'react-redux'; import useNetworkSelector from '../useNetwork'; const useStxPendingTxData = () => { - const { stxAddress } = useSelector((state: StoreState) => state.walletState); + const { stxAddress } = useSelectedAccount(); const selectedNetwork = useNetworkSelector(); const result = useQuery({ queryKey: ['stx-pending-transaction', { stxAddress, selectedNetwork }], diff --git a/src/app/hooks/queries/useStxWalletData.ts b/src/app/hooks/queries/useStxWalletData.ts index e584a3320..c12ec2362 100644 --- a/src/app/hooks/queries/useStxWalletData.ts +++ b/src/app/hooks/queries/useStxWalletData.ts @@ -1,15 +1,16 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import type { StxAddressData } from '@secretkeylabs/xverse-core'; import { fetchStxAddressData } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; import { PAGINATION_LIMIT } from '@utils/constants'; import useNetworkSelector from '../useNetwork'; -import useWalletSelector from '../useWalletSelector'; -export const useStxWalletData = () => { - const { stxAddress } = useWalletSelector(); +const useStxWalletData = () => { + const { stxAddress } = useSelectedAccount(); const currentNetworkInstance = useNetworkSelector(); const fetchStxWalletData = async (): Promise => fetchStxAddressData(stxAddress, currentNetworkInstance, 0, PAGINATION_LIMIT); + return useQuery({ queryKey: ['stx-wallet-data', stxAddress], queryFn: fetchStxWalletData, diff --git a/src/app/hooks/queries/useTransaction.ts b/src/app/hooks/queries/useTransaction.ts index 30396baf1..8b778ffa6 100644 --- a/src/app/hooks/queries/useTransaction.ts +++ b/src/app/hooks/queries/useTransaction.ts @@ -1,10 +1,10 @@ -import useBtcClient from '@hooks/useBtcClient'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import { fetchBtcTransaction } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; export default function useTransaction(id?: string) { - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const btcClient = useBtcClient(); const fetchTransaction = async () => { diff --git a/src/app/hooks/queries/useTransactions.ts b/src/app/hooks/queries/useTransactions.ts index f38e9e4eb..e2fe94031 100644 --- a/src/app/hooks/queries/useTransactions.ts +++ b/src/app/hooks/queries/useTransactions.ts @@ -1,30 +1,50 @@ -import useBtcClient from '@hooks/useBtcClient'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; -import type { Brc20HistoryTransactionData, BtcTransactionData } from '@secretkeylabs/xverse-core'; -import { fetchBtcTransactionsData, getBrc20History } from '@secretkeylabs/xverse-core'; +import { + APIGetRunesActivityForAddressResponse, + Brc20HistoryTransactionData, + BtcTransactionData, + fetchBtcTransactionsData, + getBrc20History, + getXverseApiClient, +} from '@secretkeylabs/xverse-core'; import { AddressTransactionWithTransfers, MempoolTransaction, } from '@stacks/stacks-blockchain-api-types'; import { useQuery } from '@tanstack/react-query'; import { CurrencyTypes, PAGINATION_LIMIT } from '@utils/constants'; +import { handleRetries } from '@utils/query'; import { getStxAddressTransactions } from '@utils/transactions/transactions'; import useNetworkSelector from '../useNetwork'; -export default function useTransactions(coinType: CurrencyTypes, brc20Token: string | null) { - const { network, stxAddress, btcAddress, ordinalsAddress, hasActivatedOrdinalsKey } = - useWalletSelector(); +export default function useTransactions( + coinType: CurrencyTypes, + brc20Token: string | null, + runeToken: string | null, +) { + const { stxAddress, btcAddress, ordinalsAddress } = useSelectedAccount(); + const { network, hasActivatedOrdinalsKey } = useWalletSelector(); const selectedNetwork = useNetworkSelector(); const btcClient = useBtcClient(); - const fetchTransactions = async (): Promise< | BtcTransactionData[] | (AddressTransactionWithTransfers | MempoolTransaction)[] | Brc20HistoryTransactionData[] + | APIGetRunesActivityForAddressResponse > => { if (coinType === 'FT' && brc20Token) { return getBrc20History(network.type, ordinalsAddress, brc20Token); } + if (coinType === 'FT' && runeToken) { + return getXverseApiClient(network.type).getRuneTxHistory( + ordinalsAddress, + runeToken, + 0, + PAGINATION_LIMIT, + ); + } if (coinType === 'STX' || coinType === 'FT' || coinType === 'NFT') { return getStxAddressTransactions(stxAddress, selectedNetwork, 0, PAGINATION_LIMIT); } @@ -40,8 +60,9 @@ export default function useTransactions(coinType: CurrencyTypes, brc20Token: str }; return useQuery({ - queryKey: [`transactions-${coinType}-${brc20Token}`], + queryKey: ['transactions', coinType, brc20Token, runeToken], queryFn: fetchTransactions, - refetchInterval: 10000, + staleTime: 10 * 1000, // 10 secs + retry: handleRetries, }); } diff --git a/src/app/hooks/useBtcFees.ts b/src/app/hooks/useBtcFees.ts index 976b26308..e87bc3003 100644 --- a/src/app/hooks/useBtcFees.ts +++ b/src/app/hooks/useBtcFees.ts @@ -6,8 +6,9 @@ import { UTXO, } from '@secretkeylabs/xverse-core'; import { useEffect, useMemo, useState } from 'react'; -import useBtcClient from './useBtcClient'; +import useBtcClient from './apiClients/useBtcClient'; import useOrdinalsByAddress from './useOrdinalsByAddress'; +import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; interface Params { @@ -40,7 +41,8 @@ const useBtcFees = ({ }); const [highFeeError, setHighFeeError] = useState(''); const [standardFeeError, setStandardFeeError] = useState(''); - const { network, btcAddress, ordinalsAddress } = useWalletSelector(); + const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { network } = useWalletSelector(); const btcClient = useBtcClient(); const { ordinals } = useOrdinalsByAddress(btcAddress); const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); diff --git a/src/app/hooks/useCacheMigration.ts b/src/app/hooks/useCacheMigration.ts deleted file mode 100644 index 13026f9e7..000000000 --- a/src/app/hooks/useCacheMigration.ts +++ /dev/null @@ -1,16 +0,0 @@ -import useWalletReducer from './useWalletReducer'; - -const useCacheMigration = () => { - const { unlockWallet } = useWalletReducer(); - - const migrateCachedStorage = async (password: string) => { - await unlockWallet(password); - chrome.storage.local.clear(); - localStorage.setItem('migrated', 'true'); - }; - return { - migrateCachedStorage, - }; -}; - -export default useCacheMigration; diff --git a/src/app/hooks/useChromeLocalStorage.ts b/src/app/hooks/useChromeLocalStorage.ts index b507a0cba..50aa84d1e 100644 --- a/src/app/hooks/useChromeLocalStorage.ts +++ b/src/app/hooks/useChromeLocalStorage.ts @@ -1,19 +1,19 @@ -import { chromeLocalStorage } from '@utils/chromeStorage'; +import chromeStorage from '@utils/chromeStorage'; import { useEffect, useState } from 'react'; -export const useChromeLocalStorage = (key: string, defaultValue?: T) => { +const useChromeLocalStorage = (key: string, defaultValue?: T) => { const [value, setValueState] = useState(undefined); useEffect(() => { setValueState(undefined); - chromeLocalStorage.getItem(key).then((result) => { + chromeStorage.local.getItem(key).then((result) => { const newValue = result === undefined ? defaultValue : result; setValueState(newValue); }); }, [key]); const setValue = (newValue: T) => { - chromeLocalStorage.setItem(key, newValue); + chromeStorage.local.setItem(key, newValue); setValueState(newValue); }; diff --git a/src/app/hooks/useDetectOrdinalInSignPsbt.ts b/src/app/hooks/useDetectOrdinalInSignPsbt.ts deleted file mode 100644 index d4b552e69..000000000 --- a/src/app/hooks/useDetectOrdinalInSignPsbt.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - Bundle, - getUtxoOrdinalBundleIfFound, - mapRareSatsAPIResponseToBundle, - ParsedPSBT, -} from '@secretkeylabs/xverse-core'; -import useWalletSelector from './useWalletSelector'; - -export type InputsBundle = (Pick & { - inputIndex: number; -})[]; - -const useDetectOrdinalInSignPsbt = () => { - const { ordinalsAddress, network } = useWalletSelector(); - - const handleOrdinalAndOrdinalInfo = async ( - parsedPsbt?: ParsedPSBT, - ): Promise<{ bundleItemsData: InputsBundle; userReceivesOrdinal: boolean }> => { - const bundleItemsData: InputsBundle = []; - let userReceivesOrdinal = false; - - if (parsedPsbt) { - const inputsRequest = parsedPsbt.inputs.map((input) => - getUtxoOrdinalBundleIfFound(network.type, input.txid, input.index), - ); - const inputsResponse = await Promise.all(inputsRequest); - inputsResponse.forEach((inputResponse, index) => { - if (!inputResponse) { - return; - } - const bundle = mapRareSatsAPIResponseToBundle(inputResponse); - if ( - bundle.inscriptions.length > 0 || - bundle.satributes.some((satributes) => !satributes.includes('COMMON')) - ) { - bundleItemsData.push({ - satRanges: bundle.satRanges, - totalExoticSats: bundle.totalExoticSats, - inputIndex: index, - }); - } - }); - - parsedPsbt.outputs.forEach((output) => { - if (output.address === ordinalsAddress) { - userReceivesOrdinal = true; - } - }); - } - - return { - bundleItemsData, - userReceivesOrdinal, - }; - }; - - return handleOrdinalAndOrdinalInfo; -}; - -export default useDetectOrdinalInSignPsbt; diff --git a/src/app/hooks/useFeaturedDapps.ts b/src/app/hooks/useFeaturedDapps.ts index ca2dda010..de9763173 100644 --- a/src/app/hooks/useFeaturedDapps.ts +++ b/src/app/hooks/useFeaturedDapps.ts @@ -7,7 +7,7 @@ import useWalletSession from './useWalletSession'; function useFeaturedDapps() { const { network } = useWalletSelector(); const { getSessionStartTime } = useWalletSession(); - const [sessionStartTime, setSessionStartTime] = useState(null); + const [sessionStartTime, setSessionStartTime] = useState(undefined); const fetchSessionStartTime = async () => { const time = await getSessionStartTime(); diff --git a/src/app/hooks/useHasFeature.ts b/src/app/hooks/useHasFeature.ts index c8007dec7..9435a0198 100644 --- a/src/app/hooks/useHasFeature.ts +++ b/src/app/hooks/useHasFeature.ts @@ -1,9 +1,11 @@ import { FeatureId, getXverseApiClient } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; +import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; const useAppFeatures = () => { - const { network, masterPubKey } = useWalletSelector(); + const { masterPubKey } = useSelectedAccount(); + const { network } = useWalletSelector(); return useQuery({ queryKey: ['appFeatures', network.type, masterPubKey], diff --git a/src/app/hooks/useNonOrdinalUtxo.ts b/src/app/hooks/useNonOrdinalUtxo.ts deleted file mode 100644 index edd4a64b5..000000000 --- a/src/app/hooks/useNonOrdinalUtxo.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getNonOrdinalUtxo, UTXO } from '@secretkeylabs/xverse-core'; -import { useQuery } from '@tanstack/react-query'; -import { REFETCH_UNSPENT_UTXO_TIME } from '@utils/constants'; -import { getTimeForNonOrdinalTransferTransaction } from '@utils/localStorage'; -import useBtcClient from './useBtcClient'; -import useWalletSelector from './useWalletSelector'; - -const useNonOrdinalUtxos = () => { - const { network, ordinalsAddress } = useWalletSelector(); - const btcClient = useBtcClient(); - - const fetchNonOrdinalUtxo = async () => { - const lastTransactionTime = await getTimeForNonOrdinalTransferTransaction(ordinalsAddress); - if (!lastTransactionTime) { - return getNonOrdinalUtxo(ordinalsAddress, btcClient, network.type); - } - const diff = new Date().getTime() - Number(lastTransactionTime); - if (diff > REFETCH_UNSPENT_UTXO_TIME) { - return getNonOrdinalUtxo(ordinalsAddress, btcClient, network.type); - } - return [] as UTXO[]; - }; - - const { data: unspentUtxos, isLoading } = useQuery({ - keepPreviousData: false, - queryKey: [`getNonOrdinalsUtxo-${ordinalsAddress}`], - queryFn: fetchNonOrdinalUtxo, - }); - return { - unspentUtxos, - isLoading, - }; -}; - -export default useNonOrdinalUtxos; diff --git a/src/app/hooks/useNotificationBanners.ts b/src/app/hooks/useNotificationBanners.ts index 7b2bfb852..a689408d7 100644 --- a/src/app/hooks/useNotificationBanners.ts +++ b/src/app/hooks/useNotificationBanners.ts @@ -7,7 +7,7 @@ import useWalletSession from './useWalletSession'; function useNotificationBanners() { const { network } = useWalletSelector(); const { getSessionStartTime } = useWalletSession(); - const [sessionStartTime, setSessionStartTime] = useState(null); + const [sessionStartTime, setSessionStartTime] = useState(undefined); const fetchSessionStartTime = async () => { const time = await getSessionStartTime(); diff --git a/src/app/hooks/useOrdinalsByAddress.ts b/src/app/hooks/useOrdinalsByAddress.ts index 79b346e49..1bd997ef8 100644 --- a/src/app/hooks/useOrdinalsByAddress.ts +++ b/src/app/hooks/useOrdinalsByAddress.ts @@ -1,6 +1,6 @@ import { BtcOrdinal, getOrdinalsByAddress } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -import useBtcClient from './useBtcClient'; +import useBtcClient from './apiClients/useBtcClient'; import useWalletSelector from './useWalletSelector'; const useOrdinalsByAddress = (address: string) => { diff --git a/src/app/hooks/useRbfTransactionData.ts b/src/app/hooks/useRbfTransactionData.ts index 92711d0f1..6693806be 100644 --- a/src/app/hooks/useRbfTransactionData.ts +++ b/src/app/hooks/useRbfTransactionData.ts @@ -17,8 +17,9 @@ import { useCallback, useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import useBtcClient from './useBtcClient'; +import useBtcClient from './apiClients/useBtcClient'; import useNetworkSelector from './useNetwork'; +import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; // TODO: move the types and helper functions below to xverse-core @@ -115,7 +116,8 @@ const sortFees = (fees: RbfRecommendedFees) => const useRbfTransactionData = (transaction?: BtcTransactionData | StxTransactionData): RbfData => { const [isLoading, setIsLoading] = useState(true); const [rbfData, setRbfData] = useState({}); - const { accountType, network, selectedAccount, feeMultipliers } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { selectedAccountType, network, feeMultipliers } = useWalletSelector(); const { data: stxData } = useStxWalletData(); const btcClient = useBtcClient(); const selectedNetwork = useNetworkSelector(); @@ -219,7 +221,7 @@ const useRbfTransactionData = (transaction?: BtcTransactionData | StxTransaction const rbfTx = new rbf.RbfTransaction(transaction, { ...selectedAccount, - accountType: accountType || 'software', + accountType: selectedAccountType || 'software', accountId: isLedgerAccount(selectedAccount) && selectedAccount.deviceAccountIndex ? selectedAccount.deviceAccountIndex @@ -249,7 +251,7 @@ const useRbfTransactionData = (transaction?: BtcTransactionData | StxTransaction }, [ selectedAccount, transaction, - accountType, + selectedAccountType, network.type, btcClient, fetchStxData, diff --git a/src/app/hooks/useResetUserFlow.ts b/src/app/hooks/useResetUserFlow.ts index 24429d686..29da4ce7d 100644 --- a/src/app/hooks/useResetUserFlow.ts +++ b/src/app/hooks/useResetUserFlow.ts @@ -11,7 +11,7 @@ const resetUserFlowChannel = 'resetUserFlow'; const userFlowConfig: Record = { '/send-btc': { resetTo: '/send-btc' }, '/confirm-btc-tx': { resetTo: '/send-btc' }, - '/send-brc20': { resetTo: '/' }, + '/send-brc20-one-step': { resetTo: '/' }, '/confirm-brc20-tx': { resetTo: '/' }, '/confirm-inscription-request': { resetTo: '/' }, '/ordinals-collection': { resetTo: '/nft-dashboard?tab=inscriptions' }, @@ -28,6 +28,7 @@ const userFlowConfig: Record = { '/verify-ledger': { resetTo: '/verify-ledger?mismatch=true' }, '/add-stx-address-ledger': { resetTo: '/add-stx-address-ledger?mismatch=true' }, '/send-rune': { resetTo: '/' }, + '/coinDashboard': { resetTo: '/' }, }; type UserFlowConfigKey = keyof typeof userFlowConfig; @@ -35,7 +36,7 @@ type UserFlowConfigKey = keyof typeof userFlowConfig; * Usage: * * To subscribe: - * useResetUserFlow('/send-brc20'); + * useResetUserFlow('/send-brc20-one-step'); * * To broadcast: * broadcastResetUserFlow(); diff --git a/src/app/hooks/useSeedVault.ts b/src/app/hooks/useSeedVault.ts index 299f1c5ba..15d4f0545 100644 --- a/src/app/hooks/useSeedVault.ts +++ b/src/app/hooks/useSeedVault.ts @@ -1,10 +1,10 @@ import { CryptoUtilsAdapter, - generateRandomKey, - SeedVault, + SeedVaultInstance as SeedVault, StorageAdapter, + generateRandomKey, } from '@secretkeylabs/xverse-core'; -import { chromeLocalStorage, chromeSessionStorage } from '@utils/chromeStorage'; +import chromeStorage from '@utils/chromeStorage'; import { decryptSeedPhraseHandler, encryptSeedPhraseHandler, @@ -20,21 +20,21 @@ const cryptoUtilsAdapter: CryptoUtilsAdapter = { }; const secureStorageAdapter: StorageAdapter = { - get: async (key: string) => chromeSessionStorage.getItem(key, null), - set: async (key: string, value: string) => chromeSessionStorage.setItem(key, value), - remove: async (key: string) => chromeSessionStorage.removeItem(key), + get: async (key: string) => chromeStorage.session.getItem(key, null), + set: async (key: string, value: string) => chromeStorage.session.setItem(key, value), + remove: async (key: string) => chromeStorage.session.removeItem(key), }; const commonStorageAdapter: StorageAdapter = { - get: async (key: string) => chromeLocalStorage.getItem(key, null), - set: async (key: string, value: string) => chromeLocalStorage.setItem(key, value), - remove: async (key: string) => chromeLocalStorage.removeItem(key), + get: async (key: string) => chromeStorage.local.getItem(key, null), + set: async (key: string, value: string) => chromeStorage.local.setItem(key, value), + remove: async (key: string) => chromeStorage.local.removeItem(key), }; const useSeedVault = () => { const vault = useMemo( () => - new SeedVault({ + SeedVault({ cryptoUtilsAdapter, secureStorageAdapter, commonStorageAdapter, diff --git a/src/app/hooks/useSeedVaultMigration.ts b/src/app/hooks/useSeedVaultMigration.ts new file mode 100644 index 000000000..47beadf33 --- /dev/null +++ b/src/app/hooks/useSeedVaultMigration.ts @@ -0,0 +1,73 @@ +import { SeedVaultStorageKeys } from '@secretkeylabs/xverse-core'; +import ChromeStorage from '@utils/chromeStorage'; +import { useDispatch } from 'react-redux'; +import useSeedVault from './useSeedVault'; +import useSelectedAccount from './useSelectedAccount'; +import useWalletReducer from './useWalletReducer'; +import useWalletSelector from './useWalletSelector'; + +const useSeedVaultMigration = () => { + const SeedVault = useSeedVault(); + const { accountsList } = useWalletSelector(); + const { switchAccount } = useWalletReducer(); + const selectedAccount = useSelectedAccount(); + const dispatch = useDispatch(); + + const isVaultUpdated = async () => { + const currentVaultVersion = await ChromeStorage.local.getItem( + SeedVaultStorageKeys.SEED_VAULT_VERSION, + ); + return currentVaultVersion === SeedVault.VERSION.toString(); + }; + + const createMigrationBackup = (backup: { + [SeedVaultStorageKeys.ENCRYPTED_KEY]: string; + [SeedVaultStorageKeys.PASSWORD_SALT]: string; + }) => { + const hasBackup = localStorage.getItem('SEED_VAULT_MIGRATION_BACKUP'); + if (!hasBackup) { + localStorage.setItem('SEED_VAULT_MIGRATION_BACKUP', JSON.stringify(backup)); + } + }; + + const updateSeedVault = async (encryptedKey: string, passwordSalt: string) => { + await chrome.storage.local.clear(); + await SeedVault.restoreVault(encryptedKey, passwordSalt); + localStorage.removeItem('SEED_VAULT_MIGRATION_BACKUP'); + /** + * The migration clears the redux store cache, so if the user quits the extension without going to the home screen the cache would be empty for the next session, + * this is a workaround to trigger flushing the redux-store state to cache, + */ + switchAccount(selectedAccount!); + }; + + const migrateCachedStorage = async () => { + const backup = localStorage.getItem('SEED_VAULT_MIGRATION_BACKUP'); + if (!backup) { + const passwordSalt = await ChromeStorage.local.getItem( + SeedVaultStorageKeys.PASSWORD_SALT, + ); + const encryptedKey = await ChromeStorage.local.getItem( + SeedVaultStorageKeys.ENCRYPTED_KEY, + ); + if (!passwordSalt || !encryptedKey) return; + createMigrationBackup({ + [SeedVaultStorageKeys.ENCRYPTED_KEY]: encryptedKey, + [SeedVaultStorageKeys.PASSWORD_SALT]: passwordSalt, + }); + await updateSeedVault(encryptedKey, passwordSalt); + return; + } + const { + [SeedVaultStorageKeys.ENCRYPTED_KEY]: encryptedKey, + [SeedVaultStorageKeys.PASSWORD_SALT]: passwordSalt, + } = JSON.parse(backup); + await updateSeedVault(encryptedKey, passwordSalt); + }; + return { + isVaultUpdated, + migrateCachedStorage, + }; +}; + +export default useSeedVaultMigration; diff --git a/src/app/hooks/useSelectedAccount.ts b/src/app/hooks/useSelectedAccount.ts new file mode 100644 index 000000000..de931e311 --- /dev/null +++ b/src/app/hooks/useSelectedAccount.ts @@ -0,0 +1,52 @@ +import getSelectedAccount from '@common/utils/getSelectedAccount'; +import { Account } from '@secretkeylabs/xverse-core'; +import { StoreState } from '@stores/index'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import useWalletReducer from './useWalletReducer'; + +const useSelectedAccount = (): Account => { + const { switchAccount } = useWalletReducer(); + + const selectedAccountIndex = useSelector( + (state: StoreState) => state.walletState.selectedAccountIndex, + ); + const selectedAccountType = useSelector( + (state: StoreState) => state.walletState.selectedAccountType, + ); + const softwareAccountsList = useSelector((state: StoreState) => state.walletState.accountsList); + const ledgerAccountsList = useSelector( + (state: StoreState) => state.walletState.ledgerAccountsList, + ); + + const selectedAccount = useMemo(() => { + const existingAccount = getSelectedAccount({ + selectedAccountIndex, + selectedAccountType, + softwareAccountsList, + ledgerAccountsList, + }); + + if (existingAccount) { + return existingAccount; + } + + const fallbackAccount = softwareAccountsList[0]; + + if (fallbackAccount) { + switchAccount(fallbackAccount); + } + + return fallbackAccount; + }, [ + selectedAccountIndex, + selectedAccountType, + softwareAccountsList, + ledgerAccountsList, + switchAccount, + ]); + + return selectedAccount; +}; + +export default useSelectedAccount; diff --git a/src/app/hooks/useSignBatchPsbtTx.ts b/src/app/hooks/useSignBatchPsbtTx.ts index 98248742b..11c448b3a 100644 --- a/src/app/hooks/useSignBatchPsbtTx.ts +++ b/src/app/hooks/useSignBatchPsbtTx.ts @@ -1,10 +1,10 @@ import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types'; import useWalletSelector from '@hooks/useWalletSelector'; +import { SignMultiplePsbtPayload, SignMultipleTransactionOptions } from '@sats-connect/core'; import { InputToSign, signPsbt } from '@secretkeylabs/xverse-core'; import { decodeToken } from 'jsontokens'; import { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { SignMultiplePsbtPayload, SignMultipleTransactionOptions } from 'sats-connect'; import useSeedVault from './useSeedVault'; const useSignBatchPsbtTx = () => { diff --git a/src/app/hooks/useSignatureRequest.ts b/src/app/hooks/useSignatureRequest.ts index e4faeeb44..53dfa90e0 100644 --- a/src/app/hooks/useSignatureRequest.ts +++ b/src/app/hooks/useSignatureRequest.ts @@ -13,9 +13,10 @@ import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import useNetworkSelector from './useNetwork'; import useSeedVault from './useSeedVault'; +import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; -export type SignatureMessageType = 'utf8' | 'structured'; +type SignatureMessageType = 'utf8' | 'structured'; export function isStructuredMessage( messageType: SignatureMessageType, @@ -31,7 +32,7 @@ function useSignatureRequest() { const params = useMemo(() => new URLSearchParams(search), [search]); const tabId = params.get('tabId') ?? '0'; const requestId = params.get('messageId') ?? ''; - const { stxPublicKey, stxAddress } = useWalletSelector(); + const { stxPublicKey, stxAddress } = useSelectedAccount(); const selectedNetwork = useNetworkSelector(); const { payload, domain, messageType, requestToken } = useMemo(() => { @@ -79,7 +80,8 @@ function useSignatureRequest() { } export function useSignMessage(messageType: SignatureMessageType) { - const { selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network } = useWalletSelector(); const { getSeed } = useSeedVault(); return useCallback( async ({ message, domain }: { message: string; domain?: TupleCV }) => { diff --git a/src/app/hooks/useStxAccountRequest.ts b/src/app/hooks/useStxAccountRequest.ts index 805bbe3af..e21898bb1 100644 --- a/src/app/hooks/useStxAccountRequest.ts +++ b/src/app/hooks/useStxAccountRequest.ts @@ -1,16 +1,16 @@ import { MESSAGE_SOURCE } from '@common/types/message-types'; -import { - sendGetAccountsSuccessResponseMessage, - sendUserRejectionMessage, -} from '@common/utils/rpc/stx/rpcResponseMessages'; + +import { sendUserRejectionMessage } from '@common/utils/rpc/responseMessages/errors'; +import { sendGetAccountsSuccessResponseMessage } from '@common/utils/rpc/responseMessages/stacks'; import useWalletSelector from '@hooks/useWalletSelector'; +import { GetAddressOptions } from '@sats-connect/core'; import { bip32, bip39, bs58 } from '@secretkeylabs/xverse-core'; import { GAIA_HUB_URL } from '@secretkeylabs/xverse-core/constant'; import { decodeToken } from 'jsontokens'; import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { GetAddressOptions } from 'sats-connect'; import useSeedVault from './useSeedVault'; +import useSelectedAccount from './useSelectedAccount'; const useStxAccountRequest = () => { // Params @@ -18,7 +18,8 @@ const useStxAccountRequest = () => { const params = new URLSearchParams(search); // Utils - const { stxAddress, stxPublicKey, network } = useWalletSelector(); + const { stxAddress, stxPublicKey } = useSelectedAccount(); + const { network } = useWalletSelector(); const { getSeed } = useSeedVault(); // Related to WebBTC RPC request diff --git a/src/app/hooks/useStxAddressRequest.ts b/src/app/hooks/useStxAddressRequest.ts index 1af610442..9ccf0cee1 100644 --- a/src/app/hooks/useStxAddressRequest.ts +++ b/src/app/hooks/useStxAddressRequest.ts @@ -1,10 +1,11 @@ import { MESSAGE_SOURCE } from '@common/types/message-types'; -import { sendGetAddressesSuccessResponseMessage } from '@common/utils/rpc/stx/rpcResponseMessages'; +import { sendGetAddressesSuccessResponseMessage } from '@common/utils/rpc/responseMessages/stacks'; import useWalletSelector from '@hooks/useWalletSelector'; +import { AddressPurpose, AddressType, GetAddressOptions } from '@sats-connect/core'; import { decodeToken } from 'jsontokens'; import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { GetAddressOptions } from 'sats-connect'; +import useSelectedAccount from './useSelectedAccount'; const useStxAddressRequest = () => { // Params @@ -12,7 +13,8 @@ const useStxAddressRequest = () => { const params = new URLSearchParams(search); // Utils - const { stxAddress, stxPublicKey, network } = useWalletSelector(); + const { stxAddress, stxPublicKey } = useSelectedAccount(); + const { network } = useWalletSelector(); // Related to WebBTC RPC request const messageId = params.get('messageId') ?? ''; @@ -32,6 +34,8 @@ const useStxAddressRequest = () => { { address: stxAddress, publicKey: stxPublicKey, + addressType: AddressType.stacks, + purpose: AddressPurpose.Stacks, }, ]; diff --git a/src/app/hooks/useTrackMixPanelPageViewed.ts b/src/app/hooks/useTrackMixPanelPageViewed.ts index 375c76307..2583c0a8b 100644 --- a/src/app/hooks/useTrackMixPanelPageViewed.ts +++ b/src/app/hooks/useTrackMixPanelPageViewed.ts @@ -2,10 +2,10 @@ import { isLedgerAccount } from '@utils/helper'; import { getMixpanelInstance } from 'app/mixpanelSetup'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import useWalletSelector from './useWalletSelector'; +import useSelectedAccount from './useSelectedAccount'; const useTrackMixPanelPageViewed = (properties?: any, deps: any[] = []) => { - const { selectedAccount } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const location = useLocation(); useEffect(() => { diff --git a/src/app/hooks/useTransactionContext.ts b/src/app/hooks/useTransactionContext.ts index 58fd225c1..ded91882f 100644 --- a/src/app/hooks/useTransactionContext.ts +++ b/src/app/hooks/useTransactionContext.ts @@ -1,11 +1,13 @@ import { btcTransaction, UtxoCache } from '@secretkeylabs/xverse-core'; import { useMemo } from 'react'; -import useBtcClient from './useBtcClient'; +import useBtcClient from './apiClients/useBtcClient'; import useSeedVault from './useSeedVault'; +import useSelectedAccount from './useSelectedAccount'; import useWalletSelector from './useWalletSelector'; const useTransactionContext = () => { - const { selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network } = useWalletSelector(); const seedVault = useSeedVault(); const btcClient = useBtcClient(); diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 0d655a994..d7aa92f01 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -1,10 +1,9 @@ -import { filterLedgerAccounts, getDeviceAccountIndex } from '@common/utils/ledger'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useStxWalletData from '@hooks/queries/useStxWalletData'; +import { getDeviceAccountIndex } from '@common/utils/ledger'; import useNetworkSelector from '@hooks/useNetwork'; import { Account, AnalyticsEvents, + NetworkType, SettingsNetwork, StacksMainnet, StacksNetwork, @@ -12,57 +11,163 @@ import { createWalletAccount, decryptSeedPhraseCBC, getBnsName, - newWallet, restoreWalletWithAccounts, walletFromSeedPhrase, } from '@secretkeylabs/xverse-core'; +import { StoreState } from '@stores/index'; import { ChangeNetworkAction, - addAccountAction, - fetchAccountAction, - getActiveAccountsAction, - renameAccountAction, + changeShowDataCollectionAlertAction, resetWalletAction, selectAccount, - setWalletAction, setWalletHideStxAction, setWalletUnlockedAction, storeEncryptedSeedAction, updateLedgerAccountsAction, + updateSavedNamesAction, + updateSoftwareAccountsAction, } from '@stores/wallet/actions/actionCreators'; import { useQueryClient } from '@tanstack/react-query'; import { generatePasswordHash } from '@utils/encryptionUtils'; -import { isHardwareAccount, isLedgerAccount } from '@utils/helper'; -import { resetMixPanel, trackMixPanel } from '@utils/mixpanel'; -import { useDispatch } from 'react-redux'; +import { + hasOptedInMixPanelTracking, + optInMixPanel, + resetMixPanel, + trackMixPanel, +} from '@utils/mixpanel'; +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import useSeedVault from './useSeedVault'; -import useWalletSelector from './useWalletSelector'; import useWalletSession from './useWalletSession'; -const useWalletReducer = () => { +// TODO: move this to core as the primary way to create an account +const createSingleAccount = async ( + seedPhrase: string, + accountIndex: number, + btcNetwork: NetworkType, + stacksNetwork: StacksNetwork, + savedNames: { id: number; name?: string }[] = [], +) => { const { - accountsList, - selectedAccount, - network, - ledgerAccountsList, - encryptedSeed, + stxAddress, + btcAddress, + ordinalsAddress, masterPubKey, - } = useWalletSelector(); + stxPublicKey, + btcPublicKey, + ordinalsPublicKey, + } = await walletFromSeedPhrase({ + mnemonic: seedPhrase, + index: BigInt(accountIndex), + network: btcNetwork, + }); + const bnsName = await getBnsName(stxAddress, stacksNetwork); + const customName = savedNames.find((name) => name.id === accountIndex)?.name; + const account: Account = { + id: accountIndex, + stxAddress, + btcAddress, + ordinalsAddress, + masterPubKey, + stxPublicKey, + btcPublicKey, + ordinalsPublicKey, + bnsName, + accountName: customName, + accountType: 'software', + }; + + return account; +}; + +const useWalletReducer = () => { + const network = useSelector((state: StoreState) => state.walletState.network); + const encryptedSeed = useSelector((state: StoreState) => state.walletState.encryptedSeed); + + const selectedAccountIndex = useSelector( + (state: StoreState) => state.walletState.selectedAccountIndex, + ); + const selectedAccountType = useSelector( + (state: StoreState) => state.walletState.selectedAccountType, + ); + const savedNames = useSelector((state: StoreState) => state.walletState.savedNames); + + const softwareAccountsList = useSelector((state: StoreState) => state.walletState.accountsList); + const ledgerAccountsList = useSelector( + (state: StoreState) => state.walletState.ledgerAccountsList, + ); + + const showDataCollectionAlert = useSelector( + (state: StoreState) => state.walletState.showDataCollectionAlert, + ); + + const hideStx = useSelector((state: StoreState) => state.walletState.hideStx); + const seedVault = useSeedVault(); const selectedNetwork = useNetworkSelector(); + const dispatch = useDispatch(); - const { refetch: refetchStxData } = useStxWalletData(); - const { refetch: refetchBtcData } = useBtcWalletData(); const { setSessionStartTime, clearSessionTime, setSessionStartTimeAndMigrate } = useWalletSession(); const queryClient = useQueryClient(); - const { hideStx } = useWalletSelector(); + + const ensureSelectedAccountValid = useCallback( + async ( + selectedType = selectedAccountType, + selectedIndex = selectedAccountIndex, + ): Promise => { + if (selectedType === 'ledger') { + // these accounts are created by Ledger, so we cannot regenerate them + return; + } + + const seedPhrase = await seedVault.getSeed(); + const recreatedAccount = await createSingleAccount( + seedPhrase, + selectedIndex, + network.type, + selectedNetwork, + savedNames[network.type], + ); + + const selectedAccount = softwareAccountsList.find((account) => account.id === selectedIndex); + + if (!selectedAccount) { + // if the selected account index does not exist, we cannot update it + // in this case, the account selection will be reset to the first account elsewhere + return; + } + + const accountsMatch = Object.keys(recreatedAccount).every( + (key) => selectedAccount[key] === recreatedAccount[key], + ); + + if (!accountsMatch) { + const newAccountsList = softwareAccountsList.map((account) => + account.id === selectedIndex ? recreatedAccount : account, + ); + + dispatch(updateSoftwareAccountsAction(newAccountsList)); + } + }, + [ + dispatch, + network.type, + seedVault, + selectedAccountIndex, + selectedAccountType, + selectedNetwork, + softwareAccountsList, + savedNames, + ], + ); const loadActiveAccounts = async ( secretKey: string, currentNetwork: SettingsNetwork, currentNetworkObject: StacksNetwork, currentAccounts: Account[], + resetIndex?: boolean, ) => { const walletAccounts = await restoreWalletWithAccounts( secretKey, @@ -71,53 +176,14 @@ const useWalletReducer = () => { currentAccounts, ); - // we sanitise the account to remove any unknown properties which we had in pervious versions of the app - walletAccounts[0] = { - id: walletAccounts[0].id, - btcAddress: walletAccounts[0].btcAddress, - btcPublicKey: walletAccounts[0].btcPublicKey, - masterPubKey: walletAccounts[0].masterPubKey, - ordinalsAddress: walletAccounts[0].ordinalsAddress, - ordinalsPublicKey: walletAccounts[0].ordinalsPublicKey, - stxAddress: walletAccounts[0].stxAddress, - stxPublicKey: walletAccounts[0].stxPublicKey, - bnsName: walletAccounts[0].bnsName, - accountName: walletAccounts[0].accountName, - }; - - let selectedAccountData: Account | undefined; - if (!selectedAccount) { - [selectedAccountData] = walletAccounts; - } else if (isLedgerAccount(selectedAccount)) { - const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, currentNetwork.type); - const selectedAccountDataInNetwork = networkLedgerAccounts.find( - (a) => a.id === selectedAccount.id, - ); - - // we try find the specific matching ledger account - // If we can't find it, we default to the first ledger account in the selected network - // If we can't find that, we default to the first software account in the wallet - selectedAccountData = - selectedAccountDataInNetwork ?? networkLedgerAccounts[0] ?? walletAccounts[0]; - } else { - selectedAccountData = walletAccounts.find((a) => a.id === selectedAccount.id); - } - - if (!selectedAccountData) { - // this should not happen but is a good fallback to have, just in case - [selectedAccountData] = walletAccounts; - } - - if (!isHardwareAccount(selectedAccountData)) { - dispatch( - setWalletAction({ - ...selectedAccountData, - seedPhrase: secretKey, - }), - ); - } - - dispatch(fetchAccountAction(selectedAccountData, walletAccounts)); + // Load custom account names for the new network + const savedCustomAccountNames = savedNames[currentNetwork.type]; + walletAccounts.forEach((account) => { + const savedAccount = savedCustomAccountNames?.find((acc) => acc.id === account.id); + if (savedAccount) { + account.accountName = savedAccount.name; + } + }); // ledger accounts initially didn't have a deviceAccountIndex // this is a migration to add the deviceAccountIndex to the ledger accounts without them @@ -131,50 +197,83 @@ const useWalletReducer = () => { account.masterPubKey, ), })); - dispatch(updateLedgerAccountsAction(newLedgerAccountsList)); } - dispatch(getActiveAccountsAction(walletAccounts)); + dispatch(updateSoftwareAccountsAction(walletAccounts)); + + if (resetIndex) { + dispatch(selectAccount(walletAccounts[0])); + } + + return walletAccounts; }; const migrateLegacySeedStorage = async (password: string) => { const pHash = await generatePasswordHash(password); await decryptSeedPhraseCBC(encryptedSeed, pHash.hash).then(async (decrypted) => { - await seedVault.init(password); - await seedVault.storeSeed(decrypted); + await seedVault.storeSeed(decrypted, password); localStorage.removeItem('salt'); dispatch(storeEncryptedSeedAction('')); }); }; + const loadWallet = async () => { + const seedPhrase = await seedVault.getSeed(); + const currentAccounts = softwareAccountsList || []; + + if (currentAccounts.length === 0) { + // This will happen on first load after the wallet is created. We create the accounts here to ensure + // the wallet seed phrase is finalised and in the vault before we create the accounts. + // We create one account to ensure the wallet is functional on load, if there were others, they will + // get populated in the loadActiveAccounts method. + const account = await createSingleAccount( + seedPhrase, + 0, + network.type, + selectedNetwork, + savedNames[network.type], + ); + currentAccounts.push(account); + + await loadActiveAccounts(seedPhrase, network, selectedNetwork, currentAccounts); + } + + await ensureSelectedAccountValid(); + + dispatch(setWalletUnlockedAction(true)); + setSessionStartTimeAndMigrate(); + }; + const unlockWallet = async (password: string) => { if (encryptedSeed && encryptedSeed.length > 0) { await migrateLegacySeedStorage(password); return; } await seedVault.unlockVault(password); - try { - const decrypted = await seedVault.getSeed(); - await loadActiveAccounts(decrypted, network, selectedNetwork, accountsList); - } catch (err) { - dispatch(fetchAccountAction(accountsList[0], accountsList)); - dispatch(getActiveAccountsAction(accountsList)); - } finally { - setSessionStartTimeAndMigrate(); - } + + loadWallet(); }; const lockWallet = async () => { - await seedVault.lockVault(); dispatch(setWalletUnlockedAction(false)); + if (await seedVault.isVaultUnlocked()) { + await seedVault.lockVault(); + } }; const toggleStxVisibility = async () => { dispatch(setWalletHideStxAction(!hideStx)); }; + const changeShowDataCollectionAlert = (showDataCollectionAlertUpdate = false) => { + dispatch(changeShowDataCollectionAlertAction(showDataCollectionAlertUpdate)); + }; + const resetWallet = async () => { + await queryClient.cancelQueries(); + queryClient.clear(); + resetMixPanel(); dispatch(resetWalletAction()); localStorage.clear(); @@ -186,172 +285,117 @@ const useWalletReducer = () => { ]); }; - const restoreWallet = async (seed: string, password: string) => { - const wallet = await walletFromSeedPhrase({ - mnemonic: seed, - index: 0n, - network: 'Mainnet', - }); - const account: Account = { - id: 0, - btcAddress: wallet.btcAddress, - btcPublicKey: wallet.btcPublicKey, - masterPubKey: wallet.masterPubKey, - ordinalsAddress: wallet.ordinalsAddress, - ordinalsPublicKey: wallet.ordinalsPublicKey, - stxAddress: wallet.stxAddress, - stxPublicKey: wallet.stxPublicKey, - }; - const hasSeed = await seedVault.hasSeed(); - if (hasSeed && !masterPubKey) { - await seedVault.clearVaultStorage(); + const initialiseSeedVault = async (seedPhrase: string, password: string) => { + // We create an account to ensure that the seed phrase is valid, but we don't store it + // The actual account creation is done on startup of the wallet + // If the seed phrase is invalid, then this will throw an error + await createSingleAccount( + seedPhrase, + 0, + network.type, + selectedNetwork, + savedNames[network.type], + ); + + await chrome.storage.local.clear(); + await chrome.storage.session.clear(); + + await seedVault.storeSeed(seedPhrase, password); + + // since we cleared up storage above, the store won't be populated in storage + // and the selected value of showDataCollectionAlert will be lost + // we need to set it back here, making sure it changes so that redux-persist will save it + if (showDataCollectionAlert !== null && showDataCollectionAlert !== undefined) { + changeShowDataCollectionAlert(!showDataCollectionAlert); + changeShowDataCollectionAlert(showDataCollectionAlert); + + // reinitialise with masterpubkey hash now that we have it + if (hasOptedInMixPanelTracking()) { + const seed = await seedVault.getSeed(); + const wallet = await walletFromSeedPhrase({ + mnemonic: seed, + index: 0n, + network: 'Mainnet', + }); + optInMixPanel(wallet.masterPubKey); + } } - await seedVault.init(password); - await seedVault.storeSeed(seed); - trackMixPanel(AnalyticsEvents.RestoreWallet); - const bnsName = await getBnsName(wallet.stxAddress, selectedNetwork); - dispatch(setWalletAction(wallet)); localStorage.setItem('migrated', 'true'); - try { - await loadActiveAccounts(seed, network, selectedNetwork, [ - { - bnsName, - ...account, - }, - ]); - } catch (err) { - dispatch( - fetchAccountAction( - { - ...account, - bnsName, - }, - [ - { - ...account, - }, - ], - ), - ); - dispatch( - getActiveAccountsAction([ - { - ...account, - bnsName, - }, - ]), - ); - } finally { - setSessionStartTime(); - } + setSessionStartTime(); }; - const createWallet = async () => { - const mnemonic = await seedVault.getSeed(); - // TODO refactor to use createWalletAccount instead, which also adds bns name - // and gaiahub config - const wallet = await walletFromSeedPhrase({ mnemonic, index: 0n, network: 'Mainnet' }); - - const account: Account = { - id: 0, - btcAddress: wallet.btcAddress, - btcPublicKey: wallet.btcPublicKey, - masterPubKey: wallet.masterPubKey, - ordinalsAddress: wallet.ordinalsAddress, - ordinalsPublicKey: wallet.ordinalsPublicKey, - stxAddress: wallet.stxAddress, - stxPublicKey: wallet.stxPublicKey, - }; - trackMixPanel(AnalyticsEvents.CreateNewWallet); + const restoreWallet = async (seedPhrase: string, password: string) => { + await initialiseSeedVault(seedPhrase, password); - dispatch(setWalletAction(wallet)); - dispatch(fetchAccountAction(account, [account])); - setSessionStartTime(); - localStorage.setItem('migrated', 'true'); + trackMixPanel(AnalyticsEvents.RestoreWallet); + }; + + const createWallet = async (seedPhrase: string, password: string) => { + await initialiseSeedVault(seedPhrase, password); + + trackMixPanel(AnalyticsEvents.CreateNewWallet); }; const createAccount = async () => { const seedPhrase = await seedVault.getSeed(); - const newAccountsList = await createWalletAccount( + const newAccounts = await createWalletAccount( seedPhrase, network, selectedNetwork, - accountsList, + softwareAccountsList, ); - dispatch(addAccountAction(newAccountsList)); + dispatch(updateSoftwareAccountsAction(newAccounts)); }; - const switchAccount = async (account: Account) => { + const switchAccount = useCallback( + async (account: Account) => { + // we clear the query cache to prevent data from the other account potentially being displayed + await queryClient.cancelQueries(); + queryClient.clear(); + + await ensureSelectedAccountValid(account.accountType, account.id); + + dispatch(selectAccount(account)); + }, + [dispatch, ensureSelectedAccountValid, queryClient], + ); + + const changeNetwork = async (changedNetwork: SettingsNetwork) => { + // Save current custom account names + const currentNetworkType = network.type; + const customAccountNames = softwareAccountsList.map((account) => ({ + id: account.id, + name: account.accountName, + })); + dispatch(updateSavedNamesAction(currentNetworkType, customAccountNames)); + // we clear the query cache to prevent data from the other account potentially being displayed await queryClient.cancelQueries(); queryClient.clear(); - dispatch( - selectAccount( - account, - account.stxAddress, - account.btcAddress, - account.ordinalsAddress, - account.masterPubKey, - account.stxPublicKey, - account.btcPublicKey, - account.ordinalsPublicKey, - network, - undefined, - account.accountType, - account.accountName, - ), - ); - dispatch(fetchAccountAction(account, accountsList)); - }; + dispatch(ChangeNetworkAction(changedNetwork)); - const changeNetwork = async (changedNetwork: SettingsNetwork) => { const seedPhrase = await seedVault.getSeed(); - dispatch(ChangeNetworkAction(changedNetwork)); - const wallet = await walletFromSeedPhrase({ - mnemonic: seedPhrase, - index: 0n, - network: changedNetwork.type, - }); - const account: Account = { - id: 0, - btcAddress: wallet.btcAddress, - btcPublicKey: wallet.btcPublicKey, - masterPubKey: wallet.masterPubKey, - ordinalsAddress: wallet.ordinalsAddress, - ordinalsPublicKey: wallet.ordinalsPublicKey, - stxAddress: wallet.stxAddress, - stxPublicKey: wallet.stxPublicKey, - }; - dispatch(setWalletAction(wallet)); - const networkObject = + const changedStacksNetwork = changedNetwork.type === 'Mainnet' ? new StacksMainnet({ url: changedNetwork.address }) : new StacksTestnet({ url: changedNetwork.address }); - try { - await loadActiveAccounts(wallet.seedPhrase, changedNetwork, networkObject, [account]); - } catch (err) { - const bnsName = await getBnsName(wallet.stxAddress, networkObject); - dispatch( - fetchAccountAction( - { - ...account, - bnsName, - }, - [account], - ), - ); - dispatch( - getActiveAccountsAction([ - { - ...account, - bnsName, - }, - ]), + + const accounts: Account[] = []; + + // we recreate the same number of accounts on the new network + for (let i = 0; i < softwareAccountsList.length; i++) { + const account = await createSingleAccount( + seedPhrase, + i, + changedNetwork.type, + changedStacksNetwork, + savedNames[changedNetwork.type], ); + accounts.push(account); } - await refetchStxData(); - await refetchBtcData(); + + await loadActiveAccounts(seedPhrase, changedNetwork, changedStacksNetwork, accounts, true); }; const addLedgerAccount = async (ledgerAccount: Account) => { @@ -367,32 +411,43 @@ const useWalletReducer = () => { }; const updateLedgerAccounts = async (updatedLedgerAccount: Account) => { + if (updatedLedgerAccount.accountType !== 'ledger') { + throw new Error('Expected ledger account. Update cancelled.'); + } + const newLedgerAccountsList = ledgerAccountsList.map((account) => account.id === updatedLedgerAccount.id ? updatedLedgerAccount : account, ); + dispatch(updateLedgerAccountsAction(newLedgerAccountsList)); - if (isLedgerAccount(selectedAccount) && updatedLedgerAccount.id === selectedAccount?.id) { - switchAccount(updatedLedgerAccount); - } }; + // TODO: refactor this to be more specific to renaming software accounts const renameAccount = async (updatedAccount: Account) => { - const newAccountsList = accountsList.map((account) => - account.accountType === updatedAccount.accountType && account.id === updatedAccount.id - ? updatedAccount - : account, + if (updatedAccount.accountType !== 'software') { + throw new Error('Expected software account. Renaming cancelled.'); + } + const newAccountsList = softwareAccountsList.map((account) => + account.id === updatedAccount.id ? updatedAccount : account, ); - const newSelectedAccount = - selectedAccount?.accountType === updatedAccount.accountType && - selectedAccount?.id === updatedAccount.id - ? { ...selectedAccount, accountName: updatedAccount.accountName } - : selectedAccount; - dispatch(renameAccountAction(newAccountsList, newSelectedAccount)); + dispatch(updateSoftwareAccountsAction(newAccountsList)); + + const updatedSavedNames = + savedNames[network.type]?.map((item) => + item.id === updatedAccount.id ? { ...item, name: updatedAccount.accountName } : item, + ) || []; + + if (!updatedSavedNames.find((item) => item.id === updatedAccount.id)) { + updatedSavedNames.push({ id: updatedAccount.id, name: updatedAccount.accountName }); + } + + dispatch(updateSavedNamesAction(network.type, updatedSavedNames)); }; return { unlockWallet, + loadWallet, lockWallet, resetWallet, restoreWallet, @@ -405,6 +460,7 @@ const useWalletReducer = () => { updateLedgerAccounts, renameAccount, toggleStxVisibility, + changeShowDataCollectionAlert, }; }; diff --git a/src/app/hooks/useWalletSession.ts b/src/app/hooks/useWalletSession.ts index 3c80724d2..8de50b35d 100644 --- a/src/app/hooks/useWalletSession.ts +++ b/src/app/hooks/useWalletSession.ts @@ -1,7 +1,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { setWalletLockPeriodAction } from '@stores/wallet/actions/actionCreators'; import { WalletSessionPeriods } from '@stores/wallet/actions/types'; -import { chromeSessionStorage } from '@utils/chromeStorage'; +import chromeStorage from '@utils/chromeStorage'; import { addMinutes } from 'date-fns'; import { useDispatch } from 'react-redux'; import useSeedVault from './useSeedVault'; @@ -15,19 +15,22 @@ const useWalletSession = () => { const setSessionStartTime = () => { const sessionStartTime = new Date().getTime(); - chromeSessionStorage.setItem(SESSION_START_TIME_KEY, sessionStartTime); + chromeStorage.session.setItem(SESSION_START_TIME_KEY, sessionStartTime); }; - const getSessionStartTime = async () => chromeSessionStorage.getItem(SESSION_START_TIME_KEY); + const getSessionStartTime = async () => + chromeStorage.session.getItem(SESSION_START_TIME_KEY); const clearSessionTime = async () => { - await chromeSessionStorage.removeItem(SESSION_START_TIME_KEY); + await chromeStorage.session.removeItem(SESSION_START_TIME_KEY); }; const shouldLock = async () => { const isUnlocked = await isVaultUnlocked(); if (!isUnlocked) return false; const startTime = await getSessionStartTime(); + // we don't know when the session started, so we assume we need to lock + if (!startTime) return true; const currentTime = new Date().getTime(); return currentTime >= addMinutes(startTime, walletLockPeriod).getTime(); }; diff --git a/src/app/layouts/sendLayout.tsx b/src/app/layouts/sendLayout.tsx index 030fc266c..a76982e79 100644 --- a/src/app/layouts/sendLayout.tsx +++ b/src/app/layouts/sendLayout.tsx @@ -60,8 +60,16 @@ const BottomBarContainer = styled.div({ const Button = styled.button` display: flex; + justify-content: center; + align-items: center; background-color: transparent; + border-radius: 24px; + padding: 5px; + width: 30px; margin-bottom: ${(props) => props.theme.space.l}; + :hover { + background-color: ${(props) => props.theme.colors.white_900}; + } `; function SendLayout({ diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index a871566a8..da921328b 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -21,10 +21,12 @@ import AuthenticationRequest from '@screens/connect/authenticationRequest'; import BtcSelectAddressScreen from '@screens/connect/btcSelectAddressScreen'; import StxSelectAccountScreen from '@screens/connect/stxSelectAccountScreen'; import StxSelectAddressScreen from '@screens/connect/stxSelectAddressScreen'; +import { ConnectionRequest } from '@screens/connectionRequest'; import CreateInscription from '@screens/createInscription'; import CreatePassword from '@screens/createPassword'; import CreateWalletSuccess from '@screens/createWalletSuccess'; import ErrorBoundary from '@screens/error'; +import EtchRune from '@screens/etchRune'; import ExecuteBrc20Transaction from '@screens/executeBrc20Transaction'; import Explore from '@screens/explore'; import ForgotPassword from '@screens/forgotPassword'; @@ -37,6 +39,7 @@ import VerifyLedger from '@screens/ledger/verifyLedgerAccountAddress'; import Legal from '@screens/legal'; import Login from '@screens/login'; import ManageTokens from '@screens/manageTokens'; +import MintRune from '@screens/mintRune'; import NftCollection from '@screens/nftCollection'; import NftDashboard from '@screens/nftDashboard'; import SupportedRarities from '@screens/nftDashboard/supportedRarities'; @@ -50,7 +53,6 @@ import RestoreFunds from '@screens/restoreFunds'; import RecoverRunes from '@screens/restoreFunds/recoverRunes'; import RestoreOrdinals from '@screens/restoreFunds/restoreOrdinals'; import RestoreWallet from '@screens/restoreWallet'; -// import SendBrc20Screen from '@screens/sendBrc20'; import SendBrc20OneStepScreen from '@screens/sendBrc20OneStep'; import SendBtcScreen from '@screens/sendBtc'; import SendSip10Screen from '@screens/sendFt'; @@ -63,11 +65,13 @@ import Setting from '@screens/settings'; import BackupWalletScreen from '@screens/settings/backupWallet'; import ChangeNetworkScreen from '@screens/settings/changeNetwork'; import ChangePasswordScreen from '@screens/settings/changePassword'; +import ConnectedAppsAndPermissionsScreen from '@screens/settings/connectedAppsAndPermissions'; import FiatCurrencyScreen from '@screens/settings/fiatCurrency'; import LockCountdown from '@screens/settings/lockCountdown'; import PrivacyPreferencesScreen from '@screens/settings/privacyPreferences'; import SignBatchPsbtRequest from '@screens/signBatchPsbtRequest'; import SignMessageRequest from '@screens/signMessageRequest'; +import SignMessageRequestInApp from '@screens/signMessageRequestInApp'; import SignPsbtRequest from '@screens/signPsbtRequest'; import SignatureRequest from '@screens/signatureRequest'; import SpeedUpTransactionScreen from '@screens/speedUpTransaction'; @@ -76,8 +80,11 @@ import SwapScreen from '@screens/swap'; import SwapConfirmScreen from '@screens/swap/swapConfirmation'; import TransactionRequest from '@screens/transactionRequest'; import TransactionStatus from '@screens/transactionStatus'; +import UnlistRuneScreen from '@screens/unlistRune'; import WalletExists from '@screens/walletExists'; +import ListRuneScreen from 'app/screens/listRune'; import { createHashRouter } from 'react-router-dom'; +import RoutePaths from './paths'; const router = createHashRouter([ { @@ -137,10 +144,6 @@ const router = createHashRouter([ path: 'receive/:currency', element: , }, - { - path: 'send-stx', - element: , - }, { path: 'send-sip10', element: , @@ -365,6 +368,10 @@ const router = createHashRouter([ path: 'backup-wallet', element: , }, + { + path: RoutePaths.ConnectedAppsAndPermissions, + element: , + }, { path: 'tx-status', element: , @@ -373,6 +380,14 @@ const router = createHashRouter([ path: 'buy/:currency', element: , }, + { + path: 'list-rune/:runeId', + element: , + }, + { + path: 'unlist-rune/:runeId', + element: , + }, { path: 'coinDashboard/:currency', element: , @@ -393,6 +408,14 @@ const router = createHashRouter([ ), }, + { + path: RequestsRoutes.SignMessageRequestInApp, + element: ( + + + + ), + }, { path: 'send-ordinal', element: ( @@ -401,16 +424,6 @@ const router = createHashRouter([ ), }, - // ENG-4020 - Disable BRC20 Sending on Ledger - // { - // // TODO deprecate this after brc20 one step ledger support done - // path: 'send-brc20', - // element: ( - // - // - // - // ), - // }, { path: 'send-brc20-one-step', element: ( @@ -443,6 +456,30 @@ const router = createHashRouter([ ), }, + { + path: RequestsRoutes.ConnectionRequest, + element: ( + + + + ), + }, + { + path: RequestsRoutes.MintRune, + element: ( + + + + ), + }, + { + path: RequestsRoutes.EtchRune, + element: ( + + + + ), + }, ], }, { @@ -454,6 +491,10 @@ const router = createHashRouter([ path: 'send-btc', element: , }, + { + path: 'send-stx', + element: , + }, { path: 'confirm-btc-tx', element: , diff --git a/src/app/routes/paths.ts b/src/app/routes/paths.ts new file mode 100644 index 000000000..4632a617f --- /dev/null +++ b/src/app/routes/paths.ts @@ -0,0 +1,5 @@ +enum RoutePaths { + ConnectedAppsAndPermissions = '/connected-apps-and-permissions', +} + +export default RoutePaths; diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index 8712d1eb7..18aa34be7 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -6,6 +6,7 @@ import Separator from '@components/separator'; import TopRow from '@components/topRow'; import useAccountBalance from '@hooks/queries/useAccountBalance'; import { broadcastResetUserFlow } from '@hooks/useResetUserFlow'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { Plus } from '@phosphor-icons/react'; @@ -15,7 +16,7 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -export const Container = styled.div({ +const Container = styled.div({ height: '100%', display: 'flex', flexDirection: 'column', @@ -59,7 +60,8 @@ function AccountList(): JSX.Element { const navigate = useNavigate(); const { search } = useLocation(); const params = new URLSearchParams(search); - const { network, accountsList, selectedAccount, ledgerAccountsList } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network, accountsList, ledgerAccountsList } = useWalletSelector(); const { createAccount, switchAccount } = useWalletReducer(); const { enqueueFetchBalances } = useAccountBalance(); diff --git a/src/app/screens/backupWallet/index.tsx b/src/app/screens/backupWallet/index.tsx index e7734795d..58483a5b1 100644 --- a/src/app/screens/backupWallet/index.tsx +++ b/src/app/screens/backupWallet/index.tsx @@ -2,7 +2,8 @@ import backup from '@assets/img/backupWallet/backup.svg'; import useSeedVault from '@hooks/useSeedVault'; import { generateMnemonic } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; -import { useEffect } from 'react'; +import Spinner from '@ui-library/spinner'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -50,35 +51,42 @@ const BackupActionsContainer = styled.div((props) => ({ columnGap: props.theme.space.xs, })); +const LoadingContainer = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginTop: props.theme.spacing(20), + width: '100%', +})); + function BackupWallet(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'BACKUP_WALLET_SCREEN' }); const navigate = useNavigate(); - const { - init: initSeedVault, - storeSeed, - unlockVault, - hasSeed, - clearVaultStorage, - } = useSeedVault(); + const { storeSeed, unlockVault, hasSeed, clearVaultStorage } = useSeedVault(); + const [isLoading, setIsLoading] = useState(false); // TODO move this to SeedVault? const generateAndStoreSeedPhrase = async () => { const newSeedPhrase = generateMnemonic(); - await initSeedVault(''); - await storeSeed(newSeedPhrase); + await storeSeed(newSeedPhrase, ''); }; useEffect(() => { (async () => { + setIsLoading(true); const hasSeedPhrase = await hasSeed(); - if (!hasSeedPhrase) { - await generateAndStoreSeedPhrase(); - } else { - // attempt to unlock the wallet with an empty password (verifies the user didn't finish onboarding) - await unlockVault(''); - // clear the vault storage and generate a new seed phrase - await clearVaultStorage(); - await generateAndStoreSeedPhrase(); + try { + if (!hasSeedPhrase) { + await generateAndStoreSeedPhrase(); + } else { + // attempt to unlock the wallet with an empty password (verifies the user didn't finish onboarding) + await unlockVault(''); + // clear the vault storage and generate a new seed phrase + await clearVaultStorage(); + await generateAndStoreSeedPhrase(); + } + } finally { + setIsLoading(false); } })(); }, []); @@ -99,10 +107,22 @@ function BackupWallet(): JSX.Element { {t('SCREEN_TITLE')} {t('SCREEN_SUBTITLE')} - - diff --git a/src/app/screens/coinDashboard/coinHeader.styled.ts b/src/app/screens/coinDashboard/coinHeader.styled.ts new file mode 100644 index 000000000..f5a0721d6 --- /dev/null +++ b/src/app/screens/coinDashboard/coinHeader.styled.ts @@ -0,0 +1,122 @@ +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + +export const RowContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', +}); + +export const ProtocolText = styled.p((props) => ({ + ...props.theme.headline_category_s, + fontWeight: 700, + height: 15, + marginTop: props.theme.spacing(3), + textTransform: 'uppercase', + marginLeft: props.theme.spacing(2), + backgroundColor: props.theme.colors.white_400, + padding: '1px 6px 1px', + color: props.theme.colors.elevation0, + borderRadius: props.theme.radius(2), +})); + +export const BalanceInfoContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}); + +export const BalanceValuesContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const CoinBalanceText = styled.p((props) => ({ + ...props.theme.typography.headline_l, + fontSize: '1.5rem', + color: props.theme.colors.white_0, + textAlign: 'center', + wordBreak: 'break-all', +})); + +export const FiatAmountText = styled.p((props) => ({ + ...props.theme.headline_category_s, + color: props.theme.colors.white_200, + fontSize: '0.875rem', + marginTop: props.theme.spacing(2), + textAlign: 'center', +})); + +export const BalanceTitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + textAlign: 'center', + marginTop: props.theme.spacing(4), +})); + +export const RowButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + marginTop: props.theme.spacing(11), + columnGap: props.theme.space.l, +})); + +export const HeaderSeparator = styled.div((props) => ({ + border: `0.5px solid ${props.theme.colors.white_400}`, + width: '50%', + alignSelf: 'center', + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(8), +})); + +export const StxLockedText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, +})); + +export const LockedStxContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + span: { + color: props.theme.colors.white_400, + marginRight: props.theme.spacing(3), + }, + img: { + marginRight: props.theme.spacing(3), + }, +})); + +export const AvailableStxContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginTop: props.theme.spacing(4), + span: { + color: props.theme.colors.white_400, + marginRight: props.theme.spacing(3), + }, +})); + +export const VerifyOrViewContainer = styled.div((props) => ({ + margin: props.theme.spacing(8), + marginTop: props.theme.spacing(16), + marginBottom: props.theme.spacing(20), +})); + +export const VerifyButtonContainer = styled.div((props) => ({ + marginBottom: props.theme.spacing(6), +})); + +export const StacksLockedInfoText = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + textAlign: 'left', +})); diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 6f68da1aa..537dd0ece 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -1,6 +1,8 @@ import ArrowDown from '@assets/img/dashboard/arrow_down.svg'; import ArrowUp from '@assets/img/dashboard/arrow_up.svg'; import Buy from '@assets/img/dashboard/black_plus.svg'; +import List from '@assets/img/dashboard/list.svg'; +import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; import Lock from '@assets/img/transactions/Lock.svg'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; @@ -9,163 +11,51 @@ import TokenImage from '@components/tokenImage'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useCoinRates from '@hooks/queries/useCoinRates'; import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useHasFeature from '@hooks/useHasFeature'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { - currencySymbolMap, + FeatureId, FungibleToken, + currencySymbolMap, + getFiatEquivalent, microstacksToStx, - satsToBtc, } from '@secretkeylabs/xverse-core'; import { CurrencyTypes } from '@utils/constants'; import { isInOptions, isLedgerAccount } from '@utils/helper'; -import { getFtBalance, getFtTicker } from '@utils/tokens'; +import { getBalanceAmount, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -interface CoinBalanceProps { - coin: CurrencyTypes; +import { + AvailableStxContainer, + BalanceInfoContainer, + BalanceTitleText, + BalanceValuesContainer, + CoinBalanceText, + Container, + FiatAmountText, + HeaderSeparator, + LockedStxContainer, + ProtocolText, + RowButtonContainer, + RowContainer, + StacksLockedInfoText, + StxLockedText, + VerifyButtonContainer, + VerifyOrViewContainer, +} from './coinHeader.styled'; + +type Props = { + currency: CurrencyTypes; fungibleToken?: FungibleToken; -} - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', -}); - -const ProtocolText = styled.p((props) => ({ - ...props.theme.headline_category_s, - fontWeight: 700, - height: 15, - marginTop: props.theme.spacing(3), - textTransform: 'uppercase', - marginLeft: props.theme.spacing(2), - backgroundColor: props.theme.colors.white_400, - padding: '1px 6px 1px', - color: props.theme.colors.elevation0, - borderRadius: props.theme.radius(2), -})); - -const BalanceInfoContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -}); - -const BalanceValuesContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const CoinBalanceText = styled.p((props) => ({ - ...props.theme.headline_l, - fontSize: '1.5rem', - color: props.theme.colors.white_0, - textAlign: 'center', - wordBreak: 'break-all', -})); - -const FiatAmountText = styled.p((props) => ({ - ...props.theme.headline_category_s, - color: props.theme.colors.white_200, - fontSize: '0.875rem', - marginTop: props.theme.spacing(2), - textAlign: 'center', -})); - -const BalanceTitleText = styled.p((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - textAlign: 'center', - marginTop: props.theme.spacing(4), -})); - -const RowButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - marginTop: props.theme.spacing(11), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - position: 'relative', - marginRight: props.theme.spacing(12), -})); - -const RecieveButtonContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const HeaderSeparator = styled.div((props) => ({ - border: `0.5px solid ${props.theme.colors.white_400}`, - width: '50%', - alignSelf: 'center', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - -const StxLockedText = styled.p((props) => ({ - ...props.theme.body_medium_m, -})); - -const LockedStxContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - span: { - color: props.theme.colors.white_400, - marginRight: props.theme.spacing(3), - }, - img: { - marginRight: props.theme.spacing(3), - }, -})); +}; -const AvailableStxContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginTop: props.theme.spacing(4), - span: { - color: props.theme.colors.white_400, - marginRight: props.theme.spacing(3), - }, -})); - -const VerifyOrViewContainer = styled.div((props) => ({ - margin: props.theme.spacing(8), - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(20), -})); - -const VerifyButtonContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(6), -})); - -const StacksLockedInfoText = styled.span((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_400, - textAlign: 'left', -})); - -export default function CoinHeader(props: CoinBalanceProps) { - const { coin, fungibleToken } = props; - const { fiatCurrency, selectedAccount } = useWalletSelector(); +export default function CoinHeader({ currency, fungibleToken }: Props) { + const selectedAccount = useSelectedAccount(); + const { fiatCurrency, network } = useWalletSelector(); const { data: btcBalance } = useBtcWalletData(); const { data: stxData } = useStxWalletData(); const { btcFiatRate, stxBtcRate } = useCoinRates(); @@ -173,6 +63,15 @@ export default function CoinHeader(props: CoinBalanceProps) { const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); const [openReceiveModal, setOpenReceiveModal] = useState(false); const isReceivingAddressesVisible = !isLedgerAccount(selectedAccount); + const showSwaps = + useHasFeature(FeatureId.SWAPS) && + currency === 'STX' && + !isLedgerAccount(selectedAccount) && + network.type !== 'Testnet'; + + const showRunesListing = + (useHasFeature(FeatureId.RUNES_LISTING) || process.env.NODE_ENV === 'development') && + network.type === 'Mainnet'; const handleReceiveModalOpen = () => { setOpenReceiveModal(true); @@ -182,58 +81,18 @@ export default function CoinHeader(props: CoinBalanceProps) { setOpenReceiveModal(false); }; - function getBalanceAmount() { - switch (coin) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? 0)).toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)).toString(); - default: - return fungibleToken ? getFtBalance(fungibleToken) : ''; - } - } - - function getFtFiatEquivalent() { - if (fungibleToken?.tokenFiatRate) { - const balance = new BigNumber(getFtBalance(fungibleToken)); - const rate = new BigNumber(fungibleToken.tokenFiatRate); - return balance.multipliedBy(rate).toFixed(2).toString(); - } - return ''; - } - const getTokenTicker = () => { - if (coin === 'STX' || coin === 'BTC') { - return coin; + if (currency === 'STX' || currency === 'BTC') { + return currency; } - if (coin === 'FT' && fungibleToken) { + if (currency === 'FT' && fungibleToken) { return getFtTicker(fungibleToken); } return ''; }; - function getFiatEquivalent() { - switch (coin) { - case 'STX': - return microstacksToStx(new BigNumber(stxData?.balance ?? '0')) - .multipliedBy(new BigNumber(stxBtcRate)) - .multipliedBy(new BigNumber(btcFiatRate)) - .toFixed(2) - .toString(); - case 'BTC': - return satsToBtc(new BigNumber(btcBalance ?? 0)) - .multipliedBy(new BigNumber(btcFiatRate)) - .toFixed(2) - .toString(); - case 'FT': - return getFtFiatEquivalent(); - default: - return ''; - } - } - const renderStackingBalances = () => { - if (!new BigNumber(stxData?.locked ?? 0).eq(0) && coin === 'STX') { + if (!new BigNumber(stxData?.locked ?? 0).eq(0) && currency === 'STX') { return ( <> @@ -264,78 +123,31 @@ export default function CoinHeader(props: CoinBalanceProps) { }; const goToSendScreen = async () => { - if (isLedgerAccount(selectedAccount) && !isInOptions()) { - switch (coin) { - case 'BTC': - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/send-btc'), - }); - return; - case 'STX': - await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/send-stx'), - }); - return; - default: - break; - } + let route = ''; + if (currency === 'BTC' || currency === 'STX') { + route = `/send-${currency}`; + } else { switch (fungibleToken?.protocol) { case 'stacks': - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/send-sip10?coinTicker=${fungibleToken?.ticker}`, - ), - }); - return; + route = `/send-sip10?principal=${fungibleToken?.principal}`; + break; case 'brc-20': - // TODO replace with send-brc20-one-step route, when ledger support is ready - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/send-brc20?coinTicker=${fungibleToken?.ticker}`, - ), - }); - return; + route = `/send-brc20-one-step?principal=${fungibleToken?.principal}`; + break; case 'runes': - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/send-rune?coinTicker=${fungibleToken?.name}`), - }); - return; + route = `/send-rune?principal=${fungibleToken?.principal}`; + break; default: break; } } - switch (coin) { - case 'BTC': - case 'STX': - navigate(`/send-${coin}`); - break; - default: - break; - } - switch (fungibleToken?.protocol) { - case 'stacks': - navigate('/send-sip10', { - state: { - fungibleToken, - }, - }); - break; - case 'brc-20': - navigate('/send-brc20-one-step', { - state: { - fungibleToken, - }, - }); - break; - case 'runes': - navigate('/send-rune', { - state: { - fungibleToken, - }, - }); - break; - default: - break; + + if (isLedgerAccount(selectedAccount) && !isInOptions()) { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#${route}`), + }); + } else { + navigate(route); } }; @@ -343,58 +155,30 @@ export default function CoinHeader(props: CoinBalanceProps) { if (fungibleToken?.name) { return `${fungibleToken.name} ${t('BALANCE')}`; } - if (coin === 'STX') { + + if (!currency) { + return ''; + } + + if (currency === 'STX') { return `Stacks ${t('BALANCE')}`; } - if (coin === 'BTC') { + if (currency === 'BTC') { return `Bitcoin ${t('BALANCE')}`; } - if (coin) { - return `${coin} ${t('BALANCE')}`; - } - return ''; + return `${currency} ${t('BALANCE')}`; }; - const verifyOrViewAddresses = ( - - - { - await chrome.tabs.create({ - url: chrome.runtime.getURL( - `options.html#/verify-ledger?currency=${ - !fungibleToken ? coin : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' - }`, - ), - }); - }} - /> - - { - navigate( - `/receive/${ - !fungibleToken ? coin : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' - }`, - ); - }} - /> - - ); - return ( - {getDashboardTitle()} + {getDashboardTitle()} {fungibleToken?.protocol && ( {fungibleToken?.protocol === 'stacks' @@ -405,18 +189,24 @@ export default function CoinHeader(props: CoinBalanceProps) { ( - {`${value} ${getTokenTicker()}`} + {`${value} ${getTokenTicker()}`} )} /> {value}} /> @@ -424,31 +214,9 @@ export default function CoinHeader(props: CoinBalanceProps) { {renderStackingBalances()} - {/* ENG-4020 - Disable BRC20 Sending on Ledger */} - {!(fungibleToken?.protocol === 'brc-20' && isLedgerAccount(selectedAccount)) && ( - - goToSendScreen()} /> - - )} - {!fungibleToken ? ( + goToSendScreen()} /> + {fungibleToken ? ( <> - - { - if (isReceivingAddressesVisible) { - navigate(`/receive/${coin}`); - } else { - handleReceiveModalOpen(); - } - }} - /> - - navigate(`/buy/${coin}`)} /> - - ) : ( - - + {showRunesListing && fungibleToken.protocol === 'runes' && ( + navigate(`/list-rune/${fungibleToken.principal}`)} + /> + )} + + ) : ( + <> + { + if (isReceivingAddressesVisible) { + navigate(`/receive/${currency}`); + } else { + handleReceiveModalOpen(); + } + }} + /> + {showSwaps && ( + navigate(`/swap?from=${currency}`)} + /> + )} + navigate(`/buy/${currency}`)} + /> + )} - - {verifyOrViewAddresses} + + + { + await chrome.tabs.create({ + url: chrome.runtime.getURL( + `options.html#/verify-ledger?currency=${ + !fungibleToken + ? currency + : fungibleToken?.protocol === 'stacks' + ? 'STX' + : 'ORD' + }`, + ), + }); + }} + /> + + { + navigate( + `/receive/${ + !fungibleToken ? currency : fungibleToken?.protocol === 'stacks' ? 'STX' : 'ORD' + }`, + ); + }} + /> + ); diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx index 20322a877..76754d38e 100644 --- a/src/app/screens/coinDashboard/index.tsx +++ b/src/app/screens/coinDashboard/index.tsx @@ -1,18 +1,32 @@ import linkIcon from '@assets/img/linkIcon.svg'; import CopyButton from '@components/copyButton'; +import OptionsDialog from '@components/optionsDialog/optionsDialog'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; -import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useGetRuneFungibleTokens'; +import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; +import useSpamTokens from '@hooks/queries/useSpamTokens'; +import useResetUserFlow, { broadcastResetUserFlow } from '@hooks/useResetUserFlow'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; -import { CurrencyTypes } from '@utils/constants'; +import { Flag } from '@phosphor-icons/react'; +import { FungibleToken } from '@secretkeylabs/xverse-core'; +import { + setBrc20ManageTokensAction, + setRunesManageTokensAction, + setSip10ManageTokensAction, + setSpamTokenAction, +} from '@stores/wallet/actions/actionCreators'; +import { StyledP } from '@ui-library/common.styled'; +import { CurrencyTypes, SPAM_OPTIONS_WIDTH } from '@utils/constants'; import { getExplorerUrl } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { useParams, useSearchParams } from 'react-router-dom'; import styled from 'styled-components'; +import Theme from 'theme'; import CoinHeader from './coinHeader'; import TransactionsHistoryList from './transactionsHistoryList'; @@ -106,25 +120,68 @@ const Button = styled.button<{ opacity: props.isSelected ? 1 : 0.6, })); +const ButtonRow = styled.button` + display: flex; + align-items: center; + background-color: transparent; + flex-direction: row; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + padding-top: ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.s}; + transition: background-color 0.2s ease; + :hover { + background-color: ${(props) => props.theme.colors.elevation3}; + } + :active { + background-color: ${(props) => props.theme.colors.elevation3}; + } +`; + +const TokenText = styled(StyledP)` + margin-left: ${(props) => props.theme.space.m}; +`; + export default function CoinDashboard() { const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); - const navigate = useNavigate(); const [showFtContractDetails, setShowFtContractDetails] = useState(false); - const { currency } = useParams(); + const [showOptionsDialog, setShowOptionsDialog] = useState(false); + const [optionsDialogIndents, setOptionsDialogIndents] = useState< + { top: string; left: string } | undefined + >(); const [searchParams] = useSearchParams(); + const { addToSpamTokens } = useSpamTokens(); + const dispatch = useDispatch(); + const { currency } = useParams(); const { visible: runesCoinsList } = useVisibleRuneFungibleTokens(); const { visible: sip10CoinsList } = useVisibleSip10FungibleTokens(); const { visible: brc20CoinsList } = useVisibleBrc20FungibleTokens(); - const ftKey = searchParams.get('ftKey'); - const selectedFt = - sip10CoinsList.find((ft) => ft.principal === ftKey) ?? - brc20CoinsList.find((ft) => ft.principal === ftKey) ?? - runesCoinsList.find((ft) => ft.principal === ftKey); + const ftKey = searchParams.get('ftKey'); + const protocol = searchParams.get('protocol'); + let selectedFt: FungibleToken | undefined; - const protocol = selectedFt?.protocol; + if (ftKey && protocol) { + switch (protocol) { + case 'stacks': + selectedFt = sip10CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'brc-20': + selectedFt = brc20CoinsList.find((ft) => ft.principal === ftKey); + break; + case 'runes': + selectedFt = runesCoinsList.find((ft) => ft.principal === ftKey); + break; + default: + selectedFt = undefined; + } + } + useResetUserFlow('/coinDashboard'); useBtcWalletData(); + + const handleGoBack = () => broadcastResetUserFlow(); + useTrackMixPanelPageViewed( protocol ? { @@ -133,8 +190,17 @@ export default function CoinDashboard() { : {}, ); - const handleBack = () => { - navigate(-1); + const openOptionsDialog = (event: React.MouseEvent) => { + setShowOptionsDialog(true); + + setOptionsDialogIndents({ + top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, + left: `calc(100% - ${SPAM_OPTIONS_WIDTH}px)`, + }); + }; + + const closeOptionsDialog = () => { + setShowOptionsDialog(false); }; const openContractDeployment = () => @@ -152,9 +218,50 @@ export default function CoinDashboard() { return ( <> - + + {showOptionsDialog && ( + + { + if (!selectedFt) { + handleGoBack(); + return; + } + // set the visibility to false + const payload = { + principal: selectedFt.principal, + isEnabled: false, + }; + if (protocol === 'runes') { + dispatch(setRunesManageTokensAction(payload)); + } else if (protocol === 'stacks') { + dispatch(setSip10ManageTokensAction(payload)); + } else if (protocol === 'brc-20') { + dispatch(setBrc20ManageTokensAction(payload)); + } + + addToSpamTokens(selectedFt.principal); + dispatch(setSpamTokenAction(selectedFt)); + + handleGoBack(); + }} + > + + + {t('HIDE_AND_REPORT')} + + + + )} - + {protocol === 'stacks' && ( diff --git a/src/app/screens/settings/connectedAppsAndPermissions/README.md b/src/app/screens/settings/connectedAppsAndPermissions/README.md new file mode 100644 index 000000000..6db6f27d0 --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/README.md @@ -0,0 +1,8 @@ +Queries are being persisted using `@tanstack/react-query-persist-client`. The key files involved are, + +- [`src/pages/Popup/index.tsx`](/src/pages/Popup/index.tsx) +- [`src/app/utils/query.ts`](/src/app/utils/query.ts) + +When restoring persited data, more advanced data types like maps and sets are incorrectly restored as empty objects. The permissions store uses maps and sets, causing the Connected Apps and Permissions screen to break when using React Query's restored data. + +To avoid errors, the component should not use stale data from React Query, which can be avoided with an `isFetching` check while the query function runs. The data returned by the query function is provided by permissions-related utitlites that return data in the expected shape. diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts new file mode 100644 index 000000000..0cd594b60 --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts @@ -0,0 +1,43 @@ +/* eslint-disable import/prefer-default-export */ + +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginTop: props.theme.spacing(20), + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + +export const ClientHeader = styled('div')({ + display: 'flex', + justifyContent: 'space-between', +}); + +export const ClientName = styled('div')({ + fontWeight: 'bold', +}); + +export const Row = styled('div')({ + paddingLeft: '10px', +}); + +export const PermissionContainer = styled('div')({ + display: 'flex', + flexDirection: 'column', +}); + +export const PermissionTitle = styled('div')({ + fontWeight: 'bold', +}); + +export const PermissionDescription = styled('div')({ + paddingLeft: '10px', +}); + +export const Button = styled('button')({ + borderRadius: '4px', + padding: '0.2em 0.5em', +}); diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx new file mode 100644 index 000000000..60bef473d --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx @@ -0,0 +1,83 @@ +import { usePermissionsStore, usePermissionsUtils } from '@components/permissionsManager'; +import * as utils from '@components/permissionsManager/utils'; +import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Button, + ClientHeader, + ClientName, + Container, + PermissionContainer, + PermissionDescription, + PermissionTitle, + Row, +} from './index.styles'; + +function ConnectedAppsAndPermissionsScreen() { + const navigate = useNavigate(); + const { removeClient } = usePermissionsUtils(); + const { store } = usePermissionsStore(); + + const handleBackButtonClick = useCallback(() => { + navigate('/settings'); + }, [navigate]); + + if (!store) { + return null; + } + + return ( + <> + + + {[...store.clients].map((client) => ( +
+ + {client.name} + + + {utils + .getClientPermissions(store.permissions, client.id) + .sort((p1, p2) => p1.resourceId.localeCompare(p2.resourceId)) + .map((p) => ( +
+ {(() => { + const resource = utils.getResource(store.resources, p.resourceId); + + if (!resource) { + return null; + } + + return ( + + + {resource.name} + + {[...p.actions].map((a) => ( +
{a}
+ ))} +
+
+
+ ); + })()} +
+ ))} +
+ ))} +
+ + + ); +} + +export default ConnectedAppsAndPermissionsScreen; diff --git a/src/app/screens/settings/fiatCurrency/currencyRow.tsx b/src/app/screens/settings/fiatCurrency/currencyRow.tsx index a30542240..8bbfde1a3 100644 --- a/src/app/screens/settings/fiatCurrency/currencyRow.tsx +++ b/src/app/screens/settings/fiatCurrency/currencyRow.tsx @@ -3,33 +3,34 @@ import type { SupportedCurrency } from '@secretkeylabs/xverse-core'; import { Currency } from '@utils/currency'; import styled, { useTheme } from 'styled-components'; -interface TitleProps { - color: string; -} - -interface ButtonProps { - border: string; -} - -const Button = styled.button((props) => ({ +const Button = styled.button<{ + $border: string; + $color: string; +}>((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.$color, display: 'flex', - flexDirection: 'row', alignItems: 'center', - background: 'transparent', - justifyContent: 'flex-start', - paddingBottom: props.theme.spacing(10), - paddingTop: props.theme.spacing(10), - borderBottom: props.border, + justifyContent: 'space-between', + padding: `${props.theme.space.m} 0`, + backgroundColor: 'transparent', + borderBottom: props.$border, + transition: 'color 0.1s ease', + '&:hover': { + color: props.theme.colors.white_200, + }, })); -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.color, - flex: 1, - textAlign: 'left', - marginLeft: props.theme.spacing(6), +const CurrencyWrapper = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + columnGap: props.theme.space.s, })); +const StyledImg = styled.img({ + width: 21, +}); + interface Props { currency: Currency; isSelected: boolean; @@ -42,16 +43,18 @@ function CurrencyRow({ currency, isSelected, onCurrencySelected, showDivider }: const onClick = () => { onCurrencySelected(currency.name); }; + return ( ); diff --git a/src/app/screens/settings/fiatCurrency/index.tsx b/src/app/screens/settings/fiatCurrency/index.tsx index ba62981f8..b2dfd1e98 100644 --- a/src/app/screens/settings/fiatCurrency/index.tsx +++ b/src/app/screens/settings/fiatCurrency/index.tsx @@ -37,8 +37,9 @@ function FiatCurrencyScreen() { navigate('/settings'); }; - const onClick = (currency: SupportedCurrency) => { + const handleCurrencyClick = (currency: SupportedCurrency) => { dispatch(ChangeFiatCurrencyAction(currency)); + navigate(-1); }; function showDivider(index: number): boolean { @@ -53,7 +54,7 @@ function FiatCurrencyScreen() { diff --git a/src/app/screens/settings/index.tsx b/src/app/screens/settings/index.tsx index 8576c24eb..e632f9ecf 100644 --- a/src/app/screens/settings/index.tsx +++ b/src/app/screens/settings/index.tsx @@ -1,10 +1,12 @@ import ArrowSquareOut from '@assets/img/arrow_square_out.svg'; import XverseLogo from '@assets/img/full_logo_horizontal.svg'; import ArrowIcon from '@assets/img/settings/arrow.svg'; +import RequestsRoutes from '@common/utils/route-urls'; import PasswordInput from '@components/passwordInput'; import BottomBar from '@components/tabBar'; import useChromeLocalStorage from '@hooks/useChromeLocalStorage'; import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { @@ -15,6 +17,7 @@ import { import { chromeLocalStorageKeys } from '@utils/chromeLocalStorage'; import { PRIVACY_POLICY_LINK, SUPPORT_LINK, TERMS_LINK } from '@utils/constants'; import { getLockCountdownLabel, isInOptions, isLedgerAccount } from '@utils/helper'; +import RoutePaths from 'app/routes/paths'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -68,8 +71,8 @@ function Setting() { hasActivatedOrdinalsKey, hasActivatedRareSatsKey, hasActivatedRBFKey, - selectedAccount, } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); const [isPriorityWallet, setIsPriorityWallet] = useChromeLocalStorage( chromeLocalStorageKeys.isPriorityWallet, true, @@ -107,6 +110,10 @@ function Setting() { navigate('/backup-wallet'); }; + const openConnectedAppsAndPermissionsScreen = () => { + navigate(RoutePaths.ConnectedAppsAndPermissions); + }; + const switchIsPriorityWallet = () => { setIsPriorityWallet(!isPriorityWallet); }; @@ -228,6 +235,14 @@ function Setting() { icon={ArrowIcon} showDivider /> + {process.env.NODE_ENV !== 'production' && ( + + )} >; function SignBatchPsbtRequest() { - const { btcAddress, ordinalsAddress, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { network } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { payload, confirmSignPsbt, cancelSignPsbt, requestToken } = useSignBatchPsbtTx(); @@ -145,7 +155,8 @@ function SignBatchPsbtRequest() { const [inscriptionToShow, setInscriptionToShow] = useState< btcTransaction.IOInscription | undefined >(undefined); - const hasRunesSupport = useHasFeature('RUNES_SUPPORT'); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); + useTrackMixPanelPageViewed(); const [parsedPsbts, setParsedPsbts] = useState< { summary: PsbtSummary; runeSummary: RuneSummary | undefined }[] @@ -190,7 +201,10 @@ function SignBatchPsbtRequest() { }, [payload.psbts.length, handlePsbtParsing]); const checkAddressMismatch = (input) => { - if (input.address !== btcAddress && input.address !== ordinalsAddress) { + if ( + input.address !== selectedAccount.btcAddress && + input.address !== selectedAccount.ordinalsAddress + ) { navigate('/tx-status', { state: { txid: '', @@ -247,6 +261,13 @@ function SignBatchPsbtRequest() { } } + trackMixPanel(AnalyticsEvents.TransactionConfirmed, { + protocol: 'bitcoin', + action: 'sign-psbt', + wallet_type: selectedAccount.accountType || 'software', + batch: payload.psbts.length, + }); + setIsSigningComplete(true); setIsSigning(false); @@ -295,8 +316,8 @@ function SignBatchPsbtRequest() { getNetAmount({ inputs: psbt.summary.inputs, outputs: psbt.summary.outputs, - btcAddress, - ordinalsAddress, + btcAddress: selectedAccount.btcAddress, + ordinalsAddress: selectedAccount.ordinalsAddress, }), ), ) diff --git a/src/app/screens/signMessageRequest/index.styled.ts b/src/app/screens/signMessageRequest/index.styled.ts new file mode 100644 index 000000000..2b2b349ad --- /dev/null +++ b/src/app/screens/signMessageRequest/index.styled.ts @@ -0,0 +1,80 @@ +import styled from 'styled-components'; + +export const MainContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + padding: `0 ${props.theme.space.m}`, +})); + +export const RequestType = styled.h1((props) => ({ + ...props.theme.typography.headline_s, + marginTop: props.theme.space.s, + color: props.theme.colors.white_0, + textAlign: 'left', + marginBottom: props.theme.space.l, +})); + +export const MessageHash = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + lineHeight: 1.6, + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddressContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.s} ${props.theme.space.m}`, + marginBottom: props.theme.space.s, + flex: 1, +})); + +export const SigningAddressTitle = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + wordWrap: 'break-word', + color: props.theme.colors.white_200, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddress = styled.div({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +export const SigningAddressType = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const SigningAddressValue = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + wordWrap: 'break-word', + color: props.theme.colors.white_0, + marginBottom: props.theme.space.xs, +})); + +export const ActionDisclaimer = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_400, + marginTop: props.theme.space.xs, + marginBottom: props.theme.space.m, +})); + +export const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + padding: `0 ${props.theme.space.m}`, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); diff --git a/src/app/screens/signMessageRequest/index.tsx b/src/app/screens/signMessageRequest/index.tsx index 6c37e3e36..a6c800aac 100644 --- a/src/app/screens/signMessageRequest/index.tsx +++ b/src/app/screens/signMessageRequest/index.tsx @@ -9,8 +9,10 @@ import ConfirmScreen from '@components/confirmScreen'; import InfoContainer from '@components/infoContainer'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import RequestError from '@components/requests/requestError'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; +import { Return, RpcErrorCode } from '@sats-connect/core'; import CollapsableContainer from '@screens/signatureRequest/collapsableContainer'; import SignatureRequestMessage from '@screens/signatureRequest/signatureRequestMessage'; import { finalizeMessageSignature } from '@screens/signatureRequest/utils'; @@ -20,94 +22,24 @@ import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; import { handleBip322LedgerMessageSigning } from '@utils/ledger'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Return, RpcErrorCode } from 'sats-connect'; -import styled from 'styled-components'; +import { + ActionDisclaimer, + MainContainer, + MessageHash, + RequestType, + SigningAddress, + SigningAddressContainer, + SigningAddressTitle, + SigningAddressType, + SigningAddressValue, + SuccessActionsContainer, +} from './index.styled'; import { useSignMessageRequest, useSignMessageValidation } from './useSignMessageRequest'; -const MainContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const RequestType = styled.h1((props) => ({ - ...props.theme.typography.headline_s, - marginTop: props.theme.spacing(11), - color: props.theme.colors.white_0, - textAlign: 'left', - marginBottom: props.theme.spacing(12), -})); - -const MessageHash = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - lineHeight: 1.6, - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddressContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '12px 16px', - marginBottom: props.theme.spacing(6), - flex: 1, -})); - -const SigningAddressTitle = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - wordWrap: 'break-word', - color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddress = styled.div({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', -}); - -const SigningAddressType = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const SigningAddressValue = styled.p((props) => ({ - ...props.theme.typography.body_medium_m, - textAlign: 'left', - wordWrap: 'break-word', - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(4), -})); - -const ActionDisclaimer = styled.p((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_400, - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(8), -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); - function SignMessageRequest() { const { t } = useTranslation('translation'); - const { accountsList, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { accountsList, network } = useWalletSelector(); const { payload, tabId, requestToken, confirmSignMessage, requestId } = useSignMessageRequest(); const { validationError } = useSignMessageValidation(payload); @@ -282,7 +214,11 @@ function SignMessageRequest() { setCurrentStepIndex(0); }; - return !validationError ? ( + if (validationError) { + return ; + } + + return ( <>
- ) : ( - ); } diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index e5bb984d6..b6d45ef34 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -1,13 +1,14 @@ import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; +import { BitcoinNetworkType, SignMessageOptions, SignMessagePayload } from '@sats-connect/core'; import { SettingsNetwork, signBip322Message } from '@secretkeylabs/xverse-core'; import { isHardwareAccount } from '@utils/helper'; import { decodeToken } from 'jsontokens'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { BitcoinNetworkType, SignMessageOptions, SignMessagePayload } from 'sats-connect'; const useSignMessageRequestParams = (network: SettingsNetwork) => { const { search } = useLocation(); @@ -30,14 +31,9 @@ const useSignMessageRequestParams = (network: SettingsNetwork) => { const rpcPayload: SignMessagePayload = { message, address, - network: - network.type === 'Mainnet' - ? { - type: BitcoinNetworkType.Mainnet, - } - : { - type: BitcoinNetworkType.Testnet, - }, + network: { + type: BitcoinNetworkType[network.type], + }, }; return { payload: rpcPayload, @@ -56,7 +52,8 @@ type ValidationError = { export const useSignMessageValidation = (requestPayload: SignMessagePayload | undefined) => { const [validationError, setValidationError] = useState(null); const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); - const { accountsList, selectedAccount, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const { accountsList, network } = useWalletSelector(); const { switchAccount } = useWalletReducer(); const checkAddressAvailability = () => { @@ -128,18 +125,3 @@ export const useSignMessageRequest = () => { confirmSignMessage, }; }; - -export function useSignBip322Message(message: string, address: string) { - const { accountsList, network } = useWalletSelector(); - const { getSeed } = useSeedVault(); - return useCallback(async () => { - const seedPhrase = await getSeed(); - return signBip322Message({ - accounts: accountsList, - message, - signatureAddress: address, - seedPhrase, - network: network.type, - }); - }, []); -} diff --git a/src/app/screens/signMessageRequestInApp/index.tsx b/src/app/screens/signMessageRequestInApp/index.tsx new file mode 100644 index 000000000..b8fa2ffbc --- /dev/null +++ b/src/app/screens/signMessageRequestInApp/index.tsx @@ -0,0 +1,294 @@ +import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; +import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; +import { delay } from '@common/utils/ledger'; +import ConfirmScreen from '@components/confirmScreen'; +import InfoContainer from '@components/infoContainer'; +import LedgerConnectionView from '@components/ledger/connectLedgerView'; +import TopRow from '@components/topRow'; +import useRunesApi from '@hooks/apiClients/useRunesApi'; +import useSeedVault from '@hooks/useSeedVault'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import Transport from '@ledgerhq/hw-transport-webusb'; +import CollapsableContainer from '@screens/signatureRequest/collapsableContainer'; +import SignatureRequestMessage from '@screens/signatureRequest/signatureRequestMessage'; +import { bip0322Hash, signBip322Message } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import Sheet from '@ui-library/sheet'; +import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; +import { handleBip322LedgerMessageSigning } from '@utils/ledger'; +import { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + ActionDisclaimer, + MainContainer, + MessageHash, + RequestType, + SigningAddress, + SigningAddressContainer, + SigningAddressTitle, + SigningAddressType, + SigningAddressValue, + SuccessActionsContainer, +} from '../signMessageRequest/index.styled'; + +function SignMessageRequestInApp() { + const { t } = useTranslation('translation'); + const { accountsList, network } = useWalletSelector(); + const selectedAccount = useSelectedAccount(); + const location = useLocation(); + const { payload } = location.state?.requestPayload || {}; + const navigate = useNavigate(); + const { getSeed } = useSeedVault(); + const runesApi = useRunesApi(); + + const [addressType, setAddressType] = useState(''); + const [isSigning, setIsSigning] = useState(false); + + // Ledger state + const [isModalVisible, setIsModalVisible] = useState(false); + const [isConnectSuccess, setIsConnectSuccess] = useState(false); + const [isConnectFailed, setIsConnectFailed] = useState(false); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [isTxRejected, setIsTxRejected] = useState(false); + const [isTxInvalid, setIsTxInvalid] = useState(false); + + useEffect(() => { + const checkAddressAvailability = () => { + const account = accountsList.filter((acc) => { + if (acc.btcAddress === payload.address) { + setAddressType(t('SIGNATURE_REQUEST.SIGNING_ADDRESS_SEGWIT')); + return true; + } + if (acc.ordinalsAddress === payload?.address) { + setAddressType(t('SIGNATURE_REQUEST.SIGNING_ADDRESS_TAPROOT')); + return true; + } + return false; + }); + return isHardwareAccount(selectedAccount) ? account[0] || selectedAccount : account[0]; + }; + checkAddressAvailability(); + }, [accountsList, payload, selectedAccount, t]); + + const getConfirmationError = (type: 'title' | 'subtitle') => { + if (type === 'title') { + if (isTxRejected) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.DENIED.ERROR_TITLE'); + } + + if (isTxInvalid) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.INVALID.ERROR_TITLE'); + } + + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.ERROR_TITLE'); + } + + if (isTxRejected) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.DENIED.ERROR_SUBTITLE'); + } + + if (isTxInvalid) { + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.INVALID.ERROR_SUBTITLE'); + } + + return t('SIGNATURE_REQUEST.LEDGER.CONFIRM.ERROR_SUBTITLE'); + }; + + const handleCancelClick = () => { + navigate(`/coinDashboard/FT?ftKey=${payload.selectedRuneId}&protocol=runes`); + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsTxInvalid(false); + setIsConnectFailed(false); + setIsConnectSuccess(false); + setCurrentStepIndex(0); + }; + + const handleGoBack = () => navigate(-1); + + const handleConnectAndConfirm = async () => { + if (!selectedAccount) { + return; + } + setIsButtonDisabled(true); + + const transport = await Transport.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + setCurrentStepIndex(1); + + try { + const bip322signature = await handleBip322LedgerMessageSigning({ + transport, + addressIndex: selectedAccount.deviceAccountIndex, + address: payload.address, + networkType: network.type, + message: payload.message, + }); + + await runesApi.submitCancelRunesSellOrder({ + orderIds: payload.orderIds, + makerPublicKey: selectedAccount?.ordinalsPublicKey!, + makerAddress: selectedAccount?.ordinalsAddress!, + token: payload.token, + signature: bip322signature, + }); + + handleGoBack(); + toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + } catch (e: any) { + if (e.name === 'LockedDeviceError') { + setCurrentStepIndex(0); + setIsConnectSuccess(false); + setIsConnectFailed(true); + } else if (e.statusCode === 28160) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + } else if (e.cause === 27012) { + setIsTxInvalid(true); + } else { + setIsTxRejected(true); + } + } finally { + await transport.close(); + setIsButtonDisabled(false); + } + }; + + const confirmSignMessage = async () => { + const seedPhrase = await getSeed(); + return signBip322Message({ + accounts: accountsList, + message: payload.message, + signatureAddress: payload.address, + seedPhrase, + network: network.type, + }); + }; + + const confirmCallback = async () => { + if (!payload) return; + try { + setIsSigning(true); + if (isHardwareAccount(selectedAccount)) { + setIsModalVisible(true); + return; + } + const bip322signature = await confirmSignMessage(); + + await runesApi.submitCancelRunesSellOrder({ + orderIds: payload.orderIds, + makerPublicKey: selectedAccount?.ordinalsPublicKey!, + makerAddress: selectedAccount?.ordinalsAddress!, + token: payload.token, + signature: bip322signature, + }); + + handleGoBack(); + toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + } catch (err) { + toast(`${t('SIGNATURE_REQUEST.UNLISTED_ERROR')}`); + } finally { + setIsSigning(false); + } + }; + + return ( + <> + + + + {t('SIGNATURE_REQUEST.TITLE')} + + + {bip0322Hash(payload.message)} + + + + {t('SIGNATURE_REQUEST.SIGNING_ADDRESS_TITLE')} + + + {addressType && {addressType}} + + {getTruncatedAddress(payload.address, 6)} + + + + {t('SIGNATURE_REQUEST.ACTION_DISCLAIMER')} + + + + setIsModalVisible(false)}> + {currentStepIndex === 0 && ( + + )} + {currentStepIndex === 1 && ( + + )} + + + ); @@ -226,7 +229,7 @@ function TransactionStatus() { {t('TRANSACTION_ID')} - {txid} + {txid} @@ -234,8 +237,30 @@ function TransactionStatus() { ); + if (runeListed) { + return ( + + {renderTransactionSuccessStatus} + +