diff --git a/.env.example b/.env.example index 3e46d77c8..25817e64a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Behavior +SKIP_ANIMATION_WALLET_STARTUP=false; + # Providers TRANSAC_API_KEY= MOON_PAY_API_KEY= diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0aa70c0a1..504fed181 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,5 @@ # 🔘 PR Type + What kind of change does this PR introduce? @@ -13,22 +14,44 @@ What kind of change does this PR introduce? - [ ] Other... Please describe: # 📜 Background + Provide a brief explanation of why this pull request is needed. Include the problem you are solving or the functionality you are adding. Reference any related issues. Issue Link: #[issue_number] Context Link (if applicable): # 🔄 Changes + Enumerate the changes made in this pull request, detailing what has been modified, added, or removed. Include technical details and implications if necessary. Impact: + - Explain the broader impact of these changes. - How it improves performance, fixes bugs, adds functionality, etc. +# 🧪 E2E Test Result + +Include a screenshot of the e2e test result. + +`Run E2E Tests` +Our End-to-end (E2E) test suite is build with Playwright. To run the whole E2E test suite, run: +`npm run e2etest` + +If you only want to run the smoke test suite, run +`npm run e2etest:smoketest` + +If you want to run the e2e test in UI Mode: +`npm run e2etest:ui` + +To generate test report, run: +`npm run e2etest:report` + # 🖼 Screenshot / 📹 Video + Include screenshots or a video demonstrating the changes. This is especially helpful for UI changes. # ✅ Review checklist + Please ensure the following are true before merging: - [ ] Code Style is consistent with the project guidelines. diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index b2a30ffc1..e87401195 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -34,13 +34,14 @@ jobs: npm run knip npx eslint . npx tsc --noEmit - npm test - name: Build env: TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} MOON_PAY_API_KEY: ${{ secrets.MOON_PAY_API_KEY }} MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} MIX_PANEL_EXPLORE_APP_TOKEN: ${{ secrets.MIX_PANEL_EXPLORE_APP_TOKEN }} + SKIP_ANIMATION_WALLET_STARTUP: 'false' + run: npm run build - name: Upload Archive uses: actions/upload-artifact@v3 @@ -131,7 +132,7 @@ jobs: publish-rc: # TODO also keep the develop PR description up to date if: ${{ github.base_ref == 'main' }} - needs: [UItest] + needs: build runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a706c7343..11573eef4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,13 +31,13 @@ jobs: npm run knip npx eslint . npx tsc --noEmit - npm test - name: Build env: TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} MOON_PAY_API_KEY: ${{ secrets.MOON_PAY_API_KEY }} MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} MIX_PANEL_EXPLORE_APP_TOKEN: ${{ secrets.MIX_PANEL_EXPLORE_APP_TOKEN }} + SKIP_ANIMATION_WALLET_STARTUP: 'false' run: npm run build --if-present - name: Install Playwright Browsers run: npx playwright install chromium --with-deps diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index d2a026cd0..d8e184658 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -35,6 +35,7 @@ jobs: MOON_PAY_API_KEY: ${{ secrets.MOON_PAY_API_KEY }} MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} MIX_PANEL_EXPLORE_APP_TOKEN: ${{ secrets.MIX_PANEL_EXPLORE_APP_TOKEN }} + SKIP_ANIMATION_WALLET_STARTUP: 'true' run: npm run build --if-present - name: Upload Archive uses: actions/upload-artifact@v3 diff --git a/README.md b/README.md index 1bca9fcc6..3237ba9bc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### Procedures -1. Check if your [Node.js](https://nodejs.org/) version is >= **14**. +1. Check if your [Node.js](https://nodejs.org/) version is >= **18**. 2. Clone this repository. 3. Make sure you're logged in to the @secretkeylabs scope on the GitHub NPM package registry. See the [Guide](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token) 1. Create a GitHub personal access token (classic) @@ -32,30 +32,3 @@ make or pull your local changes to xverse-core, then: cd ../xverse-core && npm i && npm run build:esm && \ cd $OLDPWD && npm i --legacy-peer-deps @secretkeylabs/xverse-core@../xverse-core && npm start ``` - -### Run E2E Tests - -Our End-to-end (E2E) test suite is build with Playwright. -To run the whole E2E test suite, run: - -``` -npm run e2etest -``` - -If you only want to run the smoke test suite, run - -``` -npm run e2etest:smoketest -``` - -If you want to run the e2e test in UI Mode: - -``` -npm run e2etest:ui -``` - -To generate test report, run: - -``` -npm run e2etest:report -``` diff --git a/package-lock.json b/package-lock.json index adadede5e..801e726ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,32 +1,33 @@ { "name": "xverse-web-extension", - "version": "0.40.0", + "version": "0.44.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.40.0", + "version": "0.44.3", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", - "@phosphor-icons/react": "^2.0.10", - "@playwright/test": "1.45.2", + "@noble/hashes": "^1.5.0", + "@phosphor-icons/react": "^2.1.0", + "@playwright/test": "1.46.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.2.1", + "@sats-connect/core": "0.3.0", + "@scure/base": "^1.1.9", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.0", + "@secretkeylabs/xverse-core": "27.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", - "@stacks/transactions": "6.13.1", + "@stacks/transactions": "6.16.1", "@tanstack/query-sync-storage-persister": "^4.29.1", "@tanstack/react-query": "^4.29.3", "@tanstack/react-query-devtools": "^4.29.3", "@tanstack/react-query-persist-client": "^4.29.3", - "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", "async-mutex": "^0.5.0", - "axios": "1.7.0", + "axios": "1.7.7", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", "classnames": "^2.3.2", @@ -54,7 +55,6 @@ "react-qr-code": "^2.0.8", "react-redux": "^7.2.1", "react-router-dom": "^6.4.0", - "react-switch": "^7.0.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.4.0", "redux": "^4.0.5", @@ -101,7 +101,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.6.0", + "eslint-plugin-playwright": "^1.6.2", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -122,11 +122,8 @@ "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", - "typescript": "^5.5.3", + "typescript": "^5.5.4", "typescript-plugin-styled-components": "^3.0.0", - "vite": "5.3.2", - "vite-tsconfig-paths": "4.3.2", - "vitest": "0.34.6", "webpack": "^5.89.0", "webpack-dev-server": "^4.11.0" }, @@ -659,374 +656,6 @@ "vlq": "^0.2.1" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1165,18 +794,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -1324,7 +941,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/hashes": { + "node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", @@ -1335,6 +952,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/secp256k1": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", @@ -1382,9 +1010,9 @@ } }, "node_modules/@phosphor-icons/react": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.0.10.tgz", - "integrity": "sha512-q5ITPNFhmEiYZLZOvEhjo2phlfxoOmit7vE1tBYMxcMqnZX2vdbMw3deDE7wCegpBKM/q/p39BJmhhoPcjZyCg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", + "integrity": "sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==", "engines": { "node": ">=10" }, @@ -1394,12 +1022,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", - "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.45.2" + "playwright": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -1543,234 +1171,23 @@ "node": ">=14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sats-connect/core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.2.1.tgz", - "integrity": "sha512-e7ARFqN4JE8kBiSUNOcEvBffiBK9v7k+/PlwgYY5lGyanmBOMj5twBn/f/4+52GBLZXkwhJtidIWZ8LgnBdlbw==", - "license": "ISC", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.3.0.tgz", + "integrity": "sha512-4Qy4/Mm4lJrDLnEqzVjC8p3xcHnOcG0GzIOcnuVQa7SbdiAPSqYxAlFECwbSHhsxyiEoE7faC528B3AUHim1Ag==", "dependencies": { - "axios": "1.6.8", + "axios": "1.7.4", "bitcoin-address-validation": "2.2.3", "buffer": "6.0.3", "jsontokens": "4.0.1", - "lodash.omit": "4.5.0" - }, - "peerDependencies": { + "lodash.omit": "4.5.0", "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==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1779,43 +1196,35 @@ } }, "node_modules/@scure/base": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", - "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "dependencies": { - "@noble/hashes": "1.4.0" + "@noble/hashes": "1.5.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32/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" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1831,6 +1240,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "@noble/hashes": "~1.1.1", "@scure/base": "~1.1.0" @@ -1845,7 +1255,8 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@scure/btc-signer": { "version": "1.2.1", @@ -1861,10 +1272,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/btc-signer/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.7.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0/e3ab51151b17b6c49fade2a104bb3122f76a1e17", - "integrity": "sha512-hoNCtY7LNBL908Hmh8Yxpo0IPLGg6wnrUYsdVkxyvhn9+IsXZE2GtrafVn5bAibw02wbQdTUX0OUckyd1/ZW+Q==", + "version": "27.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/27.0.1/f7f818e03657662c070cac51861c7f9343b05e72", + "integrity": "sha512-EFKN1ZDxUmQv/fyVrHy3hKdQLCUkMPZjpdtsWayhdLr5VE9zgNE3Y8hfH9q+3KtZp3yiSKDMmSkX2vfAtzhLOg==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1874,18 +1296,20 @@ "@scure/bip32": "^1.4.0", "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", - "@stacks/auth": "6.13.1", + "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.13.1", - "@stacks/network": "6.13.0", - "@stacks/stacking": "6.13.2", - "@stacks/storage": "6.13.1", - "@stacks/transactions": "6.13.1", - "@stacks/wallet-sdk": "6.13.1", + "@stacks/encryption": "6.16.1", + "@stacks/network": "6.16.0", + "@stacks/stacking": "6.16.1", + "@stacks/stacks-blockchain-api-types": "^7.14.1", + "@stacks/storage": "6.16.1", + "@stacks/transactions": "6.16.1", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", - "axios": "1.7.0", + "axios": "1.7.7", + "axios-retry": "4.5.0", "base64url": "^3.0.1", "bip32": "^4.0.0", "bip39": "3.0.3", @@ -1917,24 +1341,13 @@ "react-dom": ">18.0.0" } }, - "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/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1961,6 +1374,11 @@ "@stencil/core": "^2.17.1" } }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -1985,12 +1403,6 @@ "randombytes": "^2.0.1" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -2033,22 +1445,24 @@ } }, "node_modules/@stacks/auth": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.13.1.tgz", - "integrity": "sha512-nr5VeLIJBVI72eWYs/oQ7nrmuZxe89AlS5Lt/WxCcUDK9Z+oqQR1qsxBo86yVDlmrxoNWncHWD38LX54HsOEzA==", - "dependencies": { - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.16.1.tgz", + "integrity": "sha512-6Co7VZTlfvWVKEYXh+x2TD1e+3XmCPumJut1jRgW6pYPGruZ4Dz/R4xfkf9qX3MEJabLe4AfasSfoqftg0twTg==", + "license": "MIT", + "dependencies": { + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, "node_modules/@stacks/common": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.13.0.tgz", - "integrity": "sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", + "license": "MIT", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -2084,14 +1498,15 @@ } }, "node_modules/@stacks/encryption": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.13.1.tgz", - "integrity": "sha512-y5IFX3/nGI3fCk70gE0JwH70GpshD8RhUfvhMLcL96oNaec1cCdj1ZUiQupeicfYTHuraaVBYU9xLls4TRmypg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.16.1.tgz", + "integrity": "sha512-DtVNNW/iipyxxRDz73S9DbLfRmBMqQCCog89F1Q1i6JUnl2kBB1PR9SPQfYv9zcAJ37oHoNB4i4b2tJWYr01vg==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -2108,50 +1523,54 @@ "type": "individual", "url": "https://paulmillr.com/funding/" } - ] + ], + "license": "MIT" }, "node_modules/@stacks/encryption/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.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@stacks/network": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.13.0.tgz", - "integrity": "sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", + "license": "MIT", "dependencies": { - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "node_modules/@stacks/profile": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.13.1.tgz", - "integrity": "sha512-GCDE13hwoUYZvZKTb5c0Tr74DcxIP/n4bffcYrKa5UabITPQ7JwsJIOyDoAwdtl3lu7fi9aBsKrdfHpBBUSQIQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.16.1.tgz", + "integrity": "sha512-FJcwKN6oVDfmwymivkZfm1PcO7gv5fcWN4HI+jtYdMftrl6yxAB4/XD63FSvshIeLPzBJjnCmaZSnPAV2mtdeA==", + "license": "MIT", "dependencies": { - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/transactions": "^6.13.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", + "@stacks/transactions": "^6.16.1", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, "node_modules/@stacks/stacking": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", - "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", "dependencies": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.13.1", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" } }, @@ -2188,27 +1607,28 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "node_modules/@stacks/storage": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", - "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", - "dependencies": { - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.1.tgz", + "integrity": "sha512-bElxB03dg3XGTX8r/J8Os2tIsodcTglg7zeq6RU4FVUU7tUA+Agpmn9FKl8MmnsWfO4ls5UgeujyBaxUJ//yAw==", + "dependencies": { + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, "node_modules/@stacks/transactions": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", - "integrity": "sha512-PWw2I+2Fj3CaFYQIoVcqQN6E2qGHNhFv03nuR0CxMq0sx8stPgYZbdzUlnlBcJQdsFiHrw3sPeqnXDZt+Hg5YQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", + "license": "MIT", "dependencies": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" } @@ -2225,19 +1645,19 @@ ] }, "node_modules/@stacks/wallet-sdk": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", - "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.16.1.tgz", + "integrity": "sha512-QYm/BfRpHZfD5FjvaOJp0hy8yeqde6qrEmSrcIl06IC2SNr4R5NOy0xK/RPUgJFB1/Gd16HbqS9EdyV3o6ybwg==", "dependencies": { "@scure/bip32": "1.1.3", "@scure/bip39": "1.1.0", - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", - "@stacks/storage": "^6.13.1", - "@stacks/transactions": "^6.13.1", + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", + "@stacks/storage": "^6.16.1", + "@stacks/transactions": "^6.16.1", "buffer": "^6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", @@ -2439,26 +1859,11 @@ }, "node_modules/@types/bonjour": { "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", - "dev": true - }, - "node_modules/@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", "dev": true, "dependencies": { - "@types/chai": "*" + "@types/node": "*" } }, "node_modules/@types/chrome": { @@ -3102,162 +2507,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "dependencies": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "dependencies": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "dependencies": { - "tinyspy": "^2.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -3465,9 +2714,9 @@ } }, "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz", + "integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -3536,15 +2785,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3622,21 +2862,6 @@ "ajv": "^6.9.1" } }, - "node_modules/alex-sdk": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.26.tgz", - "integrity": "sha512-uUjbONoAit6htxZGLOFev8v2h59kE31fM1X9efH0Yi1eLXYSSXojj+iFPTlQTQvIysyseXGxkX4VVTc9aQ13sg==", - "dependencies": { - "clarity-codegen": "^0.3.5" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@stacks/network": "*", - "@stacks/transactions": "*" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3680,6 +2905,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3915,15 +3141,6 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -3981,15 +3198,26 @@ } }, "node_modules/axios": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.0.tgz", - "integrity": "sha512-IiB0wQeKyPRdsFVhBgIo31FbzOyf2M6wYl7/NVutFwFBRMiAbjNiydJIHKeLmPugF4kJLfA1uWZ82Is2QzqqFA==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -4589,15 +3817,6 @@ "node": ">=8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4662,24 +3881,6 @@ } ] }, - "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4693,18 +3894,6 @@ "node": ">=4" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4762,29 +3951,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/clarity-codegen": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/clarity-codegen/-/clarity-codegen-0.3.6.tgz", - "integrity": "sha512-Be1J8RFtPFGfWtQ/7enl3xkU1j2KkIS9W7RbuofIVYfKp11MCZSGQeNV+Gh0dQOQIs3O0WQLLg03ikyNaWtYzQ==", - "dependencies": { - "@stacks/stacks-blockchain-api-types": "^7.1.10", - "axios": "^1.5.0", - "lodash": "^4.17.21", - "yargs": "^17.7.2", - "yqueue": "^1.0.1" - }, - "bin": { - "clarity-codegen": "lib/generate/cli.js" - }, - "peerDependencies": { - "@stacks/transactions": "*" - } - }, - "node_modules/clarity-codegen/node_modules/@stacks/stacks-blockchain-api-types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.9.0.tgz", - "integrity": "sha512-29tcIOxEj0jIAVrMLH2hDuTM0wWtOT4z9EnNgOCEQvipEDPyvoaPjbYJmQUTxzvVjfoLjZAQsvTlJw7fnbkkrg==" - }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -4863,45 +4029,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -5481,18 +4608,6 @@ } } }, - "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -5968,9 +5083,9 @@ "integrity": "sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA==" }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -6191,44 +5306,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6514,10 +5591,14 @@ } }, "node_modules/eslint-plugin-playwright": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", - "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.2.tgz", + "integrity": "sha512-mraN4Em3b5jLt01q7qWPyLg0Q5v3KAWfJSlEWwldyUXoa7DSPrBR4k6B6LROLqipsG8ndkwWMdjl1Ffdh15tag==", "dev": true, + "license": "MIT", + "workspaces": [ + "examples" + ], "dependencies": { "globals": "^13.23.0" }, @@ -7493,23 +6574,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -7636,12 +6700,6 @@ "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", @@ -8577,6 +7635,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", @@ -8864,12 +7933,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -9417,18 +8480,6 @@ "node": ">=8.9.0" } }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9620,15 +8671,6 @@ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -9647,18 +8689,6 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -9851,18 +8881,6 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, - "node_modules/mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, "node_modules/more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", @@ -9890,9 +8908,9 @@ } }, "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" }, "node_modules/nano-time": { "version": "1.0.0", @@ -10522,29 +9540,14 @@ "node_modules/path/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "node_modules/path/node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/path/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" } }, "node_modules/pbkdf2": { @@ -10620,24 +9623,13 @@ "node": ">=0.10.0" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/playwright": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", - "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.45.2" + "playwright-core": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -10650,9 +9642,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", - "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -11207,7 +10199,8 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true }, "node_modules/react-is-visible": { "version": "1.2.0", @@ -11368,18 +10361,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-switch": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", - "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-tabs": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", @@ -11523,14 +10504,6 @@ "strip-ansi": "^6.0.1" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -11655,41 +10628,6 @@ "node": ">=8" } }, - "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -11820,9 +10758,9 @@ } }, "node_modules/secp256k1": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", - "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", + "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -11830,7 +10768,7 @@ "bn.js": "^4.11.8", "create-hash": "^1.2.0", "drbg.js": "^1.0.1", - "elliptic": "^6.5.2", + "elliptic": "^6.5.7", "nan": "^2.14.0", "safe-buffer": "^5.1.2" }, @@ -12110,12 +11048,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -12236,12 +11168,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -12257,12 +11183,6 @@ "node": ">= 0.8" } }, - "node_modules/std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", - "dev": true - }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -12432,6 +11352,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12469,18 +11390,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -12693,30 +11602,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -12926,26 +11811,6 @@ "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", @@ -13106,15 +11971,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -13211,9 +12067,10 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13231,16 +12088,10 @@ "typescript": "~4.8 || 5" } }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -13394,6 +12245,7 @@ "version": "6.2.13", "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dependencies": { "@types/uuid": "8.3.4", "uuid": "8.3.2" @@ -13426,180 +12278,6 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "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", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, "node_modules/vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -14011,22 +12689,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", @@ -14065,6 +12727,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -14081,6 +12744,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -14095,114 +12759,40 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "peer": true - }, - "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "dev": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "color-name": "~1.1.4" }, "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/yargs/node_modules/emoji-regex": { + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/yargs/node_modules/string-width": { + "node_modules/wrap-ansi/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -14212,6 +12802,26 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "peer": true + }, + "node_modules/yaml": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -14224,11 +12834,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yqueue": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/yqueue/-/yqueue-1.0.1.tgz", - "integrity": "sha512-DBxJZBRafFLA/tCc5uO8ZTGFr+sQgn1FRJkZ4cVrIQIk6bv2bInraE3mbpLAJw9z93JGrLkqDoyTLrrZaCNq5w==" - }, "node_modules/zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", @@ -14609,167 +13214,6 @@ } } }, - "@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "dev": true, - "optional": true - }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -14870,15 +13314,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -15007,12 +13442,19 @@ "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "requires": { "@noble/hashes": "1.3.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==" }, "@noble/secp256k1": { "version": "1.7.1", @@ -15046,17 +13488,17 @@ } }, "@phosphor-icons/react": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.0.10.tgz", - "integrity": "sha512-q5ITPNFhmEiYZLZOvEhjo2phlfxoOmit7vE1tBYMxcMqnZX2vdbMw3deDE7wCegpBKM/q/p39BJmhhoPcjZyCg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", + "integrity": "sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==", "requires": {} }, "@playwright/test": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", - "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", "requires": { - "playwright": "1.45.2" + "playwright": "1.46.1" } }, "@pmmmwh/react-refresh-webpack-plugin": { @@ -15134,134 +13576,23 @@ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==" }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "dev": true, - "optional": true - }, "@sats-connect/core": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.2.1.tgz", - "integrity": "sha512-e7ARFqN4JE8kBiSUNOcEvBffiBK9v7k+/PlwgYY5lGyanmBOMj5twBn/f/4+52GBLZXkwhJtidIWZ8LgnBdlbw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.3.0.tgz", + "integrity": "sha512-4Qy4/Mm4lJrDLnEqzVjC8p3xcHnOcG0GzIOcnuVQa7SbdiAPSqYxAlFECwbSHhsxyiEoE7faC528B3AUHim1Ag==", "requires": { - "axios": "1.6.8", + "axios": "1.7.4", "bitcoin-address-validation": "2.2.3", "buffer": "6.0.3", "jsontokens": "4.0.1", - "lodash.omit": "4.5.0" + "lodash.omit": "4.5.0", + "valibot": "0.33.2" }, "dependencies": { "axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -15271,32 +13602,27 @@ } }, "@scure/base": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", - "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==" + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==" }, "@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.5.0.tgz", + "integrity": "sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==", "requires": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@noble/curves": "~1.6.0", + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.7" }, "dependencies": { "@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz", + "integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==", "requires": { - "@noble/hashes": "1.4.0" + "@noble/hashes": "1.5.0" } - }, - "@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" } } }, @@ -15325,12 +13651,19 @@ "@noble/hashes": "~1.3.3", "@scure/base": "~1.1.5", "micro-packed": "~0.5.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } } }, "@secretkeylabs/xverse-core": { - "version": "18.7.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.7.0/e3ab51151b17b6c49fade2a104bb3122f76a1e17", - "integrity": "sha512-hoNCtY7LNBL908Hmh8Yxpo0IPLGg6wnrUYsdVkxyvhn9+IsXZE2GtrafVn5bAibw02wbQdTUX0OUckyd1/ZW+Q==", + "version": "27.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/27.0.1/f7f818e03657662c070cac51861c7f9343b05e72", + "integrity": "sha512-EFKN1ZDxUmQv/fyVrHy3hKdQLCUkMPZjpdtsWayhdLr5VE9zgNE3Y8hfH9q+3KtZp3yiSKDMmSkX2vfAtzhLOg==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -15339,18 +13672,20 @@ "@scure/bip32": "^1.4.0", "@scure/bip39": "^1.3.0", "@scure/btc-signer": "1.2.1", - "@stacks/auth": "6.13.1", + "@stacks/auth": "6.16.1", "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.13.1", - "@stacks/network": "6.13.0", - "@stacks/stacking": "6.13.2", - "@stacks/storage": "6.13.1", - "@stacks/transactions": "6.13.1", - "@stacks/wallet-sdk": "6.13.1", + "@stacks/encryption": "6.16.1", + "@stacks/network": "6.16.0", + "@stacks/stacking": "6.16.1", + "@stacks/stacks-blockchain-api-types": "^7.14.1", + "@stacks/storage": "6.16.1", + "@stacks/transactions": "6.16.1", + "@stacks/wallet-sdk": "6.16.1", "@tanstack/react-query": "^4.29.3", "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", - "axios": "1.7.0", + "axios": "1.7.7", + "axios-retry": "4.5.0", "base64url": "^3.0.1", "bip32": "^4.0.0", "bip39": "3.0.3", @@ -15373,18 +13708,13 @@ "varuint-bitcoin": "^1.1.2" }, "dependencies": { - "@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" - }, "@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", "requires": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" } }, "@stacks/connect": { @@ -15408,6 +13738,11 @@ "@stencil/core": "^2.17.1" } }, + "@stacks/stacks-blockchain-api-types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.14.1.tgz", + "integrity": "sha512-65hvhXxC+EUqHJAQsqlBCqXB+zwfxZICSKYJugdg6BCp9I9qniyfz5XyQeC4RMVo0tgEoRdS/b5ZCFo5kLWmxA==" + }, "@types/node": { "version": "11.11.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", @@ -15434,12 +13769,6 @@ } } }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -15469,22 +13798,22 @@ } }, "@stacks/auth": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.13.1.tgz", - "integrity": "sha512-nr5VeLIJBVI72eWYs/oQ7nrmuZxe89AlS5Lt/WxCcUDK9Z+oqQR1qsxBo86yVDlmrxoNWncHWD38LX54HsOEzA==", - "requires": { - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.16.1.tgz", + "integrity": "sha512-6Co7VZTlfvWVKEYXh+x2TD1e+3XmCPumJut1jRgW6pYPGruZ4Dz/R4xfkf9qX3MEJabLe4AfasSfoqftg0twTg==", + "requires": { + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, "@stacks/common": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.13.0.tgz", - "integrity": "sha512-wwzyihjaSdmL6NxKvDeayy3dqM0L0Q2sawmdNtzJDi0FnXuJGm5PeapJj7bEfcI9XwI7Bw5jZoC6mCn9nc5YIw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.16.0.tgz", + "integrity": "sha512-PnzvhrdGRMVZvxTulitlYafSK4l02gPCBBoI9QEoTqgSnv62oaOXhYAUUkTMFKxdHW1seVEwZsrahuXiZPIAwg==", "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -15522,14 +13851,14 @@ } }, "@stacks/encryption": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.13.1.tgz", - "integrity": "sha512-y5IFX3/nGI3fCk70gE0JwH70GpshD8RhUfvhMLcL96oNaec1cCdj1ZUiQupeicfYTHuraaVBYU9xLls4TRmypg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.16.1.tgz", + "integrity": "sha512-DtVNNW/iipyxxRDz73S9DbLfRmBMqQCCog89F1Q1i6JUnl2kBB1PR9SPQfYv9zcAJ37oHoNB4i4b2tJWYr01vg==", "requires": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -15543,9 +13872,9 @@ "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" }, "@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.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.47.tgz", + "integrity": "sha512-1f7dB3BL/bpd9tnDJrrHb66Y+cVrhxSOTGorRNdHwYTUlTay3HuTDPKo9a/4vX9pMQkhYBcAbL4jQdNlhCFP9A==", "requires": { "undici-types": "~5.26.4" } @@ -15553,39 +13882,39 @@ } }, "@stacks/network": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.13.0.tgz", - "integrity": "sha512-Ss/Da4BNyPBBj1OieM981fJ7SkevKqLPkzoI1+Yo7cYR2df+0FipIN++Z4RfpJpc8ne60vgcx7nJZXQsiGhKBQ==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.16.0.tgz", + "integrity": "sha512-uqz9Nb6uf+SeyCKENJN+idt51HAfEeggQKrOMfGjpAeFgZV2CR66soB/ci9+OVQR/SURvasncAz2ScI1blfS8A==", "requires": { - "@stacks/common": "^6.13.0", + "@stacks/common": "^6.16.0", "cross-fetch": "^3.1.5" } }, "@stacks/profile": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.13.1.tgz", - "integrity": "sha512-GCDE13hwoUYZvZKTb5c0Tr74DcxIP/n4bffcYrKa5UabITPQ7JwsJIOyDoAwdtl3lu7fi9aBsKrdfHpBBUSQIQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.16.1.tgz", + "integrity": "sha512-FJcwKN6oVDfmwymivkZfm1PcO7gv5fcWN4HI+jtYdMftrl6yxAB4/XD63FSvshIeLPzBJjnCmaZSnPAV2mtdeA==", "requires": { - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", - "@stacks/transactions": "^6.13.1", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", + "@stacks/transactions": "^6.16.1", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, "@stacks/stacking": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", - "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.16.1.tgz", + "integrity": "sha512-Bv7TSyoMrb1wYOfKrPxwDQPSjsohyKCduN1HYMlKL9hHF0J8+MvlJFw9eIj1c2SEOyYaElT+Ly3CI56J/acVxw==", "requires": { "@noble/hashes": "1.1.5", "@scure/base": "1.1.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "@stacks/stacks-blockchain-api-types": "^0.61.0", - "@stacks/transactions": "^6.13.1", + "@stacks/transactions": "^6.16.1", "bs58": "^5.0.0" }, "dependencies": { @@ -15612,27 +13941,27 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "@stacks/storage": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", - "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", - "requires": { - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.16.1.tgz", + "integrity": "sha512-bElxB03dg3XGTX8r/J8Os2tIsodcTglg7zeq6RU4FVUU7tUA+Agpmn9FKl8MmnsWfO4ls5UgeujyBaxUJ//yAw==", + "requires": { + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, "@stacks/transactions": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", - "integrity": "sha512-PWw2I+2Fj3CaFYQIoVcqQN6E2qGHNhFv03nuR0CxMq0sx8stPgYZbdzUlnlBcJQdsFiHrw3sPeqnXDZt+Hg5YQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.16.1.tgz", + "integrity": "sha512-yCtUM+8IN0QJbnnlFhY1wBW7Q30Cxje3Zmy8DgqdBoM/EPPWadez/8wNWFANVAMyUZeQ9V/FY+8MAw4E+pCReA==", "requires": { "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", - "@stacks/common": "^6.13.0", - "@stacks/network": "^6.13.0", + "@stacks/common": "^6.16.0", + "@stacks/network": "^6.16.0", "c32check": "^2.0.0", "lodash.clonedeep": "^4.5.0" }, @@ -15645,19 +13974,19 @@ } }, "@stacks/wallet-sdk": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", - "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.16.1.tgz", + "integrity": "sha512-QYm/BfRpHZfD5FjvaOJp0hy8yeqde6qrEmSrcIl06IC2SNr4R5NOy0xK/RPUgJFB1/Gd16HbqS9EdyV3o6ybwg==", "requires": { "@scure/bip32": "1.1.3", "@scure/bip39": "1.1.0", - "@stacks/auth": "^6.13.1", - "@stacks/common": "^6.13.0", - "@stacks/encryption": "^6.13.1", - "@stacks/network": "^6.13.0", - "@stacks/profile": "^6.13.1", - "@stacks/storage": "^6.13.1", - "@stacks/transactions": "^6.13.1", + "@stacks/auth": "^6.16.1", + "@stacks/common": "^6.16.0", + "@stacks/encryption": "^6.16.1", + "@stacks/network": "^6.16.0", + "@stacks/profile": "^6.16.1", + "@stacks/storage": "^6.16.1", + "@stacks/transactions": "^6.16.1", "buffer": "6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", @@ -15792,21 +14121,6 @@ "@types/node": "*" } }, - "@types/chai": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", - "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", - "dev": true - }, - "@types/chai-subset": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", - "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, "@types/chrome": { "version": "0.0.237", "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.237.tgz", @@ -16302,154 +14616,40 @@ "semver": "^7.3.7" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "@vitest/expect": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", - "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", - "dev": true, - "requires": { - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "chai": "^4.3.10" - } - }, - "@vitest/runner": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", - "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", - "dev": true, - "requires": { - "@vitest/utils": "0.34.6", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" - }, - "dependencies": { - "p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "requires": { - "yocto-queue": "^1.0.0" - } - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true - } - } - }, - "@vitest/snapshot": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", - "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", - "dev": true, - "requires": { - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "lru-cache": "^6.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "@vitest/spy": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", - "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", - "dev": true, - "requires": { - "tinyspy": "^2.1.1" - } - }, - "@vitest/utils": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", - "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", + "@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "requires": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - } + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" } }, "@webassemblyjs/ast": { @@ -16659,9 +14859,9 @@ } }, "@types/node": { - "version": "18.19.42", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", - "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", + "version": "18.19.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz", + "integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==", "requires": { "undici-types": "~5.26.4" } @@ -16716,12 +14916,6 @@ "dev": true, "requires": {} }, - "acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "dev": true - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -16780,14 +14974,6 @@ "dev": true, "requires": {} }, - "alex-sdk": { - "version": "0.1.26", - "resolved": "https://registry.npmjs.org/alex-sdk/-/alex-sdk-0.1.26.tgz", - "integrity": "sha512-uUjbONoAit6htxZGLOFev8v2h59kE31fM1X9efH0Yi1eLXYSSXojj+iFPTlQTQvIysyseXGxkX4VVTc9aQ13sg==", - "requires": { - "clarity-codegen": "^0.3.5" - } - }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -16814,7 +15000,8 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -17000,12 +15187,6 @@ } } }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -17051,15 +15232,23 @@ "dev": true }, "axios": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.0.tgz", - "integrity": "sha512-IiB0wQeKyPRdsFVhBgIo31FbzOyf2M6wYl7/NVutFwFBRMiAbjNiydJIHKeLmPugF4kJLfA1uWZ82Is2QzqqFA==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, + "axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "requires": { + "is-retry-allowed": "^2.2.0" + } + }, "axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -17546,12 +15735,6 @@ "base-x": "^4.0.0" } }, - "cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true - }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -17590,21 +15773,6 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz", "integrity": "sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==" }, - "chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -17615,15 +15783,6 @@ "supports-color": "^5.3.0" } }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -17666,25 +15825,6 @@ "safe-buffer": "^5.0.1" } }, - "clarity-codegen": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/clarity-codegen/-/clarity-codegen-0.3.6.tgz", - "integrity": "sha512-Be1J8RFtPFGfWtQ/7enl3xkU1j2KkIS9W7RbuofIVYfKp11MCZSGQeNV+Gh0dQOQIs3O0WQLLg03ikyNaWtYzQ==", - "requires": { - "@stacks/stacks-blockchain-api-types": "^7.1.10", - "axios": "^1.5.0", - "lodash": "^4.17.21", - "yargs": "^17.7.2", - "yqueue": "^1.0.1" - }, - "dependencies": { - "@stacks/stacks-blockchain-api-types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.9.0.tgz", - "integrity": "sha512-29tcIOxEj0jIAVrMLH2hDuTM0wWtOT4z9EnNgOCEQvipEDPyvoaPjbYJmQUTxzvVjfoLjZAQsvTlJw7fnbkkrg==" - } - } - }, "classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -17741,38 +15881,6 @@ "string-width": "^5.0.0" } }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -18223,15 +16331,6 @@ "ms": "2.1.2" } }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-equal": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", @@ -18609,9 +16708,9 @@ "integrity": "sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA==" }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -18795,37 +16894,6 @@ "is-symbol": "^1.0.2" } }, - "esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -19139,9 +17207,9 @@ } }, "eslint-plugin-playwright": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", - "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.2.tgz", + "integrity": "sha512-mraN4Em3b5jLt01q7qWPyLg0Q5v3KAWfJSlEWwldyUXoa7DSPrBR4k6B6LROLqipsG8ndkwWMdjl1Ffdh15tag==", "dev": true, "requires": { "globals": "^13.23.0" @@ -19761,17 +17829,6 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "peer": true }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, "get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -19856,12 +17913,6 @@ "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", @@ -20509,6 +18560,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==" + }, "is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", @@ -20713,12 +18769,6 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, - "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -21102,12 +19152,6 @@ "json5": "^2.1.2" } }, - "local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -21261,15 +19305,6 @@ "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" }, - "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.1" - } - }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -21288,15 +19323,6 @@ "yallist": "^3.0.2" } }, - "magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -21446,18 +19472,6 @@ "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.47.0.tgz", "integrity": "sha512-Ldrva0fRBEIFWmEibBQO1PulfpJVF3pf28Guk09lDirDaSQqqU/xs9zQLwN2rL5VwVtsP1aD3JaCgaa98EjojQ==" }, - "mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "requires": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, "more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", @@ -21482,9 +19496,9 @@ } }, "nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" }, "nano-time": { "version": "1.0.0", @@ -21964,18 +19978,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -22025,24 +20027,13 @@ "pinkie": "^2.0.0" } }, - "pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "requires": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "playwright": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", - "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", "requires": { "fsevents": "2.3.2", - "playwright-core": "1.45.2" + "playwright-core": "1.46.1" }, "dependencies": { "fsevents": { @@ -22054,9 +20045,9 @@ } }, "playwright-core": { - "version": "1.45.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", - "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==" + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==" }, "postcss": { "version": "8.4.39", @@ -22427,7 +20418,8 @@ "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true }, "react-is-visible": { "version": "1.2.0", @@ -22530,14 +20522,6 @@ "react-router": "6.14.2" } }, - "react-switch": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", - "integrity": "sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA==", - "requires": { - "prop-types": "^15.7.2" - } - }, "react-tabs": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", @@ -22659,11 +20643,6 @@ "strip-ansi": "^6.0.1" } }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -22754,32 +20733,6 @@ "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==" }, - "rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", - "@types/estree": "1.0.5", - "fsevents": "~2.3.2" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -22865,16 +20818,16 @@ } }, "secp256k1": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", - "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", + "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", "requires": { "bindings": "^1.5.0", "bip66": "^1.1.5", "bn.js": "^4.11.8", "create-hash": "^1.2.0", "drbg.js": "^1.0.1", - "elliptic": "^6.5.2", + "elliptic": "^6.5.7", "nan": "^2.14.0", "safe-buffer": "^5.1.2" }, @@ -23117,12 +21070,6 @@ "object-inspect": "^1.9.0" } }, - "siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -23214,12 +21161,6 @@ "wbuf": "^1.7.3" } }, - "stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, "stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -23232,12 +21173,6 @@ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, - "std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", - "dev": true - }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23370,6 +21305,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -23392,15 +21328,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", - "dev": true, - "requires": { - "acorn": "^8.10.0" - } - }, "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", @@ -23538,24 +21465,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", - "dev": true - }, - "tinypool": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", - "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", - "dev": true - }, - "tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", - "dev": true - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -23714,13 +21623,6 @@ "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", @@ -23848,12 +21750,6 @@ "prelude-ls": "^1.2.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -23923,9 +21819,9 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==" + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==" }, "typescript-plugin-styled-components": { "version": "3.0.0", @@ -23934,16 +21830,10 @@ "dev": true, "requires": {} }, - "ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==" + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==" }, "unbox-primitive": { "version": "1.0.2", @@ -24083,75 +21973,6 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, - "vite": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", - "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", - "dev": true, - "requires": { - "esbuild": "^0.21.3", - "fsevents": "~2.3.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" - } - }, - "vite-node": { - "version": "0.34.6", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", - "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", - "dev": true, - "requires": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "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", - "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", - "dev": true, - "requires": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.34.6", - "@vitest/runner": "0.34.6", - "@vitest/snapshot": "0.34.6", - "@vitest/spy": "0.34.6", - "@vitest/utils": "0.34.6", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.10", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.1", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.7.0", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", - "vite-node": "0.34.6", - "why-is-node-running": "^2.2.2" - } - }, "vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -24446,16 +22267,6 @@ "has-tostringtag": "^1.0.0" } }, - "why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "requires": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - } - }, "wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", @@ -24496,6 +22307,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -24506,6 +22318,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -24514,6 +22327,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -24521,22 +22335,26 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -24550,11 +22368,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -24567,58 +22380,12 @@ "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "dev": true }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "dependencies": { - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - } - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true }, - "yqueue": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/yqueue/-/yqueue-1.0.1.tgz", - "integrity": "sha512-DBxJZBRafFLA/tCc5uO8ZTGFr+sQgn1FRJkZ4cVrIQIk6bv2bInraE3mbpLAJw9z93JGrLkqDoyTLrrZaCNq5w==" - }, "zod": { "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", diff --git a/package.json b/package.json index 974c9bc0b..59d72c8d7 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,22 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.40.0", + "version": "0.44.3", "private": true, "engines": { "node": "^18.18.2" }, "scripts": { - "build": "npm run clean && NODE_ENV=production npx node webpack/utils/build.js", + "build": "npm run clean && NODE_ENV=production node webpack/utils/build.js", "build-named": "npm run clean && NODE_ENV=production node scripts/build-named.js", - "start": "npx node webpack/utils/devServer.js", + "start": "node webpack/utils/devServer.js", "clean": "rimraf build", - "test": "vitest ./src", "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install", - "e2etest": "npx playwright test -g \"\" --grep-invert \"#localexecution\"", - "e2etest:ui": "npx playwright test --ui", - "e2etest:smoketest": "npx playwright test --grep \"#smoketest\"", - "e2etest:skipped": "npx playwright test --grep \"#localexecution\"", + "e2etest": "playwright test -g \"\" --grep-invert \"#localexecution\"", + "e2etest:ui": "playwright test --ui", + "e2etest:smoketest": "playwright test --grep \"#smoketest\"", + "e2etest:skipped": "playwright test --grep \"#localexecution\" --workers=1", "e2etest:report": "playwright show-report", "knip": "knip", "ts-check": "tsc --noEmit", @@ -39,23 +38,24 @@ "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", - "@phosphor-icons/react": "^2.0.10", - "@playwright/test": "1.45.2", + "@noble/hashes": "^1.5.0", + "@phosphor-icons/react": "^2.1.0", + "@playwright/test": "1.46.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.2.1", + "@sats-connect/core": "0.3.0", + "@scure/base": "^1.1.9", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.7.0", + "@secretkeylabs/xverse-core": "27.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", - "@stacks/transactions": "6.13.1", + "@stacks/transactions": "6.16.1", "@tanstack/query-sync-storage-persister": "^4.29.1", "@tanstack/react-query": "^4.29.3", "@tanstack/react-query-devtools": "^4.29.3", "@tanstack/react-query-persist-client": "^4.29.3", - "alex-sdk": "0.1.26", "argon2-browser": "^1.18.0", "async-mutex": "^0.5.0", - "axios": "1.7.0", + "axios": "1.7.7", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", "classnames": "^2.3.2", @@ -83,7 +83,6 @@ "react-qr-code": "^2.0.8", "react-redux": "^7.2.1", "react-router-dom": "^6.4.0", - "react-switch": "^7.0.0", "react-tabs": "^6.0.2", "react-tooltip": "^5.4.0", "redux": "^4.0.5", @@ -130,7 +129,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.6.0", + "eslint-plugin-playwright": "^1.6.2", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", @@ -151,11 +150,8 @@ "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", - "typescript": "^5.5.3", + "typescript": "^5.5.4", "typescript-plugin-styled-components": "^3.0.0", - "vite": "5.3.2", - "vite-tsconfig-paths": "4.3.2", - "vitest": "0.34.6", "webpack": "^5.89.0", "webpack-dev-server": "^4.11.0" } diff --git a/playwright.config.ts b/playwright.config.ts index 288391240..e7774b2c6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,10 +18,10 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 1 : 0, - /* 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. - */ + /* 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', diff --git a/scripts/find-tag.sh b/scripts/find-tag.sh index ade904106..d959711de 100755 --- a/scripts/find-tag.sh +++ b/scripts/find-tag.sh @@ -10,10 +10,17 @@ if [[ -z "$TAG" ]]; then exit 1 fi -if cat releases.json | jq '.[].tag_name' | grep $TAG; then +if cat releases.json | jq -r '.[].tag_name' | grep $TAG; then echo found releases matching $TAG - LATEST_TAG=$(cat releases.json | jq -r '.[].tag_name' | grep $TAG | head -1) - LATEST_RC=$(echo $LATEST_TAG | grep rc | sed 's/.*-rc.\(.*\)/\1/') + + for i in $(cat releases.json | jq -r '.[].tag_name' | grep $TAG); do + LATEST_RUNNING=$(echo $i | grep rc | sed 's/.*-rc.\(.*\)/\1/') + + if [[ -z "$LATEST_RC" || $LATEST_RUNNING -gt $LATEST_RC ]]; then + LATEST_RC=$LATEST_RUNNING + fi + done + if [[ -z "$LATEST_RC" ]]; then echo $TAG was already released exit 1; diff --git a/src/app/App.tsx b/src/app/App.tsx index 365944a72..a323fc46c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,12 +1,13 @@ import { PermissionsProvider } from '@components/permissionsManager'; import StartupLoadingScreen from '@components/startupLoadingScreen'; +import Toaster from '@components/toaster'; +import TooltipProvider from '@components/tooltip/provider'; import { CheckCircle, XCircle } from '@phosphor-icons/react'; import { setXClientVersion } from '@secretkeylabs/xverse-core'; import rootStore from '@stores/index'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { queryClient } from '@utils/query'; -import { Toaster } from 'react-hot-toast'; import { Provider } from 'react-redux'; import { RouterProvider } from 'react-router-dom'; import { PersistGate } from 'redux-persist/integration/react'; @@ -34,62 +35,65 @@ function App(): React.ReactNode { <> - - - - }> - - - - - - - ), - 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, + }, }, - }, - }} - /> - - - - - + }} + /> + + + + + + ); diff --git a/src/app/components/accountHeader/index.tsx b/src/app/components/accountHeader/index.tsx index 477418fee..fc4d95b9c 100644 --- a/src/app/components/accountHeader/index.tsx +++ b/src/app/components/accountHeader/index.tsx @@ -1,6 +1,4 @@ import AccountRow from '@components/accountRow'; -import PasswordInput from '@components/passwordInput'; -import ResetWalletPrompt from '@components/resetWallet'; import useWalletReducer from '@hooks/useWalletReducer'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,36 +6,18 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import OptionsDialog from '@components/optionsDialog/optionsDialog'; -import useSeedVault from '@hooks/useSeedVault'; 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) => ({ +const SelectedAccountContainer = styled.div<{ $showBorderBottom?: boolean }>((props) => ({ display: 'flex', flexDirection: 'row', position: 'relative', alignItems: 'center', justifyContent: 'space-between', - padding: `${props.theme.spacing(10)}px ${props.theme.spacing(8)}px`, - borderBottom: props.showBorderBottom - ? `0.5px solid ${props.theme.colors.background.elevation3}` - : 'none', -})); - -const ResetWalletContainer = styled.div((props) => ({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(25, 25, 48, 0.5)', - backdropFilter: 'blur(16px)', - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(30), + padding: `${props.theme.space.l} ${props.theme.space.m}`, + borderBottom: props.$showBorderBottom ? `0.5px solid ${props.theme.colors.elevation3}` : 'none', })); const OptionsButton = styled.button(() => ({ @@ -45,6 +25,14 @@ const OptionsButton = styled.button(() => ({ alignItems: 'center', justifyContent: 'flex-end', background: 'transparent', + opacity: 0.7, + transition: 'opacity 0.1s ease', + '&:hover': { + opacity: 1, + }, + '&:active': { + opacity: 0.6, + }, })); const ButtonRow = styled.button` @@ -56,7 +44,7 @@ const ButtonRow = styled.button` 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}; + font: ${(props) => props.theme.typography.body_medium_m}; color: ${(props) => props.theme.colors.white_0}; transition: background-color 0.2s ease; :hover { @@ -67,10 +55,6 @@ const ButtonRow = styled.button` } `; -const WarningButton = styled(ButtonRow)` - color: ${(props) => props.theme.colors.feedback.error}; -`; - type Props = { disableMenuOption?: boolean; disableAccountSwitch?: boolean; @@ -85,49 +69,15 @@ function AccountHeaderComponent({ const navigate = useNavigate(); const selectedAccount = useSelectedAccount(); - const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { t: optionsDialogTranslation } = useTranslation('translation', { keyPrefix: 'OPTIONS_DIALOG', }); const [showOptionsDialog, setShowOptionsDialog] = useState(false); - const [showResetWalletPrompt, setShowResetWalletPrompt] = useState(false); - const [showResetWalletDisplay, setShowResetWalletDisplay] = useState(false); - const [password, setPassword] = useState(''); - const { lockWallet, resetWallet } = useWalletReducer(); - const { unlockVault } = useSeedVault(); - const [error, setError] = useState(''); + const { lockWallet } = useWalletReducer(); const [optionsDialogIndents, setOptionsDialogIndents] = useState< { top: string; left: string } | undefined >(); - const handlePasswordNextClick = async () => { - try { - await unlockVault(password); - setPassword(''); - setError(''); - await resetWallet(); - } catch (e) { - setError(t('INCORRECT_PASSWORD_ERROR')); - } - }; - - const onGoBack = () => { - navigate(0); - }; - - const onResetWalletPromptClose = () => { - setShowResetWalletPrompt(false); - }; - - const handleResetWalletPromptOpen = () => { - setShowResetWalletPrompt(true); - }; - - const openResetWalletScreen = () => { - setShowResetWalletPrompt(false); - setShowResetWalletDisplay(true); - }; - const handleAccountSelect = () => { if (!disableAccountSwitch) { navigate('/account-list'); @@ -152,54 +102,27 @@ function AccountHeaderComponent({ }; return ( - <> - {showResetWalletDisplay && ( - - - - )} - - - {!disableMenuOption && ( - - - - )} - {showOptionsDialog && ( - - - {optionsDialogTranslation('SWITCH_ACCOUNT')} - - {optionsDialogTranslation('LOCK')} - - {optionsDialogTranslation('RESET_WALLET')} - - - )} - - + - + {!disableMenuOption && ( + + + + )} + {showOptionsDialog && ( + + + {optionsDialogTranslation('SWITCH_ACCOUNT')} + + {optionsDialogTranslation('LOCK')} + + )} + ); } diff --git a/src/app/components/accountRow/accountAvatar.tsx b/src/app/components/accountRow/accountAvatar.tsx new file mode 100644 index 000000000..5c678cbbd --- /dev/null +++ b/src/app/components/accountRow/accountAvatar.tsx @@ -0,0 +1,46 @@ +import NftImage from '@screens/nftDashboard/nftImage'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { type Account } from '@secretkeylabs/xverse-core'; +import { getAccountGradient } from '@utils/gradient'; + +import type { AvatarInfo } from '@stores/wallet/actions/types'; +import { AvatarContainer, GradientCircle } from './index.styled'; + +type Props = { + account: Account | null; + isSelected: boolean; + isAccountListView?: boolean; + avatar?: AvatarInfo; +}; + +function AccountAvatar({ account, avatar, isSelected, isAccountListView = false }: Props) { + if (avatar?.type === 'inscription') { + return ( + + + + ); + } + + if (avatar?.type === 'stacks') { + return ( + + + + ); + } + + const gradient = getAccountGradient(account); + + return ( + + ); +} + +export default AccountAvatar; diff --git a/src/app/components/accountRow/index.styled.ts b/src/app/components/accountRow/index.styled.ts new file mode 100644 index 000000000..dda96509c --- /dev/null +++ b/src/app/components/accountRow/index.styled.ts @@ -0,0 +1,157 @@ +import Button from '@ui-library/button'; +import styled from 'styled-components'; + +export const Container = styled.div({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: 'transparent', +}); + +export const AccountInfoContainer = styled.button<{ $disableClick?: boolean }>((props) => ({ + width: '100%', + display: 'flex', + alignItems: 'center', + backgroundColor: 'transparent', + cursor: props.$disableClick ? 'initial' : 'pointer', +})); + +export const GradientCircle = styled.div<{ + $firstGradient: string; + $secondGradient: string; + $thirdGradient: string; + $isBig: boolean; + $isSelected: boolean; +}>((props) => ({ + width: props.$isBig ? 32 : 20, + height: props.$isBig ? 32 : 20, + borderRadius: 25, + background: `linear-gradient(to bottom, ${props.$firstGradient}, ${props.$secondGradient}, ${props.$thirdGradient})`, + opacity: props.$isSelected ? 1 : 0.5, +})); + +export const AvatarContainer = styled.div<{ + $isBig: boolean; + $isSelected: boolean; +}>((props) => ({ + width: props.$isBig ? 32 : 20, + height: props.$isBig ? 32 : 20, + borderRadius: props.$isBig ? 16 : 10, + opacity: props.$isSelected ? 1 : 0.5, + overflow: 'hidden', +})); + +export const CurrentAccountContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.space.s, +})); + +export const CurrentAccountTextContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: props.theme.space.xs, +})); + +export 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', +})); + +export const BarLoaderContainer = styled.div((props) => ({ + width: 200, + paddingTop: props.theme.space.xxs, + backgroundColor: 'transparent', +})); + +export const TransparentSpan = styled.span` + background: transparent; +`; + +export const OptionsButton = styled.button({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + background: 'transparent', +}); + +export const ModalContent = styled.form((props) => ({ + paddingBottom: props.theme.space.xxl, +})); + +export const ModalDescription = styled.div((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_200, +})); + +export const ModalControlsContainer = styled.div<{ + $bigSpacing?: boolean; +}>((props) => ({ + display: 'flex', + columnGap: props.theme.space.s, + marginTop: props.$bigSpacing ? props.theme.space.l : props.theme.space.m, +})); + +export const ModalButtonContainer = styled.div({ + width: '100%', +}); + +export const ButtonRow = styled.button` + display: flex; + align-items: center; + background-color: transparent; + justify-content: flex-start; + padding: ${(props) => props.theme.space.l}; + padding-top: ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.s}; + font: ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_0}; + transition: background-color 0.2s ease; + :hover { + background-color: ${(props) => props.theme.colors.elevation3}; + } + :active { + background-color: ${(props) => props.theme.colors.elevation3}; + } +`; + +export const InputLabel = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + color: props.theme.colors.white_200, + marginTop: props.theme.space.m, + marginBottom: props.theme.space.xs, +})); + +export const Balance = styled.div<{ $isSelected?: boolean }>((props) => ({ + ...props.theme.typography.body_medium_m, + marginTop: props.theme.space.xxs, + color: props.$isSelected ? props.theme.colors.white_200 : props.theme.colors.white_600, + display: 'flex', + alignItems: 'center', + columnGap: props.theme.space.xs, +})); + +export const StyledButton = styled(Button)((props) => ({ + padding: 0, + width: 'auto', + transition: 'opacity 0.1s ease', + div: { + color: props.theme.colors.tangerine, + }, + ':hover:enabled': { + opacity: 0.8, + }, + ':active:enabled': { + opacity: 0.6, + }, +})); diff --git a/src/app/components/accountRow/index.tsx b/src/app/components/accountRow/index.tsx index 273cd0a6f..d4b25018a 100644 --- a/src/app/components/accountRow/index.tsx +++ b/src/app/components/accountRow/index.tsx @@ -1,163 +1,45 @@ import LedgerBadge from '@assets/img/ledger/ledger_badge.svg'; import BarLoader from '@components/barLoader'; import OptionsDialog from '@components/optionsDialog/optionsDialog'; +import useOptionsDialog from '@hooks/useOptionsDialog'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import { CaretDown, DotsThreeVertical } from '@phosphor-icons/react'; import { currencySymbolMap, type Account } from '@secretkeylabs/xverse-core'; +import { removeAccountAvatarAction } from '@stores/wallet/actions/actionCreators'; import Button from '@ui-library/button'; import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; import Spinner from '@ui-library/spinner'; -import { EMPTY_LABEL, LoaderSize, OPTIONS_DIALOG_WIDTH } from '@utils/constants'; -import { getAccountGradient } from '@utils/gradient'; -import { isLedgerAccount, validateAccountName } from '@utils/helper'; +import { EMPTY_LABEL, HIDDEN_BALANCE_LABEL, LoaderSize } from '@utils/constants'; +import { getAccountBalanceKey, isLedgerAccount, validateAccountName } from '@utils/helper'; import { useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; +import { useDispatch } from 'react-redux'; import 'react-tooltip/dist/react-tooltip.css'; -import styled from 'styled-components'; - -const GradientCircle = styled.div<{ - firstGradient: string; - secondGradient: string; - thirdGradient: string; - isBig: boolean; -}>((props) => ({ - width: props.isBig ? 32 : 20, - height: props.isBig ? 32 : 20, - borderRadius: 25, - background: `linear-gradient(to bottom,${props.firstGradient}, ${props.secondGradient},${props.thirdGradient} )`, -})); - -const TopSectionContainer = styled.div<{ disableClick?: boolean }>((props) => ({ - position: 'relative', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - backgroundColor: 'transparent', - cursor: props.disableClick ? 'initial' : 'pointer', -})); - -const AccountInfoContainer = styled.div({ - width: '100%', - display: 'flex', - alignItems: 'center', -}); - -const CurrentAccountContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.space.s, -})); - -const CurrentAccountTextContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: props.theme.space.xs, -})); - -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) => ({ - width: 200, - paddingTop: props.theme.space.xxs, - backgroundColor: 'transparent', -})); - -const TransparentSpan = styled.span` - background: transparent; -`; - -const OptionsButton = styled.button({ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - background: 'transparent', -}); - -const ModalContent = styled.form((props) => ({ - paddingBottom: props.theme.space.xxl, -})); - -const ModalDescription = styled.div((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, -})); - -const ModalControlsContainer = styled.div<{ - $bigSpacing?: boolean; -}>((props) => ({ - display: 'flex', - columnGap: props.theme.space.s, - marginTop: props.$bigSpacing ? props.theme.space.l : props.theme.space.m, -})); - -const ModalButtonContainer = styled.div({ - width: '100%', -}); - -const ButtonRow = styled.button` - display: flex; - align-items: center; - background-color: transparent; - justify-content: flex-start; - padding: ${(props) => props.theme.space.l}; - padding-top: ${(props) => props.theme.space.s}; - padding-bottom: ${(props) => props.theme.space.s}; - font: ${(props) => props.theme.typography.body_medium_m}; - color: ${(props) => props.theme.colors.white_0}; - transition: background-color 0.2s ease; - :hover { - background-color: ${(props) => props.theme.colors.elevation3}; - } - :active { - background-color: ${(props) => props.theme.colors.elevation3}; - } -`; - -const InputLabel = styled.div((props) => ({ - ...props.theme.typography.body_medium_m, - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - color: props.theme.colors.white_200, - marginTop: props.theme.space.m, - marginBottom: props.theme.space.xs, -})); - -const Balance = styled.div<{ isSelected?: boolean }>((props) => ({ - ...props.theme.typography.body_medium_m, - marginTop: props.theme.space.xxs, - color: props.isSelected ? props.theme.colors.white_200 : props.theme.colors.white_400, - display: 'flex', - alignItems: 'center', - columnGap: props.theme.space.xs, -})); - -const StyledButton = styled(Button)((props) => ({ - padding: 0, - width: 'auto', - transition: 'opacity 0.1s ease', - div: { - color: props.theme.colors.tangerine, - }, - ':hover:enabled': { - opacity: 0.8, - }, - ':active:enabled': { - opacity: 0.6, - }, -})); +import Theme from 'theme'; +import AccountAvatar from './accountAvatar'; +import { + AccountInfoContainer, + AccountName, + Balance, + BarLoaderContainer, + ButtonRow, + Container, + CurrentAccountContainer, + CurrentAccountTextContainer, + InputLabel, + ModalButtonContainer, + ModalContent, + ModalControlsContainer, + ModalDescription, + OptionsButton, + StyledButton, + TransparentSpan, +} from './index.styled'; function AccountRow({ account, @@ -172,25 +54,31 @@ function AccountRow({ isAccountListView?: boolean; disabledAccountSelect?: boolean; }) { + const dispatch = useDispatch(); + const menuDialog = useOptionsDialog(); const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' }); const { t: optionsDialogTranslation } = useTranslation('translation', { keyPrefix: 'OPTIONS_DIALOG', }); - const { accountsList, ledgerAccountsList, fiatCurrency, accountBalances } = useWalletSelector(); - const totalBalance = accountBalances[account?.btcAddress ?? '']; - const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); + const { + accountsList, + ledgerAccountsList, + fiatCurrency, + accountBalances, + avatarIds, + balanceHidden, + } = useWalletSelector(); + const accountAvatar = avatarIds[account?.btcAddresses.taproot.address ?? '']; + // TODO: refactor this into a hook + const totalBalance = accountBalances[getAccountBalanceKey(account)]; const btcCopiedTooltipTimeoutRef = useRef(); const stxCopiedTooltipTimeoutRef = useRef(); - const [showOptionsDialog, setShowOptionsDialog] = useState(false); const [showRemoveAccountModal, setShowRemoveAccountModal] = useState(false); const [showRenameAccountModal, setShowRenameAccountModal] = useState(false); const [accountName, setAccountName] = useState(''); const [accountNameError, setAccountNameError] = useState(null); const [isAccountNameChangeLoading, setIsAccountNameChangeLoading] = useState(false); - const [optionsDialogIndents, setOptionsDialogIndents] = useState< - { top: string; left: string } | undefined - >(); - const { removeLedgerAccount, renameAccount, updateLedgerAccounts } = useWalletReducer(); + const { removeLedgerAccount, renameSoftwareAccount, updateLedgerAccounts } = useWalletReducer(); useEffect( () => () => { @@ -212,27 +100,12 @@ function AccountRow({ } else { setAccountNameError(null); } - }, [accountName]); + }, [accountName, accountsList, ledgerAccountsList, optionsDialogTranslation]); const handleClick = () => { onAccountSelected(account!); }; - const openOptionsDialog = (event: React.MouseEvent) => { - setShowOptionsDialog(true); - - setOptionsDialogIndents({ - top: `${(event.target as HTMLElement).parentElement?.getBoundingClientRect().top}px`, - left: `calc(${ - (event.target as HTMLElement).parentElement?.getBoundingClientRect().right - }px - ${OPTIONS_DIALOG_WIDTH}px)`, - }); - }; - - const closeOptionsDialog = () => { - setShowOptionsDialog(false); - }; - const handleRemoveAccountModalOpen = () => { setShowRemoveAccountModal(true); }; @@ -249,6 +122,14 @@ function AccountRow({ setShowRenameAccountModal(false); }; + const handleRemoveAvatar = () => { + if (!account) return; + dispatch(removeAccountAvatarAction({ address: account?.btcAddresses.taproot.address })); + toast.custom( + , + ); + }; + const handleRemoveLedgerAccount = async () => { if (!account) { return; @@ -284,7 +165,7 @@ function AccountRow({ if (isLedgerAccount(account)) { await updateLedgerAccounts({ ...account, accountName }); } else { - await renameAccount({ ...account, accountName }); + await renameSoftwareAccount({ ...account, accountName }); } handleRenameAccountModalClose(); } catch (err) { @@ -304,7 +185,7 @@ function AccountRow({ if (isLedgerAccount(account)) { updateLedgerAccounts({ ...account, accountName: undefined }); } else { - renameAccount({ ...account, accountName: undefined }); + renameSoftwareAccount({ ...account, accountName: undefined }); } setAccountName(''); handleRenameAccountModalClose(); @@ -316,26 +197,26 @@ function AccountRow({ }; return ( - - - + + {account && ( - + {account?.accountName ?? account?.bnsName ?? `${t('ACCOUNT_NAME')} ${`${(account?.id ?? 0) + 1}`}`} {isLedgerAccount(account) && Ledger icon} {isSelected && !disabledAccountSelect && !isAccountListView && ( - + )} {isAccountListView && totalBalance && ( @@ -345,14 +226,14 @@ function AccountRow({ prefix={`${currencySymbolMap[fiatCurrency]}`} thousandSeparator renderText={(value: string) => ( - - {value} + + {balanceHidden ? HIDDEN_BALANCE_LABEL : value} )} /> )} {isAccountListView && !totalBalance && ( - + {EMPTY_LABEL} @@ -370,16 +251,21 @@ function AccountRow({ {isAccountListView && ( - + )} - {showOptionsDialog && ( - + {menuDialog.isVisible && ( + {optionsDialogTranslation('RENAME_ACCOUNT')} + {accountAvatar?.type && ( + + {optionsDialogTranslation('NFT_AVATAR.REMOVE_ACTION')} + + )} {isLedgerAccount(account) && ( {optionsDialogTranslation('REMOVE_FROM_LIST')} @@ -459,7 +345,7 @@ function AccountRow({ )} - + ); } diff --git a/src/app/components/accountRow/lazyAccountRow.tsx b/src/app/components/accountRow/lazyAccountRow.tsx index e88332884..b79353cf0 100644 --- a/src/app/components/accountRow/lazyAccountRow.tsx +++ b/src/app/components/accountRow/lazyAccountRow.tsx @@ -1,30 +1,39 @@ import useIntersectionObserver from '@hooks/useIntersectionObserver'; import useWalletSelector from '@hooks/useWalletSelector'; import type { Account } from '@secretkeylabs/xverse-core'; +import { getAccountBalanceKey } from '@utils/helper'; import { useEffect, useRef, useState } from 'react'; import AccountRow from '.'; -function LazyAccountRow(props: { +type Props = { account: Account | null; isSelected: boolean; onAccountSelected: (account: Account, goBack?: boolean) => void; isAccountListView?: boolean; disabledAccountSelect?: boolean; fetchBalance?: (account: Account | null) => void; -}) { +}; + +function LazyAccountRow(props: Props) { const { fetchBalance, account } = props; const { accountBalances } = useWalletSelector(); - const totalBalance = accountBalances[account?.btcAddress ?? '']; const [shouldFetch, setShouldFetch] = useState(false); const ref = useRef(null); useIntersectionObserver(ref, () => setShouldFetch(true), {}); useEffect(() => { - if (fetchBalance && shouldFetch && !totalBalance) { - fetchBalance(account); + if ( + !shouldFetch || + !fetchBalance || + !account || + getAccountBalanceKey(account) in accountBalances + ) { + return; } - }, [shouldFetch, totalBalance]); + + fetchBalance(account); + }, [account, shouldFetch]); return (
diff --git a/src/app/components/alertMessage/index.tsx b/src/app/components/alertMessage/index.tsx index 6287416aa..1621bdbdf 100644 --- a/src/app/components/alertMessage/index.tsx +++ b/src/app/components/alertMessage/index.tsx @@ -1,6 +1,8 @@ import Cross from '@assets/img/dashboard/X.svg'; import ActionButton from '@components/button'; +import Button from '@ui-library/button'; import Checkbox from '@ui-library/checkbox'; +import { useEffect, useRef } from 'react'; import styled from 'styled-components'; const Container = styled.div((props) => ({ @@ -75,7 +77,7 @@ const OuterContainer = styled.div((props) => ({ opacity: 0.6, })); -interface Props { +type Props = { onClose: () => void; title: string; description: string; @@ -87,7 +89,7 @@ interface Props { onSecondButtonClick?: () => void; tickMarkButtonClick?: (e: React.ChangeEvent) => void; tickMarkButtonChecked?: boolean; -} +}; function AlertMessage({ onClose, @@ -102,13 +104,30 @@ function AlertMessage({ tickMarkButtonClick, tickMarkButtonChecked, }: Props) { + const buttonRef = useRef(null); + const previousFocusRef = useRef(null); + + useEffect(() => { + previousFocusRef.current = document.activeElement as HTMLElement; + + if (buttonRef.current) { + buttonRef.current.focus(); + } + + return () => { + if (previousFocusRef.current) { + previousFocusRef.current.focus(); + } + }; + }, []); + return ( <> {title} - + cross @@ -127,7 +146,7 @@ function AlertMessage({ )} {!onSecondButtonClick && onButtonClick && ( - + {isExpanded && ( - {summary?.inputs.map((input) => ( + {extractedTxSummary.inputs.map((input) => ( ))} {t('OUTPUT')} - {summary?.outputs.map((output, index) => ( + {extractedTxSummary.outputs.map((output, index) => ( ))} diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index 438843d94..a89af0725 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -28,19 +28,18 @@ type Props = { }; function TransactionInput({ input }: Props) { - const { btcAddress, ordinalsAddress } = useSelectedAccount(); + const { btcAddresses } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const isPaymentsAddress = input.extendedUtxo.address === btcAddress; - const isOrdinalsAddress = input.extendedUtxo.address === ordinalsAddress; - const isExternalInput = !isPaymentsAddress && !isOrdinalsAddress; + const userAddresses = Object.values(btcAddresses).map((address) => address.address); + const isExternalInput = userAddresses.every((address) => address !== input.extendedUtxo.address); // TODO: show this in the UI? // const insecureInput = // input.sigHash === btc.SigHash.NONE || input.sigHash === btc.SigHash.NONE_ANYONECANPAY; const renderAddress = (addressToBeDisplayed: string) => - addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( + userAddresses.some((address) => address === addressToBeDisplayed) ? ( {getTruncatedAddress(addressToBeDisplayed)} @@ -59,7 +58,7 @@ function TransactionInput({ input }: Props) { icon={InputIcon} hideAddress dataTestID="confirm-balance" - hideCopyButton={isPaymentsAddress || isOrdinalsAddress} + hideCopyButton={!isExternalInput} amount={`${satsToBtc( new BigNumber(input.extendedUtxo.utxo.value.toString()), ).toFixed()} BTC`} diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index c684fb8e6..ec18a8f8f 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -8,7 +8,6 @@ import { getTruncatedAddress } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { isAddressOutput, isPubKeyOutput, isScriptOutput } from '../utils'; const TransferDetailContainer = styled.div((props) => ({ paddingBottom: props.theme.space.m, @@ -35,16 +34,24 @@ type Props = { }; function TransactionOutput({ output, scriptOutputCount }: Props) { - const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const isOutputWithScript = isScriptOutput(output); - const isOutputWithPubKey = isPubKeyOutput(output); + + const { btcAddresses, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); + + const userAddresses = Object.values(btcAddresses).map((address) => address.address); + + const isOutputWithScript = output.type === 'script'; + const isOutputWithAddress = output.type === 'address'; + const isOutputWithPubKey = !isOutputWithScript && !isOutputWithAddress; + + const isOutputToOwnAddress = + isOutputWithScript || isOutputWithPubKey + ? false + : userAddresses.some((address) => address === output.address); const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; const detailViewHideCopyButton = - isOutputWithScript || isOutputWithPubKey - ? true - : btcAddress === output.address || ordinalsAddress === output.address; + isOutputWithScript || isOutputWithPubKey ? true : isOutputToOwnAddress; const detailView = () => { if (isOutputWithScript) { @@ -67,7 +74,7 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { ); } - if (output.address === btcAddress || output.address === ordinalsAddress) { + if (isOutputToOwnAddress) { return ( @@ -93,7 +100,7 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { hideCopyButton={detailViewHideCopyButton} dataTestID="confirm-amount" amount={`${satsToBtc( - new BigNumber(isAddressOutput(output) ? output.amount.toString() : '0'), + new BigNumber(isOutputWithAddress ? output.amount.toString() : '0'), ).toFixed()} BTC`} address={isOutputWithScript || isOutputWithPubKey ? '' : output.address} outputScript={isOutputWithScript ? output.script : undefined} diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts deleted file mode 100644 index 93147f812..000000000 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ /dev/null @@ -1,315 +0,0 @@ -import type { btcTransaction, BundleSatRange, FungibleToken } from '@secretkeylabs/xverse-core'; - -// TODO this should all be in core and unit tested - -export type SatRangeTx = { - totalSats: number; - offset: number; - fromAddress: string; - inscriptions: (Omit & { - content_type: string; - inscription_number: number; - })[]; - satributes: btcTransaction.IOSatribute['types']; -}; - -const DUMMY_OFFSET = -1; - -export const isScriptOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionScriptOutput => - (output as btcTransaction.TransactionScriptOutput).script !== undefined; - -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; - -type CommonInputOutputUtilProps = { - inputs?: btcTransaction.EnhancedInput[]; - outputs?: btcTransaction.EnhancedOutput[]; - btcAddress: string; - ordinalsAddress: string; -}; - -export const getNetAmount = ({ - inputs, - outputs, - btcAddress, - ordinalsAddress, -}: CommonInputOutputUtilProps) => { - if (!inputs || !outputs) { - return 0; - } - - const initialValue = 0; - - const totalUserSpend = inputs.reduce((accumulator: number, input) => { - const isFromUserAddress = [btcAddress, ordinalsAddress].includes(input.extendedUtxo.address); - if (isFromUserAddress) { - return accumulator + input.extendedUtxo.utxo.value; - } - return accumulator; - }, initialValue); - - const totalUserReceive = outputs.reduce((accumulator: number, output) => { - const isToUserAddress = - isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); - if (isToUserAddress) { - return accumulator + output.amount; - } - return accumulator; - }, initialValue); - - return totalUserReceive - totalUserSpend; -}; - -export const getOutputsWithAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit): { - outputsFromPayment: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; - outputsFromOrdinal: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; -} => { - // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes - const outputsFromPayment: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - const outputsFromOrdinal: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - outputs?.forEach((output) => { - if (isScriptOutput(output)) { - return; - } - - const itemsFromPayment: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - const itemsFromOrdinal: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - [...output.inscriptions, ...output.satributes].forEach((item) => { - if (item.fromAddress === btcAddress) { - return itemsFromPayment.push(item); - } - if (item.fromAddress === ordinalsAddress) { - itemsFromOrdinal.push(item); - } - }); - - if (itemsFromOrdinal.length > 0) { - outputsFromOrdinal.push(output); - } - if (itemsFromPayment.length > 0) { - outputsFromPayment.push(output); - } - }); - - return { outputsFromPayment, outputsFromOrdinal }; -}; - -export const getInputsWithAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - inputs, -}: Omit): { - inputsFromPayment: btcTransaction.EnhancedInput[]; - inputsFromOrdinal: btcTransaction.EnhancedInput[]; -} => { - // we want to discard inputs that are not from user address and do not have inscriptions or satributes - const inputsFromPayment: btcTransaction.EnhancedInput[] = []; - const inputsFromOrdinal: btcTransaction.EnhancedInput[] = []; - inputs?.forEach((input) => { - if (!input.inscriptions.length && !input.satributes.length) { - return; - } - - if (input.extendedUtxo.address === btcAddress) { - return inputsFromPayment.push(input); - } - if (input.extendedUtxo.address === ordinalsAddress) { - inputsFromOrdinal.push(input); - } - }); - - return { inputsFromPayment, inputsFromOrdinal }; -}; - -export const getOutputsWithAssetsToUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit): { - outputsToPayment: btcTransaction.TransactionOutput[]; - outputsToOrdinal: btcTransaction.TransactionOutput[]; -} => { - const outputsToPayment: btcTransaction.TransactionOutput[] = []; - 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) || - isPubKeyOutput(output) || - ![btcAddress, ordinalsAddress].includes(output.address) - ) { - return; - } - - if (output.address === btcAddress) { - return outputsToPayment.push(output); - } - - // we don't want to show amount to ordinals address, because it's not spendable - if ( - output.address === ordinalsAddress && - (output.inscriptions.length > 0 || output.satributes.length > 0) - ) { - outputsToOrdinal.push(output); - } - }); - - return { outputsToPayment, outputsToOrdinal }; -}; - -export const mapTxSatributeInfoToBundleInfo = (item: btcTransaction.IOSatribute | SatRangeTx) => { - const commonProps = { - offset: item.offset, - block: 0, - range: { - start: '0', - end: '0', - }, - yearMined: 0, - }; - - // SatRangeTx - if ('totalSats' in item) { - return { - ...commonProps, - totalSats: item.totalSats, - inscriptions: item.inscriptions, - satributes: item.satributes, - } as BundleSatRange; - } - - // btcTransaction.IOSatribute - return { - ...commonProps, - totalSats: item.amount, - inscriptions: [], - satributes: item.types, - } as BundleSatRange; -}; - -export const getSatRangesWithInscriptions = ({ - satributes, - inscriptions, - amount, -}: { - inscriptions: btcTransaction.IOInscription[]; - satributes: btcTransaction.IOSatribute[]; - amount: number; -}) => { - const satRanges: { - [offset: number]: SatRangeTx; - } = {}; - - satributes.forEach((satribute) => { - const { types, amount: totalSats, ...rest } = satribute; - satRanges[rest.offset] = { ...rest, satributes: types, totalSats, inscriptions: [] }; - }); - - inscriptions.forEach((inscription) => { - const { contentType, number, ...inscriptionRest } = inscription; - const mappedInscription = { - ...inscriptionRest, - content_type: contentType, - inscription_number: number, - }; - if (satRanges[inscription.offset]) { - satRanges[inscription.offset] = { - ...satRanges[inscription.offset], - inscriptions: [...satRanges[inscription.offset].inscriptions, mappedInscription], - }; - return; - } - - satRanges[inscription.offset] = { - totalSats: 1, - offset: inscription.offset, - fromAddress: inscription.fromAddress, - inscriptions: [mappedInscription], - satributes: ['COMMON'], - }; - }); - - const { amountOfExoticsOrInscribedSats, totalExoticSats } = Object.values(satRanges).reduce( - (acc, range) => ({ - amountOfExoticsOrInscribedSats: acc.amountOfExoticsOrInscribedSats + range.totalSats, - totalExoticSats: - acc.totalExoticSats + (!range.satributes.includes('COMMON') ? range.totalSats : 0), - }), - { - amountOfExoticsOrInscribedSats: 0, - totalExoticSats: 0, - }, - ); - - if (amountOfExoticsOrInscribedSats < amount) { - satRanges[DUMMY_OFFSET] = { - totalSats: amount - amountOfExoticsOrInscribedSats, - offset: DUMMY_OFFSET, - fromAddress: '', - inscriptions: [], - satributes: ['COMMON'], - }; - } - - // sort should be: inscribed rare, rare, inscribed common, common - const satRangesArray = Object.values(satRanges).sort((a, b) => { - // Check conditions for each category - const aHasInscriptions = a.inscriptions.length > 0; - const bHasInscriptions = b.inscriptions.length > 0; - const aHasRareSatributes = a.satributes.some((s) => s !== 'COMMON'); - const bHasRareSatributes = b.satributes.some((s) => s !== 'COMMON'); - - // sats not rare and not inscribed at bottom - if (!aHasInscriptions && !aHasRareSatributes) return 1; - - // sats inscribed and rare at top - if (aHasInscriptions && aHasRareSatributes) return -1; - - // sats not inscribed and rare below inscribed and rare - if (bHasInscriptions && bHasRareSatributes) return 1; - - // sats inscribed and not rare above sats not inscribed and not rare - if (aHasRareSatributes) return -1; - if (bHasRareSatributes) return 1; - - // equal ranges - return 0; - }); - - return { satRanges: satRangesArray, totalExoticSats }; -}; - -export const mapRuneNameToPlaceholder = ( - runeName: string, - symbol: string, - inscriptionId: string, -): FungibleToken => ({ - protocol: 'runes', - name: runeName, - assetName: '', - balance: '', - principal: '', - total_received: '', - total_sent: '', - runeSymbol: symbol, - runeInscriptionId: inscriptionId, -}); diff --git a/src/app/components/confirmStxTransactionComponent/index.styled.ts b/src/app/components/confirmStxTransactionComponent/index.styled.ts index 8de1152e3..967e2faea 100644 --- a/src/app/components/confirmStxTransactionComponent/index.styled.ts +++ b/src/app/components/confirmStxTransactionComponent/index.styled.ts @@ -26,10 +26,6 @@ export const EditNonceButton = styled(Button)((props) => ({ 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, diff --git a/src/app/components/confirmStxTransactionComponent/index.tsx b/src/app/components/confirmStxTransactionComponent/index.tsx index 07a021414..c4a0c78d6 100644 --- a/src/app/components/confirmStxTransactionComponent/index.tsx +++ b/src/app/components/confirmStxTransactionComponent/index.tsx @@ -1,12 +1,12 @@ 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 { delay } from '@common/utils/promises'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import TransactionSettingAlert from '@components/transactionSetting'; -import useCoinRates from '@hooks/queries/useCoinRates'; import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useNetworkSelector from '@hooks/useNetwork'; import useSeedVault from '@hooks/useSeedVault'; import useSelectedAccount from '@hooks/useSelectedAccount'; @@ -15,6 +15,7 @@ import Transport from '@ledgerhq/hw-transport-webusb'; import { FadersHorizontal } from '@phosphor-icons/react'; import type { StacksTransaction } from '@secretkeylabs/xverse-core'; import { + estimateStacksTransactionWithFallback, getNonce, getStxFiatEquivalent, microstacksToStx, @@ -23,7 +24,7 @@ import { signTransaction, stxToMicrostacks, } from '@secretkeylabs/xverse-core'; -import { estimateTransaction, PostConditionMode } from '@stacks/transactions'; +import { PostConditionMode } from '@stacks/transactions'; import SelectFeeRate from '@ui-components/selectFeeRate'; import Button from '@ui-library/button'; import Callout from '@ui-library/callout'; @@ -32,6 +33,7 @@ import { modifyRecommendedStxFees } from '@utils/transactions/transactions'; import BigNumber from 'bignumber.js'; import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; import Theme from 'theme'; import { ButtonsContainer, @@ -46,6 +48,13 @@ import { WarningWrapper, } from './index.styled'; +const Subtitle = styled.p` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.xs}; +`; + // todo: make fee non option - that'll require change in all components using it type Props = { initialStxTransactions: StacksTransaction[]; @@ -86,12 +95,12 @@ function ConfirmStxTransactionComponent({ keyPrefix: 'TRANSACTION_SETTING', }); const selectedNetwork = useNetworkSelector(); - const { stxBtcRate, btcFiatRate } = useCoinRates(); + const { stxBtcRate, btcFiatRate } = useSupportedCoinRates(); const { data: stxData } = useStxWalletData(); const { getSeed } = useSeedVault(); const [showFeeSettings, setShowFeeSettings] = useState(false); const selectedAccount = useSelectedAccount(); - const { feeMultipliers, fiatCurrency } = useWalletSelector(); + const { feeMultipliers, fiatCurrency, network } = useWalletSelector(); const [openTransactionSettingModal, setOpenTransactionSettingModal] = useState(false); const [buttonLoading, setButtonLoading] = useState(loading); const [isModalVisible, setIsModalVisible] = useState(false); @@ -119,9 +128,8 @@ function ConfirmStxTransactionComponent({ const fetchStxFees = async () => { try { setFeesLoading(true); - const [low, medium, high] = await estimateTransaction( - initialStxTransactions[0].payload, - undefined, + const [low, medium, high] = await estimateStacksTransactionWithFallback( + initialStxTransactions[0], selectedNetwork, ); @@ -132,6 +140,7 @@ function ConfirmStxTransactionComponent({ high: high.fee, }, feeMultipliers, + initialStxTransactions[0].payload.payloadType, ); setFeeRates({ @@ -139,7 +148,7 @@ function ConfirmStxTransactionComponent({ medium: microstacksToStx(BigNumber(modifiedFees.medium)).toNumber(), high: microstacksToStx(BigNumber(modifiedFees.high)).toNumber(), }); - if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(medium.fee))).toString()); + if (!fee) setFeeRate?.(Number(microstacksToStx(BigNumber(modifiedFees.low))).toString()); } catch (e) { console.error(e); } finally { @@ -148,15 +157,16 @@ function ConfirmStxTransactionComponent({ }; fetchStxFees(); - }, [selectedNetwork, initialStxTransactions]); + }, [selectedNetwork, initialStxTransactions, feeMultipliers, fee, setFeeRate]); useEffect(() => { - const stxTxFee = BigNumber(initialStxTransactions[0].auth.spendingCondition.fee.toString()); + if (!feeMultipliers || !fee) return; - if ( - feeMultipliers && - stxTxFee.isGreaterThan(BigNumber(feeMultipliers.thresholdHighStacksFee)) - ) { + const feeExceedsThreshold = stxToMicrostacks(new BigNumber(fee)).isGreaterThan( + BigNumber(feeMultipliers.thresholdHighStacksFee), + ); + + if (feeExceedsThreshold) { setShowFeeWarning(true); } else if (showFeeWarning) { setShowFeeWarning(false); @@ -212,8 +222,9 @@ function ConfirmStxTransactionComponent({ } if (initialStxTransactions.length === 1) { + const transaction = initialStxTransactions[0]; const signedContractCall = await signTransaction( - initialStxTransactions[0], + transaction, seed, selectedAccount?.id ?? 0, selectedNetwork, @@ -352,6 +363,8 @@ function ConfirmStxTransactionComponent({ )} {children} + + {t('FEES')} void; }; -export default function Steps({ +export default function Dots({ numDots, activeIndex, dotStrategy, diff --git a/src/app/components/explore/FeaturedCard.tsx b/src/app/components/explore/FeaturedCard.tsx index 144c9d06a..003120375 100644 --- a/src/app/components/explore/FeaturedCard.tsx +++ b/src/app/components/explore/FeaturedCard.tsx @@ -2,14 +2,16 @@ import { AnalyticsEvents } from '@secretkeylabs/xverse-core'; import { trackMixPanel } from '@utils/mixpanel'; import styled from 'styled-components'; -const Card = styled.div` +const Card = styled.button` cursor: pointer; - display: block; + display: flex; + flex-direction: column; border-radius: 12px; background-color: ${({ theme }) => theme.colors.elevation2}; height: 182px; width: 212px; color: ${({ theme }) => theme.colors.white_0}; + text-align: left; transition: opacity 0.1s ease; &:hover { diff --git a/src/app/components/explore/FeaturedCarousel.tsx b/src/app/components/explore/FeaturedCarousel.tsx index a8b311dd5..5779afc67 100644 --- a/src/app/components/explore/FeaturedCarousel.tsx +++ b/src/app/components/explore/FeaturedCarousel.tsx @@ -13,7 +13,6 @@ const CarouselContainer = styled.div` padding-bottom: 35px; } - // So silly that they don't have a native API for this .swiper-slide { width: fit-content !important; } @@ -24,11 +23,17 @@ const CarouselContainer = styled.div` } .swiper-pagination-bullet { - background: ${(props) => props.theme.colors.white_800}; + width: 6px; + height: 6px; + background: ${(props) => props.theme.colors.white_600}; + opacity: 1; + border-radius: 50px; + transition: width 0.2s ease, background 0.1s ease; } .swiper-pagination-bullet-active { - background: ${(props) => props.theme.colors.white_200}; + width: 18px; + background: ${(props) => props.theme.colors.white_0}; } `; diff --git a/src/app/components/explore/RecommendedApps.tsx b/src/app/components/explore/RecommendedApps.tsx index e33899518..f997883ba 100644 --- a/src/app/components/explore/RecommendedApps.tsx +++ b/src/app/components/explore/RecommendedApps.tsx @@ -10,13 +10,15 @@ const Container = styled.div` width: 100%; `; -const Card = styled.div` +const Card = styled.button` cursor: pointer; display: flex; align-items: center; column-gap: ${({ theme }) => theme.space.m}; width: 100%; color: ${({ theme }) => theme.colors.white_0}; + background-color: transparent; + text-align: left; transition: opacity 0.1s ease; &:hover { @@ -47,7 +49,14 @@ const CardText = styled.div` overflow: hidden; `; -type Props = { items: { url: string; icon: string; name: string; description: string }[] }; +type Props = { + items: { + url: string; + icon: string; + name: string; + description: string; + }[]; +}; function RecommendedApps({ items }: Props) { return ( diff --git a/src/app/components/explore/SwiperNavigation.tsx b/src/app/components/explore/SwiperNavigation.tsx index 366d2864c..a4c0dac1e 100644 --- a/src/app/components/explore/SwiperNavigation.tsx +++ b/src/app/components/explore/SwiperNavigation.tsx @@ -8,6 +8,9 @@ const Container = styled.div` const Button = styled.button` background-color: transparent; + width: 18px; + height: 18px; + margin: 6px; svg { color: ${({ theme }) => theme.colors.white_0}; @@ -21,13 +24,11 @@ const Button = styled.button` } &:disabled { + cursor: default; svg { - color: ${({ theme }) => theme.colors.white_400}; + color: ${({ theme }) => theme.colors.white_600}; } } - width: 18px; - height: 18px; - margin: 6px; `; function SwiperNavigation() { diff --git a/src/app/components/fiatAmountText/index.tsx b/src/app/components/fiatAmountText/index.tsx index 46b197585..128cc7f69 100644 --- a/src/app/components/fiatAmountText/index.tsx +++ b/src/app/components/fiatAmountText/index.tsx @@ -46,4 +46,8 @@ export const StyledFiatAmountText = styled(FiatAmountText)` color: ${(props) => props.theme.colors.white_400}; `; +export const RightAlignedStyledFiatAmountText = styled(StyledFiatAmountText)` + text-align: right; +`; + export default FiatAmountText; diff --git a/src/app/components/guards/auth.tsx b/src/app/components/guards/auth.tsx index 9a8eac8c5..ec3251d96 100644 --- a/src/app/components/guards/auth.tsx +++ b/src/app/components/guards/auth.tsx @@ -66,7 +66,9 @@ function AuthGuard({ children }: PropsWithChildren) { navigate('/login'); } - await loadWallet(); + await loadWallet(() => { + isInitialised.current = true; + }); }; useEffect(() => { diff --git a/src/app/components/guards/onboarding/index.tsx b/src/app/components/guards/onboarding/index.tsx index 7251342cf..2498b235a 100644 --- a/src/app/components/guards/onboarding/index.tsx +++ b/src/app/components/guards/onboarding/index.tsx @@ -38,7 +38,7 @@ function OnboardingGuard({ children }: WalletExistsGuardProps): React.ReactEleme return; } - unlockVault(''); + await unlockVault(''); setIsWalletInitialized(false); } catch (e) { // seed exists and unlocking with empty password failed diff --git a/src/app/components/ledger/ledgerAssetSelectCard/index.tsx b/src/app/components/ledger/ledgerAssetSelectCard/index.tsx index decc0e47d..b880faed6 100644 --- a/src/app/components/ledger/ledgerAssetSelectCard/index.tsx +++ b/src/app/components/ledger/ledgerAssetSelectCard/index.tsx @@ -1,28 +1,25 @@ import styled from 'styled-components'; -const CardContainer = styled.label((props) => ({ +const CardContainer = styled.button((props) => ({ display: 'flex', - flexDirection: 'row', - gap: props.theme.spacing(6), + gap: props.theme.space.s, justifyContent: 'flex-start', + alignItems: 'center', padding: props.theme.spacing(7), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, cursor: 'pointer', - transition: 'background 0.2s ease, border 0.2s ease', - background: - props.className === 'checked' ? props.theme.colors.background.selectBackground : 'transparent', + transition: 'background 0.1s ease, border 0.1s ease', + textAlign: 'left', + color: props.theme.colors.white_0, + background: props.theme.colors.elevation1, borderRadius: props.theme.radius(2), border: `1px solid ${ - props.className === 'checked' ? props.theme.colors.border.select : props.theme.colors.white_900 + props.className === 'checked' ? '#7383ff4d' : props.theme.colors.elevation1 }`, userSelect: 'none', })); -const CardInput = styled.input({ - display: 'none', -}); - const CardIconContainer = styled.div((props) => ({ display: 'flex', width: props.theme.spacing(24), @@ -46,38 +43,35 @@ const CardText = styled.p((props) => ({ color: props.theme.colors.white_400, })); -interface Props { +type Props = { icon: string; title: string; text: string; - id: string; + name: 'Bitcoin' | 'Stacks'; isChecked: boolean; - onChange: (e: React.ChangeEvent) => void; + onClick: (selectedAsset: 'Bitcoin' | 'Stacks') => void; squareIcon?: boolean; -} +}; function LedgerAssetSelectCard({ icon, title, text, - id, + name, isChecked, - onChange: handleChange, + onClick: handleClick, squareIcon = false, }: Props) { return ( -
- - - - -
- {title} - {text} -
-
- -
+ handleClick(name)}> + + + +
+ {title} + {text} +
+
); } diff --git a/src/app/components/ledgerSteps/index.tsx b/src/app/components/ledgerSteps/index.tsx new file mode 100644 index 000000000..23c79d97c --- /dev/null +++ b/src/app/components/ledgerSteps/index.tsx @@ -0,0 +1,121 @@ +import { delay } from '@common/utils/promises'; +import TransportFactory from '@ledgerhq/hw-transport-webusb'; +import { type Transport } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import LedgerStepView, { Steps } from './ledgerStepView'; + +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, +})); + +type Props = { + onConfirm: (ledgerTransport?: Transport) => void; + onCancel: () => void; + txnToSignCount?: number; + txnSignIndex?: number; +}; + +function LedgerSteps({ onConfirm, onCancel, txnToSignCount, txnSignIndex }: Props) { + const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [isConnectSuccess, setIsConnectSuccess] = useState(false); + const [isConnectFailed, setIsConnectFailed] = useState(false); + const [isTxRejected, setIsTxRejected] = useState(false); + + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { t: signatureRequestTranslate } = useTranslation('translation', { + keyPrefix: 'SIGNATURE_REQUEST', + }); + + const handleConnectAndConfirm = async () => { + setIsButtonDisabled(true); + + const transport = await TransportFactory.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + + if (currentStep !== Steps.ExternalInputs && currentStep !== Steps.ConfirmTransaction) { + setCurrentStep(Steps.ExternalInputs); + return; + } + + if (currentStep !== Steps.ConfirmTransaction) { + setCurrentStep(Steps.ConfirmTransaction); + } + + try { + onConfirm(transport); + } catch (err) { + console.error(err); + setIsTxRejected(true); + } + }; + + const goToConfirmationStep = () => { + setCurrentStep(Steps.ConfirmTransaction); + + handleConnectAndConfirm(); + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsConnectSuccess(false); + setCurrentStep(Steps.ConnectLedger); + }; + + return ( + <> + + + {currentStep === Steps.ExternalInputs && !isTxRejected && !isConnectFailed ? ( +
); - case Steps.ConfirmTransaction: + case Steps.ConfirmTransaction: { + let title = signatureRequestTranslate('LEDGER.CONFIRM.TITLE'); + if (txnToSignCount && txnSignIndex && txnToSignCount > 1) { + title = signatureRequestTranslate('LEDGER.CONFIRM.TITLE_WITH_COUNT', { + current: txnSignIndex, + total: txnToSignCount, + }); + } return ( ); + } default: return null; } diff --git a/src/app/screens/signMessageRequest/index.styled.ts b/src/app/components/messageSigning/index.styled.ts similarity index 100% rename from src/app/screens/signMessageRequest/index.styled.ts rename to src/app/components/messageSigning/index.styled.ts diff --git a/src/app/screens/signMessageRequestInApp/index.tsx b/src/app/components/messageSigning/index.tsx similarity index 69% rename from src/app/screens/signMessageRequestInApp/index.tsx rename to src/app/components/messageSigning/index.tsx index c2c1dd276..3e47d8180 100644 --- a/src/app/screens/signMessageRequestInApp/index.tsx +++ b/src/app/components/messageSigning/index.tsx @@ -1,26 +1,27 @@ 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 { delay } from '@common/utils/promises'; 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 CollapsibleContainer from '@screens/signatureRequest/collapsableContainer'; import SignatureRequestMessage from '@screens/signatureRequest/signatureRequestMessage'; -import { bip0322Hash, MessageSigningProtocols, signMessage } from '@secretkeylabs/xverse-core'; +import { + bip0322Hash, + MessageSigningProtocols, + signMessage, + type SignedMessage, +} from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import Sheet from '@ui-library/sheet'; import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; import { handleLedgerMessageSigning } from '@utils/ledger'; -import { useEffect, useState } from 'react'; -import toast from 'react-hot-toast'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; import { ActionDisclaimer, MainContainer, @@ -32,19 +33,32 @@ import { SigningAddressType, SigningAddressValue, SuccessActionsContainer, -} from '../signMessageRequest/index.styled'; +} from './index.styled'; + +interface MessageSigningProps { + address: string; + message: string; + protocol: MessageSigningProtocols; + onSigned: (signedMessage: SignedMessage) => Promise; + onSignedError?: (error: unknown) => void; + onCancel: () => void; + header?: React.ReactNode; +} -function SignMessageRequestInApp() { +function MessageSigning({ + address, + message, + protocol, + onSigned, + onSignedError, + onCancel, + header, +}: MessageSigningProps) { 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 @@ -56,24 +70,6 @@ function SignMessageRequestInApp() { 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) { @@ -98,10 +94,6 @@ function SignMessageRequestInApp() { 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); @@ -110,8 +102,6 @@ function SignMessageRequestInApp() { setCurrentStepIndex(0); }; - const handleGoBack = () => navigate(-1); - const handleConnectAndConfirm = async () => { if (!selectedAccount) { return; @@ -135,23 +125,14 @@ function SignMessageRequestInApp() { const signedMessage = await handleLedgerMessageSigning({ transport, addressIndex: selectedAccount.deviceAccountIndex, - address: payload.address, + address, networkType: network.type, - message: payload.message, - protocol: MessageSigningProtocols.BIP322, + message, + protocol, }); - - await runesApi.submitCancelRunesSellOrder({ - orderIds: payload.orderIds, - makerPublicKey: selectedAccount?.ordinalsPublicKey!, - makerAddress: selectedAccount?.ordinalsAddress!, - token: payload.token, - signature: signedMessage.signature, - }); - - handleGoBack(); - toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + await onSigned(signedMessage); } catch (e: any) { + onSignedError?.(e); if (e.name === 'LockedDeviceError') { setCurrentStepIndex(0); setIsConnectSuccess(false); @@ -174,16 +155,15 @@ function SignMessageRequestInApp() { const seedPhrase = await getSeed(); return signMessage({ accounts: accountsList, - message: payload.message, - address: payload.address, + message, + address, seedPhrase, network: network.type, - protocol: MessageSigningProtocols.BIP322, + protocol, }); }; const confirmCallback = async () => { - if (!payload) return; try { setIsSigning(true); if (isHardwareAccount(selectedAccount)) { @@ -191,43 +171,40 @@ function SignMessageRequestInApp() { return; } const signedMessage = await confirmSignMessage(); - - await runesApi.submitCancelRunesSellOrder({ - orderIds: payload.orderIds, - makerPublicKey: selectedAccount?.ordinalsPublicKey!, - makerAddress: selectedAccount?.ordinalsAddress!, - token: payload.token, - signature: signedMessage.signature, - }); - - handleGoBack(); - toast(`${t('SIGNATURE_REQUEST.UNLISTED_SUCCESS')}`); + await onSigned(signedMessage); } catch (err) { - toast(`${t('SIGNATURE_REQUEST.UNLISTED_ERROR')}`); + onSignedError?.(err); } finally { setIsSigning(false); } }; + const addressType = + address === selectedAccount.btcAddress + ? t('SIGNATURE_REQUEST.SIGNING_ADDRESS_PAYMENT') + : address === selectedAccount.ordinalsAddress + ? t('SIGNATURE_REQUEST.SIGNING_ADDRESS_ORDINALS') + : undefined; + return ( <> - + {header} {t('SIGNATURE_REQUEST.TITLE')} - - + - {bip0322Hash(payload.message)} - + {bip0322Hash(message)} + {t('SIGNATURE_REQUEST.SIGNING_ADDRESS_TITLE')} @@ -235,7 +212,7 @@ function SignMessageRequestInApp() { {addressType && {addressType}} - {getTruncatedAddress(payload.address, 6)} + {getTruncatedAddress(address, 6)} @@ -243,7 +220,7 @@ function SignMessageRequestInApp() { - setIsModalVisible(false)}> + setIsModalVisible(false)}> {currentStepIndex === 0 && ( + ); +} diff --git a/src/app/components/rareSatIcon/rareSatIcon.tsx b/src/app/components/rareSatIcon/rareSatIcon.tsx index 9a2a4e376..79130d074 100644 --- a/src/app/components/rareSatIcon/rareSatIcon.tsx +++ b/src/app/components/rareSatIcon/rareSatIcon.tsx @@ -35,8 +35,8 @@ import styled from 'styled-components'; const Image = styled.img<{ size?: number }>` object-fit: cover; - width: ${(props) => `${props.size}px` ?? '100%'}; - height: ${(props) => `${props.size}px` ?? '100%'}; + width: ${(props) => (typeof props.size === 'undefined' ? '100%' : `${props.size}px`)}; + height: ${(props) => (typeof props.size === 'undefined' ? '100%' : `${props.size}px`)}; `; interface Props { diff --git a/src/app/components/receiveCardComponent/index.tsx b/src/app/components/receiveCardComponent/index.tsx index 8e3e3348f..099e25b18 100644 --- a/src/app/components/receiveCardComponent/index.tsx +++ b/src/app/components/receiveCardComponent/index.tsx @@ -1,7 +1,9 @@ import Copy from '@assets/img/nftDashboard/Copy.svg'; import QrCode from '@assets/img/nftDashboard/QrCode.svg'; import Tick from '@assets/img/tick.svg'; +import { BtcAddressTypeForAddressLabel } from '@components/btcAddressTypeLabel'; import ActionButton from '@components/button'; +import useWalletSelector from '@hooks/useWalletSelector'; import { getShortTruncatedAddress } from '@utils/helper'; import { useEffect, useState, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +14,18 @@ import styled from 'styled-components'; const ReceiveCard = styled.div((props) => ({ background: props.theme.colors.elevation6_600, borderRadius: props.theme.radius(2), - width: 328, - height: 104, + width: '100%', + minHeight: 105, padding: props.theme.space.m, + display: 'flex', + flexDirection: 'column', +})); + +const Container = styled.div({ display: 'flex', flexDirection: 'row', alignItems: 'center', -})); +}); const Button = styled.button((props) => ({ background: props.theme.colors.elevation6, @@ -52,6 +59,10 @@ const TitleText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, marginTop: props.theme.spacing(3), color: props.theme.colors.white_0, + display: 'flex', + flexDirection: 'row', + gap: props.theme.space.xs, + alignItems: 'center', })); const AddressText = styled.h1((props) => ({ @@ -71,16 +82,17 @@ const VerifyButtonContainer = styled.div({ width: 68, }); -interface Props { +type Props = { className?: string; title: string; address: string; onQrAddressClick: () => void; - children: ReactNode; + children?: ReactNode; onCopyAddressClick?: () => void; showVerifyButton?: boolean; currency?: string; -} + icon?: React.ReactNode; +}; function ReceiveCardComponent({ className, @@ -91,9 +103,13 @@ function ReceiveCardComponent({ onCopyAddressClick, showVerifyButton, currency, + icon, }: Props) { const [isCopied, setIsCopied] = useState(false); const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + + const { network } = useWalletSelector(); + let addressText = 'Receive Ordinals, Runes & BRC20 tokens'; if (currency === 'BTC') addressText = 'Receive payments in BTC'; @@ -115,43 +131,51 @@ function ReceiveCardComponent({ return ( - - {children} - {title} - - {showVerifyButton ? addressText : getShortTruncatedAddress(address)} - - - {showVerifyButton ? ( - - { - await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/verify-ledger?currency=${currency}`), - }); - }} - /> - - ) : ( - - - - )} + + + {icon} + + {title} + {currency === 'BTC' && ( + + )} + + + {showVerifyButton ? addressText : getShortTruncatedAddress(address)} + + + {showVerifyButton ? ( + + { + await chrome.tabs.create({ + url: chrome.runtime.getURL(`options.html#/verify-ledger?currency=${currency}`), + }); + }} + /> + + ) : ( + + + + )} + + {children} ); } diff --git a/src/app/components/recipientComponent/index.tsx b/src/app/components/recipientComponent/index.tsx index f0424c984..5a14ef5ed 100644 --- a/src/app/components/recipientComponent/index.tsx +++ b/src/app/components/recipientComponent/index.tsx @@ -3,14 +3,14 @@ import WalletIcon from '@assets/img/transactions/wallet.svg'; import { StyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; import TransferDetailView from '@components/transferDetailView'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { CubeTransparent } from '@phosphor-icons/react'; import { type FungibleToken, getFiatEquivalent } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import type { CurrencyTypes } from '@utils/constants'; -import { getTicker } from '@utils/helper'; +import { getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -115,6 +115,13 @@ const FiatText = styled(StyledFiatAmountText)((props) => ({ marginTop: props.theme.space.xxxs, })); +const Title = styled.p` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.xs}; +`; + type Props = { address?: string; value: string; @@ -148,7 +155,7 @@ function RecipientComponent({ const [fiatAmount, setFiatAmount] = useState('0'); const { ordinalsAddress } = useSelectedAccount(); const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate, stxBtcRate } = useCoinRates(); + const { btcFiatRate, stxBtcRate } = useSupportedCoinRates(); useEffect(() => { setFiatAmount( @@ -162,16 +169,6 @@ function RecipientComponent({ ); }, [value]); - const getFtTicker = () => { - if (fungibleToken?.ticker) { - return fungibleToken?.ticker.toUpperCase(); - } - if (fungibleToken?.name) { - return getTicker(fungibleToken.name).toUpperCase(); - } - return ''; - }; - const renderIcon = () => { if (currencyType === 'RareSat') { return ( @@ -180,11 +177,9 @@ function RecipientComponent({ ); } - if (icon) { return ; } - return ( - {address && ( -
- {showSenderAddress ? ( - - - - - - ) : ( - - )} -
- )} - - {heading && {heading}} - {value && ( - - - {renderIcon()} -
- {title} - {currencyType === 'BTC' && Bitcoin} - {currencyType === 'STX' && Stacks} -
-
- {currencyType === 'NFT' || currencyType === 'Ordinal' || currencyType === 'RareSat' ? ( - - {value} - {valueDetail && {valueDetail}} - - ) : ( - - {amount}} + <> + {t('YOU_WILL_SEND')} + + {address && ( +
+ {showSenderAddress ? ( + + + + + + ) : ( + - - - )} - - )} - + )} +
+ )} + {heading && {heading}} + {value && ( + + + {renderIcon()} +
+ {title} + {currencyType === 'BTC' && Bitcoin} + {currencyType === 'STX' && Stacks} +
+
+ {currencyType === 'NFT' || currencyType === 'Ordinal' || currencyType === 'RareSat' ? ( + + {value} + {valueDetail && {valueDetail}} + + ) : ( + + {amount}} + /> + + + )} +
+ )} +
+ ); } diff --git a/src/app/components/requests/requestError.tsx b/src/app/components/requests/requestError.tsx index f7030dea7..3f0ecab39 100644 --- a/src/app/components/requests/requestError.tsx +++ b/src/app/components/requests/requestError.tsx @@ -7,13 +7,11 @@ import styled from 'styled-components'; */ const Container = styled.div((props) => ({ - background: props.theme.colors.elevation0, display: 'flex', flexDirection: 'column', height: '100%', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - paddingTop: props.theme.spacing(60), + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, })); const Image = styled.img({ @@ -23,34 +21,51 @@ const Image = styled.img({ }); const HeadingText = styled.h1((props) => ({ - ...props.theme.typography.headline_xs, + ...props.theme.typography.headline_s, color: props.theme.colors.white_0, textAlign: 'center', - marginTop: props.theme.spacing(8), + marginTop: props.theme.space.m, })); -const BodyText = styled.h1((props) => ({ - ...props.theme.typography.body_l, +const BodyText = styled.h1<{ $textAlignment: 'center' | 'left' }>((props) => ({ + ...props.theme.typography.body_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), - textAlign: 'center', + marginTop: props.theme.space.m, + textAlign: props.$textAlignment, overflowWrap: 'break-word', wordWrap: 'break-word', wordBreak: 'break-word', - marginBottom: props.theme.spacing(42), + whiteSpace: 'pre-line', +})); + +const OuterContainer = styled.div((_props) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flex: 1, })); -const CloseButton = styled(Button)((props) => ({ - marginBottom: props.theme.spacing(42), +const BodyContainer = styled.div({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', +}); + +const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.space.xl, + marginBottom: props.theme.space.xl, })); interface RequestErrorProps { errorTitle?: string; error: string; + textAlignment?: 'center' | 'left'; onClose?: () => void; } -function RequestError({ error, errorTitle, onClose }: RequestErrorProps) { +function RequestError({ error, errorTitle, onClose, textAlignment = 'center' }: RequestErrorProps) { const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const handleClose = () => { @@ -60,12 +75,19 @@ function RequestError({ error, errorTitle, onClose }: RequestErrorProps) { window.close(); } }; + return ( - - {errorTitle || t('INVALID_REQUEST')} - {error} - + + + + {errorTitle || t('INVALID_REQUEST')} + {error} + + + + - {text} - + {icon && {icon}} + + {text} + ); } diff --git a/src/app/components/stepper/index.tsx b/src/app/components/stepper/index.tsx deleted file mode 100644 index d73bdb191..000000000 --- a/src/app/components/stepper/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import checkmarkIcon from '@assets/img/checkmarkIcon.svg'; -import styled from 'styled-components'; - -interface StepperProps { - isActive?: boolean; - isCompleted?: boolean; -} - -const Container = styled.div({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - userSelect: 'none', -}); - -const Title = styled.div((props) => ({ - marginTop: props.theme.spacing(8), - fontSize: '0.875rem', - fontWeight: 500, - color: props.isActive ? props.theme.colors.white_0 : props.theme.colors.white_600, - transition: 'color 0.3s ease', -})); - -const getBackgroundColor = (props) => { - if (props.isCompleted) { - return props.theme.colors.success_medium; - } - if (props.isActive) { - return '#4C525F'; - } - return 'transparent'; -}; - -const Dot = styled.div((props) => ({ - height: 30, - width: 30, - backgroundColor: getBackgroundColor(props), - border: '2px solid', - borderColor: props.isCompleted - ? props.theme.colors.success_medium - : props.theme.colors.elevation6, - borderRadius: '50%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - color: props.theme.colors.white_0, - fontWeight: 'bold', - fontSize: '0.875rem', - flex: '1 0 auto', - transition: 'background-color 0.3s ease, border-color 0.3s ease', - '&::after': { - content: '""', - display: props.isActive ? 'block' : 'none', - position: 'absolute', - width: 8, - height: 8, - borderRadius: '50%', - backgroundColor: props.theme.colors.white_0, - }, -})); - -const calculateColorStops = (props) => { - const successColor = props.theme.colors.success_medium; - - // Calculate the color stops - let startStop: string; - if (props.step === 0) startStop = '0%'; - if (props.step > 1) startStop = '100%'; - else startStop = '50%'; - const endStop = props.step > 0 ? '50%' : '0%'; - - return `${successColor} ${startStop}, #4C525F ${endStop}`; -}; - -type LineProps = { step: number }; -const Line = styled.div((props) => ({ - height: 2, - width: '100%', - background: `linear-gradient(90deg, ${calculateColorStops(props)})`, - marginTop: props.theme.spacing(8), -})); - -interface Props { - steps: { title: string; isCompleted: boolean }[]; -} - -// TODO: Make this component more generic -export default function Stepper({ steps }: Props): JSX.Element { - const currentStepIndex = steps.findIndex((step) => !step.isCompleted); - const currentStep = currentStepIndex > -1 ? currentStepIndex : steps.length; - - function getContentForDot(stepIndex: number, isCompleted: boolean, currentPosition: number) { - if (isCompleted) { - return Check Icon; - } - if (currentPosition !== stepIndex) { - return String(stepIndex + 1); - } - return ''; - } - return ( - <> - - - {getContentForDot(0, steps[0].isCompleted, currentStep)} - - - - {getContentForDot(1, steps[1].isCompleted, currentStep)} - - - - {steps.map((step, index) => ( - - {step.title} - - ))} - - - ); -} diff --git a/src/app/components/tabBar/index.tsx b/src/app/components/tabBar/index.tsx index 64846f5dc..28b71659e 100644 --- a/src/app/components/tabBar/index.tsx +++ b/src/app/components/tabBar/index.tsx @@ -4,6 +4,9 @@ import { isInOptions } from '@utils/helper'; import { useNavigate } from 'react-router-dom'; import styled, { useTheme } from 'styled-components'; +const BUTTON_WIDTH = 56; +const BUTTON_HEIGHT = 32; + const RowContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', @@ -11,46 +14,61 @@ const RowContainer = styled.div((props) => ({ width: '100%', minHeight: 64, justifyContent: 'space-between', - paddingLeft: props.theme.spacing(30), - paddingRight: props.theme.spacing(30), + paddingLeft: props.theme.space.xl, + paddingRight: props.theme.space.xl, borderTop: `1px solid ${props.theme.colors.elevation3}`, })); const MovingDiv = styled(animated.div)((props) => ({ - width: 56, - height: 32, + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, backgroundColor: props.theme.colors.white_900, position: 'absolute', - bottom: 18, - left: 20, + bottom: BUTTON_HEIGHT / 2, borderRadius: 16, })); const Button = styled.button({ - backgroundColor: 'transparent', zIndex: 2, + width: BUTTON_WIDTH, + height: BUTTON_HEIGHT, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 16, + backgroundColor: 'transparent', }); export type Tab = 'dashboard' | 'nft' | 'stacking' | 'explore' | 'settings'; -interface Props { +type Props = { tab: Tab; -} +}; + function BottomTabBar({ tab }: Props) { const navigate = useNavigate(); const theme = useTheme(); const getPosition = () => { - if (tab === 'nft') return 78; - if (tab === 'stacking') return 132; - if (tab === 'explore') return 186; - if (tab === 'settings') return 239; - return 25; + const containerPadding = parseInt(theme.space.xl, 10); + const gap = (window.innerWidth - 2 * containerPadding - 5 * BUTTON_WIDTH) / 4; + + switch (tab) { + case 'nft': + return containerPadding + BUTTON_WIDTH + gap; + case 'stacking': + return containerPadding + 2 * (BUTTON_WIDTH + gap); + case 'explore': + return containerPadding + 3 * (BUTTON_WIDTH + gap); + case 'settings': + return containerPadding + 4 * (BUTTON_WIDTH + gap); + default: // 'dashboard' + return containerPadding; + } }; const styles = useSpring({ - from: { x: getPosition() }, // TODO: enable slide animation - to: { x: getPosition() }, + left: getPosition(), // TODO: enable slide animation config: { duration: 200, easing: easings.easeOutCirc, diff --git a/src/app/components/toaster/index.tsx b/src/app/components/toaster/index.tsx new file mode 100644 index 000000000..72740d4ee --- /dev/null +++ b/src/app/components/toaster/index.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from 'react'; +import toast, { Toaster, useToasterStore } from 'react-hot-toast'; + +// this solution is from https://github.com/timolins/react-hot-toast/issues/31 +function useMaxToasts(max: number) { + const { toasts } = useToasterStore(); + + useEffect(() => { + toasts + .filter((t) => t.visible) // Only consider visible toasts + .filter((_, i) => i >= max) // Is toast index over limit? + .forEach((t) => toast.remove(t.id)); // Dismiss + }, [toasts, max]); +} + +export default function ToasterComponent({ + max = 3, + ...props +}: React.ComponentProps & { + max?: number; +}) { + useMaxToasts(max); + + return ; +} diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index 3243ed5f5..0a8bb3202 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -7,7 +7,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import type { FungibleToken } from '@secretkeylabs/xverse-core'; import { XVERSE_ORDIVIEW_URL, type CurrencyTypes } from '@utils/constants'; import { getTicker } from '@utils/helper'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; const DEFAULT_SIZE = 40; @@ -64,13 +64,14 @@ const ProtocolImage = styled.img({ width: '100%', }); -export interface TokenImageProps { +interface TokenImageProps { currency?: CurrencyTypes; fungibleToken?: FungibleToken; loading?: boolean; size?: number; round?: boolean; showProtocolIcon?: boolean; + customProtocolIcon?: string; } export default function TokenImage({ @@ -80,6 +81,7 @@ export default function TokenImage({ size, round, showProtocolIcon = true, + customProtocolIcon, }: TokenImageProps) { const { network } = useWalletSelector(); const ftProtocol = fungibleToken?.protocol; @@ -94,35 +96,35 @@ export default function TokenImage({ } }, [currency]); - const getProtocolIcon = () => { - if (!ftProtocol) { + const ticker = + fungibleToken?.ticker || + (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); + + const tickerComponent = () => ( + + {ticker.substring(0, 4)} + + ); + + const protocolIcon = useMemo(() => { + if (!ftProtocol && !customProtocolIcon) { return null; } - switch (ftProtocol) { - case 'stacks': - return ; - case 'brc-20': - return ; - case 'runes': - return ; - default: - return null; - } - }; - const renderIcon = () => { - const ticker = - fungibleToken?.ticker || - (fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || ''); + const protocolToIcon = { + 'brc-20': , + stacks: , + runes: , + }; - if (imageError) { - return ( - - {ticker.substring(0, 4)} - - ); - } + return ftProtocol ? ( + protocolToIcon[ftProtocol] + ) : ( + + ); + }, [ftProtocol, customProtocolIcon]); + const renderIcon = () => { if (!fungibleToken) { return ( ); } + if (fungibleToken.protocol === 'runes') { + if (fungibleToken.runeInscriptionId) { + return ( + setImageError(true)} + /> + ); + } + if (fungibleToken?.runeSymbol) { + return ( + + {fungibleToken.runeSymbol} + + ); + } + } if (fungibleToken?.image) { return ( ); } - if (fungibleToken.runeInscriptionId) { - return ( - setImageError(true)} - /> - ); - } - if (fungibleToken.runeSymbol) { - return ( - - {fungibleToken.runeSymbol} - - ); - } - - return ( - - {ticker.substring(0, 4)} - - ); + return tickerComponent(); }; if (loading) { @@ -180,9 +181,9 @@ export default function TokenImage({ return ( - {renderIcon()} - {ftProtocol && showProtocolIcon && ( - {getProtocolIcon()} + {imageError ? tickerComponent() : renderIcon()} + {showProtocolIcon && protocolIcon && ( + {protocolIcon} )} ); diff --git a/src/app/components/tokenTile/index.tsx b/src/app/components/tokenTile/index.tsx index 6a4d5bfcb..44959bbc1 100644 --- a/src/app/components/tokenTile/index.tsx +++ b/src/app/components/tokenTile/index.tsx @@ -1,16 +1,16 @@ import { BetterBarLoader } from '@components/barLoader'; import { StyledFiatAmountText } from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance'; import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; +import useWalletSelector from '@hooks/useWalletSelector'; import { getFiatEquivalent, type FungibleToken } from '@secretkeylabs/xverse-core'; -import type { StoreState } from '@stores/index'; import type { CurrencyTypes } from '@utils/constants'; +import { HIDDEN_BALANCE_LABEL } from '@utils/constants'; import { getBalanceAmount, getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { NumericFormat } from 'react-number-format'; -import { useSelector } from 'react-redux'; import styled from 'styled-components'; const TileContainer = styled.button((props) => ({ @@ -18,12 +18,8 @@ const TileContainer = styled.button((props) => ({ flexDirection: 'row', backgroundColor: props.color, width: '100%', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - paddingTop: props.theme.spacing(7.25), - paddingBottom: props.theme.spacing(7.25), + padding: `${props.theme.space.m} 0`, borderRadius: props.theme.radius(2), - marginBottom: props.theme.spacing(0), })); const RowContainer = styled.div({ @@ -59,11 +55,11 @@ const CoinTickerText = styled.p((props) => ({ })); const SubText = styled.p<{ fullWidth: boolean }>((props) => ({ - ...props.theme.typography.body_medium_m, + ...props.theme.typography.body_medium_s, color: props.theme.colors.white_200, - fontSize: 12, textAlign: 'left', - maxWidth: props.fullWidth ? undefined : 100, + maxWidth: props.fullWidth ? 'unset' : 120, + whiteSpace: props.fullWidth ? 'normal' : 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', })); @@ -88,9 +84,19 @@ const StyledBarLoader = styled(BetterBarLoader)<{ }>((props) => ({ padding: 0, borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(2) : 0, + marginBottom: props.withMarginBottom ? props.theme.space.xxs : 0, })); +const CoinBalanceContainer = styled.div` + ${(props) => props.theme.typography.body_medium_m} + color: ${(props) => props.theme.colors.white_0}; +`; + +const FiatAmountContainer = styled.div` + ${(props) => props.theme.typography.body_medium_s} + color: ${(props) => props.theme.colors.white_400}; +`; + function TokenLoader() { return ( @@ -100,7 +106,7 @@ function TokenLoader() { ); } -interface Props { +type Props = { title: string; loading?: boolean; currency: CurrencyTypes; @@ -109,8 +115,8 @@ interface Props { enlargeTicker?: boolean; className?: string; showProtocolIcon?: boolean; - hideBalance?: boolean; -} + hideSwapBalance?: boolean; +}; function TokenTile({ title, @@ -121,12 +127,12 @@ function TokenTile({ enlargeTicker = false, className, showProtocolIcon = true, - hideBalance = false, + hideSwapBalance = false, }: Props) { - const { fiatCurrency } = useSelector((state: StoreState) => state.walletState); - const { btcFiatRate, stxBtcRate } = useCoinRates(); + const { fiatCurrency, balanceHidden } = useWalletSelector(); + const { btcFiatRate, stxBtcRate } = useSupportedCoinRates(); const { data: stxData } = useStxWalletData(); - const { data: btcBalance } = useBtcWalletData(); + const { confirmedPaymentBalance: btcBalance } = useSelectedAccountBtcBalance(); const getTickerTitle = () => { if (currency === 'STX' || currency === 'BTC') return `${currency}`; @@ -162,26 +168,33 @@ function TokenTile({ {getTickerTitle()} - + {title}
- {loading ? ( - - ) : ( - !hideBalance && ( - - {value}} - /> - - - ) + {loading && } + {!loading && !hideSwapBalance && ( + + + {balanceHidden && HIDDEN_BALANCE_LABEL} + {!balanceHidden && ( + {value}} + /> + )} + + + {balanceHidden && HIDDEN_BALANCE_LABEL} + {!balanceHidden && ( + + )} + + )} ); diff --git a/src/app/components/tooltip/context.ts b/src/app/components/tooltip/context.ts new file mode 100644 index 000000000..7688a3453 --- /dev/null +++ b/src/app/components/tooltip/context.ts @@ -0,0 +1,8 @@ +import { createContext } from 'react'; + +export type TooltipContext = { + targetDiv?: HTMLDivElement; + initialised: boolean; +}; + +export const tooltipContext = createContext({ initialised: false }); diff --git a/src/app/components/tooltip/index.tsx b/src/app/components/tooltip/index.tsx new file mode 100644 index 000000000..517949008 --- /dev/null +++ b/src/app/components/tooltip/index.tsx @@ -0,0 +1,93 @@ +import Callout, { type CalloutProps } from '@ui-library/callout'; +import { useContext, useEffect, useState, type MutableRefObject } from 'react'; +import { createPortal } from 'react-dom'; +import styled from 'styled-components'; +import { tooltipContext } from './context'; + +type Position = { + $left?: number; + $right?: number; + $top?: number; + $bottom?: number; +}; + +const TooltipContainer = styled.div` + position: absolute; + ${(props) => (props.$left !== undefined ? `left: ${props.$left}px` : '')}; + ${(props) => (props.$right !== undefined ? `right: ${props.$right}px` : '')}; + ${(props) => (props.$top !== undefined ? `top: ${props.$top}px` : '')}; + ${(props) => (props.$bottom !== undefined ? `bottom: ${props.$bottom}px` : '')}; + + &::after { + content: ''; + position: absolute; + ${(props) => (props.$left !== undefined ? `left: 10px` : '')}; + ${(props) => (props.$right !== undefined ? `right: 10px` : '')}; + ${(props) => (props.$top !== undefined ? `top: -5px` : '')}; + ${(props) => (props.$bottom !== undefined ? `bottom: -5px` : '')}; + border-width: 5px; + border-style: solid; + border-color: #2d2f34; + transform: rotate(45deg); + } +`; + +const StyledCallout = styled(Callout)(() => ({ + backgroundColor: '#2d2f34', +})); + +type Props = CalloutProps & { + target: MutableRefObject; + positionVertical?: 'top' | 'bottom'; + positionHorizontal?: 'left' | 'right'; +}; + +export default function TooltipCallout(props: Props) { + const tooltip = useContext(tooltipContext); + + const [position, setPosition] = useState({}); + + const { + target, + className, + positionHorizontal = 'left', + positionVertical = 'bottom', + ...calloutProps + } = props; + + const tooltipBody = ( + + + + ); + + useEffect(() => { + if (!target.current) { + console.warn('Tooltip set without a target'); + return; + } + + const targetRect = target.current.getBoundingClientRect(); + const newPosition: Position = {}; + + const spacer = 5; + + if (positionVertical === 'bottom') { + newPosition.$top = targetRect.y + targetRect.height + spacer; + } else { + newPosition.$bottom = window.innerHeight - targetRect.y + spacer; + } + + if (positionHorizontal === 'left') { + newPosition.$right = window.innerWidth - targetRect.x - targetRect.width; + } else { + newPosition.$left = targetRect.x; + } + + setPosition(newPosition); + }, [positionHorizontal, positionVertical, target]); + + if (tooltip.initialised && tooltip.targetDiv) { + return createPortal(tooltipBody, tooltip.targetDiv); + } +} diff --git a/src/app/components/tooltip/provider.tsx b/src/app/components/tooltip/provider.tsx new file mode 100644 index 000000000..abfba93af --- /dev/null +++ b/src/app/components/tooltip/provider.tsx @@ -0,0 +1,29 @@ +import { useEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; +import { tooltipContext, type TooltipContext } from './context'; + +const TooltipContainer = styled.div(() => ({ + width: 0, + height: 0, + overflow: 'hidden', +})); + +type Props = { + children: React.ReactNode; +}; + +export default function TooltipProvider({ children }: Props) { + const tooltipParentRef = useRef(null); + const [contextValue, setContextValue] = useState({ initialised: false }); + + useEffect(() => { + setContextValue({ initialised: true, targetDiv: tooltipParentRef.current ?? undefined }); + }, []); + + return ( + + {children} + + + ); +} diff --git a/src/app/components/topRow/index.tsx b/src/app/components/topRow/index.tsx index 4d2621063..310d2d376 100644 --- a/src/app/components/topRow/index.tsx +++ b/src/app/components/topRow/index.tsx @@ -1,13 +1,12 @@ -import { ArrowLeft, DotsThreeVertical } from '@phosphor-icons/react'; +import { ArrowLeft, DotsThreeVertical, FadersHorizontal, Star } from '@phosphor-icons/react'; +import type { MutableRefObject } from 'react'; import styled from 'styled-components'; import Theme from 'theme'; const TopSectionContainer = styled.div((props) => ({ display: 'flex', - minHeight: 18, - marginTop: props.theme.space.l, - marginBottom: props.theme.spacing(9), - marginLeft: props.theme.space.m, + minHeight: 20, + margin: `${props.theme.space.l} ${props.theme.space.m}`, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', @@ -32,14 +31,25 @@ const BackButton = styled.button((props) => ({ }, })); -const MenuButton = styled.button((props) => ({ +const MenuButton = styled.button({ display: 'flex', justifyContent: 'flex-end', backgroundColor: 'transparent', - padding: props.theme.space.xxs, position: 'absolute', - right: props.theme.space.xxs, -})); + right: 0, + opacity: 0.7, + transition: 'opacity 0.1s ease', + '&:hover': { + opacity: 1, + }, + '&:active': { + opacity: 0.6, + }, +}); + +const StyledMenuButton = styled(MenuButton)<{ $marginRight: boolean }>` + margin-right: ${(props) => (props.$marginRight ? '36px' : '0px')}; +`; type Props = { title?: string; @@ -47,9 +57,23 @@ type Props = { showBackButton?: boolean; className?: string; onMenuClick?: (e: React.MouseEvent) => void; + onSettingsClick?: (e: React.MouseEvent) => void; + settingsRef?: MutableRefObject; + onStarClick?: (e: React.MouseEvent) => void; + isStarred?: boolean; }; -function TopRow({ title, onClick, showBackButton = true, className, onMenuClick }: Props) { +function TopRow({ + title, + onClick, + showBackButton = true, + className, + onMenuClick, + onSettingsClick, + settingsRef, + onStarClick, + isStarred, +}: Props) { return ( {showBackButton && ( @@ -58,9 +82,20 @@ function TopRow({ title, onClick, showBackButton = true, className, onMenuClick )} {title && {title}} + {onStarClick && ( + + {!isStarred && } + {isStarred && } + + )} {onMenuClick && ( - + + + )} + {onSettingsClick && ( + + )} diff --git a/src/app/components/transactionDetailComponent/index.tsx b/src/app/components/transactionDetailComponent/index.tsx index 4e4ed3017..94acf8027 100644 --- a/src/app/components/transactionDetailComponent/index.tsx +++ b/src/app/components/transactionDetailComponent/index.tsx @@ -65,7 +65,6 @@ function TransactionDetailComponent({ titleColor, }: Props) { const { fiatCurrency } = useWalletSelector(); - return ( diff --git a/src/app/components/transactionSetting/editBtcFee.tsx b/src/app/components/transactionSetting/editBtcFee.tsx deleted file mode 100644 index eb012a6e1..000000000 --- a/src/app/components/transactionSetting/editBtcFee.tsx +++ /dev/null @@ -1,408 +0,0 @@ -import FiatAmountText from '@components/fiatAmountText'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; -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 { - ErrorCodes, - getBtcFees, - getBtcFeesForNonOrdinalBtcSend, - getBtcFeesForOrdinalSend, - getBtcFiatEquivalent, - type Recipient, - type UTXO, -} from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import Theme from 'theme'; -import FeeItem from './feeItem'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingBottom: props.theme.space.m, -})); - -const DetailText = styled.h1((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, -})); - -const InputContainer = styled.div<{ - withError?: boolean; -}>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginTop: props.theme.space.xs, - marginBottom: props.theme.space.s, - border: `1px solid ${ - props.withError ? props.theme.colors.danger_medium : props.theme.colors.elevation6 - }`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: props.theme.radius(1), - padding: props.theme.space.s, -})); - -const InputField = styled.input((props) => ({ - ...props.theme.typography.body_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - border: 'transparent', - width: '50%', - '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&[type=number]': { - '-moz-appearance': 'textfield', - }, -})); - -const FeeText = styled.span((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_0, -})); - -const FeeContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const ErrorText = styled.h1((props) => ({ - ...props.theme.typography.body_s, - color: props.theme.colors.danger_light, - marginBottom: props.theme.space.xxs, -})); - -const FeePrioritiesContainer = styled.div` - display: flex; - margin-top: ${(props) => props.theme.space.m}; - flex-direction: column; -`; - -const FeeItemContainer = styled.button<{ - isSelected: boolean; -}>` - display: flex; - padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; - align-items: center; - gap: ${(props) => props.theme.space.s}; - align-self: stretch; - border-radius: ${(props) => props.theme.space.s}; - border: 1px solid ${(props) => props.theme.colors.elevation6}; - flex-direction: row; - background: ${(props) => (props.isSelected ? props.theme.colors.elevation6_600 : 'transparent')}; - margin-top: ${(props) => props.theme.space.xs}; - flex: 1; - transition: background-color 0.1s ease; - - &:hover:enabled { - background-color: ${(props) => props.theme.colors.elevation6_400}; - } - - &:active:enabled { - background-color: ${(props) => props.theme.colors.elevation6_600}; - } -`; - -const TextRow = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - flex: 1; -`; - -const CustomTextsContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - flex: 1; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const TotalFeeText = styled(StyledP)` - margin-right: ${(props) => props.theme.space.xxs}; -`; - -type Props = { - type?: string; - fee: string; - feeRate?: BigNumber | string; - btcRecipients?: Recipient[]; - ordinalTxUtxo?: UTXO; - isRestoreFlow?: boolean; - nonOrdinalUtxos?: UTXO[]; - feeMode: string; - error: string; - customFeeSelected: boolean; - setIsLoading: () => void; - setIsNotLoading: () => void; - setFee: (fee: string) => void; - setFeeRate: (feeRate: string) => void; - setFeeMode: (feeMode: string) => void; - setError: (error: string) => void; - setCustomFeeSelected: (selected: boolean) => void; - feeOptionSelected: (feeRate: string, totalFee: string) => void; -}; - -function EditBtcFee({ - type, - fee, - feeRate, - btcRecipients, - ordinalTxUtxo, - isRestoreFlow, - nonOrdinalUtxos, - feeMode, - error, - customFeeSelected, - setIsLoading, - setIsNotLoading, - setFee, - setFeeRate, - setError, - setFeeMode, - setCustomFeeSelected, - feeOptionSelected, -}: Props) { - const { t } = useTranslation('translation'); - - 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() ?? ''); - const inputRef = useRef(null); - const debouncedFeeRateInput = useDebounce(feeRateInput, 500); - const { ordinals } = useOrdinalsByAddress(btcAddress); - const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); - const btcClient = useBtcClient(); - const { feeData, highFeeError, mediumFeeError } = useBtcFees({ - isRestoreFlow: !!isRestoreFlow, - nonOrdinalUtxos, - btcRecipients, - type, - ordinalTxUtxo, - }); - - useEffect(() => { - setFee(totalFee); - }, [totalFee]); - - const recalculateFees = async () => { - if (type === 'BTC') { - try { - setIsLoading(); - setError(''); - - if (isRestoreFlow) { - const { fee: modifiedFee, selectedFeeRate } = await getBtcFeesForNonOrdinalBtcSend( - btcAddress, - nonOrdinalUtxos!, - ordinalsAddress, - network.type, - feeMode, - feeRateInput, - ); - setFeeRateInput(selectedFeeRate!.toString()); - setTotalFee(modifiedFee.toString()); - } else if (btcRecipients && selectedAccount) { - const { fee: modifiedFee, selectedFeeRate } = await getBtcFees( - btcRecipients, - btcAddress, - btcClient, - network.type, - feeMode, - feeRateInput, - ); - setFeeRateInput(selectedFeeRate!.toString()); - setTotalFee(modifiedFee.toString()); - } - } catch (err: any) { - if (Number(err) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(err) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(err.toString()); - } finally { - setIsNotLoading(); - } - } else if (type === 'Ordinals' && btcRecipients && ordinalTxUtxo) { - try { - setIsLoading(); - setError(''); - - const { fee: modifiedFee, selectedFeeRate } = await getBtcFeesForOrdinalSend( - btcRecipients[0].address, - ordinalTxUtxo, - btcAddress, - btcClient, - network.type, - ordinalsUtxos || [], - feeMode, - feeRateInput, - ); - if (selectedFeeRate) setFeeRateInput(selectedFeeRate.toString()); - setTotalFee(modifiedFee.toString()); - } catch (err: any) { - if (Number(err) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(err) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(err.toString()); - } finally { - setIsNotLoading(); - } - } - }; - - useEffect(() => { - if (feeRateInput) { - setFeeRate(feeRateInput); - } - }, [feeRateInput]); - - useEffect(() => { - if (debouncedFeeRateInput) { - recalculateFees(); - } - }, [debouncedFeeRateInput]); - - const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { - if (error) { - setError(''); - } - - if (feeMode !== 'custom') { - setFeeMode('custom'); - } - - setFeeRateInput(value); - - if (type !== 'BTC' && type !== 'Ordinals') { - setFeeRateInput(value); - setTotalFee(value); - } - }; - - return ( - - {t('TRANSACTION_SETTING.FEE_INFO')} - - {!customFeeSelected && ( - - { - feeOptionSelected(feeData?.highFeeRate?.toString() || '', feeData?.highTotalFee); - setFeeMode('high'); - }} - selected={feeMode === 'high'} - error={highFeeError} - /> - { - feeOptionSelected( - feeData?.standardFeeRate?.toString() || '', - feeData?.standardTotalFee, - ); - setFeeMode('medium'); - }} - selected={feeMode === 'medium'} - error={mediumFeeError} - /> - { - setCustomFeeSelected(true); - }} - > - - - - {t('TRANSACTION_SETTING.CUSTOM')} - - - {t('TRANSACTION_SETTING.MANUAL_SETTING')} - - - - - )} - - {customFeeSelected && ( - - - - {t('UNITS.SATS_PER_VB')} - - - - - {t('TRANSACTION_SETTING.TOTAL_FEE')}: - - ( - - {value} - - )} - /> - - - - {error && {error}} - - )} - - ); -} - -export default EditBtcFee; diff --git a/src/app/components/transactionSetting/editStxFee.tsx b/src/app/components/transactionSetting/editStxFee.tsx index 117c68e9e..9f2925f4a 100644 --- a/src/app/components/transactionSetting/editStxFee.tsx +++ b/src/app/components/transactionSetting/editStxFee.tsx @@ -1,5 +1,5 @@ import FiatAmountText from '@components/fiatAmountText'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; import { getStxFiatEquivalent, stxToMicrostacks } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; @@ -105,7 +105,6 @@ const ErrorText = styled.p((props) => ({ // TODO tim: this component needs refactoring. separate business logic from presentation type Props = { - type?: string; fee: string; feeRate?: BigNumber | string; feeMode: string; @@ -117,7 +116,6 @@ type Props = { }; function EditStxFee({ - type, fee, feeRate, feeMode, @@ -129,11 +127,10 @@ function EditStxFee({ }: Props) { const { t } = useTranslation('translation'); const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate, stxBtcRate } = useCoinRates(); + const { btcFiatRate, stxBtcRate } = useSupportedCoinRates(); const [totalFee, setTotalFee] = useState(fee); const [feeRateInput, setFeeRateInput] = useState(feeRate?.toString() ?? ''); const inputRef = useRef(null); - const isStx = type === 'STX'; const modifyStxFees = (mode: string) => { const currentFee = new BigNumber(fee); @@ -161,7 +158,7 @@ function EditStxFee({ }; useEffect(() => { - if (isStx && feeMode !== 'custom') { + if (feeMode !== 'custom') { modifyStxFees(feeMode); } }, [feeMode]); @@ -215,11 +212,9 @@ function EditStxFee({ {error && {error}} - {isStx && ( - modifyStxFees('low')}> - {t('TRANSACTION_SETTING.LOW')} - - )} + modifyStxFees('low')}> + {t('TRANSACTION_SETTING.LOW')} + modifyStxFees('medium')}> {t('TRANSACTION_SETTING.STANDARD')} diff --git a/src/app/components/transactionSetting/feeItem.tsx b/src/app/components/transactionSetting/feeItem.tsx deleted file mode 100644 index 4af201283..000000000 --- a/src/app/components/transactionSetting/feeItem.tsx +++ /dev/null @@ -1,186 +0,0 @@ -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'; - -const FeeItemContainer = styled.button<{ - isSelected: boolean; -}>` - display: flex; - padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; - align-items: center; - gap: ${(props) => props.theme.space.s}; - align-self: stretch; - border-radius: ${(props) => props.theme.space.s}; - border: 1px solid ${(props) => props.theme.colors.elevation6}; - flex-direction: row; - background: ${(props) => (props.isSelected ? props.theme.colors.elevation6_600 : 'transparent')}; - margin-top: ${(props) => props.theme.space.xs}; - flex: 1; - transition: background-color 0.1s ease; - - &:hover:enabled { - background-color: ${(props) => props.theme.colors.elevation6_400}; - } - - &:active:enabled { - background-color: ${(props) => props.theme.colors.elevation6_600}; - } -`; - -const IconContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; -`; - -const TextsContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - - flex: 1; -`; - -const ColumnsTexts = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; -`; -const EndColumnTexts = styled.div` - display: flex; - flex-direction: column; - align-items: flex-end; -`; - -const StyledHeading = styled(StyledP)` - margin-bottom: ${(props) => props.theme.space.xxs}; -`; - -const StyledSubText = styled(StyledP)` - margin-bottom: ${(props) => props.theme.space.xxs}; -`; - -const LoaderContainer = styled.div` - display: flex; - align-items: center; - justify-content: flex-end; - 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'; - -type Props = { - priority: FeePriority; - time: string; - feeRate: string; - totalFee: string; - fiatAmount: BigNumber; - selected: boolean; - onClick?: () => void; - error?: string; -}; - -function FeeItem({ - priority, - time, - feeRate, - totalFee, - fiatAmount, - selected, - error, - onClick, -}: Props) { - const { t } = useTranslation('translation'); - const { fiatCurrency } = useWalletSelector(); - - const getIcon = () => { - switch (priority) { - case 'high': - return ; - case 'medium': - return ; - case 'low': - return ; - default: - return ; - } - }; - - const getLabel = () => { - switch (priority) { - case 'high': - return t('SPEED_UP_TRANSACTION.HIGH_PRIORITY'); - case 'medium': - return t('SPEED_UP_TRANSACTION.MED_PRIORITY'); - case 'low': - return t('SPEED_UP_TRANSACTION.LOW_PRIORITY'); - default: - return t('SPEED_UP_TRANSACTION.HIGH_PRIORITY'); - } - }; - - const getErrorMessage = (btcError: string) => { - if ( - Number(btcError) === ErrorCodes.InSufficientBalance || - Number(btcError) === ErrorCodes.InSufficientBalanceWithTxFee - ) { - return t('SEND.ERRORS.INSUFFICIENT_BALANCE'); - } - return btcError; - }; - - return ( - - {getIcon()} - - - - {getLabel()} - - - {time} - - {`${feeRate} sats/vB`} - - - - {totalFee && ( - - {`${totalFee} sats`} - - )} - - {error && ( - - {getErrorMessage(error)} - - )} - - - {!totalFee && !error && ( - - - - )} - - - ); -} - -export default FeeItem; diff --git a/src/app/components/transactionSetting/index.tsx b/src/app/components/transactionSetting/index.tsx index 7ae8c456e..d3701050c 100644 --- a/src/app/components/transactionSetting/index.tsx +++ b/src/app/components/transactionSetting/index.tsx @@ -1,20 +1,12 @@ import ArrowIcon from '@assets/img/settings/arrow.svg'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useStxWalletData from '@hooks/queries/useStxWalletData'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { - isCustomFeesAllowed, - stxToMicrostacks, - type Recipient, - type UTXO, -} from '@secretkeylabs/xverse-core'; +import { stxToMicrostacks } 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'; import styled from 'styled-components'; -import EditBtcFee from './editBtcFee'; import EditNonce from './editNonce'; import EditStxFee from './editStxFee'; @@ -49,21 +41,13 @@ const TransactionSettingNonceOptionButton = styled.button((props) => ({ justifyContent: 'space-between', })); -type TxType = 'STX' | 'BTC' | 'Ordinals'; - type Props = { visible: boolean; fee: string; - feePerVByte?: BigNumber; loading?: boolean; nonce?: string; onApplyClick: (params: { fee: string; feeRate?: string; nonce?: string }) => void; onCrossClick: () => void; - type?: TxType; - btcRecipients?: Recipient[]; - ordinalTxUtxo?: UTXO; - isRestoreFlow?: boolean; - nonOrdinalUtxos?: UTXO[]; showFeeSettings: boolean; nonceSettings?: boolean; setShowFeeSettings: (value: boolean) => void; @@ -72,32 +56,22 @@ type Props = { function TransactionSettingAlert({ visible, fee, - feePerVByte, loading, nonce, onApplyClick, onCrossClick, - type = 'STX', - btcRecipients, - ordinalTxUtxo, - isRestoreFlow, - nonOrdinalUtxos, showFeeSettings, nonceSettings = false, setShowFeeSettings, }: Props) { const { t } = useTranslation('translation'); const [feeInput, setFeeInput] = useState(fee); - const [feeRate, setFeeRate] = useState(feePerVByte); + const [feeRate, setFeeRate] = useState(); const [nonceInput, setNonceInput] = useState(nonce); const [error, setError] = useState(''); const [selectedOption, setSelectedOption] = useState('medium'); const [showNonceSettings, setShowNonceSettings] = useState(false); - const [isLoading, setIsLoading] = useState(loading); - const [customFeeSelected, setCustomFeeSelected] = useState(false); - const { network } = useWalletSelector(); const { data: stxData } = useStxWalletData(); - const { data: btcBalance } = useBtcWalletData(); const applyClickForStx = () => { if (stxData?.availableBalance) { @@ -130,40 +104,6 @@ function TransactionSettingAlert({ onApplyClick({ fee: feeInput.toString(), nonce: nonceInput }); }; - const applyClickForBtc = async () => { - const currentFee = new BigNumber(feeInput); - if (currentFee.gt(btcBalance ?? 0)) { - // show fee exceeds total balance error - setError(t('TRANSACTION_SETTING.GREATER_FEE_ERROR')); - return; - } - if (selectedOption === 'custom' && feeRate) { - const response = await isCustomFeesAllowed(network.type, feeRate.toString()); - if (!response) { - setError(t('TRANSACTION_SETTING.LOWER_THAN_MINIMUM')); - return; - } - } - setShowNonceSettings(false); - setShowFeeSettings(false); - setCustomFeeSelected(false); - setError(''); - onApplyClick({ fee: feeInput.toString(), feeRate: feeRate?.toString() }); - }; - - const btcFeeOptionSelected = async (selectedFeeRate: string, totalFee: string) => { - const currentFee = new BigNumber(feeInput); - if (currentFee.gt(btcBalance ?? 0)) { - // show fee exceeds total balance error - setError(t('TRANSACTION_SETTING.GREATER_FEE_ERROR')); - return; - } - setShowNonceSettings(false); - setShowFeeSettings(false); - setError(''); - onApplyClick({ fee: totalFee, feeRate: selectedFeeRate }); - }; - const onEditFeesPress = () => { setShowFeeSettings(true); }; @@ -172,14 +112,6 @@ function TransactionSettingAlert({ setShowNonceSettings(true); }; - const onLoading = () => { - setIsLoading(true); - }; - - const onComplete = () => { - setIsLoading(false); - }; - const onClosePress = () => { setShowNonceSettings(false); setShowFeeSettings(false); @@ -192,43 +124,16 @@ function TransactionSettingAlert({ } if (showFeeSettings) { - if (type === 'STX') { - return ( - - ); - } return ( - { - setCustomFeeSelected(selected); - }} - customFeeSelected={customFeeSelected} - feeOptionSelected={btcFeeOptionSelected} /> ); } @@ -241,14 +146,12 @@ function TransactionSettingAlert({ Arrow - {type === 'STX' && ( - - - {t('TRANSACTION_SETTING.ADVANCED_SETTING_NONCE_OPTION')} - - Arrow - - )} + + + {t('TRANSACTION_SETTING.ADVANCED_SETTING_NONCE_OPTION')} + + Arrow + ); }; @@ -266,7 +169,7 @@ function TransactionSettingAlert({ onClose={onClosePress} > {renderContent()} - {type === 'STX' && (showFeeSettings || showNonceSettings || nonceSettings) && ( + {(showFeeSettings || showNonceSettings || nonceSettings) && ( + + + )} + {showTxHistory && ( + + )} + {showStxContract && ( + +

{t('FT_CONTRACT_PREFIX')}

+ navigator.clipboard.writeText(selectedFt?.principal as string)} + > + + {getTruncatedAddress(selectedFt?.principal as string, 20)} + + + + + + window.open(getExplorerUrl(selectedFt?.principal as string), '_blank')} + > + {t('OPEN_FT_CONTRACT_DEPLOYMENT')} + {t('STACKS_EXPLORER')} + + +
+ )} + {showRuneBundles && ( + + + {runeUtxos?.map((utxo) => { + const fullTxId = getFullTxId(utxo); + const runeAmount = utxo.runes?.filter((rune) => rune[0] === selectedFt?.name)[0][1] + .amount; + return ( + + ); + })} + + + )} +
+ + + ); +} diff --git a/src/app/screens/coinDashboard/index.styled.ts b/src/app/screens/coinDashboard/index.styled.ts new file mode 100644 index 000000000..d77300c26 --- /dev/null +++ b/src/app/screens/coinDashboard/index.styled.ts @@ -0,0 +1,120 @@ +import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flex: 1, + marginTop: props.theme.space.xs, + flexDirection: 'column', + overflowY: 'auto', + '&::-webkit-scrollbar': { + display: 'none', + }, +})); + +export const SecondaryContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginTop: props.theme.space.m, + marginBottom: props.theme.space.xl, + h1: { + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, + }, +})); + +export const ContractAddressCopyButton = styled.button((props) => ({ + display: 'flex', + marginTop: props.theme.space.xxs, + background: 'transparent', +})); + +export const TokenContractAddress = styled.p((props) => ({ + ...props.theme.typography.body_medium_l, + color: props.theme.colors.white_0, + textAlign: 'left', + overflowWrap: 'break-word', + width: 300, +})); + +export const FtInfoContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + borderTop: `1px solid ${props.theme.colors.elevation2}`, + paddingTop: props.theme.space.l, + marginTop: props.theme.space.xl, + paddingLeft: props.theme.space.m, +})); + +export const ShareIcon = styled.img({ + width: 18, + height: 18, +}); + +export const CopyButtonContainer = styled.div((props) => ({ + marginRight: props.theme.space.xxs, +})); + +export const ContractDeploymentButton = styled.button((props) => ({ + ...props.theme.typography.body_m, + display: 'flex', + alignItems: 'center', + marginTop: props.theme.space.l, + background: 'none', + color: props.theme.colors.white_400, + span: { + color: props.theme.colors.white_0, + marginLeft: props.theme.space.xs, + }, + img: { + marginLeft: props.theme.space.xs, + }, +})); + +export const Button = styled.button<{ + isSelected: boolean; +}>((props) => ({ + ...props.theme.typography.body_bold_l, + fontSize: 11, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: 31, + paddingLeft: props.theme.space.s, + paddingRight: props.theme.space.s, + marginRight: props.theme.space.xxs, + borderRadius: 44, + background: props.isSelected ? props.theme.colors.elevation2 : 'transparent', + color: props.theme.colors.white_0, + opacity: props.isSelected ? 1 : 0.6, +})); + +export 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}; + } +`; + +export const TokenText = styled(StyledP)` + margin-left: ${(props) => props.theme.space.m}; +`; + +export const RuneBundlesContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; +`; diff --git a/src/app/screens/coinDashboard/index.tsx b/src/app/screens/coinDashboard/index.tsx index 35d8a17b9..a89395893 100644 --- a/src/app/screens/coinDashboard/index.tsx +++ b/src/app/screens/coinDashboard/index.tsx @@ -1,319 +1,15 @@ -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/useRuneFungibleTokensQuery'; -import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useSpamTokens from '@hooks/queries/useSpamTokens'; -import { broadcastResetUserFlow, useResetUserFlow } from '@hooks/useResetUserFlow'; -import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; -import { Flag } from '@phosphor-icons/react'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; -import { - setBrc20ManageTokensAction, - setRunesManageTokensAction, - setSip10ManageTokensAction, - setSpamTokenAction, -} from '@stores/wallet/actions/actionCreators'; -import { StyledP } from '@ui-library/common.styled'; -import { SPAM_OPTIONS_WIDTH, type CurrencyTypes } from '@utils/constants'; -import { getExplorerUrl } from '@utils/helper'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -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'; - -const Container = styled.div((props) => ({ - display: 'flex', - flex: 1, - marginTop: props.theme.spacing(4), - flexDirection: 'column', - overflowY: 'auto', - '&::-webkit-scrollbar': { - display: 'none', - }, -})); - -const TokenContractContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginTop: props.theme.spacing(16), - h1: { - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, - }, -})); - -const ContractAddressCopyButton = styled.button((props) => ({ - display: 'flex', - marginTop: props.theme.spacing(2), - background: 'transparent', -})); - -const TokenContractAddress = styled.p((props) => ({ - ...props.theme.typography.body_medium_l, - color: props.theme.colors.white_0, - textAlign: 'left', - overflowWrap: 'break-word', - width: 300, -})); - -const FtInfoContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - borderTop: `1px solid ${props.theme.colors.elevation2}`, - paddingTop: props.theme.spacing(12), - marginTop: props.theme.spacing(16), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(14), -})); - -const ShareIcon = styled.img({ - width: 18, - height: 18, -}); - -const CopyButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(2), -})); - -const ContractDeploymentButton = styled.button((props) => ({ - ...props.theme.typography.body_m, - display: 'flex', - alignItems: 'center', - marginTop: props.theme.spacing(12), - background: 'none', - color: props.theme.colors.white_400, - span: { - color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(3), - }, - img: { - marginLeft: props.theme.spacing(3), - }, -})); - -const Button = styled.button<{ - isSelected: boolean; -}>((props) => ({ - ...props.theme.typography.body_bold_l, - fontSize: 11, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: 31, - paddingLeft: props.theme.spacing(6), - paddingRight: props.theme.spacing(6), - marginRight: props.theme.spacing(2), - borderRadius: 44, - background: props.isSelected ? props.theme.colors.elevation2 : 'transparent', - color: props.theme.colors.white_0, - 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}; -`; +import { useParams } from 'react-router-dom'; +import Btc from './coins/btc'; +import Other from './coins/other'; export default function CoinDashboard() { - const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); - const [showFtContractDetails, setShowFtContractDetails] = useState(false); - 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 protocol = searchParams.get('protocol'); - let selectedFt: FungibleToken | undefined; - 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; - } + switch (currency) { + case 'BTC': + return ; + default: + // TODO: split this more. Other is currently doing too much + return ; } - - useResetUserFlow('/coinDashboard'); - useBtcWalletData(); - - const handleGoBack = () => broadcastResetUserFlow(); - - useTrackMixPanelPageViewed( - protocol - ? { - protocol, - } - : {}, - ); - - 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 = () => - window.open(getExplorerUrl(selectedFt?.principal as string), '_blank'); - - const onContractClick = () => setShowFtContractDetails(true); - - const handleCopyContractAddress = () => - navigator.clipboard.writeText(selectedFt?.principal as string); - - const onTransactionsClick = () => setShowFtContractDetails(false); - - const formatAddress = (addr: string): string => - addr ? `${addr.substring(0, 20)}...${addr.substring(addr.length - 20, addr.length)}` : ''; - - 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' && ( - - - - - )} - {selectedFt && protocol === 'stacks' && showFtContractDetails && ( - -

{t('FT_CONTRACT_PREFIX')}

- - - {formatAddress(selectedFt?.principal as string)} - - - - - - - {t('OPEN_FT_CONTRACT_DEPLOYMENT')} - {t('STACKS_EXPLORER')} - - -
- )} - {!showFtContractDetails && ( - - )} -
- - - ); } diff --git a/src/app/screens/coinDashboard/runes/bundleRow.tsx b/src/app/screens/coinDashboard/runes/bundleRow.tsx new file mode 100644 index 000000000..8492595e5 --- /dev/null +++ b/src/app/screens/coinDashboard/runes/bundleRow.tsx @@ -0,0 +1,160 @@ +import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; +import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; +import useWalletSelector from '@hooks/useWalletSelector'; +import type { Bundle } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { HIDDEN_BALANCE_LABEL } from '@utils/constants'; +import { getTruncatedAddress } from '@utils/helper'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-between; + flex: 1; + padding: ${(props) => props.theme.space.s}; + padding-left ${(props) => props.theme.space.m}; + border-radius: ${(props) => props.theme.space.xs}; + border: 1px solid 'transparent'; + background-color: ${(props) => props.theme.colors.elevation1}; + gap: ${(props) => props.theme.space.s}; + :hover { + cursor: pointer; + }, +`; + +const InfoContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: ${(props) => props.theme.space.xxxs}; +`; + +const SubContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + justify-content: space-between; +`; + +const RuneTitle = styled(StyledP)` + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + text-align: left; +`; + +const StyledBundleSub = styled(StyledP)` + width: 100%; + text-align: left; +`; + +const SizeInfoContainer = styled.div` + display: flex; + align-items: center; + column-gap: ${(props) => props.theme.space.xxs}; +`; + +const RangeContainer = styled.div``; + +const Range = styled.div` + display: flex; + border-radius: 6px; + border: 1px solid ${(props) => props.theme.colors.white_800}; + padding: 1px; + flex-wrap: wrap; + flex-direction: row; + align-items: center; +`; + +type Props = { + runeAmount: string; + runeSymbol: string; + runeId: string; + txId: string; + vout: string; + satAmount: number; + bundle: Bundle; +}; + +function RuneBundleRow({ runeAmount, runeSymbol, runeId, txId, vout, satAmount, bundle }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const { balanceHidden } = useWalletSelector(); + const navigate = useNavigate(); + const satributesArr = bundle.satributes.flatMap((item) => item); + const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); + + const handleOnClick = () => { + // exotics v1 wont show range details only bundle details + setSelectedSatBundleDetails(bundle); + navigate('/nft-dashboard/rare-sats-bundle', { state: { source: 'RuneBundlesTab', runeId } }); + }; + + const onKeyDown = (event) => { + if (event.key === 'Enter') { + handleOnClick(); + } + }; + + return ( + + + + {satributesArr.map((satribute) => ( + + ))} + + + + {balanceHidden ? ( + + {HIDDEN_BALANCE_LABEL} + + ) : ( + ( + + {value} + + )} + /> + )} + + + {`${getTruncatedAddress(txId, 6)}:${vout}`} + + + ( + + {value} + + )} + /> + + + + + ); +} + +export default RuneBundleRow; diff --git a/src/app/screens/coinDashboard/transactionsHistoryList.tsx b/src/app/screens/coinDashboard/transactionsHistoryList.tsx index f78c73e08..ccc976e4e 100644 --- a/src/app/screens/coinDashboard/transactionsHistoryList.tsx +++ b/src/app/screens/coinDashboard/transactionsHistoryList.tsx @@ -18,7 +18,7 @@ import type { } from '@stacks/stacks-blockchain-api-types'; import Spinner from '@ui-library/spinner'; import type { CurrencyTypes } from '@utils/constants'; -import { formatDate } from '@utils/date'; +import { formatDate, formatDateKey } from '@utils/date'; import { isLedgerAccount } from '@utils/helper'; import { isAddressTransactionWithTransfers, @@ -34,18 +34,18 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -const ListItemsContainer = styled.div({ +const ListItemsContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', flex: 1, -}); + marginTop: props.theme.space.l, +})); const ListHeader = styled.h1((props) => ({ - marginTop: props.theme.spacing(20), - marginBottom: props.theme.spacing(12), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - ...props.theme.headline_s, + ...props.theme.typography.headline_xs, + margin: props.theme.space.m, + marginTop: 0, + marginBottom: props.theme.space.l, })); const LoadingContainer = styled.div({ @@ -65,7 +65,7 @@ const NoTransactionsContainer = styled.div((props) => ({ })); const GroupContainer = styled(animated.div)((props) => ({ - marginBottom: props.theme.spacing(8), + marginBottom: props.theme.space.m, })); const SectionHeader = styled.div((props) => ({ @@ -73,8 +73,8 @@ const SectionHeader = styled.div((props) => ({ flexDirection: 'row', alignItems: 'center', marginBottom: props.theme.spacing(7), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, })); const SectionSeparator = styled.div((props) => ({ @@ -86,87 +86,161 @@ const SectionSeparator = styled.div((props) => ({ const SectionTitle = styled.p((props) => ({ ...props.theme.body_xs, color: props.theme.colors.white_200, - marginRight: props.theme.spacing(4), + marginRight: props.theme.space.xs, })); -interface TransactionsHistoryListProps { - coin: CurrencyTypes; - stxTxFilter: string | null; - brc20Token: string | null; - runeToken: string | null; - runeSymbol: string | null; -} - -const sortTransactionsByBlockHeight = (transactions: BtcTransactionData[]) => - transactions.sort((txA, txB) => { - if (txB.blockHeight > txA.blockHeight) { - return 1; - } - return -1; - }); - const groupBtcTxsByDate = ( transactions: BtcTransactionData[], -): { [x: string]: BtcTransactionData[] } => { +): [Date, string, BtcTransactionData[]][] => { const pendingTransactions: BtcTransactionData[] = []; const processedTransactions: { [x: string]: BtcTransactionData[] } = {}; transactions.forEach((transaction) => { - const txDate = formatDate(new Date(transaction.seenTime)); if (transaction.txStatus === 'pending') { pendingTransactions.push(transaction); } else { - if (!processedTransactions[txDate]) processedTransactions[txDate] = [transaction]; - else processedTransactions[txDate].push(transaction); - - sortTransactionsByBlockHeight(processedTransactions[txDate]); + const txDateKey = formatDateKey(new Date(transaction.seenTime)); + if (!processedTransactions[txDateKey]) { + processedTransactions[txDateKey] = []; + } + processedTransactions[txDateKey].push(transaction); } }); - sortTransactionsByBlockHeight(pendingTransactions); + + const result: [Date, string, BtcTransactionData[]][] = []; + if (pendingTransactions.length > 0) { - const result = { Pending: pendingTransactions, ...processedTransactions }; - return result; + result.push([new Date(), 'Pending', pendingTransactions]); } - return processedTransactions; + + Object.values(processedTransactions).forEach((grp) => { + if (grp.length === 0) { + return; + } + + grp.sort((txA, txB) => { + // sort by block height first + const blockHeightDiff = txB.blockHeight - txA.blockHeight; + if (blockHeightDiff !== 0) { + return blockHeightDiff; + } + + // if block height is the same, sort by txid for consistency + return txB.txid.localeCompare(txA.txid); + }); + + result.push([new Date(grp[0].seenTime), formatDate(new Date(grp[0].seenTime)), grp]); + }); + + result.sort((a, b) => b[0].getTime() - a[0].getTime()); + + return result; }; const groupRuneTxsByDate = ( transactions: GetRunesActivityForAddressEvent[], -): Record => { - const mappedTransactions = {}; +): [Date, string, GetRunesActivityForAddressEvent[]][] => { + const processedTransactions: { [x: string]: GetRunesActivityForAddressEvent[] } = {}; + transactions.forEach((transaction) => { - const txDate = formatDate(new Date(transaction.blockTimestamp)); - if (!mappedTransactions[txDate]) { - mappedTransactions[txDate] = [transaction]; - } else { - mappedTransactions[txDate].push(transaction); + const txDateKey = formatDateKey(new Date(transaction.blockTimestamp)); + if (!processedTransactions[txDateKey]) { + processedTransactions[txDateKey] = []; } + processedTransactions[txDateKey].push(transaction); }); - return mappedTransactions; + + const result: [Date, string, GetRunesActivityForAddressEvent[]][] = []; + + Object.values(processedTransactions).forEach((grp) => { + if (grp.length === 0) { + return; + } + + grp.sort((txA, txB) => { + // sort by block height first + const blockHeightDiff = txB.blockHeight - txA.blockHeight; + if (blockHeightDiff !== 0) { + return blockHeightDiff; + } + + // if block height is the same, sort by txid for consistency + return txB.txid.localeCompare(txA.txid); + }); + + result.push([ + new Date(grp[0].blockTimestamp), + formatDate(new Date(grp[0].blockTimestamp)), + grp, + ]); + }); + + result.sort((a, b) => b[0].getTime() - a[0].getTime()); + + return result; }; -const groupedTxsByDateMap = (txs: (AddressTransactionWithTransfers | MempoolTransaction)[]) => - txs.reduce( - ( - all: { [x: string]: (AddressTransactionWithTransfers | Tx)[] }, - transaction: AddressTransactionWithTransfers | Tx, - ) => { - const date = formatDate( - new Date( - isAddressTransactionWithTransfers(transaction) && transaction.tx?.burn_block_time_iso - ? transaction.tx.burn_block_time_iso - : Date.now(), - ), - ); - if (!all[date]) { - all[date] = [transaction]; - } else { - all[date].push(transaction); +const groupedTxsByDateMap = ( + transactions: (AddressTransactionWithTransfers | MempoolTransaction)[], +): [Date, string, (AddressTransactionWithTransfers | Tx)[]][] => { + const getBlockTimestamp = (tx: AddressTransactionWithTransfers | Tx): Date => { + let dateStr: string; + + if (isAddressTransactionWithTransfers(tx)) { + dateStr = tx.tx.burn_block_time_iso; + } else if ('receipt_time_iso' in tx) { + dateStr = tx.receipt_time_iso; + } else { + dateStr = ''; + } + + return dateStr ? new Date(dateStr) : new Date(); + }; + + const getTxid = (tx: AddressTransactionWithTransfers | Tx): string => { + if (isAddressTransactionWithTransfers(tx)) { + return tx.tx.tx_id; + } + return tx.tx_id; + }; + + const processedTransactions: { [x: string]: (AddressTransactionWithTransfers | Tx)[] } = {}; + + transactions.forEach((transaction) => { + const txDate = getBlockTimestamp(transaction); + const txDateKey = formatDateKey(txDate); + + if (!processedTransactions[txDateKey]) { + processedTransactions[txDateKey] = []; + } + processedTransactions[txDateKey].push(transaction); + }); + + const result: [Date, string, (AddressTransactionWithTransfers | Tx)[]][] = []; + + Object.values(processedTransactions).forEach((grp) => { + if (grp.length === 0) { + return; + } + + grp.sort((txA, txB) => { + // sort by block height first + const blockHeightDiff = getBlockTimestamp(txB).getTime() - getBlockTimestamp(txA).getTime(); + if (blockHeightDiff !== 0) { + return blockHeightDiff; } - return all; - }, - {}, - ); + + // if block height is the same, sort by txid for consistency + return getTxid(txB).localeCompare(getTxid(txA)); + }); + + result.push([getBlockTimestamp(grp[0]), formatDate(new Date(getBlockTimestamp(grp[0]))), grp]); + }); + + result.sort((a, b) => b[0].getTime() - a[0].getTime()); + + return result; +}; const filterStxTxs = ( txs: (AddressTransactionWithTransfers | MempoolTransaction)[], @@ -189,13 +263,29 @@ const filterStxTxs = ( ); }); -export default function TransactionsHistoryList(props: TransactionsHistoryListProps) { - const { coin, stxTxFilter, brc20Token, runeToken, runeSymbol } = props; +type Props = { + coin: CurrencyTypes; + stxTxFilter: string | null; + brc20Token: string | null; + runeToken: string | null; + runeSymbol: string | null; + withTitle?: boolean; +}; + +function TransactionsHistoryList({ + coin, + stxTxFilter, + brc20Token, + runeToken, + runeSymbol, + withTitle = true, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); const selectedAccount = useSelectedAccount(); const { network, selectedAccountType } = useWalletSelector(); const btcClient = useBtcClient(); const seedVault = useSeedVault(); - const { data, isLoading, isFetching, error } = useTransactions( + const { data, isLoading, error } = useTransactions( (coin as CurrencyTypes) || 'STX', brc20Token, runeToken, @@ -209,7 +299,6 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr }, }); - const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); const wallet = selectedAccount ? { ...selectedAccount, @@ -245,20 +334,20 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr return groupedTxsByDateMap(filteredTxs); } return groupedTxsByDateMap(data as (AddressTransactionWithTransfers | MempoolTransaction)[]); - }, [data, isLoading, isFetching]); + }, [data, coin, stxTxFilter]); return ( - {t('TRANSACTION_HISTORY_TITLE')} + {withTitle && {t('TRANSACTION_HISTORY_TITLE')}} {groupedTxs && !isLoading && - Object.keys(groupedTxs).map((group) => ( + groupedTxs.map(([, group, items]) => ( {group} - {groupedTxs[group].map((transaction) => { + {items.map((transaction) => { if (wallet && isRuneTransaction(transaction)) { return ( ); } + +export default TransactionsHistoryList; diff --git a/src/app/screens/confirmBrc20Transaction/index.tsx b/src/app/screens/confirmBrc20Transaction/index.tsx index 1439d3ba5..3fe59c373 100644 --- a/src/app/screens/confirmBrc20Transaction/index.tsx +++ b/src/app/screens/confirmBrc20Transaction/index.tsx @@ -2,7 +2,7 @@ import InfoContainer from '@components/infoContainer'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useDebounce from '@hooks/useDebounce'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; @@ -53,7 +53,7 @@ function ConfirmBrc20Transaction() { const { network, fiatCurrency, feeMultipliers } = useWalletSelector(); const selectedAccount = useSelectedAccount(); const { btcAddress, ordinalsAddress } = selectedAccount; - const { btcFiatRate } = useCoinRates(); + const { btcFiatRate } = useSupportedCoinRates(); const navigate = useNavigate(); const { recipientAddress, diff --git a/src/app/screens/confirmFtTransaction/index.tsx b/src/app/screens/confirmFtTransaction/index.tsx index 9e9f010ba..0a7efc666 100644 --- a/src/app/screens/confirmFtTransaction/index.tsx +++ b/src/app/screens/confirmFtTransaction/index.tsx @@ -1,4 +1,4 @@ -import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger'; +import type { ConfirmStxTransactionState } from '@common/types/ledger'; import ConfirmStxTransactionComponent from '@components/confirmStxTransactionComponent'; import TransferMemoView from '@components/confirmStxTransactionComponent/transferMemoView'; import RecipientComponent from '@components/recipientComponent'; @@ -82,15 +82,13 @@ function ConfirmFtTransaction() { const handleOnConfirmClick = (txs: StacksTransaction[]) => { if (isLedgerAccount(selectedAccount)) { - const type: LedgerTransactionType = 'STX'; const state: ConfirmStxTransactionState = { unsignedTx: Buffer.from(unsignedTx.serialize()), - type, recipients: [{ address: recipientAddress, amountMicrostacks: new BigNumber(amount) }], fee: new BigNumber(unsignedTx.auth.spendingCondition.fee.toString()), }; - navigate('/confirm-ledger-tx', { state }); + navigate('/confirm-ledger-stx-tx', { state }); return; } diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index 82e4f3350..3c39993f8 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -1,5 +1,5 @@ import AssetIcon from '@assets/img/transactions/Assets.svg'; -import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger'; +import type { ConfirmStxTransactionState } from '@common/types/ledger'; import AccountHeaderComponent from '@components/accountHeader'; import ConfirmStxTransactionComponent from '@components/confirmStxTransactionComponent'; import RecipientComponent from '@components/recipientComponent'; @@ -21,12 +21,14 @@ import { type StacksTransaction, } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; +import { removeAccountAvatarAction } from '@stores/wallet/actions/actionCreators'; import { useMutation } from '@tanstack/react-query'; import { isLedgerAccount } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import BigNumber from 'bignumber.js'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; @@ -71,9 +73,12 @@ const ReviewTransactionText = styled.h1((props) => ({ })); function ConfirmNftTransaction() { + const dispatch = useDispatch(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const isGalleryOpen: boolean = document.documentElement.clientWidth > 360; const selectedAccount = useSelectedAccount(); + const { avatarIds, network } = useWalletSelector(); + const selectedAvatar = avatarIds[selectedAccount.ordinalsAddress]; const [fee, setFee] = useState(); const navigate = useNavigate(); const location = useLocation(); @@ -84,7 +89,6 @@ function ConfirmNftTransaction() { const { unsignedTx: unsignedTxHex, recipientAddress } = location.state; const unsignedTx = useMemo(() => deserializeTransaction(unsignedTxHex), [unsignedTxHex]); - const { network } = useWalletSelector(); const { refetch } = useStxWalletData(); const selectedNetwork = useNetworkSelector(); const { @@ -107,11 +111,24 @@ function ConfirmNftTransaction() { isNft: true, }, }); + setTimeout(() => { refetch(); }, 1000); + + if (selectedAvatar?.type === 'stacks' && selectedAvatar.nft?.token_id === nft?.token_id) { + dispatch(removeAccountAvatarAction({ address: selectedAccount.ordinalsAddress })); + } } - }, [stxTxBroadcastData]); + }, [ + dispatch, + navigate, + refetch, + stxTxBroadcastData, + selectedAccount.ordinalsAddress, + nft, + selectedAvatar, + ]); useEffect(() => { if (txError) { @@ -128,10 +145,8 @@ function ConfirmNftTransaction() { const handleOnConfirmClick = (txs: StacksTransaction[]) => { if (isLedgerAccount(selectedAccount)) { - const type: LedgerTransactionType = 'STX'; const state: ConfirmStxTransactionState = { unsignedTx: Buffer.from(unsignedTx.serialize()), - type, recipients: [ { address: recipientAddress, @@ -148,7 +163,7 @@ function ConfirmNftTransaction() { ), }; - navigate('/confirm-ledger-tx', { state }); + navigate('/confirm-ledger-stx-tx', { state }); return; } diff --git a/src/app/screens/confirmStxTransaction/index.tsx b/src/app/screens/confirmStxTransaction/index.tsx index 92efa41af..b85630963 100644 --- a/src/app/screens/confirmStxTransaction/index.tsx +++ b/src/app/screens/confirmStxTransaction/index.tsx @@ -1,5 +1,5 @@ import IconStacks from '@assets/img/dashboard/stx_icon.svg'; -import type { ConfirmStxTransactionState, LedgerTransactionType } from '@common/types/ledger'; +import type { ConfirmStxTransactionState } from '@common/types/ledger'; import { sendInternalErrorMessage, sendUserRejectionMessage, @@ -55,6 +55,13 @@ const SpendDelegatedStxWarning = styled(Callout)((props) => ({ marginBottom: props.theme.space.m, })); +const Subtitle = styled.p` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.xs}; +`; + function ConfirmStxTransaction() { const { t } = useTranslation('translation'); const [hasTabClosed, setHasTabClosed] = useState(false); @@ -211,16 +218,14 @@ function ConfirmStxTransaction() { const handleConfirmClick = (txs: StacksTransaction[]) => { if (isLedgerAccount(selectedAccount)) { - const type: LedgerTransactionType = 'STX'; const fee = new BigNumber(txs[0].auth.spendingCondition.fee.toString()); const state: ConfirmStxTransactionState = { unsignedTx: Buffer.from(unsignedTx.serialize()), - type, recipients: [{ address: recipient, amountMicrostacks: amount }], fee, }; - navigate('/confirm-ledger-tx', { state }); + navigate('/confirm-ledger-stx-tx', { state }); return; } const rawTx = buf2hex(txs[0].serialize()); @@ -350,6 +355,7 @@ function ConfirmStxTransaction() { currencyType="STX" title={t('CONFIRM_TRANSACTION.AMOUNT')} /> + {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} {memo && } {hasTabClosed && ( diff --git a/src/app/screens/connect/authenticationRequest/index.tsx b/src/app/screens/connect/authenticationRequest/index.tsx index 0a6bae09f..e13471afd 100644 --- a/src/app/screens/connect/authenticationRequest/index.tsx +++ b/src/app/screens/connect/authenticationRequest/index.tsx @@ -3,7 +3,7 @@ import stxIcon from '@assets/img/dashboard/stx_icon.svg'; import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg'; import { MESSAGE_SOURCE } from '@common/types/message-types'; -import { delay } from '@common/utils/ledger'; +import { delay } from '@common/utils/promises'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; diff --git a/src/app/screens/connect/selectAccount.tsx b/src/app/screens/connect/selectAccount.tsx index ba98ddab3..dcdb969e3 100644 --- a/src/app/screens/connect/selectAccount.tsx +++ b/src/app/screens/connect/selectAccount.tsx @@ -74,7 +74,7 @@ type Props = { }; function SelectAccount({ account, handlePressAccount }: Props) { - const gradient = getAccountGradient(account?.stxAddress || account?.btcAddress!); + const gradient = getAccountGradient(account); const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' }); const theme = useTheme(); diff --git a/src/app/screens/createInscription/index.tsx b/src/app/screens/createInscription/index.tsx index 5e46b157b..b2c3d55fa 100644 --- a/src/app/screens/createInscription/index.tsx +++ b/src/app/screens/createInscription/index.tsx @@ -27,10 +27,9 @@ import ConfirmScreen from '@components/confirmScreen'; import useWalletSelector from '@hooks/useWalletSelector'; import { isLedgerAccount } from '@utils/helper'; -import InscribeSection from '@components/confirmBtcTransaction/inscribeSection'; import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; import useConfirmedBtcBalance from '@hooks/queries/useConfirmedBtcBalance'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; import Button from '@ui-library/button'; @@ -38,6 +37,7 @@ import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; import { trackMixPanel } from '@utils/mixpanel'; +import InscribeSection from 'app/components/confirmBtcTransaction/sections/inscribeSection'; import CompleteScreen from './CompleteScreen'; import EditFee from './EditFee'; import ErrorModal from './ErrorModal'; @@ -108,7 +108,7 @@ function CreateInscription() { const selectedAccount = useSelectedAccount(); const { ordinalsAddress, btcAddress } = selectedAccount; const { network, fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); + const { btcFiatRate } = useSupportedCoinRates(); const transactionContext = useTransactionContext(); @@ -481,7 +481,6 @@ function CreateInscription() { /> )} diff --git a/src/app/screens/createPassword/index.tsx b/src/app/screens/createPassword/index.tsx index 572c78b7e..29b01c37e 100644 --- a/src/app/screens/createPassword/index.tsx +++ b/src/app/screens/createPassword/index.tsx @@ -101,7 +101,6 @@ function CreatePassword(): JSX.Element { handleContinue={handleContinuePasswordCreation} handleBack={handleNewPasswordBack} checkPasswordStrength - createPasswordFlow autoFocus /> ) : ( diff --git a/src/app/screens/etchRune/index.tsx b/src/app/screens/etchRune/index.tsx index 9ad6169d9..61eb7ecb9 100644 --- a/src/app/screens/etchRune/index.tsx +++ b/src/app/screens/etchRune/index.tsx @@ -67,18 +67,7 @@ function EtchRune() { {orderTx && orderTx.summary && !etchError && ( - input.extendedUtxo.address !== btcAddress && - input.extendedUtxo.address !== ordinalsAddress, - ), - }} + runeEtchDetails={etchRequest} feeRate={+feeRate} confirmText={t('CONFIRM')} cancelText={t('CANCEL')} diff --git a/src/app/screens/executeBrc20Transaction/index.tsx b/src/app/screens/executeBrc20Transaction/index.tsx index b5882c0d6..3bd44db13 100644 --- a/src/app/screens/executeBrc20Transaction/index.tsx +++ b/src/app/screens/executeBrc20Transaction/index.tsx @@ -165,7 +165,7 @@ function ExecuteBrc20Transaction() { loadingPercentage={loadingPercentageAwareOfStatus} /> )} - + theme.typography.body_medium_m}; display: flex; align-items: center; + align-self: flex-start; column-gap: ${({ theme }) => theme.space.xs}; color: ${({ theme }) => theme.colors.white_0}; margin-top: ${({ theme }) => theme.space.s}; @@ -58,7 +59,7 @@ const LoaderContainer = styled.div((props) => ({ flex: 1, justifyContent: 'center', alignItems: 'center', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); function ExploreScreen() { diff --git a/src/app/screens/forgotPassword/index.tsx b/src/app/screens/forgotPassword/index.tsx index 93e2c5303..860719d34 100644 --- a/src/app/screens/forgotPassword/index.tsx +++ b/src/app/screens/forgotPassword/index.tsx @@ -1,7 +1,7 @@ -import ActionButton from '@components/button'; -import CheckBox from '@components/checkBox'; import TopRow from '@components/topRow'; import useWalletReducer from '@hooks/useWalletReducer'; +import Button from '@ui-library/button'; +import Checkbox from '@ui-library/checkbox'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -12,14 +12,14 @@ const Container = styled.div((props) => ({ flexDirection: 'column', height: '100%', backgroundColor: props.theme.colors.elevation0, - padding: `0 ${props.theme.spacing(8)}px 0 ${props.theme.spacing(8)}px`, + padding: `0 ${props.theme.space.m}`, })); const Paragraph = styled.p((props) => ({ - ...props.theme.body_l, + ...props.theme.typography.body_l, color: props.theme.colors.white_200, textAlign: 'left', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); const BottomContainer = styled.div((props) => ({ @@ -30,8 +30,8 @@ const ButtonsContainer = styled.div((props) => ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - marginTop: props.theme.spacing(16), - columnGap: props.theme.spacing(8), + marginTop: props.theme.space.xl, + columnGap: props.theme.space.xs, })); const StyledTopRow = styled(TopRow)({ @@ -54,7 +54,7 @@ function ForgotPassword(): JSX.Element { const handleResetWallet = async () => { await resetWallet(); - navigate('/'); + onBack(); }; return ( @@ -63,19 +63,19 @@ function ForgotPassword(): JSX.Element { {t('PARAGRAPH1')} {t('PARAGRAPH2')} - - - + + + + )} + + + ); +} + +export default BannerCarousel; diff --git a/src/app/screens/home/index.styled.ts b/src/app/screens/home/index.styled.ts index 5f34e3d64..bebc02d64 100644 --- a/src/app/screens/home/index.styled.ts +++ b/src/app/screens/home/index.styled.ts @@ -1,4 +1,5 @@ import TokenTile from '@components/tokenTile'; +import Callout from '@ui-library/callout'; import Divider from '@ui-library/divider'; import styled from 'styled-components'; @@ -15,16 +16,13 @@ export const ColumnContainer = styled.div((props) => ({ flexDirection: 'column', alignItems: 'space-between', justifyContent: 'space-between', - gap: props.theme.space.s, - marginTop: props.theme.space.l, + marginTop: props.theme.space.xs, marginBottom: props.theme.space.s, })); export const ReceiveContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginTop: props.theme.spacing(12), - marginBottom: props.theme.spacing(16), gap: props.theme.space.m, })); @@ -64,12 +62,12 @@ export const TokenListButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', justifyContent: 'center', - marginTop: props.theme.spacing(6), + marginTop: props.theme.space.s, marginBottom: props.theme.spacing(22), })); export const StyledTokenTile = styled(TokenTile)` - background-color: ${(props) => props.theme.colors.elevation1}; + background-color: transparent; `; export const Icon = styled.img({ @@ -83,7 +81,7 @@ export const MergedOrdinalsIcon = styled.img({ }); export const VerifyOrViewContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(16), + marginTop: props.theme.space.xl, marginBottom: props.theme.spacing(20), })); @@ -92,9 +90,9 @@ export const VerifyButtonContainer = styled.div((props) => ({ })); export const ModalContent = styled.div((props) => ({ - padding: props.theme.spacing(8), + padding: props.theme.space.m, paddingTop: 0, - paddingBottom: props.theme.spacing(16), + paddingBottom: props.theme.space.xl, display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -106,14 +104,14 @@ export const ModalIcon = styled.img((props) => ({ export const ModalTitle = styled.div((props) => ({ ...props.theme.typography.body_bold_l, - marginBottom: props.theme.spacing(4), + marginBottom: props.theme.space.xs, textAlign: 'center', })); export const ModalDescription = styled.div((props) => ({ ...props.theme.typography.body_m, color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(16), + marginBottom: props.theme.space.xl, textAlign: 'center', })); @@ -140,7 +138,7 @@ export const StacksIcon = styled.img({ export const MergedIcon = styled.div((props) => ({ position: 'relative', - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); export const IconBackground = styled.div((props) => ({ @@ -157,9 +155,23 @@ export const IconBackground = styled.div((props) => ({ alignItems: 'center', })); -export const StyledDivider = styled(Divider)` +export const StyledDivider = styled(Divider)<{ $noMarginBottom?: boolean }>` flex: 1 0 auto; width: calc(100% + ${(props) => props.theme.space.xl}); margin-left: -${(props) => props.theme.space.m}; margin-right: -${(props) => props.theme.space.m}; + transition: margin-bottom 0.1s ease; + ${(props) => + props.$noMarginBottom && + ` + margin-bottom: 0; + `} `; + +export const StyledDividerSingle = styled(StyledDivider)` + margin-bottom: 0; +`; + +export const SpacedCallout = styled(Callout)((props) => ({ + marginTop: props.theme.space.s, +})); diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index fce35f393..60a78c5e6 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -1,32 +1,42 @@ import dashboardIcon from '@assets/img/dashboard-icon.svg'; -import BitcoinToken from '@assets/img/dashboard/bitcoin_token.svg'; import ListDashes from '@assets/img/dashboard/list_dashes.svg'; -import ordinalsIcon from '@assets/img/dashboard/ordinalBRC20.svg'; -import stacksIcon from '@assets/img/dashboard/stx_icon.svg'; import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; -import ReceiveCardComponent from '@components/receiveCardComponent'; -import ShowBtcReceiveAlert from '@components/showBtcReceiveAlert'; -import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; import BottomBar from '@components/tabBar'; -import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; -import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; -import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; +import { + useGetBrc20FungibleTokens, + useVisibleBrc20FungibleTokens, +} from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; +import { + useRuneFungibleTokensQuery, + useVisibleRuneFungibleTokens, +} from '@hooks/queries/runes/useRuneFungibleTokensQuery'; +import { + useGetSip10FungibleTokens, + useVisibleSip10FungibleTokens, +} from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useAppConfig from '@hooks/queries/useAppConfig'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useCoinRates from '@hooks/queries/useCoinRates'; import useFeeMultipliers from '@hooks/queries/useFeeMultipliers'; +import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance'; import useSpamTokens from '@hooks/queries/useSpamTokens'; import useStxWalletData from '@hooks/queries/useStxWalletData'; +import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates'; +import useAvatarCleanup from '@hooks/useAvatarCleanup'; import useHasFeature from '@hooks/useHasFeature'; import useNotificationBanners from '@hooks/useNotificationBanners'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowDown, ArrowUp, Plus } from '@phosphor-icons/react'; +import { animated, useTransition } from '@react-spring/web'; import CoinSelectModal from '@screens/home/coinSelectModal'; -import { AnalyticsEvents, FeatureId, type FungibleToken } from '@secretkeylabs/xverse-core'; +import { + AnalyticsEvents, + FeatureId, + type FungibleToken, + type FungibleTokenWithStates, +} from '@secretkeylabs/xverse-core'; import { changeShowDataCollectionAlertAction, setBrc20ManageTokensAction, @@ -35,13 +45,11 @@ import { setSpamTokenAction, } from '@stores/wallet/actions/actionCreators'; import Button from '@ui-library/button'; -import Sheet from '@ui-library/sheet'; import SnackBar from '@ui-library/snackBar'; import type { CurrencyTypes } from '@utils/constants'; import { isInOptions, isLedgerAccount } from '@utils/helper'; import { optInMixPanel, optOutMixPanel, trackMixPanel } from '@utils/mixpanel'; import { sortFtByFiatBalance } from '@utils/tokens'; -import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -49,88 +57,72 @@ import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { useTheme } from 'styled-components'; import SquareButton from '../../components/squareButton'; +import AnnouncementModal from './announcementModal'; import BalanceCard from './balanceCard'; -import Banner from './banner'; +import BannerCarousel from './bannerCarousel'; import { ButtonImage, ButtonText, ColumnContainer, Container, - Icon, - IconBackground, - MergedIcon, - MergedOrdinalsIcon, ModalButtonContainer, ModalContent, ModalControlsContainer, ModalDescription, ModalIcon, ModalTitle, - ReceiveContainer, RowButtonContainer, - StacksIcon, StyledDivider, + StyledDividerSingle, StyledTokenTile, TokenListButton, TokenListButtonContainer, - VerifyButtonContainer, - VerifyOrViewContainer, } from './index.styled'; +import ReceiveSheet from './receiveSheet'; function Home() { const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN', }); const selectedAccount = useSelectedAccount(); - const { stxAddress, btcAddress, ordinalsAddress } = selectedAccount; - const { - showBtcReceiveAlert, - showOrdinalReceiveAlert, - showDataCollectionAlert, - network, - hideStx, - spamToken, - notificationBanners, - } = useWalletSelector(); + const { stxAddress, btcAddress } = selectedAccount; + const { showDataCollectionAlert, hideStx, spamToken, notificationBanners } = useWalletSelector(); const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const [openReceiveModal, setOpenReceiveModal] = useState(false); const [openSendModal, setOpenSendModal] = useState(false); const [openBuyModal, setOpenBuyModal] = useState(false); - const [isBtcReceiveAlertVisible, setIsBtcReceiveAlertVisible] = useState(false); - const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); - const [areReceivingAddressesVisible, setAreReceivingAddressesVisible] = useState( - !isLedgerAccount(selectedAccount), - ); - const [choseToVerifyAddresses, setChoseToVerifyAddresses] = useState(false); - const { isInitialLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } = - useBtcWalletData(); + const { isLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } = + useSelectedAccountBtcBalance(); const { isInitialLoading: loadingStxWalletData, isRefetching: refetchingStxWalletData } = useStxWalletData(); - const { btcFiatRate, stxBtcRate } = useCoinRates(); - const { data: notificationBannersArr } = useNotificationBanners(); + const { btcFiatRate, stxBtcRate } = useSupportedCoinRates(); + const { data: notificationBannersArr, isFetching: isFetchingNotificationBannersArr } = + useNotificationBanners(); + + const { data: fullSip10CoinsList } = useGetSip10FungibleTokens(); + const { data: fullBrc20CoinsList } = useGetBrc20FungibleTokens(); + const { data: fullRunesCoinsList } = useRuneFungibleTokensQuery(); const { - unfilteredData: fullSip10CoinsList, - visible: sip10CoinsList, + data: sip10CoinsList, isInitialLoading: loadingStxCoinData, isRefetching: refetchingStxCoinData, } = useVisibleSip10FungibleTokens(); const { - unfilteredData: fullBrc20CoinsList, - visible: brc20CoinsList, + data: brc20CoinsList, isInitialLoading: loadingBrcCoinData, isRefetching: refetchingBrcCoinData, } = useVisibleBrc20FungibleTokens(); const { - unfilteredData: fullRunesCoinsList, - visible: runesCoinsList, + data: runesCoinsList, isInitialLoading: loadingRunesData, isRefetching: refetchingRunesData, } = useVisibleRuneFungibleTokens(); useFeeMultipliers(); useAppConfig(); + useAvatarCleanup(); useTrackMixPanelPageViewed(); const { removeFromSpamTokens } = useSpamTokens(); @@ -159,11 +151,23 @@ function Home() { isEnabled: true, }; - if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) { + if ( + fullRunesCoinsList?.find( + (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal, + ) + ) { dispatch(setRunesManageTokensAction(payload)); - } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + } else if ( + fullSip10CoinsList?.find( + (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal, + ) + ) { dispatch(setSip10ManageTokensAction(payload)); - } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + } else if ( + fullBrc20CoinsList?.find( + (ft: FungibleTokenWithStates) => ft.principal === spamToken.principal, + ) + ) { dispatch(setBrc20ManageTokensAction(payload)); } @@ -177,29 +181,23 @@ function Home() { } }, [spamToken]); - const combinedFtList = sip10CoinsList - .concat(brc20CoinsList) - .concat(runesCoinsList) - .sort((a, b) => sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate)); + const combinedFtList = (sip10CoinsList ?? []) + .concat(brc20CoinsList ?? []) + .concat(runesCoinsList ?? []) + .sort((a: FungibleTokenWithStates, b: FungibleTokenWithStates) => + sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate), + ); - const showNotificationBanner = - notificationBannersArr?.length && - notificationBannersArr.length > 0 && - !notificationBanners[notificationBannersArr[0].id]; + const filteredNotificationBannersArr = notificationBannersArr + ? notificationBannersArr.filter((banner) => !notificationBanners[banner.id]) + : []; + const showBannerCarousel = + !isFetchingNotificationBannersArr && !!filteredNotificationBannersArr?.length; const onReceiveModalOpen = () => { setOpenReceiveModal(true); }; - const onReceiveModalClose = () => { - setOpenReceiveModal(false); - - if (isLedgerAccount(selectedAccount)) { - setAreReceivingAddressesVisible(false); - setChoseToVerifyAddresses(false); - } - }; - const onSendModalOpen = () => { setOpenSendModal(true); }; @@ -240,14 +238,6 @@ function Home() { navigate('/send-btc'); }; - const onBTCReceiveSelect = () => { - navigate('/receive/BTC'); - }; - - const onSTXReceiveSelect = () => { - navigate('/receive/STX'); - }; - const onSendFtSelect = async (fungibleToken: FungibleToken) => { let route = ''; switch (fungibleToken?.protocol) { @@ -281,22 +271,6 @@ function Home() { navigate('/buy/BTC'); }; - const onOrdinalReceiveAlertOpen = () => { - if (showOrdinalReceiveAlert) setIsOrdinalReceiveAlertVisible(true); - }; - - const onOrdinalReceiveAlertClose = () => { - setIsOrdinalReceiveAlertVisible(false); - }; - - const onReceiveAlertClose = () => { - setIsBtcReceiveAlertVisible(false); - }; - - const onReceiveAlertOpen = () => { - if (showBtcReceiveAlert) setIsBtcReceiveAlertVisible(true); - }; - const handleTokenPressed = (currency: CurrencyTypes, fungibleToken?: FungibleToken) => { if (fungibleToken) { navigate( @@ -307,99 +281,11 @@ function Home() { } }; - const onOrdinalsReceivePress = () => { - navigate('/receive/ORD'); - }; - const onSwapPressed = () => { trackMixPanel(AnalyticsEvents.InitiateSwapFlow, {}); navigate('/swap'); }; - const receiveContent = ( - - - - - - - - - - {stxAddress && ( - - - - - - - - - )} - - {isLedgerAccount(selectedAccount) && !stxAddress && ( - - - {selectedProtocol === 'runes' && !showRunes ? ( - - - - ) : ( - getCoinsList() - )} - + {getCoinsList()} diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx index 69c43d161..5d118d152 100644 --- a/src/app/screens/mintRune/index.tsx +++ b/src/app/screens/mintRune/index.tsx @@ -1,6 +1,5 @@ import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import RequestError from '@components/requests/requestError'; -import useSelectedAccount from '@hooks/useSelectedAccount'; import { RUNE_DISPLAY_DEFAULTS, type Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; import { useCallback, useEffect } from 'react'; @@ -18,7 +17,6 @@ const LoaderContainer = styled.div(() => ({ function MintRune() { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { btcAddress, ordinalsAddress } = useSelectedAccount(); const navigate = useNavigate(); const { mintRequest, @@ -70,28 +68,18 @@ function MintRune() { {orderTx && orderTx.summary && runeInfo && !mintError && ( - input.extendedUtxo.address !== btcAddress && - input.extendedUtxo.address !== ordinalsAddress, - ), - receipts: [], - transfers: [], - mint: { - runeName: runeInfo.entry.spaced_rune, - amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), - divisibility: runeInfo.entry.divisibility.toNumber(), - symbol: runeInfo.entry.symbol, - inscriptionId: runeInfo.parent ?? '', - runeIsOpen: runeInfo.mintable, - runeIsMintable: runeInfo.mintable, - destinationAddress: mintRequest.destinationAddress, - repeats: mintRequest.repeats, - runeSize: RUNE_DISPLAY_DEFAULTS.size, - }, + runeMintDetails={{ + runeId: runeInfo.id, + runeName: runeInfo.entry.spaced_rune, + amount: BigInt(runeInfo.entry.terms.amount?.toNumber() ?? 0), + divisibility: runeInfo.entry.divisibility.toNumber(), + symbol: runeInfo.entry.symbol, + inscriptionId: runeInfo.parent ?? '', + runeIsOpen: runeInfo.mintable, + runeIsMintable: runeInfo.mintable, + destinationAddress: mintRequest.destinationAddress, + repeats: mintRequest.repeats, + runeSize: RUNE_DISPLAY_DEFAULTS.size, }} feeRate={+feeRate} confirmText={t('CONFIRM')} diff --git a/src/app/screens/mintRune/useMintRequest.ts b/src/app/screens/mintRune/useMintRequest.ts index 52c203847..0c5a2266c 100644 --- a/src/app/screens/mintRune/useMintRequest.ts +++ b/src/app/screens/mintRune/useMintRequest.ts @@ -1,4 +1,5 @@ import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; +import useBtcClient from '@hooks/apiClients/useBtcClient'; import useOrdinalsServiceApi from '@hooks/apiClients/useOrdinalsServiceApi'; import useRunesApi from '@hooks/apiClients/useRunesApi'; import useTransactionContext from '@hooks/useTransactionContext'; @@ -40,6 +41,7 @@ const useMintRequest = (): { const txContext = useTransactionContext(); const ordinalsServiceApi = useOrdinalsServiceApi(); const runesApi = useRunesApi(); + const btcClient = useBtcClient(); const [mintError, setMintError] = useState<{ code: number | undefined; @@ -139,11 +141,8 @@ const useMintRequest = (): { const payAndConfirmMintRequest = async (ledgerTransport?: Transport) => { try { setIsExecuting(true); - const txid = await orderTx?.transaction.broadcast({ - ledgerTransport, - rbfEnabled: false, - }); - if (!txid) { + + if (!orderTx) { const response = makeRPCError(requestId, { code: RpcErrorCode.INTERNAL_ERROR, message: 'Failed to broadcast transaction', @@ -151,6 +150,21 @@ const useMintRequest = (): { sendRpcResponse(+tabId, response); return; } + + // TODO: make enhancedTransaction class use a passed in btcClient and use: + /* + orderTx.transaction.broadcast({ + ledgerTransport, + rbfEnabled: false, + }); + */ + + const { hex: transactionHex, id: txid } = await orderTx.transaction.getTransactionHexAndId({ + ledgerTransport, + rbfEnabled: false, + }); + await btcClient.sendRawTransaction(transactionHex); + await ordinalsServiceApi.executeMint(orderId, txid); const mintRequestResponse = makeRpcSuccessResponse<'runes_mint'>(requestId, { fundingAddress: txContext.paymentAddress.address, diff --git a/src/app/screens/nftCollection/index.styled.ts b/src/app/screens/nftCollection/index.styled.ts new file mode 100644 index 000000000..552e0c923 --- /dev/null +++ b/src/app/screens/nftCollection/index.styled.ts @@ -0,0 +1,99 @@ +import { GridContainer } from '@screens/nftDashboard/collectiblesTabs/index.styled'; +import styled from 'styled-components'; + +interface Props { + $isGalleryOpen?: boolean; +} + +export const Container = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, +})); + +export const NoCollectiblesText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.space.xl, + marginBottom: 'auto', + textAlign: 'center', +})); + +export const HeadingText = styled.p((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_400, +})); + +export const CollectionText = styled.p((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + marginTop: props.theme.space.xxxs, + marginBottom: props.theme.space.xs, + wordBreak: 'break-word', +})); + +export const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +export const PageHeader = styled.div` + padding: ${(props) => props.theme.space.xs}; + padding-top: 0; + max-width: 1224px; + margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +export const PageHeaderContent = styled.div` + display: flex; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; + row-gap: ${(props) => props.theme.space.xl}; +`; + +export const NftContainer = styled.div` + display: flex; + flex-direction: ${(props) => (props.$isGalleryOpen ? 'column' : 'row')}; + justify-content: ${(props) => (props.$isGalleryOpen ? 'space-between' : 'initial')}; + column-gap: ${(props) => props.theme.space.m}; +`; + +export const BackButtonContainer = styled.div((props) => ({ + display: 'flex', + marginBottom: props.theme.space.xxl, +})); + +export const BackButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.space.l, +})); + +export const AssetDetailButtonText = styled.div((props) => ({ + ...props.theme.typography.body_m, + marginLeft: props.theme.space.xxxs, + color: props.theme.colors.white_0, + textAlign: 'center', +})); + +export const StyledGridContainer = styled(GridContainer)` + margin-top: ${(props) => props.theme.space.s}; + padding: 0 ${(props) => props.theme.space.xs}; + padding-bottom: ${(props) => props.theme.space.xl}; + max-width: 1224px; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +export const CollectionNameDiv = styled.div` + display: flex; + align-items: center; + gap: ${(props) => props.theme.space.s}; +`; diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx index 3d4f8a067..5eb7d96f0 100644 --- a/src/app/screens/nftCollection/index.tsx +++ b/src/app/screens/nftCollection/index.tsx @@ -1,121 +1,55 @@ import AccountHeaderComponent from '@components/accountHeader'; import CollectibleCollectionGridItem from '@components/collectibleCollectionGridItem'; import CollectibleDetailTile from '@components/collectibleDetailTile'; +import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader'; import TopRow from '@components/topRow'; import WebGalleryButton from '@components/webGalleryButton'; import WrenchErrorMessage from '@components/wrenchErrorMessage'; import useNftDetail from '@hooks/queries/useNftDetail'; -import { ArrowLeft } from '@phosphor-icons/react'; -import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArchiveTray, ArrowLeft, DotsThreeVertical, Star } from '@phosphor-icons/react'; import Nft from '@screens/nftDashboard/nft'; import NftImage from '@screens/nftDashboard/nftImage'; +import { StyledButton } from '@screens/ordinalsCollection/index.styled'; import type { NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core'; -import { EMPTY_LABEL } from '@utils/constants'; +import { + addToHideCollectiblesAction, + addToStarCollectiblesAction, + removeAccountAvatarAction, + removeFromHideCollectiblesAction, + removeFromStarCollectiblesAction, +} from '@stores/wallet/actions/actionCreators'; +import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { getFullyQualifiedKey, getNftCollectionsGridItemId, isBnsCollection } from '@utils/nfts'; -import { useRef, type PropsWithChildren } from 'react'; +import { useRef, useState, type PropsWithChildren } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useIsVisible } from 'react-is-visible'; +import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; +import Theme from 'theme'; +import { + AssetDetailButtonText, + BackButton, + BackButtonContainer, + BottomBarContainer, + CollectionNameDiv, + CollectionText, + Container, + HeadingText, + NftContainer, + NoCollectiblesText, + PageHeader, + PageHeaderContent, + StyledGridContainer, +} from './index.styled'; import useNftCollection from './useNftCollection'; -interface Props { - isGalleryOpen?: boolean; -} -const Container = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - flex: 1, -})); - -const NoCollectiblesText = styled.p((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: 'auto', - textAlign: 'center', -})); - -const HeadingText = styled.p((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_400, -})); - -const CollectionText = styled.p((props) => ({ - ...props.theme.typography.headline_s, - color: props.theme.colors.white_0, - marginTop: props.theme.spacing(1), - marginBottom: props.theme.spacing(4), - wordBreak: 'break-word', -})); - -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); - -const PageHeader = styled.div` - padding: ${(props) => props.theme.space.xs}; - padding-top: 0; - max-width: 1224px; - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - -const PageHeaderContent = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; - row-gap: ${(props) => props.theme.space.xl}; -`; - -const NftContainer = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'column' : 'row')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; - column-gap: ${(props) => props.theme.space.m}; -`; - -const BackButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - width: 800, - marginTop: props.theme.spacing(40), -})); - -const BackButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDetailButtonText = styled.div((props) => ({ - ...props.theme.typography.body_m, - fontWeight: 400, - fontSize: 14, - marginLeft: 2, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const StyledGridContainer = styled(GridContainer)` - margin-top: ${(props) => props.theme.space.s}; - padding: 0 ${(props) => props.theme.space.xs}; - padding-bottom: ${(props) => props.theme.space.xl}; - max-width: 1224px; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - /* * component to virtualise the grid item if not in window * placeholder is required to match grid item size, in order to negate scroll jank @@ -176,6 +110,12 @@ function CollectionGridItemWithData({ function NftCollection() { const { t } = useTranslation('translation', { keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN' }); + const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const selectedAccount = useSelectedAccount(); + const { starredCollectibleIds, hiddenCollectibleIds, avatarIds } = useWalletSelector(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false); const { collectionData, portfolioValue, @@ -186,37 +126,193 @@ function NftCollection() { handleBackButtonClick, openInGalleryView, } = useNftCollection(); + const currentAvatar = avatarIds[selectedAccount.btcAddress]; + + const openOptionsDialog = () => { + setIsOptionsModalVisible(true); + }; + + const closeOptionsDialog = () => { + setIsOptionsModalVisible(false); + }; + + const collectionStarred = starredCollectibleIds[selectedAccount.stxAddress]?.some( + ({ id }) => id === collectionData?.collection_id, + ); + const collectionHidden = Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).some( + (id) => id === collectionData?.collection_id, + ); + + const handleUnHideCollection = () => { + const isLastHiddenItem = + Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).length === 1; + dispatch( + removeFromHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + closeOptionsDialog(); + toast.custom(); + navigate(`/nft-dashboard/${isLastHiddenItem ? '' : 'hidden'}?tab=nfts`); + }; + + const handleClickUndoHiding = (toastId: string) => { + dispatch( + removeFromHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.remove(toastId); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + }; + + const handleHideCollection = () => { + dispatch( + addToHideCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + + if (currentAvatar?.type === 'stacks') { + const isHidingUsedAvatar = collectionData?.all_nfts.some( + (nft) => + `${nft.asset_identifier}:${nft.identifier.tokenId}` === + currentAvatar.nft.fully_qualified_token_id, + ); + + if (isHidingUsedAvatar) { + dispatch(removeAccountAvatarAction({ address: selectedAccount.btcAddress })); + } + } + + closeOptionsDialog(); + navigate('/nft-dashboard?tab=nfts'); + const toastId = toast.custom( + handleClickUndoHiding(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + }; + + const handleClickUndoStarring = (toastId: string) => { + dispatch( + removeFromStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.remove(toastId); + toast.custom(); + }; + + const handleStarClick = () => { + if (collectionStarred) { + dispatch( + removeFromStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + } else { + dispatch( + addToStarCollectiblesAction({ + address: selectedAccount.stxAddress, + id: collectionData?.collection_id ?? '', + }), + ); + const toastId = toast.custom( + handleClickUndoStarring(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + } + }; return ( <> {isGalleryOpen ? ( ) : ( - + )} - + {isGalleryOpen && ( <> - {t('BACK_TO_GALLERY')} + {t(collectionHidden ? 'BACK_TO_HIDDEN_COLLECTIBLES' : 'BACK_TO_GALLERY')} )} - +
{t('COLLECTION')} - - {collectionData?.collection_name || } - + + + {collectionData?.collection_name || } + + {isGalleryOpen && ( + <> + {collectionHidden ? null : ( + + ) : ( + + ) + } + onPress={handleStarClick} + isTransparent + size={44} + radiusSize={12} + /> + )} + + } + onPress={openOptionsDialog} + isTransparent + size={44} + radiusSize={12} + /> + + )} + {!isGalleryOpen && }
- + {isEmpty && {t('NO_COLLECTIBLES')}} {!!isError && } - + {isLoading ? ( ) : ( - collectionData?.all_nfts - .sort((a, b) => (a.value.repr > b.value.repr ? 1 : -1)) - .map((nft) => ( - - - - )) + collectionData?.all_nfts.map((nft) => ( + + + + )) )} @@ -266,6 +360,29 @@ function NftCollection() { )} + {isOptionsModalVisible && ( + + {collectionHidden ? ( + } + title={t('UNHIDE_COLLECTION')} + onClick={handleUnHideCollection} + /> + ) : ( + } + title={t('HIDE_COLLECTION')} + onClick={handleHideCollection} + /> + )} + + )} ); } diff --git a/src/app/screens/nftCollection/useNftCollection.ts b/src/app/screens/nftCollection/useNftCollection.ts index 37e64bc07..cf74fa48b 100644 --- a/src/app/screens/nftCollection/useNftCollection.ts +++ b/src/app/screens/nftCollection/useNftCollection.ts @@ -1,5 +1,7 @@ import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -7,12 +9,18 @@ export default function useNftCollection() { const navigate = useNavigate(); useResetUserFlow('/nft-collection'); - const { id: collectionId } = useParams(); - const { data, isLoading, error } = useStacksCollectibles(); + const { id: collectionId, from } = useParams(); + const comesFromHidden = from === 'hidden'; + const { data, isLoading, error } = useStacksCollectibles(comesFromHidden); + const { hiddenCollectibleIds } = useWalletSelector(); + const { stxAddress } = useSelectedAccount(); const collectionData = data?.results.find( (collection) => collection.collection_id === collectionId, ); + const collectionHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some( + (id) => id === collectionId, + ); const portfolioValue = collectionData?.floor_price && !Number.isNaN(collectionData?.all_nfts?.length) @@ -21,13 +29,16 @@ export default function useNftCollection() { const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); - const handleBackButtonClick = () => { - navigate('/nft-dashboard?tab=nfts'); - }; + const handleBackButtonClick = () => + navigate(`/nft-dashboard${comesFromHidden || collectionHidden ? '/hidden' : ''}?tab=nfts`); const openInGalleryView = async () => { await chrome.tabs.create({ - url: chrome.runtime.getURL(`options.html#/nft-dashboard/nft-collection/${collectionId}`), + url: chrome.runtime.getURL( + `options.html#/nft-dashboard/nft-collection/${collectionId}${ + comesFromHidden || collectionHidden ? '/hidden' : '' + }`, + ), }); }; diff --git a/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts new file mode 100644 index 000000000..ec0bd8425 --- /dev/null +++ b/src/app/screens/nftDashboard/collectiblesTabs/index.styled.ts @@ -0,0 +1,103 @@ +import WrenchErrorMessage from '@components/wrenchErrorMessage'; +import Button from '@ui-library/button'; +import { StyledP, StyledTabList } from '@ui-library/common.styled'; +import styled from 'styled-components'; + +export const GridContainer = styled.div<{ + $isGalleryOpen: boolean; +}>((props) => ({ + display: 'grid', + columnGap: props.$isGalleryOpen ? props.theme.space.xl : props.theme.space.m, + rowGap: props.$isGalleryOpen ? props.theme.space.xl : props.theme.space.l, + marginTop: props.theme.space.l, + gridTemplateColumns: props.$isGalleryOpen + ? 'repeat(auto-fill,minmax(220px,1fr))' + : 'repeat(auto-fill,minmax(150px,1fr))', +})); + +export const RareSatsTabContainer = styled.div((props) => ({ + marginTop: props.theme.space.l, +})); + +export const StickyStyledTabList = styled(StyledTabList)` + position: sticky; + background: ${(props) => props.theme.colors.elevation0}; + top: -1px; + z-index: 1; + padding: ${(props) => props.theme.space.m} 0; +`; + +export const StyledTotalItems = styled(StyledP)` + margin-top: ${(props) => props.theme.space.s}; +`; + +export const NoticeContainer = styled.div((props) => ({ + marginTop: props.theme.space.m, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +})); + +export const StyledWrenchErrorMessage = styled(WrenchErrorMessage)` + margin-top: ${(props) => props.theme.space.xxl}; +`; + +export const NoCollectiblesText = styled.div((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.space.xl, + textAlign: 'center', +})); + +export const LoadMoreButtonContainer = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginBottom: props.theme.spacing(30), + marginTop: props.theme.space.xl, + button: { + width: 156, + }, +})); + +export const LoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const CountLoaderContainer = styled.div((props) => ({ + marginTop: props.theme.space.s, + marginBottom: props.theme.space.l, +})); + +export const StyledButton = styled(Button)` + &.tertiary { + color: ${(props) => props.theme.colors.white_200}; + padding: 0; + width: auto; + min-height: 20px; + + &:hover:enabled { + opacity: 0.8; + } + } +`; + +export const StyledSheetButton = styled(Button)` + &.tertiary { + justify-content: flex-start; + color: ${(props) => props.theme.colors.white_200}; + padding-left: 0; + + &:hover:enabled { + opacity: 0.8; + } + } +`; + +export const TopBarContainer = styled.div((props) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: props.theme.space.s, +})); diff --git a/src/app/screens/nftDashboard/collectiblesTabs.tsx b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx similarity index 55% rename from src/app/screens/nftDashboard/collectiblesTabs.tsx rename to src/app/screens/nftDashboard/collectiblesTabs/index.tsx index 41af28166..4a8eec173 100644 --- a/src/app/screens/nftDashboard/collectiblesTabs.tsx +++ b/src/app/screens/nftDashboard/collectiblesTabs/index.tsx @@ -1,105 +1,39 @@ -import ActionButton from '@components/button'; -import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader'; -import WrenchErrorMessage from '@components/wrenchErrorMessage'; import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; +import { ArchiveTray } from '@phosphor-icons/react'; import { mapRareSatsAPIResponseToBundle, type Bundle } from '@secretkeylabs/xverse-core'; -import { StyledP, StyledTab, StyledTabList } from '@ui-library/common.styled'; +import Button from '@ui-library/button'; +import { StyledP } from '@ui-library/common.styled'; +import { TabItem } from '@ui-library/tabs'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { TabPanel, Tabs } from 'react-tabs'; -import styled from 'styled-components'; -import Notice from './notice'; -import RareSatsTabGridItem from './rareSatsTabGridItem'; -import type { NftDashboardState } from './useNftDashboard'; +import Theme from 'theme'; +import Notice from '../notice'; +import RareSatsTabGridItem from '../rareSatsTabGridItem'; +import type { NftDashboardState } from '../useNftDashboard'; + +import { + LoadMoreButtonContainer, + NoCollectiblesText, + NoticeContainer, + RareSatsTabContainer, + StickyStyledTabList, + StyledButton, + StyledTotalItems, + StyledWrenchErrorMessage, + TopBarContainer, +} from './index.styled'; +import SkeletonLoader from './skeletonLoader'; const MAX_SATS_ITEMS_EXTENSION = 5; const MAX_SATS_ITEMS_GALLERY = 20; -export const GridContainer = styled.div<{ - isGalleryOpen: boolean; -}>((props) => ({ - display: 'grid', - columnGap: props.isGalleryOpen ? props.theme.space.xl : props.theme.space.m, - rowGap: props.isGalleryOpen ? props.theme.space.xl : props.theme.space.l, - marginTop: props.theme.space.l, - gridTemplateColumns: props.isGalleryOpen - ? 'repeat(auto-fill,minmax(220px,1fr))' - : 'repeat(auto-fill,minmax(150px,1fr))', -})); - -const RareSatsTabContainer = styled.div<{ - isGalleryOpen: boolean; -}>((props) => ({ - marginTop: props.theme.space.l, -})); - -const StickyStyledTabList = styled(StyledTabList)` - position: sticky; - background: ${(props) => props.theme.colors.elevation0}; - top: -1px; - z-index: 1; - padding: ${(props) => props.theme.space.m} 0; -`; - -const StyledTotalItems = styled(StyledP)` - margin-top: ${(props) => props.theme.space.s}; -`; - -const NoticeContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', -})); - -const StyledWrenchErrorMessage = styled(WrenchErrorMessage)` - margin-top: ${(props) => props.theme.space.xxl}; -`; - -const NoCollectiblesText = styled.div((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - textAlign: 'center', -})); - -const LoadMoreButtonContainer = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - marginBottom: props.theme.spacing(30), - marginTop: props.theme.space.xl, - button: { - width: 156, - }, -})); - -const LoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const CountLoaderContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(6), - marginBottom: props.theme.spacing(12), -})); - -function SkeletonLoader({ isGalleryOpen }: { isGalleryOpen: boolean }) { - return ( - - - - - - - ); -} - type TabButton = { key: string; label: string; }; + const tabs: TabButton[] = [ { key: 'inscriptions', @@ -120,21 +54,24 @@ const tabKeyToIndex = (key?: string | null) => { return tabs.findIndex((tab) => tab.key === key); }; +type Props = { + className?: string; + nftListView: React.ReactNode; + inscriptionListView: React.ReactNode; + nftDashboard: NftDashboardState; +}; + export default function CollectiblesTabs({ className, nftListView, inscriptionListView, nftDashboard, -}: { - className?: string; - nftListView: React.ReactNode; - inscriptionListView: React.ReactNode; - nftDashboard: NftDashboardState; -}) { +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [tabIndex, setTabIndex] = useState(tabKeyToIndex(searchParams?.get('tab'))); + const { isGalleryOpen, rareSatsQuery, @@ -155,18 +92,15 @@ export default function CollectiblesTabs({ [tabIndex], ); - const handleSelectTab = (index: number) => { - setTabIndex(index); - }; + const handleSelectTab = (index: number) => setTabIndex(index); useEffect(() => { setSearchParams({ tab: tabs[tabIndex]?.key }); - }, [tabIndex, setSearchParams]); + }, [tabIndex]); const ordinalBundleCount = rareSatsQuery?.data?.pages?.[0]?.total || 0; const showNoBundlesNotice = ordinalBundleCount === 0 && !rareSatsQuery.isLoading && !rareSatsQuery.error; - const visibleTabButtons = tabs.filter((tab: TabButton) => { if (tab.key === 'rareSats' && !hasActivatedRareSatsKey) { return false; @@ -179,33 +113,48 @@ export default function CollectiblesTabs({ return ( + {/* TODO: replace with Tabs component from `src/app/ui-library/tabs.tsx` */} {visibleTabButtons.length > 1 && ( {visibleTabButtons.map(({ key, label }) => ( - {t(label)} + handleSelectTab(tabKeyToIndex(key))} + > + {t(label)} + ))} )} {hasActivatedOrdinalsKey && ( - {inscriptionsQuery.isInitialLoading ? ( - - ) : ( - <> - {totalInscriptions > 0 && ( - - {totalInscriptions === 1 - ? t('TOTAL_ITEMS_ONE') - : t('TOTAL_ITEMS', { count: totalInscriptions })} - - )} - {inscriptionListView} - - )} +
+ {inscriptionsQuery.isInitialLoading ? ( + + ) : ( + <> + {totalInscriptions > 0 && ( + + + {totalInscriptions === 1 + ? t('TOTAL_ITEMS_ONE') + : t('TOTAL_ITEMS', { count: totalInscriptions })} + + } + title={t('HIDDEN_COLLECTIBLES')} + onClick={() => { + navigate(`/nft-dashboard/hidden?tab=${tabs[tabIndex]?.key}`); + }} + /> + + )} + {inscriptionListView} + + )} +
)} @@ -214,13 +163,19 @@ export default function CollectiblesTabs({ ) : ( <> {totalNfts > 0 && ( - - {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })} - + + + {totalNfts === 1 ? t('TOTAL_ITEMS_ONE') : t('TOTAL_ITEMS', { count: totalNfts })} + + } + title={t('HIDDEN_COLLECTIBLES')} + onClick={() => { + navigate('/nft-dashboard/hidden?tab=nfts'); + }} + /> + )} {nftListView} @@ -239,7 +194,6 @@ export default function CollectiblesTabs({ : t('TOTAL_ITEMS', { count: ordinalBundleCount })} )} - {!rareSatsQuery.isLoading && showNoticeAlert && ( ) : ( - + {!rareSatsQuery.error && !rareSatsQuery.isLoading && rareSatsQuery.data?.pages @@ -278,12 +232,12 @@ export default function CollectiblesTabs({ )} {rareSatsQuery.hasNextPage && ( - rareSatsQuery.fetchNextPage()} + onClick={() => rareSatsQuery.fetchNextPage()} /> )} diff --git a/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx b/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx new file mode 100644 index 000000000..faed4ea84 --- /dev/null +++ b/src/app/screens/nftDashboard/collectiblesTabs/skeletonLoader.tsx @@ -0,0 +1,15 @@ +import { StyledBarLoader, TilesSkeletonLoader } from '@components/tilesSkeletonLoader'; +import { CountLoaderContainer, LoaderContainer } from './index.styled'; + +function SkeletonLoader({ isGalleryOpen }: { isGalleryOpen: boolean }) { + return ( + + + + + + + ); +} + +export default SkeletonLoader; diff --git a/src/app/screens/nftDashboard/hidden/index.tsx b/src/app/screens/nftDashboard/hidden/index.tsx new file mode 100644 index 000000000..abc8942ab --- /dev/null +++ b/src/app/screens/nftDashboard/hidden/index.tsx @@ -0,0 +1,329 @@ +import AccountHeaderComponent from '@components/accountHeader'; +import BottomTabBar from '@components/tabBar'; +import TopRow from '@components/topRow'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArrowLeft, TrayArrowUp } from '@phosphor-icons/react'; +import { + removeAllFromHideCollectiblesAction, + setHiddenCollectiblesAction, +} from '@stores/wallet/actions/actionCreators'; +import { StyledHeading, StyledP } from '@ui-library/common.styled'; +import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; +import { TabItem } from '@ui-library/tabs'; +import { LONG_TOAST_DURATION } from '@utils/constants'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { TabPanel, Tabs } from 'react-tabs'; +import styled from 'styled-components'; +import Theme from 'theme'; +import { + StickyStyledTabList, + StyledButton, + StyledSheetButton, +} from '../collectiblesTabs/index.styled'; +import SkeletonLoader from '../collectiblesTabs/skeletonLoader'; +import useNftDashboard from '../useNftDashboard'; + +const Container = styled.div` + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; + ${(props) => props.theme.scrollbar} +`; + +const PageHeader = styled.div` + padding: ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.l}; + border-bottom: 0.5px solid ${(props) => props.theme.colors.elevation3}; + max-width: 1224px; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +const CollectiblesContainer = styled.div` + padding: 0 ${(props) => props.theme.space.s}; + padding-bottom: ${(props) => props.theme.space.xl}; + max-width: 1224px; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +const RowCenterSpaceBetween = styled.div<{ addMarginBottom?: boolean }>` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: ${(props) => (props.addMarginBottom ? props.theme.space.m : 0)}; +`; + +const BackButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + gap: ${(props) => props.theme.space.xxs}; + background: transparent; + margin-bottom: ${(props) => props.theme.space.xl}; + color: ${(props) => props.theme.colors.white_0}; +`; + +const ItemCountContainer = styled.div<{ $isGalleryOpen: boolean }>` + margin-top: ${(props) => (props.$isGalleryOpen ? props.theme.space.l : props.theme.space.m)}; +`; + +type TabButton = { + key: string; + label: string; +}; + +const tabs: TabButton[] = [ + { + key: 'inscriptions', + label: 'INSCRIPTIONS', + }, + { + key: 'nfts', + label: 'NFTS', + }, +]; + +const tabKeyToIndex = (key?: string | null) => { + if (!key) return 0; + return tabs.findIndex((tab) => tab.key === key); +}; + +function NftDashboardHidden() { + const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); + const { t: tCommon } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const { t: tCollectibles } = useTranslation('translation', { + keyPrefix: 'COLLECTIBLE_COLLECTION_SCREEN', + }); + const [searchParams] = useSearchParams(); + const tab = searchParams?.get('tab'); + const { + isGalleryOpen, + hiddenInscriptionsQuery, + totalHiddenInscriptions, + HiddenInscriptionListView, + hiddenStacksNftsQuery, + totalHiddenNfts, + HiddenNftListView, + hasActivatedOrdinalsKey, + } = useNftDashboard(); + const { ordinalsAddress, stxAddress } = useSelectedAccount(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { hiddenCollectibleIds } = useWalletSelector(); + const [isOptionsModalVisible, setIsOptionsModalVisible] = useState(false); + const [tabIndex, setTabIndex] = useState(tabKeyToIndex(tab)); + + const visibleTabButtons = tabs.filter((tabItem: TabButton) => { + if (tabItem.key === 'inscriptions' && !hasActivatedOrdinalsKey) { + return false; + } + return true; + }); + + const handleBackButtonClick = () => { + navigate(`/nft-dashboard?tab=${tab}`); + }; + + const handleClickUndoHidingAll = ({ + toastId, + currentInscriptionsHidden, + currentStacksNftsHidden, + }: { + toastId: string; + currentInscriptionsHidden: Record; + currentStacksNftsHidden: Record; + }) => { + dispatch( + setHiddenCollectiblesAction({ + collectibleIds: { + [ordinalsAddress]: currentInscriptionsHidden, + [stxAddress]: currentStacksNftsHidden, + }, + }), + ); + toast.remove(toastId); + toast.custom(, { + duration: LONG_TOAST_DURATION, + }); + }; + + const handleUnHideAll = () => { + const currentInscriptionsHidden = { + ...(hiddenCollectibleIds[ordinalsAddress] ?? {}), + }; + const currentStacksNftsHidden = { + ...(hiddenCollectibleIds[stxAddress] ?? {}), + }; + + dispatch(removeAllFromHideCollectiblesAction({ address: ordinalsAddress })); + dispatch(removeAllFromHideCollectiblesAction({ address: stxAddress })); + handleBackButtonClick(); + + const toastId = toast.custom( + + handleClickUndoHidingAll({ + toastId, + currentInscriptionsHidden, + currentStacksNftsHidden, + }), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + }; + + const handleSelectTab = (index: number) => setTabIndex(index); + + const openOptionsDialog = () => { + setIsOptionsModalVisible(true); + }; + + const closeOptionsDialog = () => { + setIsOptionsModalVisible(false); + }; + + return ( + <> + {isGalleryOpen ? ( + + ) : ( + 0 || totalHiddenNfts > 0 ? openOptionsDialog : undefined + } + /> + )} + + + {isGalleryOpen && ( + + + + {tCollectibles('BACK_TO_GALLERY')} + + + )} + + + {t('HIDDEN_COLLECTIBLES')} + + {isGalleryOpen && (totalHiddenInscriptions > 0 || totalHiddenNfts > 0) ? ( + } + title={t('UNHIDE_ALL')} + onClick={handleUnHideAll} + /> + ) : null} + + + + {/* TODO: replace with Tabs component from `src/app/ui-library/tabs.tsx` */} + + {visibleTabButtons.length > 1 && ( + + {visibleTabButtons.map(({ key, label }) => ( + handleSelectTab(tabKeyToIndex(key))} + > + {t(label)} + + ))} + + )} + {hasActivatedOrdinalsKey && ( + +
+
+ {hiddenInscriptionsQuery.isInitialLoading ? ( + + ) : ( + <> + + {totalHiddenInscriptions > 0 ? ( + + {totalHiddenInscriptions === 1 + ? t('TOTAL_ITEMS_ONE') + : t('TOTAL_ITEMS', { count: totalHiddenInscriptions })} + + ) : ( +
+ )} + + + + )} +
+
+ + )} + + {hiddenStacksNftsQuery.isInitialLoading ? ( + + ) : ( + <> + + {totalHiddenNfts > 0 ? ( + + {totalHiddenNfts === 1 + ? t('TOTAL_ITEMS_ONE') + : t('TOTAL_ITEMS', { count: totalHiddenNfts })} + + ) : ( +
+ )} + + + + )} + + + + + {!isGalleryOpen && } + {isOptionsModalVisible && ( + + } + title={t('UNHIDE_ALL')} + onClick={handleUnHideAll} + /> + + )} + + ); +} + +export default NftDashboardHidden; diff --git a/src/app/screens/nftDashboard/index.tsx b/src/app/screens/nftDashboard/index.tsx index fade3b948..a9b98eac3 100644 --- a/src/app/screens/nftDashboard/index.tsx +++ b/src/app/screens/nftDashboard/index.tsx @@ -1,10 +1,10 @@ import FeatureIcon from '@assets/img/nftDashboard/rareSats/NewFeature.svg'; import AccountHeaderComponent from '@components/accountHeader'; -import ActionButton from '@components/button'; import ShowOrdinalReceiveAlert from '@components/showOrdinalReceiveAlert'; import BottomTabBar from '@components/tabBar'; import WebGalleryButton from '@components/webGalleryButton'; import { ArrowDown } from '@phosphor-icons/react'; +import Button from '@ui-library/button'; import { StyledHeading } from '@ui-library/common.styled'; import Dialog from '@ui-library/dialog'; import { useTranslation } from 'react-i18next'; @@ -31,7 +31,7 @@ const PageHeader = styled.div` width: 100%; `; -const StyledCollectiblesTabs = styled(CollectiblesTabs)` +const CollectiblesContainer = styled.div` padding: 0 ${(props) => props.theme.space.s}; padding-bottom: ${(props) => props.theme.space.xl}; max-width: 1224px; @@ -54,7 +54,7 @@ const ReceiveNftContainer = styled.div((props) => ({ })); const CollectibleContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); const ButtonContainer = styled.div({ @@ -90,7 +90,6 @@ function NftDashboard() { onActivateRareSatsAlertEnablePress, isGalleryOpen, } = nftDashboard; - return ( <> {isOrdinalReceiveAlertVisible && ( @@ -124,10 +123,10 @@ function NftDashboard() { - } - text={t('RECEIVE')} - onPress={onReceiveModalOpen} + title={t('RECEIVE')} + onClick={onReceiveModalOpen} /> {openReceiveModal && ( @@ -142,11 +141,13 @@ function NftDashboard() { )} - } - inscriptionListView={} - nftDashboard={nftDashboard} - /> + + } + inscriptionListView={} + nftDashboard={nftDashboard} + /> + {!isGalleryOpen && } diff --git a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx index c83077702..74d384f0b 100644 --- a/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx +++ b/src/app/screens/nftDashboard/inscriptionsTabGridItem.tsx @@ -1,4 +1,7 @@ import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { Star } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import type { InscriptionCollectionsData } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; @@ -10,6 +13,7 @@ import { } from '@utils/inscriptions'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import Theme from '../../../theme'; const CollectionContainer = styled.div((props) => ({ display: 'flex', @@ -28,6 +32,16 @@ const InfoContainer = styled.div` width: 100%; `; +const StyledItemIdContainer = styled.div` + display: flex; + gap: ${(props) => props.theme.space.xxs}; + width: 100%; +`; + +const StyledStar = styled(Star)` + margin-top: ${(props) => props.theme.space.xxxs}; +`; + const StyledItemId = styled(StyledP)` text-align: left; text-wrap: nowrap; @@ -46,15 +60,30 @@ const StyledItemSub = styled(StyledP)` function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollectionsData }) { const navigate = useNavigate(); + const { starredCollectibleIds, hiddenCollectibleIds } = useWalletSelector(); + const { ordinalsAddress } = useSelectedAccount(); + const collectionStarred = starredCollectibleIds[ordinalsAddress]?.some( + ({ id }) => id === collection.collection_id, + ); + const standaloneInscriptionStarred = + !collection.collection_id && + collection.total_inscriptions === 1 && + starredCollectibleIds[ordinalsAddress]?.some( + ({ id }) => id === collection.thumbnail_inscriptions?.[0].id, + ); - const handleClickCollectionId = (e: React.MouseEvent) => { + const isItemHidden = Object.keys(hiddenCollectibleIds[ordinalsAddress] ?? {}).some( + (id) => id === getCollectionKey(collection), + ); + + const handleClickCollection = (e: React.MouseEvent) => { const collectionId = e.currentTarget.value; - navigate(`/nft-dashboard/ordinals-collection/${collectionId}`); + navigate(`/nft-dashboard/ordinals-collection/${collectionId}/${isItemHidden ? 'hidden' : ''}`); }; - const handleClickInscriptionId = (e: React.MouseEvent) => { + const handleClickInscription = (e: React.MouseEvent) => { const inscriptionId = e.currentTarget.value; - navigate(`/nft-dashboard/ordinal-detail/${inscriptionId}`); + navigate(`/nft-dashboard/ordinal-detail/${inscriptionId}/${isItemHidden ? 'hidden' : ''}`); }; const itemId = getInscriptionsTabGridItemId(collection); @@ -66,7 +95,7 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec data-testid="inscription-container" type="button" value={getCollectionKey(collection)} - onClick={isCollection(collection) ? handleClickCollectionId : handleClickInscriptionId} + onClick={isCollection(collection) ? handleClickCollection : handleClickInscription} > {!collection.thumbnail_inscriptions ? ( // eslint-disable-line no-nested-ternary @@ -79,9 +108,15 @@ function InscriptionsTabGridItem({ item: collection }: { item: InscriptionCollec )} - - {itemId} - + + {(collectionStarred || standaloneInscriptionStarred) && ( + + )} + + {itemId} + + + ((props) => ({ +const NftImageContainer = styled.div<{ + $isGalleryView: boolean; +}>((props) => ({ display: 'flex', justifyContent: 'center', alignItems: 'flex-start', @@ -25,7 +19,7 @@ const NftImageContainer = styled.div((props) => ({ '> img': { width: '100%', }, - flexGrow: props.isGalleryView ? 1 : 'initial', + flexGrow: props.$isGalleryView ? 1 : 'initial', })); const GridItemContainer = styled.div((props) => ({ @@ -43,11 +37,17 @@ const BnsImage = styled.img({ height: '100%', }); +type Props = { + asset: NonFungibleToken; + isGalleryOpen: boolean; +}; + function Nft({ asset, isGalleryOpen }: Props) { const { data } = useNftDetail(asset.identifier); + return ( - + {isBnsContract(asset?.asset_identifier) ? ( ) : ( @@ -57,4 +57,5 @@ function Nft({ asset, isGalleryOpen }: Props) { ); } + export default Nft; diff --git a/src/app/screens/nftDashboard/nftImage.tsx b/src/app/screens/nftDashboard/nftImage.tsx index 07aa50521..c3249d26e 100644 --- a/src/app/screens/nftDashboard/nftImage.tsx +++ b/src/app/screens/nftDashboard/nftImage.tsx @@ -94,6 +94,10 @@ function NftImage({ metadata, isInCollage = false }: Props) { playsInline controls preload="auto" + onClick={(event) => { + // Prevent playback when clicking anywhere other than the controls + event.preventDefault(); + }} /> ); } diff --git a/src/app/screens/nftDashboard/nftTabGridItem.tsx b/src/app/screens/nftDashboard/nftTabGridItem.tsx index ee3ee663f..5fd1c8c15 100644 --- a/src/app/screens/nftDashboard/nftTabGridItem.tsx +++ b/src/app/screens/nftDashboard/nftTabGridItem.tsx @@ -1,9 +1,13 @@ import CollectibleCollage from '@components/collectibleCollage/collectibleCollage'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { Star } from '@phosphor-icons/react'; import type { StacksCollectionData } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import { getNftsTabGridItemSubText } from '@utils/nfts'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import Theme from 'theme'; import Nft from './nft'; import NftImage from './nftImage'; @@ -24,6 +28,12 @@ const InfoContainer = styled.div` width: 100%; `; +const TitleContainer = styled.div` + display: flex; + gap: 4px; + width: 100%; +`; + const StyledItemId = styled(StyledP)` text-align: left; text-wrap: nowrap; @@ -40,17 +50,32 @@ const StyledItemSub = styled(StyledP)` width: 100%; `; -function NftTabGridItem({ - item: collection, - isLoading = false, -}: { +const StyledStar = styled(Star)` + margin-top: ${(props) => props.theme.space.xxxs}; +`; + +type Props = { item: StacksCollectionData; isLoading?: boolean; -}) { +}; + +function NftTabGridItem({ item: collection, isLoading = false }: Props) { const navigate = useNavigate(); + const { stxAddress } = useSelectedAccount(); + const { hiddenCollectibleIds, starredCollectibleIds } = useWalletSelector(); + + const isItemHidden = Object.keys(hiddenCollectibleIds[stxAddress] ?? {}).some( + (id) => id === collection.collection_id, + ); + + const collectionStarred = starredCollectibleIds[stxAddress]?.some( + ({ id }) => id === collection.collection_id, + ); const handleClickCollection = () => { - navigate(`nft-collection/${collection.collection_id}`); + navigate( + `/nft-dashboard/nft-collection/${collection.collection_id}/${isItemHidden ? 'hidden' : ''}`, + ); }; const itemId = collection.collection_name; @@ -68,9 +93,12 @@ function NftTabGridItem({ )} - - {itemId} - + + {collectionStarred && } + + {itemId} + + {itemSubText} diff --git a/src/app/screens/nftDashboard/receiveNft/index.tsx b/src/app/screens/nftDashboard/receiveNft/index.tsx index 9d7d72432..16a5d4c91 100644 --- a/src/app/screens/nftDashboard/receiveNft/index.tsx +++ b/src/app/screens/nftDashboard/receiveNft/index.tsx @@ -3,24 +3,26 @@ import plusIcon from '@assets/img/dashboard/plus.svg'; import stacksIcon from '@assets/img/dashboard/stx_icon.svg'; import ordinalsIcon from '@assets/img/nftDashboard/ordinals_icon.svg'; import ActionButton from '@components/button'; -import UpdatedBottomModal from '@components/updatedBottomModal'; +import ReceiveCardComponent from '@components/receiveCardComponent'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { Plus } from '@phosphor-icons/react'; +import Sheet from '@ui-library/sheet'; import { isInOptions, isLedgerAccount } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import ReceiveCardComponent from '../../../components/receiveCardComponent'; + +const GalleryModalContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); const ColumnContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', marginTop: props.theme.space.l, marginBottom: props.theme.space.xl, - paddingLeft: props.theme.space.m, - paddingRight: props.theme.space.m, gap: props.theme.space.m, })); @@ -80,12 +82,12 @@ const VerifyButtonContainer = styled.div((props) => ({ marginBottom: props.theme.spacing(6), })); -interface Props { +type Props = { visible: boolean; onClose: () => void; setOrdinalReceiveAlert: () => void; isGalleryOpen: boolean; -} +}; function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAlert }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DASHBOARD_SCREEN' }); @@ -140,14 +142,15 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle onQrAddressClick={onOrdinalsReceivePress} showVerifyButton={choseToVerifyAddresses} currency="ORD" - > - - - - - - - + icon={ + + + + + + + } + /> )} {stxAddress && ( @@ -157,14 +160,15 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle onQrAddressClick={onReceivePress} showVerifyButton={choseToVerifyAddresses} currency="STX" - > - - - - - - - + icon={ + + + + + + + } + /> )} {isLedgerAccount(selectedAccount) && !stxAddress && ( @@ -214,16 +218,14 @@ function ReceiveNftModal({ visible, onClose, isGalleryOpen, setOrdinalReceiveAle cross - {isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses} + + {isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses} + ) : ( - + {isReceivingAddressesVisible ? receiveContent : verifyOrViewAddresses} - + ); } diff --git a/src/app/screens/nftDashboard/useNftDashboard.tsx b/src/app/screens/nftDashboard/useNftDashboard.tsx index b2cbd6971..67427cb19 100644 --- a/src/app/screens/nftDashboard/useNftDashboard.tsx +++ b/src/app/screens/nftDashboard/useNftDashboard.tsx @@ -1,5 +1,4 @@ -import ActionButton from '@components/button'; -import useAddressInscriptionCollections from '@hooks/queries/ordinals/useAddressInscriptionCollections'; +import useAddressInscriptions from '@hooks/queries/ordinals/useAddressInscriptions'; import { useAddressRareSats } from '@hooks/queries/ordinals/useAddressRareSats'; import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import useWalletSelector from '@hooks/useWalletSelector'; @@ -10,6 +9,7 @@ import { ChangeActivateRareSatsAction, SetRareSatsNoticeDismissedAction, } from '@stores/wallet/actions/actionCreators'; +import Button from '@ui-library/button'; import { getCollectionKey } from '@utils/inscriptions'; import { InvalidParamsError } from '@utils/query'; import { useCallback, useEffect, useMemo, useRef, useState, type PropsWithChildren } from 'react'; @@ -17,27 +17,27 @@ import { useTranslation } from 'react-i18next'; import { useIsVisible } from 'react-is-visible'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { GridContainer } from './collectiblesTabs'; +import { GridContainer } from './collectiblesTabs/index.styled'; import InscriptionsTabGridItem from './inscriptionsTabGridItem'; import NftTabGridItem from './nftTabGridItem'; const NoCollectiblesText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), + marginTop: props.theme.space.xl, marginBottom: 'auto', textAlign: 'center', })); const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(20), + marginTop: props.theme.space.xxl, display: 'flex', flexDirection: 'column', alignItems: 'center', })); const ErrorTextContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), + marginTop: props.theme.space.m, display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -75,7 +75,9 @@ export type NftDashboardState = { showNewFeatureAlert: boolean; isOrdinalReceiveAlertVisible: boolean; stacksNftsQuery: ReturnType; - inscriptionsQuery: ReturnType; + inscriptionsQuery: ReturnType; + hiddenInscriptionsQuery: ReturnType; + hiddenStacksNftsQuery: ReturnType; rareSatsQuery: ReturnType; openInGalleryView: () => void; onReceiveModalOpen: () => void; @@ -83,7 +85,9 @@ export type NftDashboardState = { onOrdinalReceiveAlertOpen: () => void; onOrdinalReceiveAlertClose: () => void; InscriptionListView: () => JSX.Element; + HiddenInscriptionListView: () => JSX.Element; NftListView: () => JSX.Element; + HiddenNftListView: () => JSX.Element; onActivateRareSatsAlertCrossPress: () => void; onActivateRareSatsAlertDenyPress: () => void; onActivateRareSatsAlertEnablePress: () => void; @@ -93,7 +97,9 @@ export type NftDashboardState = { hasActivatedRareSatsKey?: boolean; showNoticeAlert?: boolean; totalNfts: number; + totalHiddenNfts: number; totalInscriptions: number; + totalHiddenInscriptions: number; }; const useNftDashboard = (): NftDashboardState => { @@ -106,11 +112,16 @@ const useNftDashboard = (): NftDashboardState => { const [showNoticeAlert, setShowNoticeAlert] = useState(false); const [isOrdinalReceiveAlertVisible, setIsOrdinalReceiveAlertVisible] = useState(false); const stacksNftsQuery = useStacksCollectibles(); - const inscriptionsQuery = useAddressInscriptionCollections(); + const hiddenStacksNftsQuery = useStacksCollectibles(true); + const inscriptionsQuery = useAddressInscriptions(); + const hiddenInscriptionsQuery = useAddressInscriptions(true); const rareSatsQuery = useAddressRareSats(); const totalInscriptions = inscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0; + const totalHiddenInscriptions = hiddenInscriptionsQuery.data?.pages?.[0]?.total_inscriptions ?? 0; + const totalNfts = stacksNftsQuery.data?.total_nfts ?? 0; + const totalHiddenNfts = hiddenStacksNftsQuery.data?.total_nfts ?? 0; const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); @@ -168,7 +179,7 @@ const useNftDashboard = (): NftDashboardState => { return ( <> - + {inscriptionsQuery.data?.pages ?.map((page) => page?.results) .flat() @@ -178,12 +189,12 @@ const useNftDashboard = (): NftDashboardState => { {inscriptionsQuery.hasNextPage && ( - inscriptionsQuery.fetchNextPage()} + onClick={() => inscriptionsQuery.fetchNextPage()} /> )} @@ -191,6 +202,51 @@ const useNftDashboard = (): NftDashboardState => { ); }, [inscriptionsQuery, isGalleryOpen, totalInscriptions, t]); + const HiddenInscriptionListView = useCallback(() => { + if ( + hiddenInscriptionsQuery.error && + !(hiddenInscriptionsQuery.error instanceof InvalidParamsError) + ) { + return ( + + + + {t('ERROR_RETRIEVING')} + {t('TRY_AGAIN')} + + + ); + } + + if (totalHiddenInscriptions === 0) { + return {t('NO_COLLECTIBLES')}; + } + + return ( + <> + + {hiddenInscriptionsQuery.data?.pages + ?.map((page) => page?.results) + .flat() + .map((collection: InscriptionCollectionsData) => ( + + ))} + + {hiddenInscriptionsQuery.hasNextPage && ( + + - @@ -201,31 +286,12 @@ function NftDetailScreen() { <> - {t('MOVE_TO_ASSET_DETAIL')} + {t('BACK_TO_COLLECTION')} - - - - - - - - - - - - - - - - - - - - + ) : ( @@ -234,7 +300,7 @@ function NftDetailScreen() { - {t('MOVE_TO_ASSET_DETAIL')} + {t('BACK_TO_COLLECTION')} @@ -254,38 +320,59 @@ function NftDetailScreen() { )}`} - - - } - text={t('SEND')} - onPress={handleOnSendClick} - /> - - - - } - text={t('SHARE')} - onPress={onSharePress} - hoverDialogId={`copy-nft-url-${nftData?.asset_id}`} - transparent - /> - - + + } + title={t('SEND')} + onClick={handleOnSendClick} + /> + } + title={t('SHARE')} + onClick={onSharePress} + variant="secondary" + /> + + {isNftCollectionHidden ? null : ( + <> + + ) : ( + + ) + } + onPress={handleStarClick} + isTransparent + size={44} + radiusSize={12} + /> + + } + onPress={optionsSheet.open} + isTransparent + size={44} + radiusSize={12} + /> + + )} {nftDetails} - - @@ -300,7 +387,12 @@ function NftDetailScreen() { {isGalleryOpen ? ( ) : ( - + )} {isGalleryOpen ? galleryView : extensionView} {!isGalleryOpen && ( @@ -308,6 +400,29 @@ function NftDetailScreen() { )} + {optionsSheet.isVisible && ( + + {isNftSelectedAsAvatar ? ( + } + title={optionsDialogT('NFT_AVATAR.REMOVE_ACTION')} + onClick={handleRemoveAvatar} + /> + ) : ( + } + title={optionsDialogT('NFT_AVATAR.SET_ACTION')} + onClick={handleSetAvatar} + /> + )} + + )} ); } diff --git a/src/app/screens/nftDetail/loaders.tsx b/src/app/screens/nftDetail/loaders.tsx new file mode 100644 index 000000000..f816cfd89 --- /dev/null +++ b/src/app/screens/nftDetail/loaders.tsx @@ -0,0 +1,113 @@ +import { BetterBarLoader } from '@components/barLoader'; +import Separator from '@components/separator'; +import styled from 'styled-components'; +import { ButtonContainer, GalleryRowContainer } from './index.styled'; + +const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.space.xs, +})); + +const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.space.m}`, +})); + +const StyledBarLoader = styled(BetterBarLoader)<{ + withMarginBottom?: boolean; +}>((props) => ({ + padding: 0, + borderRadius: props.theme.radius(1), + marginBottom: props.withMarginBottom ? props.theme.space.s : 0, +})); + +const StyledSeparator = styled(Separator)` + width: 100%; +`; + +const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; + +export function GalleryLoader() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} + +export function ExtensionLoader() { + return ( + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+
+ ); +} diff --git a/src/app/screens/nftDetail/useNftDetail.ts b/src/app/screens/nftDetail/useNftDetail.ts index d8817b04f..c211ad327 100644 --- a/src/app/screens/nftDetail/useNftDetail.ts +++ b/src/app/screens/nftDetail/useNftDetail.ts @@ -2,6 +2,7 @@ import useNftDetail from '@hooks/queries/useNftDetail'; import useStacksCollectibles from '@hooks/queries/useStacksCollectibles'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; import { GAMMA_URL } from '@utils/constants'; import { getExplorerUrl, isInOptions, isLedgerAccount } from '@utils/helper'; import { useMemo } from 'react'; @@ -11,10 +12,13 @@ export default function useNftDetailScreen() { const navigate = useNavigate(); const selectedAccount = useSelectedAccount(); const { id } = useParams(); - + const { hiddenCollectibleIds } = useWalletSelector(); const nftDetailQuery = useNftDetail(id!); - const nftCollectionsQuery = useStacksCollectibles(); const collectionId = nftDetailQuery.data?.data.collection_contract_id; + const collectionHidden = Object.keys(hiddenCollectibleIds[selectedAccount.stxAddress] ?? {}).some( + (itemId) => itemId === collectionId, + ); + const nftCollectionsQuery = useStacksCollectibles(collectionHidden); const collection = nftCollectionsQuery.data?.results.find( (c) => c.collection_id === collectionId, ); @@ -32,7 +36,7 @@ export default function useNftDetailScreen() { }; const handleBackButtonClick = () => { - navigate(`/nft-dashboard/nft-collection/${collectionId}`); + navigate(`/nft-dashboard/nft-collection/${collectionId}${collectionHidden ? '/hidden' : ''}`); }; const onGammaPress = () => { diff --git a/src/app/screens/ordinalDetail/index.styled.ts b/src/app/screens/ordinalDetail/index.styled.ts index b9cb58569..fea6707a7 100644 --- a/src/app/screens/ordinalDetail/index.styled.ts +++ b/src/app/screens/ordinalDetail/index.styled.ts @@ -31,9 +31,13 @@ export const GalleryContainer = styled.div({ export const BackButtonContainer = styled.div((props) => ({ display: 'flex', - flexDirection: 'row', - width: 820, + justifyContent: 'flex-start', + width: '100%', + maxWidth: 820, marginTop: props.theme.spacing(40), + button: { + width: 'auto', + }, })); export const ButtonContainer = styled.div((props) => ({ @@ -41,7 +45,7 @@ export const ButtonContainer = styled.div((props) => ({ position: 'relative', flexDirection: 'row', maxWidth: 400, - columnGap: props.theme.spacing(8), + columnGap: props.theme.space.m, marginBottom: props.theme.spacing(10.5), })); @@ -49,11 +53,11 @@ export const ExtensionContainer = styled.div((props) => ({ ...props.theme.scrollbar, display: 'flex', flexDirection: 'column', - marginTop: props.theme.spacing(4), + marginTop: props.theme.space.xs, alignItems: 'center', flex: 1, - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), + paddingLeft: props.theme.space.xs, + paddingRight: props.theme.space.xs, })); export const OrdinalsContainer = styled.div((props) => ({ @@ -65,7 +69,7 @@ export const OrdinalsContainer = styled.div((props) => ({ justifyContent: 'flex-start', alignItems: 'flex-start', borderRadius: 8, - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, })); export const ExtensionOrdinalsContainer = styled.div((props) => ({ @@ -76,7 +80,7 @@ export const ExtensionOrdinalsContainer = styled.div((props) => ({ justifyContent: 'center', alignItems: 'center', borderRadius: props.theme.radius(1), - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, marginBottom: props.theme.space.m, })); @@ -144,7 +148,7 @@ export const DescriptionContainer = styled.div((props) => ({ })); export const StyledWebGalleryButton = styled(WebGalleryButton)` - margintop: ${(props) => props.theme.space.s}; + margin-top: ${(props) => props.theme.space.xs}; `; export const ViewInExplorerButton = styled.button((props) => ({ @@ -184,29 +188,6 @@ export const StyledTooltip = styled(Tooltip)` } `; -export const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -export const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -export const AssetDeatilButtonText = styled.div((props) => ({ - ...props.theme.typography.body_s, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - export const CollectibleText = styled.h1((props) => ({ ...props.theme.typography.body_bold_m, color: props.theme.colors.white_400, @@ -246,54 +227,12 @@ export const DetailSection = styled.div((props) => ({ width: '100%', })); -export const ExtensionLoaderContainer = styled.div({ - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-around', - alignItems: 'center', -}); - -export const GalleryLoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - export const StyledBarLoader = styled(BetterBarLoader)<{ withMarginBottom?: boolean; }>((props) => ({ padding: 0, borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, -})); - -export const StyledSeparator = styled(Separator)` - width: 100%; -`; - -export const TitleLoader = styled.div` - display: flex; - flex-direction: column; -`; - -export const ActionButtonsLoader = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - columnGap: props.theme.spacing(11), -})); - -export const ActionButtonLoader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - rowGap: props.theme.spacing(4), -})); - -export const InfoContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - padding: `0 ${props.theme.spacing(8)}px`, + marginBottom: props.withMarginBottom ? props.theme.space.s : 0, })); export const RareSatsBundleCallout = styled(Callout)((props) => ({ diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index ea8bd71a7..8780cb8bc 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -1,30 +1,52 @@ import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; import AlertMessage from '@components/alertMessage'; -import ActionButton from '@components/button'; import CollectibleDetailTile from '@components/collectibleDetailTile'; import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; +import useOptionsSheet from '@hooks/useOptionsSheet'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import { ArrowUp, Share } from '@phosphor-icons/react'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { + ArchiveTray, + ArrowUp, + DotsThreeVertical, + Share, + Star, + UserCircleCheck, + UserCircleMinus, +} from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { StyledButton } from '@screens/ordinalsCollection/index.styled'; +import { + addToHideCollectiblesAction, + addToStarCollectiblesAction, + removeAccountAvatarAction, + removeFromHideCollectiblesAction, + removeFromStarCollectiblesAction, + setAccountAvatarAction, +} from '@stores/wallet/actions/actionCreators'; +import Button from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; -import { EMPTY_LABEL } from '@utils/constants'; +import Sheet from '@ui-library/sheet'; +import SnackBar from '@ui-library/snackBar'; +import { EMPTY_LABEL, LONG_TOAST_DURATION } from '@utils/constants'; import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; +import { useCallback } from 'react'; +import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import Theme from '../../../theme'; import { - ActionButtonLoader, - ActionButtonsLoader, - AssetDeatilButtonText, BackButtonContainer, Badge, BottomBarContainer, - Button, ButtonContainer, ButtonHiglightedText, - ButtonImage, ButtonText, CollectibleText, ColumnContainer, @@ -34,14 +56,11 @@ import { DetailSection, Divider, ExtensionContainer, - ExtensionLoaderContainer, ExtensionOrdinalsContainer, GalleryButtonContainer, GalleryCollectibleText, GalleryContainer, - GalleryLoaderContainer, GalleryScrollContainer, - InfoContainer, OrdinalDetailsContainer, OrdinalGalleryTitleText, OrdinalsContainer, @@ -55,18 +74,21 @@ import { SatributesBadges, SatributesIconsContainer, StyledBarLoader, - StyledSeparator, StyledTooltip, StyledWebGalleryButton, - TitleLoader, ViewInExplorerButton, } from './index.styled'; +import { ExtensionLoader, GalleryLoader } from './loaders'; import OrdinalAttributeComponent from './ordinalAttributeComponent'; import useOrdinalDetail from './useOrdinalDetail'; function OrdinalDetailScreen() { + const optionsSheet = useOptionsSheet(); + const { t: optionsDialogT } = useTranslation('translation', { keyPrefix: 'OPTIONS_DIALOG' }); const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); + const navigate = useNavigate(); + const ordinalDetails = useOrdinalDetail(); const { ordinal, @@ -90,11 +112,137 @@ function OrdinalDetailScreen() { onCopyClick, backButtonText, } = ordinalDetails; - + const { starredCollectibleIds, hiddenCollectibleIds, avatarIds } = useWalletSelector(); + const selectedAvatar = avatarIds[ordinalsAddress]; + const isInscriptionSelectedAsAvatar = + selectedAvatar?.type === 'inscription' && selectedAvatar.inscription.id === ordinal?.id; + const inscriptionStarred = starredCollectibleIds[ordinalsAddress]?.some( + ({ id }) => id === ordinal?.id, + ); + const isStandaloneInscription = !ordinal?.collection_id; + const isInscriptionHidden = Object.keys(hiddenCollectibleIds[ordinalsAddress] ?? {}).some( + (id) => id === ordinal?.id, + ); + const isInscriptionCollectionHidden = Object.keys( + hiddenCollectibleIds[ordinalsAddress] ?? {}, + ).some((id) => id === ordinal?.collection_id); + const isHidden = isInscriptionHidden || isInscriptionCollectionHidden; const isBrc20Ordinal = Boolean(brc20Details); + const dispatch = useDispatch(); useResetUserFlow('/ordinal-detail'); + const handleUnstarClick = (toastId: string) => { + dispatch(removeFromStarCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' })); + toast.remove(toastId); + toast.custom(); + }; + + const handleStarClick = () => { + if (inscriptionStarred) { + dispatch( + removeFromStarCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' }), + ); + toast.custom(); + } else { + const toastId = toast.custom( + handleUnstarClick(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + dispatch( + addToStarCollectiblesAction({ + address: ordinalsAddress, + id: ordinal?.id ?? '', + collectionId: ordinal?.collection_id ?? '', + }), + ); + } + }; + + const handleClickUndoHiding = (toastId: string) => { + dispatch(removeFromHideCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' })); + toast.remove(toastId); + toast.custom(, { duration: 2000 }); + }; + + const handleHideStandaloneInscription = () => { + dispatch(addToHideCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' })); + + if (isInscriptionSelectedAsAvatar) { + dispatch(removeAccountAvatarAction({ address: ordinalsAddress })); + } + + optionsSheet.close(); + navigate('/nft-dashboard?tab=inscriptions'); + const toastId = toast.custom( + handleClickUndoHiding(toastId), + }} + />, + { duration: LONG_TOAST_DURATION }, + ); + }; + + const handleUnHideStandaloneInscription = () => { + const isLastHiddenItem = Object.keys(hiddenCollectibleIds[ordinalsAddress] ?? {}).length === 1; + dispatch(removeFromHideCollectiblesAction({ address: ordinalsAddress, id: ordinal?.id ?? '' })); + optionsSheet.close(); + toast.custom(); + navigate(`/nft-dashboard/${isLastHiddenItem ? '' : 'hidden'}?tab=inscriptions`); + }; + + const handleSetAvatar = useCallback(() => { + if (ordinalsAddress && ordinal?.id) { + dispatch( + setAccountAvatarAction({ + address: ordinalsAddress, + avatar: { type: 'inscription', inscription: ordinal }, + }), + ); + + const toastId = toast.custom( + { + if (selectedAvatar?.type) { + dispatch( + setAccountAvatarAction({ address: ordinalsAddress, avatar: selectedAvatar }), + ); + } else { + dispatch(removeAccountAvatarAction({ address: ordinalsAddress })); + } + + toast.remove(toastId); + toast.custom(); + }, + }} + />, + ); + } + + optionsSheet.close(); + }, [dispatch, optionsDialogT, commonT, ordinalsAddress, ordinal, optionsSheet, selectedAvatar]); + + const handleRemoveAvatar = useCallback(() => { + dispatch(removeAccountAvatarAction({ address: ordinalsAddress })); + toast.custom(); + optionsSheet.close(); + }, [dispatch, ordinalsAddress, optionsDialogT, optionsSheet]); + const ordinalDetailAttributes = ( {!isGalleryOpen && ordinal?.collection_id && ( @@ -296,35 +444,7 @@ function OrdinalDetailScreen() { ); const extensionView = isLoading ? ( - - - - - - - - - - - - - - - - - - - -
- - -
-
- - -
-
-
+ ) : ( @@ -372,33 +492,18 @@ function OrdinalDetailScreen() { - + + )} diff --git a/src/app/screens/settings/connectedAppsAndPermissions/client.tsx b/src/app/screens/settings/connectedAppsAndPermissions/client.tsx new file mode 100644 index 000000000..3e6d30710 --- /dev/null +++ b/src/app/screens/settings/connectedAppsAndPermissions/client.tsx @@ -0,0 +1,93 @@ +import { usePermissionsUtils } from '@components/permissionsManager'; +import type { Client as ClientType } from '@components/permissionsManager/schemas'; +import { CaretRight } from '@phosphor-icons/react'; +import { getAppIconFromWebManifest } from '@secretkeylabs/xverse-core'; +import { useQuery } from '@tanstack/react-query'; +import { useTheme } from 'styled-components'; +import type { ConnectedApp } from '.'; +import { + Client, + ClientDescription, + ClientHeader, + ClientName, + DappIcon, + DappIconPlaceholder, + PermissionContainer, + PermissionText, +} from './index.styles'; + +async function getWebsiteInfo(url: string): Promise<{ name: string; icon: string }> { + try { + const parsedUrl = new URL(url); + const { hostname } = parsedUrl; + const appIcon = await getAppIconFromWebManifest(url); + return { name: hostname, icon: appIcon }; + } catch (error) { + return { name: '', icon: '' }; + } +} + +type PermissionsProps = { + clientId: string; + color?: string; +}; +export function ClientPermissions({ clientId, color }: PermissionsProps) { + const utils = usePermissionsUtils(); + const sortedPermissions = utils + .getClientPermissions(clientId) + .sort((p1, p2) => p1.resourceId.localeCompare(p2.resourceId)); + + return sortedPermissions.map((p) => { + const resource = utils.getResource(p.resourceId); + if (!resource) return null; + + return ( + + {[...p.actions].map((a) => ( + + {a} + + ))} + + ); + }); +} + +interface Props { + client: ConnectedApp; + setSelectedApp: (app: ConnectedApp) => void; +} + +export default function ClientApp({ client, setSelectedApp }: Props) { + const theme = useTheme(); + const { data: appDetails } = useQuery({ + queryKey: ['websiteInfo', client.id], + queryFn: () => getWebsiteInfo(client.id), + placeholderData: { name: client.name, icon: '' }, + enabled: !!client.id, + }); + + const handleSelectApp = () => { + setSelectedApp({ + ...client, + name: appDetails?.name || client.name, + }); + }; + + return ( + + + {appDetails?.icon ? ( + + ) : ( + + )} + + {appDetails?.name} + + + + + + ); +} diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts index 29edd0662..5d32a06a0 100644 --- a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts @@ -1,5 +1,6 @@ /* eslint-disable import/prefer-default-export */ +import { Globe } from '@phosphor-icons/react'; import styled from 'styled-components'; export const Container = styled.div((props) => ({ @@ -10,33 +11,81 @@ export const Container = styled.div((props) => ({ paddingRight: props.theme.space.m, })); -export const ClientHeader = styled('div')({ +export const Client = styled('button')((props) => ({ display: 'flex', + alignItems: 'center', justifyContent: 'space-between', -}); + backgroundColor: props.theme.colors.elevation1, + padding: props.theme.space.s, + marginBottom: props.theme.space.s, + borderRadius: props.theme.space.s, +})); -export const ClientName = styled('div')({ - fontWeight: 'bold', +export const ClientDescription = styled('div')({ + display: 'flex', + alignItems: 'center', }); -export const Row = styled('div')({ - paddingLeft: '10px', +export const ClientHeader = styled('div')({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'flex-start', }); +export const DappIconPlaceholder = styled(Globe)((props) => ({ + width: 32, + height: 32, + marginRight: props.theme.space.s, +})); + +export const DappIcon = styled('img')((props) => ({ + width: 32, + height: 32, + marginRight: props.theme.space.s, + borderRadius: props.theme.radius(1), +})); + export const PermissionContainer = styled('div')({ display: 'flex', flexDirection: 'column', }); -export const PermissionTitle = styled('div')({ - fontWeight: 'bold', -}); +export const ButtonContainer = styled('div')((props) => ({ + marginTop: 'auto', + marginBottom: props.theme.space.l, +})); -export const PermissionDescription = styled('div')({ - paddingLeft: '10px', -}); +export const ClientName = styled('div')((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, +})); -export const Button = styled('button')({ - borderRadius: '4px', - padding: '0.2em 0.5em', -}); +export const ClientPropertyContainer = styled('div')((props) => ({ + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.space.xs, +})); + +export const Row = styled('div')((props) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: props.theme.space.m, +})); + +export const ClientProperty = styled('div')((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, +})); + +export const ClientPropertyValue = styled('div')((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, +})); + +export const PermissionText = styled('div')(({ theme, color }) => ({ + ...theme.typography.body_m, + color: color || theme.colors.white_0, + textTransform: 'capitalize', +})); diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx index 716ad6ceb..8df7aa416 100644 --- a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx @@ -1,84 +1,125 @@ +import { dispatchEventToOrigin } from '@common/utils/messages/extensionToContentScript/dispatchEvent'; import { usePermissionsStore, usePermissionsUtils } from '@components/permissionsManager'; -import * as utils from '@components/permissionsManager/utils'; +import type { Client as ClientType } from '@components/permissionsManager/schemas'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import { useCallback } from 'react'; +import Button from '@ui-library/button'; +import { formatDate } from '@utils/date'; +import { useCallback, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { SubTitle, Title } from '../index.styles'; +import ClientApp, { ClientPermissions } from './client'; import { - Button, - ClientHeader, - ClientName, + ButtonContainer, + ClientProperty, + ClientPropertyContainer, + ClientPropertyValue, Container, - PermissionContainer, - PermissionDescription, - PermissionTitle, Row, } from './index.styles'; +export type ConnectedApp = ClientType & { lastUsed: number }; + function ConnectedAppsAndPermissionsScreen() { const { t } = useTranslation('translation', { keyPrefix: 'CONNECTED_APPS' }); + const [selectedApp, setSelectedApp] = useState(null); const navigate = useNavigate(); - const { removeClient } = usePermissionsUtils(); + const { removeClient, removeAllClients, getClientMetadata } = usePermissionsUtils(); const { store } = usePermissionsStore(); + const getClientsSortedByLastUsed = useCallback((): ConnectedApp[] | undefined => { + if (!store || !store.clients) { + return undefined; + } + + const clientsArray = Array.from(store.clients); + + return clientsArray + .map((client) => { + const lastUsed = getClientMetadata(client.id)?.lastUsed || 0; + return { ...client, lastUsed }; + }) + .sort((a, b) => b.lastUsed - a.lastUsed); + }, [store, getClientMetadata]); + + const clientsSortedByLastUsed = useMemo( + () => getClientsSortedByLastUsed(), + [getClientsSortedByLastUsed], + ); + const handleBackButtonClick = useCallback(() => { + if (selectedApp) { + setSelectedApp(null); + return; + } navigate('/settings'); - }, [navigate]); + }, [navigate, selectedApp]); - if (!store) { - return null; - } + const handleDisconnect = useCallback(async () => { + if (selectedApp) { + await removeClient(selectedApp.id); + dispatchEventToOrigin(selectedApp.origin, { + type: 'disconnect', + }); + toast.success(t('DISCONNECT_SUCCESS')); + setSelectedApp(null); + return; + } + if (store) { + store.clients.forEach((client) => { + dispatchEventToOrigin(client.origin, { + type: 'disconnect', + }); + }); + await removeAllClients(); + } + toast.success(t('DISCONNECT_ALL_SUCCESS')); + }, [selectedApp, removeClient, removeAllClients, t, store]); return ( <> - {t('TITLE')} - {store.clients.size === 0 ? t('EMPTY_MESSAGE') : t('SUBTITLE')} - {[...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}
- ))} -
-
-
- ); - })()} -
- ))} -
- ))} + {selectedApp ? ( + <> + {selectedApp.name} + + + {t('URL')} + {selectedApp.name} + + + {t('LAST_USED')} + + {selectedApp.lastUsed ? formatDate(new Date(selectedApp.lastUsed)) : '-'} + + + + {t('PERMISSIONS')} + + + + + ) : ( + <> + {t('TITLE')} + {store?.clients.size === 0 ? t('EMPTY_MESSAGE') : t('SUBTITLE')} + {clientsSortedByLastUsed?.map((client) => ( + + ))} + + )} + {(store?.clients?.size ?? 0) > 0 || selectedApp ? ( + + diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index 60777d69e..fcadbddf3 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -1,184 +1,53 @@ import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types'; -import { delay } from '@common/utils/ledger'; -import AccountHeaderComponent from '@components/accountHeader'; -import AssetModal from '@components/assetModal'; -import BurnSection from '@components/confirmBtcTransaction/burnSection'; -import DelegateSection from '@components/confirmBtcTransaction/delegateSection'; -import { - ParsedTxSummaryContext, - type ParsedTxSummaryContextProps, -} from '@components/confirmBtcTransaction/hooks/useParsedTxSummaryContext'; -import MintSection from '@components/confirmBtcTransaction/mintSection'; -import ReceiveSection from '@components/confirmBtcTransaction/receiveSection'; -import TransactionSummary from '@components/confirmBtcTransaction/transactionSummary'; -import TransferSection from '@components/confirmBtcTransaction/transferSection'; -import { isScriptOutput } from '@components/confirmBtcTransaction/utils'; -import InfoContainer from '@components/infoContainer'; -import LoadingTransactionStatus from '@components/loadingTransactionStatus'; -import type { ConfirmationStatus } from '@components/loadingTransactionStatus/circularSvgAnimation'; -import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useHasFeature from '@hooks/useHasFeature'; +import BatchPsbtSigning from '@components/batchPsbtSigning'; import useSelectedAccount from '@hooks/useSelectedAccount'; -import useSignBatchPsbtTx from '@hooks/useSignBatchPsbtTx'; -import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed'; -import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import { ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import type { SignMultiplePsbtPayload } from '@sats-connect/core'; -import { - AnalyticsEvents, - FeatureId, - btcTransaction, - parseSummaryForRunes, - type RuneSummary, -} from '@secretkeylabs/xverse-core'; -import Button from '@ui-library/button'; -import Callout from '@ui-library/callout'; -import Spinner from '@ui-library/spinner'; -import { isLedgerAccount } from '@utils/helper'; -import { trackMixPanel } from '@utils/mixpanel'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { SignMultipleTransactionOptions } from '@sats-connect/core'; +import { decodeToken } from 'jsontokens'; +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { - BundleLinkContainer, - BundleLinkText, - ButtonsContainer, - Container, - LoaderContainer, - ModalContainer, - OuterContainer, - ReviewTransactionText, - StyledSheet, - TransparentButtonContainer, - TxReviewModalControls, -} from './index.styled'; - -interface TxResponse { - txId: string; - psbtBase64: string; -} - -type PsbtSummary = btcTransaction.PsbtSummary; -type ParsedPsbt = { summary: PsbtSummary; runeSummary: RuneSummary | undefined }; function SignBatchPsbtRequest() { + const navigate = useNavigate(); const selectedAccount = useSelectedAccount(); + const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const { network } = useWalletSelector(); - const navigate = useNavigate(); - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { payload, confirmSignPsbt, cancelSignPsbt, requestToken } = useSignBatchPsbtTx(); - const [isSigning, setIsSigning] = useState(false); - const [isSigningComplete, setIsSigningComplete] = useState(false); - const [signingPsbtIndex, setSigningPsbtIndex] = useState(1); - const [currentPsbtIndex, setCurrentPsbtIndex] = useState(0); - const [reviewTransaction, setReviewTransaction] = useState(false); const { search } = useLocation(); const params = new URLSearchParams(search); const tabId = params.get('tabId') ?? '0'; - const [isLoading, setIsLoading] = useState(true); - const txnContext = useTransactionContext(); - const [inscriptionToShow, setInscriptionToShow] = useState< - btcTransaction.IOInscription | undefined - >(undefined); - const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); - useTrackMixPanelPageViewed(); - const [parsedPsbts, setParsedPsbts] = useState([]); - - const individualParsedTxSummaryContext = useMemo( - () => ({ - summary: parsedPsbts[currentPsbtIndex]?.summary, - runeSummary: parsedPsbts[currentPsbtIndex]?.runeSummary, - }), - [parsedPsbts, currentPsbtIndex], - ); - - const aggregatedParsedTxSummaryContext: ParsedTxSummaryContextProps = useMemo( - () => ({ - summary: { - inputs: parsedPsbts.map((psbt) => psbt.summary.inputs).flat(), - outputs: parsedPsbts.map((psbt) => psbt.summary.outputs).flat(), - feeOutput: undefined, - isFinal: parsedPsbts.reduce((acc, psbt) => acc && psbt.summary.isFinal, true), - hasSigHashNone: parsedPsbts.reduce( - (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashNone, - false, - ), - hasSigHashSingle: parsedPsbts.reduce( - (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashSingle, - false, - ), - } as PsbtSummary, - runeSummary: { - burns: parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(), - transfers: parsedPsbts.map((psbt) => psbt.runeSummary?.transfers ?? []).flat(), - receipts: parsedPsbts.map((psbt) => psbt.runeSummary?.receipts ?? []).flat(), - mint: undefined, - inputsHadRunes: false, - } as RuneSummary, - }), - [parsedPsbts], - ); + const requestToken = params.get('signBatchPsbtRequest') ?? ''; + const { payload } = decodeToken(requestToken) as any as SignMultipleTransactionOptions; + + const onSigned = (signedPsbts: string[]) => { + const signingMessage = { + source: MESSAGE_SOURCE, + method: SatsConnectMethods.signBatchPsbtResponse, + payload: { + signBatchPsbtRequest: requestToken, + signBatchPsbtResponse: signedPsbts.map((psbtBase64) => ({ psbtBase64 })), + }, + }; + + chrome.tabs.sendMessage(+tabId, signingMessage); + }; - const handlePsbtParsing = useCallback( - async (psbt: SignMultiplePsbtPayload, index: number): Promise => { - try { - const parsedPsbt = new btcTransaction.EnhancedPsbt(txnContext, psbt.psbtBase64); - const summary = await parsedPsbt.getSummary(); - const runeSummary = hasRunesSupport - ? await parseSummaryForRunes(txnContext, summary, network.type, { - separateTransfersOnNoExternalInputs: true, - }) - : undefined; - return { summary, runeSummary }; - } catch (err) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_INDEX_CANT_PARSE_ERROR_DESCRIPTION', { index }), - browserTx: true, - }, - }); - return undefined; - } - }, - [txnContext], - ); + const onCancel = () => { + const signingMessage = { + source: MESSAGE_SOURCE, + method: SatsConnectMethods.signBatchPsbtResponse, + payload: { signBatchPsbtRequest: requestToken, signBatchPsbtResponse: 'cancel' }, + }; - useEffect(() => { - (async () => { - const parsedPsbtsRes = await Promise.all(payload.psbts.map(handlePsbtParsing)); - if (parsedPsbtsRes.some((item) => item === undefined)) { - setIsLoading(false); - return; - } - const validParsedPsbts = parsedPsbtsRes.filter( - (item): item is ParsedPsbt => item !== undefined, - ); - setParsedPsbts(validParsedPsbts); - setIsLoading(false); - })(); - }, [payload.psbts.length, handlePsbtParsing]); + chrome.tabs.sendMessage(+tabId, signingMessage); + window.close(); + }; - const checkAddressMismatch = (input) => { - if ( - input.address !== selectedAccount.btcAddress && - input.address !== selectedAccount.ordinalsAddress - ) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('ADDRESS_MISMATCH'), - browserTx: true, - }, - }); - } + const onDone = () => { + window.close(); }; - const checkIfMismatch = () => { + useEffect(() => { if (payload.network.type !== network.type) { navigate('/tx-status', { state: { @@ -188,231 +57,54 @@ function SignBatchPsbtRequest() { browserTx: true, }, }); + return; } - payload.psbts.forEach((psbt) => psbt.inputsToSign.forEach(checkAddressMismatch)); - }; - - useEffect(() => { - checkIfMismatch(); - }, []); - - const onSignPsbtConfirmed = async () => { - try { - if (isLedgerAccount(selectedAccount)) { - // setIsModalVisible(true); - return; - } - setIsSigning(true); - - const signedPsbts: TxResponse[] = []; - // eslint-disable-next-line no-restricted-syntax - for (const psbt of payload.psbts) { - // eslint-disable-next-line no-await-in-loop - await delay(100); - // eslint-disable-next-line no-await-in-loop - const signedPsbt = await confirmSignPsbt(psbt); - signedPsbts.push({ - txId: signedPsbt.txId, - psbtBase64: signedPsbt.signingResponse, - }); - if (payload.psbts.findIndex((item) => item === psbt) !== payload.psbts.length - 1) { - setSigningPsbtIndex((prevIndex) => prevIndex + 1); + const checkAddressMismatch = (input) => { + if ( + input.address !== selectedAccount.btcAddress && + input.address !== selectedAccount.ordinalsAddress + ) { + let errorTitle = ''; + let error = ''; + if ( + selectedAccount.btcAddresses.native?.address === input.address || + selectedAccount.btcAddresses.nested?.address === input.address + ) { + errorTitle = t('ADDRESS_TYPE_MISMATCH_TITLE'); + error = t('ADDRESS_TYPE_MISMATCH'); + } else { + errorTitle = t('ADDRESS_MISMATCH_TITLE'); + error = t('ADDRESS_MISMATCH'); } - } - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: 'bitcoin', - action: 'sign-psbt', - wallet_type: selectedAccount.accountType || 'software', - batch: payload.psbts.length, - }); - setIsSigningComplete(true); - setIsSigning(false); - - const signingMessage = { - source: MESSAGE_SOURCE, - method: SatsConnectMethods.signBatchPsbtResponse, - payload: { - signBatchPsbtRequest: requestToken, - signBatchPsbtResponse: signedPsbts, - }, - }; - chrome.tabs.sendMessage(+tabId, signingMessage); - } catch (err) { - setIsSigning(false); - setIsSigningComplete(false); - - if (err instanceof Error) { navigate('/tx-status', { state: { txid: '', currency: 'BTC', - errorTitle: t('PSBT_CANT_SIGN_ERROR_TITLE'), - error: err.message, + errorTitle, + error, browserTx: true, + textAlignment: 'left', }, }); - } - } - }; - - const onCancelClick = async () => { - cancelSignPsbt(); - window.close(); - }; - const closeCallback = () => { - window.close(); - }; - - const hasOutputScript = useMemo( - () => parsedPsbts.some((psbt) => psbt.summary.outputs.some((output) => isScriptOutput(output))), - [parsedPsbts.length], - ); - - const signingStatus: ConfirmationStatus = isSigningComplete ? 'SUCCESS' : 'LOADING'; - const runeBurns = parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(); - const runeDelegations = parsedPsbts - .filter((psbt) => !psbt.summary.isFinal) - .map((psbt) => psbt.runeSummary?.receipts ?? []) - .flat(); - const hasSomeRuneDelegation = runeDelegations.length > 0; + return true; + } + return false; + }; - if (isSigning || isSigningComplete) { - return ( - - ); - } + payload.psbts?.some((psbt) => psbt.inputsToSign?.some(checkAddressMismatch)); + // eslint-disable-next-line react-hooks/exhaustive-deps -- Only run this once on load + }, []); return ( - <> - - {isLoading ? ( - - - - ) : ( - <> - - {isLedgerAccount(selectedAccount) ? ( - - - - ) : ( - - - - {t('SIGN_TRANSACTIONS', { count: parsedPsbts.length })} - - setReviewTransaction(true)}> - {t('REVIEW_ALL')} - - - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={{ - content_type: inscriptionToShow.contentType, - id: inscriptionToShow.id, - inscription_number: inscriptionToShow.number, - }} - /> - )} - {hasSomeRuneDelegation && } - - - {!hasSomeRuneDelegation && } - psbt.runeSummary?.mint)} /> - - {hasOutputScript && - !parsedPsbts.some((psbt) => psbt.runeSummary !== undefined) && ( - - )} - - - )} - - - - - { - setShowModal(false); - setShowFeeSettings(false); - }} - onApplyClick={onApplyClick} - showFeeSettings={showFeeSettings} - setShowFeeSettings={setShowFeeSettings} - /> - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx b/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx deleted file mode 100644 index a424bbf01..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/feesBlock/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import useWalletSelector from '@hooks/useWalletSelector'; -import { Container, TitleText } from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { EstimateUSDText } from '@screens/swap/swapTokenBlock'; -import { currencySymbolMap } from '@secretkeylabs/xverse-core'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const RowContainer = styled.div({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', -}); - -const FeeText = styled.p((props) => ({ - ...props.theme.body_m, - fontSize: 14, - fontWeight: 500, -})); - -interface FeeTextProps { - txFee: number; - txFeeFiatAmount?: number; -} - -export default function FeesBlock({ txFee, txFeeFiatAmount }: FeeTextProps) { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - const { fiatCurrency } = useWalletSelector(); - return ( - - - {t('FEES')} - {`${txFee.toFixed(6)} STX`} - - {` ~ ${currencySymbolMap[fiatCurrency]} ${txFeeFiatAmount} ${fiatCurrency}`} - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx b/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx deleted file mode 100644 index 48f9e441a..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/functionBlock/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { - Container, - TitleContainer, - TitleText, -} from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const FunctionName = styled.div((props) => ({ - ...props.theme.body_medium_m, - marginLeft: 10, - color: props.theme.colors.white_0, - textAlign: 'right', -})); - -interface FunctionBlockProps { - name: string; -} - -export default function FunctionBlock({ name }: FunctionBlockProps) { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - return ( - - - {t('FUNCTION')} - {name} - - - ); -} diff --git a/src/app/screens/swap/swapStacksConfirmation/index.tsx b/src/app/screens/swap/swapStacksConfirmation/index.tsx deleted file mode 100644 index bf3a003f1..000000000 --- a/src/app/screens/swap/swapStacksConfirmation/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import SponsoredTransactionIcon from '@assets/img/transactions/CircleWavyCheck.svg'; -import InfoContainer from '@components/infoContainer'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; -import { Container } from '@screens/home/index.styled'; -import { AdvanceSettings } from '@screens/swap/swapStacksConfirmation/advanceSettings'; -import FeesBlock from '@screens/swap/swapStacksConfirmation/feesBlock'; -import FunctionBlock from '@screens/swap/swapStacksConfirmation/functionBlock'; -import RouteBlock from '@screens/swap/swapStacksConfirmation/routeBlock'; -import StxInfoBlock from '@screens/swap/swapStacksConfirmation/stxInfoBlock'; -import { useConfirmSwap } from '@screens/swap/swapStacksConfirmation/useConfirmSwap'; -import Button from '@ui-library/button'; -import { SUPPORT_URL_TAB_TARGET, SWAP_SPONSOR_DISABLED_SUPPORT_URL } from '@utils/constants'; -import { useCallback, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -const TitleText = styled.div((props) => ({ - fontSize: 21, - fontWeight: 700, - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), - marginTop: props.theme.spacing(12), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - columnGap: props.theme.space.s, - marginTop: props.theme.space.m, - position: 'sticky', - bottom: 0, - background: props.theme.colors.elevation0, - padding: `${props.theme.space.s} 0`, -})); - -const SponsoredTransactionText = styled.div((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(10), - display: 'flex', - gap: props.theme.spacing(4), -})); - -const Icon = styled.img((props) => ({ - marginTop: props.theme.spacing(1), - width: 24, - height: 24, -})); - -const StyledInfoContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(4), -})); - -export default function SwapStacksConfirmation() { - const { t } = useTranslation('translation', { keyPrefix: 'SWAP_CONFIRM_SCREEN' }); - const location = useLocation(); - const navigate = useNavigate(); - const swap = useConfirmSwap(location.state); - - const handleGoBack = () => { - navigate(-1); - }; - - const [confirming, setConfirming] = useState(false); - const onConfirm = useCallback(() => { - setConfirming(true); - swap.onConfirm().finally(() => { - setConfirming(false); - }); - }, [swap]); - - const handleClickLearnMore = () => { - window.open(SWAP_SPONSOR_DISABLED_SUPPORT_URL, SUPPORT_URL_TAB_TARGET, 'noreferrer noopener'); - }; - - return ( - <> - navigate(-1)} /> - - {t('TOKEN_SWAP')} - {swap.isSponsorDisabled && ( - - - - )} - - - - - {!swap.isSponsored && ( - - )} - {swap.isSponsored ? ( - - - {t('THIS_IS_A_SPONSORED_TRANSACTION')} - - ) : ( - - )} - - + ); +} + +export default CrossButton; diff --git a/src/app/ui-library/dialog.tsx b/src/app/ui-library/dialog.tsx index e91cee627..7aa35a866 100644 --- a/src/app/ui-library/dialog.tsx +++ b/src/app/ui-library/dialog.tsx @@ -1,6 +1,6 @@ -import styled, { useTheme } from 'styled-components'; import ActionButton from '@components/button'; import { XCircle } from '@phosphor-icons/react'; +import styled, { useTheme } from 'styled-components'; type DialogType = 'default' | 'feedback'; @@ -88,7 +88,9 @@ interface Props { title: string; description: string; leftButtonText?: string; + leftButtonDisabled?: boolean; rightButtonText?: string; + rightButtonDisabled?: boolean; type?: DialogType; icon?: JSX.Element; onLeftButtonClick?: () => void; @@ -102,7 +104,9 @@ function Dialog({ title, description, leftButtonText, + leftButtonDisabled, rightButtonText, + rightButtonDisabled, type = 'default', icon, onLeftButtonClick, @@ -131,14 +135,27 @@ function Dialog({ {onRightButtonClick && onLeftButtonClick && ( - + - + )} {!onRightButtonClick && onLeftButtonClick && ( - + )} diff --git a/src/app/ui-library/input.tsx b/src/app/ui-library/input.tsx index 77edf4757..0b893ead2 100644 --- a/src/app/ui-library/input.tsx +++ b/src/app/ui-library/input.tsx @@ -91,7 +91,7 @@ const InputField = styled.input<{ $bgColor?: string; $hasLeftAccessory?: boolean const ComplicationsContainer = styled.div` position: absolute; - right: ${(props) => props.theme.spacing(8)}px; + right: ${(props) => props.theme.space.m}; top: 0; height: 100%; @@ -101,10 +101,17 @@ const ComplicationsContainer = styled.div` align-items: center; > *:first-child { - margin-left: ${(props) => props.theme.spacing(4)}px; + margin-left: ${(props) => props.theme.space.xs}; } `; +const ClearButtonContainer = styled.button` + cursor: pointer; + background-color: transparent; + border: none; + border-radius: 50%; +`; + const ClearButton = styled.div<{ $hasSiblings?: boolean }>` cursor: pointer; @@ -118,8 +125,8 @@ const ClearButton = styled.div<{ $hasSiblings?: boolean }>` height: 16px; background-color: ${(props) => props.theme.colors.white_200}; color: ${(props) => props.theme.colors.elevation_n1}; - border-radius: 50%; - margin-right: ${(props) => (props.$hasSiblings ? props.theme.spacing(4) : 0)}px; + border-radius: inherit; + margin-right: ${(props) => (props.$hasSiblings ? props.theme.space.xs : 0)}px; transition: opacity 0.1s ease; :before, @@ -165,22 +172,23 @@ const LeftAccessoryContainer = styled.div` const SubText = styled.div` color: ${(props) => props.theme.colors.white_400}; - margin-top: ${(props) => props.theme.spacing(4)}px; + margin-top: ${(props) => props.theme.space.xs}; ${(props) => props.theme.typography.body_s} `; const Feedback = styled.div` - margin-top: 8px; + margin-top: ${(props) => props.theme.space.xs}; `; type Props = { + id?: string; title?: string; placeholder?: string; value: string; dataTestID?: string; onChange: (event: ChangeEvent) => void; onBlur?: (event: ChangeEvent) => void; - type?: 'text' | 'number'; + type?: 'text' | 'number' | 'password'; hideClear?: boolean; infoPanel?: React.ReactNode; complications?: React.ReactNode; @@ -200,6 +208,7 @@ type Props = { }; function Input({ + id, title, placeholder, value, @@ -266,6 +275,7 @@ function Input({ {leftAccessory && {leftAccessory.icon}} {!hideClear && value && ( - + + + )} {complications} diff --git a/src/app/ui-library/sheet.tsx b/src/app/ui-library/sheet.tsx index 18a5309e1..6ba1efa7c 100644 --- a/src/app/ui-library/sheet.tsx +++ b/src/app/ui-library/sheet.tsx @@ -1,51 +1,48 @@ -import { XCircle } from '@phosphor-icons/react'; import { isInOptions } from '@utils/helper'; import Modal from 'react-modal'; import styled, { useTheme } from 'styled-components'; - -export const CrossButton = styled.button` - background-color: transparent; - cursor: pointer; - display: flex; - transition: opacity 0.1s ease; - - &:hover { - opacity: 0.8; - } - - &:active { - opacity: 0.6; - } -`; +import CrossButton from './crossButton'; const Title = styled.h1((props) => ({ ...props.theme.typography.body_bold_l, flex: 1, + width: '100%', })); -const RowContainer = styled.div((props) => ({ +const HeaderContainer = styled.div((props) => ({ display: 'flex', - flexDirection: 'row', + flexDirection: 'column', alignItems: 'center', justifyContent: 'space-between', margin: props.theme.space.m, + gap: props.theme.space.m, + minHeight: props.theme.space.l, })); const CustomisedModal = styled(Modal)` - overflow-y: auto; position: absolute; - &::-webkit-scrollbar { - display: none; + max-height: 100%; + display: flex; + flex-direction: column; + + &:focus-visible::after { + border: none; + outline: none; } `; const BodyContainer = styled.div` + position: relative; + flex: 1; + overflow-y: auto; margin: ${(props) => props.theme.space.m}; + margin-top: 0; `; type Props = { - title: string; + title?: string; visible: boolean; + logo?: React.ReactNode; children: React.ReactNode; onClose?: () => void; overlayStylesOverriding?: {}; @@ -56,6 +53,7 @@ type Props = { function Sheet({ title, + logo, children, visible, onClose, @@ -97,21 +95,18 @@ function Sheet({ return ( document.getElementById('app') as HTMLElement} + appElement={document.getElementById('app') as HTMLElement} ariaHideApp={false} style={customStyles} className={className} shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} onRequestClose={onClose} > - + {onClose && } + + {logo} {title} - {onClose && ( - - - - )} - + {children} ); diff --git a/src/app/ui-library/snackBar.tsx b/src/app/ui-library/snackBar.tsx index 18517ecd4..1a200abfa 100644 --- a/src/app/ui-library/snackBar.tsx +++ b/src/app/ui-library/snackBar.tsx @@ -3,13 +3,6 @@ import styled from 'styled-components'; type ToastType = 'success' | 'error' | 'neutral'; -interface ToastProps { - text: string; - type: ToastType; - dismissToast?: () => void; - action?: { text: string; onClick: () => void }; -} - const getBackgroundColor = (type: ToastType, theme: any): string => { const colors = { success: theme.colors.feedback.success, @@ -35,7 +28,7 @@ const ToastContainer = styled.div<{ type: ToastType }>` border-radius: 12px; box-shadow: 0px 7px 16px -4px rgba(25, 25, 48, 0.25); min-height: 44px; - padding: 12px 20px; + padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; width: auto; max-width: 328px; align-items: center; @@ -43,21 +36,33 @@ const ToastContainer = styled.div<{ type: ToastType }>` margin-bottom: 80px; `; -const ToastMessage = styled.h1<{ type: ToastType }>` +const ToastMessage = styled.p<{ type: ToastType; addMarginRight: boolean }>` ${({ theme }) => theme.typography.body_medium_m}; color: ${(props) => getTextColor(props.type, props.theme)}; - margin-left: ${(props) => props.theme.space.s}; - margin-right: ${(props) => props.theme.space.l}; `; -const ToastDismissButton = styled.h1<{ type: ToastType }>` +const ToastDismissButton = styled.div<{ type: ToastType }>` + display: flex; ${({ theme }) => theme.typography.body_medium_m}; color: ${(props) => getTextColor(props.type, props.theme)}; background: transparent; cursor: pointer; + margin-right: ${(props) => props.theme.space.s}; +`; + +const ToastActionButton = styled(ToastDismissButton)` + margin-left: ${(props) => props.theme.space.l}; + margin-right: 0; `; -function SnackBar({ text, type, dismissToast, action }: ToastProps) { +type Props = { + text: string; + type: ToastType; + dismissToast?: () => void; + action?: { text: string; onClick: () => void }; +}; + +function SnackBar({ text, type, dismissToast, action }: Props) { return ( {type !== 'neutral' && dismissToast && ( @@ -67,12 +72,14 @@ function SnackBar({ text, type, dismissToast, action }: ToastProps) { )} - {text} + + {text} + {action?.text && ( - + {action?.text} - + )} ); diff --git a/src/app/components/tabs/index.tsx b/src/app/ui-library/tabs.tsx similarity index 74% rename from src/app/components/tabs/index.tsx rename to src/app/ui-library/tabs.tsx index dac97d295..41a148b82 100644 --- a/src/app/components/tabs/index.tsx +++ b/src/app/ui-library/tabs.tsx @@ -5,12 +5,13 @@ const TabContainer = styled.div` gap: ${(props) => props.theme.space.xxs}; `; -const TabItem = styled.div<{ $active?: boolean }>` - padding: 7px 12px 8px; - border-radius: 12px; +export const TabItem = styled.button<{ $active?: boolean }>` ${(props) => props.theme.typography.body_bold_s}; + padding: ${(props) => props.theme.space.xs} ${(props) => props.theme.space.s}; + border-radius: 12px; color: ${(props) => props.theme.colors.white_200}; text-transform: uppercase; + background-color: transparent; cursor: pointer; user-select: none; transition: color 0.1s ease; @@ -29,13 +30,16 @@ const TabItem = styled.div<{ $active?: boolean }>` `} `; -interface TabProps { - tabs: { label: string; value: T }[]; +export type TabProp = { label: string; value: T }; + +type TabsProps = { + tabs: TabProp[]; activeTab: T; onTabClick: (value: T) => void; className?: string; -} -function Tabs({ tabs, activeTab, onTabClick, className }: TabProps) { +}; + +export function Tabs({ tabs, activeTab, onTabClick, className }: TabsProps) { return ( {tabs.map((tab) => ( @@ -50,5 +54,3 @@ function Tabs({ tabs, activeTab, onTabClick, className }: Tab ); } - -export default Tabs; diff --git a/src/app/ui-library/toggle.tsx b/src/app/ui-library/toggle.tsx new file mode 100644 index 000000000..0907dbe79 --- /dev/null +++ b/src/app/ui-library/toggle.tsx @@ -0,0 +1,104 @@ +import styled from 'styled-components'; + +const Container = styled.div` + display: inline-block; + position: relative; +`; + +const Input = styled.input` + opacity: 0; + width: 0; + height: 0; +`; + +const Slider = styled.span` + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: ${(props) => props.theme.colors.elevation3}; + transition: 0.4s; + border-radius: inherit; + + &::before { + position: absolute; + content: ''; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: ${(props) => props.theme.colors.white_600}; + border-radius: 50%; + filter: drop-shadow(0px 1px 6px rgba(0, 0, 0, 0.15)); + transition: 0.4s; + } + + &::after { + position: absolute; + content: ''; + height: 8px; + width: 8px; + left: 8px; + bottom: 8px; + border-radius: 50%; + background-color: inherit; + transition: transform 0.4s, background-color 0.1s; + } +`; + +const Label = styled.label` + display: block; + width: 40px; + height: 24px; + position: relative; + background-color: transparent; + border-radius: 24px; + + ${Input}:checked + ${Slider} { + background-color: ${(props) => props.theme.colors.tangerine}; + } + + ${Input}:checked + ${Slider}::before, + ${Input}:checked + ${Slider}::after { + transform: translateX(16px); + background-color: ${(props) => props.theme.colors.white_0}; + } +`; + +type Props = { + className?: string; + checked: boolean; + onChange: (newValue: boolean) => void; + disabled?: boolean; +}; + +function Toggle({ className, checked, onChange, disabled }: Props) { + const handleToggle = () => { + onChange(!checked); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!disabled && event.key === ' ') { + event.preventDefault(); + onChange(!checked); + } + }; + + return ( + + + + ); +} + +export default Toggle; diff --git a/src/app/utils/alertTracker.ts b/src/app/utils/alertTracker.ts new file mode 100644 index 000000000..d8829b2f3 --- /dev/null +++ b/src/app/utils/alertTracker.ts @@ -0,0 +1,47 @@ +export type AnnouncementKey = 'native_segwit_intro'; +type CalloutKey = + | 'co:panel:address_changed_to_native' + | 'co:receive:address_changed_to_native' + | 'co:receive:address_change_button'; + +const alertTrackerStorageKey = 'alertTracker:alertsToShow'; + +const getAlertData = () => { + try { + const alertsToShow = localStorage.getItem(alertTrackerStorageKey); + const alertsToShowParsed = alertsToShow && JSON.parse(alertsToShow); + return Array.isArray(alertsToShowParsed) ? alertsToShowParsed : []; + } catch (e) { + return []; + } +}; + +export const shouldShowAlert = (callout: AnnouncementKey | CalloutKey): boolean => { + const alertsToShow = getAlertData(); + return alertsToShow.includes(callout); +}; + +export const markAlertSeen = (alertType: AnnouncementKey | CalloutKey): void => { + const alertsToShow = getAlertData(); + + if (alertsToShow.includes(alertType)) { + const newAlertsToShow = alertsToShow.filter((alert) => alert !== alertType); + localStorage.setItem(alertTrackerStorageKey, JSON.stringify(newAlertsToShow)); + } +}; + +export const markAlertsForShow = (...alertTypes: (AnnouncementKey | CalloutKey)[]): void => { + const alertsToShow = getAlertData(); + + let changed = false; + alertTypes.forEach((alertType) => { + if (!alertsToShow.includes(alertType)) { + alertsToShow.push(alertType); + changed = true; + } + }); + + if (changed) { + localStorage.setItem(alertTrackerStorageKey, JSON.stringify(alertsToShow)); + } +}; diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index 9d683a9a4..57743f118 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -53,10 +53,6 @@ export const PAGINATION_LIMIT = 50; export const SEND_MANY_TOKEN_TRANSFER_CONTRACT_PRINCIPAL = 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-many-memo'; -export const SWAP_SPONSOR_DISABLED_SUPPORT_URL = - 'https://support.xverse.app/hc/en-us/articles/18319388355981'; -export const SUPPORT_URL_TAB_TARGET = 'SupportURLTabTarget'; - export const DEFAULT_TRANSITION_OPTIONS = { from: { x: 24, @@ -69,11 +65,12 @@ export const DEFAULT_TRANSITION_OPTIONS = { }; export const MAX_ACC_NAME_LENGTH = 20; - // UI export const EMPTY_LABEL = '--'; +export const HIDDEN_BALANCE_LABEL = '●●●●●●'; export const OPTIONS_DIALOG_WIDTH = 179; export const SPAM_OPTIONS_WIDTH = 244; +export const LONG_TOAST_DURATION = 4000; export const XverseProviderInfo: Provider = { id: 'XverseProviders.BitcoinProvider', diff --git a/src/app/utils/currency.ts b/src/app/utils/currency.ts index 5d8e80e5b..98633356f 100644 --- a/src/app/utils/currency.ts +++ b/src/app/utils/currency.ts @@ -1,35 +1,66 @@ import ARG from '@assets/img/settings/currencies/ars.svg'; -import AUD from '@assets/img/settings/currencies/aud.svg'; -import BRL from '@assets/img/settings/currencies/brl.svg'; -import CAD from '@assets/img/settings/currencies/cad.svg'; -import CNY from '@assets/img/settings/currencies/cny.svg'; -import EUR from '@assets/img/settings/currencies/eur.svg'; -import GBP from '@assets/img/settings/currencies/gbp.svg'; -import HKD from '@assets/img/settings/currencies/hkd.svg'; -import JPY from '@assets/img/settings/currencies/jpy.svg'; -import KRW from '@assets/img/settings/currencies/krw.svg'; -import RUB from '@assets/img/settings/currencies/rub.svg'; -import SGD from '@assets/img/settings/currencies/sgd.svg'; -import USD from '@assets/img/settings/currencies/usd.svg'; +import AUS from '@assets/img/settings/currencies/aus.svg'; +import BRA from '@assets/img/settings/currencies/bra.svg'; +import CAN from '@assets/img/settings/currencies/can.svg'; +import CHE from '@assets/img/settings/currencies/che.svg'; +import CHN from '@assets/img/settings/currencies/chn.svg'; +import EU from '@assets/img/settings/currencies/eu.svg'; +import GBR from '@assets/img/settings/currencies/gbr.svg'; +import HKG from '@assets/img/settings/currencies/hkg.svg'; +import HUN from '@assets/img/settings/currencies/hun.svg'; +import IDN from '@assets/img/settings/currencies/idn.svg'; +import IND from '@assets/img/settings/currencies/ind.svg'; +import JPN from '@assets/img/settings/currencies/jpn.svg'; +import KOR from '@assets/img/settings/currencies/kor.svg'; +import MEX from '@assets/img/settings/currencies/mex.svg'; +import MYS from '@assets/img/settings/currencies/mys.svg'; +import NGA from '@assets/img/settings/currencies/nga.svg'; +import PAK from '@assets/img/settings/currencies/pak.svg'; +import PHL from '@assets/img/settings/currencies/phl.svg'; +import POL from '@assets/img/settings/currencies/pol.svg'; +import RUS from '@assets/img/settings/currencies/rus.svg'; +import SGP from '@assets/img/settings/currencies/sgp.svg'; +import THA from '@assets/img/settings/currencies/tha.svg'; +import TUR from '@assets/img/settings/currencies/tur.svg'; +import TWN from '@assets/img/settings/currencies/twn.svg'; +import USA from '@assets/img/settings/currencies/usa.svg'; +import VNM from '@assets/img/settings/currencies/vnm.svg'; +import ZAF from '@assets/img/settings/currencies/zaf.svg'; + import type { SupportedCurrency } from '@secretkeylabs/xverse-core'; export interface Currency { name: SupportedCurrency; - flag: string; + isoFlag: string; } export const currencyList: Currency[] = [ - { name: 'CAD', flag: CAD }, - { name: 'CNY', flag: CNY }, - { name: 'EUR', flag: EUR }, - { name: 'USD', flag: USD }, - { name: 'ARS', flag: ARG }, - { name: 'KRW', flag: KRW }, - { name: 'HKD', flag: HKD }, - { name: 'JPY', flag: JPY }, - { name: 'SGD', flag: SGD }, - { name: 'GBP', flag: GBP }, - { name: 'BRL', flag: BRL }, - { name: 'RUB', flag: RUB }, - { name: 'AUD', flag: AUD }, + { name: 'CAD', isoFlag: CAN }, // Canada + { name: 'CNY', isoFlag: CHN }, // China + { name: 'EUR', isoFlag: EU }, // Eurozone + { name: 'USD', isoFlag: USA }, // United States + { name: 'ARS', isoFlag: ARG }, // Argentina + { name: 'KRW', isoFlag: KOR }, // South Korea + { name: 'HKD', isoFlag: HKG }, // Hong Kong + { name: 'JPY', isoFlag: JPN }, // Japan + { name: 'SGD', isoFlag: SGP }, // Singapore + { name: 'GBP', isoFlag: GBR }, // United Kingdom + { name: 'BRL', isoFlag: BRA }, // Brazil + { name: 'RUB', isoFlag: RUS }, // Russia + { name: 'AUD', isoFlag: AUS }, // Australia + { name: 'NGN', isoFlag: NGA }, // Nigeria + { name: 'TRY', isoFlag: TUR }, // Turkey + { name: 'INR', isoFlag: IND }, // India + { name: 'CHF', isoFlag: CHE }, // Switzerland + { name: 'VND', isoFlag: VNM }, // Vietnam + { name: 'PLN', isoFlag: POL }, // Poland + { name: 'MYR', isoFlag: MYS }, // Malaysia + { name: 'TWD', isoFlag: TWN }, // Taiwan + { name: 'IDR', isoFlag: IDN }, // Indonesia + { name: 'HUF', isoFlag: HUN }, // Hungary + { name: 'THB', isoFlag: THA }, // Thailand + { name: 'PHP', isoFlag: PHL }, // Philippines + { name: 'PKR', isoFlag: PAK }, // Pakistan + { name: 'ZAR', isoFlag: ZAF }, // South Africa + { name: 'MXN', isoFlag: MEX }, // Mexico ]; diff --git a/src/app/utils/date.ts b/src/app/utils/date.ts index 87da8b5f5..c0062f288 100644 --- a/src/app/utils/date.ts +++ b/src/app/utils/date.ts @@ -1,4 +1,4 @@ -import { format, isYesterday, isToday } from 'date-fns'; +import { format, isToday, isYesterday } from 'date-fns'; // eslint-disable-next-line import/prefer-default-export export const formatDate = (date: Date) => { @@ -10,3 +10,5 @@ export const formatDate = (date: Date) => { } return format(date, 'MMMM dd, yyyy'); }; + +export const formatDateKey = (date: Date) => format(date, 'yyyy-MM-dd'); diff --git a/src/app/utils/gradient.ts b/src/app/utils/gradient.ts index 802f2269e..9b0f988c5 100644 --- a/src/app/utils/gradient.ts +++ b/src/app/utils/gradient.ts @@ -1,3 +1,5 @@ +import type { Account } from '@secretkeylabs/xverse-core'; + /* eslint-disable import/prefer-default-export */ function toHex(str: string) { let result = ''; @@ -35,7 +37,8 @@ function stringToHslColor(str: string, saturation: number, lightness: number): s return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } -export function getAccountGradient(text: string): string[] { +export function getAccountGradient(account: Account | null | undefined): string[] { + const text = account?.stxAddress || account?.btcAddresses.taproot.address || ''; const pubKeyLikeString = toHex(text); const bg = stringToHslColor(text, 50, 60); diff --git a/src/app/utils/helper.ts b/src/app/utils/helper.ts index b6eee0a4d..67e28b3d2 100644 --- a/src/app/utils/helper.ts +++ b/src/app/utils/helper.ts @@ -4,7 +4,7 @@ import { microstacksToStx, satsToBtc, type Account, - type FungibleToken, + type FungibleTokenWithStates, type NetworkType, type NftData, type SettingsNetwork, @@ -82,9 +82,12 @@ export const getTruncatedAddress = (address: string, lengthToShow = 4) => address.length, )}`; -export const getShortTruncatedAddress = (address: string) => { +export const getShortTruncatedAddress = (address: string, charCount = 8) => { if (address) { - return `${address.substring(0, 8)}...${address.substring(address.length - 8, address.length)}`; + return `${address.substring(0, charCount)}...${address.substring( + address.length - charCount, + address.length, + )}`; } }; @@ -170,12 +173,16 @@ export const isValidBtcApi = async (url: string, network: NetworkType) => { }); try { - const [customHash, defaultHash] = await Promise.all([ - btcClient.getBlockHash(1), - defaultBtcClient.getBlockHash(1), - ]); - // this ensures the URL is for correct network - return customHash === defaultHash; + if (network === 'Mainnet') { + const [customHash, defaultHash] = await Promise.all([ + btcClient.getBlockHash(1), + defaultBtcClient.getBlockHash(1), + ]); + // this ensures the URL is for correct network + return customHash === defaultHash; + } + const blockHash = await btcClient.getBlockHash(1); + return !!blockHash; } catch (e) { return false; } @@ -236,6 +243,11 @@ export const validateAccountName = ( return null; }; +export const getAccountBalanceKey = (account: Account | null) => { + if (!account) return ''; + return `${account.accountType}-${account.id}`; +}; + export const calculateTotalBalance = ({ stxBalance, btcBalance, @@ -248,9 +260,9 @@ export const calculateTotalBalance = ({ }: { stxBalance?: string; btcBalance?: string; - sipCoinsList: FungibleToken[]; - brcCoinsList: FungibleToken[]; - runesCoinList: FungibleToken[]; + sipCoinsList: FungibleTokenWithStates[]; + brcCoinsList: FungibleTokenWithStates[]; + runesCoinList: FungibleTokenWithStates[]; stxBtcRate: string; btcFiatRate: string; hideStx: boolean; @@ -273,7 +285,7 @@ export const calculateTotalBalance = ({ if (sipCoinsList) { totalBalance = sipCoinsList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate && coin.decimals) { + if (coin.isEnabled && coin.tokenFiatRate && coin.decimals) { const tokenUnits = new BigNumber(10).exponentiatedBy(new BigNumber(coin.decimals)); const coinFiatValue = new BigNumber(coin.balance) .dividedBy(tokenUnits) @@ -287,7 +299,7 @@ export const calculateTotalBalance = ({ if (brcCoinsList) { totalBalance = brcCoinsList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate) { + if (coin.isEnabled && coin.tokenFiatRate) { const coinFiatValue = new BigNumber(coin.balance).multipliedBy( new BigNumber(coin.tokenFiatRate), ); @@ -300,7 +312,7 @@ export const calculateTotalBalance = ({ if (runesCoinList) { totalBalance = runesCoinList.reduce((acc, coin) => { - if (coin.visible && coin.tokenFiatRate) { + if (coin.isEnabled && coin.tokenFiatRate) { const coinFiatValue = new BigNumber(getFtBalance(coin)).multipliedBy( new BigNumber(coin.tokenFiatRate), ); @@ -325,16 +337,9 @@ export const getLockCountdownLabel = ( return t('LOCK_COUNTDOWN_HS', { count: hours }); }; -export const isFungibleToken = (token: any): token is FungibleToken => - token && - typeof token === 'object' && - 'balance' in token && - 'total_sent' in token && - 'total_received' in token && - 'principal' in token && - 'assetName' in token; - export const satsToBtcString = (num: BigNumber) => satsToBtc(num) .toFixed(8) .replace(/\.?0+$/, ''); + +export const sanitizeRuneName = (runeName) => runeName.replace(/[^A-Za-z]+/g, '').toUpperCase(); diff --git a/src/app/utils/mappers.ts b/src/app/utils/mappers.ts new file mode 100644 index 000000000..20f0fa2dc --- /dev/null +++ b/src/app/utils/mappers.ts @@ -0,0 +1,14 @@ +import type { FungibleToken, RuneBase } from '@secretkeylabs/xverse-core'; + +// eslint-disable-next-line import/prefer-default-export +export const mapRuneBaseToFungibleToken = (rune: RuneBase): FungibleToken => ({ + protocol: 'runes', + name: rune.runeName, + principal: rune.runeId, + assetName: '', + balance: '', + total_received: '', + total_sent: '', + runeSymbol: rune.symbol, + runeInscriptionId: rune.inscriptionId, +}); diff --git a/src/app/utils/tokens.ts b/src/app/utils/tokens.ts index c0c68d77f..e02ed01d9 100644 --- a/src/app/utils/tokens.ts +++ b/src/app/utils/tokens.ts @@ -9,7 +9,10 @@ import type { CurrencyTypes } from '@utils/constants'; import BigNumber from 'bignumber.js'; import { ftDecimals, getTicker } from './helper'; -export function getFtTicker(ft: FungibleToken) { +export function getFtTicker(ft: FungibleToken): string { + if (ft?.protocol === 'runes') { + return ft.runeSymbol ?? ft.ticker ?? ''; + } if (ft?.ticker) { return ft.protocol === 'brc-20' ? ft.ticker.toUpperCase() : ft.ticker; } diff --git a/src/app/utils/transactions/transactions.ts b/src/app/utils/transactions/transactions.ts index b0a0cb8c1..8a19eacea 100644 --- a/src/app/utils/transactions/transactions.ts +++ b/src/app/utils/transactions/transactions.ts @@ -1,7 +1,6 @@ import { API_TIMEOUT_MILLI, StacksNetwork, - getNetworkURL, type APIGetRunesActivityForAddressResponse, type AppInfo, type Brc20HistoryTransactionData, @@ -15,6 +14,7 @@ import { type MempoolTransactionListResponse, type Transaction, } from '@stacks/stacks-blockchain-api-types'; +import { PayloadType } from '@stacks/transactions'; import axios from 'axios'; interface PaginatedResults { @@ -31,9 +31,7 @@ async function getTransferTransactions(reqParams: { offset: number; }): Promise { const { stxAddress, limit, network, offset } = reqParams; - const apiUrl = `${getNetworkURL( - network, - )}/extended/v1/address/${stxAddress}/transactions_with_transfers`; + const apiUrl = `${network.coreApiUrl}/extended/v1/address/${stxAddress}/transactions_with_transfers`; const response = await axios.get>(apiUrl, { params: { limit, @@ -145,8 +143,13 @@ export const modifyRecommendedStxFees = ( high: number; }, appInfo: AppInfo | undefined | null, + txType: PayloadType, ): { low: number; medium: number; high: number } => { - const multiplier = appInfo?.stxSendTxMultiplier || 1; + const multiplier = appInfo + ? txType === PayloadType.ContractCall + ? appInfo.otherTxMultiplier + : appInfo.stxSendTxMultiplier + : 1; const highCap = appInfo?.thresholdHighStacksFee; let adjustedLow = Math.round(baseFees.low * multiplier); diff --git a/src/assets/img/btcFlashy.svg b/src/assets/img/btcFlashy.svg new file mode 100644 index 000000000..0a60887b6 --- /dev/null +++ b/src/assets/img/btcFlashy.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/listings/CheckCircle.svg b/src/assets/img/listings/CheckCircle.svg new file mode 100644 index 000000000..2639e9ec7 --- /dev/null +++ b/src/assets/img/listings/CheckCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/listings/XCircle.svg b/src/assets/img/listings/XCircle.svg new file mode 100644 index 000000000..a3fea84bc --- /dev/null +++ b/src/assets/img/listings/XCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/send/info_circle.svg b/src/assets/img/send/info_circle.svg new file mode 100644 index 000000000..0f9ab6136 --- /dev/null +++ b/src/assets/img/send/info_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/settings/currencies/aud.svg b/src/assets/img/settings/currencies/aus.svg similarity index 100% rename from src/assets/img/settings/currencies/aud.svg rename to src/assets/img/settings/currencies/aus.svg diff --git a/src/assets/img/settings/currencies/brl.svg b/src/assets/img/settings/currencies/bra.svg similarity index 100% rename from src/assets/img/settings/currencies/brl.svg rename to src/assets/img/settings/currencies/bra.svg diff --git a/src/assets/img/settings/currencies/cad.svg b/src/assets/img/settings/currencies/can.svg similarity index 100% rename from src/assets/img/settings/currencies/cad.svg rename to src/assets/img/settings/currencies/can.svg diff --git a/src/assets/img/settings/currencies/che.svg b/src/assets/img/settings/currencies/che.svg new file mode 100644 index 000000000..c0b86ae11 --- /dev/null +++ b/src/assets/img/settings/currencies/che.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/cny.svg b/src/assets/img/settings/currencies/chn.svg similarity index 100% rename from src/assets/img/settings/currencies/cny.svg rename to src/assets/img/settings/currencies/chn.svg diff --git a/src/assets/img/settings/currencies/eur.svg b/src/assets/img/settings/currencies/eu.svg similarity index 100% rename from src/assets/img/settings/currencies/eur.svg rename to src/assets/img/settings/currencies/eu.svg diff --git a/src/assets/img/settings/currencies/gbp.svg b/src/assets/img/settings/currencies/gbr.svg similarity index 100% rename from src/assets/img/settings/currencies/gbp.svg rename to src/assets/img/settings/currencies/gbr.svg diff --git a/src/assets/img/settings/currencies/hkd.svg b/src/assets/img/settings/currencies/hkg.svg similarity index 100% rename from src/assets/img/settings/currencies/hkd.svg rename to src/assets/img/settings/currencies/hkg.svg diff --git a/src/assets/img/settings/currencies/hun.svg b/src/assets/img/settings/currencies/hun.svg new file mode 100644 index 000000000..f51c943e2 --- /dev/null +++ b/src/assets/img/settings/currencies/hun.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/idn.svg b/src/assets/img/settings/currencies/idn.svg new file mode 100644 index 000000000..bf9df2a92 --- /dev/null +++ b/src/assets/img/settings/currencies/idn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/ind.svg b/src/assets/img/settings/currencies/ind.svg new file mode 100644 index 000000000..676e78817 --- /dev/null +++ b/src/assets/img/settings/currencies/ind.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/jpy.svg b/src/assets/img/settings/currencies/jpn.svg similarity index 100% rename from src/assets/img/settings/currencies/jpy.svg rename to src/assets/img/settings/currencies/jpn.svg diff --git a/src/assets/img/settings/currencies/krw.svg b/src/assets/img/settings/currencies/kor.svg similarity index 100% rename from src/assets/img/settings/currencies/krw.svg rename to src/assets/img/settings/currencies/kor.svg diff --git a/src/assets/img/settings/currencies/mex.svg b/src/assets/img/settings/currencies/mex.svg new file mode 100644 index 000000000..e68c31bc0 --- /dev/null +++ b/src/assets/img/settings/currencies/mex.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/mys.svg b/src/assets/img/settings/currencies/mys.svg new file mode 100644 index 000000000..b5a1d9d1d --- /dev/null +++ b/src/assets/img/settings/currencies/mys.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/nga.svg b/src/assets/img/settings/currencies/nga.svg new file mode 100644 index 000000000..0db8695d8 --- /dev/null +++ b/src/assets/img/settings/currencies/nga.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/pak.svg b/src/assets/img/settings/currencies/pak.svg new file mode 100644 index 000000000..37d707196 --- /dev/null +++ b/src/assets/img/settings/currencies/pak.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/phl.svg b/src/assets/img/settings/currencies/phl.svg new file mode 100644 index 000000000..bbca901e0 --- /dev/null +++ b/src/assets/img/settings/currencies/phl.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/pol.svg b/src/assets/img/settings/currencies/pol.svg new file mode 100644 index 000000000..2e707c067 --- /dev/null +++ b/src/assets/img/settings/currencies/pol.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/rub.svg b/src/assets/img/settings/currencies/rus.svg similarity index 100% rename from src/assets/img/settings/currencies/rub.svg rename to src/assets/img/settings/currencies/rus.svg diff --git a/src/assets/img/settings/currencies/sgd.svg b/src/assets/img/settings/currencies/sgp.svg similarity index 100% rename from src/assets/img/settings/currencies/sgd.svg rename to src/assets/img/settings/currencies/sgp.svg diff --git a/src/assets/img/settings/currencies/tha.svg b/src/assets/img/settings/currencies/tha.svg new file mode 100644 index 000000000..fbd5ba069 --- /dev/null +++ b/src/assets/img/settings/currencies/tha.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/tur.svg b/src/assets/img/settings/currencies/tur.svg new file mode 100644 index 000000000..34fac9b7c --- /dev/null +++ b/src/assets/img/settings/currencies/tur.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/twn.svg b/src/assets/img/settings/currencies/twn.svg new file mode 100644 index 000000000..2d13f22cf --- /dev/null +++ b/src/assets/img/settings/currencies/twn.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/usd.svg b/src/assets/img/settings/currencies/usa.svg similarity index 100% rename from src/assets/img/settings/currencies/usd.svg rename to src/assets/img/settings/currencies/usa.svg diff --git a/src/assets/img/settings/currencies/vnm.svg b/src/assets/img/settings/currencies/vnm.svg new file mode 100644 index 000000000..4561861df --- /dev/null +++ b/src/assets/img/settings/currencies/vnm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/img/settings/currencies/zaf.svg b/src/assets/img/settings/currencies/zaf.svg new file mode 100644 index 000000000..49ee153be --- /dev/null +++ b/src/assets/img/settings/currencies/zaf.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/background/background.ts b/src/background/background.ts index 74b9a13ca..6fd3dfa5f 100755 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -3,13 +3,48 @@ import { CONTENT_SCRIPT_PORT } from '@common/types/message-types'; import { handleLegacyExternalMethodFormat } from '@common/utils/legacy-external-message-handler'; import internalBackgroundMessageHandler from '@common/utils/messageHandlers'; import handleRPCRequest from '@common/utils/rpc'; +import { initPermissionsStore, permissionsStoreMutex } from '@components/permissionsManager/utils'; import { rpcRequestMessageSchema } from '@sats-connect/core'; import * as v from 'valibot'; +let hasInitializationFinished = false; +permissionsStoreMutex + .runExclusive(initPermissionsStore) + .catch(([error]) => { + if (error) { + // eslint-disable-next-line no-console + console.error('Failed to load permissions store:', error); + } + }) + // Even if the store initialization fails, the initialization is considered to + // have finished. This allows the app to continue running after a failure + // since many of the app's features are not dependent on the permissions + // store. + .finally(() => { + hasInitializationFinished = true; + }); + +// All event handlers that could potentially lead to the permissions store +// being accessed should wait for the store to be initialized before +// proceeding. +async function waitForStoreInitialization() { + if (hasInitializationFinished) return; + await new Promise((resolve) => { + const interval = setInterval(() => { + if (hasInitializationFinished) { + clearInterval(interval); + resolve(undefined); + } + }, 10); + }); +} + // Listen for connection to the content-script - port for two-way communication chrome.runtime.onConnect.addListener((port) => { if (port.name !== CONTENT_SCRIPT_PORT) return; - port.onMessage.addListener((message, messagingPort) => { + + async function onMessageListener(message: any, messagingPort: chrome.runtime.Port) { + await waitForStoreInitialization(); const parseResult = v.safeParse(rpcRequestMessageSchema, message); if (!parseResult.success) { @@ -19,6 +54,13 @@ chrome.runtime.onConnect.addListener((port) => { } handleRPCRequest(parseResult.output, port); + } + + port.onMessage.addListener((message, messagingPort) => { + onMessageListener(message, messagingPort).catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to handle message:', error); + }); }); }); @@ -28,10 +70,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; }); -chrome.runtime.onInstalled.addListener((details) => { +async function onInstalledListener(details: chrome.runtime.InstalledDetails) { + await waitForStoreInitialization(); if (details.reason === 'install') { chrome.tabs.create({ url: chrome.runtime.getURL('options.html#/landing') }); } +} +chrome.runtime.onInstalled.addListener((details) => { + onInstalledListener(details).catch((error) => { + // eslint-disable-next-line no-console + console.error('Failed to handle onInstalled event:', error); + }); }); if (process.env.WALLET_LABEL) { diff --git a/src/common/types/ledger.ts b/src/common/types/ledger.ts index 2ed0fe32f..5022b9df3 100644 --- a/src/common/types/ledger.ts +++ b/src/common/types/ledger.ts @@ -1,14 +1,8 @@ import type { StacksRecipient } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; -export type LedgerTransactionType = 'BTC' | 'STX' | 'ORDINALS' | 'BRC-20'; - -type ConfirmLedgerTransactionState = { - type: LedgerTransactionType; - fee: BigNumber; -}; - -export type ConfirmStxTransactionState = ConfirmLedgerTransactionState & { +export type ConfirmStxTransactionState = { recipients: StacksRecipient[]; unsignedTx: Buffer; + fee: BigNumber; }; diff --git a/src/common/utils/getSelectedAccount.ts b/src/common/utils/getSelectedAccount.ts index 6304c8853..7149ec3ae 100644 --- a/src/common/utils/getSelectedAccount.ts +++ b/src/common/utils/getSelectedAccount.ts @@ -1,4 +1,5 @@ -import type { Account, AccountType } from '@secretkeylabs/xverse-core'; +import type { Account, AccountType, BtcPaymentType } from '@secretkeylabs/xverse-core'; +import { getAccountAddressDetails } from '@secretkeylabs/xverse-core'; type GetSelectedAccountProps = { selectedAccountType: AccountType; @@ -7,7 +8,42 @@ type GetSelectedAccountProps = { softwareAccountsList: Account[]; }; -const getSelectedAccount = (props: GetSelectedAccountProps) => { +export type AccountWithDetails = Account & { + btcAddress: string; + btcPublicKey: string; + btcAddressType: BtcPaymentType; + ordinalsAddress: string; + ordinalsPublicKey: string; +}; + +export function embellishAccountWithDetails( + account: Account, + btcPaymentType: BtcPaymentType, +): AccountWithDetails; +export function embellishAccountWithDetails( + account: undefined, + btcPaymentType: BtcPaymentType, +): undefined; +export function embellishAccountWithDetails( + account: Account | undefined, + btcPaymentType: BtcPaymentType, +): AccountWithDetails | undefined { + if (!account) { + return undefined; + } + + if (account.accountType === 'ledger') { + return { ...account, ...getAccountAddressDetails(account, 'native'), btcAddressType: 'native' }; + } + + return { + ...account, + ...getAccountAddressDetails(account, btcPaymentType), + btcAddressType: btcPaymentType, + }; +} + +const getSelectedAccount = (props: GetSelectedAccountProps): Account | undefined => { const { selectedAccountType, selectedAccountIndex, ledgerAccountsList, softwareAccountsList } = props; diff --git a/src/common/utils/ledger.ts b/src/common/utils/ledger.ts index 4aa8de115..3f38f08e4 100644 --- a/src/common/utils/ledger.ts +++ b/src/common/utils/ledger.ts @@ -1,13 +1,8 @@ import type { Account, NetworkType } from '@secretkeylabs/xverse-core'; -export const delay = (ms: number) => - new Promise((res) => { - setTimeout(res, ms); - }); - -export const filterLedgerAccounts = (accounts: Account[], network: NetworkType) => +export const filterLedgerAccountsByNetwork = (accounts: Account[], network: NetworkType) => accounts.filter((account) => - account.ordinalsAddress?.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), + account.btcAddresses.taproot.address.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), ); // this is used for migrating the old ledger accounts to the new format @@ -43,7 +38,7 @@ export const getDeviceNewAccountIndex = ( network: NetworkType, masterKey?: string, ) => { - const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network); + const networkLedgerAccounts = filterLedgerAccountsByNetwork(ledgerAccountsList, network); const ledgerAccountsIndexList = networkLedgerAccounts .filter((account) => masterKey === account.masterPubKey) diff --git a/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts b/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts index 361a15942..609508629 100644 --- a/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts +++ b/src/common/utils/messages/extensionToContentScript/dispatchEvent/index.ts @@ -6,6 +6,7 @@ import { sendMessageAuthorizedConnectedClients, sendMessageConnectedClient, sendMessageConnectedClients, + sendMessageToOrigin, } from '../utils'; /** @@ -38,10 +39,17 @@ export async function dispatchEventConnectedClients(data: WalletEvent) { * @public */ export async function dispatchEventAuthorizedConnectedClients( - permission: Omit, + permissions: Omit[], data: WalletEvent, ) { - sendMessageAuthorizedConnectedClients(permission, { + sendMessageAuthorizedConnectedClients(permissions, { + type: contentScriptWalletEventMessageName, + data, + }); +} + +export async function dispatchEventToOrigin(origin: string, data: WalletEvent) { + sendMessageToOrigin(origin, { type: contentScriptWalletEventMessageName, data, }); diff --git a/src/common/utils/messages/extensionToContentScript/utils/index.ts b/src/common/utils/messages/extensionToContentScript/utils/index.ts index 13e6205c6..01ea31764 100644 --- a/src/common/utils/messages/extensionToContentScript/utils/index.ts +++ b/src/common/utils/messages/extensionToContentScript/utils/index.ts @@ -1,5 +1,5 @@ import type { Permission } from '@components/permissionsManager/schemas'; -import { loadPermissionsStore } from '@components/permissionsManager/utils'; +import { getPermissionsStore } from '@components/permissionsManager/utils'; import { type ContentScriptMessage } from '../schemas'; /** @@ -37,7 +37,7 @@ function clientOriginToUrlMatchPattern(tabOrigin: string) { * @public */ export async function sendMessageConnectedClient(id: string, message: ContentScriptMessage) { - const [error, store] = await loadPermissionsStore(); + const [error, store] = await getPermissionsStore(); if (error) { // eslint-disable-next-line no-console console.error('Failed to load permissions store:', error); @@ -76,7 +76,7 @@ export async function sendMessageConnectedClient(id: string, message: ContentScr * @public */ export async function sendMessageConnectedClients(message: ContentScriptMessage) { - const [error, store] = await loadPermissionsStore(); + const [error, store] = await getPermissionsStore(); if (error) { // eslint-disable-next-line no-console console.error('Failed to load permissions store:', error); @@ -108,10 +108,10 @@ export async function sendMessageConnectedClients(message: ContentScriptMessage) * @public */ export async function sendMessageAuthorizedConnectedClients( - permission: Omit, + permissions: Omit[], message: ContentScriptMessage, ) { - const [error, store] = await loadPermissionsStore(); + const [error, store] = await getPermissionsStore(); if (error) { // eslint-disable-next-line no-console console.error('Failed to load permissions store:', error); @@ -125,10 +125,12 @@ export async function sendMessageAuthorizedConnectedClients( } const authorizedClientIds = [...store.permissions] - .filter( - (p) => - p.resourceId === permission.resourceId && - [...permission.actions].every((action) => p.actions.has(action)), + .filter((p) => + permissions.some( + (permission) => + p.resourceId === permission.resourceId && + [...permission.actions].every((action) => p.actions.has(action)), + ), ) .map((p) => p.clientId); @@ -157,3 +159,14 @@ export async function sendMessageAuthorizedConnectedClients( message, ); } + +export async function sendMessageToOrigin(origin: string, message: ContentScriptMessage) { + const url = clientOriginToUrlMatchPattern(origin); + const clientTabs = (await chrome.tabs.query({ url })).filter( + (tab): tab is chrome.tabs.Tab & { readonly id: number } => typeof tab.id === 'number', + ); + sendMessageContentScriptTabs( + clientTabs.map((tab) => tab.id), + message, + ); +} diff --git a/src/common/utils/promises.ts b/src/common/utils/promises.ts new file mode 100644 index 000000000..fc888ab97 --- /dev/null +++ b/src/common/utils/promises.ts @@ -0,0 +1,2 @@ +// a function that sleeps for a given amount of time (ms) +export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/common/utils/route-urls.ts b/src/common/utils/route-urls.ts index a67a7775b..c7f96ee5c 100644 --- a/src/common/utils/route-urls.ts +++ b/src/common/utils/route-urls.ts @@ -4,12 +4,13 @@ enum RequestsRoutes { AuthenticationRequest = '/authentication-request', SignatureRequest = '/signature-request', SignMessageRequest = '/sign-message-request', - SignMessageRequestInApp = '/sign-message-request-in-app', + SignRuneDelistingMessage = '/sign-rune-delisting-message', AddressRequest = '/btc-select-address-request', StxAddressRequest = '/stx-select-address-request', StxAccountRequest = '/stx-select-account-request', SignBtcTx = '/psbt-signing-request', SignBatchBtcTx = '/batch-psbt-signing-request', + RuneListingBatchSigning = '/rune-listing-batch-signing', SendBtcTx = '/btc-send-request', CreateInscription = '/create-inscription', CreateRepeatInscriptions = '/create-repeat-inscriptions', diff --git a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts index 7ccb8e375..3c67c7643 100644 --- a/src/common/utils/rpc/btc/getAddresses/getAddresses.ts +++ b/src/common/utils/rpc/btc/getAddresses/getAddresses.ts @@ -1,60 +1,77 @@ /* eslint-disable import/prefer-default-export */ import { getTabIdFromPort } from '@common/utils'; -import getSelectedAccount from '@common/utils/getSelectedAccount'; +import getSelectedAccount, { embellishAccountWithDetails } from '@common/utils/getSelectedAccount'; import { makeContext, openPopup } from '@common/utils/popup'; +import * as utils from '@components/permissionsManager/utils'; import { getAddressesRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; import rootStore from '@stores/index'; import * as v from 'valibot'; import RequestsRoutes from '../../../route-urls'; import { handleInvalidMessage } from '../../handle-invalid-message'; -import { hasPermissions, makeSendPopupClosedUserRejectionMessage } from '../../helpers'; +import { hasAccountReadPermissions, makeSendPopupClosedUserRejectionMessage } from '../../helpers'; import { sendGetAddressesSuccessResponseMessage } from '../../responseMessages/bitcoin'; +import { sendInternalErrorMessage } from '../../responseMessages/errors'; import { accountPurposeAddresses } from './utils'; export const handleGetAddresses = async (message: RpcRequestMessage, port: chrome.runtime.Port) => { const parseResult = v.safeParse(getAddressesRequestMessageSchema, message); - if (!parseResult.success) { handleInvalidMessage(message, getTabIdFromPort(port), parseResult.issues); return; } const { origin, tabId } = makeContext(port); - if (await hasPermissions(origin)) { - const { - selectedAccountIndex, - selectedAccountType, - accountsList: softwareAccountsList, - ledgerAccountsList, - } = rootStore.store.getState().walletState; - - const account = getSelectedAccount({ - selectedAccountIndex, - selectedAccountType, - softwareAccountsList, - ledgerAccountsList, + + const [error, store] = await utils.getPermissionsStore(); + if (error) { + sendInternalErrorMessage({ + tabId, + messageId: parseResult.output.id, + message: 'Error loading permissions store.', + }); + return; + } + + if (!hasAccountReadPermissions(origin, store)) { + await openPopup({ + path: RequestsRoutes.AddressRequest, + data: parseResult.output, + context: makeContext(port), + onClose: makeSendPopupClosedUserRejectionMessage({ + tabId: getTabIdFromPort(port), + messageId: parseResult.output.id, + }), }); + return; + } - if (account) { - const addresses = accountPurposeAddresses(account, parseResult.output.params.purposes); - sendGetAddressesSuccessResponseMessage({ - tabId, - messageId: message.id, - result: { - addresses, - }, - }); - return; - } + const { + selectedAccountIndex, + selectedAccountType, + accountsList: softwareAccountsList, + ledgerAccountsList, + btcPaymentAddressType, + } = rootStore.store.getState().walletState; + + const account = getSelectedAccount({ + selectedAccountIndex, + selectedAccountType, + softwareAccountsList, + ledgerAccountsList, + }); + + if (!account) { + sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); + return; } - await openPopup({ - path: RequestsRoutes.AddressRequest, - data: parseResult.output, - context: makeContext(port), - onClose: makeSendPopupClosedUserRejectionMessage({ - tabId: getTabIdFromPort(port), - messageId: parseResult.output.id, - }), + const embellishedAccount = embellishAccountWithDetails(account, btcPaymentAddressType); + const addresses = accountPurposeAddresses(embellishedAccount, parseResult.output.params.purposes); + sendGetAddressesSuccessResponseMessage({ + tabId, + messageId: message.id, + result: { + addresses, + }, }); }; diff --git a/src/common/utils/rpc/btc/getAddresses/utils.ts b/src/common/utils/rpc/btc/getAddresses/utils.ts index 9793e41ca..32d61b73c 100644 --- a/src/common/utils/rpc/btc/getAddresses/utils.ts +++ b/src/common/utils/rpc/btc/getAddresses/utils.ts @@ -1,8 +1,8 @@ /* eslint-disable import/prefer-default-export */ +import type { AccountWithDetails } from '@common/utils/getSelectedAccount'; import { AddressPurpose, AddressType } from '@sats-connect/core'; -import type { Account } from '@secretkeylabs/xverse-core'; -export function accountPurposeAddresses(account: Account, purposes: AddressPurpose[]) { +export function accountPurposeAddresses(account: AccountWithDetails, purposes: AddressPurpose[]) { return purposes.map((purpose) => { if (purpose === AddressPurpose.Ordinals) { return { @@ -24,7 +24,7 @@ export function accountPurposeAddresses(account: Account, purposes: AddressPurpo address: account.btcAddress, publicKey: account.btcPublicKey, purpose: AddressPurpose.Payment, - addressType: account.accountType === 'ledger' ? AddressType.p2wpkh : AddressType.p2sh, + addressType: account.btcAddressType === 'native' ? AddressType.p2wpkh : AddressType.p2sh, }; }); } diff --git a/src/common/utils/rpc/btc/getBalance.ts b/src/common/utils/rpc/btc/getBalance.ts index 305f39768..c497fac89 100644 --- a/src/common/utils/rpc/btc/getBalance.ts +++ b/src/common/utils/rpc/btc/getBalance.ts @@ -1,14 +1,14 @@ import { getTabIdFromPort } from '@common/utils'; -import getSelectedAccount from '@common/utils/getSelectedAccount'; +import getSelectedAccount, { embellishAccountWithDetails } from '@common/utils/getSelectedAccount'; import { makeContext } from '@common/utils/popup'; import { safePromise, type Result } from '@common/utils/safe'; -import { makeAccountResourceId } from '@components/permissionsManager/resources'; import * as utils from '@components/permissionsManager/utils'; import { getBalanceRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; import { BitcoinEsploraApiProvider, type NetworkType } from '@secretkeylabs/xverse-core'; import rootStore from '@stores/index'; import * as v from 'valibot'; import { handleInvalidMessage } from '../handle-invalid-message'; +import { hasAccountReadPermissions } from '../helpers'; import { sendGetBalanceSuccessResponseMessage } from '../responseMessages/bitcoin'; import { sendAccessDeniedResponseMessage, @@ -47,68 +47,65 @@ async function getBalance( const handleGetBalance = async (message: RpcRequestMessage, port: chrome.runtime.Port) => { const parseResult = v.safeParse(getBalanceRequestMessageSchema, message); - if (!parseResult.success) { handleInvalidMessage(message, getTabIdFromPort(port), parseResult.issues); return; } const { origin, tabId } = makeContext(port); - const [error, store] = await utils.loadPermissionsStore(); + const [error, store] = await utils.getPermissionsStore(); if (error) { - sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); + sendInternalErrorMessage({ + tabId, + messageId: parseResult.output.id, + message: 'Error loading permissions store.', + }); return; } - if (!store) { + if (!hasAccountReadPermissions(origin, store)) { sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); return; } + await utils.permissionsStoreMutex.runExclusive(async () => { + // Update the last used time for the client + utils.updateClientMetadata(store, origin, { lastUsed: new Date().getTime() }); + await utils.savePermissionsStore(store); + }); + const { selectedAccountIndex, selectedAccountType, accountsList: softwareAccountsList, ledgerAccountsList, network, + btcPaymentAddressType, } = rootStore.store.getState().walletState; - const existingAccount = getSelectedAccount({ + const account = getSelectedAccount({ selectedAccountIndex, selectedAccountType, softwareAccountsList, ledgerAccountsList, }); - if (!existingAccount) { + if (!account) { sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); return; } - const permission = utils.getClientPermission( - store.permissions, - origin, - makeAccountResourceId({ - accountId: selectedAccountIndex, - networkType: network.type, - masterPubKey: existingAccount.masterPubKey, - }), - ); - if (!permission) { - sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); - return; - } - - if (!permission.actions.has('read')) { - sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); - return; - } + const detailedAccount = embellishAccountWithDetails(account, btcPaymentAddressType); - const address = existingAccount.btcAddress; + const address = detailedAccount.btcAddress; const [getBalanceError, balances] = await getBalance(address, network.type); if (getBalanceError) { - sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); + sendInternalErrorMessage({ + tabId, + messageId: parseResult.output.id, + message: 'Error retrieving balance.', + }); return; } diff --git a/src/common/utils/rpc/btc/signPsbt.ts b/src/common/utils/rpc/btc/signPsbt.ts index 67811c67c..ef94f949a 100644 --- a/src/common/utils/rpc/btc/signPsbt.ts +++ b/src/common/utils/rpc/btc/signPsbt.ts @@ -16,7 +16,6 @@ const SignPsbtSchema = z.object({ psbt: z.string(), signInputs: z.record(z.array(z.number())), // Record broadcast: z.boolean().optional(), - allowedSignHash: z.number().optional(), }); export const handleSignPsbt = async ( @@ -40,8 +39,6 @@ export const handleSignPsbt = async ( ]; if (message.params.broadcast) requestParams.push(['broadcast', String(message.params.broadcast)]); - if (message.params.allowedSignHash) - requestParams.push(['allowedSigHash', message.params.allowedSignHash.toString()]); const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams); @@ -51,7 +48,7 @@ export const handleSignPsbt = async ( id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to sign a psbt', }), }); diff --git a/src/common/utils/rpc/helpers.ts b/src/common/utils/rpc/helpers.ts index 425c7bc67..a602590e2 100644 --- a/src/common/utils/rpc/helpers.ts +++ b/src/common/utils/rpc/helpers.ts @@ -1,5 +1,6 @@ import { MESSAGE_SOURCE } from '@common/types/message-types'; import { makeAccountResourceId } from '@components/permissionsManager/resources'; +import type { PermissionsStore } from '@components/permissionsManager/schemas'; import * as utils from '@components/permissionsManager/utils'; import { RpcErrorCode, @@ -55,16 +56,7 @@ export function makeSendPopupClosedUserRejectionMessage({ }; } -export async function hasPermissions(origin: string): Promise { - const [error, store] = await utils.loadPermissionsStore(); - if (error) { - return false; - } - - if (!store) { - return false; - } - +export function hasAccountReadPermissions(origin: string, store: PermissionsStore): boolean { const { selectedAccountIndex, selectedAccountType, diff --git a/src/common/utils/rpc/ordinals/getInscriptions.ts b/src/common/utils/rpc/ordinals/getInscriptions.ts index ba4c1d10e..05688f367 100644 --- a/src/common/utils/rpc/ordinals/getInscriptions.ts +++ b/src/common/utils/rpc/ordinals/getInscriptions.ts @@ -22,10 +22,14 @@ const handleGetInscriptions = async (message: RpcRequestMessage, port: chrome.ru return; } const { origin, tabId } = makeContext(port); - const [loadError, store] = await utils.loadPermissionsStore(); + const [loadError, store] = await utils.getPermissionsStore(); if (loadError) { - sendInternalErrorMessage({ tabId, messageId: message.id }); + sendInternalErrorMessage({ + tabId, + messageId: message.id, + message: 'Error loading permissions store.', + }); return; } @@ -80,11 +84,17 @@ const handleGetInscriptions = async (message: RpcRequestMessage, port: chrome.ru return; } + await utils.permissionsStoreMutex.runExclusive(async () => { + // Update the last used time for the client + utils.updateClientMetadata(store, origin, { lastUsed: new Date().getTime() }); + await utils.savePermissionsStore(store); + }); + const ordinalsApi = new OrdinalsApi({ network: network.type }); try { const inscriptionsList = await ordinalsApi.getInscriptions( - existingAccount.ordinalsAddress, + existingAccount.btcAddresses.taproot.address, parseResult.output.params.offset, parseResult.output.params.limit, ); diff --git a/src/common/utils/rpc/responseMessages/errors.ts b/src/common/utils/rpc/responseMessages/errors.ts index b57b002c1..0095fc746 100644 --- a/src/common/utils/rpc/responseMessages/errors.ts +++ b/src/common/utils/rpc/responseMessages/errors.ts @@ -61,12 +61,16 @@ export function sendAddressMismatchMessage({ tabId, messageId }: BaseArgs) { ); } -export function sendInternalErrorMessage({ tabId, messageId }: BaseArgs) { +export function sendInternalErrorMessage({ + tabId, + messageId, + message, +}: BaseArgs & { message?: string }) { sendRpcResponse( tabId, makeRPCError(messageId, { code: RpcErrorCode.INTERNAL_ERROR, - message: 'Internal error.', + message: message ?? 'Internal error.', }), ); } diff --git a/src/common/utils/rpc/runes/etch.ts b/src/common/utils/rpc/runes/etch.ts index 02cd3fe51..140ae02bd 100644 --- a/src/common/utils/rpc/runes/etch.ts +++ b/src/common/utils/rpc/runes/etch.ts @@ -68,7 +68,7 @@ const handleEtchRune = async (message: WebBtcMessage<'runes_etch'>, port: chrome id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to etch a rune', }), }); listenForOriginTabClose({ tabId }); diff --git a/src/common/utils/rpc/runes/getBalance.ts b/src/common/utils/rpc/runes/getBalance.ts index a867aa862..b45c8f6fd 100644 --- a/src/common/utils/rpc/runes/getBalance.ts +++ b/src/common/utils/rpc/runes/getBalance.ts @@ -26,10 +26,14 @@ const handleGetRunesBalance = async (message: RpcRequestMessage, port: chrome.ru return; } const { origin, tabId } = makeContext(port); - const [loadError, store] = await utils.loadPermissionsStore(); + const [loadError, store] = await utils.getPermissionsStore(); if (loadError) { - sendInternalErrorMessage({ tabId, messageId: message.id }); + sendInternalErrorMessage({ + tabId, + messageId: message.id, + message: 'Error loading permissions store.', + }); return; } @@ -84,10 +88,18 @@ const handleGetRunesBalance = async (message: RpcRequestMessage, port: chrome.ru return; } + await utils.permissionsStoreMutex.runExclusive(async () => { + // Update the last used time for the client + utils.updateClientMetadata(store, origin, { lastUsed: new Date().getTime() }); + await utils.savePermissionsStore(store); + }); + const runesApi = getRunesClient(network.type); try { - const runesBalances = await runesApi.getRuneBalances(existingAccount.ordinalsAddress); + const runesBalances = await runesApi.getRuneBalances( + existingAccount.btcAddresses.taproot.address, + ); sendRpcResponse( tabId, makeRpcSuccessResponse<'runes_getBalance'>(message.id, { diff --git a/src/common/utils/rpc/runes/mint.ts b/src/common/utils/rpc/runes/mint.ts index 3919b8f8b..0942f0318 100644 --- a/src/common/utils/rpc/runes/mint.ts +++ b/src/common/utils/rpc/runes/mint.ts @@ -14,7 +14,7 @@ import RequestsRoutes from '../../route-urls'; import { makeRPCError, sendRpcResponse } from '../helpers'; const MintRuneParamsSchema = z.object({ - appServiceFee: z.string().optional(), + appServiceFee: z.number().optional(), appServiceFeeAddress: z.string().optional(), destinationAddress: z.string(), feeRate: z.number(), @@ -49,7 +49,7 @@ const handleMintRune = async (message: WebBtcMessage<'runes_mint'>, port: chrome id, response: makeRPCError(message.id, { code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to send transfer', + message: 'User rejected request to mint a rune', }), }); listenForOriginTabClose({ tabId }); diff --git a/src/common/utils/rpc/stx/getAddresses/index.ts b/src/common/utils/rpc/stx/getAddresses/index.ts index 258f6f49d..e5cc12b68 100644 --- a/src/common/utils/rpc/stx/getAddresses/index.ts +++ b/src/common/utils/rpc/stx/getAddresses/index.ts @@ -1,6 +1,7 @@ import { type WebBtcMessage } from '@common/types/message-types'; import getSelectedAccount from '@common/utils/getSelectedAccount'; import { makeContext } from '@common/utils/popup'; +import * as utils from '@components/permissionsManager/utils'; import { AddressPurpose, AddressType, RpcErrorCode } from '@sats-connect/core'; import rootStore from '@stores/index'; import { @@ -9,7 +10,11 @@ import { triggerRequestWindowOpen, } from '../../../legacy-external-message-handler'; import RequestsRoutes from '../../../route-urls'; -import { hasPermissions, makeRPCError } from '../../helpers'; +import { hasAccountReadPermissions, makeRPCError } from '../../helpers'; +import { + sendAccessDeniedResponseMessage, + sendInternalErrorMessage, +} from '../../responseMessages/errors'; import { sendGetAddressesSuccessResponseMessage } from '../../responseMessages/stacks'; const handleGetStxAddresses = async ( @@ -20,52 +25,65 @@ const handleGetStxAddresses = async ( messageId: String(message.id), rpcMethod: 'stx_getAddresses', }; - const { origin, tabId } = makeContext(port); - if (await hasPermissions(origin)) { - const { - selectedAccountIndex, - selectedAccountType, - accountsList: softwareAccountsList, - ledgerAccountsList, - } = rootStore.store.getState().walletState; - const account = getSelectedAccount({ - selectedAccountIndex, - selectedAccountType, - softwareAccountsList, - ledgerAccountsList, + const [error, store] = await utils.getPermissionsStore(); + if (error) { + sendInternalErrorMessage({ + tabId, + messageId: message.id, + message: 'Error loading permissions store.', }); + return; + } - if (account) { - sendGetAddressesSuccessResponseMessage({ - tabId, - messageId: message.id, - result: { - addresses: [ - { - address: account.stxAddress, - publicKey: account.stxPublicKey, - addressType: AddressType.stacks, - purpose: AddressPurpose.Stacks, - }, - ], - }, - }); - return; - } + if (!hasAccountReadPermissions(origin, store)) { + sendAccessDeniedResponseMessage({ tabId, messageId: message.id }); + return; } - const { urlParams } = makeSearchParamsWithDefaults(port, popupParams); + const { + selectedAccountIndex, + selectedAccountType, + accountsList: softwareAccountsList, + ledgerAccountsList, + } = rootStore.store.getState().walletState; + + const account = getSelectedAccount({ + selectedAccountIndex, + selectedAccountType, + softwareAccountsList, + ledgerAccountsList, + }); - const { id } = await triggerRequestWindowOpen(RequestsRoutes.StxAddressRequest, urlParams); - listenForPopupClose({ + if (!account) { + const { urlParams } = makeSearchParamsWithDefaults(port, popupParams); + + const { id } = await triggerRequestWindowOpen(RequestsRoutes.StxAddressRequest, urlParams); + listenForPopupClose({ + tabId, + id, + response: makeRPCError(message.id, { + code: RpcErrorCode.USER_REJECTION, + message: 'User rejected request to get addresses', + }), + }); + return; + } + + sendGetAddressesSuccessResponseMessage({ tabId, - id, - response: makeRPCError(message.id, { - code: RpcErrorCode.USER_REJECTION, - message: 'User rejected request to get addresses', - }), + messageId: message.id, + result: { + addresses: [ + { + address: account.stxAddress, + publicKey: account.stxPublicKey, + addressType: AddressType.stacks, + purpose: AddressPurpose.Stacks, + }, + ], + }, }); }; diff --git a/src/common/utils/rpc/wallet/getWalletType.ts b/src/common/utils/rpc/wallet/getWalletType.ts index 56801e62e..d48a41234 100644 --- a/src/common/utils/rpc/wallet/getWalletType.ts +++ b/src/common/utils/rpc/wallet/getWalletType.ts @@ -2,12 +2,12 @@ import { getTabIdFromPort } from '@common/utils'; import getSelectedAccount from '@common/utils/getSelectedAccount'; import { makeContext } from '@common/utils/popup'; -import { makeAccountResourceId } from '@components/permissionsManager/resources'; import * as utils from '@components/permissionsManager/utils'; import { getWalletTypeRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; import rootStore from '@stores/index'; import * as v from 'valibot'; import { handleInvalidMessage } from '../handle-invalid-message'; +import { hasAccountReadPermissions } from '../helpers'; import { sendAccessDeniedResponseMessage, sendInternalErrorMessage, @@ -19,66 +19,56 @@ export const handleGetWalletType = async ( port: chrome.runtime.Port, ) => { const parseResult = v.safeParse(getWalletTypeRequestMessageSchema, message); - if (!parseResult.success) { handleInvalidMessage(message, getTabIdFromPort(port), parseResult.issues); return; } const { origin, tabId } = makeContext(port); - const [error, store] = await utils.loadPermissionsStore(); + const [error, store] = await utils.getPermissionsStore(); if (error) { - sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); + sendInternalErrorMessage({ + tabId, + messageId: parseResult.output.id, + message: 'Error loading permissions store.', + }); return; } - if (!store) { + if (!hasAccountReadPermissions(origin, store)) { sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); return; } + await utils.permissionsStoreMutex.runExclusive(async () => { + // Update the last used time for the client + utils.updateClientMetadata(store, origin, { lastUsed: new Date().getTime() }); + await utils.savePermissionsStore(store); + }); + const { selectedAccountIndex, selectedAccountType, accountsList: softwareAccountsList, ledgerAccountsList, - network, } = rootStore.store.getState().walletState; - const existingAccount = getSelectedAccount({ + const account = getSelectedAccount({ selectedAccountIndex, selectedAccountType, softwareAccountsList, ledgerAccountsList, }); - if (!existingAccount) { + if (!account) { sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); return; } - const permission = utils.getClientPermission( - store.permissions, - origin, - makeAccountResourceId({ - accountId: selectedAccountIndex, - networkType: network.type, - masterPubKey: existingAccount.masterPubKey, - }), - ); - if (!permission) { - sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); - return; - } - - if (!permission.actions.has('read')) { - sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); - } - sendGetWalletTypeSuccessResponseMessage({ tabId, messageId: parseResult.output.id, - result: existingAccount.accountType ?? 'software', + result: account.accountType ?? 'software', }); }; diff --git a/src/common/utils/rpc/wallet/renouncePermissions.ts b/src/common/utils/rpc/wallet/renouncePermissions.ts index 1e4881ce6..377caab3a 100644 --- a/src/common/utils/rpc/wallet/renouncePermissions.ts +++ b/src/common/utils/rpc/wallet/renouncePermissions.ts @@ -1,5 +1,6 @@ /* eslint-disable import/prefer-default-export */ import { getTabIdFromPort } from '@common/utils'; +import { dispatchEventToOrigin } from '@common/utils/messages/extensionToContentScript/dispatchEvent'; import { makeContext } from '@common/utils/popup'; import * as utils from '@components/permissionsManager/utils'; import { @@ -8,10 +9,7 @@ import { } from '@sats-connect/core'; import * as v from 'valibot'; import { handleInvalidMessage } from '../handle-invalid-message'; -import { - sendAccessDeniedResponseMessage, - sendInternalErrorMessage, -} from '../responseMessages/errors'; +import { sendInternalErrorMessage } from '../responseMessages/errors'; import { sendRenouncePermissionsSuccessResponseMessage } from '../responseMessages/wallet'; export const handleRenouncePermissions = async ( @@ -26,23 +24,26 @@ export const handleRenouncePermissions = async ( } const { origin, tabId } = makeContext(port); - const [error, store] = await utils.loadPermissionsStore(); + const [error, store] = await utils.getPermissionsStore(); if (error) { - sendInternalErrorMessage({ tabId, messageId: parseResult.output.id }); - return; - } - - if (!store) { - sendAccessDeniedResponseMessage({ tabId, messageId: parseResult.output.id }); + sendInternalErrorMessage({ + tabId, + messageId: parseResult.output.id, + message: 'Error loading permissions store.', + }); return; } await utils.permissionsStoreMutex.runExclusive(async () => { - utils.removeClient(store.clients, store.permissions, origin); + utils.removeClient(store, origin); utils.savePermissionsStore(store); }); + dispatchEventToOrigin(origin, { + type: 'disconnect', + }); + sendRenouncePermissionsSuccessResponseMessage({ tabId, messageId: parseResult.output.id, diff --git a/src/common/utils/rpc/wallet/requestPermissions.ts b/src/common/utils/rpc/wallet/requestPermissions.ts index bb38eed8b..d59eb1292 100644 --- a/src/common/utils/rpc/wallet/requestPermissions.ts +++ b/src/common/utils/rpc/wallet/requestPermissions.ts @@ -1,11 +1,13 @@ /* eslint-disable import/prefer-default-export */ import { getTabIdFromPort } from '@common/utils'; import { makeContext, openPopup } from '@common/utils/popup'; +import * as utils from '@components/permissionsManager/utils'; import { requestPermissionsRequestMessageSchema, type RpcRequestMessage } from '@sats-connect/core'; import * as v from 'valibot'; import RequestsRoutes from '../../route-urls'; import { handleInvalidMessage } from '../handle-invalid-message'; -import { makeSendPopupClosedUserRejectionMessage } from '../helpers'; +import { hasAccountReadPermissions, makeSendPopupClosedUserRejectionMessage } from '../helpers'; +import { sendRequestPermissionsSuccessResponseMessage } from '../responseMessages/wallet'; export const handleRequestPermissions = async ( message: RpcRequestMessage, @@ -18,10 +20,22 @@ export const handleRequestPermissions = async ( return; } + const requestContext = makeContext(port); + + const [error, store] = await utils.getPermissionsStore(); + if (!error && hasAccountReadPermissions(requestContext.origin, store)) { + sendRequestPermissionsSuccessResponseMessage({ + messageId: parseResult.output.id, + tabId: requestContext.tabId, + result: true, + }); + return; + } + await openPopup({ path: RequestsRoutes.ConnectionRequest, data: parseResult.output, - context: makeContext(port), + context: requestContext, onClose: makeSendPopupClosedUserRejectionMessage({ tabId: getTabIdFromPort(port), messageId: parseResult.output.id, diff --git a/src/common/utils/safe.ts b/src/common/utils/safe.ts index 18296a156..5a03be77b 100644 --- a/src/common/utils/safe.ts +++ b/src/common/utils/safe.ts @@ -1,11 +1,40 @@ -type SuccessResult = [null, T]; -type ErrorResult = [E, null]; -export type Result = SuccessResult | ErrorResult; +/* eslint-disable @typescript-eslint/no-shadow */ -export async function safePromise(promise: Promise): Promise> { +// NOTE: The above rule is disabled due to the currently used version not +// detecting modern Typescript syntax. + +export type SafeError = { + readonly name: TName; + readonly message: string; + readonly data?: TData; +}; + +type SuccessResult = [null, Data]; +type ErrorResult = [Error, null]; +export type Result = + | SuccessResult + | ErrorResult; + +/** + * @public + */ +export function success(data: Data): Result { + return [null, data]; +} + +/** + * @public + */ +export function error(error: E): Result { + return [error, null]; +} + +export async function safePromise( + promise: Promise, +): Promise>> { try { - return [null, await promise]; - } catch (error) { - return [new Error('Safe promise rejected.', { cause: error }), null]; + return success(await promise); + } catch (e) { + return error({ name: 'SafeError', message: 'Promise rejected.', data: e }); } } diff --git a/src/content-scripts/README.md b/src/content-scripts/README.md index 26f30e142..79df33e75 100644 --- a/src/content-scripts/README.md +++ b/src/content-scripts/README.md @@ -7,4 +7,4 @@ The content script listens for incomming messages using `chrome.runtime.onMessag The complete message list and schema definitions see [extensionToContentScript](../common/utils/messages/extensionToContentScript/). -Messages that aren't notifications are assumed to be RPC responses and are forwarded to the tab using `window.postMessage()`. +Messages that aren't events are assumed to be RPC responses and are forwarded to the tab using `window.postMessage()`. diff --git a/src/locales/en.json b/src/locales/en.json index 62d463952..920d14d53 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,6 +5,9 @@ "COMBO": "Combo", "SATS": "Sats", "BTC": "BTC", + "BALANCE": "Balance", + "BITCOIN": "Bitcoin", + "STACKS": "Stacks", "SATRIBUTES": "Satributes", "INPUT": "Input", "SIZE": "Size", @@ -16,7 +19,13 @@ "CONFIRM": "Confirm", "APPLY": "Apply", "IN": "In", - "TO": "To" + "TO": "To", + "FROM": "From", + "OPTIONS": "Options", + "UNDO": "Undo" + }, + "INFORMATION": { + "ORDINALS_ADDRESS_TOOLTIP": "These sats are used for your ordinals and runes. They cannot be spent with Xverse wallet." }, "UNITS": { "SATS_PER_VB": "sats/vB" @@ -35,7 +44,7 @@ "LEDGER_IMPORT_1_BUTTON": "Get started", "LEDGER_IMPORT_2_TITLE": "Select assets", "LEDGER_IMPORT_2_SUBTITLE": "Choose the assets you want to manage with this wallet.", - "LEDGER_IMPORT_2_FOOTNOTE": "If you don’t want to set a Stacks address now, you have the possibility to set it up later.", + "LEDGER_IMPORT_2_FOOTNOTE": "If you don't want to set a Stacks address now, you have the possibility to set it up later.", "LEDGER_IMPORT_2_SELECT": { "BTC_TITLE": "Bitcoin, Runes & Ordinals (default)", "BTC_SUBTITLE": "Native SegWit and Taproot addresses", @@ -103,10 +112,10 @@ "LEARN_MORE": "Learn More", "LEDGER_BEFORE_GETTING_STARTED": { "TITLE": "Before getting started", - "DESCRIPTION": "Do you use Ledger Live with the hardware wallet device you wish to connect?", + "DESCRIPTION": "Please select one of the following options to continue.", "OPTIONS": { - "USE_LEDGER_LIVE": "I use Ledger Live with the device.", - "DONT_USE_LEDGER_LIVE": "I do not use Ledger Live with the device." + "USE_LEDGER_LIVE": "I am using Ledger Live with this device.", + "DONT_USE_LEDGER_LIVE": "I am not using Ledger Live with this device." }, "IMPORTANT_WARNING": { "TITLE": "Important - Please read", @@ -118,7 +127,7 @@ "UNDERSTAND_SHOULD_NOT_USE_LEDGER_LIVE": "I understand I should not use Ledger Live or other Bitcoin wallets with the same hardware device" } }, - "ADDRESS_MISMATCH": "There’s a mismatch between your signing address and the address you’re logged with.", + "ADDRESS_MISMATCH": "There's a mismatch between your signing address and the address you're logged in with.", "CLOSE_BUTTON": "Close", "TITLE_FAILED": "Connection failed" }, @@ -146,7 +155,7 @@ "ORDINALS_TITLE_VERIFIED": "Ordinals address verified successfully", "STACKS_TITLE_VERIFIED": "Stacks address verified successfully", "WRONG_DEVICE_ERROR_SUBTITLE": "The public key of the connected device did not match the wallet public key. Make sure you have the correct device connected.", - "ADDRESS_MISMATCH": "There’s a mismatch between the address you’re verifying and the address you’re logged with.", + "ADDRESS_MISMATCH": "There's a mismatch between the address you're verifying and the address you're logged with.", "LEDGER_CONNECT": { "BTC_TITLE": "Connect Your Ledger", "BTC_SUBTITLE": "To continue, connect your Ledger device, make sure it's unlocked, and open the Bitcoin app.", @@ -195,7 +204,8 @@ "SEND_ORDINAL": "Send ordinal", "CONFIRM_FEES": "Confirm fees", "RETRY_BUTTON": "Try again", - "FEES": "Fees" + "FEES": "Fees", + "AMOUNT": "Amount" }, "DASHBOARD_SCREEN": { "TOTAL_BALANCE": "total balance", @@ -225,7 +235,25 @@ "ALLOW": "Allow" }, "TOKEN_HIDDEN": "Token hidden and reported", - "UNDO": "Undo" + "UNDO": "Undo", + "ANNOUNCEMENTS": { + "NATIVE_SEGWIT": { + "TITLE": "Native SegWit is here!", + "DESCRIPTION_1a": "Xverse now supports ", + "DESCRIPTION_1b": "Native SegWit", + "DESCRIPTION_1c": " addresses for lower fees and better performance!", + "DESCRIPTION_2": "You can select your preferred Bitcoin payment address (native or nested) used for sending, receiving, and connecting with apps.", + "DESCRIPTION_3": "Your Bitcoin balance includes funds from both addresses, and you can switch back anytime.", + "SELECT": "Select my Preferred Bitcoin Address", + "LATER": "Maybe later" + } + }, + "CALLOUTS": { + "NATIVE_SEGWIT": { + "TITLE": "Now using Native SegWit!", + "DESCRIPTION": "Your new Native SegWit address is set as preferred. Switch back to Nested SegWit in Settings." + } + } }, "TOKEN_SCREEN": { "ADD_COINS": "Manage tokens", @@ -250,7 +278,17 @@ "ORDINAL_ADDRESS": "Ordinal, Runes & BRC-20 address", "ORDINALS_RECEIVE_MESSAGE": "Only use this address to receive Ordinals, Runes, and BRC-20 tokens.", "STX_RECEIVE_MESSAGE": "Only use this address to receive SIP-10 tokens.", - "BTC_RECEIVE_MESSAGE": "Only use this address to receive Bitcoin." + "BTC_RECEIVE_MESSAGE": "Only use this address to receive Bitcoin.", + "CALLOUTS": { + "NATIVE_SEGWIT": { + "TITLE": "Now using Native SegWit!", + "DESCRIPTION": "Your new Native SegWit address is set as preferred address." + }, + "ADDRESS_CHANGE_TOOLTIP": { + "TITLE": "Switch Preferred Bitcoin Address", + "DESCRIPTION": "You can switch between Native SegWit and Nested SegWit." + } + } }, "SEND": { "BTC": { @@ -259,7 +297,9 @@ "MAX_IGNORING_DUST_UTXO_MSG": "Small amounts may remain in your wallet, as sending them costs more in fees than their value.", "NO_FUNDS_TITLE": "Your balance is empty", "NO_FUNDS": "You don't have any funds to send. You can purchase crypto quickly and deposit directly into your Xverse wallet.", - "BUY_BTC": "Buy Bitcoin" + "BUY_BTC": "Buy Bitcoin", + "SELECT_ADDRESS_TYPE": "Select address type", + "SELECT_ADDRESS_TYPE_DESCRIPTION": "Select your preferred address to send BTC. Using Native SegWit is recommended for reduced fees and better performance." }, "STX": { "BUY_STX": "Buy Stacks" @@ -345,6 +385,7 @@ "BUNDLE_PLUS_FEES": "Bundle size + fees", "REVIEW_TRANSACTION": "Review transaction", "SIGN_TRANSACTIONS": "Sign {{count}} transactions", + "SIGN_TRANSACTION": "Sign transaction", "AMOUNT": "Amount", "INSUFFICIENT_BALANCE": "Insufficient balance", "YOUR_ADDRESS": "My address", @@ -373,8 +414,6 @@ "TRANSACTIONS_FAILED_TO_BROADCAST": "{{failed}}/{{total}} transactions failed to broadcast", "CHECK_THE_APP_YOU_USE": "Please check the app you are using.", "TRANSACTIONS_SIGNED": "Transactions signed", - "NETWORK_MISMATCH": "There’s a mismatch between your active network and the network you’re logged with.", - "ADDRESS_MISMATCH": "There’s a mismatch between your signing address and the address you’re logged with.", "PSBT_NO_BROADCAST_DISCLAIMER": "This transaction will not be broadcasted from your wallet. It may be broadcasted later by a third party.", "PSBTS_NO_BROADCAST_DISCLAIMER": "These transactions will not be broadcasted from your wallet. They may be broadcasted later by a third party.", "PSBT_SIG_HASH_NONE_DISCLAIMER_TITLE": "Transaction uses SIGHASH_NONE", @@ -409,7 +448,7 @@ "RUNES_CENOTAPH_WARNING": "This transaction will burn all input Runes. Make sure you trust the requesting app.", "YOU_WILL_SEND": "You will send", "TRANSACTION_DETAILS": "Transaction details", - "BTC_TRANSFER_WARNING": "This amount may include the tx fee & postage associated to transferred ordinals, runes & BRC-20.", + "BTC_TRANSFER_WARNING": "This amount may include the network fee.", "LEDGER": { "CONNECT": { "TITLE": "Connect your hardware wallet", @@ -502,8 +541,12 @@ }, "TRANSACTION_STATUS": { "BROADCASTED": "Transaction broadcasted", + "BROADCASTED_MULTIPLE": "Transactions broadcasted", + "BROADCASTED_MULTIPLE_PARTIALLY": "Transactions partially broadcasted", + "BROADCASTED_MULTIPLE_PARTIALLY_SUBTITLE": "{{failedTranscations}}/{{totalTranscations}} transcations failed to broadcast.", "SUCCESS_MSG": "Your transaction has been successfully submitted.", "SEE_ON": "See on", + "SEE_DETAIL": "See detail", "SEE_ON_MAGICEDEN": "See {{runeSymbol}} on Magic Eden", "STACKS_EXPLORER": "Stacks Explorer", "BITCOIN_EXPLORER": "Explorer", @@ -550,7 +593,6 @@ "CONFIRM_PASSWORD_TITLE": "Confirm your password", "ENTER_PASSWORD": "Enter your current password", "TEXT_INPUT_NEW_PASSWORD_LABEL": "Password", - "TEXT_INPUT_ENTER_PASSWORD_LABEL": "Enter your current password", "TEXT_INPUT_CONFIRM_PASSWORD_LABEL": "Confirm Password", "CONTINUE_BUTTON": "Continue", "BACK_BUTTON": "Back", @@ -558,7 +600,6 @@ "PASSWORD_STRENGTH_WEAK": "Weak", "PASSWORD_STRENGTH_MEDIUM": "Medium", "PASSWORD_STRENGTH_STRONG": "Strong", - "PASSWORD_STRENGTH_ERROR": "Your password should be at least 9 characters long. Use a mix of uppercase letters, lowercase letters, numbers, and symbols", "CONFIRM_PASSWORD_MATCH_ERROR": "Please make sure your passwords match", "INCORRECT_PASSWORD_ERROR": "Incorrect password" }, @@ -625,6 +666,7 @@ } }, "LOGIN_SCREEN": { + "BETA_VERSION": "Beta", "WELCOME_MESSAGE_FIRST_LOGIN": "Welcome!", "WELCOME_MESSAGE": "Welcome back!", "PASSWORD_INPUT_LABEL": "Password", @@ -635,17 +677,25 @@ }, "FORGOT_PASSWORD_SCREEN": { "TITLE": "Forgot Password", - "PARAGRAPH1": "Xverse does not keep a copy of your password. If you're unable to access your account, you will need to reset your wallet and input the seedphrase you used when you generated your wallet.", + "PARAGRAPH1": "Xverse does not keep a copy of your password. If you're unable to access your account, you will need to reset your wallet and input the seed phrase you used when you generated your wallet.", "PARAGRAPH2": "This will reset your wallet from this browser. Make sure you have your seed phrase backed up.", - "BACKUP_CHECKBOX_LABEL": "I backed up my seedphrase", + "BACKUP_CHECKBOX_LABEL": "I backed up my seed phrase", "CANCEL": "Cancel", "RESET": "Reset" }, "RESTORE_WALLET_SCREEN": { - "ENTER_SEED_HEADER": "Enter your seedphrase to restore your wallet.", + "ENTER_SEED_HEADER": "Enter your seed phrase to restore your wallet.", "SEED_INPUT_ERROR": "Invalid seed phrase, please try again", "CONTINUE_BUTTON": "Continue", - "HAVE_A_24_WORDS_SEEDPHRASE?": "Have a {{number}} words seedphrase?" + "HAVE_A_COUNT_WORDS_SEED_PHRASE": "Have a {{number}} words seed phrase?", + "SELECT_ADDRESS_TYPE": { + "TITLE": "Preferred Address Type", + "DESCRIPTION": "Select the default Bitcoin payment address type you would like to use for all accounts. You can switch at any time from settings.", + "LEARN_MORE": "Learn more about address types.", + "ACCOUNT_COUNT": "Accounts: {{accounts}}", + "ACCOUNT_SUMMARY_DESCRIPTION": "The balances shown are cumulative across all found accounts for each address type.", + "ACCOUNT_SUMMARY_DESCRIPTION_NO_FUNDS": "No balances found for this seed phrase." + } }, "STACKING_SCREEN": { "STACK_AND_EARN": "Stack STX, earn BTC", @@ -665,6 +715,7 @@ "NO_EARN_OPTION": "No Earn options currently available for this wallet type." }, "NFT_DASHBOARD_SCREEN": { + "BUNDLE": "Bundle", "COLLECTIBLES": "Collectibles", "NO_COLLECTIBLES": "There's nothing here yet.", "INSCRIPTIONS": "Inscriptions", @@ -695,6 +746,7 @@ "MINT": "Mint", "TRANSFER": "Transfer", "DEPLOY": "Deploy", + "VERIFY": "Verify", "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", "ADD_STACKS_ADDRESS": "Add a Stacks address", @@ -710,7 +762,11 @@ "RARE_SATS_NOTICE_DETAIL": "Xverse supports most rare sats, but your sats may have other attributes.", "SEE_SUPPORTED": "See supported rarities", "FROM_RARE_SAT_BUNDLE": "This inscription belongs to the same bundle as other assets. Transferring it will involve transferring the full bundle.", - "HOLDS_RARE_SAT": "This inscription holds a rare sat." + "HOLDS_RARE_SAT": "This inscription holds a rare sat.", + "HIDDEN_COLLECTIBLES": "Hidden collectibles", + "UNHIDE_ALL": "Unhide all", + "HIDDEN_ITEMS_RESTORED": "Hidden items restored", + "ITEMS_RETURNED_TO_HIDDEN": "Items returned to hidden" }, "RESTORE_FUND_SCREEN": { "TITLE": "Recover assets", @@ -750,8 +806,15 @@ "TRANSFER_ALL": "Transfer all" }, "NFT_DETAIL_SCREEN": { + "RARE_SATS": "Rare Sats", "NFT_DETAIL": "Item detail", "WEB_GALLERY": "Open gallery", + "STAR_INSCRIPTION": "Item starred", + "UNSTAR_INSCRIPTION": "Item un-starred", + "HIDE_INSCRIPTION": "Hide", + "UNHIDE_INSCRIPTION": "Unhide", + "INSCRIPTION_HIDDEN": "Item hidden", + "INSCRIPTION_UNHIDDEN": "Item un-hidden", "SEND": "Send", "SHARE": "Share", "OWNED_BY": "Owned by", @@ -769,6 +832,7 @@ "GAMMA": "Gamma.io", "MOVE_TO_ASSET_DETAIL": "Back to gallery", "BACK_TO_COLLECTION": "Back to collection", + "BACK_TO_RUNES": "Back to runes", "ORDINALS": "Ordinal", "ORDINAL_PENDING_SEND_TITLE": "Transfer Pending", "ORDINAL_PENDING_SEND_DESCRIPTION": "This Ordinal is already in a pending transfer.", @@ -803,7 +867,8 @@ "SAT": "Sat", "NFT_TYPE": "Asset Type", "DATA": "Data", - "COPIED": "Sharing Link Copied" + "COPIED": "Sharing Link Copied", + "CONTENT": "Content" }, "RESET_WALLET_SCREEN": { "ENTER_PASSWORD": "Enter your password to reset your wallet", @@ -876,6 +941,13 @@ "ENABLE_RARE_SATS_DETAIL": "Automatically scan your ordinal address for rare sats and display them in your collectibles and transaction review screens", "ENABLE_SPEED_UP_TRANSACTIONS": "Enable speed up transactions", "ENABLE_SPEED_UP_TRANSACTIONS_DETAIL": "Allows you to speed up unconfirmed transactions by paying a higher fee.", + "PREFERRED_BTC_ADDRESS": { + "TITLE": "Preferred Bitcoin Address", + "DESCRIPTION": "You can switch between Native SegWit (more efficient) and Nested SegWit for your transactions. The selected address will be used for sending, receiving, and connecting with dapps. Your total balance includes funds from both addresses, and you can switch back anytime.", + "NESTED_SEGWIT": "Nested SegWit", + "NATIVE_SEGWIT": "Native SegWit", + "SUCCESS": "Preferred Bitcoin address changed" + }, "ADVANCED": "Advanced", "XVERSE_DEFAULT": "Use Xverse as default wallet", "XVERSE_DEFAULT_DESCRIPTION": "Allow apps to prioritize Xverse when looking for a wallet with which to connect." @@ -883,7 +955,14 @@ "CONNECTED_APPS": { "TITLE": "Connected Apps", "SUBTITLE": "You are connected to the following apps.", - "EMPTY_MESSAGE": "You're not currently connected to any app." + "EMPTY_MESSAGE": "You're not currently connected to any app.", + "DISCONNECT_SUCCESS": "App disconnected", + "DISCONNECT_ALL_SUCCESS": "All apps disconnected", + "DISCONNECT": "Disconnect", + "DISCONNECT_ALL": "Disconnect all", + "URL": "URL", + "LAST_USED": "Last used", + "PERMISSIONS": "Permissions" }, "EXPLORE_SCREEN": { "TITLE": "Explore", @@ -905,6 +984,13 @@ "ALREADY_EXISTS_ERR": "You already have an account with this name", "NAME_RULES": "Your account's name can only include alphabetical and numerical characters.", "RESET_NAME": "Reset name" + }, + "NFT_AVATAR": { + "SET_ACTION": "Set as avatar", + "SET_TOAST": "Item set as avatar", + "REMOVE_ACTION": "Remove avatar", + "REMOVE_TOAST": "Avatar removed", + "UNDO": "Avatar reverted" } }, "BUY_SCREEN": { @@ -918,16 +1004,26 @@ "THIRD_PARTY_WARNING": "You will be redirected to a third-party service provider to complete the purchase. If you encounter any issues during the process please contact the provider." }, "LIST_RUNE_SCREEN": { + "LISTING_STATUS": "Listing status", + "FLOOR_PRICE": "Floor Price: {{floor_price}} Sats/{{symbol}}", + "LISTED_FOR": "Listed for {{floor_price}} Sats/{{symbol}}", + "FAILED_TO_LIST": "Failed to list", + "SELECT_RUNES": "Select Runes", + "SELECT_MARKETPLACES": "Select Marketplaces", "LIST_RUNES": "List Runes", - "SELECT_RUNES_SECTION": "Select Runes bundles to list on Magic Eden’s runes marketplace.", + "SELECT_RUNES_SECTION": "Select Runes bundles to list on marketplaces.", + "SELECT_MARKETPLACES_SECTION": "Select the Marketplaces you want to list your Runes on.", "SET_PRICES_SECTION": "Set the listing price for your Runes bundles.", "CONTINUE": "Continue", "LISTED": "LISTED", "NOT_LISTED": "NOT LISTED", "SELECT_ALL": "Select all", "DESELECT_ALL": "Deselect all", + "NEXT": "Next", "SET_PRICES": "Set price", - "MAGIC_EDEN_FLOOR_PRICE": "The Magic Eden floor price is currently {{sats}} Sats/{{symbol}}", + "EDIT_PRICE": "Edit price", + "MAGIC_EDEN_FLOOR_PRICE": "The Magic Eden floor price is currently {{sats}} sats/{{symbol}}", + "MARKETPLACE_FLOOR_PRICE": "The floor price for {{marketplaces}} is currently {{sats}} Sats/{{symbol}}", "NO_FLOOR_PRICE": "There is no floor price for {{symbol}}", "TOTAL_LISTED": "Total listed", "TOTAL_RECEIVED_IF_SOLD": "Total received if sold", @@ -937,19 +1033,34 @@ "BELOW_FLOOR_PRICE_LABEL": "This listing is {{percentage}}% below floor price", "ABOVE_FLOOR_PRICE_LABEL": "This listing is {{percentage}}% above floor price", "LISTING_PRICE_TOO_LOW": "Bundle price must be > 10000 sats", - "LISTING_PRICE_TOO_HIGH": "Your bundle price price must be < 10 BTC", + "LISTING_PRICE_TOO_HIGH": "Bundle price must be < 5 BTC", "EDIT": "Edit", "MIN_PRICE_LABEL": "The listing price per {{symbol}} must be at least {{price}} sats", "NO_LISTED_ITEMS": "There're no listed items for this rune.", "NO_UNLISTED_ITEMS": "There're no unlisted items for this rune." }, + "UNLIST_RUNE_SCREEN": { + "AMOUNT": "Amount", + "BUNDLE": "Bundle", + "LISTED_ON": "Listed on", + "LISTING": "Listing", + "DELIST": "Delist", + "LOWEST_LISTING": "Lowest listing:", + "UNISAT_DIALOG": { + "TITLE": "Delist on Unisat", + "DESCRIPTION": "Delisting on Unisat will perform on-chain delisting for your runes bundle on all marketplaces. The transaction needs to confirm before you can relist.", + "BACK": "Back", + "CONTINUE": "Continue", + "CALLOUT": "This transaction will perform on-chain delisting for your runes on all marketplaces and has to confirm before you can list again." + } + }, "COIN_DASHBOARD_SCREEN": { "SEND": "Send", "RECEIVE": "Receive", "SWAP": "Swap", "BUY": "Buy", "LIST": "List", - "TRANSACTION_HISTORY_TITLE": "Transaction history", + "TRANSACTION_HISTORY_TITLE": "Transactions", "TRANSACTIONS_LIST_EMPTY": "No transactions found.", "TRANSACTIONS_LIST_ERROR": "Error fetching transaction history for this token", "TRANSACTION_SENT": "Sent", @@ -983,6 +1094,8 @@ "BALANCE": "Balance", "TRANSACTIONS": "TRANSACTIONS", "CONTRACT": "CONTRACT", + "BUNDLES": "BUNDLES", + "BREAKDOWN": "BREAKDOWN", "COMING_SOON": "Coming soon", "VERIFY_ADDRESS_ON_LEDGER": "Verify address on Ledger", "VIEW_ADDRESS": "View address", @@ -1066,8 +1179,8 @@ "SIGNING_WARNING": "By signing this message, you prove that you are the owner of an account without broadcasting any on chain transactions. This signature cannot be used to send transactions on your behalf. You should only sign messages you trust.", "SIGNING_ADDRESS_TITLE": "Signing with address", "SIGNING_ADDRESS_STX": "Your Stacks address", - "SIGNING_ADDRESS_SEGWIT": "Your Bitcoin payment address", - "SIGNING_ADDRESS_TAPROOT": "Your Ordinals address", + "SIGNING_ADDRESS_PAYMENT": "Your Bitcoin payment address", + "SIGNING_ADDRESS_ORDINALS": "Your Ordinals address", "SIGN_BUTTON": "Sign", "CANCEL_BUTTON": "Cancel", "SIGNATURE_ERROR_TITLE": "Signature Error", @@ -1084,7 +1197,8 @@ "ERROR_SUBTITLE_DEVICE_LOCKED": "Your Ledger device is locked. Please unlock it to continue." }, "CONFIRM": { - "TITLE": "Confirm the signature", + "TITLE": "Confirm the transaction", + "TITLE_WITH_COUNT": "Confirm the transaction ({{current}} of {{total}})", "SUBTITLE": "Authorise the signature on your device to continue.", "ERROR_TITLE": "Error requesting signature", "ERROR_SUBTITLE": "The signature request encountered an error. Please check that your Ledger Bitcoin app is version 2.1.3 or newer.", @@ -1349,7 +1463,7 @@ "SELECT_BUNDLES": "Select bundles to proceed with the swap", "SWAP_FROM": "Swap from", "SWAP_TO": "Swap to", - "LIST_YOUR_RUNES": "List your Runes on Magic Eden", + "LIST_YOUR_RUNES": "List your Runes on a marketplace", "SLIPPAGE_WARNING": "Your transaction may be frontrun and result in an unfavorable trade", "BAD_QUOTE_WARNING_TITLE": "The quote you are receiving may result in significant value loss.", "BAD_QUOTE_WARNING_DESC": "The minimum amount you will receive will result in a loss of over {{percentage}}% of your trade’s value. This is due to low liquidity in the pool, causing discrepancies in pricing. Please review the details carefully before proceeding with the swap." @@ -1468,10 +1582,17 @@ }, "COLLECTIBLE_COLLECTION_SCREEN": { "BACK_TO_GALLERY": "Back to gallery", + "BACK_TO_HIDDEN_COLLECTIBLES": "Back to hidden collectibles", "COLLECTION_FLOOR_PRICE": "Collection floor price", "EST_PORTFOLIO_VALUE": "Est. portfolio value", "COLLECTION": "Collection", "LOAD_MORE": "Load more", + "STAR_COLLECTION": "Collection starred", + "UNSTAR_COLLECTION": "Collection un-starred", + "HIDE_COLLECTION": "Hide collection", + "UNHIDE_COLLECTION": "Unhide collection", + "COLLECTION_HIDDEN": "Collection hidden", + "COLLECTION_UNHIDDEN": "Collection un-hidden", "ERRORS": { "FAILED_TO_FETCH": "Failed to fetch data" } @@ -1479,8 +1600,12 @@ "REQUEST_ERRORS": { "INVALID_REQUEST": "Invalid request", "MISSING_ARGUMENTS": "Contract function call missing arguments", - "ADDRESS_MISMATCH": "There’s a mismatch between your signing address and the address you’re logged with.", - "NETWORK_MISMATCH": "There’s a mismatch between your active network and the network you’re logged with.", + "ADDRESS_MISMATCH_TITLE": "Address Mismatch", + "ADDRESS_MISMATCH": "The app is requesting a signature from an address that doesn't match the account currently active in your wallet.\n\nTo fix this:\n- Make sure the account in Xverse matches the one connected to the app\n- Try reconnecting to the app", + "ADDRESS_TYPE_MISMATCH_TITLE": "Address Type Mismatch", + "ADDRESS_TYPE_MISMATCH": "The app is requesting a signature from a Bitcoin payment address type that doesn't match the one currently active in your wallet.\n\nTo fix this:\n- Make sure the Bitcoin payment address type (Native SegWit or Nested SegWit) selected in Xverse matches the app\n- Try reconnecting to the app", + "ADDRESS_MISMATCH_STX": "The app is requesting a signature from an address that doesn't match the one currently active in your Xverse wallet.", + "NETWORK_MISMATCH": "There's a mismatch between your active network and the network you're logged in with on the app.", "PSBT_CANT_PARSE_ERROR_TITLE": "Transaction Error", "PSBT_CANT_PARSE_ERROR_DESCRIPTION": "The requested transaction is invalid and cannot be processed. Please contact the developer of the requesting app for support." } diff --git a/src/manifest.json b/src/manifest.json index 33670d842..7a6f07ffd 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -16,7 +16,6 @@ "icons": { "128": "xverse_icon.png" }, - "devtools_page": "devtools.html", "web_accessible_resources": [{ "resources": ["inpage.js"], "matches": ["*://*/*"] }], "host_permissions": ["*://*/*"], "permissions": ["storage", "tabs"], @@ -27,8 +26,7 @@ { "run_at": "document_start", "js": ["browser-polyfill.js", "content-script.js"], - "matches": ["*://*/*"], - "all_frames": true + "matches": ["*://*/*"] } ] } diff --git a/src/pages/Options/index.css b/src/pages/Options/index.css index cc87c7ff7..78a8d52ab 100644 --- a/src/pages/Options/index.css +++ b/src/pages/Options/index.css @@ -41,3 +41,18 @@ header h2 { :focus { outline: none; } + +:focus-visible { + position: relative; + border-radius: 4px; +} + +:focus-visible::after { + content: ''; + position: absolute; + inset: -2px; + outline: 1px solid #ffffff; + border: 2px solid #ee7a30; + border-radius: inherit; + pointer-events: none; +} diff --git a/src/pages/Popup/index.css b/src/pages/Popup/index.css index f74e9369a..6acc83c91 100644 --- a/src/pages/Popup/index.css +++ b/src/pages/Popup/index.css @@ -28,3 +28,18 @@ body { :focus { outline: none; } + +:focus-visible { + position: relative; + border-radius: 4px; +} + +:focus-visible::after { + content: ''; + position: absolute; + inset: -2px; + outline: 1px solid #ffffff; + border: 2px solid #ee7a30; + border-radius: inherit; + pointer-events: none; +} diff --git a/src/theme/index.ts b/src/theme/index.ts index 3e01d0118..c02cb0907 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -95,6 +95,9 @@ const Theme = { lilac: '#5E41C5', lilac_dark: '#4F34BA', + // utility + transparent: 'rgba(0, 0, 0, 0)', + action: { /** * @deprecated diff --git a/tests/fixtures/base.ts b/tests/fixtures/base.ts index fc3da8f9f..7a50fc870 100644 --- a/tests/fixtures/base.ts +++ b/tests/fixtures/base.ts @@ -32,7 +32,7 @@ export const test = baseTest.extend<{ // Xverse opens the landing page on install. Wait for it to load and // use it as the main page by closing the default page. // we give it 5 secs to start up - const MAX_STARTUP_TIME = 5000; + const MAX_STARTUP_TIME = 5_000; const startTime = Date.now(); while (context.pages().length === 1) { if (Date.now() - startTime > MAX_STARTUP_TIME) { diff --git a/tests/pages/onboarding.ts b/tests/pages/onboarding.ts index e1748a0bb..8aa3915ff 100644 --- a/tests/pages/onboarding.ts +++ b/tests/pages/onboarding.ts @@ -26,8 +26,6 @@ export default class Onboarding { readonly inputPassword: Locator; - readonly errorMessage: Locator; - readonly errorMessage2: Locator; readonly errorMessageSeedPhrase: Locator; @@ -89,8 +87,7 @@ export default class Onboarding { this.buttonSeedWords = page.locator('button[value]:not([value=""])'); this.header = page.locator('#app h3'); this.inputPassword = page.locator('input[type="password"]'); - this.errorMessage = page.getByRole('heading', { name: 'Your password should be at' }); - this.errorMessage2 = page.getByRole('heading', { name: 'Please make sure your' }); + this.errorMessage2 = page.locator('p').filter({ hasText: 'Please make sure your' }); this.errorMessageSeedPhrase = page.locator('p').filter({ hasText: 'Invalid seed phrase' }); this.labelSecurityLevelWeak = page.locator('p').filter({ hasText: 'Weak' }); this.labelSecurityLevelMedium = page.locator('p').filter({ hasText: 'Medium' }); @@ -129,7 +126,7 @@ export default class Onboarding { for (let i = 0; i < (await linkList.count()); i++) { expect(await linkList.nth(i).getAttribute('href')).not.toBeNull(); } - await expect(this.page.locator('input[type="checkbox"]')).toBeVisible(); + await expect(this.page.locator('label[role="checkbox"]')).toBeVisible(); await expect(this.buttonAccept).toBeVisible(); // check links @@ -186,7 +183,6 @@ export default class Onboarding { await expect(this.inputPassword).toBeVisible(); await expect(this.buttonContinue).toBeVisible(); await expect(this.buttonContinue).toBeDisabled(); - await expect(this.errorMessage).toBeHidden(); await expect(this.labelSecurityLevelWeak).toBeHidden(); await expect(this.labelSecurityLevelMedium).toBeHidden(); await expect(this.labelSecurityLevelStrong).toBeHidden(); @@ -224,6 +220,10 @@ export default class Onboarding { } await expect(this.buttonContinue).toBeEnabled(); await this.buttonContinue.click(); + // choose the default address type between native and nested segwit + // will be the one with the most funds + await expect(this.buttonContinue).toBeEnabled(); + await this.buttonContinue.click(); await this.inputPassword.fill(password); await this.buttonContinue.click(); await this.inputPassword.fill(password); @@ -263,15 +263,6 @@ export default class Onboarding { // Fill in the password input field with the specified password. await this.inputPassword.fill(password); - // Check if an error message is expected to be visible. - if (expectations.errorMessageVisible) { - // If yes, verify that the error message element is visible. - await expect(this.errorMessage).toBeVisible(); - } else { - // If not, verify that the error message element is hidden. - await expect(this.errorMessage).toBeHidden(); - } - // Define a mapping of security levels to their corresponding label elements. const visibilityChecks = { Weak: this.labelSecurityLevelWeak, diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 2b2ffe8f7..2929860c2 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -36,8 +36,6 @@ export default class Wallet { readonly buttonConfirm: Locator; - readonly buttonResetWallet: Locator; - readonly buttonDenyDataCollection: Locator; readonly buttonNetwork: Locator; @@ -214,9 +212,9 @@ export default class Wallet { readonly noFundsBTCMessage: Locator; - readonly buttonCoinContract: Locator; + readonly coinSecondaryButton: Locator; - readonly coinContractContainer: Locator; + readonly coinSecondaryContainer: Locator; readonly coinContractAddress: Locator; @@ -268,7 +266,7 @@ export default class Wallet { readonly labelOwnedBy: Locator; - readonly labelRareSats: Locator; + readonly labelBundle: Locator; readonly buttonSupportRarity: Locator; @@ -385,7 +383,6 @@ export default class Wallet { this.buttonMenu = page.getByRole('button', { name: 'Open Header Options' }); this.buttonLock = page.getByRole('button', { name: 'Lock' }); this.buttonConfirm = page.getByRole('button', { name: 'Confirm' }); - this.buttonResetWallet = page.getByRole('button', { name: 'Reset Wallet' }); this.buttonDenyDataCollection = page.getByRole('button', { name: 'Deny' }); this.labelBalanceAmountSelector = page.getByTestId('balance-label'); this.buttonClose = page.getByRole('button', { name: 'Close' }); @@ -428,9 +425,9 @@ export default class Wallet { this.inputBTCURL = page.getByTestId('BTC URL'); this.inputFallbackBTCURL = page.getByTestId('Fallback BTC URL'); this.buttonUpdatePassword = page.getByRole('button', { name: 'Update Password' }); - this.errorMessage = page.getByRole('heading', { name: 'Incorrect password' }); + this.errorMessage = page.getByText(/incorrect password/i); this.headerNewPassword = page.getByRole('heading', { name: 'Enter your new password' }); - this.infoUpdatePassword = page.getByRole('heading', { name: 'Password successfully updated' }); + this.infoUpdatePassword = page.getByText(/password successfully updated/i); this.buttonCurrency = page.getByRole('button', { name: 'Fiat Currency' }); this.buttonShowSeedphrase = page.getByRole('button', { name: 'Show Seedphrase' }); this.selectCurrency = page.getByTestId('currency-button'); @@ -438,9 +435,9 @@ export default class Wallet { // Token this.labelCoinTitle = page.getByLabel('Coin Title'); - this.checkboxToken = page.locator('input[type="checkbox"]'); - this.checkboxTokenActive = page.locator('input[type="checkbox"]:checked'); - this.checkboxTokenInactive = page.locator('input[type="checkbox"]:not(:checked)'); + this.checkboxToken = page.locator('label[role="checkbox"]'); + this.checkboxTokenActive = page.locator('label[role="checkbox"][aria-checked="true"]'); + this.checkboxTokenInactive = page.locator('label[role="checkbox"][aria-checked="false"]'); this.buttonSip10 = page.getByRole('button', { name: 'SIP-10' }); this.buttonBRC20 = page.getByRole('button', { name: 'BRC-20' }); this.buttonRunes = page.getByRole('button', { name: 'RUNES' }); @@ -455,14 +452,14 @@ export default class Wallet { this.containerTransactionHistory = page.getByTestId('transaction-container'); this.transactionHistoryAmount = page.getByTestId('transaction-amount'); this.transactionHistoryInfo = page.getByTestId('transaction-info'); - this.buttonCoinContract = page.getByTestId('coin-contract-button'); - this.coinContractContainer = page.getByTestId('coin-contract-container'); + this.coinSecondaryButton = page.getByTestId('coin-secondary-button'); + this.coinSecondaryContainer = page.getByTestId('coin-secondary-container'); this.coinContractAddress = page.getByTestId('coin-contract-address'); this.textCoinTitle = page.getByTestId('coin-title-text'); // Collectibles this.totalItem = page.getByTestId('total-items'); - this.tabsCollectiblesItems = page.getByTestId('tab-list').locator('li'); + this.tabsCollectiblesItems = page.getByTestId('tab-list').locator('button'); this.containerRareSats = page.getByTestId('rareSats-container'); this.nameInscription = page.getByTestId('inscription-name'); this.containersCollectibleItem = page.getByTestId('collection-container'); @@ -490,9 +487,9 @@ export default class Wallet { this.buttonShare = page.getByRole('button', { name: 'Share' }); this.buttonReceive = page.getByRole('button', { name: 'Receive', exact: true }); this.buttonOpenOrdinalViewer = page.getByRole('button', { name: 'Open in Ordinal Viewer' }); + this.labelBundle = page.locator('h1').filter({ hasText: 'Bundle' }); this.labelSatsValue = page.locator('h1').filter({ hasText: 'Sats value' }); this.labelOwnedBy = page.locator('h1').filter({ hasText: 'Owned by' }); - this.labelRareSats = page.locator('p').filter({ hasText: 'Rare Sats' }); this.buttonSupportRarity = page.getByRole('button', { name: 'See supported rarity scale' }); this.numberInscription = page.getByTestId('inscription-number'); this.numberOrdinal = page.getByTestId('ordinal-number'); @@ -633,18 +630,18 @@ export default class Wallet { await expect(this.labelAccountName).toBeVisible(); await expect(this.buttonMenu).toBeVisible(); - await expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); + expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); await expect(this.navigationDashboard).toBeVisible(); await expect(this.navigationNFT).toBeVisible(); await expect(this.navigationStacking).toBeVisible(); await expect(this.navigationExplore).toBeVisible(); await expect(this.navigationSettings).toBeVisible(); - await expect(await this.divTokenRow.count()).toBeGreaterThan(1); + expect(await this.divTokenRow.count()).toBeGreaterThan(1); } async checkVisualsSendSTXPage3() { - await expect(this.page.url()).toContain('confirm-stx-tx'); + expect(this.page.url()).toContain('confirm-stx-tx'); await expect(this.buttonConfirm).toBeVisible(); await expect(this.buttonCancel).toBeVisible(); await expect(this.receiveAddress).toBeVisible(); @@ -658,10 +655,10 @@ export default class Wallet { * Checks the visibility and state of UI elements state on first page in Send Flow * * @param {string} url - The expected URL to validate the correct page navigation. - * @param {boolean} isSTX - Optional flag to apply STX-specific element checks (default: false). + * @param {boolean} moreInputFields - (default: false). */ async checkVisualsSendPage1(url: string, moreInputFields: boolean = false) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); @@ -685,7 +682,7 @@ export default class Wallet { * @param {boolean} isSTX - Indicates if the page is STX-specific; adjusts element checks accordingly (default: false). */ async checkVisualsSendPage2(url: string, isSTX: boolean = false) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonNext).toBeVisible(); await expect(this.buttonNext).toBeDisabled(); @@ -725,7 +722,7 @@ export default class Wallet { tokenImageShown: boolean = true, ordinalNumber?: string, ) { - await expect(this.page.url()).toContain(url); + expect(this.page.url()).toContain(url); await expect(this.buttonExpand).toBeVisible(); await expect(this.buttonCancel).toBeEnabled(); await expect(this.buttonConfirm).toBeEnabled(); @@ -752,29 +749,27 @@ export default class Wallet { // Execute these checks only if sendAddress is provided if (sendAddress) { await expect(this.sendAddress.first()).toBeVisible(); - await expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); + expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); } // Execute these checks only if recipientAddress is provided if (recipientAddress) { await expect(this.receiveAddress.first()).toBeVisible(); - await expect(await this.receiveAddress.first().innerText()).toContain( - recipientAddress.slice(-4), - ); + expect(await this.receiveAddress.first().innerText()).toContain(recipientAddress.slice(-4)); } // Collection Inscriptions don't have the ordinal number displayed in the Review // Check if the right ordinal number is shown if (ordinalNumber) { const reviewNumberOrdinal = await this.numberInscription.first().innerText(); - await expect(ordinalNumber).toMatch(reviewNumberOrdinal); + expect(ordinalNumber).toMatch(reviewNumberOrdinal); } } // Check Visuals of Rune Dashboard (without List button), return balance amount - async checkVisualsRunesDashboard(runeName) { + async checkVisualsRunesDashboard(runeName: string) { await expect(this.imageToken.first()).toBeVisible(); await expect(this.textCoinTitle).toBeVisible(); - await expect(await this.textCoinTitle).toContainText(runeName); + await expect(this.textCoinTitle).toContainText(runeName); await expect(this.coinBalance).toBeVisible(); await expect(this.buttonReceive).toBeVisible(); await expect(this.buttonSend).toBeVisible(); @@ -789,11 +784,11 @@ export default class Wallet { await expect(this.buttonSetPrice).toBeVisible(); await expect(this.buttonSetPrice).toBeDisabled(); await expect(this.runeItem.first()).toBeVisible(); - await expect(await this.runeItem.count()).toBeGreaterThanOrEqual(1); + expect(await this.runeItem.count()).toBeGreaterThanOrEqual(1); } async checkVisualsSwapPage() { - await expect(this.page.url()).toContain('swap'); + expect(this.page.url()).toContain('swap'); await expect(this.buttonDownArrow.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeVisible(); await expect(this.buttonGetQuotes.first()).toBeDisabled(); @@ -801,8 +796,8 @@ export default class Wallet { await expect(this.swapTokenBalance).toContainText('--'); await expect(this.buttonBack).toBeVisible(); await expect(this.nameToken.first()).toContainText('Select asset'); - await expect(await this.nameToken).toHaveCount(2); - await expect(await this.buttonDownArrow).toHaveCount(2); + await expect(this.nameToken).toHaveCount(2); + await expect(this.buttonDownArrow).toHaveCount(2); await expect(this.buttonGetQuotes).toBeVisible(); await expect(this.textUSD).toBeVisible(); await expect(this.buttonSwapToken).toBeVisible(); @@ -816,7 +811,7 @@ export default class Wallet { const usdAmount = await this.textUSD.innerText(); const numericUSDValue = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); - await expect(numericUSDValue).toBeGreaterThan(0); + expect(numericUSDValue).toBeGreaterThan(0); return numericUSDValue; } @@ -826,8 +821,8 @@ export default class Wallet { async checkVisualsQuotePage( tokenName: string, slippage: boolean, - numericQuoteValue, - numericUSDValue, + numericQuoteValue: number, + numericUSDValue: number, ) { await expect(this.buttonSwap).toBeVisible(); await expect(this.buttonEditFee).toBeVisible(); @@ -835,7 +830,7 @@ export default class Wallet { await expect(this.buttonSlippage).toBeVisible(); } // Only 2 token should be visible - await expect(await this.buttonSwapPlace.count()).toBe(2); + expect(await this.buttonSwapPlace.count()).toBe(2); // await expect(await this.imageToken.count()).toBe(2); // Check Rune token name @@ -844,18 +839,18 @@ export default class Wallet { // Check if USD amount from quote page is the same as from th swap start flow page const usdAmountQuote = await this.textUSD.first().innerText(); const numericUSDQuote = parseFloat(usdAmountQuote.replace(/[^0-9.]/g, '')); - await expect(numericUSDQuote).toEqual(numericUSDValue); + expect(numericUSDQuote).toEqual(numericUSDValue); // min-received-amount value should be the same as quoteAmount const minReceivedAmount = await this.minReceivedAmount.innerText(); const numericMinReceivedAmount = parseFloat(minReceivedAmount.replace(/[^0-9.]/g, '')); const formattedNumericMinReceivedAmount = parseFloat(numericMinReceivedAmount.toFixed(3)); - await expect(formattedNumericMinReceivedAmount).toEqual(numericQuoteValue); + expect(formattedNumericMinReceivedAmount).toEqual(numericQuoteValue); // check if quoteAmount is the same from the page before const quoteAmount2Page = await this.quoteAmount.last().innerText(); const numericQuote2Page = parseFloat(quoteAmount2Page.replace(/[^0-9.]/g, '')); - await expect(numericQuote2Page).toEqual(numericQuoteValue); + expect(numericQuote2Page).toEqual(numericQuoteValue); } async checkVisualsListOnMEPage() { @@ -882,7 +877,7 @@ export default class Wallet { // Save the current fee amount for comparison const originalFee = await this.feeAmount.innerText(); const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); - await expect(numericOriginalFee).toBeGreaterThan(0); + expect(numericOriginalFee).toBeGreaterThan(0); let feePriority = 'Medium'; if (feePriorityShown) { feePriority = await this.labelFeePriority.innerText(); @@ -899,7 +894,7 @@ export default class Wallet { .locator(this.labelTotalFee) .innerText(); const numericFee = parseFloat(fee.replace(/[^0-9.]/g, '')); - await expect(numericFee).toBe(numericOriginalFee); + expect(numericFee).toBe(numericOriginalFee); // Save high fee rate for comparison const highFee = await this.labelTotalFee.first().innerText(); @@ -910,12 +905,12 @@ export default class Wallet { const newFee = await this.feeAmount.innerText(); const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); - await expect(numericNewFee).toBe(numericHighFee); + expect(numericNewFee).toBe(numericHighFee); } async navigateToCollectibles() { await this.navigationNFT.click(); - await expect(this.page.url()).toContain('nft-dashboard'); + expect(this.page.url()).toContain('nft-dashboard'); // If 'enable' rare sats pop up is appearing if (await this.buttonEnable.isVisible()) { await this.buttonEnable.click(); @@ -926,32 +921,30 @@ export default class Wallet { } // had to disable this rule as my first assertion was always changed to a wrong assertion - /* eslint-disable playwright/prefer-web-first-assertions */ async checkAmountsSendingSTX(amountSTXSend, STXTest, sendFee) { - await expect(await this.receiveAddress.first().innerText()).toContain(STXTest.slice(-4)); + expect(await this.receiveAddress.first().innerText()).toContain(STXTest.slice(-4)); // Sending amount without Fee const sendAmount = await this.confirmAmount.first().innerText(); const numericValueSendAmount = parseFloat(sendAmount.replace(/[^0-9.]/g, '')); - await expect(numericValueSendAmount).toEqual(amountSTXSend); + expect(numericValueSendAmount).toEqual(amountSTXSend); // Fees const fee = await this.feeAmount.innerText(); const numericValueFee = parseFloat(fee.replace(/[^0-9.]/g, '')); - await expect(numericValueFee).toEqual(sendFee); + expect(numericValueFee).toEqual(sendFee); } - /* eslint-disable playwright/prefer-web-first-assertions */ async checkAmountsSendingBTC(selfBTCTest, BTCTest, amountBTCSend) { // Sending amount without Fee const amountText = await this.confirmAmount.first().innerText(); const numericValueAmountText = parseFloat(amountText.replace(/[^0-9.]/g, '')); - await expect(numericValueAmountText).toEqual(amountBTCSend); + expect(numericValueAmountText).toEqual(amountBTCSend); // Address check sending and receiving - await expect(await this.sendAddress.innerText()).toContain(selfBTCTest.slice(-4)); - await expect(await this.receiveAddress.first().innerText()).toContain(BTCTest.slice(-4)); + expect(await this.sendAddress.innerText()).toContain(selfBTCTest.slice(-4)); + expect(await this.receiveAddress.first().innerText()).toContain(BTCTest.slice(-4)); const confirmAmountAfter = await this.confirmAmount.last().innerText(); const originalFee = await this.feeAmount.innerText(); @@ -966,7 +959,7 @@ export default class Wallet { // Balance - fees - sending amount const roundedResult = Number((num3 - num2 - amountBTCSend).toFixed(9)); // Check if Balance value after the transaction is the same as the calculated value - await expect(num1).toEqual(roundedResult); + expect(num1).toEqual(roundedResult); } async confirmSendTransaction(transactionIDShown: boolean = true) { @@ -979,7 +972,7 @@ export default class Wallet { await this.buttonClose.click(); } - async getAddress(whichAddress): Promise { + async getAddress(whichAddress: string): Promise { // click on 'Receive' button await this.allUpperButtons.nth(1).click(); @@ -997,7 +990,7 @@ export default class Wallet { return address; } - async getTokenBalance(tokenname) { + async getTokenBalance(tokenname: string) { const locator = this.page .getByRole('button') .filter({ has: this.labelTokenSubtitle.getByText(tokenname, { exact: true }) }) @@ -1008,14 +1001,14 @@ export default class Wallet { return numericValue; } - async clickOnSpecificToken(tokenname) { + async clickOnSpecificToken(tokenname: string) { const specificToken = this.page .getByRole('button') .filter({ has: this.labelTokenSubtitle.getByText(tokenname, { exact: true }) }); await specificToken.last().click(); } - async clickOnSpecificInscription(inscriptionName) { + async clickOnSpecificInscription(inscriptionName: string) { const specificToken = this.containersCollectibleItem .filter({ has: this.nameInscription.getByText(inscriptionName, { exact: true }), @@ -1025,7 +1018,7 @@ export default class Wallet { } // This function tries to click on a specific rune, if the rune is not enabled it will enable the test rune and then click on it - async checkAndClickOnSpecificRune(tokenname) { + async checkAndClickOnSpecificRune(tokenname: string) { // Check if test rune is enabled and if not enabled the test rune try { // click on the test rune @@ -1049,7 +1042,7 @@ export default class Wallet { 'No active token checkbox found or there are multiple, taking alternative action.', ); // Activate Test rune - await this.runeSKIBIDI.locator('div.react-switch-handle').click(); + await this.runeSKIBIDI.locator('label[role="checkbox"]').click(); } else { console.log('One active token checkbox is present.'); } @@ -1069,7 +1062,7 @@ export default class Wallet { await expect(this.inputFallbackBTCURL).toBeVisible(); } - async checkTestnetUrls(shouldContainTestnet) { + async checkTestnetUrls(shouldContainTestnet: boolean) { const inputsURL = [this.inputStacksURL, this.inputBTCURL, this.inputFallbackBTCURL]; const checks = inputsURL.map(async (input) => { const inputValue = await input.inputValue(); @@ -1100,6 +1093,10 @@ export default class Wallet { await this.checkTestnetUrls(true); + // TODO think of a better way to do this + // Wait for the network to be switched so that API doesn't fail because of the rate limiting + await this.page.waitForTimeout(15000); + await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); @@ -1119,6 +1116,10 @@ export default class Wallet { await this.checkTestnetUrls(false); + // TODO think of a better way to do this + // Wait for the network to be switched so that API doesn't fail because of the rate limiting + await this.page.waitForTimeout(15000); + await this.buttonSave.click(); await expect(this.buttonNetwork).toBeVisible({ timeout: 30000 }); await expect(this.buttonNetwork).toHaveText('NetworkMainnet'); @@ -1153,30 +1154,21 @@ export default class Wallet { const balanceText = await this.labelCoinBalanceCurrency.innerText(); totalBalance = parseFloat(balanceText.replace(/[^\d.-]/g, '')); } - // Check if total balance of all tokens is the same as total wallet balance - const totalBalanceText = await this.balance.innerText(); - const totalBalanceWallet = parseFloat(totalBalanceText.replace(/[^\d.-]/g, '')); - await expect(totalBalanceWallet).toBe(totalBalance); return totalBalance; } - // The enableRandomToken function takes a parameter tokenType which can either be ‘BRC20’ or ‘SIP10’. This parameter determines additional actions specific to BRC20 tokens. - async enableRandomToken(tokenType: 'BRC20' | 'SIP10'): Promise { + async selectLastToken(tokenType: 'BRC20' | 'SIP10'): Promise { await this.manageTokenButton.click(); - await expect(this.page.url()).toContain('manage-tokens'); + expect(this.page.url()).toContain('manage-tokens'); // Click on the specific token type button if BRC20 is selected if (tokenType === 'BRC20') { await this.buttonBRC20.click(); } - // Enable a random token - const tokenName = await this.toggleRandomToken(true); - - // Navigate back and verify the token is visible + const chosenToken = this.divTokenRow.last(); + const tokenName = (await chosenToken.getAttribute('data-testid')) || 'default-value'; await this.buttonBack.click(); - await expect(this.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - return tokenName; } @@ -1196,7 +1188,7 @@ export default class Wallet { const tokenName = (await chosenToken.getAttribute('data-testid')) || 'default-value'; // Click the switch handle to toggle the token's state - await chosenToken.locator('div.react-switch-handle').click(); + await chosenToken.locator('label[role="checkbox"]').click(); return tokenName; } @@ -1212,7 +1204,7 @@ export default class Wallet { for (let i = 0; i < count; i++) { // Since clicking the switch will change its state, always interact with the first one - await actionTokens.first().locator('div.react-switch-handle').click(); + await actionTokens.first().locator('label[role="checkbox"]').click(); } } diff --git a/tests/specs/createWallet.spec.ts b/tests/specs/createWallet.spec.ts index d38d57bfc..b145d6350 100644 --- a/tests/specs/createWallet.spec.ts +++ b/tests/specs/createWallet.spec.ts @@ -90,16 +90,21 @@ test.describe('Create and Restore Wallet Flow', () => { // Write the file fs.writeFileSync(filePathAddresses, dataAddress, 'utf8'); }); - await test.step('reset Wallet via Menu', async () => { - await expect(wallet.buttonMenu).toBeVisible(); - await wallet.buttonMenu.click(); - await expect(wallet.buttonResetWallet).toBeVisible(); - await wallet.buttonResetWallet.click(); - await wallet.buttonResetWallet.click(); - await expect(onboardingPage.inputPassword).toBeVisible(); + + await test.step('Reset Wallet via Settings', async () => { + // Go to Settings -> Security + await wallet.navigationSettings.click(); + await wallet.buttonSecurity.click(); + + // Confirm reset + await page.getByRole('button', { name: 'Reset Wallet' }).first().click(); + await page.getByRole('dialog').getByRole('button', { name: 'Reset Wallet' }).click(); + + // Enter password to confirm reset await onboardingPage.inputPassword.fill(strongPW); await onboardingPage.buttonContinue.click(); }); + await test.step('Restore wallet with 12 word seed phrase', async () => { const landingPage = new Landing(page); await expect(landingPage.buttonRestoreWallet).toBeVisible(); @@ -124,6 +129,11 @@ test.describe('Create and Restore Wallet Flow', () => { } await expect(onboardingPage2.buttonContinue).toBeEnabled(); await onboardingPage2.buttonContinue.click(); + + // address type screen (native/nested), we'll just continue with the default + await expect(onboardingPage2.buttonContinue).toBeEnabled(); + await onboardingPage2.buttonContinue.click(); + await onboardingPage2.inputPassword.fill(strongPW); await onboardingPage2.buttonContinue.click(); await onboardingPage2.inputPassword.fill(strongPW); @@ -137,7 +147,7 @@ test.describe('Create and Restore Wallet Flow', () => { await newWallet.checkVisualsStartpage(); const balanceText = newWallet.balance; - await await expect(balanceText).toHaveText('$0.00'); + await expect(balanceText).toHaveText('$0.00'); // Get the Addresses const addressBitcoinCheck = await newWallet.getAddress('Bitcoin'); diff --git a/tests/specs/managementAccount.spec.ts b/tests/specs/managementAccount.spec.ts index 03afbfded..20d8a5286 100644 --- a/tests/specs/managementAccount.spec.ts +++ b/tests/specs/managementAccount.spec.ts @@ -12,7 +12,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await expect(wallet.buttonGenerateAccount).toBeVisible(); await expect(wallet.buttonConnectHardwareWallet).toBeVisible(); @@ -32,7 +32,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonAccountOptions.click(); await expect(wallet.buttonRenameAccount).toBeVisible(); @@ -65,7 +65,7 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonAccountOptions.click(); await expect(wallet.buttonRenameAccount).toBeVisible(); @@ -93,14 +93,14 @@ test.describe('Account Management', () => { await page.goto(`chrome-extension://${extensionId}/popup.html`); await wallet.checkVisualsStartpage(); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonGenerateAccount.click(); await expect(wallet.labelAccountName).toHaveCount(2); await expect(wallet.buttonAccountOptions).toHaveCount(2); await expect(wallet.accountBalance).toHaveCount(2); const balanceText = await wallet.getBalanceOfAllAccounts(); - await expect(balanceText).toBe(0); + expect(balanceText).toBe(0); }); test('Switch to another account and switch back', async ({ page, extensionId }) => { @@ -111,12 +111,12 @@ test.describe('Account Management', () => { await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 1'); await wallet.labelAccountName.click(); - await expect(page.url()).toContain('account-list'); + expect(page.url()).toContain('account-list'); await expect(wallet.labelAccountName).toHaveCount(1); await wallet.buttonGenerateAccount.click(); await expect(wallet.labelAccountName).toHaveCount(2); const balanceText = await wallet.getBalanceOfAllAccounts(); - await expect(balanceText).toBe(0); + expect(balanceText).toBe(0); await wallet.labelAccountName.last().click(); await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 2'); diff --git a/tests/specs/managementToken.spec.ts b/tests/specs/managementToken.spec.ts index 154ac4e66..69ded0ed7 100644 --- a/tests/specs/managementToken.spec.ts +++ b/tests/specs/managementToken.spec.ts @@ -13,278 +13,143 @@ test.describe('Token Management', () => { await wallet.checkVisualsStartpage(); await expect(wallet.balance).toHaveText('$0.00'); await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); + expect(page.url()).toContain('manage-tokens'); await expect(wallet.buttonBack).toBeVisible(); await expect(wallet.buttonSip10).toBeVisible(); await expect(wallet.buttonBRC20).toBeVisible(); await expect(wallet.buttonRunes).toBeVisible(); await expect(wallet.headingTokens).toBeVisible(); - // Check SIP10 token - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - - await await expect(amounttokenSIP).toBeGreaterThanOrEqual(15); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 1); + // Check SIP10 token tab - only Stacks should be showing when user has no sip10 balances + await wallet.buttonSip10.click(); + await expect(wallet.labelCoinTitle).toHaveCount(1); + await expect(wallet.checkboxToken).toHaveCount(1); await expect(wallet.checkboxTokenActive).toHaveCount(1); - await expect(wallet.checkboxToken).toHaveCount(amounttokenSIP); + await expect(wallet.checkboxTokenInactive).toHaveCount(0); - // Check BRC20 token + // Check BRC20 token tab - nothing shows when user has no brc20 balances await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await expect(amounttokenBRC20).toBeGreaterThanOrEqual(8); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20); + await expect(wallet.labelCoinTitle).toHaveCount(0); + await expect(wallet.checkboxToken).toHaveCount(0); + await expect(wallet.checkboxTokenInactive).toHaveCount(0); await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxToken).toHaveCount(amounttokenBRC20); - // Check rune token + // Check rune token tab - nothing shows when user has no runes balances await wallet.buttonRunes.click(); await expect(wallet.labelCoinTitle).toHaveCount(0); + await expect(wallet.checkboxToken).toHaveCount(0); await expect(wallet.checkboxTokenInactive).toHaveCount(0); await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxToken).toHaveCount(0); }); - test('Enable and disable some BRC-20 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + test('Toggle a BRC-20 token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - // Enable random token - const tokenName = await wallet.toggleRandomToken(true); - // Check that amount of checkboxes changed - await expect(wallet.checkboxTokenActive).toHaveCount(1); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 1); - await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); - await test.step('Enable some more token', async () => { - await wallet.manageTokenButton.click(); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - const tokenName1 = await wallet.toggleRandomToken(true); - const tokenName2 = await wallet.toggleRandomToken(true); - const tokenName3 = await wallet.toggleRandomToken(true); - await expect(wallet.checkboxTokenActive).toHaveCount(4); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 4); + // NOTE: requires an account with at least 1 brc20 token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); + + // disable a random token + const tokenName = await wallet.toggleRandomToken(false); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/brc20/tokens'), + ); await wallet.buttonBack.click(); - // new enabled tokens should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName1, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName2, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName3, { exact: true })).toBeVisible(); - }); + await fetchTokens; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - await test.step('Disable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); + // enable the token again await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - const tokenName = await wallet.toggleRandomToken(false); - await expect(wallet.checkboxTokenActive).toHaveCount(3); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20 - 3); + await page.getByTestId(tokenName).locator('label').click(); + + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/brc20/tokens'), + ); await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); - test('Enable and disable some SIP-10 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + test('Toggle a SIP-10 token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - // Enable random token - const tokenName = await wallet.toggleRandomToken(true); - // Check that amount of checkboxes changed - await expect(wallet.checkboxTokenActive).toHaveCount(2); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 2); - await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await wallet.buttonSip10.click(); - await test.step('Enable some more token', async () => { - await wallet.manageTokenButton.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - const tokenName1 = await wallet.toggleRandomToken(true); - const tokenName2 = await wallet.toggleRandomToken(true); - const tokenName3 = await wallet.toggleRandomToken(true); - await expect(wallet.checkboxTokenActive).toHaveCount(5); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 5); - await wallet.buttonBack.click(); - // new enabled tokens should be visible on dashboard - await expect(wallet.labelTokenSubtitle.getByText(tokenName1, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName2, { exact: true })).toBeVisible(); - await expect(wallet.labelTokenSubtitle.getByText(tokenName3, { exact: true })).toBeVisible(); - }); + // NOTE: requires an account with at least 1 sip10 token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); - await test.step('Disable a random token', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); + // disable a random token const tokenName = await wallet.toggleRandomToken(false); - await expect(wallet.checkboxTokenActive).toHaveCount(4); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP - 4); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/sip10/tokens'), + ); await wallet.buttonBack.click(); - // new enabled token should be visible on dashboard + await fetchTokens; await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); - }); - test('Enable and disable all SIP-10 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); - const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); - - await test.step('Enable a all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); + // enable the token again await wallet.manageTokenButton.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - await expect(page.url()).toContain('manage-tokens'); - await expect(wallet.checkboxToken).toHaveCount(amounttokenSIP); - await wallet.toggleAllTokens(true); - await expect(wallet.checkboxTokenActive).toHaveCount(amounttokenSIP); - await expect(wallet.checkboxTokenInactive).toHaveCount(0); - await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(amounttokenSIP + 1); - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await wallet.buttonSip10.click(); + await page.getByTestId(tokenName).locator('label').click(); - await test.step('Disable all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.toggleAllTokens(false); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenSIP = await wallet.labelCoinTitle.count(); - await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenSIP); + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/sip10/tokens'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(1); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); - test('Enable and disable all BRC-20 token', async ({ page, extensionId }) => { - const onboardingPage = new Onboarding(page); + + test('Toggle a Runes token', async ({ page, extensionId }) => { const wallet = new Wallet(page); - await onboardingPage.createWalletSkipBackup(strongPW); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - await test.step('Enable a all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - let balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); + await test.step('Toggle a random token', async () => { await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenInactive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await expect(wallet.checkboxToken).toHaveCount(amounttokenBRC20); - await wallet.toggleAllTokens(true); - await expect(wallet.checkboxTokenActive).toHaveCount(amounttokenBRC20); - await expect(wallet.checkboxTokenInactive).toHaveCount(0); + await wallet.buttonRunes.click(); + + // NOTE: requires an account with at least 1 runes token with balance + await expect(wallet.checkboxTokenActive.first()).toBeVisible(); + + // disable a random token + const tokenName = await wallet.toggleRandomToken(false); + + // expect token to be hidden on dashboard + const fetchTokens = page.waitForResponse((response) => + response.url().includes('/runes/fiat-rates'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(amounttokenBRC20 + 2); - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); - }); + await fetchTokens; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeHidden(); - await test.step('Disable all tokens', async () => { - await page.goto(`chrome-extension://${extensionId}/popup.html`); - await wallet.checkVisualsStartpage(); + // enable the token again await wallet.manageTokenButton.click(); - await expect(page.url()).toContain('manage-tokens'); - await wallet.buttonBRC20.click(); - await expect(wallet.checkboxTokenActive.first()).toBeVisible(); - const amounttokenBRC20 = await wallet.labelCoinTitle.count(); - await wallet.toggleAllTokens(false); - await expect(wallet.checkboxTokenActive).toHaveCount(0); - await expect(wallet.checkboxTokenInactive).toHaveCount(amounttokenBRC20); + await wallet.buttonRunes.click(); + await page.getByTestId(tokenName).locator('label').click(); + + // expect to be visible again on dashboard + const fetchTokensAgain = page.waitForResponse((response) => + response.url().includes('/runes/fiat-rates'), + ); await wallet.buttonBack.click(); - await expect(wallet.labelTokenSubtitle).toHaveCount(2); - // Check balances - await expect(wallet.balance).toBeVisible(); - await expect(wallet.balance).toHaveText('$0.00'); - const balanceText = await wallet.getBalanceOfAllTokens(); - await expect(balanceText).toBe(0); + await fetchTokensAgain; + await expect(wallet.labelTokenSubtitle.getByText(tokenName, { exact: true })).toBeVisible(); }); }); }); diff --git a/tests/specs/runesList.spec.ts b/tests/specs/runesList.spec.ts index 2d5ac3cab..1a3c9213c 100644 --- a/tests/specs/runesList.spec.ts +++ b/tests/specs/runesList.spec.ts @@ -59,7 +59,7 @@ test.describe('List runes', () => { // Select custom await wallet.buttonCustomPrice.click(); - // Ste price modal appears + // Set price modal appears await expect(wallet.buttonApply).toBeVisible(); await expect(wallet.buttonApply).toBeDisabled(); await expect(wallet.inputListingPrice).toBeVisible(); @@ -81,7 +81,7 @@ test.describe('List runes', () => { const num1Currency = parseFloat(sendCurrencyAmount.replace(/[^0-9.]/g, '')); // click on continue - await wallet.buttonContinue.click(); + await page.getByRole('button', { name: 'Continue' }).click(); // Check Visuals Review transaction await expect(wallet.confirmTotalAmount).toBeVisible(); @@ -176,7 +176,7 @@ test.describe('List runes', () => { // click on the first UTXO await wallet.runeItemCheckbox.first().click(); - // Click on set price + // Click on edit price await expect(wallet.buttonSetPrice).toBeEnabled(); await wallet.buttonSetPrice.click(); diff --git a/tests/specs/runesSend.spec.ts b/tests/specs/runesSend.spec.ts index aa8493ca6..2daa4f077 100644 --- a/tests/specs/runesSend.spec.ts +++ b/tests/specs/runesSend.spec.ts @@ -41,7 +41,7 @@ test.describe('Send runes', () => { await expect(wallet.buttonInsufficientFunds).toBeDisabled(); }); - test('Cancel - send one rune testnet', async ({ page, extensionId }) => { + test('Cancel - send one rune testnet #localexecution', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); @@ -66,13 +66,18 @@ test.describe('Send runes', () => { await wallet.buttonNext.click(); await wallet.checkVisualsSendPage2('send-rune'); - await wallet.inputSendAmount.fill(price.toString()); - await expect(wallet.buttonNext).toBeEnabled(); + await page.getByRole('textbox', { name: '0' }).fill(price.toString()); + await page.getByRole('button', { name: 'Edit' }).click(); + await page.getByRole('button', { name: /custom/i }).click(); + await page.getByRole('dialog').getByPlaceholder('0').fill('3'); + await page.getByRole('button', { name: 'Apply' }).click(); + await page.getByRole('button', { name: 'Next' }).click(); const displayBalance = await wallet.labelBalanceAmountSelector.innerText(); const displayBalanceNumerical = parseFloat(displayBalance.replace(/[^0-9.]/g, '')); await expect(originalBalanceAmount).toEqual(displayBalanceNumerical); - await wallet.buttonNext.click(); + + await page.getByRole('button', { name: 'Next' }).click(); await wallet.checkVisualsSendTransactionReview( 'send-rune', @@ -124,17 +129,22 @@ test.describe('Send runes', () => { await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); - await expect(wallet.buttonNext).toBeEnabled(); - await wallet.buttonNext.click(); + await page.getByRole('button', { name: 'Next' }).click(); await wallet.checkVisualsSendPage2('send-rune'); - await wallet.inputSendAmount.fill(price.toString()); - await expect(wallet.buttonNext).toBeEnabled(); + await page.getByRole('textbox', { name: '0' }).fill('3'); + + await page.getByRole('button', { name: 'Edit' }).click(); + await page.getByRole('button', { name: /custom/i }).click(); + await page.getByRole('dialog').getByPlaceholder('0').fill('3'); + await page.getByRole('button', { name: 'Apply' }).click(); + await expect(page.getByRole('button', { name: 'Next' })).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); const displayBalance = await wallet.labelBalanceAmountSelector.innerText(); const displayBalanceNumerical = parseFloat(displayBalance.replace(/[^0-9.]/g, '')); await expect(originalBalanceAmount).toEqual(displayBalanceNumerical); - await wallet.buttonNext.click(); + await page.getByRole('button', { name: 'Next' }).click(); await wallet.checkVisualsSendTransactionReview( 'send-rune', diff --git a/tests/specs/swapExchange.spec.ts b/tests/specs/swapExchange.spec.ts index 3d63862bc..df8a167b9 100644 --- a/tests/specs/swapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -30,7 +30,7 @@ test.describe('Swap Flow Exchange', () => { // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); - await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); await wallet.divTokenRow.first().click(); await expect(wallet.nameToken.first()).not.toContainText('Select asset'); await expect(wallet.imageToken.first()).toBeVisible(); @@ -40,7 +40,7 @@ test.describe('Swap Flow Exchange', () => { await wallet.buttonDownArrow.nth(1).click(); // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); - await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); await wallet.divTokenRow.first().click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); @@ -62,7 +62,7 @@ test.describe('Swap Flow Exchange', () => { const quoteAmount = await wallet.quoteAmount.first().innerText(); const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); - await expect(numericQuoteValue).toBeGreaterThan(0); + expect(numericQuoteValue).toBeGreaterThan(0); // Click on DotSwap await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); @@ -77,14 +77,14 @@ test.describe('Swap Flow Exchange', () => { await wallet.buttonSwap.click(); await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); - await expect(await wallet.confirmAmount.count()).toBeGreaterThan(3); + expect(await wallet.confirmAmount.count()).toBeGreaterThan(3); // Confirm Amount is the same as swapAmount const swapSendAmount = await wallet.confirmAmount .filter({ hasText: swapAmount.toString() }) .innerText(); const numericValueSwap = parseFloat(swapSendAmount.replace(/[^0-9.]/g, '')); - await expect(numericValueSwap).toEqual(swapAmount); + expect(numericValueSwap).toEqual(swapAmount); // Check Rune token name await expect(wallet.nameRune).toContainText(tokenName1); @@ -98,7 +98,7 @@ test.describe('Swap Flow Exchange', () => { // Check BTC Balance after cancel the transaction const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); - await expect(initialBTCBalance).toEqual(balanceAfterCancel); + expect(initialBTCBalance).toEqual(balanceAfterCancel); }); test('Exchange token via DotSwap with standard fee testnet #localexecution', async ({ @@ -122,7 +122,7 @@ test.describe('Swap Flow Exchange', () => { // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); - await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); await wallet.divTokenRow.first().click(); await expect(wallet.nameToken.first()).not.toContainText('Select asset'); await expect(wallet.imageToken.first()).toBeVisible(); @@ -132,14 +132,14 @@ test.describe('Swap Flow Exchange', () => { await wallet.buttonDownArrow.nth(1).click(); // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); - await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); await wallet.divTokenRow.filter({ hasText: 'COOK•RUNES•ON•TESTNET' }).click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); // tried a calculated value but had multiple problems with that, for now we stick to a specific value - const swapAmount = 0.00002646; + const swapAmount = 0.00010646; const numericUSDValue = await wallet.fillSwapAmount(swapAmount); @@ -154,7 +154,7 @@ test.describe('Swap Flow Exchange', () => { const quoteAmount = await wallet.quoteAmount.first().innerText(); const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); - await expect(numericQuoteValue).toBeGreaterThan(0); + expect(numericQuoteValue).toBeGreaterThan(0); // Click on DotSwap await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); @@ -167,7 +167,7 @@ test.describe('Swap Flow Exchange', () => { // Save the current fee amount for comparison const originalFee = await wallet.feeAmount.innerText(); const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); - await expect(numericOriginalFee).toBeGreaterThan(0); + expect(numericOriginalFee).toBeGreaterThan(0); await wallet.buttonSwap.click(); await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); @@ -177,7 +177,7 @@ test.describe('Swap Flow Exchange', () => { .filter({ hasText: swapAmount.toString() }) .innerText(); const numericValueSwap = parseFloat(swapSendAmount.replace(/[^0-9.]/g, '')); - await expect(numericValueSwap).toEqual(swapAmount); + expect(numericValueSwap).toEqual(swapAmount); // Check Rune token name await expect(wallet.nameRune).toContainText(tokenName1); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts index 65ee2fd00..8e09a1292 100644 --- a/tests/specs/swapME.spec.ts +++ b/tests/specs/swapME.spec.ts @@ -11,7 +11,7 @@ test.describe('Swap Flow ME', () => { const marketplace = 'Magic Eden'; const token = 'THE•MONEY•BEES'; - test('Cancel swap token via ME', async ({ page, extensionId }) => { + test('Cancel swap token via ME #localexecution', async ({ page, extensionId }) => { // Restore wallet const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); diff --git a/tests/specs/swapSip10Velar.spec.ts b/tests/specs/swapSip10Velar.spec.ts new file mode 100644 index 000000000..111ef61b6 --- /dev/null +++ b/tests/specs/swapSip10Velar.spec.ts @@ -0,0 +1,80 @@ +import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; +import Wallet from '../pages/wallet'; + +// swap sip-10 to STX on Velar - mainnet + +test.describe('Swap sip-10 token to STX', () => { + test.beforeAll(async ({ page }) => { + await enableCrossChainSwaps(page); + }); + + test('Swap sip-10 to STX #localexecution', async ({ page, extensionId }) => { + // set up the testing wallet + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // click swap button on the homepage + await page.getByRole('button', { name: 'Swap' }).click(); + + // choose the tokens + await page + .getByRole('button', { name: /select asset/i }) + .first() + .click(); + await page.getByText('VELAR', { exact: true }).first().click(); + await expect(page.getByText(/velar/i)).toBeVisible(); + await page + .getByRole('button', { name: /select asset/i }) + .last() + .click(); + await page.getByText('STX', { exact: true }).first().click(); + + // assert chosen tokens populated the dropdown inputs velar -> stx + await expect(page.getByText(/velar/i)).toBeVisible(); + await expect(page.getByText(/stx/i)).toBeVisible(); + + // input the amount for swapping + await page.getByRole('textbox', { name: '0' }).fill('0.1'); + await expect(page.getByText(/^~\$\d+\.\d{2}\sUSD$/)).toBeVisible(); + + // assert presence balance and amount + await expect(page.getByText(/balance:/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+$/i)).toBeVisible(); + + // assert presence of max button + await expect(page.getByRole('button', { name: 'MAX' })).toBeVisible(); + + page.getByRole('button', { name: /get quotes/i }).click(); + + await expect(page.getByText('Rates', { exact: true })).toBeVisible(); + await page.getByText(/^\d+\.\d+\s+STX$/i).click(); + + // Quotes page velar -> stacks + await expect(page.getByText(/quote/i)).toBeVisible(); + await expect(page.getByText(/4%/i)).toBeVisible(); + page.getByRole('button', { name: /4%/i }).click(); + + // edit slippage tolerance + await page.getByRole('textbox', { name: '4' }).fill('1.22'); + page.getByRole('button', { name: /apply/i }).click(); + await expect(page.getByText('1.22')).toBeVisible(); + await expect(page.getByRole('img', { name: /velar logo/i })).toBeVisible(); + + page.getByRole('button', { name: /swap/i }).click(); + + // Arrive to the final step - swap contract page + await expect(page.getByText(/swap-exact-tokens-for-tokens/i)).toHaveCount(2); + await expect(page.getByText(/network fee/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+\s+STX$/i).last()).toBeVisible(); + await expect(page.getByRole('button', { name: /edit nonce/i })).toBeVisible(); + + // User clicks confirm + page.getByRole('button', { name: /confirm/i }).click(); + await expect(page.getByText(/transaction broadcasted/i)).toBeVisible(); + page.getByRole('button', { name: /close/i }).click(); + + // After closing user should arrive to homepage + await expect(page).toHaveURL(/popup\.html/); + }); +}); diff --git a/tests/specs/swapVelar.spec.ts b/tests/specs/swapVelar.spec.ts new file mode 100644 index 000000000..0c0b478ee --- /dev/null +++ b/tests/specs/swapVelar.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; +import Wallet from '../pages/wallet'; +//* swap STX for runes using Velar on mainnet(not supported on testnet) + +test.describe('Velar sip-10 swap flow', () => { + test.beforeAll(async ({ page }) => { + await enableCrossChainSwaps(page); + }); + test('Check the Velar sip-10 flow on mainnet #localexecution', async ({ page, extensionId }) => { + // set up the testing wallet + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // click the swap button and choose STX->Velar route + await page.getByRole('button', { name: 'Swap' }).click(); + + await page + .getByRole('button', { name: /select asset/i }) + .first() + .click(); + await page.getByText(/stacks/i).click(); + page + .getByRole('button', { name: /select asset/i }) + .last() + .click(); + + await page.getByText(/velar/i).first().click(); + page.getByRole('textbox', { name: '0' }).fill('0.1'); + page.getByRole('button', { name: /get quotes/i }).click(); + await expect(page.getByText('Rates', { exact: true })).toBeVisible(); + await page.getByText(/^\d+\.\d+\s+Velar$/i).click(); + + // Arrive to Quotes page + await expect(page.getByText(/quote/i)).toBeVisible(); + await expect(page.getByText(/4%/i)).toBeVisible(); + + page.getByRole('img', { name: /velar logo/i }).isVisible(); + page.getByRole('button', { name: /swap/i }).click(); + + // Arrive to the final step - swap contract page + await expect(page.getByText(/swap-exact-tokens-for-tokens/i)).toHaveCount(2); + await expect(page.getByText(/network fee/i)).toBeVisible(); + await expect(page.getByText(/^\d+\.\d+\s+STX$/i)).toBeVisible(); + await expect(page.getByRole('button', { name: /edit nonce/i })).toBeVisible(); + + // User clicks confirm + page.getByRole('button', { name: /confirm/i }).click(); + await expect(page.getByText(/transaction broadcasted/i)).toBeVisible(); + page.getByRole('button', { name: /close/i }).click(); + + // After closing user should arrive to homepage + await expect(page).toHaveURL(/popup\.html/); + }); +}); diff --git a/tests/specs/tabCollectiblesInscriptions.spec.ts b/tests/specs/tabCollectiblesInscriptions.spec.ts index 70695b810..ca55feb09 100644 --- a/tests/specs/tabCollectiblesInscriptions.spec.ts +++ b/tests/specs/tabCollectiblesInscriptions.spec.ts @@ -4,7 +4,10 @@ import Wallet from '../pages/wallet'; const TEST_ORDINALS_ADDRESS = 'tb1pprpcu07x8fd02keqx9wtfncz99fhg6hepvpw34w9l2lnazmmf7rspw96ql'; test.describe('Collectibles Tab - Inscriptions', () => { - test('Cancel send Collection Inscriptions testnet', async ({ page, extensionId }) => { + test('Cancel send Collection Inscriptions testnet #localexecution', async ({ + page, + extensionId, + }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); @@ -171,7 +174,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(await wallet.containersCollectibleItem.count()).toBeGreaterThanOrEqual(1); }); - test('Cancel send single Inscriptions testnet', async ({ page, extensionId }) => { + test('Cancel send single Inscriptions testnet #localexecution', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); diff --git a/tests/specs/tabCollectiblesRareSats.spec.ts b/tests/specs/tabCollectiblesRareSats.spec.ts index ed63df9c3..aed4cd65f 100644 --- a/tests/specs/tabCollectiblesRareSats.spec.ts +++ b/tests/specs/tabCollectiblesRareSats.spec.ts @@ -4,7 +4,7 @@ import Wallet from '../pages/wallet'; const TEST_ORDINALS_ADDRESS = 'tb1pprpcu07x8fd02keqx9wtfncz99fhg6hepvpw34w9l2lnazmmf7rspw96ql'; test.describe('Collectibles Tab - Rare sats', () => { - test('Check rare sats testnet', async ({ page, extensionId }) => { + test('Check rare sats testnet #localexecution', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); @@ -22,11 +22,11 @@ test.describe('Collectibles Tab - Rare sats', () => { await expect(wallet.buttonSend).toBeVisible(); await expect(wallet.labelSatsValue).toBeVisible(); await expect(wallet.labelOwnedBy).toBeVisible(); - await expect(wallet.labelRareSats).toBeVisible(); + await expect(wallet.labelBundle).toBeVisible(); await expect(wallet.buttonSupportRarity).toBeVisible(); }); - test('Cancel send rare sats testnet', async ({ page, extensionId }) => { + test('Cancel send rare sats testnet #localexecution', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); diff --git a/tests/specs/tabSettings.spec.ts b/tests/specs/tabSettings.spec.ts index fef6ccd37..d814122cc 100644 --- a/tests/specs/tabSettings.spec.ts +++ b/tests/specs/tabSettings.spec.ts @@ -90,10 +90,10 @@ test.describe('Settings Tab', () => { await expect(onboardingPage.buttonContinue).toBeEnabled(); await onboardingPage.buttonContinue.click(); await expect(onboardingPage.inputPassword).toBeVisible(); - await expect(onboardingPage.buttonContinue).toBeDisabled(); + await expect(wallet.buttonConfirm).toBeDisabled(); await onboardingPage.inputPassword.fill(`${strongPW}ABC`); - await expect(onboardingPage.buttonContinue).toBeEnabled(); - await onboardingPage.buttonContinue.click(); + await expect(wallet.buttonConfirm).toBeEnabled(); + await wallet.buttonConfirm.click(); await expect(wallet.infoUpdatePassword).toBeVisible(); }); test('Show Seedphrase', async ({ page, extensionId }) => { diff --git a/tests/specs/transactionBTC.spec.ts b/tests/specs/transactionBTC.spec.ts index 1f284f89b..b5361db43 100644 --- a/tests/specs/transactionBTC.spec.ts +++ b/tests/specs/transactionBTC.spec.ts @@ -53,7 +53,7 @@ test.describe('Transaction BTC', () => { await expect(initialBTCBalance).toEqual(displayBalanceNumerical); }); - test('Cancel BTC transaction testnet', async ({ page, extensionId }) => { + test('Cancel BTC transaction testnet #localexecution', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); diff --git a/tests/specs/transactionHistory.spec.ts b/tests/specs/transactionHistory.spec.ts index 8aa556b40..3acaccbeb 100644 --- a/tests/specs/transactionHistory.spec.ts +++ b/tests/specs/transactionHistory.spec.ts @@ -5,20 +5,19 @@ test.describe('Transaction', () => { test('Visual Check SIP 10 Token Transaction history mainnet', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - - const tokenName = await wallet.enableRandomToken('SIP10'); + const tokenName = await wallet.selectLastToken('SIP10'); await wallet.clickOnSpecificToken(tokenName); - await expect(page.url()).toContain('coinDashboard'); + expect(page.url()).toContain('coinDashboard'); // Check token detail page for token image and coin title await expect(wallet.imageToken).toBeVisible(); await expect(wallet.textCoinTitle).toBeVisible(); await expect(wallet.textCoinTitle).toContainText(tokenName); // Check contract details are displayed - await wallet.buttonCoinContract.click(); - await expect(wallet.buttonCoinContract).toBeVisible(); + await wallet.coinSecondaryButton.click(); + await expect(wallet.coinSecondaryButton).toBeVisible(); await expect(wallet.coinContractAddress).toBeVisible(); await expect(wallet.coinContractAddress).not.toBeEmpty(); }); @@ -26,10 +25,10 @@ test.describe('Transaction', () => { test('Visual Check BRC 20 Token Transaction history mainnet', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - const tokenName = await wallet.enableRandomToken('BRC20'); + const tokenName = await wallet.selectLastToken('BRC20'); await wallet.clickOnSpecificToken(tokenName); - await expect(page.url()).toContain('coinDashboard'); + expect(page.url()).toContain('coinDashboard'); // Check token detail page for coin title await expect(wallet.textCoinTitle).toBeVisible(); await expect(wallet.textCoinTitle).toContainText(tokenName); @@ -76,15 +75,19 @@ test.describe('Transaction', () => { test('Visual Check Runes Transaction history', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune('SKIBIDI•OHIO•RIZZ'); - const originalBalanceAmount = await wallet.checkVisualsRunesDashboard('SKIBIDI•OHIO•RIZZ'); await expect(originalBalanceAmount).toBeGreaterThan(0); - await expect(wallet.containerTransactionHistory.first()).toBeVisible(); + await expect(wallet.containerTransactionHistory.first()).toBeHidden(); // There should be at least one transaction visible await expect(await wallet.containerTransactionHistory.count()).toBeGreaterThanOrEqual(1); + // check able to see rune bundles + await wallet.coinSecondaryButton.click(); + await expect(wallet.coinSecondaryButton).toBeVisible(); + // can navigate to rare-sats-bundle page + await wallet.runeItem.last().click(); + await expect(page.url()).toContain('rare-sats-bundle'); }); // TODO: add test for sending NFT - https://linear.app/xverseapp/issue/ENG-4321/transaction-send-nft diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 41b65f0a5..db74f12bc 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -6,7 +6,10 @@ const STXTest = `STN2AMZQ54Y0NN4H5Z4S0DGMWP27CTXY5QEDCQAN`; const amountSTXSend = 10; test.describe('Transaction STX', () => { - test('Send STX Page Visual Check without funds Mainnet', async ({ page, extensionId }) => { + test('Send STX Page Visual Check with insufficient funds Mainnet', async ({ + page, + extensionId, + }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); @@ -42,67 +45,47 @@ test.describe('Transaction STX', () => { // No funds on mainnet in this wallet -->Page opens and Next button is hidden and info message is shown await expect(wallet.buttonNext).toBeHidden(); // Amount input is visible - await expect(wallet.inputField.first()).toBeVisible(); + await expect(page.getByRole('textbox', { name: '0' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: '0' })).toBeEnabled(); await expect(wallet.labelBalanceAmountSelector).toBeVisible(); await expect(wallet.imageToken).toBeVisible(); - await expect(wallet.inputField.first()).toBeDisabled(); - await expect(wallet.noFundsBTCMessage).toBeVisible(); + page.getByRole('textbox', { name: '0' }).fill('200000000'); + await expect(page.getByRole('button', { name: /insufficient funds/i })).toBeVisible(); }); - test('Send STX - Cancel transaction testnet', async ({ page, extensionId }) => { - // Restore wallet and setup Testnet network + test('Send STX - Cancel transaction mainnet', async ({ page, extensionId }) => { + // Restore wallet and setup Mainnet network const wallet = new Wallet(page); - await wallet.setupTest(extensionId, 'SEED_WORDS1', true); - - // Save initial Balance for later Balance checks - const initialSTXBalance = await wallet.getTokenBalance('Stacks'); - - // Click on send button - await wallet.buttonTransactionSend.click(); - - await expect(await wallet.divTokenRow.count()).toBeGreaterThanOrEqual(2); - await wallet.clickOnSpecificToken('Stacks'); - await wallet.checkVisualsSendPage1('send-stx', true); - - // Fill in Receiver Address - await wallet.inputField.first().fill(STXTest); - await expect(wallet.buttonNext).toBeEnabled(); - await wallet.buttonNext.click(); - - // Send Amount - await wallet.checkVisualsSendPage2('', true); - await wallet.inputField.first().fill(amountSTXSend.toString()); - await expect(wallet.buttonNext).toBeEnabled(); - - // Balance check - const displayBalance = await wallet.labelBalanceAmountSelector.innerText(); - const displayBalanceNumerical = parseFloat(displayBalance.replace(/[^0-9.]/g, '')); - await expect(initialSTXBalance).toEqual(displayBalanceNumerical); - - // Save Fees to check on next Page - const fee = await wallet.feeAmount.innerText(); - const sendFee = parseFloat(fee.replace(/[^0-9.]/g, '')); - - await wallet.buttonNext.click(); - - // Transaction Review Page - await wallet.checkVisualsSendSTXPage3(); - - // Check correct amounts - await wallet.checkAmountsSendingSTX(amountSTXSend, STXTest, sendFee); - - await wallet.switchToHighFees(); - - // Cancel the transaction - await expect(wallet.buttonCancel).toBeEnabled(); - await wallet.buttonCancel.click(); - - // Check startpage - await wallet.checkVisualsStartpage(); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // Check STX Balance after cancel the transaction - const balanceAfterCancel = await wallet.getTokenBalance('Stacks'); - await expect(initialSTXBalance).toEqual(balanceAfterCancel); + await page.getByText('STX').click(); + await page.getByRole('button', { name: /send/i }).click(); + await page.getByRole('textbox', { name: /STX Address or .btc domain/i }).fill('zhfr.btc'); + await expect(page.getByText(/associated address/i)).toBeVisible(); + await expect(page.getByText(/SP2VCZJDTT5TJ7A3QPPJPTEF7A9CD8FRG2BEEJF3D/i)).toBeVisible(); + await expect(page.getByRole('button', { name: /next/i })).toBeEnabled(); + await page.getByRole('button', { name: /next/i }).click(); + await page.getByRole('textbox', { name: /0/i }).last().fill('1'); + + // edit fee component set to custom + await page.getByRole('button', { name: /edit/i }).click(); + await page.getByRole('button', { name: /custom/i }).click(); + await page.getByRole('textbox', { name: /0/i }).last().fill('0.0974'); + await page.getByRole('button', { name: /apply/i }).click(); + await page.getByRole('button', { name: /next/i }).click(); + + // review transaction screen and clicking cancel btn + await expect(page.getByText(/review transaction/i)).toBeVisible(); + await expect(page.getByText(/you will send/i)).toBeVisible(); + await expect(page.getByText(/SP2VCZ...EEJF3D/i)).toBeVisible(); + await expect(page.getByText(/Mainnet/i)).toBeVisible(); + await expect(page.getByText(/0.0974 STX/i)).toBeVisible(); + await expect(page.getByText(/1 STX/i)).toBeVisible(); + await expect(page.getByRole('button', { name: /confirm/i })).toBeVisible(); + await page.getByRole('button', { name: /cancel/i }).click(); + + // arriving to homepage + await page.url().includes('/options.html'); }); test('Send STX - confirm transaction testnet #localexecution', async ({ page, extensionId }) => { diff --git a/vitest.config.js b/vitest.config.js deleted file mode 100644 index 43d27dc46..000000000 --- a/vitest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; - -export default defineConfig({ - plugins: [tsconfigPaths()], -});