From 74394d3f4ab127a587a00af04cd1c058411893ce Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 2 Jan 2024 16:27:20 +0800 Subject: [PATCH 01/20] [ENG-3325] chore: refactor workflows (#714) * chore: trigger create rc release on PR to main * chore: move release asset uploads for rc to build * chore: split workflows into 2 for build and 1 for release * chore: remove extra build deps step * chore: add back missing env vars * chore: comments and remove unnecessary conditionals * chore: add git checkout where needed * chore: clean up and remove create release step since it was moved * chore: clean up workflow file * chore: more cleanup * chore: minor naming * chore: minor naming * chore: minot naming * chore: more naming --- .github/workflows/build-rc.yml | 132 ++++++++++++++++++++++++ .github/workflows/build.yml | 66 ++++++------ .github/workflows/create-release-pr.yml | 13 ++- .github/workflows/release.yml | 98 ++++++++++-------- scripts/.gitignore | 1 + scripts/create-release-pr.sh | 35 ++----- scripts/find-tag.sh | 29 ++++++ 7 files changed, 269 insertions(+), 105 deletions(-) create mode 100644 .github/workflows/build-rc.yml create mode 100755 scripts/find-tag.sh diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml new file mode 100644 index 000000000..cfe31363e --- /dev/null +++ b/.github/workflows/build-rc.yml @@ -0,0 +1,132 @@ +name: Build & Publish release candidate +## +# This workflow builds new release candidates (create release + upload asset): +# - for a new release PR and +# - for every push to the release PR head branch +# +# It should also keep the release PR description in sync with the latest release candidate +# +on: + pull_request: + branches: + - main + - develop +jobs: + test: + if: ${{ startsWith(github.head_ref, 'release/') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + always-auth: true + node-version: 18 + registry-url: https://npm.pkg.github.com + scope: '@secretkeylabs' + cache: npm + - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} + run: npm ci + - name: Test + run: | + npx eslint . + npx tsc --noEmit + npm test + publish-rc: + # TODO also keep the develop PR description up to date + if: ${{ github.base_ref == 'main' }} + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + env: + GH_TOKEN: ${{ github.token }} + outputs: + upload_url: ${{ steps.publish-prerelease.outputs.UPLOAD_URL }} + filename: ${{ steps.publish-prerelease.outputs.FILENAME }} + steps: + - uses: actions/checkout@v4 + - id: publish-prerelease + name: Publish release candidate as prerelease + env: + SOURCE_BRANCH: ${{ github.head_ref }} + TARGET_COMMITISH: ${{ github.event.pull_request.head.sha }} + run: | + # find the next rc tag + gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/releases > releases.json + # get $TAG from branch name, e.g. v0.25.0 + TAG=$(echo $SOURCE_BRANCH | sed 's/release\/\(.*\)/\1/') + # export $NEXT_TAG using releases.json and $TAG, e.g. v0.25.0-rc.0 + cd scripts + ./find-tag.sh + # publish the release as prerelease rc + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/releases \ + -f tag_name="$NEXT_TAG" \ + -f target_commitish="$TARGET_COMMITISH" \ + -f name="$NEXT_TAG" \ + -F draft=false \ + -F prerelease=true \ + -F generate_release_notes=true > release.json + # save output for upload + echo "FILENAME=xverse-web-extension.$NEXT_TAG.zip" >> $GITHUB_OUTPUT + echo "UPLOAD_URL=$(cat release.json | jq -r .upload_url)" >> $GITHUB_OUTPUT + - id: update-description + name: Update PR description with release notes + env: + PR_ID: ${{ github.event.pull_request.number }} + run: | + # update PR description + cat release.json | jq -r .body > body.md + echo -e "\n\nRelease candidate: $(cat release.json | jq -r .html_url)" >> body.md + echo -e "\nTo publish this rc as latest: Merge Commit this PR" >> body.md + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/pulls/$PR_ID \ + -F 'body=@body.md' + build-rc: + needs: publish-rc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + always-auth: true + node-version: 18 + registry-url: https://npm.pkg.github.com + scope: '@secretkeylabs' + cache: npm + - name: Install dependencies + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} + run: npm ci + - name: Build & zip + env: + TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} + MOON_PAY_API_KEY: ${{ secrets.MOON_PAY_API_KEY }} + MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} + run: | + npm run build + zip -rj build.zip ./build + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UPLOAD_URL: ${{needs.publish-rc.outputs.upload_url}} + FILENAME: ${{needs.publish-rc.outputs.filename}} + with: + upload_url: $UPLOAD_URL + asset_path: build.zip + asset_name: $FILENAME + asset_content_type: application/zip diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2faef5aa0..c6ab6ce9d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,16 @@ -name: Build - +name: Build & Test for feature PR +## +# This workflow tests, builds, and uploads the extension code for each PR +# +# It should also keep an updated comment on the PR showing where the upload is +# on: pull_request: - branches: [main, develop] - + branches: + - develop jobs: build: + if: ${{ !startsWith(github.head_ref, 'release/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -13,9 +18,10 @@ jobs: uses: actions/setup-node@v4 with: always-auth: true - node-version: '18.x' + node-version: 18 registry-url: https://npm.pkg.github.com scope: '@secretkeylabs' + cache: npm - name: Install dependencies env: NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} @@ -34,7 +40,7 @@ jobs: - name: Save Filename run: | BRANCH_NAME=$(echo ${{ github.head_ref }} | sed 's/\//-/g') - GIT_SHA_SHORT=$(git rev-parse --short ${{ github.sha }}) + GIT_SHA_SHORT=$(git rev-parse --short ${{ github.event.pull_request.head.sha }}) echo "FILENAME=xverse-extension.$BRANCH_NAME.$GIT_SHA_SHORT" >> $GITHUB_ENV - name: Upload Archive uses: actions/upload-artifact@v3 @@ -46,8 +52,14 @@ jobs: comment-on-pr: needs: build runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} steps: - - name: Get Artifact URL + - uses: actions/checkout@v4 + - name: Get artifact URL env: OWNER: ${{ github.repository_owner }} REPO: ${{ github.event.repository.name }} @@ -56,33 +68,25 @@ jobs: ARTIFACT_URL="https://github.com/$OWNER/$REPO/actions/runs/$WORKFLOW_ID" echo "ARTIFACT_URL=$ARTIFACT_URL" >> $GITHUB_ENV - name: Delete old bot comments - if: ${{ github.event_name == 'pull_request' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_ID: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} run: | - curl \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GITHUB_TOKEN" \ - https://api.github.com/repos/$REPO/issues/$PR_ID/comments \ + gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/issues/$PR_ID/comments \ | jq ".[] | select(.user.login==\"github-actions[bot]\") | .id" \ - | xargs -I %q curl \ - -L \ - -X DELETE \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GITHUB_TOKEN"\ - https://api.github.com/repos/$REPO/issues/comments/%q + | xargs -I %q gh api \ + --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/issues/comments/%q - name: Post test package PR comment - if: ${{ github.event_name == 'pull_request' }} env: - VERSION: ${{ steps.published-version.outputs.version }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_URL: ${{ github.event.pull_request.comments_url }} + PR_ID: ${{ github.event.pull_request.number }} run: | - curl \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token $GITHUB_TOKEN" \ - $GITHUB_URL \ - -d "{\"body\":\"> Test with build here: ${{ env.ARTIFACT_URL }}\"}" + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/issues/$PR_ID/comments \ + -f body="Test with build here: $ARTIFACT_URL" diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml index 3b84416ef..b0d85aed2 100644 --- a/.github/workflows/create-release-pr.yml +++ b/.github/workflows/create-release-pr.yml @@ -1,5 +1,9 @@ -name: Create release PR - +name: Create release PRs +## +# This workflow initiates the release process (create release PRs): +# - creates a release branch with version bump +# - creates a release PR to main & develop +# on: workflow_dispatch: inputs: @@ -12,7 +16,6 @@ on: - patch - minor - major - jobs: create-release-pr: runs-on: ubuntu-latest @@ -21,8 +24,8 @@ jobs: pull-requests: write issues: write steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 547fe3bed..90a4247f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,50 +1,66 @@ -name: Release - +name: Publish latest release +## +# This workflow creates a latest release with the same target_commitish +# as the highest rc matching the release PR version +# +# It should also update the release PR description +# It should also attach the highest rc asset to the latest release +# on: - release: - types: [created] - + pull_request: + branches: + - main + types: + - closed jobs: - build: + publish-latest: + if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/')}} runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + env: + GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - always-auth: true - node-version: '18.x' - registry-url: https://npm.pkg.github.com - scope: '@secretkeylabs' - - name: Install dependencies + - id: create-latest-release + name: Create latest release env: - NODE_AUTH_TOKEN: ${{ secrets.GH_PACKAGE_REGISTRY_TOKEN }} - run: npm ci - - name: Test + SOURCE_BRANCH: ${{ github.head_ref }} run: | - npx eslint . - npx tsc --noEmit - npm test - - name: Build + # find the target commitish of the latest release matching our tag + TAG=$(echo $SOURCE_BRANCH | sed 's/release\/\(.*\)/\1/') + gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/releases > releases.json + TARGET_COMMITISH=$(cat releases.json | jq '.[] | select(.tag_name | match("$TAG")) | .target_commitish' | head -1) + # publish the latest release + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/releases \ + -f tag_name="$TAG" \ + -f target_commitish="$TARGET_COMMITISH" \ + -f name="$TAG" \ + -F generate_release_notes=true > release.json + # TODO attach the rc asset to latest release + # save output for upload + # echo "FILENAME=xverse-web-extension.$TAG.zip" >> $GITHUB_OUTPUT + # echo "UPLOAD_URL=$(cat release.json | jq -r .upload_url)" >> $GITHUB_OUTPUT + - id: update-description + name: Update PR description with release notes env: - TRANSAC_API_KEY: ${{ secrets.TRANSAC_API_KEY }} - MOON_PAY_API_KEY: ${{ secrets.MOON_PAY_API_KEY }} - MIX_PANEL_TOKEN: ${{ secrets.MIX_PANEL_TOKEN }} - run: npm run build --if-present - - name: Save Filename - id: save-filename + PR_ID: ${{ github.event.pull_request.number }} run: | - echo "FILENAME=xverse-extension.$(echo ${{github.ref_name}}| sed 's/\//-/').zip" >> $GITHUB_OUTPUT - - name: Create Archive - run: | - zip -rj build.zip ./build - - name: Upload Release Asset - if: ${{ github.event.release.upload_url }} - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: build.zip - asset_name: ${{ steps.save-filename.outputs.FILENAME }} - asset_content_type: application/zip + # update PR description + cat release.json | jq -r .body > body.md + echo -e "\n\nPublished latest release: $(cat release.json | jq -r .html_url)" >> body.md + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/{owner}/{repo}/pulls/$PR_ID \ + -F 'body=@body.md' diff --git a/scripts/.gitignore b/scripts/.gitignore index 62c82f717..3f4dae6b5 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -1,3 +1,4 @@ release.json pr-*.json body.md +releases.json diff --git a/scripts/create-release-pr.sh b/scripts/create-release-pr.sh index 84e3721d7..267c41b0d 100755 --- a/scripts/create-release-pr.sh +++ b/scripts/create-release-pr.sh @@ -1,5 +1,12 @@ #! /bin/bash +## +# create-release-pr.sh for xverse-web-extension +# +# NOTE: make sure you git commit your work before running this locally. +# Alternatively trigger it from the github action +# + if [[ -z "$BUMP" ]]; then echo "BUMP is required. major|minor|patch" exit 1 @@ -23,23 +30,6 @@ git merge origin/main -s ours git push --set-upstream origin $BRANCH -echo -e "\n--- Create draft release for $TAG ---" - -gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/{owner}/{repo}/releases \ - -f tag_name=$TAG \ - -f target_commitish="$BRANCH" \ - -f name=$TAG \ - -F draft=true \ - -F prerelease=true \ - -F generate_release_notes=true > release.json - -cat release.json | jq -r .body > body.md -echo -e "\n\nDraft release: $(cat release.json | jq -r .html_url)" >> body.md - for b in main develop; do echo -e "\n--- Create PR to $b ---" @@ -53,17 +43,6 @@ for b in main develop; do -f head="$BRANCH" \ -f base="$b" > pr-$b.json - echo -e "\n--- Update PR to $b with description ---" - - PR_ID=$(cat pr-$b.json | jq -r .number) - - gh api \ - --method PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/{owner}/{repo}/pulls/$PR_ID \ - -F 'body=@body.md' - # clean up temp files # rm pr-$b.json done diff --git a/scripts/find-tag.sh b/scripts/find-tag.sh new file mode 100755 index 000000000..c190ca360 --- /dev/null +++ b/scripts/find-tag.sh @@ -0,0 +1,29 @@ +#! /bin/bash + +## +# find-tag.sh +# +# a util for looking through a list of github releases and exporting the next tag +# +if [[ -z "$TAG" ]]; then + echo "TAG is required. e.g. v0.26.0" + exit 1 +fi + +if cat releases.json | jq '.[].tag_name' | grep $TAG; then + echo found releases matching $TAG + LATEST_TAG=$(cat releases.json | jq '.[].tag_name' | grep $TAG | head -1) + LATEST_RC=$(echo $LATEST_TAG | grep rc | sed 's/.*-rc\(.*\)/\1/') + if [[ -z "$LATEST_RC" ]]; then + echo $TAG was already released + exit 1; + elif [[ -n "$LATEST_RC" ]]; then + NEXT_TAG="$TAG-rc.$($LATEST_RC +1)" + fi +else + echo no releases matching $TAG yet + NEXT_TAG="$TAG-rc.0" +fi + +echo next tag will be $NEXT_TAG +export NEXT_TAG=$NEXT_TAG From f32e96cee458314aa40c4874f9b7971453824937 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Wed, 3 Jan 2024 12:09:03 +0800 Subject: [PATCH 02/20] chore: fix build workflow file (#734) --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6ab6ce9d..694746aa6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,6 +68,7 @@ jobs: ARTIFACT_URL="https://github.com/$OWNER/$REPO/actions/runs/$WORKFLOW_ID" echo "ARTIFACT_URL=$ARTIFACT_URL" >> $GITHUB_ENV - name: Delete old bot comments + env: PR_ID: ${{ github.event.pull_request.number }} run: | gh api \ From f5067a2f46b181072e96fd138ef558999f3140cf Mon Sep 17 00:00:00 2001 From: Jordan K <65149726+jordankzf@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:54:34 +0800 Subject: [PATCH 03/20] ENG-3402 Named classes for styled-components (#721) * Bump typescript to v5.0.0 * Add typescript-plugin-styled-components * Add webpack plugin * Update webpack/webpack.config.js --- package-lock.json | 33 +++++++++++++++++++++++++-------- package.json | 5 +++-- webpack/webpack.config.js | 29 ++++++++++++++++++----------- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab8679dc6..8c211947b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,7 +117,8 @@ "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", - "typescript": "^4.8.2", + "typescript": "^5.0.0", + "typescript-plugin-styled-components": "^3.0.0", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", @@ -13268,16 +13269,25 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/typescript-plugin-styled-components": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/typescript-plugin-styled-components/-/typescript-plugin-styled-components-3.0.0.tgz", + "integrity": "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A==", + "dev": true, + "peerDependencies": { + "typescript": "~4.8 || 5" } }, "node_modules/ufo": { @@ -24102,11 +24112,18 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true }, + "typescript-plugin-styled-components": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/typescript-plugin-styled-components/-/typescript-plugin-styled-components-3.0.0.tgz", + "integrity": "sha512-QWlhTl6NqsFxtJyxn7pJjm3RhgzXSByUftZ3AoQClrMMpa4yAaHuJKTN1gFpH3Ti+Rwm56fNUfG9pXSBU+WW3A==", + "dev": true, + "requires": {} + }, "ufo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", diff --git a/package.json b/package.json index d4d5692f3..a65838930 100644 --- a/package.json +++ b/package.json @@ -138,10 +138,11 @@ "tsc-files": "^1.1.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", - "typescript": "^4.8.2", + "typescript": "^5.0.0", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", - "webpack-dev-server": "^4.11.0" + "webpack-dev-server": "^4.11.0", + "typescript-plugin-styled-components": "^3.0.0" } } diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js index 6ea404cd4..b6b7210f4 100644 --- a/webpack/webpack.config.js +++ b/webpack/webpack.config.js @@ -9,6 +9,8 @@ const ReactRefreshTypeScript = require('react-refresh-typescript'); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const Dotenv = require('dotenv-webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const createStyledComponentsTransformer = require('typescript-plugin-styled-components').default; +const styledComponentsTransformer = createStyledComponentsTransformer(); const aliases = { // alias stacks.js packages to their esm (default prefers /dist/polyfill) @@ -68,9 +70,10 @@ var options = { loader: 'ts-loader', options: { getCustomTransformers: () => ({ - before: [env.NODE_ENV === 'development' && ReactRefreshTypeScript()].filter( - Boolean - ), + before: + env.NODE_ENV === 'development' + ? [ReactRefreshTypeScript(), styledComponentsTransformer] + : [], }), transpileOnly: false, }, @@ -91,9 +94,11 @@ var options = { ], }, resolve: { - plugins: [new TsconfigPathsPlugin({ - configFile: path.join(__dirname, '../', 'tsconfig.json') - })], + plugins: [ + new TsconfigPathsPlugin({ + configFile: path.join(__dirname, '../', 'tsconfig.json'), + }), + ], extensions: fileExtensions .map((extension) => '.' + extension) .concat(['.js', '.jsx', '.ts', '.tsx', '.css']), @@ -124,7 +129,7 @@ var options = { description: process.env.npm_package_description, version: process.env.npm_package_version, ...JSON.parse(content.toString()), - }) + }), ); }, }, @@ -139,9 +144,11 @@ var options = { ], }), new CopyWebpackPlugin({ - patterns: [{ - from: 'node_modules/webextension-polyfill/dist/browser-polyfill.js', - }], + patterns: [ + { + from: 'node_modules/webextension-polyfill/dist/browser-polyfill.js', + }, + ], }), new HtmlWebpackPlugin({ template: path.join(SRC_ROOT_PATH, 'pages', 'Options', 'index.html'), @@ -160,7 +167,7 @@ var options = { Buffer: ['buffer', 'Buffer'], }), new webpack.DefinePlugin({ - VERSION: JSON.stringify(require("../package.json").version), + VERSION: JSON.stringify(require('../package.json').version), }), ], From 6c8029f4c2081582ff36e162707108b9538f1058 Mon Sep 17 00:00:00 2001 From: Jordan K <65149726+jordankzf@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:00:44 +0800 Subject: [PATCH 04/20] ENG-2630: Remove BRC-20 / SIP-10 token labels (#735) * Remove token labels * Remove unused TagContainer and ProtocolText * Show SIP-10 in coinHeader --- src/app/components/tokenTile/index.tsx | 23 -------------------- src/app/screens/coinDashboard/coinHeader.tsx | 1 + 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/app/components/tokenTile/index.tsx b/src/app/components/tokenTile/index.tsx index cd9fce043..3b605a588 100644 --- a/src/app/components/tokenTile/index.tsx +++ b/src/app/components/tokenTile/index.tsx @@ -124,22 +124,6 @@ const TokenTitleContainer = styled.div({ justifyContent: 'flex-start', }); -const TagContainer = styled.div({ - display: 'flex', - alignItems: 'center', -}); - -const ProtocolText = styled.p((props) => ({ - ...props.theme.headline_category_s, - fontWeight: '700', - textTransform: 'uppercase', - marginLeft: props.theme.spacing(5), - backgroundColor: props.theme.colors.white_400, - padding: '2px 6px 1px', - borderRadius: props.theme.radius(2), - whiteSpace: 'nowrap', -})); - const StyledBarLoader = styled(BetterBarLoader)<{ withMarginBottom?: boolean; }>((props) => ({ @@ -363,13 +347,6 @@ function TokenTile({ {getTickerTitle()} {title} - {fungibleToken?.protocol ? ( - - - {fungibleToken?.protocol === 'stacks' ? 'Sip-10' : fungibleToken?.protocol} - - - ) : null} diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 8ee22353d..ca9404d07 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -355,6 +355,7 @@ export default function CoinHeader(props: CoinBalanceProps) { {getDashboardTitle()} {coin === 'brc20' && BRC-20} + {fungibleToken?.protocol === 'stacks' && SIP-10} Date: Mon, 8 Jan 2024 19:51:57 +0500 Subject: [PATCH 05/20] feat: BTC fee UI (#733) * chore: fee item component * feat: BTC fee options * chore: localisation * chore: remove unused code * feat: selected state * fix: custom fee UI * chore: localization * refactor: clean up edit fee * chore: address PR comments * fix: custom option selection * fix: back button --------- Co-authored-by: Tim Man Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> --- src/app/components/bottomModal/index.tsx | 10 +- .../transactionSetting/editBtcFee.tsx | 420 ++++++++++++++++++ .../{editFee.tsx => editStxFee.tsx} | 224 +--------- .../components/transactionSetting/feeItem.tsx | 136 ++++++ .../components/transactionSetting/index.tsx | 91 +++- src/app/hooks/useBtcFees.ts | 111 +++++ src/locales/en.json | 4 +- 7 files changed, 778 insertions(+), 218 deletions(-) create mode 100644 src/app/components/transactionSetting/editBtcFee.tsx rename src/app/components/transactionSetting/{editFee.tsx => editStxFee.tsx} (50%) create mode 100644 src/app/components/transactionSetting/feeItem.tsx create mode 100644 src/app/hooks/useBtcFees.ts diff --git a/src/app/components/bottomModal/index.tsx b/src/app/components/bottomModal/index.tsx index 959444c61..6fa5be20d 100644 --- a/src/app/components/bottomModal/index.tsx +++ b/src/app/components/bottomModal/index.tsx @@ -1,5 +1,5 @@ -import Cross from '@assets/img/dashboard/X.svg'; import Separator from '@components/separator'; +import { XCircle } from '@phosphor-icons/react'; import Modal from 'react-modal'; import styled, { useTheme } from 'styled-components'; @@ -11,9 +11,9 @@ const BottomModalHeaderText = styled.h1((props) => ({ const RowContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', - alignItems: 'space-between', - margin: props.theme.spacing(12), - marginBottom: props.theme.spacing(10), + alignItems: 'center', + justifyContent: 'space-between', + margin: props.theme.space.m, })); const ButtonImage = styled.button({ @@ -87,7 +87,7 @@ function BottomModal({ {header} - cross + {header && } diff --git a/src/app/components/transactionSetting/editBtcFee.tsx b/src/app/components/transactionSetting/editBtcFee.tsx new file mode 100644 index 000000000..5489f56b6 --- /dev/null +++ b/src/app/components/transactionSetting/editBtcFee.tsx @@ -0,0 +1,420 @@ +import useBtcClient from '@hooks/useBtcClient'; +import useBtcFees from '@hooks/useBtcFees'; +import useDebounce from '@hooks/useDebounce'; +import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { Faders } from '@phosphor-icons/react'; +import { + currencySymbolMap, + ErrorCodes, + getBtcFees, + getBtcFeesForNonOrdinalBtcSend, + getBtcFeesForOrdinalSend, + getBtcFiatEquivalent, + Recipient, + 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', + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, + paddingBottom: props.theme.space.m, +})); + +const DetailText = styled.h1((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_200, +})); + +interface InputContainerProps { + withError?: boolean; +} +const InputContainer = styled.div((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.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.h1((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; +`; + +interface FeeContainerProps { + isSelected: boolean; +} + +const FeeItemContainer = styled.button` + 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; +`; + +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}; +`; + +interface 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 { network, btcAddress, btcFiatRate, fiatCurrency, selectedAccount, ordinalsAddress } = + useWalletSelector(); + 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 = 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]); + + function getFiatEquivalent() { + return getBtcFiatEquivalent(new BigNumber(totalFee), BigNumber(btcFiatRate)); + } + + const getFiatAmountString = (fiatAmount: BigNumber) => { + if (fiatAmount) { + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + return ( + ( + + {value} + + )} + /> + ); + } + return ''; + }; + + const onInputEditFeesChange = ({ target: { value } }: React.ChangeEvent) => { + if (error) { + setError(''); + } + + 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'} + /> + { + feeOptionSelected( + feeData?.standardFeeRate?.toString() || '', + feeData?.standardTotalFee, + ); + setFeeMode('medium'); + }} + selected={feeMode === 'medium'} + /> + { + setCustomFeeSelected(true); + }} + > + + + + {t('TRANSACTION_SETTING.CUSTOM')} + + + {t('TRANSACTION_SETTING.MANUAL_SETTING')} + + + + + )} + + {customFeeSelected && ( + + + + Sats /vByte + + + + + {t('TRANSACTION_SETTING.TOTAL_FEE')} + + ( + + {value} + + )} + /> + + + {getFiatAmountString(getFiatEquivalent())} + + + {error && {error}} + + )} + + ); +} + +export default EditBtcFee; diff --git a/src/app/components/transactionSetting/editFee.tsx b/src/app/components/transactionSetting/editStxFee.tsx similarity index 50% rename from src/app/components/transactionSetting/editFee.tsx rename to src/app/components/transactionSetting/editStxFee.tsx index c58c7fbe5..43b167a89 100644 --- a/src/app/components/transactionSetting/editFee.tsx +++ b/src/app/components/transactionSetting/editStxFee.tsx @@ -1,21 +1,11 @@ -import useBtcClient from '@hooks/useBtcClient'; -import useDebounce from '@hooks/useDebounce'; -import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; import useWalletSelector from '@hooks/useWalletSelector'; import { currencySymbolMap, - ErrorCodes, - getBtcFees, - getBtcFeesForNonOrdinalBtcSend, - getBtcFeesForOrdinalSend, - getBtcFiatEquivalent, getStxFiatEquivalent, - Recipient, stxToMicrostacks, - UTXO, } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; @@ -36,7 +26,6 @@ const FiatAmountText = styled.h1((props) => ({ const DetailText = styled.h1((props) => ({ ...props.theme.body_m, color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), })); const Text = styled.h1((props) => ({ @@ -80,11 +69,6 @@ const InputField = styled.input((props) => ({ }, })); -const FeeText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_0, -})); - const SubText = styled.h1((props) => ({ ...props.theme.body_xs, color: props.theme.colors.white_400, @@ -93,7 +77,6 @@ const SubText = styled.h1((props) => ({ interface ButtonProps { isSelected: boolean; isLastInRow?: boolean; - isBtc?: boolean; } const FeeButton = styled.button((props) => ({ ...props.theme.body_medium_m, @@ -101,7 +84,7 @@ const FeeButton = styled.button((props) => ({ background: `${props.isSelected ? props.theme.colors.white : 'transparent'}`, border: `1px solid ${props.isSelected ? 'transparent' : props.theme.colors.elevation6}`, borderRadius: props.theme.radius(9), - width: props.isBtc ? 104 : 82, + width: 82, height: 40, display: 'flex', justifyContent: 'center', @@ -140,55 +123,30 @@ interface Props { type?: string; fee: string; feeRate?: BigNumber | string; - btcRecipients?: Recipient[]; - ordinalTxUtxo?: UTXO; - isRestoreFlow?: boolean; - nonOrdinalUtxos?: UTXO[]; feeMode: string; error: string; - setIsLoading: () => void; - setIsNotLoading: () => void; setFee: (fee: string) => void; setFeeRate: (feeRate: string) => void; setFeeMode: (feeMode: string) => void; setError: (error: string) => void; } -function EditFee({ +function EditStxFee({ type, fee, feeRate, - btcRecipients, - ordinalTxUtxo, - isRestoreFlow, - nonOrdinalUtxos, feeMode, error, - setIsLoading, - setIsNotLoading, setFee, setFeeRate, setError, setFeeMode, }: Props) { const { t } = useTranslation('translation'); - const { - network, - btcAddress, - stxBtcRate, - btcFiatRate, - fiatCurrency, - selectedAccount, - ordinalsAddress, - } = useWalletSelector(); + const { stxBtcRate, btcFiatRate, fiatCurrency } = useWalletSelector(); const [totalFee, setTotalFee] = useState(fee); const [feeRateInput, setFeeRateInput] = useState(feeRate?.toString() ?? ''); const inputRef = useRef(null); - const debouncedFeeRateInput = useDebounce(feeRateInput, 500); - const isBtcOrOrdinals = type === 'BTC' || type === 'Ordinals'; const isStx = type === 'STX'; - const { ordinals } = useOrdinalsByAddress(btcAddress); - const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); - const btcClient = useBtcClient(); const modifyStxFees = (mode: string) => { const currentFee = new BigNumber(fee); @@ -199,7 +157,7 @@ function EditFee({ setFeeRateInput(currentFee.dividedBy(2).toString()); setTotalFee(currentFee.dividedBy(2).toString()); break; - case 'standard': + case 'medium': setFeeRateInput(currentFee.toString()); setTotalFee(currentFee.toString()); break; @@ -215,58 +173,6 @@ function EditFee({ } }; - const modifyFees = async (mode: string) => { - try { - setFeeMode(mode); - setIsLoading(); - setError(''); - if (mode === 'custom') inputRef?.current?.focus(); - else if (type === 'BTC') { - if (isRestoreFlow) { - const { fee: modifiedFee, selectedFeeRate } = await getBtcFeesForNonOrdinalBtcSend( - btcAddress, - nonOrdinalUtxos!, - ordinalsAddress, - network.type, - mode, - ); - setFeeRateInput(selectedFeeRate?.toString() || ''); - setTotalFee(modifiedFee.toString()); - } else if (btcRecipients && selectedAccount) { - const { fee: modifiedFee, selectedFeeRate } = await getBtcFees( - btcRecipients, - btcAddress, - btcClient, - network.type, - mode, - ); - setFeeRateInput(selectedFeeRate?.toString() || ''); - setTotalFee(modifiedFee.toString()); - } - } else if (type === 'Ordinals' && btcRecipients && ordinalTxUtxo) { - const { fee: modifiedFee, selectedFeeRate } = await getBtcFeesForOrdinalSend( - btcRecipients[0].address, - ordinalTxUtxo, - btcAddress, - btcClient, - network.type, - ordinalsUtxos || [], - mode, - ); - 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 (isStx && feeMode !== 'custom') { modifyStxFees(feeMode); @@ -277,93 +183,18 @@ function EditFee({ 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, - ); - 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]); - function getFiatEquivalent() { - return isStx - ? getStxFiatEquivalent( - stxToMicrostacks(new BigNumber(totalFee)), - BigNumber(stxBtcRate), - BigNumber(btcFiatRate), - ) - : getBtcFiatEquivalent(new BigNumber(totalFee), BigNumber(btcFiatRate)); + return getStxFiatEquivalent( + stxToMicrostacks(new BigNumber(totalFee)), + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + ); } const getFiatAmountString = (fiatAmount: BigNumber) => { @@ -395,15 +226,12 @@ function EditFee({ } setFeeRateInput(value); - - if (type !== 'BTC' && type !== 'Ordinals') { - setFeeRateInput(value); - setTotalFee(value); - } + setTotalFee(value); }; return ( + {t('TRANSACTION_SETTING.FEE_INFO')} {t('TRANSACTION_SETTING.FEE')} @@ -413,17 +241,7 @@ function EditFee({ value={feeRateInput?.toString()} onChange={onInputEditFeesChange} /> - {isBtcOrOrdinals && sats /vB} - {isBtcOrOrdinals && ( - {value}} - /> - )} {getFiatAmountString(getFiatEquivalent())} @@ -435,32 +253,22 @@ function EditFee({ {t('TRANSACTION_SETTING.LOW')} )} - (isStx ? modifyStxFees('standard') : modifyFees('standard'))} - > + modifyStxFees('medium')}> {t('TRANSACTION_SETTING.STANDARD')} - (isStx ? modifyStxFees('high') : modifyFees('high'))} - > + modifyStxFees('high')}> {t('TRANSACTION_SETTING.HIGH')} (isStx ? modifyStxFees('custom') : modifyFees('custom'))} + onClick={() => modifyStxFees('custom')} > {t('TRANSACTION_SETTING.CUSTOM')} - {t('TRANSACTION_SETTING.FEE_INFO')} ); } -export default EditFee; +export default EditStxFee; diff --git a/src/app/components/transactionSetting/feeItem.tsx b/src/app/components/transactionSetting/feeItem.tsx new file mode 100644 index 000000000..222996530 --- /dev/null +++ b/src/app/components/transactionSetting/feeItem.tsx @@ -0,0 +1,136 @@ +import { Bicycle, CarProfile, RocketLaunch } from '@phosphor-icons/react'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { MoonLoader } from 'react-spinners'; +import styled from 'styled-components'; +import Theme from 'theme'; + +interface FeeContainer { + isSelected: boolean; +} + +const FeeItemContainer = styled.button` + 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; +`; + +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)` + color: ${(props) => props.theme.colors.white_200}; + margin-bottom: ${(props) => props.theme.space.xxs}; +`; + +const LoaderContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; +`; + +type FeePriority = 'high' | 'medium' | 'low'; + +interface FeeItemProps { + priority: FeePriority; + time: string; + feeRate: string; + totalFee: string; + fiat: string | JSX.Element; + selected: boolean; + onClick?: () => void; +} + +function FeeItem({ priority, time, feeRate, totalFee, fiat, selected, onClick }: FeeItemProps) { + const { t } = useTranslation('translation'); + 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'); + } + }; + + return ( + + {getIcon()} + + + + {getLabel()} + + {time} + {`${feeRate} Sats/ vByte`} + + {totalFee ? ( + + + {`${totalFee} Sats`} + + {fiat} + + ) : ( + + + + )} + + + ); +} + +export default FeeItem; diff --git a/src/app/components/transactionSetting/index.tsx b/src/app/components/transactionSetting/index.tsx index 7cbacfa1e..9668f8438 100644 --- a/src/app/components/transactionSetting/index.tsx +++ b/src/app/components/transactionSetting/index.tsx @@ -7,8 +7,10 @@ import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import EditFee from './editFee'; +import Theme from 'theme'; +import EditBtcFee from './editBtcFee'; import EditNonce from './editNonce'; +import EditStxFee from './editStxFee'; const ButtonContainer = styled.div((props) => ({ display: 'flex', @@ -19,6 +21,26 @@ const ButtonContainer = styled.div((props) => ({ marginRight: props.theme.spacing(8), })); +const ButtonsContainer = styled.div` + display: flex; + flex-direction: row; + margin-left: ${(props) => props.theme.space.m}; + margin-right: ${(props) => props.theme.space.m}; + margin-bottom: ${(props) => props.theme.space.m}; +`; + +const LeftButton = styled.div` + display: flex; + margin-right: ${(props) => props.theme.space.xs}; + flex: 1; +`; + +const RightButton = styled.div` + display: flex; + margin-left: ${(props) => props.theme.space.xs}; + flex: 1; +`; + const TransactionSettingOptionText = styled.h1((props) => ({ ...props.theme.body_medium_l, color: props.theme.colors.white_200, @@ -87,9 +109,10 @@ function TransactionSettingAlert({ const [feeRate, setFeeRate] = useState(feePerVByte); const [nonceInput, setNonceInput] = useState(nonce); const [error, setError] = useState(''); - const [selectedOption, setSelectedOption] = useState('standard'); + const [selectedOption, setSelectedOption] = useState('medium'); const [showNonceSettings, setShowNonceSettings] = useState(false); const [isLoading, setIsLoading] = useState(loading); + const [customFeeSelected, setCustomFeeSelected] = useState(false); const { btcBalance, stxAvailableBalance, network } = useWalletSelector(); const applyClickForStx = () => { @@ -134,10 +157,24 @@ function TransactionSettingAlert({ } 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)) { + // 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); }; @@ -166,8 +203,23 @@ function TransactionSettingAlert({ } if (showFeeSettings) { + if (type === 'STX') { + return ( + + ); + } return ( - { + setCustomFeeSelected(selected); + }} + customFeeSelected={customFeeSelected} + feeOptionSelected={btcFeeOptionSelected} /> ); } @@ -221,9 +278,14 @@ function TransactionSettingAlert({ overlayStylesOverriding={{ height: 600, }} + contentStylesOverriding={{ + background: Theme.colors.elevation6_600, + backdropFilter: 'blur(10px)', + paddingBottom: Theme.spacing(8), + }} > {renderContent()} - {(showFeeSettings || showNonceSettings) && ( + {type === 'STX' && (showFeeSettings || showNonceSettings) && ( )} + {customFeeSelected && ( + + + { + setCustomFeeSelected(false); + }} + transparent + /> + + + + + + )} ); } diff --git a/src/app/hooks/useBtcFees.ts b/src/app/hooks/useBtcFees.ts new file mode 100644 index 000000000..5a118bf24 --- /dev/null +++ b/src/app/hooks/useBtcFees.ts @@ -0,0 +1,111 @@ +import { + getBtcFees, + getBtcFeesForNonOrdinalBtcSend, + getBtcFeesForOrdinalSend, + Recipient, + UTXO, +} from '@secretkeylabs/xverse-core'; +import { useEffect, useMemo, useState } from 'react'; +import useBtcClient from './useBtcClient'; +import useOrdinalsByAddress from './useOrdinalsByAddress'; +import useWalletSelector from './useWalletSelector'; + +interface Params { + isRestoreFlow: boolean; + nonOrdinalUtxos?: UTXO[]; + btcRecipients?: Recipient[]; + type?: string; + ordinalTxUtxo?: UTXO; +} + +interface FeeData { + standardFeeRate: string; + standardTotalFee: string; + highFeeRate: string; + highTotalFee: string; +} + +const useBtcFees = ({ + isRestoreFlow, + nonOrdinalUtxos, + btcRecipients, + type, + ordinalTxUtxo, +}: Params): FeeData => { + const [feeData, setFeeData] = useState({ + standardFeeRate: '', + standardTotalFee: '', + highFeeRate: '', + highTotalFee: '', + }); + const { network, btcAddress, selectedAccount, ordinalsAddress } = useWalletSelector(); + const btcClient = useBtcClient(); + const { ordinals } = useOrdinalsByAddress(btcAddress); + const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); + + useEffect(() => { + async function fetchFees(mode: 'standard' | 'high') { + try { + let feeInfo; + if (isRestoreFlow) { + feeInfo = await getBtcFeesForNonOrdinalBtcSend( + btcAddress, + nonOrdinalUtxos!, + ordinalsAddress, + network.type, + mode, + ); + } else if (btcRecipients && selectedAccount) { + feeInfo = await getBtcFees(btcRecipients, btcAddress, btcClient, network.type, mode); + } else if (type === 'Ordinals' && btcRecipients && ordinalTxUtxo) { + feeInfo = await getBtcFeesForOrdinalSend( + btcRecipients[0].address, + ordinalTxUtxo, + btcAddress, + btcClient, + network.type, + ordinalsUtxos || [], + mode, + ); + } + + return { + fee: feeInfo?.fee.toString() || '', + feeRate: feeInfo?.selectedFeeRate.toString() || '', + }; + } catch (error) { + return { fee: '', feeRate: '' }; + } + } + + async function calculateFees() { + const standard = await fetchFees('standard'); + const high = await fetchFees('high'); + + setFeeData({ + standardFeeRate: standard.feeRate, + standardTotalFee: standard.fee, + highFeeRate: high.feeRate, + highTotalFee: high.fee, + }); + } + + calculateFees(); + }, [ + isRestoreFlow, + nonOrdinalUtxos, + btcRecipients, + type, + ordinalTxUtxo, + btcAddress, + btcClient, + network, + ordinalsUtxos, + ordinalsAddress, + selectedAccount, + ]); + + return feeData; +}; + +export default useBtcFees; diff --git a/src/locales/en.json b/src/locales/en.json index 18ec77fac..3412ce678 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -404,7 +404,8 @@ "SAME_FEE_ERROR": "New fee must be greater than current fee", "GREATER_FEE_ERROR": "Fee is more than available balance", "LOWER_THAN_MINIMUM": "Set fee is below minimum", - "NONCE_WARNING": "Entering an erroneous nonce can result in a failed transaction. Only apply changes if you know what you are doing." + "NONCE_WARNING": "Entering an erroneous nonce can result in a failed transaction. Only apply changes if you know what you are doing.", + "MANUAL_SETTING": "Manual setting" }, "TRANSACTION_STATUS": { "BROADCASTED": "Transaction Broadcasted", @@ -807,6 +808,7 @@ "HIGHER_PRIORITY": "Higher priority", "HIGHEST_PRIORITY": "Highest priority", "MED_PRIORITY": "Medium priority", + "LOW_PRIORITY": "Low priority", "CUSTOM": "Custom", "INSUFFICIENT_FUNDS": "Insufficient funds", "INSUFFICIENT_FEE": "Insufficient fee", From 2b1b387cebd171833f9118eb5ca0c1b95a4f22c2 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Tue, 9 Jan 2024 19:44:49 +0800 Subject: [PATCH 06/20] [ENG-3471] feat: use estimated fees for stx transactions (#720) * feat: use estimated network fees for stx transactions * fix: use bigint in comparison * refactor: use core function for stx applyFeeMultiplier * chore: update core version * fix: use safer default instead of non-nullish assert --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> --- package-lock.json | 126 ++++++++++-------- package.json | 6 +- .../screens/confirmFtTransaction/index.tsx | 2 +- .../screens/confirmNftTransaction/index.tsx | 2 +- .../screens/confirmStxTransaction/index.tsx | 6 +- src/app/screens/sendFt/index.tsx | 16 +-- src/app/screens/sendNft/index.tsx | 13 +- src/app/screens/sendStx/index.tsx | 10 +- src/app/screens/transactionRequest/index.tsx | 8 +- 9 files changed, 95 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28f679ceb..a882f6fc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "7.0.0", + "@secretkeylabs/xverse-core": "7.2.0-60f9ae5", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -1287,20 +1287,20 @@ "dev": true }, "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "dependencies": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "1.3.3" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "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" }, @@ -1658,9 +1658,9 @@ ] }, "node_modules/@scure/base": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", - "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1719,30 +1719,30 @@ ] }, "node_modules/@scure/btc-signer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.1.0.tgz", - "integrity": "sha512-kCX7WaaTJr0VZIXDvaY0wNZfzZoZuLnPz4G0qmKXN8bnNx5M86wb1cce9XrZcfzb0jrVAbZJqNpxmE1e7Ka2hA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.1.1.tgz", + "integrity": "sha512-oXDbQFnGEQNHLNcTxM/MHXaQnZzSmoxunwXQbBr2Eg9ALAjYB9xvUa+EywkUSbU82Gn0/OEm0Gg9dz5HYifAIg==", "dependencies": { - "@noble/curves": "~1.2.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.3", - "micro-packed": "~0.3.2" + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.3", + "@scure/base": "~1.1.4", + "micro-packed": "~0.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "7.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.0.0/a9d4470c0bee31b2b751b0f2a3e064d51453ddd4", - "integrity": "sha512-8z5g5dHFin0d9695EwI0t6a/Ji7vzUenCq1AHpTtsj5Z1/SlH6Oa7p95fyJ1usBH99M5NQAYFMBGkjOcNE+jQw==", + "version": "7.2.0-60f9ae5", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.2.0-60f9ae5/b4d99a44aac6a9cd2552f2e4456407530e1c0862", + "integrity": "sha512-nRysYuNGccHF0EaGtj0xI3jDAhYMaOQUBVwMahV9I6UB6NMRqpu+FtBoNd2/5PbSVea6RnhKUtROF2P4XCOjSA==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", - "@scure/btc-signer": "1.1.0", + "@scure/btc-signer": "1.1.1", "@stacks/auth": "^6.9.0", "@stacks/connect": "^7.4.1", "@stacks/encryption": "6.9.0", @@ -1751,6 +1751,7 @@ "@stacks/transactions": "6.9.0", "@stacks/wallet-sdk": "^6.9.0", "@zondax/ledger-stacks": "^1.0.4", + "async-mutex": "^0.4.0", "axios": "1.6.2", "base64url": "^3.0.1", "bip32": "^4.0.0", @@ -3949,6 +3950,14 @@ "lodash": "^4.17.14" } }, + "node_modules/async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -9718,9 +9727,9 @@ } }, "node_modules/micro-packed": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.3.2.tgz", - "integrity": "sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.4.0.tgz", + "integrity": "sha512-H1+8SUMwcm68RXLOj3t5S8wXVf49FuR5m9IAG7XZ1XUOexbnQriyql5lk2I3fx/KyYf48LWNf5Lnbc2OjyQFMw==", "funding": [ { "type": "individual", @@ -9728,7 +9737,7 @@ } ], "dependencies": { - "@scure/base": "~1.1.1" + "@scure/base": "~1.1.3" } }, "node_modules/micromatch": { @@ -13128,8 +13137,7 @@ "node_modules/tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "dev": true + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -15124,17 +15132,17 @@ "dev": true }, "@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", "requires": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "1.3.3" } }, "@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + "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/secp256k1": { "version": "1.7.1", @@ -15333,9 +15341,9 @@ "optional": true }, "@scure/base": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.3.tgz", - "integrity": "sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==" + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" }, "@scure/bip32": { "version": "1.1.3", @@ -15371,26 +15379,26 @@ } }, "@scure/btc-signer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.1.0.tgz", - "integrity": "sha512-kCX7WaaTJr0VZIXDvaY0wNZfzZoZuLnPz4G0qmKXN8bnNx5M86wb1cce9XrZcfzb0jrVAbZJqNpxmE1e7Ka2hA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.1.1.tgz", + "integrity": "sha512-oXDbQFnGEQNHLNcTxM/MHXaQnZzSmoxunwXQbBr2Eg9ALAjYB9xvUa+EywkUSbU82Gn0/OEm0Gg9dz5HYifAIg==", "requires": { - "@noble/curves": "~1.2.0", - "@noble/hashes": "~1.3.1", - "@scure/base": "~1.1.3", - "micro-packed": "~0.3.2" + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.3", + "@scure/base": "~1.1.4", + "micro-packed": "~0.4.0" } }, "@secretkeylabs/xverse-core": { - "version": "7.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.0.0/a9d4470c0bee31b2b751b0f2a3e064d51453ddd4", - "integrity": "sha512-8z5g5dHFin0d9695EwI0t6a/Ji7vzUenCq1AHpTtsj5Z1/SlH6Oa7p95fyJ1usBH99M5NQAYFMBGkjOcNE+jQw==", + "version": "7.2.0-60f9ae5", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.2.0-60f9ae5/b4d99a44aac6a9cd2552f2e4456407530e1c0862", + "integrity": "sha512-nRysYuNGccHF0EaGtj0xI3jDAhYMaOQUBVwMahV9I6UB6NMRqpu+FtBoNd2/5PbSVea6RnhKUtROF2P4XCOjSA==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", "@noble/secp256k1": "^1.7.1", "@scure/base": "^1.1.1", - "@scure/btc-signer": "1.1.0", + "@scure/btc-signer": "1.1.1", "@stacks/auth": "^6.9.0", "@stacks/connect": "^7.4.1", "@stacks/encryption": "6.9.0", @@ -15399,6 +15407,7 @@ "@stacks/transactions": "6.9.0", "@stacks/wallet-sdk": "^6.9.0", "@zondax/ledger-stacks": "^1.0.4", + "async-mutex": "^0.4.0", "axios": "1.6.2", "base64url": "^3.0.1", "bip32": "^4.0.0", @@ -17154,6 +17163,14 @@ "lodash": "^4.17.14" } }, + "async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -21482,11 +21499,11 @@ "dev": true }, "micro-packed": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.3.2.tgz", - "integrity": "sha512-D1Bq0/lVOzdxhnX5vylCxZpdw5LylH7Vd81py0DfRsKUP36XYpwvy8ZIsECVo3UfnoROn8pdKqkOzL7Cd82sGA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.4.0.tgz", + "integrity": "sha512-H1+8SUMwcm68RXLOj3t5S8wXVf49FuR5m9IAG7XZ1XUOexbnQriyql5lk2I3fx/KyYf48LWNf5Lnbc2OjyQFMw==", "requires": { - "@scure/base": "~1.1.1" + "@scure/base": "~1.1.3" } }, "micromatch": { @@ -24008,8 +24025,7 @@ "tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", - "dev": true + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 4cd826d00..4e9e43fa7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "7.0.0", + "@secretkeylabs/xverse-core": "7.2.0-60f9ae5", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -139,10 +139,10 @@ "tsconfig-paths-webpack-plugin": "^4.0.0", "type-fest": "^2.19.0", "typescript": "^5.0.0", + "typescript-plugin-styled-components": "^3.0.0", "vitest": "^0.34.6", "webpack": "^5.89.0", "webpack-cli": "^4.0.0", - "webpack-dev-server": "^4.11.0", - "typescript-plugin-styled-components": "^3.0.0" + "webpack-dev-server": "^4.11.0" } } diff --git a/src/app/screens/confirmFtTransaction/index.tsx b/src/app/screens/confirmFtTransaction/index.tsx index 54ebed157..9b62dee13 100644 --- a/src/app/screens/confirmFtTransaction/index.tsx +++ b/src/app/screens/confirmFtTransaction/index.tsx @@ -8,7 +8,7 @@ import TransactionDetailComponent from '@components/transactionDetailComponent'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; -import { broadcastSignedTransaction, StacksTransaction } from '@secretkeylabs/xverse-core'; +import { StacksTransaction, broadcastSignedTransaction } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; import { useMutation } from '@tanstack/react-query'; import { isLedgerAccount } from '@utils/helper'; diff --git a/src/app/screens/confirmNftTransaction/index.tsx b/src/app/screens/confirmNftTransaction/index.tsx index df0c9ef0d..35807d992 100644 --- a/src/app/screens/confirmNftTransaction/index.tsx +++ b/src/app/screens/confirmNftTransaction/index.tsx @@ -12,7 +12,7 @@ import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useWalletSelector from '@hooks/useWalletSelector'; import NftImage from '@screens/nftDashboard/nftImage'; -import { broadcastSignedTransaction, StacksTransaction } from '@secretkeylabs/xverse-core'; +import { StacksTransaction, broadcastSignedTransaction } from '@secretkeylabs/xverse-core'; import { deserializeTransaction } from '@stacks/transactions'; import { useMutation } from '@tanstack/react-query'; import { isLedgerAccount } from '@utils/helper'; diff --git a/src/app/screens/confirmStxTransaction/index.tsx b/src/app/screens/confirmStxTransaction/index.tsx index 62544c0b6..0fd2c680a 100644 --- a/src/app/screens/confirmStxTransaction/index.tsx +++ b/src/app/screens/confirmStxTransaction/index.tsx @@ -14,16 +14,16 @@ import useNetworkSelector from '@hooks/useNetwork'; import useOnOriginTabClose from '@hooks/useOnTabClosed'; import useWalletSelector from '@hooks/useWalletSelector'; import { + StacksTransaction, + TokenTransferPayload, addressToString, broadcastSignedTransaction, buf2hex, getStxFiatEquivalent, isMultiSig, microstacksToStx, - StacksTransaction, - TokenTransferPayload, } from '@secretkeylabs/xverse-core'; -import { deserializeTransaction, MultiSigSpendingCondition } from '@stacks/transactions'; +import { MultiSigSpendingCondition, deserializeTransaction } from '@stacks/transactions'; import { useMutation } from '@tanstack/react-query'; import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; diff --git a/src/app/screens/sendFt/index.tsx b/src/app/screens/sendFt/index.tsx index 9cde1843a..94d91cf6d 100644 --- a/src/app/screens/sendFt/index.tsx +++ b/src/app/screens/sendFt/index.tsx @@ -5,10 +5,11 @@ import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; import { - buf2hex, - generateUnsignedTransaction, StacksTransaction, UnsignedStacksTransation, + applyFeeMultiplier, + buf2hex, + generateUnsignedTransaction, validateStxAddress, } from '@secretkeylabs/xverse-core'; import { useMutation } from '@tanstack/react-query'; @@ -20,7 +21,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; function SendFtScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const navigate = useNavigate(); - const { stxAddress, stxPublicKey, network, feeMultipliers, coinsList } = useWalletSelector(); + const { stxAddress, stxPublicKey, network, coinsList, feeMultipliers } = useWalletSelector(); const [amountError, setAmountError] = useState(''); const [addressError, setAddressError] = useState(''); const [memoError, setMemoError] = useState(''); @@ -71,13 +72,8 @@ function SendFtScreen() { pendingTxs: stxPendingTxData?.pendingTransactions ?? [], memo, }; - const unsignedTx: StacksTransaction = await generateUnsignedTransaction(unsginedTx); - - const fee: bigint = BigInt(unsignedTx.auth.spendingCondition.fee.toString()) ?? BigInt(0); - if (feeMultipliers?.stxSendTxMultiplier) { - unsignedTx.setFee(fee * BigInt(feeMultipliers.stxSendTxMultiplier)); - } - + const unsignedTx = await generateUnsignedTransaction(unsginedTx); + applyFeeMultiplier(unsignedTx, feeMultipliers); return unsignedTx; }, }); diff --git a/src/app/screens/sendNft/index.tsx b/src/app/screens/sendNft/index.tsx index bf849992e..a6a06e8e9 100644 --- a/src/app/screens/sendNft/index.tsx +++ b/src/app/screens/sendNft/index.tsx @@ -7,12 +7,13 @@ import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useWalletSelector from '@hooks/useWalletSelector'; import { + StacksTransaction, + UnsignedStacksTransation, + applyFeeMultiplier, buf2hex, cvToHex, generateUnsignedTransaction, - StacksTransaction, uintCV, - UnsignedStacksTransation, validateStxAddress, } from '@secretkeylabs/xverse-core'; import { useMutation } from '@tanstack/react-query'; @@ -128,12 +129,8 @@ function SendNft() { memo: '', isNFT: true, }; - const unsignedTx: StacksTransaction = await generateUnsignedTransaction(unsginedTx); - if (feeMultipliers?.stxSendTxMultiplier) { - unsignedTx.setFee( - unsignedTx.auth.spendingCondition.fee * BigInt(feeMultipliers.stxSendTxMultiplier), - ); - } + const unsignedTx = await generateUnsignedTransaction(unsginedTx); + applyFeeMultiplier(unsignedTx, feeMultipliers); setRecipientAddress(address); return unsignedTx; }, diff --git a/src/app/screens/sendStx/index.tsx b/src/app/screens/sendStx/index.tsx index 52eaa3a9d..adff7e75a 100644 --- a/src/app/screens/sendStx/index.tsx +++ b/src/app/screens/sendStx/index.tsx @@ -4,6 +4,7 @@ import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; import useNetworkSelector from '@hooks/useNetwork'; import useWalletSelector from '@hooks/useWalletSelector'; import { + applyFeeMultiplier, buf2hex, generateUnsignedStxTokenTransferTransaction, microstacksToStx, @@ -22,7 +23,7 @@ import TopRow from '../../components/topRow'; function SendStxScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const navigate = useNavigate(); - const { stxAddress, stxAvailableBalance, stxPublicKey, feeMultipliers, network } = + const { stxAddress, stxAvailableBalance, stxPublicKey, network, feeMultipliers } = useWalletSelector(); const [amountError, setAmountError] = useState(''); const [addressError, setAddressError] = useState(''); @@ -55,12 +56,7 @@ function SendStxScreen() { stxPublicKey, selectedNetwork, ); - // increasing the fees with multiplication factor - const fee: bigint = - BigInt(unsignedSendStxTx.auth.spendingCondition.fee.toString()) ?? BigInt(0); - if (feeMultipliers?.stxSendTxMultiplier) { - unsignedSendStxTx.setFee(fee * BigInt(feeMultipliers.stxSendTxMultiplier)); - } + applyFeeMultiplier(unsignedSendStxTx, feeMultipliers); return unsignedSendStxTx; }, }); diff --git a/src/app/screens/transactionRequest/index.tsx b/src/app/screens/transactionRequest/index.tsx index 504c99eee..22e79d414 100644 --- a/src/app/screens/transactionRequest/index.tsx +++ b/src/app/screens/transactionRequest/index.tsx @@ -53,13 +53,9 @@ function TransactionRequest() { const unsignedSendStxTx = await getTokenTransferRequest( tokenTransferPayload.recipient, tokenTransferPayload.amount, - tokenTransferPayload.memo!, + tokenTransferPayload.memo ?? '', requestAccount.stxPublicKey, - { - stxSendTxMultiplier: feeMultipliers?.stxSendTxMultiplier || 1, - poolStackingTxMultiplier: feeMultipliers?.poolStackingTxMultiplier || 1, - otherTxMultiplier: feeMultipliers?.otherTxMultiplier || 1, - }, + feeMultipliers, selectedNetwork, stxPendingTxData || [], stacksTransaction?.auth, From e2795111b6afe23ea9722605e08003b6363cb8dc Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:45:02 +0100 Subject: [PATCH 07/20] [ENG-3436] chore: Make some code refactoring for RBF in the extension (#712) * [ENG-3436] chore: Make some code refactoring for RBF in the extension * Make some code fixes * Make some code fixes * fix: put speed up screen in another scrollable container * Make some code tweaks according to PR review comments * Remove the unused import --------- Co-authored-by: Tim Man --- .../confirmBrc20Transaction/editFees.tsx | 10 +- src/app/screens/nftCollection/index.tsx | 5 +- src/app/screens/nftDetail/index.tsx | 5 +- src/app/screens/ordinalDetail/index.tsx | 5 +- src/app/screens/ordinalsCollection/index.tsx | 5 +- .../screens/speedUpTransaction/customFee.tsx | 220 ------------------ .../customFee/index.styled.ts | 96 ++++++++ .../speedUpTransaction/customFee/index.tsx | 129 ++++++++++ .../speedUpTransaction/index.styled.ts | 7 +- src/app/screens/speedUpTransaction/index.tsx | 179 +++++++------- src/app/screens/swap/swapInfoBlock/index.tsx | 14 +- src/app/screens/swap/swapTokenBlock/index.tsx | 5 +- src/app/utils/constants.ts | 3 + src/app/utils/helper.ts | 9 + src/locales/en.json | 7 +- 15 files changed, 374 insertions(+), 325 deletions(-) delete mode 100644 src/app/screens/speedUpTransaction/customFee.tsx create mode 100644 src/app/screens/speedUpTransaction/customFee/index.styled.ts create mode 100644 src/app/screens/speedUpTransaction/customFee/index.tsx diff --git a/src/app/screens/confirmBrc20Transaction/editFees.tsx b/src/app/screens/confirmBrc20Transaction/editFees.tsx index e45bdc046..0beb51b1e 100644 --- a/src/app/screens/confirmBrc20Transaction/editFees.tsx +++ b/src/app/screens/confirmBrc20Transaction/editFees.tsx @@ -6,6 +6,7 @@ import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useWalletSelector from '@hooks/useWalletSelector'; import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; import InputFeedback from '@ui-library/inputFeedback'; +import { handleKeyDownFeeRateInput } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -174,15 +175,6 @@ export function EditFees({ }, [feeRateInput, onChangeFeeRate]); /* callbacks */ - const handleKeyDownFeeRateInput = (e: React.KeyboardEvent) => { - // only allow positive integers - // disable common special characters, including - and . - // eslint-disable-next-line no-useless-escape - if (e.key.match(/^[!-\/:-@[-`{-~]$/)) { - e.preventDefault(); - } - }; - const handleChangeFeeRateInput = (e: React.ChangeEvent) => { setFeeRateInput(e.target.value); if (selectedOption !== 'custom') { diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx index ae55ecaec..96c7f4434 100644 --- a/src/app/screens/nftCollection/index.tsx +++ b/src/app/screens/nftCollection/index.tsx @@ -13,6 +13,7 @@ import Nft from '@screens/nftDashboard/nft'; import NftImage from '@screens/nftDashboard/nftImage'; import { NonFungibleToken, StacksCollectionData } from '@secretkeylabs/xverse-core'; import SnackBar from '@ui-library/snackBar'; +import { EMPTY_LABEL } from '@utils/constants'; import { getFullyQualifiedKey, getNftCollectionsGridItemId, isBnsCollection } from '@utils/nfts'; import { PropsWithChildren, useRef } from 'react'; import toast from 'react-hot-toast'; @@ -219,14 +220,14 @@ function NftCollection() { value={ collectionData?.floor_price ? `${collectionData?.floor_price?.toString()} STX` - : '--' + : EMPTY_LABEL } isColumnAlignment={isGalleryOpen} isLoading={isLoading} /> diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx index 1c1c6866d..f06155188 100644 --- a/src/app/screens/nftDetail/index.tsx +++ b/src/app/screens/nftDetail/index.tsx @@ -10,6 +10,7 @@ import TopRow from '@components/topRow'; import { ArrowLeft, ArrowUp, Share } from '@phosphor-icons/react'; import NftImage from '@screens/nftDashboard/nftImage'; import { Attribute } from '@secretkeylabs/xverse-core'; +import { EMPTY_LABEL } from '@utils/constants'; import { useTranslation } from 'react-i18next'; import { Tooltip } from 'react-tooltip'; import styled from 'styled-components'; @@ -371,7 +372,9 @@ function NftDetailScreen() { )} diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index 13d056b27..e73cb84ee 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -16,6 +16,7 @@ import { ArrowUp, Share } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import Callout from '@ui-library/callout'; import { StyledP } from '@ui-library/common.styled'; +import { EMPTY_LABEL } from '@utils/constants'; import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; import { useTranslation } from 'react-i18next'; import { Tooltip } from 'react-tooltip'; @@ -444,7 +445,7 @@ function OrdinalDetailScreen() { @@ -455,7 +456,7 @@ function OrdinalDetailScreen() { value={ ordinal?.inscription_floor_price || ordinal?.inscription_floor_price !== 0 ? ordinal?.inscription_floor_price?.toString() ?? '' - : '--' + : EMPTY_LABEL } allowThousandSeperator={ !!(ordinal?.inscription_floor_price || ordinal?.inscription_floor_price !== 0) diff --git a/src/app/screens/ordinalsCollection/index.tsx b/src/app/screens/ordinalsCollection/index.tsx index 1a368089e..ee7f681be 100644 --- a/src/app/screens/ordinalsCollection/index.tsx +++ b/src/app/screens/ordinalsCollection/index.tsx @@ -18,6 +18,7 @@ import { GridContainer } from '@screens/nftDashboard/collectiblesTabs'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import { Inscription } from '@secretkeylabs/xverse-core'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; +import { EMPTY_LABEL } from '@utils/constants'; import { getInscriptionsCollectionGridItemId, getInscriptionsCollectionGridItemSubText, @@ -164,10 +165,10 @@ function OrdinalsCollection() { const estPortfolioValue = data && data?.pages?.[0].portfolio_value !== 0 ? `${data?.pages?.[0].portfolio_value.toFixed(8)} BTC` - : '--'; + : EMPTY_LABEL; const collectionFloorPrice = collectionMarketData?.floor_price ? `${collectionMarketData?.floor_price?.toFixed(8)} BTC` - : '--'; + : EMPTY_LABEL; const handleOnClick = (item: Inscription) => { setSelectedOrdinalDetails(item); diff --git a/src/app/screens/speedUpTransaction/customFee.tsx b/src/app/screens/speedUpTransaction/customFee.tsx deleted file mode 100644 index 1eacd7cee..000000000 --- a/src/app/screens/speedUpTransaction/customFee.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; -import FiatAmountText from '@components/fiatAmountText'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; -import InputFeedback from '@ui-library/inputFeedback'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const InfoContainer = styled.div((props) => ({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - marginTop: props.theme.spacing(6), - minHeight: 20, -})); - -const TotalFeeText = styled.span((props) => ({ - ...props.theme.typography.body_medium_m, - display: 'flex', - columnGap: props.theme.spacing(2), - color: props.theme.colors.white_200, -})); - -const InputContainer = styled.div<{ withError?: boolean }>((props) => ({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - border: `1px solid ${ - props.withError ? props.theme.colors.danger_dark_200 : props.theme.colors.white_800 - }`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: props.theme.radius(1), - marginTop: props.theme.spacing(4), - padding: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), -})); - -const InputField = styled.input((props) => ({ - ...props.theme.typography.body_medium_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_200, - border: 'transparent', - width: '80%', - '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&[type=number]': { - '-moz-appearance': 'textfield', - }, -})); - -const InputLabel = styled.span((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, -})); - -const FeeText = styled.span((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_0, -})); - -const FeeContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const ControlsContainer = styled.div` - display: flex; - gap: 12px; - margin: 24px 16px 40px; -`; - -const StyledInputFeedback = styled(InputFeedback)` - margin-top: ${(props) => props.theme.spacing(2)}px; -`; - -const StyledFiatAmountText = styled(FiatAmountText)((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, -})); - -const StyledActionButton = styled(ActionButton)((props) => ({ - 'div, h1': { - ...props.theme.typography.body_medium_m, - }, -})); - -export default function CustomFee({ - visible, - onClose, - onClickApply, - calculateTotalFee, - feeRate, - fee, - initialFeeRate, - initialTotalFee, - minimumFeeRate, - isFeeLoading, - error, -}: { - visible: boolean; - onClose: () => void; - onClickApply: (feeRate: string, fee: string) => void; - calculateTotalFee: (feeRate: string) => Promise; - feeRate?: string; - fee?: string; - initialFeeRate: string; - initialTotalFee: string; - minimumFeeRate?: string; - isFeeLoading: boolean; - error: string; -}) { - const { t } = useTranslation('translation'); - const { btcFiatRate, fiatCurrency } = useWalletSelector(); - const [feeRateInput, setFeeRateInput] = useState(feeRate || minimumFeeRate || initialFeeRate); - const [totalFee, setTotalFee] = useState(fee || initialTotalFee); - - const fetchTotalFee = async () => { - const response = await calculateTotalFee(feeRateInput); - - if (response) { - setTotalFee(response.toString()); - } - }; - - useEffect(() => { - fetchTotalFee(); - }, [feeRateInput]); - - /* callbacks */ - const handleKeyDownFeeRateInput = (e: React.KeyboardEvent) => { - // only allow positive integers - // disable common special characters, including - and . - // eslint-disable-next-line no-useless-escape - if (e.key.match(/^[!-\/:-@[-`{-~]$/)) { - e.preventDefault(); - } - }; - - const handleChangeFeeRateInput = (e: React.ChangeEvent) => { - setFeeRateInput(e.target.value); - }; - - const handleClickApply = () => { - // apply state to parent - onClickApply(feeRateInput, totalFee); - }; - - const fiatFee = totalFee - ? getBtcFiatEquivalent(BigNumber(totalFee), BigNumber(btcFiatRate)) - : BigNumber(0); - - return ( - - - - - - Sats /vB - - - - {error && } - {!error && minimumFeeRate && Number(feeRateInput) >= Number(minimumFeeRate) && ( - <> - - {t('TRANSACTION_SETTING.TOTAL_FEE')} - {value}} - /> - - - - )} - - - - - - - - ); -} diff --git a/src/app/screens/speedUpTransaction/customFee/index.styled.ts b/src/app/screens/speedUpTransaction/customFee/index.styled.ts new file mode 100644 index 000000000..1bcfcf066 --- /dev/null +++ b/src/app/screens/speedUpTransaction/customFee/index.styled.ts @@ -0,0 +1,96 @@ +import ActionButton from '@components/button'; +import FiatAmountText from '@components/fiatAmountText'; +import InputFeedback from '@ui-library/inputFeedback'; +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginLeft: props.theme.spacing(8), + marginRight: props.theme.spacing(8), +})); + +export const InfoContainer = styled.div((props) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: props.theme.spacing(6), + minHeight: 20, +})); + +export const TotalFeeText = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + display: 'flex', + columnGap: props.theme.spacing(2), + color: props.theme.colors.white_200, +})); + +export const InputContainer = styled.div<{ withError?: boolean }>((props) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + border: `1px solid ${ + props.withError ? props.theme.colors.danger_dark_200 : props.theme.colors.white_800 + }`, + backgroundColor: props.theme.colors.elevation1, + borderRadius: props.theme.radius(1), + marginTop: props.theme.spacing(4), + padding: props.theme.spacing(6), + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), +})); + +export const InputField = styled.input((props) => ({ + ...props.theme.typography.body_medium_m, + backgroundColor: 'transparent', + color: props.theme.colors.white_200, + border: 'transparent', + width: '80%', + '&::-webkit-outer-spin-button': { + '-webkit-appearance': 'none', + margin: 0, + }, + '&::-webkit-inner-spin-button': { + '-webkit-appearance': 'none', + margin: 0, + }, + '&[type=number]': { + '-moz-appearance': 'textfield', + }, +})); + +export const InputLabel = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, +})); + +export const FeeText = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, +})); + +export const FeeContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const ControlsContainer = styled.div` + display: flex; + gap: 12px; + margin: 24px 16px 40px; +`; + +export const StyledInputFeedback = styled(InputFeedback)` + margin-top: ${(props) => props.theme.spacing(2)}px; +`; + +export const StyledFiatAmountText = styled(FiatAmountText)((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, +})); + +export const StyledActionButton = styled(ActionButton)((props) => ({ + 'div, h1': { + ...props.theme.typography.body_medium_m, + }, +})); diff --git a/src/app/screens/speedUpTransaction/customFee/index.tsx b/src/app/screens/speedUpTransaction/customFee/index.tsx new file mode 100644 index 000000000..0435509fd --- /dev/null +++ b/src/app/screens/speedUpTransaction/customFee/index.tsx @@ -0,0 +1,129 @@ +import BottomModal from '@components/bottomModal'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; +import { handleKeyDownFeeRateInput } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { + Container, + ControlsContainer, + FeeContainer, + FeeText, + InfoContainer, + InputContainer, + InputField, + InputLabel, + StyledActionButton, + StyledFiatAmountText, + StyledInputFeedback, + TotalFeeText, +} from './index.styled'; + +export default function CustomFee({ + visible, + onClose, + onClickApply, + calculateTotalFee, + feeRate, + fee, + initialTotalFee, + minimumFeeRate, + isFeeLoading, + error, +}: { + visible: boolean; + onClose: () => void; + onClickApply: (feeRate: string, fee: string) => void; + calculateTotalFee: (feeRate: string) => Promise; + feeRate?: string; + fee?: string; + minimumFeeRate: string; + initialTotalFee: string; + isFeeLoading: boolean; + error: string; +}) { + const { t } = useTranslation('translation', { + keyPrefix: 'TRANSACTION_SETTING', + }); + const { btcFiatRate, fiatCurrency } = useWalletSelector(); + const [feeRateInput, setFeeRateInput] = useState(feeRate || minimumFeeRate); + const [totalFee, setTotalFee] = useState(fee || initialTotalFee); + + const fetchTotalFee = async () => { + const response = await calculateTotalFee(feeRateInput); + + if (response) { + setTotalFee(response.toString()); + } + }; + + useEffect(() => { + fetchTotalFee(); + }, [feeRateInput]); + + const handleChangeFeeRateInput = (e: React.ChangeEvent) => { + setFeeRateInput(e.target.value); + }; + + const handleClickApply = () => { + // apply state to parent + onClickApply(feeRateInput, totalFee); + }; + + const fiatFee = totalFee + ? getBtcFiatEquivalent(BigNumber(totalFee), BigNumber(btcFiatRate)) + : BigNumber(0); + + return ( + + + + + + Sats /vB + + + + {error && } + {!error && minimumFeeRate && Number(feeRateInput) >= Number(minimumFeeRate) && ( + <> + + {t('TOTAL_FEE')} + {value}} + /> + + + + )} + + + + + + + + ); +} diff --git a/src/app/screens/speedUpTransaction/index.styled.ts b/src/app/screens/speedUpTransaction/index.styled.ts index 71889cb67..7da1f58ee 100644 --- a/src/app/screens/speedUpTransaction/index.styled.ts +++ b/src/app/screens/speedUpTransaction/index.styled.ts @@ -19,8 +19,9 @@ export const LoaderContainer = styled.div({ export const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), + paddingLeft: props.theme.spacing(8), + paddingRight: props.theme.spacing(8), + ...props.theme.scrollbar, })); export const DetailText = styled.span((props) => ({ @@ -79,7 +80,7 @@ export const FeeButton = styled.button<{ export const ControlsContainer = styled.div` display: flex; column-gap: 12px; - margin: 38px 16px 40px; + margin: 38px 0px 40px; `; export const CustomFeeIcon = styled(Faders)({ diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index b00fdaf78..60545722b 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -3,6 +3,7 @@ import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.s import { delay } from '@common/utils/ledger'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; +import FiatAmountText from '@components/fiatAmountText'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; import TopRow from '@components/topRow'; import useTransaction from '@hooks/queries/useTransaction'; @@ -12,13 +13,13 @@ import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { CarProfile, Lightning, RocketLaunch, ShootingStar } from '@phosphor-icons/react'; import { - currencySymbolMap, getBtcFiatEquivalent, mempoolApi, rbf, RecommendedFeeResponse, Transport as TransportType, } from '@secretkeylabs/xverse-core'; +import { EMPTY_LABEL } from '@utils/constants'; import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useState } from 'react'; @@ -80,7 +81,9 @@ function SpeedUpTransactionScreen() { const [recommendedFees, setRecommendedFees] = useState(); const [rbfRecommendedFees, setRbfRecommendedFees] = useState(); const { data: transaction } = useTransaction(id!); - const [rbfTransaction, setRbfTransaction] = useState(); + const [rbfTransaction, setRbfTransaction] = useState< + InstanceType | undefined + >(); const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); @@ -95,6 +98,18 @@ function SpeedUpTransactionScreen() { const [customTotalFee, setCustomTotalFee] = useState(); const [customFeeError, setCustomFeeError] = useState(); + /* TODO: Move `fetchRbfData` function logic to a separate hook like + const useRbfTransactionData: (transaction: ReturnType) => { + rbfTransaction?: rbf.RbfTransaction, + rbfTxSummary?: { + currentFee: number; + currentFeeRate: number; + minimumRbfFee: number; + minimumRbfFeeRate: number; + }, + rbfRecommendedFees?: RbfRecommendedFees + } => { // logic here } + */ const fetchRbfData = useCallback(async () => { if (!selectedAccount || !id || !transaction) { return; @@ -122,13 +137,20 @@ function SpeedUpTransactionScreen() { setRecommendedFees(mempoolFees); const rbfRecommendedFeesResponse = await rbfTx.getRbfRecommendedFees(mempoolFees); - setRbfRecommendedFees(rbfRecommendedFeesResponse); + setRbfRecommendedFees( + Object.fromEntries( + Object.entries(rbfRecommendedFeesResponse).sort((a, b) => { + const priorityOrder = ['highest', 'higher', 'high', 'medium']; + return priorityOrder.indexOf(a[0]) - priorityOrder.indexOf(b[0]); + }), + ), + ); } catch (err: any) { console.error(err); } finally { setIsLoading(false); } - }, [selectedAccount, id, transaction, accountType, network.type, seedVault]); + }, [selectedAccount, id, transaction, accountType, network.type, seedVault, btcClient]); useEffect(() => { fetchRbfData(); @@ -157,8 +179,12 @@ function SpeedUpTransactionScreen() { }; const calculateTotalFee = async (feeRate: string) => { - if (rbfTxSummary && Number(feeRate) < rbfTxSummary?.minimumRbfFeeRate) { - setCustomFeeError(t('FEE_TOO_LOW', { minimumFee: rbfTxSummary?.minimumRbfFeeRate })); + if (!rbfTransaction) { + return; + } + + if (rbfTxSummary && Number(feeRate) < rbfTxSummary.minimumRbfFeeRate) { + setCustomFeeError(t('FEE_TOO_LOW', { minimumFee: rbfTxSummary.minimumRbfFeeRate })); return; } @@ -178,6 +204,10 @@ function SpeedUpTransactionScreen() { }; const signAndBroadcastTx = async (transport?: TransportType) => { + if (!rbfTransaction) { + return; + } + if (isLedgerAccount(selectedAccount) && !transport) { return; } @@ -195,8 +225,10 @@ function SpeedUpTransactionScreen() { } catch (err: any) { console.error(err); - if (err?.response?.data && err?.response?.data.includes('insufficient fee')) { - toast.error(t('INSUFFICIENT_FEE')); + if (err?.response?.data) { + if (err.response.data.includes('insufficient fee')) { + toast.error(t('INSUFFICIENT_FEE')); + } } } }; @@ -254,8 +286,8 @@ function SpeedUpTransactionScreen() { }; const handleApplyCustomFee = (feeRate: string, fee: string) => { - if (rbfTxSummary && Number(feeRate) < rbfTxSummary?.minimumRbfFeeRate) { - setCustomFeeError(t('FEE_TOO_LOW', { minimumFee: rbfTxSummary?.minimumRbfFeeRate })); + if (rbfTxSummary && Number(feeRate) < rbfTxSummary.minimumRbfFeeRate) { + setCustomFeeError(t('FEE_TOO_LOW', { minimumFee: rbfTxSummary.minimumRbfFeeRate })); return; } @@ -281,64 +313,48 @@ function SpeedUpTransactionScreen() { const getEstimatedCompletionTime = (feeRate?: number) => { if (!feeRate || !recommendedFees) { - return '--'; + return EMPTY_LABEL; } - if (feeRate < recommendedFees?.hourFee) { - return 'several hours or more'; + if (feeRate < recommendedFees.hourFee) { + return t('TIME.SEVERAL_HOURS_OR_MORE'); } - if (feeRate === recommendedFees?.hourFee) { - return '~1 hour'; + if (feeRate === recommendedFees.hourFee) { + return `~1 ${t('TIME.HOUR')}`; } - if (feeRate > recommendedFees?.hourFee && feeRate <= recommendedFees?.halfHourFee) { - return '~30 mins'; + if (feeRate > recommendedFees.hourFee && feeRate <= recommendedFees.halfHourFee) { + return `~30 ${t('TIME.MINUTES')}`; } - return '~10 mins'; + return `~10 ${t('TIME.MINUTES')}`; + }; + + const iconProps = { + size: 20, + color: theme.colors.tangerine, }; const feeButtonMapping = { medium: { - icon: , + icon: , title: t('MED_PRIORITY'), }, high: { - icon: , + icon: , title: t('HIGH_PRIORITY'), }, higher: { - icon: , + icon: , title: t('HIGHER_PRIORITY'), }, highest: { - icon: , + icon: , title: t('HIGHEST_PRIORITY'), }, }; - const getFiatAmountString = (fiatAmount: BigNumber) => { - if (!fiatAmount) { - return ''; - } - - if (fiatAmount.isLessThan(0.01)) { - return `< ${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; - } - - return ( - `~ ${value}`} - /> - ); - }; - return ( <> @@ -376,18 +392,16 @@ function SpeedUpTransactionScreen() { {rbfRecommendedFees && - Object.entries(rbfRecommendedFees) - .sort((a, b) => { - const priorityOrder = ['highest', 'higher', 'high', 'medium']; - return priorityOrder.indexOf(a[0]) - priorityOrder.indexOf(b[0]); - }) - .map(([key, obj]) => ( + Object.entries(rbfRecommendedFees).map(([key, obj]) => { + const isDisabled = !obj.enoughFunds; + + return ( {feeButtonMapping[key].icon} @@ -414,22 +428,27 @@ function SpeedUpTransactionScreen() { suffix=" Sats" /> ) : ( - '--' + EMPTY_LABEL )} - {obj.fee ? ( - - {getFiatAmountString( - getBtcFiatEquivalent(BigNumber(obj.fee), BigNumber(btcFiatRate)), - )} - - ) : ( - -- {fiatCurrency} - )} - {!obj.enoughFunds && {t('INSUFFICIENT_FUNDS')}} + + {obj.fee ? ( + + ) : ( + `${EMPTY_LABEL} ${fiatCurrency}` + )} + + {isDisabled && {t('INSUFFICIENT_FUNDS')}} - ))} + ); + })} - +
{t('CUSTOM')} {customFeeRate && ( @@ -469,9 +488,13 @@ function SpeedUpTransactionScreen() { />
- {getFiatAmountString( - getBtcFiatEquivalent(BigNumber(customTotalFee), BigNumber(btcFiatRate)), - )} + ) : ( @@ -479,29 +502,29 @@ function SpeedUpTransactionScreen() { )}
+ + + + - - - - - {showCustomFee && ( + {/* TODO: Move this modal and the custom option info above to a separate component */} + {rbfTxSummary && showCustomFee && ( )} diff --git a/src/app/screens/swap/swapInfoBlock/index.tsx b/src/app/screens/swap/swapInfoBlock/index.tsx index d005bb233..00c0e6091 100644 --- a/src/app/screens/swap/swapInfoBlock/index.tsx +++ b/src/app/screens/swap/swapInfoBlock/index.tsx @@ -3,7 +3,11 @@ import SlippageEditIcon from '@assets/img/swap/slippageEdit.svg'; import BottomModal from '@components/bottomModal'; import { SlippageModalContent } from '@screens/swap/slippageModal'; import { UseSwap } from '@screens/swap/types'; -import { SUPPORT_URL_TAB_TARGET, SWAP_SPONSOR_DISABLED_SUPPORT_URL } from '@utils/constants'; +import { + EMPTY_LABEL, + SUPPORT_URL_TAB_TARGET, + SWAP_SPONSOR_DISABLED_SUPPORT_URL, +} from '@utils/constants'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import Switch from 'react-switch'; @@ -101,11 +105,11 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) { -
{swap.swapInfo?.exchangeRate ?? '--'}
+
{swap.swapInfo?.exchangeRate ?? EMPTY_LABEL}
{expandDetail && ( <>
{t('MIN_RECEIVE')}
-
{swap.minReceived ?? '--'}
+
{swap.minReceived ?? EMPTY_LABEL}
{t('SLIPPAGE')}
setShowSlippageModal(true)}> @@ -114,9 +118,9 @@ export function SwapInfoBlock({ swap }: { swap: UseSwap }) {
{t('LP_FEE')}
-
{swap.swapInfo?.lpFee ?? '--'}
+
{swap.swapInfo?.lpFee ?? EMPTY_LABEL}
{t('ROUTE')}
-
{swap.swapInfo?.route ?? '--'}
+
{swap.swapInfo?.route ?? EMPTY_LABEL}
{swap.isServiceRunning && ( <> <> diff --git a/src/app/screens/swap/swapTokenBlock/index.tsx b/src/app/screens/swap/swapTokenBlock/index.tsx index 6978c692c..019d7847d 100644 --- a/src/app/screens/swap/swapTokenBlock/index.tsx +++ b/src/app/screens/swap/swapTokenBlock/index.tsx @@ -3,6 +3,7 @@ import TokenImage from '@components/tokenImage'; import useWalletSelector from '@hooks/useWalletSelector'; import { SwapToken } from '@screens/swap/types'; import { currencySymbolMap } from '@secretkeylabs/xverse-core'; +import { EMPTY_LABEL } from '@utils/constants'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; @@ -125,7 +126,7 @@ function SwapTokenBlock({ {title} {t('BALANCE')}: - {selectedCoin?.balance ?? '--'} + {selectedCoin?.balance ?? EMPTY_LABEL} @@ -146,7 +147,7 @@ function SwapTokenBlock({ !!window.location?.pathname?.match(/op export function formatNumber(value?: string | number) { return value ? new Intl.NumberFormat().format(Number(value)) : '-'; } + +export const handleKeyDownFeeRateInput = (e: React.KeyboardEvent) => { + // only allow positive integers + // disable common special characters, including - and . + // eslint-disable-next-line no-useless-escape + if (e.key.match(/^[!-\/:-@[-`{-~]$/)) { + e.preventDefault(); + } +}; diff --git a/src/locales/en.json b/src/locales/en.json index 3412ce678..25ad19d2d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -814,7 +814,12 @@ "INSUFFICIENT_FEE": "Insufficient fee", "MANUAL_SETTING": "Manual setting", "TX_FEE_UPDATED": "Transaction fee updated", - "FEE_TOO_LOW": "The minimum fee is {{minimumFee}}" + "FEE_TOO_LOW": "The minimum fee is {{minimumFee}}", + "TIME": { + "SEVERAL_HOURS_OR_MORE": "several hours or more", + "HOUR": "hour", + "MINUTES": "mins" + } }, "POST_CONDITION_MESSAGE": { "YOU": "You", From e30276a02b03dbfbe2d9429740ee30df87fc9ad2 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Wed, 10 Jan 2024 10:29:43 +0200 Subject: [PATCH 08/20] Bug fix/rbf support custom seed handler (#739) * replace seed phrase handler param * fix: add empty fallbackApiUrl to fix type errors for now * edit transaction history list prop --------- Co-authored-by: Tim Man --- package-lock.json | 14 +++++++------- package.json | 2 +- .../coinDashboard/transactionsHistoryList.tsx | 4 ++-- src/app/screens/speedUpTransaction/index.tsx | 2 +- src/app/utils/constants.ts | 2 ++ 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index a882f6fc3..8546451b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "7.2.0-60f9ae5", + "@secretkeylabs/xverse-core": "8.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -1733,9 +1733,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "7.2.0-60f9ae5", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.2.0-60f9ae5/b4d99a44aac6a9cd2552f2e4456407530e1c0862", - "integrity": "sha512-nRysYuNGccHF0EaGtj0xI3jDAhYMaOQUBVwMahV9I6UB6NMRqpu+FtBoNd2/5PbSVea6RnhKUtROF2P4XCOjSA==", + "version": "8.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", + "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -15390,9 +15390,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "7.2.0-60f9ae5", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/7.2.0-60f9ae5/b4d99a44aac6a9cd2552f2e4456407530e1c0862", - "integrity": "sha512-nRysYuNGccHF0EaGtj0xI3jDAhYMaOQUBVwMahV9I6UB6NMRqpu+FtBoNd2/5PbSVea6RnhKUtROF2P4XCOjSA==", + "version": "8.0.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", + "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index 4e9e43fa7..849c5ef66 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "7.2.0-60f9ae5", + "@secretkeylabs/xverse-core": "8.0.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", diff --git a/src/app/screens/coinDashboard/transactionsHistoryList.tsx b/src/app/screens/coinDashboard/transactionsHistoryList.tsx index 1405eb6ad..160d0d7c6 100644 --- a/src/app/screens/coinDashboard/transactionsHistoryList.tsx +++ b/src/app/screens/coinDashboard/transactionsHistoryList.tsx @@ -15,12 +15,12 @@ import { CurrencyTypes } from '@utils/constants'; import { formatDate } from '@utils/date'; import { isLedgerAccount } from '@utils/helper'; import { + Tx, isAddressTransactionWithTransfers, isBrc20Transaction, isBrc20TransactionArr, isBtcTransaction, isBtcTransactionArr, - Tx, } from '@utils/transactions/transactions'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -193,7 +193,7 @@ export default function TransactionsHistoryList(props: TransactionsHistoryListPr : selectedAccount.id, network: network.type, esploraProvider: btcClient, - seedVault, + getSeedPhrase: seedVault.getSeed, } : undefined; diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index 60545722b..7204a60b3 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -126,7 +126,7 @@ function SpeedUpTransactionScreen() { : selectedAccount.id, network: network.type, esploraProvider: btcClient, - seedVault, + getSeedPhrase: seedVault.getSeed, }); setRbfTransaction(rbfTx); diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index 6a5737d27..cd9b5254f 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -52,11 +52,13 @@ export const initialNetworksList: SettingsNetwork[] = [ type: 'Mainnet', address: HIRO_MAINNET_DEFAULT, btcApiUrl: BTC_BASE_URI_MAINNET, + fallbackBtcApiUrl: '', }, { type: 'Testnet', address: HIRO_TESTNET_DEFAULT, btcApiUrl: BTC_BASE_URI_TESTNET, + fallbackBtcApiUrl: '', }, ]; From e38eb594bf6ae139fe1f75ec4959c33e4afec92b Mon Sep 17 00:00:00 2001 From: fede erbes Date: Wed, 10 Jan 2024 14:42:33 +0100 Subject: [PATCH 09/20] feat: add btc consolidation logic and new ui components to psbt and batch psbt screens (#726) * Add skeleton for PSBT signing * Update core * Remove old file * Add ledger support * update core * Update core * feat: add new components for btc tx and use them in psbt and batch psbt tx screens * fix: double scroll in popup windows * fix: rare sats sort and scientific notation in amount component * chore: clean up code * chore: add anchor option to callout and use it in the warning of assets that are spent as payment * feat: unconfirmed callout (#728) * chore: add review suggestions * chore: small improvements * fix: scientific notation in inputs output value * fix: ui issue when content type exceeds container size and icon is placeholder * Fix PSBT signing with Core update * chore: add showContentTypeThumbnail prop to ordinal image component and use it in psbt screens (#737) * chore: add showContentTypeThumbnail prop to ordinal image component and use it in psbt screens * chore: remove bold weight * chore: update core version to dummy utxo fixes branch * chore: update core version to fix ledger input issue --------- Co-authored-by: victorkirov Co-authored-by: Abdul Haseeb --- package-lock.json | 366 ++++++++++--- package.json | 3 +- .../confirmBtcTransaction/index.tsx | 229 ++++++++ .../confirmBtcTransaction/itemRow/amount.tsx | 86 +++ .../amountWithInscriptionSatribute.tsx | 128 +++++ .../itemRow/bundleTxView.tsx | 97 ++++ .../itemRow/inscription.tsx | 117 ++++ .../itemRow/inscriptionSatributeRow.tsx | 80 +++ .../itemRow/rareSats.tsx | 125 +++++ .../confirmBtcTransaction/receiveSection.tsx | 126 +++++ .../transactionSummary.tsx | 147 ++++++ .../confirmBtcTransaction/transferSection.tsx | 122 +++++ .../txInOutput/transactionInput.tsx | 85 +++ .../txInOutput/transactionOutput.tsx | 78 +++ .../txInOutput/txInOutput.tsx | 103 ++++ .../components/confirmBtcTransaction/utils.ts | 269 ++++++++++ .../confirmBtcTransactionComponent/bundle.tsx | 3 +- .../bundleItem.tsx | 22 +- src/app/components/screenContainer/index.tsx | 3 +- src/app/components/transferFeeView/index.tsx | 110 ++-- src/app/hooks/useSignPsbtTx.ts | 25 +- src/app/hooks/useTransactionContext.ts | 49 ++ src/app/screens/ordinals/ordinalImage.tsx | 27 +- .../screens/signBatchPsbtRequest/index.tsx | 248 ++++----- .../signPsbtRequest/bundleItemsComponent.tsx | 219 -------- src/app/screens/signPsbtRequest/index.tsx | 499 +++--------------- .../signPsbtRequest/tempMockDataUtil.ts | 143 +++++ .../useSignPsbtValidationGate.ts | 56 ++ src/app/screens/speedUpTransaction/index.tsx | 4 +- src/app/ui-library/avatar.tsx | 33 ++ src/app/ui-library/callout.tsx | 34 +- src/app/ui-library/divider.tsx | 11 + .../rareSats/ic_ordinal_small_over_card.svg | 6 + src/assets/img/rareSats/link.svg | 8 + src/locales/en.json | 12 +- src/pages/Popup/index.css | 7 +- src/pages/Popup/index.html | 4 +- 37 files changed, 2726 insertions(+), 958 deletions(-) create mode 100644 src/app/components/confirmBtcTransaction/index.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/amount.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/inscription.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx create mode 100644 src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx create mode 100644 src/app/components/confirmBtcTransaction/receiveSection.tsx create mode 100644 src/app/components/confirmBtcTransaction/transactionSummary.tsx create mode 100644 src/app/components/confirmBtcTransaction/transferSection.tsx create mode 100644 src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx create mode 100644 src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx create mode 100644 src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx create mode 100644 src/app/components/confirmBtcTransaction/utils.ts create mode 100644 src/app/hooks/useTransactionContext.ts delete mode 100644 src/app/screens/signPsbtRequest/bundleItemsComponent.tsx create mode 100644 src/app/screens/signPsbtRequest/tempMockDataUtil.ts create mode 100644 src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts create mode 100644 src/app/ui-library/avatar.tsx create mode 100644 src/app/ui-library/divider.tsx create mode 100644 src/assets/img/rareSats/ic_ordinal_small_over_card.svg create mode 100644 src/assets/img/rareSats/link.svg diff --git a/package-lock.json b/package-lock.json index 8546451b9..ff4840676 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "8.0.0", + "@scure/btc-signer": "^1.1.1", + "@secretkeylabs/xverse-core": "8.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", @@ -1658,9 +1659,9 @@ ] }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.4.tgz", + "integrity": "sha512-wznebWtt+ejH8el87yuD4i9xLSbYZXf1Pe4DY0o/zq/eg5I0VQVXVbFs6XIM0pNVCJ/uE3t5wI9kh90mdLUxtw==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1732,10 +1733,32 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/btc-signer/node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "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": "8.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", - "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", + "version": "8.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.1/4ad617c75c0435ca61da77d8d0cf0d4b961d8d87", + "integrity": "sha512-Y6qH74fUZ4Wv8CX/7Ax/9DMf7HfW4GYIgdsn2a8mgCMyAxre7MPcomxmzWSvK8CViHim18b6K2P+wNOOE4l70Q==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -1803,22 +1826,58 @@ "dev": true }, "node_modules/@stacks/auth": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.9.0.tgz", - "integrity": "sha512-tBOB+H/96TUNK9pKmr1YQoiIItUFp2ms5RCNYPSjy3/lbIYYJYtw/O2fOS78fVQvCCpuObhhO65AVsrE/IzQeg==", - "dependencies": { - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", - "@stacks/profile": "^6.9.0", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.10.0.tgz", + "integrity": "sha512-5/FdD1btPovJTckVfaNdD+J/JxtKwn1jVOcyiuDwOABMOMGykgH8ZdXc0Kho2POZoOwKTIrG8sYH5TolSoH7BA==", + "dependencies": { + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/profile": "^6.10.0", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" } }, + "node_modules/@stacks/auth/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/auth/node_modules/@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@stacks/auth/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@stacks/common": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", - "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.10.0.tgz", + "integrity": "sha512-6x5Z7AKd9/kj3+DYE9xIDIkFLHihBH614i2wqrZIjN02WxVo063hWSjIlUxlx8P4gl6olVzlOy5LzhLJD9OP0A==", "dependencies": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -1853,7 +1912,7 @@ "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.8.1", + "@stacks/common": "^6.10.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -1882,36 +1941,105 @@ } }, "node_modules/@stacks/profile": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.9.0.tgz", - "integrity": "sha512-sIR60DsAHi8C6zGqKqSe1r2hXTMHgwrJkX3fAaP3de40KeplZ2bkE+0B83yismEeU2baNc+AukyVvWJv0PfP0A==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.10.0.tgz", + "integrity": "sha512-n2H1Imuu7UfwIvUv7xgHiSb3CKfbP4H+Jzy1+w73njYX7glt60uQ1SjWef7gvMgMFQNTAPELqitweyA+UII6Hg==", "dependencies": { - "@stacks/common": "^6.8.1", - "@stacks/network": "^6.8.1", - "@stacks/transactions": "^6.9.0", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/transactions": "^6.10.0", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" } }, + "node_modules/@stacks/profile/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/profile/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@stacks/profile/node_modules/@stacks/transactions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.11.0.tgz", + "integrity": "sha512-+zIDqn9j4H/+o1ER8C9rFpig1fyrQcj2hVGNIrp+YbpPyja+cxv3fPk6kI/gePzwggzxRgUkIWhBc+mZAXuXyQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + }, "node_modules/@stacks/stacks-blockchain-api-types": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-6.1.1.tgz", "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "node_modules/@stacks/storage": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.9.0.tgz", - "integrity": "sha512-aZ3tOnwRSk5cHQh9ButhfHDvAylNVxPRQzeSB8PydHfyib4XL7fSAJwizzEWNgJV4dovqW2Nsy8gm/4rM/oFKQ==", - "dependencies": { - "@stacks/auth": "^6.9.0", - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.10.0.tgz", + "integrity": "sha512-DLinjJkCN9Q7Yu5yelcXfP89CIDZ4TqXisjJYRqRlFfQdoDWDFZT5vpM2C8U2xDmgzVxfjg90HmQpIjTeIMSnw==", + "dependencies": { + "@stacks/auth": "^6.10.0", + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" } }, + "node_modules/@stacks/storage/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/storage/node_modules/@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@stacks/storage/node_modules/@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "dependencies": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@stacks/transactions": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.9.0.tgz", @@ -15341,9 +15469,9 @@ "optional": true }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.4.tgz", + "integrity": "sha512-wznebWtt+ejH8el87yuD4i9xLSbYZXf1Pe4DY0o/zq/eg5I0VQVXVbFs6XIM0pNVCJ/uE3t5wI9kh90mdLUxtw==" }, "@scure/bip32": { "version": "1.1.3", @@ -15387,12 +15515,27 @@ "@noble/hashes": "~1.3.3", "@scure/base": "~1.1.4", "micro-packed": "~0.4.0" + }, + "dependencies": { + "@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "requires": { + "@noble/hashes": "1.3.3" + } + }, + "@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": "8.0.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.0/1847cfb905bcbed1164961685131936c01892ac0", - "integrity": "sha512-3vz6g7tr5oEeluKii0KHBlR3RqZBuDrI25PLYgEJs1O5tQwlQflK8ad5yWGj+CS3qY84853JSYJEirVvoMzJkw==", + "version": "8.0.1", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/8.0.1/4ad617c75c0435ca61da77d8d0cf0d4b961d8d87", + "integrity": "sha512-Y6qH74fUZ4Wv8CX/7Ax/9DMf7HfW4GYIgdsn2a8mgCMyAxre7MPcomxmzWSvK8CViHim18b6K2P+wNOOE4l70Q==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -15454,22 +15597,54 @@ "dev": true }, "@stacks/auth": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.9.0.tgz", - "integrity": "sha512-tBOB+H/96TUNK9pKmr1YQoiIItUFp2ms5RCNYPSjy3/lbIYYJYtw/O2fOS78fVQvCCpuObhhO65AVsrE/IzQeg==", - "requires": { - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", - "@stacks/profile": "^6.9.0", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-6.10.0.tgz", + "integrity": "sha512-5/FdD1btPovJTckVfaNdD+J/JxtKwn1jVOcyiuDwOABMOMGykgH8ZdXc0Kho2POZoOwKTIrG8sYH5TolSoH7BA==", + "requires": { + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/profile": "^6.10.0", "cross-fetch": "^3.1.5", "jsontokens": "^4.0.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + } } }, "@stacks/common": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.8.1.tgz", - "integrity": "sha512-ewL9GLZNQYa5a/3K4xSHlHIgHkD4rwWW/QEaPId8zQIaL+1O9qCaF4LX9orNQeOmEk8kvG0x2xGV54fXKCZeWQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-6.10.0.tgz", + "integrity": "sha512-6x5Z7AKd9/kj3+DYE9xIDIkFLHihBH614i2wqrZIjN02WxVo063hWSjIlUxlx8P4gl6olVzlOy5LzhLJD9OP0A==", "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4" @@ -15504,7 +15679,7 @@ "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.1", "@scure/bip39": "1.1.0", - "@stacks/common": "^6.8.1", + "@stacks/common": "^6.10.0", "@types/node": "^18.0.4", "base64-js": "^1.5.1", "bs58": "^5.0.0", @@ -15529,16 +15704,45 @@ } }, "@stacks/profile": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.9.0.tgz", - "integrity": "sha512-sIR60DsAHi8C6zGqKqSe1r2hXTMHgwrJkX3fAaP3de40KeplZ2bkE+0B83yismEeU2baNc+AukyVvWJv0PfP0A==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-6.10.0.tgz", + "integrity": "sha512-n2H1Imuu7UfwIvUv7xgHiSb3CKfbP4H+Jzy1+w73njYX7glt60uQ1SjWef7gvMgMFQNTAPELqitweyA+UII6Hg==", "requires": { - "@stacks/common": "^6.8.1", - "@stacks/network": "^6.8.1", - "@stacks/transactions": "^6.9.0", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "@stacks/transactions": "^6.10.0", "jsontokens": "^4.0.1", "schema-inspector": "^2.0.2", "zone-file": "^2.0.0-beta.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + }, + "@stacks/transactions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.11.0.tgz", + "integrity": "sha512-+zIDqn9j4H/+o1ER8C9rFpig1fyrQcj2hVGNIrp+YbpPyja+cxv3fPk6kI/gePzwggzxRgUkIWhBc+mZAXuXyQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@stacks/common": "^6.10.0", + "@stacks/network": "^6.10.0", + "c32check": "^2.0.0", + "lodash.clonedeep": "^4.5.0" + } + } } }, "@stacks/stacks-blockchain-api-types": { @@ -15547,16 +15751,48 @@ "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, "@stacks/storage": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.9.0.tgz", - "integrity": "sha512-aZ3tOnwRSk5cHQh9ButhfHDvAylNVxPRQzeSB8PydHfyib4XL7fSAJwizzEWNgJV4dovqW2Nsy8gm/4rM/oFKQ==", - "requires": { - "@stacks/auth": "^6.9.0", - "@stacks/common": "^6.8.1", - "@stacks/encryption": "^6.9.0", - "@stacks/network": "^6.8.1", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.10.0.tgz", + "integrity": "sha512-DLinjJkCN9Q7Yu5yelcXfP89CIDZ4TqXisjJYRqRlFfQdoDWDFZT5vpM2C8U2xDmgzVxfjg90HmQpIjTeIMSnw==", + "requires": { + "@stacks/auth": "^6.10.0", + "@stacks/common": "^6.10.0", + "@stacks/encryption": "^6.10.0", + "@stacks/network": "^6.10.0", "base64-js": "^1.5.1", "jsontokens": "^4.0.1" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@stacks/encryption": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-6.11.0.tgz", + "integrity": "sha512-VfBkrwmCRppCasJo+R/hWfC7vgS6GmfPyoTeDsoYlfRRXz/auFbEdRaaruFPtAda/1nKdDOZ9UZEMOp5AIw0IQ==", + "requires": { + "@noble/hashes": "1.1.5", + "@noble/secp256k1": "1.7.1", + "@scure/bip39": "1.1.0", + "@stacks/common": "^6.10.0", + "@types/node": "^18.0.4", + "base64-js": "^1.5.1", + "bs58": "^5.0.0", + "ripemd160-min": "^0.0.6", + "varuint-bitcoin": "^1.1.2" + } + }, + "@stacks/network": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-6.10.0.tgz", + "integrity": "sha512-mbiZ8nlsyy77ndmBdaqhHXii22IFdK4ThRcOQs9j/O00DkAr04jCM4GV5Q+VLUnZ9OBoJq7yOV7Pf6jglh+0hw==", + "requires": { + "@stacks/common": "^6.10.0", + "cross-fetch": "^3.1.5" + } + } } }, "@stacks/transactions": { diff --git a/package.json b/package.json index 849c5ef66..a72703e37 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", "@react-spring/web": "^9.6.1", - "@secretkeylabs/xverse-core": "8.0.0", + "@scure/btc-signer": "^1.1.1", + "@secretkeylabs/xverse-core": "8.0.1", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.9.0", diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx new file mode 100644 index 000000000..c042ca43f --- /dev/null +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -0,0 +1,229 @@ +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 BottomModal from '@components/bottomModal'; +import ActionButton from '@components/button'; +import LedgerConnectionView from '@components/ledger/connectLedgerView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import TransportFactory from '@ledgerhq/hw-transport-webusb'; +import { btcTransaction, Transport } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; +import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; +import { isLedgerAccount } from '@utils/helper'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MoonLoader } from 'react-spinners'; +import styled from 'styled-components'; +import SendLayout from '../../layouts/sendLayout'; +import TransactionSummary from './transactionSummary'; + +const LoaderContainer = styled.div(() => ({ + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', +})); + +const ReviewTransactionText = styled(StyledP)` + text-align: left; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +const BroadcastCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +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 = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput; + isLoading: boolean; + isSubmitting: boolean; + isBroadcast?: boolean; + isError?: boolean; + showAccountHeader?: boolean; + hideBottomBar?: boolean; + cancelText: string; + confirmText: string; + onConfirm: (ledgerTransport?: Transport) => void; + onCancel: () => void; + onBackClick?: () => void; + confirmDisabled?: boolean; + getFeeForFeeRate?: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; + onFeeRateSet?: (feeRate: number) => void; +}; + +function ConfirmBtcTransaction({ + inputs, + outputs, + feeOutput, + isLoading, + isSubmitting, + isBroadcast, + isError = false, + cancelText, + confirmText, + onConfirm, + onCancel, + onBackClick, + showAccountHeader, + hideBottomBar, + confirmDisabled = false, + getFeeForFeeRate, + onFeeRateSet, +}: Props) { + const [isModalVisible, setIsModalVisible] = useState(false); + const [currentStepIndex, setCurrentStepIndex] = useState(0); + 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 { selectedAccount } = useWalletSelector(); + + const hideBackButton = !onBackClick; + + const onConfirmPress = async () => { + if (!isLedgerAccount(selectedAccount)) { + return onConfirm(); + } + + // show ledger connection screens + setIsModalVisible(true); + }; + + const handleConnectAndConfirm = async () => { + if (!selectedAccount) { + console.error('No account selected'); + return; + } + setIsButtonDisabled(true); + + const transport = await TransportFactory.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + setCurrentStepIndex(1); + + try { + onConfirm(transport); + } catch (err) { + console.error(err); + setIsTxRejected(true); + } + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsConnectSuccess(false); + setCurrentStepIndex(0); + }; + + // TODO: this is a bit naive, but should be correct. We may want to look at the sig hash types of the inputs instead + const isPartialTransaction = !feeOutput; + + return isLoading ? ( + + + + ) : ( + <> + + + {t('REVIEW_TRANSACTION')} + + {!isBroadcast && } + + {!isLoading && ( + + + + + )} + + setIsModalVisible(false)}> + {currentStepIndex === 0 && ( + + )} + {currentStepIndex === 1 && ( + + )} + + + + + + + ); +} + +export default ConfirmBtcTransaction; diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx new file mode 100644 index 000000000..23b7a8894 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -0,0 +1,86 @@ +import TokenImage from '@components/tokenImage'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { currencySymbolMap, getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; +import Avatar from '@ui-library/avatar'; +import { StyledP } from '@ui-library/common.styled'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; + +type Props = { + amount: number; +}; + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const NumberTypeContainer = styled.div` + text-align: right; +`; + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +export default function Amount({ amount }: Props) { + const { btcFiatRate, fiatCurrency } = useWalletSelector(); + const { t } = useTranslation('translation'); + + const getFiatAmountString = (amountParam: number, btcFiatRateParam: string) => { + const fiatAmount = getBtcFiatEquivalent( + new BigNumber(amountParam), + BigNumber(btcFiatRateParam), + ); + if (!fiatAmount) { + return ''; + } + + if (fiatAmount.isLessThan(0.01)) { + return `<${currencySymbolMap[fiatCurrency]}0.01 ${fiatCurrency}`; + } + + return ( + `~ ${value}`} + /> + ); + }; + + return ( + + + } /> + + +
+ + {t('CONFIRM_TRANSACTION.AMOUNT')} + +
+ + {value}} + /> + + {getFiatAmountString(amount, btcFiatRate)} + + +
+
+ ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx new file mode 100644 index 000000000..b7f790d4c --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx @@ -0,0 +1,128 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { WarningOctagon } from '@phosphor-icons/react'; +import { animated, config, useSpring } from '@react-spring/web'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; +import { mapTxSatributeInfoToBundleInfo } from '../utils'; +import Inscription from './inscription'; + +const WarningContainer = styled.div` + display: flex; + flex-direction: column; + border-radius: ${(props) => props.theme.space.s}; +`; + +const WarningButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; + padding-top: ${(props) => props.theme.space.m}; +`; + +const ItemsContainer = styled(animated.div)((props) => ({ + borderRadius: props.theme.space.s, + backgroundColor: props.theme.colors.elevation2, + padding: `${props.theme.space.s} 0`, + marginTop: props.theme.space.m, +})); + +const Range = styled.div` + padding: 0 ${(props) => props.theme.space.s}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const Title = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xxs}; +`; + +export default function AmountWithInscriptionSatribute({ + satributes, + inscriptions, + onShowInscription, +}: { + satributes: btcTransaction.IOSatribute[]; + inscriptions: btcTransaction.IOInscription[]; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}) { + const [showBundleDetail, setShowBundleDetail] = useState(false); + + const { t } = useTranslation('translation'); + const { hasActivatedRareSatsKey } = useWalletSelector(); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: showBundleDetail ? 1 : 0, + height: showBundleDetail ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + // we only show the satributes if the user has activated the rare sats key + const satributesArray = hasActivatedRareSatsKey ? satributes : []; + const amountOfAssets = satributesArray.length + inscriptions.length; + + return amountOfAssets > 0 ? ( + + setShowBundleDetail((prevState) => !prevState)}> + + + + {t( + `CONFIRM_TRANSACTION.${ + satributesArray.length ? 'INSCRIBED_RARE_SATS' : 'INSCRIBED_SATS' + }`, + )} + + + + + + {showBundleDetail && ( + + {inscriptions.map((item: btcTransaction.IOInscription, index: number) => ( +
+ + + + {(satributesArray.length || inscriptions.length > index + 1) && ( + + )} +
+ ))} + {satributesArray.map((item: btcTransaction.IOSatribute, index: number) => ( +
+ + + + {satributesArray.length > index + 1 && } +
+ ))} +
+ )} +
+ ) : null; +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx b/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx new file mode 100644 index 000000000..da3b22b90 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/bundleTxView.tsx @@ -0,0 +1,97 @@ +import Link from '@assets/img/rareSats/link.svg'; +import { CubeTransparent } from '@phosphor-icons/react'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Avatar from '../../../ui-library/avatar'; +import { SatRangeTx } from '../utils'; +import Inscription from './inscription'; +import RareSats from './rareSats'; + +type Props = { + inscriptions: btcTransaction.IOInscription[]; + satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; + bundleSize: number; + isRareSatsEnabled?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +const Header = styled.div((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginBottom: props.theme.space.m, +})); + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +const RowsContainer = styled.div` + padding-left: ${(props) => props.theme.space.m}; +`; +const LinkContainer = styled.div` + display: flex; + width: 32px; + justify-content: center; + margin: ${(props) => props.theme.space.xxxs} 0; +`; + +export default function BundleTxView({ + inscriptions, + satributesInfo, + bundleSize, + isRareSatsEnabled, + onShowInscription, +}: Props) { + const { t } = useTranslation('translation'); + + // we only show rare sats if there are any and the user has enabled the feature + const showRareSats = satributesInfo.totalExoticSats > 0 && isRareSatsEnabled; + + return ( + <> +
+ + } /> + +
+ + {t('COMMON.BUNDLE')} + + {bundleSize && ( + ( + + {value} + + )} + /> + )} +
+
+ + {inscriptions.map((inscription, index) => ( +
+ + {!!(inscriptions.length > index + 1 || showRareSats) && ( + + link + + )} +
+ ))} + {showRareSats && } +
+ + ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx new file mode 100644 index 000000000..400575026 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx @@ -0,0 +1,117 @@ +import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small_over_card.svg'; +import { Eye } from '@phosphor-icons/react'; +import OrdinalImage from '@screens/ordinals/ordinalImage'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Avatar from '@ui-library/avatar'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; + +type Props = { + inscription: btcTransaction.IOInscription; + bundleSize?: number; + hideTypeSizeInfo?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +const RowCenter = styled.div<{ spaceBetween?: boolean; gap?: boolean }>((props) => ({ + display: 'flex', + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', + gap: props.gap ? props.theme.space.xs : 0, +})); + +const InscriptionNumberContainer = styled.button` + background-color: transparent; +`; + +const NumberTypeContainer = styled.div` + text-align: right; +`; + +const AvatarContainer = styled.div` + margin-right: ${(props) => props.theme.space.xs}; +`; + +const InscriptionNumber = styled(StyledP)` + margin-right: ${(props) => props.theme.space.xs}; +`; +const ContentType = styled(StyledP)` + word-break: break-word; +`; + +export default function Inscription({ + inscription, + bundleSize, + hideTypeSizeInfo = false, + onShowInscription, +}: Props) { + const { t } = useTranslation('translation'); + + return ( + + + + } + /> + + +
+ {!hideTypeSizeInfo && ( + <> + + {t('COMMON.INSCRIPTION')} + + {bundleSize && ( + ( + + {value} + + )} + /> + )} + + )} +
+ + { + onShowInscription(inscription); + }} + > + + + {inscription.number} + + + + + + {inscription.contentType} + + +
+
+ ); +} diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx new file mode 100644 index 000000000..beca09ac1 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx @@ -0,0 +1,80 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Divider from '@ui-library/divider'; +import styled from 'styled-components'; +import { getSatRangesWithInscriptions } from '../utils'; +import Amount from './amount'; +import BundleTxView from './bundleTxView'; +import Inscription from './inscription'; +import RareSats from './rareSats'; + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +type Props = { + inscriptions: btcTransaction.IOInscription[]; + satributes: btcTransaction.IOSatribute[]; + amount: number; + showBottomDivider?: boolean; + showTopDivider?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +function InscriptionSatributeRow({ + inscriptions, + satributes, + amount, + showBottomDivider, + showTopDivider, + onShowInscription, +}: Props) { + const { hasActivatedRareSatsKey } = useWalletSelector(); + + const satributesInfo = getSatRangesWithInscriptions({ + satributes, + inscriptions, + amount, + }); + + const getRow = () => { + if (inscriptions.length > 0 && inscriptions.length + satributes.length > 1) { + return ( + + ); + } + + if (inscriptions.length) { + return ( + + ); + } + + // if rare sats is disabled we show the amount of btc + if (!hasActivatedRareSatsKey) { + return ; + } + + return ; + }; + + return ( + <> + {showTopDivider && } + {getRow()} + {showBottomDivider && } + + ); +} + +export default InscriptionSatributeRow; diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx new file mode 100644 index 000000000..f6c8de8e3 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -0,0 +1,125 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; +import { Butterfly } from '@phosphor-icons/react'; +import { animated, config, useSpring } from '@react-spring/web'; +import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Avatar from '../../../ui-library/avatar'; +import { mapTxSatributeInfoToBundleInfo, SatRangeTx } from '../utils'; + +const SatsBundleContainer = styled.div` + display: flex; + flex-direction: column; + border-radius: ${(props) => props.theme.space.s}; +`; + +const SatsBundleButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const RangesContainer = styled(animated.div)((props) => ({ + borderRadius: props.theme.space.s, + backgroundColor: props.theme.colors.elevation2, + padding: `${props.theme.space.s} 0`, + marginTop: props.theme.space.m, +})); + +const Range = styled.div` + padding: 0 ${(props) => props.theme.space.s}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const BundleInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: ${(props) => props.theme.space.s}; +`; + +function RareSats({ + satributesInfo, + bundleSize, +}: { + satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; + bundleSize?: number; +}) { + const [showBundleDetail, setShowBundleDetail] = useState(false); + + const { t } = useTranslation('translation'); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: showBundleDetail ? 1 : 0, + height: showBundleDetail ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + return ( + + setShowBundleDetail((prevState) => !prevState)} + > + + } /> + + {`${ + satributesInfo.totalExoticSats + } ${t('NFT_DASHBOARD_SCREEN.RARE_SATS')}`} + {bundleSize && ( + ( + + {value} + + )} + /> + )} + + + + + + {showBundleDetail && ( + + {satributesInfo.satRanges.map((item: SatRangeTx, index: number) => ( +
+ + + + {satributesInfo.satRanges.length > index + 1 && } +
+ ))} +
+ )} +
+ ); +} + +export default RareSats; diff --git a/src/app/components/confirmBtcTransaction/receiveSection.tsx b/src/app/components/confirmBtcTransaction/receiveSection.tsx new file mode 100644 index 000000000..9aa8e6923 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/receiveSection.tsx @@ -0,0 +1,126 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { ArrowRight } from '@phosphor-icons/react'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; +import Amount from './itemRow/amount'; +import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import { getOutputsWithAssetsToUserAddress } from './utils'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); +const AddressLabel = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +type Props = { + outputs: btcTransaction.EnhancedOutput[]; + netAmount: number; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; +function ReceiveSection({ outputs, netAmount, onShowInscription }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ + outputs, + btcAddress, + ordinalsAddress, + }); + + const inscriptionsRareSatsInPayment = outputsToPayment.filter( + (output) => output.inscriptions.length > 0 || output.satributes.length > 0, + ); + const areInscriptionsRareSatsInPayment = inscriptionsRareSatsInPayment.length > 0; + const showPaymentSection = areInscriptionsRareSatsInPayment || netAmount > 0; + + return ( + <> + {!!outputsToOrdinal.length && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + {t('YOUR_ORDINAL_ADDRESS')} + +
+ {outputsToOrdinal + .sort((a, b) => b.inscriptions.length - a.inscriptions.length) + .map((output, index) => ( + index + 1} + /> + ))} +
+ )} + {showPaymentSection && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + {t('YOUR_PAYMENT_ADDRESS')} + +
+ {netAmount > 0 && ( + + + + )} + {inscriptionsRareSatsInPayment + .sort((a, b) => b.inscriptions.length - a.inscriptions.length) + .map((output, index) => ( + index + 1} + /> + ))} +
+ )} + + ); +} + +export default ReceiveSection; diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx new file mode 100644 index 000000000..013071812 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -0,0 +1,147 @@ +import TransactionDetailComponent from '@components/transactionDetailComponent'; +import useWalletSelector from '@hooks/useWalletSelector'; + +import AssetModal from '@components/assetModal'; +import TransferFeeView from '@components/transferFeeView'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; +import { BLOG_LINK } from '@utils/constants'; +import BigNumber from 'bignumber.js'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import ReceiveSection from './receiveSection'; +import TransferSection from './transferSection'; +import TxInOutput from './txInOutput/txInOutput'; +import { getNetAmount, isScriptOutput, isSpendOutput } from './utils'; + +const ScriptCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.s}; +`; +const InscribedRareSatWarning = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +const UnconfirmedInputCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +type Props = { + isPartialTransaction: boolean; + + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput; + + // TODO: these are for txn screens which we will tackle next + // TODO: By having these as generic props here, we can use the generic set fee rate component for all use cases + getFeeForFeeRate?: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; + onFeeRateSet?: (feeRate: number) => void; + // TODO: use this to disable the edit fee component when it is created + isSubmitting?: boolean; +}; + +function TransactionSummary({ + inputs, + outputs, + feeOutput, + isPartialTransaction, + isSubmitting, + getFeeForFeeRate, + onFeeRateSet, +}: Props) { + const [inscriptionToShow, setInscriptionToShow] = useState< + btcTransaction.IOInscription | undefined + >(undefined); + + const { network } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { t: rareSatsT } = useTranslation('translation', { keyPrefix: 'RARE_SATS' }); + const { btcAddress, ordinalsAddress } = useWalletSelector(); + + const hasOutputScript = outputs.some((output) => isScriptOutput(output)); + + const netAmount = getNetAmount({ + inputs, + outputs, + btcAddress, + ordinalsAddress, + }); + + const isUnConfirmedInput = inputs.some((input) => !input.extendedUtxo.utxo.status.confirmed); + + const paymentHasInscribedRareSats = isPartialTransaction + ? inputs.some( + (input) => + input.extendedUtxo.address === btcAddress && + (input.inscriptions.length || input.satributes.length), + ) + : outputs.some( + (output) => + isSpendOutput(output) && + (output.inscriptions.some((inscription) => inscription.fromAddress === btcAddress) || + output.satributes.some((satribute) => satribute.fromAddress === btcAddress)), + ); + const feesHaveInscribedRareSats = feeOutput?.inscriptions.length || feeOutput?.satributes.length; + const showInscribeRareSatWarning = paymentHasInscribedRareSats || feesHaveInscribedRareSats; + + return ( + <> + {inscriptionToShow && ( + setInscriptionToShow(undefined)} + inscription={{ + content_type: inscriptionToShow.contentType, + id: inscriptionToShow.id, + inscription_number: inscriptionToShow.number, + }} + /> + )} + + {!!showInscribeRareSatWarning && ( + + )} + + {isUnConfirmedInput && ( + + )} + + + + + + + + {hasOutputScript && } + + + + {feeOutput && ( + + )} + + ); +} + +export default TransactionSummary; diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx new file mode 100644 index 000000000..ab3b917d9 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -0,0 +1,122 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Amount from './itemRow/amount'; +import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; +import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import { getInputsWitAssetsFromUserAddress, getOutputsWithAssetsFromUserAddress } from './utils'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + marginBottom: props.theme.space.m, + padding: `0 ${props.theme.space.m}`, +})); + +type Props = { + outputs: btcTransaction.EnhancedOutput[]; + inputs: btcTransaction.EnhancedInput[]; + isPartialTransaction: boolean; + netAmount: number; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; + +// if isPartialTransaction, we use inputs instead of outputs +function TransferSection({ + outputs, + inputs, + isPartialTransaction, + netAmount, + onShowInscription, +}: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const { inputFromPayment, inputFromOrdinal } = getInputsWitAssetsFromUserAddress({ + inputs, + btcAddress, + ordinalsAddress, + }); + const { outputsFromPayment, outputsFromOrdinal } = getOutputsWithAssetsFromUserAddress({ + outputs, + btcAddress, + ordinalsAddress, + }); + + const showAmount = netAmount > 0; + + const inscriptionsFromPayment: btcTransaction.IOInscription[] = []; + const satributesFromPayment: btcTransaction.IOSatribute[] = []; + (isPartialTransaction ? inputFromPayment : outputsFromPayment).forEach((item) => { + inscriptionsFromPayment.push(...item.inscriptions); + satributesFromPayment.push(...item.satributes); + }); + + return ( + +
+ + {t('YOU_WILL_TRANSFER')} + +
+ {showAmount && ( + + + + + )} + {isPartialTransaction + ? inputFromOrdinal.map((input, index) => ( + index + 1} + /> + )) + : outputsFromOrdinal.map((output, index) => ( + index + 1} + /> + ))} +
+ ); +} + +export default TransferSection; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx new file mode 100644 index 000000000..20e9469c1 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -0,0 +1,85 @@ +import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; +import TransferDetailView from '@components/transferDetailView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getTruncatedAddress } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +const TransferDetailContainer = styled.div((props) => ({ + paddingBottom: props.theme.space.m, +})); + +const SubValueText = styled(StyledP)((props) => ({ + color: props.theme.colors.white_400, +})); + +const TxIdText = styled(StyledP)((props) => ({ + marginLeft: props.theme.space.xxs, +})); + +const YourAddressText = styled(StyledP)((props) => ({ + marginRight: props.theme.space.xxs, +})); + +const TxIdContainer = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +type Props = { + input: btcTransaction.EnhancedInput; +}; + +function TransactionInput({ input }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const isPaymentsAddress = input.extendedUtxo.address === btcAddress; + const isOrdinalsAddress = input.extendedUtxo.address === ordinalsAddress; + const isExternalInput = !isPaymentsAddress && !isOrdinalsAddress; + + // 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 ? ( + + ({t('YOUR_ADDRESS')}) + {getTruncatedAddress(addressToBeDisplayed)} + + ) : ( + {getTruncatedAddress(addressToBeDisplayed)} + ); + + return ( + + + {isExternalInput ? ( + + + {getTruncatedAddress(input.extendedUtxo.utxo.txid)} + + (txid) + + ) : ( + renderAddress(input.extendedUtxo.address) + )} + + + ); +} + +export default TransactionInput; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx new file mode 100644 index 000000000..6128e9bcd --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -0,0 +1,78 @@ +import OutputIcon from '@assets/img/transactions/output.svg'; +import ScriptIcon from '@assets/img/transactions/ScriptIcon.svg'; +import TransferDetailView from '@components/transferDetailView'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; +import { getTruncatedAddress } from '@utils/helper'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { isScriptOutput, isSpendOutput } from '../utils'; + +const TransferDetailContainer = styled.div((props) => ({ + paddingBottom: props.theme.spacing(8), +})); + +const SubValueText = styled.h1((props) => ({ + ...props.theme.typography.body_m, + fontSize: 12, + color: props.theme.colors.white_400, +})); + +const YourAddressText = styled.h1((props) => ({ + ...props.theme.typography.body_m, + fontSize: 12, + color: props.theme.colors.white_0, + marginRight: props.theme.spacing(2), +})); + +const TxIdContainer = styled.div({ + display: 'flex', + flexDirection: 'row', +}); + +type Props = { + output: btcTransaction.EnhancedOutput; + scriptOutputCount?: number; +}; + +function TransactionOutput({ output, scriptOutputCount }: Props) { + const { btcAddress, ordinalsAddress } = useWalletSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const outputWithScript = isScriptOutput(output); + + const detailViewIcon = outputWithScript ? ScriptIcon : OutputIcon; + const detailViewHideCopyButton = outputWithScript + ? true + : btcAddress === output.address || ordinalsAddress === output.address; + const detailViewValue = outputWithScript ? ( + {`${t('SCRIPT_OUTPUT')} #${scriptOutputCount}`} + ) : output.address === btcAddress || output.address === ordinalsAddress ? ( + + ({t('YOUR_ADDRESS')}) + {getTruncatedAddress(output.address)} + + ) : ( + {getTruncatedAddress(output.address)} + ); + + return ( + + + {detailViewValue} + + + ); +} + +export default TransactionOutput; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx new file mode 100644 index 000000000..2ff4e71f0 --- /dev/null +++ b/src/app/components/confirmBtcTransaction/txInOutput/txInOutput.tsx @@ -0,0 +1,103 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import { animated, config, useSpring } from '@react-spring/web'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { isScriptOutput } from '../utils'; +import TransactionInput from './transactionInput'; +import TransactionOutput from './transactionOutput'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + borderRadius: props.theme.space.s, + background: props.theme.colors.elevation1, + padding: `${props.theme.space.s} ${props.theme.space.m}`, + marginBottom: props.theme.space.s, +})); + +const Button = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; +`; + +const Row = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const OutputTitleText = styled(StyledP)((props) => ({ + marginBottom: props.theme.space.s, +})); + +const ExpandedContainer = styled(animated.div)({ + display: 'flex', + flexDirection: 'column', + marginTop: 16, +}); + +type Props = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; +}; + +function TxInOutput({ inputs, outputs }: Props) { + const [isExpanded, setIsExpanded] = useState(false); + + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 400 }, + from: { opacity: 0, height: 0 }, + to: { + opacity: isExpanded ? 1 : 0, + height: isExpanded ? 'auto' : 0, + }, + }); + + const arrowRotation = useSpring({ + transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + let scriptOutputCount = 1; + + return ( + + + {isExpanded && ( + + {inputs.map((input) => ( + + ))} + + {t('OUTPUT')} + + {outputs.map((output, index) => ( + + ))} + + )} + + ); +} + +export default TxInOutput; diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts new file mode 100644 index 000000000..ededa2dbb --- /dev/null +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -0,0 +1,269 @@ +import { btcTransaction, BundleSatRange } from '@secretkeylabs/xverse-core'; + +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 isSpendOutput = ( + 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) => { + 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 = + isSpendOutput(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) => { + // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes + const outputsFromPayment: btcTransaction.TransactionOutput[] = []; + const outputsFromOrdinal: btcTransaction.TransactionOutput[] = []; + 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 getInputsWitAssetsFromUserAddress = ({ + btcAddress, + ordinalsAddress, + inputs, +}: Omit) => { + // we want to discard inputs that are not from user address and do not have inscriptions or satributes + const inputFromPayment: btcTransaction.EnhancedInput[] = []; + const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; + inputs.forEach((input) => { + if (!input.inscriptions.length && !input.satributes.length) { + return; + } + + if (input.extendedUtxo.address === btcAddress) { + return inputFromPayment.push(input); + } + if (input.extendedUtxo.address === ordinalsAddress) { + inputFromOrdinal.push(input); + } + }); + + return { inputFromPayment, inputFromOrdinal }; +}; + +export const getOutputsWithAssetsToUserAddress = ({ + btcAddress, + ordinalsAddress, + outputs, +}: Omit) => { + 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) || ![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 }; +}; diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx index 84b728b71..eeabbce28 100644 --- a/src/app/components/confirmBtcTransactionComponent/bundle.tsx +++ b/src/app/components/confirmBtcTransactionComponent/bundle.tsx @@ -3,6 +3,7 @@ import AssetModal from '@components/assetModal'; import { CaretDown } from '@phosphor-icons/react'; import { Bundle, BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; +import Divider from '@ui-library/divider'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -97,8 +98,8 @@ function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { // show ordinal modal to show asset setInscriptionToShow(inscription); }} - showDivider={index !== bundle.satRanges.length - 1} /> + {bundle.satRanges.length > index + 1 && } ))} diff --git a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx index 009220d0b..8bb38bd77 100644 --- a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx +++ b/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx @@ -20,19 +20,11 @@ const Range = styled.div` align-items: center; `; -interface ComponentWithDividerProps { - showDivider: boolean; -} - -const Container = styled.div` - padding-top:${(props) => props.theme.space.s}; - padding-bottom:${(props) => props.theme.space.s}; +const Container = styled.div` display: flex; flex-direction: row; justify-content: space-between; align-items: center; - border-bottom: ${(props) => - props.showDivider ? `1px solid ${props.theme.colors.white_900}` : 'transparent'}; width: 100%; }`; @@ -52,6 +44,7 @@ const InscriptionRow = styled.button` flex-direction: row; align-items: center; background-color: transparent; + cursor: ${(props) => (props.disabled ? 'default' : 'pointer')}; `; const InscriptionText = styled(StyledP)` text-wrap: nowrap; @@ -69,11 +62,9 @@ const BundleText = styled(StyledP)` function BundleItem({ item, ordinalEyePressed, - showDivider, }: { item: BundleSatRange; - ordinalEyePressed: (inscription: SatRangeInscription) => void; - showDivider?: boolean; + ordinalEyePressed?: (inscription: SatRangeInscription) => void; }) { const renderedIcons = () => ( @@ -90,7 +81,7 @@ function BundleItem({ ); return ( - + {renderedIcons()} {getSatLabel(item.satributes)} @@ -110,14 +101,15 @@ function BundleItem({ type="button" key={inscription.id} onClick={() => { - ordinalEyePressed(inscription); + ordinalEyePressed?.(inscription); }} + disabled={!ordinalEyePressed} > ordinal {inscription.inscription_number} - + {ordinalEyePressed && } ))} diff --git a/src/app/components/screenContainer/index.tsx b/src/app/components/screenContainer/index.tsx index 233f2df2a..7152b206c 100644 --- a/src/app/components/screenContainer/index.tsx +++ b/src/app/components/screenContainer/index.tsx @@ -8,7 +8,8 @@ const RouteContainer = styled.div` // any route should default to the chrome extension window size display: flex; flex-direction: column; - height: 600px; + height: 100%; + max-height: 600px; width: 360px; margin: auto; background-color: ${(props) => props.theme.colors.elevation0}; diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 5f0848a2e..75ade94fa 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -1,29 +1,30 @@ +import AmountWithInscriptionSatribute from '@components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; import { + btcTransaction, currencySymbolMap, getBtcFiatEquivalent, getFiatEquivalent, } from '@secretkeylabs/xverse-core'; import { StoreState } from '@stores/index'; +import { StyledP } from '@ui-library/common.styled'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; -const RowContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', +const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, borderRadius: 12, padding: '12px 16px', - justifyContent: 'center', marginBottom: 12, })); -const FeeText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); +const Row = styled.div({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', +}); const FeeTitleContainer = styled.div({ display: 'flex', @@ -37,24 +38,24 @@ const FeeContainer = styled.div({ alignItems: 'flex-end', }); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const FiatAmountText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - interface Props { feePerVByte?: BigNumber; fee: BigNumber; currency: string; title?: string; + inscriptions?: btcTransaction.IOInscription[]; + satributes?: btcTransaction.IOSatribute[]; + onShowInscription?: (inscription: btcTransaction.IOInscription) => void; } -function TransferFeeView({ feePerVByte, fee, currency, title }: Props) { +function TransferFeeView({ + feePerVByte, + fee, + currency, + title, + inscriptions = [], + satributes = [], + onShowInscription = () => {}, +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { btcFiatRate, stxBtcRate, fiatCurrency } = useSelector( (state: StoreState) => state.walletState, @@ -81,42 +82,59 @@ function TransferFeeView({ feePerVByte, fee, currency, title }: Props) { thousandSeparator prefix={`${currencySymbolMap[fiatCurrency]} `} suffix={` ${fiatCurrency}`} - renderText={(value: string) => {`~ ${value}`}} + renderText={(value: string) => `~ ${value}`} /> ); }; return ( - - - {title ?? t('FEES')} - - - {value}} - /> - {currency === 'sats' && ( + + + + + {title ?? t('FEES')} + + + {value}} + suffix={` ${currency}`} + renderText={(value: string) => ( + + {value} + + )} /> - )} - - {getFiatAmountString( - currency === 'sats' - ? getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)) - : new BigNumber(fiatRate!), + {currency === 'sats' && feePerVByte && ( + ( + + {value} + + )} + /> )} - - - + + {getFiatAmountString( + currency === 'sats' + ? getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)) + : new BigNumber(fiatRate!), + )} + +
+ + + ); } diff --git a/src/app/hooks/useSignPsbtTx.ts b/src/app/hooks/useSignPsbtTx.ts index 30cf0d0bf..88412b685 100644 --- a/src/app/hooks/useSignPsbtTx.ts +++ b/src/app/hooks/useSignPsbtTx.ts @@ -17,16 +17,21 @@ const useSignPsbtTx = () => { const tabId = params.get('tabId') ?? '0'; const btcClient = useBtcClient(); - const confirmSignPsbt = async () => { - const seedPhrase = await getSeed(); - const signingResponse = await signPsbt( - seedPhrase, - accountsList, - request.payload.inputsToSign, - request.payload.psbtBase64, - request.payload.broadcast, - network.type, - ); + const confirmSignPsbt = async (signingResponseOverride?: string) => { + let signingResponse = signingResponseOverride; + + if (!signingResponse) { + const seedPhrase = await getSeed(); + signingResponse = await signPsbt( + seedPhrase, + accountsList, + request.payload.inputsToSign, + request.payload.psbtBase64, + request.payload.broadcast, + network.type, + ); + } + let txId: string = ''; if (request.payload.broadcast) { const txHex = psbtBase64ToHex(signingResponse); diff --git a/src/app/hooks/useTransactionContext.ts b/src/app/hooks/useTransactionContext.ts new file mode 100644 index 000000000..58fd225c1 --- /dev/null +++ b/src/app/hooks/useTransactionContext.ts @@ -0,0 +1,49 @@ +import { btcTransaction, UtxoCache } from '@secretkeylabs/xverse-core'; +import { useMemo } from 'react'; +import useBtcClient from './useBtcClient'; +import useSeedVault from './useSeedVault'; +import useWalletSelector from './useWalletSelector'; + +const useTransactionContext = () => { + const { selectedAccount, network } = useWalletSelector(); + const seedVault = useSeedVault(); + const btcClient = useBtcClient(); + + const utxoCache = useMemo( + () => + new UtxoCache({ + cacheStorageController: { + get: async (key: string) => { + const value = localStorage.getItem(key); + return value; + }, + set: async (key: string, value: string) => { + localStorage.setItem(key, value); + }, + remove: async (key: string) => { + localStorage.removeItem(key); + }, + }, + network: network.type, + }), + [network.type], + ); + + const transactionContext = useMemo(() => { + if (selectedAccount?.id === undefined) { + throw new Error('No account selected'); + } + + return btcTransaction.createTransactionContext({ + account: selectedAccount, + seedVault, + utxoCache, + network: network.type, + esploraApiProvider: btcClient, + }); + }, [utxoCache, selectedAccount, network, seedVault, btcClient]); + + return transactionContext; +}; + +export default useTransactionContext; diff --git a/src/app/screens/ordinals/ordinalImage.tsx b/src/app/screens/ordinals/ordinalImage.tsx index c9091ea08..adfd48033 100644 --- a/src/app/screens/ordinals/ordinalImage.tsx +++ b/src/app/screens/ordinals/ordinalImage.tsx @@ -3,6 +3,7 @@ import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; import { BetterBarLoader } from '@components/barLoader'; import useTextOrdinalContent from '@hooks/useTextOrdinalContent'; import useWalletSelector from '@hooks/useWalletSelector'; +import { TextT } from '@phosphor-icons/react'; import { CondensedInscription, getErc721Metadata, Inscription } from '@secretkeylabs/xverse-core'; import { getBrc20Details } from '@utils/brc20'; import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; @@ -11,6 +12,7 @@ import Image from 'rc-image'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import Theme from 'theme'; import Brc20Tile from './brc20Tile'; interface ContainerProps { @@ -116,6 +118,15 @@ const StyledImage = styled(Image)` border-radius: 8px; object-fit: contain; image-rendering: pixelated; + display: flex; +`; +const ContentTypeThumbnailContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background-color: ${(props) => props.theme.colors.white_0}; `; export const StyledBarLoader = styled(BetterBarLoader)((props) => ({ @@ -132,6 +143,8 @@ interface Props { isSmallImage?: boolean; withoutSizeIncrease?: boolean; withoutTitles?: boolean; + placeholderIcon?: string; + showContentTypeThumbnail?: boolean; } function OrdinalImage({ @@ -142,6 +155,8 @@ function OrdinalImage({ isSmallImage = false, withoutSizeIncrease = false, withoutTitles = false, + placeholderIcon, + showContentTypeThumbnail = false, }: Props) { const isGalleryOpen: boolean = document.documentElement.clientWidth > 360 && !withoutSizeIncrease; const textContent = useTextOrdinalContent(ordinal); @@ -268,6 +283,16 @@ function OrdinalImage({ ); } + if (showContentTypeThumbnail) { + return ( + + + + + + ); + } + return ( - ordinal + ordinal ); } diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index a7ee9f476..116b78127 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -1,20 +1,23 @@ import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; +import AssetModal from '@components/assetModal'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import SatsBundle from '@components/confirmBtcTransactionComponent/bundle'; -import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; +import ReceiveSection from '@components/confirmBtcTransaction/receiveSection'; +import TransactionSummary from '@components/confirmBtcTransaction/transactionSummary'; +import TransferSection from '@components/confirmBtcTransaction/transferSection'; +import { getNetAmount, isScriptOutput } from '@components/confirmBtcTransaction/utils'; import InfoContainer from '@components/infoContainer'; import LoadingTransactionStatus from '@components/loadingTransactionStatus'; import { ConfirmationStatus } from '@components/loadingTransactionStatus/circularSvgAnimation'; -import RecipientComponent from '@components/recipientComponent'; import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useDetectOrdinalInSignPsbt, { InputsBundle } from '@hooks/useDetectOrdinalInSignPsbt'; import useSignBatchPsbtTx from '@hooks/useSignBatchPsbtTx'; +import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowLeft, ArrowRight } from '@phosphor-icons/react'; -import { Bundle, parsePsbt, satsToBtc } from '@secretkeylabs/xverse-core'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import Callout from '@ui-library/callout'; import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -113,33 +116,39 @@ interface TxResponse { psbtBase64: string; } +type PsbtSummary = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput | undefined; + hasSigHashNone: boolean; +}; + function SignBatchPsbtRequest() { const { btcAddress, ordinalsAddress, selectedAccount, network } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { t: tCommon } = useTranslation('translation', { keyPrefix: 'COMMON' }); - const [expandInputOutputView, setExpandInputOutputView] = useState(false); - const { payload, confirmSignPsbt, cancelSignPsbt, getSigningAddresses, requestToken } = - useSignBatchPsbtTx(); + const { payload, confirmSignPsbt, cancelSignPsbt, requestToken } = useSignBatchPsbtTx(); const [isSigning, setIsSigning] = useState(false); const [isSigningComplete, setIsSigningComplete] = useState(false); const [signingPsbtIndex, setSigningPsbtIndex] = useState(1); - const [hasOutputScript, setHasOutputScript] = useState(false); 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 handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); - const [userReceivesOrdinalArr, setUserReceivesOrdinalArr] = useState< - { bundleItemsData: InputsBundle; userReceivesOrdinal: boolean }[] - >([]); const [isLoading, setIsLoading] = useState(true); + const txnContext = useTransactionContext(); + const [inscriptionToShow, setInscriptionToShow] = useState< + btcTransaction.IOInscription | undefined + >(undefined); + + const [parsedPsbts, setParsedPsbts] = useState([]); const handlePsbtParsing = useCallback( (psbt: SignMultiplePsbtPayload, index: number) => { try { - return parsePsbt(selectedAccount!, psbt.inputsToSign, psbt.psbtBase64, network.type); + const parsedPsbt = new btcTransaction.EnhancedPsbt(txnContext, psbt.psbtBase64); + return parsedPsbt.getSummary(); } catch (err) { navigate('/tx-status', { state: { @@ -153,13 +162,21 @@ function SignBatchPsbtRequest() { return undefined; } }, - [selectedAccount, network.type], + [txnContext], ); - const parsedPsbts = useMemo( - () => payload.psbts.map(handlePsbtParsing), - [handlePsbtParsing, payload.psbts], - ); + useEffect(() => { + (async () => { + const parsedPsbtsResult = await Promise.all(payload.psbts.map(handlePsbtParsing)); + + if (parsedPsbtsResult.some((item) => item === undefined)) { + return setIsLoading(false); + } + + setParsedPsbts(parsedPsbtsResult as PsbtSummary[]); + setIsLoading(false); + })(); + }, [payload.psbts.length, handlePsbtParsing]); const checkAddressMismatch = (input) => { if (input.address !== btcAddress && input.address !== ordinalsAddress) { @@ -189,51 +206,10 @@ function SignBatchPsbtRequest() { payload.psbts.forEach((psbt) => psbt.inputsToSign.forEach(checkAddressMismatch)); }; - const checkIfUserReceivesOrdinals = async () => { - try { - const results = await Promise.all(parsedPsbts.map(handleOrdinalAndOrdinalInfo)); - setUserReceivesOrdinalArr(results); - } catch { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, - }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - checkIfUserReceivesOrdinals(); - }, []); - useEffect(() => { checkIfMismatch(); }, []); - useEffect(() => { - if (parsedPsbts) { - let outputScriptDetected = false; - - parsedPsbts.forEach((psbt) => { - if (!psbt) { - return; - } - - if (psbt.outputs.some((output) => !!output.outputScript)) { - outputScriptDetected = true; - } - }); - - setHasOutputScript(outputScriptDetected); - } - }, [parsedPsbts]); - const onSignPsbtConfirmed = async () => { try { if (isLedgerAccount(selectedAccount)) { @@ -296,28 +272,35 @@ function SignBatchPsbtRequest() { window.close(); }; - const expandInputOutputSection = () => { - setExpandInputOutputView(!expandInputOutputView); - }; - const closeCallback = () => { window.close(); }; const totalNetAmount = parsedPsbts.reduce( - (sum, psbt) => (psbt ? sum.plus(new BigNumber(psbt.netAmount.toString())) : sum), + (sum, psbt) => + psbt + ? sum.plus( + new BigNumber( + getNetAmount({ + inputs: psbt.inputs, + outputs: psbt.outputs, + btcAddress, + ordinalsAddress, + }), + ), + ) + : sum, new BigNumber(0), ); - - const userReceivesOrdinals = userReceivesOrdinalArr - .filter((item) => item.userReceivesOrdinal) - .map((item) => item.bundleItemsData) - .flat(); - - const userTransfersOrdinals = userReceivesOrdinalArr - .filter((item) => !item.userReceivesOrdinal) - .map((item) => item.bundleItemsData) - .flat(); + const totalFeeAmount = parsedPsbts.reduce((sum, psbt) => { + const feeAmount = psbt.feeOutput?.amount ?? 0; + return sum.plus(new BigNumber(feeAmount)); + }, new BigNumber(0)); + + const hasOutputScript = useMemo( + () => parsedPsbts.some((psbt) => psbt.outputs.some((output) => isScriptOutput(output))), + [parsedPsbts.length], + ); const signingStatus: ConfirmationStatus = isSigningComplete ? 'SUCCESS' : 'LOADING'; @@ -364,51 +347,30 @@ function SignBatchPsbtRequest() { {t('REVIEW_ALL')} - - {userTransfersOrdinals.length > 0 && - userTransfersOrdinals.map((item, index) => ( - - ))} - - {userReceivesOrdinals.length > 0 && - userTransfersOrdinals.map((item, index) => ( - - ))} - - setInscriptionToShow(undefined)} + inscription={{ + content_type: inscriptionToShow.contentType, + id: inscriptionToShow.id, + inscription_number: inscriptionToShow.number, + }} + /> + )} + psbt.inputs).flat()} + outputs={parsedPsbts.map((psbt) => psbt.outputs).flat()} + netAmount={(totalNetAmount.toNumber() + totalFeeAmount.toNumber()) * -1} + isPartialTransaction={parsedPsbts.some((psbt) => !psbt.feeOutput)} + onShowInscription={setInscriptionToShow} + /> + psbt.outputs).flat()} + onShowInscription={setInscriptionToShow} + netAmount={totalNetAmount.toNumber()} /> - - - {hasOutputScript && } + {hasOutputScript && } )} @@ -439,46 +401,14 @@ function SignBatchPsbtRequest() { {t('TRANSACTION')} {currentPsbtIndex + 1}/{parsedPsbts.length} - {Array.isArray(userReceivesOrdinalArr[currentPsbtIndex]?.bundleItemsData) && - userReceivesOrdinalArr[currentPsbtIndex].bundleItemsData.map((bundle, index) => ( - - ))} - - - - - - {hasOutputScript && } + {!!parsedPsbts[currentPsbtIndex] && ( + + )} diff --git a/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx b/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx deleted file mode 100644 index 82d0de52d..000000000 --- a/src/app/screens/signPsbtRequest/bundleItemsComponent.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import Eye from '@assets/img/createPassword/Eye.svg'; -import Cross from '@assets/img/dashboard/X.svg'; -import IconOrdinal from '@assets/img/transactions/ordinal.svg'; -import RareSatAsset from '@components/rareSatAsset/rareSatAsset'; -import { animated, useSpring } from '@react-spring/web'; -import { getTruncatedAddress } from '@utils/helper'; -import { BundleItem, getBundleItemSubText } from '@utils/rareSats'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: '16px 16px', - justifyContent: 'center', - marginBottom: 12, -})); - -const RecipientTitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginBottom: 10, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const TransparentButton = styled.button({ - background: 'transparent', - display: 'flex', - alignItems: 'center', - marginLeft: 10, -}); - -const Icon = styled.img((props) => ({ - marginRight: props.theme.spacing(4), - width: 32, - height: 32, - borderRadius: 30, -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); - -const SubValueText = styled.h1((props) => ({ - ...props.theme.body_m, - fontSize: 12, - color: props.theme.colors.white_400, -})); - -const InscriptionText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - fontSize: 21, - marginTop: 24, - textAlign: 'center', - color: props.theme.colors.white[0], - overflowWrap: 'break-word', - wordWrap: 'break-word', - wordBreak: 'break-word', -})); - -const ColumnContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - flex: 1, - justifyContent: 'flex-end', - alignItems: 'flex-end', - marginTop: 12, -}); - -const CrossContainer = styled.div({ - display: 'flex', - marginTop: 10, - justifyContent: 'flex-end', - alignItems: 'flex-end', -}); - -const OrdinalOuterImageContainer = styled.div({ - justifyContent: 'center', - alignItems: 'center', - borderRadius: 2, - display: 'flex', - flexDirection: 'column', - flex: 1, -}); - -const OrdinalImageContainer = styled.div({ - width: '50%', -}); - -const OrdinalBackgroundContainer = styled(animated.div)({ - width: '100%', - height: '100%', - top: 0, - left: 0, - bottom: 0, - right: 0, - position: 'fixed', - zIndex: 10, - background: 'rgba(18, 21, 30, 0.8)', - backdropFilter: 'blur(16px)', - padding: 16, - display: 'flex', - flexDirection: 'column', -}); - -const EyeIcon = styled.img({ - width: 20, - height: 20, -}); - -interface Props { - item: BundleItem; - userReceivesOrdinal: boolean; -} -function BundleItemsComponent({ item, userReceivesOrdinal }: Props) { - const { t } = useTranslation('translation'); - const [showOrdinal, setShowOrdinal] = useState(false); - const styles = useSpring({ - from: { - opacity: 0, - y: 24, - }, - to: { - y: 0, - opacity: 1, - }, - delay: 100, - }); - const onButtonClick = () => { - setShowOrdinal(true); - }; - - const onCrossClick = () => { - setShowOrdinal(false); - }; - const getItemId = () => { - if (item.type === 'inscription') { - return item.inscription.id; - } - if (item.type === 'inscribed-sat' || item.type === 'rare-sat') { - return item.number; - } - return ''; - }; - const itemSubText = getBundleItemSubText({ - satType: item.type, - rareSatsType: item.rarity_ranking as any, - }); - const getDetail = () => { - if (item.type === 'inscription' || item.type === 'inscribed-sat') { - return item.inscription.content_type; - } - return itemSubText; - }; - const getTitle = () => { - if (item.type === 'inscription') { - return t('COMMON.INSCRIPTION'); - } - if (item.type === 'inscribed-sat') { - return t('RARE_SATS.INSCRIBED_SAT'); - } - return t('RARE_SATS.RARE_SAT'); - }; - return ( - <> - {showOrdinal && ( - - - - cross - - - - - - - {`${getTitle()} ${getItemId()} `} - - - )} - - - {userReceivesOrdinal - ? t('CONFIRM_TRANSACTION.YOU_WILL_RECEIVE') - : t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER')} - - - - {getTitle()} - - - {getTruncatedAddress(String(getItemId()))} - - - - - {getDetail()} - - - - - ); -} - -export default BundleItemsComponent; diff --git a/src/app/screens/signPsbtRequest/index.tsx b/src/app/screens/signPsbtRequest/index.tsx index e322c6006..3e662ce82 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -1,231 +1,89 @@ -import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; -import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; -import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types'; -import { delay } from '@common/utils/ledger'; -import AccountHeaderComponent from '@components/accountHeader'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; -import InputOutputComponent from '@components/confirmBtcTransactionComponent/inputOutputComponent'; -import InfoContainer from '@components/infoContainer'; -import LedgerConnectionView from '@components/ledger/connectLedgerView'; -import RecipientComponent from '@components/recipientComponent'; -import TransactionDetailComponent from '@components/transactionDetailComponent'; -import useBtcClient from '@hooks/useBtcClient'; -import useDetectOrdinalInSignPsbt from '@hooks/useDetectOrdinalInSignPsbt'; +import ConfirmBitcoinTransaction from '@components/confirmBtcTransaction'; import useSignPsbtTx from '@hooks/useSignPsbtTx'; +import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import Transport from '@ledgerhq/hw-transport-webusb'; -import { - getBtcFiatEquivalent, - parsePsbt, - psbtBase64ToHex, - satsToBtc, - signLedgerPSBT, - Transport as TransportType, -} from '@secretkeylabs/xverse-core'; -import { isLedgerAccount } from '@utils/helper'; -import { BundleItem, convertV2ToV1Bundle } from '@utils/rareSats'; -import BigNumber from 'bignumber.js'; -import { decodeToken } from 'jsontokens'; +import { btcTransaction, Transport } from '@secretkeylabs/xverse-core'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { MoonLoader } from 'react-spinners'; -import { SignTransactionOptions } from 'sats-connect'; -import styled from 'styled-components'; -import BundleItemsComponent from './bundleItemsComponent'; - -const OuterContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - marginTop: props.theme.spacing(11), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const LoaderContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: props.theme.spacing(12), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(12), -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), - width: '100%', -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), - textAlign: 'left', -})); - -const SuccessActionsContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), -})); +import { useNavigate } from 'react-router-dom'; +import getPsbtDataWithMocks from './tempMockDataUtil'; +import useSignPsbtValidationGate from './useSignPsbtValidationGate'; function SignPsbtRequest() { - const { - btcAddress, - btcPublicKey, - ordinalsAddress, - ordinalsPublicKey, - selectedAccount, - network, - btcFiatRate, - } = useWalletSelector(); const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { t: signatureRequestTranslate } = useTranslation('translation', { - keyPrefix: 'SIGNATURE_REQUEST', - }); - const [expandInputOutputView, setExpandInputOutputView] = useState(false); - const { payload, confirmSignPsbt, cancelSignPsbt, getSigningAddresses } = useSignPsbtTx(); + + const [isLoading, setIsLoading] = useState(true); const [isSigning, setIsSigning] = useState(false); - const [hasOutputScript, setHasOutputScript] = useState(false); - const [isModalVisible, setIsModalVisible] = useState(false); - const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [isButtonDisabled, setIsButtonDisabled] = useState(false); - const [isConnectSuccess, setIsConnectSuccess] = useState(false); - const [isConnectFailed, setIsConnectFailed] = useState(false); - const [isTxRejected, setIsTxRejected] = useState(false); - const { search } = useLocation(); - const params = new URLSearchParams(search); - const tabId = params.get('tabId') ?? '0'; - const requestToken = params.get('signPsbtRequest') ?? ''; - const request = decodeToken(requestToken) as any as SignTransactionOptions; - const btcClient = useBtcClient(); + const [inputs, setInputs] = useState([]); + const [outputs, setOutputs] = useState([]); + const [feeOutput, setFeeOutput] = useState(); + const { payload, confirmSignPsbt, cancelSignPsbt } = useSignPsbtTx(); + const txnContext = useTransactionContext(); const parsedPsbt = useMemo(() => { try { - return parsePsbt(selectedAccount!, payload.inputsToSign, payload.psbtBase64, network.type); + return new btcTransaction.EnhancedPsbt(txnContext, payload.psbtBase64, payload.inputsToSign); } catch (err) { return undefined; } - }, [selectedAccount, payload.inputsToSign, payload.psbtBase64, network.type]); + }, [txnContext, payload.psbtBase64]); - const handleOrdinalAndOrdinalInfo = useDetectOrdinalInSignPsbt(); - const [isLoading, setIsLoading] = useState(true); - const [userReceivesOrdinal, setUserReceivesOrdinal] = useState(false); - const [bundleItemsData, setBundleItemsData] = useState([]); - const signingAddresses = useMemo( - () => getSigningAddresses(payload.inputsToSign), - [payload.inputsToSign], - ); + useSignPsbtValidationGate({ payload, parsedPsbt }); - const checkIfMismatch = () => { - if (!parsedPsbt) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, - }); - } - if (payload.network.type !== network.type) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('NETWORK_MISMATCH'), - browserTx: true, - }, - }); - } - if (payload.inputsToSign) { - payload.inputsToSign.forEach((input) => { - if (input.address !== btcAddress && input.address !== ordinalsAddress) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('ADDRESS_MISMATCH'), - browserTx: true, - }, - }); - } + const { btcAddress, ordinalsAddress } = useWalletSelector(); + useEffect(() => { + if (!parsedPsbt) return; + + parsedPsbt + .getSummary() + .then((summary) => { + const { feeOutput: psbtFeeOutput, inputs: psbtInputs, outputs: psbtOutputs } = summary; + // TODO: remove this section, this is only for testing + const { inputsWithMocks, outputsWithMocks, feeOutputWithMocks } = getPsbtDataWithMocks( + btcAddress, + ordinalsAddress, + psbtInputs, + psbtOutputs, + !psbtFeeOutput, + psbtFeeOutput, + ); + setFeeOutput(feeOutputWithMocks); + setInputs(inputsWithMocks); + setOutputs(outputsWithMocks); + + // setFeeOutput(psbtFeeOutput); + // setInputs(psbtInputs); + // setOutputs(psbtOutputs); + setIsLoading(false); + }) + .catch((error) => { + console.error(error); + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), + error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); }); - } - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [parsedPsbt]); - const checkIfUserReceivesOrdinal = async () => { + const onConfirm = async (ledgerTransport?: Transport) => { + setIsSigning(true); try { - const result = await handleOrdinalAndOrdinalInfo(parsedPsbt); - setBundleItemsData(convertV2ToV1Bundle(result.bundleItemsData)); - setUserReceivesOrdinal(result.userReceivesOrdinal); - } catch { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), - error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), - browserTx: true, - }, + const signedPsbt = await parsedPsbt?.getSignedPsbtBase64({ + finalize: payload.broadcast, + ledgerTransport, }); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - checkIfUserReceivesOrdinal(); - }, []); - - useEffect(() => { - checkIfMismatch(); - }, []); - useEffect(() => { - if (parsedPsbt) { - const outputScriptDetected = parsedPsbt.outputs.some((output) => !!output.outputScript); - setHasOutputScript(outputScriptDetected); - } - }, [parsedPsbt]); - - const onSignPsbtConfirmed = async () => { - try { - if (isLedgerAccount(selectedAccount)) { - setIsModalVisible(true); - return; + const response = await confirmSignPsbt(signedPsbt); + if (ledgerTransport) { + await ledgerTransport?.close(); } - - setIsSigning(true); - const response = await confirmSignPsbt(); setIsSigning(false); if (payload.broadcast) { navigate('/tx-status', { @@ -240,6 +98,7 @@ function SignPsbtRequest() { window.close(); } } catch (err) { + setIsSigning(false); if (err instanceof Error) { navigate('/tx-status', { state: { @@ -254,226 +113,26 @@ function SignPsbtRequest() { } }; - const onCancelClick = async () => { + const onCancel = () => { cancelSignPsbt(); window.close(); }; - const expandInputOutputSection = () => { - setExpandInputOutputView(!expandInputOutputView); - }; - - const handleLedgerPsbtSigning = async (transport: TransportType) => { - const addressIndex = selectedAccount?.deviceAccountIndex; - const { psbtBase64, broadcast } = payload; - - if (addressIndex === undefined) { - throw new Error('Account not found'); - } - - const signingResponse = await signLedgerPSBT({ - transport, - esploraProvider: btcClient, - network: network.type, - addressIndex, - psbtInputBase64: psbtBase64, - finalize: broadcast ?? false, - nativeSegwitPubKey: btcPublicKey, - taprootPubKey: ordinalsPublicKey, - }); - - let txId: string = ''; - if (request.payload.broadcast) { - const txHex = psbtBase64ToHex(signingResponse); - const response = await btcClient.sendRawTransaction(txHex); - txId = response.tx.hash; - } - - const signingMessage = { - source: MESSAGE_SOURCE, - method: ExternalSatsMethods.signPsbtResponse, - payload: { - signPsbtRequest: requestToken, - signPsbtResponse: { - psbtBase64: signingResponse, - txId, - }, - }, - }; - chrome.tabs.sendMessage(+tabId, signingMessage); - - return { - txId, - signingResponse, - }; - }; - - const handleConnectAndConfirm = async () => { - if (!selectedAccount) { - console.error('No account selected'); - return; - } - setIsButtonDisabled(true); - - const transport = await Transport.create(); - - if (!transport) { - setIsConnectSuccess(false); - setIsConnectFailed(true); - setIsButtonDisabled(false); - return; - } - - setIsConnectSuccess(true); - await delay(1500); - setCurrentStepIndex(1); - - try { - const response = await handleLedgerPsbtSigning(transport); - - if (payload.broadcast) { - navigate('/tx-status', { - state: { - txid: response.txId, - currency: 'BTC', - error: '', - browserTx: true, - }, - }); - } else { - window.close(); - } - } catch (err) { - console.error(err); - setIsTxRejected(true); - } finally { - await transport.close(); - setIsButtonDisabled(false); - } - }; - - const handleRetry = async () => { - setIsTxRejected(false); - setIsConnectSuccess(false); - setCurrentStepIndex(0); - }; - - const cancelCallback = () => { - window.close(); - }; - - const getSatsAmountString = (sats: BigNumber) => ( - - ); - return ( - <> - - {isLoading ? ( - - - - ) : ( - <> - - - {t('REVIEW_TRANSACTION')} - {!payload.broadcast && } - {bundleItemsData && - bundleItemsData.map((bundleItem, index) => ( - - ))} - - - - - {payload.broadcast ? ( - - ) : null} - {hasOutputScript && } - - - - - - - - - - )} - setIsModalVisible(false)}> - {currentStepIndex === 0 && ( - - )} - {currentStepIndex === 1 && ( - - )} - - - - - - + ); } diff --git a/src/app/screens/signPsbtRequest/tempMockDataUtil.ts b/src/app/screens/signPsbtRequest/tempMockDataUtil.ts new file mode 100644 index 000000000..b2af92428 --- /dev/null +++ b/src/app/screens/signPsbtRequest/tempMockDataUtil.ts @@ -0,0 +1,143 @@ +import { btcTransaction } from '@secretkeylabs/xverse-core'; + +// TODO: remove after testing +const getPsbtDataWithMocks = ( + btcAddress: string, + ordinalsAddress: string, + inputs: btcTransaction.EnhancedInput[], + outputs: btcTransaction.EnhancedOutput[], + isPartialTransaction: boolean, + feeOutput?: btcTransaction.TransactionFeeOutput, +) => { + const outputsWithMocks = [...outputs]; + const inputsWithMocks = [...inputs]; + const feeOutputWithMocks = feeOutput ? { ...feeOutput } : undefined; + + if (localStorage.getItem('assetsInPayment') === 'true') { + // TODO: mock data for items spend in payment address + if (isPartialTransaction) { + inputsWithMocks.push({ + // @ts-ignore + extendedUtxo: { + address: btcAddress, + outpoint: '9851e0a32f6fd352dd763624025cb55cead8954c7bdde4430c290f7f9e3bcfeb:0', + // @ts-ignore + utxo: { + value: 100000, + status: { + confirmed: false, // to test the unconfirmed utxo warning callout + }, + }, + }, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: btcAddress, + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ], + satributes: [], + sigHash: 131, + }); + } + outputsWithMocks.push( + { + address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', + amount: 10000, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: btcAddress, + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ], + satributes: [], + }, + { + address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', + amount: 546, + inscriptions: [], + satributes: [ + { + amount: 1, + fromAddress: btcAddress, + offset: 0, + types: ['PALINDROME'], + }, + ], + }, + ); + } + + if (localStorage.getItem('bundleInOrdinal') === 'true') { + // TODO: mock data for bundle item in ordinal address + outputsWithMocks.push({ + address: ordinalsAddress, + amount: 20, + inscriptions: [ + { + contentType: 'image/png', + fromAddress: 'bc1prnplwl27eedudpvl9cjhd2pysudk0gzze08wjhsyy0998mfcgmcsaz4nse', + id: '9f0a1ea3ca2e2431242350b63cf53708f0f3e560638eb26b1255d4e5dd766fc4i0', + number: 10987226, + offset: 0, + }, + { + contentType: 'image/png', + fromAddress: 'bc1pmz88ylp258alrgeqsy7jn99u20ylkc4fuqcgwva3eef8s92ye9squunk5r', + id: '2237248523bc923a7844b47cb7e2552c1666032ed54ab153a00fba1f5c3e1e22i0', + number: 10878824, + offset: 2, + }, + ], + satributes: [ + { + amount: 1, + fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', + offset: 3, + types: ['FIRST_TRANSACTION', 'VINTAGE', 'BLOCK9', 'NAKAMOTO'], + }, + { + amount: 1, + fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', + offset: 0, + types: ['PIZZA'], + }, + ], + }); + } + + if (localStorage.getItem('assetsInFees') === 'true' && feeOutputWithMocks) { + feeOutputWithMocks.satributes = [ + ...feeOutputWithMocks.satributes, + { + amount: 1, + fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', + offset: 0, + types: ['PALINDROME'], + }, + ]; + feeOutputWithMocks.inscriptions = [ + ...feeOutputWithMocks.inscriptions, + { + contentType: 'image/png', + fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', + id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', + number: 10951686, + offset: 0, + }, + ]; + } + + return { + inputsWithMocks, + outputsWithMocks, + feeOutputWithMocks, + }; +}; + +export default getPsbtDataWithMocks; diff --git a/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts new file mode 100644 index 000000000..cd81c775a --- /dev/null +++ b/src/app/screens/signPsbtRequest/useSignPsbtValidationGate.ts @@ -0,0 +1,56 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { btcTransaction } from '@secretkeylabs/xverse-core'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { SignTransactionPayload } from 'sats-connect'; + +type Props = { + payload: SignTransactionPayload; + parsedPsbt: btcTransaction.EnhancedPsbt | undefined; +}; +const useSignPsbtValidationGate = ({ payload, parsedPsbt }: Props) => { + const { btcAddress, ordinalsAddress, network } = useWalletSelector(); + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + useEffect(() => { + if (!parsedPsbt) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle: t('PSBT_CANT_PARSE_ERROR_TITLE'), + error: t('PSBT_CANT_PARSE_ERROR_DESCRIPTION'), + browserTx: true, + }, + }); + } + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('NETWORK_MISMATCH'), + browserTx: true, + }, + }); + } + if (payload.inputsToSign) { + payload.inputsToSign.forEach((input) => { + if (input.address !== btcAddress && input.address !== ordinalsAddress) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('ADDRESS_MISMATCH'), + browserTx: true, + }, + }); + } + }); + } + }); +}; + +export default useSignPsbtValidationGate; diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index 7204a60b3..4c1905e05 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -13,11 +13,11 @@ import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { CarProfile, Lightning, RocketLaunch, ShootingStar } from '@phosphor-icons/react'; import { + RecommendedFeeResponse, + Transport as TransportType, getBtcFiatEquivalent, mempoolApi, rbf, - RecommendedFeeResponse, - Transport as TransportType, } from '@secretkeylabs/xverse-core'; import { EMPTY_LABEL } from '@utils/constants'; import { isLedgerAccount } from '@utils/helper'; diff --git a/src/app/ui-library/avatar.tsx b/src/app/ui-library/avatar.tsx new file mode 100644 index 000000000..e1350f457 --- /dev/null +++ b/src/app/ui-library/avatar.tsx @@ -0,0 +1,33 @@ +import styled from 'styled-components'; + +type Props = { + icon?: React.ReactNode; + src?: React.ReactNode; + size?: number; +}; + +const ImageContainer = styled.div<{ size: number }>((props) => ({ + height: props.size, + width: props.size, + borderRadius: '50%', + backgroundColor: props.color, + overflow: 'hidden', +})); + +const IconContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + width: '100%', + backgroundColor: props.theme.colors.white_0, +})); + +export default function Avatar({ icon, src, size = 32 }: Props) { + return ( + + {icon && {icon}} + {src && src} + + ); +} diff --git a/src/app/ui-library/callout.tsx b/src/app/ui-library/callout.tsx index 4428021e4..2d7399e61 100644 --- a/src/app/ui-library/callout.tsx +++ b/src/app/ui-library/callout.tsx @@ -56,6 +56,14 @@ const RedirectButton = styled.button` text-transform: capitalize; `; +export const AnchorLink = styled.a((props) => ({ + display: 'inline-flex', + alignItems: 'center', + gap: props.theme.space.xxxs, + textTransform: 'capitalize', + color: props.theme.colors.white_0, +})); + /** * ref: https://zeroheight.com/0683c9fa7/p/051ca8-callout/t/7814dc */ @@ -66,6 +74,7 @@ export type CalloutProps = { variant?: CalloutVariant; redirectText?: string; onClickRedirect?: () => void; + anchorRedirect?: string; }; export function Callout({ className, @@ -73,7 +82,8 @@ export function Callout({ bodyText, variant = 'info', redirectText, - onClickRedirect = () => {}, + onClickRedirect, + anchorRedirect, }: CalloutProps) { const StyledIcon = icons[variant]; return ( @@ -89,12 +99,22 @@ export function Callout({ {bodyText} {redirectText && ( - - - {redirectText} - - - + <> + {onClickRedirect && ( + + + {redirectText} + + + + )} + {anchorRedirect && ( + + {redirectText} + + + )} + )} diff --git a/src/app/ui-library/divider.tsx b/src/app/ui-library/divider.tsx new file mode 100644 index 000000000..fd852ffd7 --- /dev/null +++ b/src/app/ui-library/divider.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +import Theme from 'theme'; + +const Divider = styled.div<{ verticalMargin: keyof typeof Theme.space }>((props) => ({ + display: 'flex', + width: '100%', + height: 1, + backgroundColor: props.theme.colors.white_900, + margin: `${props.theme.space[props.verticalMargin]} 0`, +})); +export default Divider; diff --git a/src/assets/img/rareSats/ic_ordinal_small_over_card.svg b/src/assets/img/rareSats/ic_ordinal_small_over_card.svg new file mode 100644 index 000000000..5ffa566eb --- /dev/null +++ b/src/assets/img/rareSats/ic_ordinal_small_over_card.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/rareSats/link.svg b/src/assets/img/rareSats/link.svg new file mode 100644 index 000000000..354bf93cb --- /dev/null +++ b/src/assets/img/rareSats/link.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 25ad19d2d..d862fde20 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5,7 +5,9 @@ "COMBO": "Combo", "SATS": "Sats", "SATTRIBUTES": "Sattributes", - "INPUT": "Input" + "INPUT": "Input", + "SIZE": "Size", + "BUNDLE": "Bundle" }, "LANDING_SCREEN": { "SCREEN_TITLE": "Wallet for Stacks & Bitcoin", @@ -376,7 +378,13 @@ "CANCEL_BUTTON": "Cancel", "SEE_TRANSACTION_BUTTON": "See transaction", "RETRY_BUTTON": "Try again" - } + }, + "YOUR_ORDINAL_ADDRESS": "Your ordinals address", + "YOUR_PAYMENT_ADDRESS": "Your payment address", + "INSCRIBED_SATS": "Some of these sats are inscribed", + "INSCRIBED_RARE_SATS": "Some of these sats are rare or inscribed", + "UNCONFIRMED_UTXO_WARNING": "You are spending unconfirmed outputs in this transaction. This may lower the effective fee rate causing delays in transaction confirmation", + "INSCRIBED_RARE_SATS_WARNING": "Your payment wallet holds rare or inscribed sats. To avoid spending them in your transactions and fees, transfer them to your ordinals wallet" }, "TX_ERRORS": { "INSUFFICIENT_BALANCE": "The requested transaction cannot be created due to insufficient balance", diff --git a/src/pages/Popup/index.css b/src/pages/Popup/index.css index f9908ff52..f74e9369a 100644 --- a/src/pages/Popup/index.css +++ b/src/pages/Popup/index.css @@ -6,7 +6,7 @@ body { background-color: #181818; } -#app-container { +#app { height: 600px; width: 360px; display: flex; @@ -19,11 +19,6 @@ body { } } -#app { - min-height: 600px; - min-width: 360px; -} - ::-webkit-scrollbar { display: none; } diff --git a/src/pages/Popup/index.html b/src/pages/Popup/index.html index 7b41ffc81..1aba6f4c3 100644 --- a/src/pages/Popup/index.html +++ b/src/pages/Popup/index.html @@ -5,8 +5,6 @@ Xverse Wallet -
-
-
+
From c5b32b564a30b7b010879f7e7f16e92dc3158320 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 11 Jan 2024 16:55:28 +0800 Subject: [PATCH 10/20] [ENG-3496] fix: utxo 500 issue add a fallback option to our api httpsbtc (#738) * chore: fix for core local dev * feat: use btc esplora api with a fallback url * feat: add fallbackBtcApiUrl to store and refactor network state * fix: types and comments * refactor: clean up local react state in changeNetwork * fix: clean up UI in change network node inputs * chore: bump core version * chore: bump core version * fix: type error from core version * fix: make fallback btc url not a required field, and update styles * fix: remove unused remote btcApiUrl config functionality * chore: not really a factory --- package-lock.json | 77 +----- package.json | 4 +- src/app/hooks/queries/useAppConfig.ts | 11 +- src/app/hooks/useBtcClient.ts | 9 +- src/app/hooks/useNetwork.ts | 8 +- src/app/hooks/useWalletReducer.ts | 15 +- .../screens/settings/changeNetwork/index.tsx | 251 ++++++++---------- .../settings/changeNetwork/networkRow.tsx | 2 +- .../settings/changeNetwork/nodeInput.tsx | 90 +++++++ .../stores/wallet/actions/actionCreators.ts | 8 +- src/app/stores/wallet/actions/types.ts | 16 +- src/app/stores/wallet/reducer.ts | 15 +- src/app/utils/constants.ts | 23 +- src/locales/en.json | 5 + tsconfig.json | 1 - 15 files changed, 249 insertions(+), 286 deletions(-) create mode 100644 src/app/screens/settings/changeNetwork/nodeInput.tsx diff --git a/package-lock.json b/package-lock.json index ff4840676..09f7772f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "axios": "^1.1.3", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "classnames": "^2.3.2", "crypto-browserify": "^3.12.0", @@ -3624,29 +3625,6 @@ "node": ">=8" } }, - "node_modules/@zondax/ledger-stacks/node_modules/c32check/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5418,29 +5396,6 @@ "buffer": "^5.6.0" } }, - "node_modules/cross-sha256/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -15829,7 +15784,7 @@ "@stacks/profile": "^6.9.0", "@stacks/storage": "^6.9.0", "@stacks/transactions": "^6.9.0", - "buffer": "^6.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "jsontokens": "^4.0.1", "triplesec": "^4.0.3", @@ -17017,7 +16972,7 @@ "requires": { "@types/bn.js": "^5.1.0", "@types/node": "^18.0.4", - "buffer": "^6.0.3" + "buffer": "6.0.3" } }, "@stacks/network": { @@ -17061,19 +17016,8 @@ "integrity": "sha512-ADADE/PjAbJRlwpG3ShaOMbBUlJJZO7xaYSRD5Tub6PixQlgR4s36y9cvMf/YRGpkqX+QOxIdMw216iC320q9A==", "requires": { "base-x": "^3.0.8", - "buffer": "^5.6.0", + "buffer": "6.0.3", "cross-sha256": "^1.2.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } } } } @@ -18453,18 +18397,7 @@ "resolved": "https://registry.npmjs.org/cross-sha256/-/cross-sha256-1.2.0.tgz", "integrity": "sha512-KViLNMDZKV7jwFqjFx+rNhG26amnFYYQ0S+VaFlVvpk8tM+2XbFia/don/SjGHg9WQxnFVi6z64CGPuF3T+nNw==", "requires": { - "buffer": "^5.6.0" - }, - "dependencies": { - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } + "buffer": "6.0.3" } }, "cross-spawn": { diff --git a/package.json b/package.json index a72703e37..6364bfe4c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "axios": "^1.1.3", "bignumber.js": "^9.1.0", "bip39": "^3.0.3", + "buffer": "6.0.3", "c32check": "^2.0.0", "classnames": "^2.3.2", "crypto-browserify": "^3.12.0", @@ -77,8 +78,7 @@ "style": "prettier --write \"src/**/*.{ts,tsx}\"", "prepare": "husky install" }, - "resolutions": { - "styled-components": "^5", + "overrides": { "buffer": "6.0.3" }, "lint-staged": { diff --git a/src/app/hooks/queries/useAppConfig.ts b/src/app/hooks/queries/useAppConfig.ts index 733d26ece..1a4c53521 100644 --- a/src/app/hooks/queries/useAppConfig.ts +++ b/src/app/hooks/queries/useAppConfig.ts @@ -1,21 +1,14 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { getAppConfig } from '@secretkeylabs/xverse-core'; -import { ChangeNetworkAction } from '@stores/wallet/actions/actionCreators'; import { useQuery } from '@tanstack/react-query'; -import { useDispatch } from 'react-redux'; const useAppConfig = () => { - const { network, networkAddress, btcApiUrl } = useWalletSelector(); - const dispatch = useDispatch(); + const { network } = useWalletSelector(); return useQuery({ - queryKey: ['app-config', network.type, btcApiUrl], + queryKey: ['app-config', network.type], queryFn: async () => { const response = await getAppConfig(network.type); - if (response.data.btcApiURL && network.type === 'Mainnet' && !btcApiUrl) { - const updatedNetwork = { ...network, btcApiUrl: response.data.btcApiURL }; - dispatch(ChangeNetworkAction(updatedNetwork, networkAddress, '')); - } return response; }, }); diff --git a/src/app/hooks/useBtcClient.ts b/src/app/hooks/useBtcClient.ts index 33993a117..18320c299 100644 --- a/src/app/hooks/useBtcClient.ts +++ b/src/app/hooks/useBtcClient.ts @@ -3,17 +3,18 @@ import { useMemo } from 'react'; import useWalletSelector from './useWalletSelector'; const useBtcClient = () => { - const { network, btcApiUrl } = useWalletSelector(); - const { type, btcApiUrl: remoteBtcApiURL } = network; - const url = btcApiUrl || remoteBtcApiURL; + const { network } = useWalletSelector(); + const { type, btcApiUrl, fallbackBtcApiUrl } = network; + const url = btcApiUrl; const esploraInstance = useMemo( () => new BitcoinEsploraApiProvider({ url, + fallbackUrl: fallbackBtcApiUrl, network: type, }), - [url, type], + [url, fallbackBtcApiUrl, type], ); return esploraInstance; diff --git a/src/app/hooks/useNetwork.ts b/src/app/hooks/useNetwork.ts index 0f757c0cc..c3ce19150 100644 --- a/src/app/hooks/useNetwork.ts +++ b/src/app/hooks/useNetwork.ts @@ -3,14 +3,14 @@ import { useMemo } from 'react'; import useWalletSelector from './useWalletSelector'; const useNetworkSelector = () => { - const { network, networkAddress } = useWalletSelector(); + const { network } = useWalletSelector(); const selectedNetwork = useMemo( () => network.type === 'Mainnet' - ? new StacksMainnet({ url: networkAddress }) - : new StacksTestnet({ url: networkAddress }), - [network.type, networkAddress], + ? new StacksMainnet({ url: network.address }) + : new StacksTestnet({ url: network.address }), + [network.type, network.address], ); return selectedNetwork; }; diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 7f642674f..366173d14 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -11,7 +11,9 @@ import { newWallet, restoreWalletWithAccounts, SettingsNetwork, + StacksMainnet, StacksNetwork, + StacksTestnet, walletFromSeedPhrase, } from '@secretkeylabs/xverse-core'; import { @@ -281,14 +283,9 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(account, accountsList)); }; - const changeNetwork = async ( - changedNetwork: SettingsNetwork, - networkObject: StacksNetwork, - networkAddress: string, - btcApiUrl: string, - ) => { + const changeNetwork = async (changedNetwork: SettingsNetwork) => { const seedPhrase = await seedVault.getSeed(); - dispatch(ChangeNetworkAction(changedNetwork, networkAddress, btcApiUrl)); + dispatch(ChangeNetworkAction(changedNetwork)); const wallet = await walletFromSeedPhrase({ mnemonic: seedPhrase, index: 0n, @@ -305,6 +302,10 @@ const useWalletReducer = () => { stxPublicKey: wallet.stxPublicKey, }; dispatch(setWalletAction(wallet)); + const networkObject = + changedNetwork.type === 'Mainnet' + ? new StacksMainnet({ url: changedNetwork.address }) + : new StacksTestnet({ url: changedNetwork.address }); try { await loadActiveAccounts(wallet.seedPhrase, changedNetwork, networkObject, [account]); } catch (err) { diff --git a/src/app/screens/settings/changeNetwork/index.tsx b/src/app/screens/settings/changeNetwork/index.tsx index 312c2b1ed..98485d348 100644 --- a/src/app/screens/settings/changeNetwork/index.tsx +++ b/src/app/screens/settings/changeNetwork/index.tsx @@ -1,19 +1,24 @@ -import Cross from '@assets/img/settings/x.svg'; import ActionButton from '@components/button'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; -import { SettingsNetwork, StacksMainnet, StacksTestnet } from '@secretkeylabs/xverse-core'; -import { initialNetworksList } from '@utils/constants'; +import { + SettingsNetwork, + defaultMainnet, + defaultTestnet, + initialNetworksList, +} from '@secretkeylabs/xverse-core'; import { isValidBtcApi, isValidStacksApi } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import NetworkRow from './networkRow'; +import NodeInput from './nodeInput'; const Container = styled.div` + ${(props) => props.theme.typography.body_medium_m} display: flex; flex-direction: column; flex: 1; @@ -26,180 +31,155 @@ const Container = styled.div` } `; -const NodeInputHeader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingLeft: props.theme.spacing(1), - paddingRight: props.theme.spacing(1), -})); - -const NodeText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(6), -})); - -const NodeResetButton = styled.button((props) => ({ - background: 'none', - color: props.theme.colors.action.classicLight, -})); - -const InputContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - width: '100%', - border: `1px solid ${props.theme.colors.elevation3}`, - backgroundColor: props.theme.colors.elevation_n1, - borderRadius: props.theme.radius(1), - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(3), -})); - -const ButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(16), -})); - -const ErrorMessage = styled.h2((props) => ({ - ...props.theme.body_medium_m, - textAlign: 'left', - color: props.theme.colors.feedback.error, -})); - -const Input = styled.input((props) => ({ - ...props.theme.body_medium_m, - height: 44, - display: 'flex', - flex: 1, - backgroundColor: props.theme.colors.elevation_n1, - color: props.theme.colors.white_0, - border: 'none', -})); - -const Button = styled.button({ - background: 'none', -}); +const ButtonContainer = styled.div` + margin: ${(props) => props.theme.space.m}; +`; + +const NodeInputsContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; + margin-top: ${(props) => props.theme.space.s}; +`; + +type NodeInputKey = keyof Pick; +const nodeInputs: { key: NodeInputKey; labelKey: string }[] = [ + { key: 'address', labelKey: 'STACKS_URL' }, + { key: 'btcApiUrl', labelKey: 'BTC_URL' }, + { key: 'fallbackBtcApiUrl', labelKey: 'FALLBACK_BTC_URL' }, +]; + +type NodeInputErrors = Record; +const initialNodeErrors: NodeInputErrors = { + address: '', + btcApiUrl: '', + fallbackBtcApiUrl: '', +}; function ChangeNetworkScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); - const { network, btcApiUrl, networkAddress } = useWalletSelector(); - const [changedNetwork, setChangedNetwork] = useState(network); - const [stacksUrlError, setStacksUrlError] = useState(''); - const [btcURLError, setBtcURLError] = useState(''); - const [btcUrl, setBtcUrl] = useState(btcApiUrl || network.btcApiUrl); - const [stacksUrl, setStacksUrl] = useState(networkAddress || network.address); - const [isChangingNetwork, setIsChangingNetwork] = useState(false); const navigate = useNavigate(); const { changeNetwork } = useWalletReducer(); + const { network, savedNetworks } = useWalletSelector(); + const [isChangingNetwork, setIsChangingNetwork] = useState(false); + const [formErrors, setFormErrors] = useState(initialNodeErrors); + const [formInputs, setFormInputs] = useState(network); const handleBackButtonClick = () => { navigate('/settings'); }; const onNetworkSelected = (networkSelected: SettingsNetwork) => { - setStacksUrl(networkSelected.address); - setChangedNetwork(networkSelected); - setBtcUrl(networkSelected.btcApiUrl); - setStacksUrlError(''); - setBtcURLError(''); - }; - - const onChangeStacksUrl = (event: React.FormEvent) => { - setStacksUrlError(''); - setStacksUrl(event.currentTarget.value); - }; - - const onChangeBtcApiUrl = (event: React.FormEvent) => { - setBtcURLError(''); - setBtcUrl(event.currentTarget.value); + setFormInputs(networkSelected); + setFormErrors(initialNodeErrors); }; - const onClearStacksUrl = () => { - setStacksUrl(''); + // TODO should validate required fields on change + const onChangeCreator = (key: NodeInputKey) => (event: React.ChangeEvent) => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: event.target.value, + })); }; - const onClearBtcUrl = () => { - setBtcUrl(''); + const onClearCreator = (key: NodeInputKey) => () => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: '', + })); }; - const onResetBtcUrl = async () => { - setBtcUrl(changedNetwork.btcApiUrl); - setBtcURLError(''); - }; - - const onResetStacks = async () => { - setStacksUrl(changedNetwork.address); - setStacksUrlError(''); + const onResetCreator = (key: NodeInputKey) => () => { + setFormErrors((prevErrors) => ({ + ...prevErrors, + [key]: '', + })); + setFormInputs((prevInputs) => ({ + ...prevInputs, + [key]: initialNetworksList.find((n) => n.type === formInputs.type)?.[key], + })); }; const onSubmit = async () => { setIsChangingNetwork(true); - const [isValidStacksUrl, isValidBtcApiUrl] = await Promise.all([ - isValidStacksApi(stacksUrl, changedNetwork.type), - isValidBtcApi(btcUrl, changedNetwork.type), + if (!formInputs.address) { + setFormErrors((prevErrors) => ({ + ...prevErrors, + address: t('REQUIRED'), + })); + setIsChangingNetwork(false); + return; + } + + if (!formInputs.btcApiUrl) { + setFormErrors((prevErrors) => ({ + ...prevErrors, + btcApiUrl: t('REQUIRED'), + })); + setIsChangingNetwork(false); + return; + } + + const [isValidStacksUrl, isValidBtcApiUrl, isValidFallbackBtcApiUrl] = await Promise.all([ + isValidStacksApi(formInputs.address, formInputs.type), + isValidBtcApi(formInputs.btcApiUrl, formInputs.type), + !formInputs.fallbackBtcApiUrl || isValidBtcApi(formInputs.fallbackBtcApiUrl, formInputs.type), ]); - if (isValidStacksUrl && isValidBtcApiUrl) { - const networkObject = - changedNetwork.type === 'Mainnet' - ? new StacksMainnet({ url: stacksUrl }) - : new StacksTestnet({ url: stacksUrl }); - await changeNetwork(changedNetwork, networkObject, stacksUrl, btcUrl); + if (isValidStacksUrl && isValidBtcApiUrl && isValidFallbackBtcApiUrl) { + await changeNetwork(formInputs); navigate('/settings'); } else { - if (!isValidStacksUrl) { - setStacksUrlError(t('INVALID_URL')); - } - if (!isValidBtcApiUrl) { - setBtcURLError(t('INVALID_URL')); - } + setFormErrors({ + address: !isValidStacksUrl ? t('INVALID_URL') : '', + btcApiUrl: !isValidBtcApiUrl ? t('INVALID_URL') : '', + fallbackBtcApiUrl: !isValidFallbackBtcApiUrl ? t('INVALID_URL') : '', + }); setIsChangingNetwork(false); } }; + const savedMainnet = savedNetworks.find((n) => n.type === 'Mainnet'); + const savedTestnet = savedNetworks.find((n) => n.type === 'Testnet'); + return ( <> - - {t('NODE')} - Reset URL - - - - - - {stacksUrlError} - - BTC API URL - Reset URL - - - - - - {btcURLError} + + {nodeInputs.map(({ key, labelKey }) => ( + + ))} + - ); diff --git a/src/app/screens/settings/changeNetwork/networkRow.tsx b/src/app/screens/settings/changeNetwork/networkRow.tsx index 2169c1354..6f7cf65c6 100644 --- a/src/app/screens/settings/changeNetwork/networkRow.tsx +++ b/src/app/screens/settings/changeNetwork/networkRow.tsx @@ -20,7 +20,7 @@ const Button = styled.button((props) => ({ })); const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, + ...props.theme.typography.body_medium_m, color: props.color, flex: 1, textAlign: 'left', diff --git a/src/app/screens/settings/changeNetwork/nodeInput.tsx b/src/app/screens/settings/changeNetwork/nodeInput.tsx new file mode 100644 index 000000000..ce1a7b32b --- /dev/null +++ b/src/app/screens/settings/changeNetwork/nodeInput.tsx @@ -0,0 +1,90 @@ +import { XCircle } from '@phosphor-icons/react'; +import InputFeedback from '@ui-library/inputFeedback'; +import { useTranslation } from 'react-i18next'; +import styled, { useTheme } from 'styled-components'; + +const NodeInputHeader = styled.div` + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + padding-left: ${(props) => props.theme.spacing(1)}; + padding-right: ${(props) => props.theme.spacing(1)}; +`; + +const NodeText = styled.label` + ${(props) => props.theme.typography.body_medium_m} + color: ${(props) => props.theme.colors.white_200}; +`; + +const NodeResetButton = styled.button` + ${(props) => props.theme.typography.body_medium_m} + background: none; + color: ${(props) => props.theme.colors.white_200}; +`; + +// TODO create and use a ui-library input with proper input box styling +const InputContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + border: 1px solid ${(props) => props.theme.colors.elevation3}; + background-color: ${(props) => props.theme.colors.elevation_n1}; + border-radius: ${(props) => props.theme.radius(1)}px; + padding-left: ${(props) => props.theme.space.m}; + padding-right: ${(props) => props.theme.space.m}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.s}; +`; + +const Input = styled.input` + ${(props) => props.theme.typography.body_medium_m} + height: 44px; + display: flex; + flex: 1; + background-color: ${(props) => props.theme.colors.elevation_n1}; + color: ${(props) => props.theme.colors.white_200}; + border: none; +`; + +const Button = styled.button` + background: none; +`; + +function NodeInput({ + label, + onChange, + value, + onClear, + onReset, + error, +}: { + label: string; + onChange: (event: React.ChangeEvent) => void; + value: string; + onClear: () => void; + onReset: () => void; + error: string; +}) { + const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); + const theme = useTheme(); + + return ( +
+ + {label} + {t('RESET_TO_DEFAULT')} + + + + + + +
+ ); +} + +export default NodeInput; diff --git a/src/app/stores/wallet/actions/actionCreators.ts b/src/app/stores/wallet/actions/actionCreators.ts index 587dbf1d6..7ad9cc88d 100644 --- a/src/app/stores/wallet/actions/actionCreators.ts +++ b/src/app/stores/wallet/actions/actionCreators.ts @@ -176,16 +176,10 @@ export function ChangeFiatCurrencyAction( }; } -export function ChangeNetworkAction( - network: SettingsNetwork, - networkAddress: string | undefined, - btcApiUrl: string, -): actions.ChangeNetwork { +export function ChangeNetworkAction(network: SettingsNetwork): actions.ChangeNetwork { return { type: actions.ChangeNetworkKey, network, - networkAddress, - btcApiUrl, }; } diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index 734210049..e252a4034 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -21,30 +21,21 @@ export const SetFeeMultiplierKey = 'SetFeeMultiplierKey'; export const ChangeFiatCurrencyKey = 'ChangeFiatCurrency'; export const ChangeNetworkKey = 'ChangeNetwork'; export const GetActiveAccountsKey = 'GetActiveAccounts'; - export const FetchStxWalletDataRequestKey = 'FetchStxWalletDataRequest'; export const SetStxWalletDataKey = 'SetStxWalletDataKey'; - export const SetBtcWalletDataKey = 'SetBtcWalletData'; - export const SetCoinRatesKey = 'SetCoinRatesKey'; - export const SetCoinDataKey = 'SetCoinDataKey'; - export const ChangeHasActivatedOrdinalsKey = 'ChangeHasActivatedOrdinalsKey'; export const RareSatsNoticeDismissedKey = 'RareSatsNoticeDismissedKey'; export const ChangeHasActivatedRareSatsKey = 'ChangeHasActivatedRareSatsKey'; export const ChangeHasActivatedRBFKey = 'ChangeHasActivatedRBFKey'; - export const ChangeShowBtcReceiveAlertKey = 'ChangeShowBtcReceiveAlertKey'; export const ChangeShowOrdinalReceiveAlertKey = 'ChangeShowOrdinalReceiveAlertKey'; export const ChangeShowDataCollectionAlertKey = 'ChangeShowDataCollectionAlertKey'; export const UpdateLedgerAccountsKey = 'UpdateLedgerAccountsKey'; - export const SetBrcCoinsListKey = 'SetBrcCoinsList'; - export const SetWalletLockPeriodKey = 'SetWalletLockPeriod'; - export const SetWalletUnlockedKey = 'SetWalletUnlocked'; export enum WalletSessionPeriods { @@ -64,7 +55,8 @@ export interface WalletState { accountsList: Account[]; ledgerAccountsList: Account[]; selectedAccount: Account | null; - network: SettingsNetwork; + network: SettingsNetwork; // currently selected network urls and type + savedNetworks: SettingsNetwork[]; // previously set network urls for type encryptedSeed: string; fiatCurrency: SupportedCurrency; btcFiatRate: string; @@ -78,7 +70,6 @@ export interface WalletState { coins: Coin[]; brcCoinsList: FungibleToken[] | null; feeMultipliers: AppInfo | null; - networkAddress: string | undefined; hasActivatedOrdinalsKey: boolean | undefined; hasActivatedRareSatsKey: boolean | undefined; hasActivatedRBFKey: boolean | undefined; @@ -88,7 +79,6 @@ export interface WalletState { showDataCollectionAlert: boolean | null; accountType: AccountType | undefined; accountName: string | undefined; - btcApiUrl: string; walletLockPeriod: WalletSessionPeriods; isUnlocked: boolean; } @@ -178,8 +168,6 @@ export interface ChangeFiatCurrency { export interface ChangeNetwork { type: typeof ChangeNetworkKey; network: SettingsNetwork; - networkAddress: string | undefined; - btcApiUrl: string; } export interface GetActiveAccounts { diff --git a/src/app/stores/wallet/reducer.ts b/src/app/stores/wallet/reducer.ts index 6e626e48c..b09f5a4b2 100644 --- a/src/app/stores/wallet/reducer.ts +++ b/src/app/stores/wallet/reducer.ts @@ -1,10 +1,10 @@ -import { initialNetworksList } from '@utils/constants'; +import { defaultMainnet, initialNetworksList } from '@secretkeylabs/xverse-core'; import { AddAccountKey, ChangeFiatCurrencyKey, ChangeHasActivatedOrdinalsKey, - ChangeHasActivatedRareSatsKey, ChangeHasActivatedRBFKey, + ChangeHasActivatedRareSatsKey, ChangeNetworkKey, ChangeShowBtcReceiveAlertKey, ChangeShowDataCollectionAlertKey, @@ -70,7 +70,8 @@ const initialWalletState: WalletState = { stxPublicKey: '', btcPublicKey: '', ordinalsPublicKey: '', - network: initialNetworksList[0], + network: { ...defaultMainnet }, + savedNetworks: initialNetworksList, accountsList: [], ledgerAccountsList: [], selectedAccount: null, @@ -87,8 +88,6 @@ const initialWalletState: WalletState = { coins: [], brcCoinsList: [], feeMultipliers: null, - networkAddress: undefined, - btcApiUrl: '', hasActivatedOrdinalsKey: undefined, hasActivatedRareSatsKey: undefined, hasActivatedRBFKey: true, @@ -204,8 +203,10 @@ const walletReducer = ( return { ...state, network: action.network, - networkAddress: action.networkAddress, - btcApiUrl: action.btcApiUrl, + savedNetworks: [ + ...state.savedNetworks.filter((n) => n.type !== action.network.type), + action.network, + ], }; case GetActiveAccountsKey: return { diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts index cd9b5254f..cbc11f1e7 100644 --- a/src/app/utils/constants.ts +++ b/src/app/utils/constants.ts @@ -1,11 +1,5 @@ /* eslint-disable prefer-destructuring */ -import type { NetworkType, SettingsNetwork } from '@secretkeylabs/xverse-core'; -import { - BTC_BASE_URI_MAINNET, - BTC_BASE_URI_TESTNET, - HIRO_MAINNET_DEFAULT, - HIRO_TESTNET_DEFAULT, -} from '@secretkeylabs/xverse-core'; +import type { NetworkType } from '@secretkeylabs/xverse-core'; export const GAMMA_URL = 'https://gamma.io/'; export const TERMS_LINK = 'https://xverse.app/terms'; @@ -47,21 +41,6 @@ export const BITCOIN_DUST_AMOUNT_SATS = 1500; export const PAGINATION_LIMIT = 50; export const REFETCH_UNSPENT_UTXO_TIME = 2 * 60 * 60 * 1000; -export const initialNetworksList: SettingsNetwork[] = [ - { - type: 'Mainnet', - address: HIRO_MAINNET_DEFAULT, - btcApiUrl: BTC_BASE_URI_MAINNET, - fallbackBtcApiUrl: '', - }, - { - type: 'Testnet', - address: HIRO_TESTNET_DEFAULT, - btcApiUrl: BTC_BASE_URI_TESTNET, - fallbackBtcApiUrl: '', - }, -]; - /** * contract id of send_many transaction type */ diff --git a/src/locales/en.json b/src/locales/en.json index d862fde20..4f67aa36d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -711,6 +711,11 @@ "DESCRIPTION": "Help improve the app experience, by allowing Xverse to collect anonymized usage data. This data cannot be used to identify your wallet individually.", "AUTHORIZE_DATA_COLLECTION": "Authorize data collection" }, + "BTC_URL": "BTC URL", + "STACKS_URL": "Stacks URL", + "FALLBACK_BTC_URL": "Fallback BTC URL", + "RESET_TO_DEFAULT": "Reset to default", + "REQUIRED": "Required", "NETWORK": "Network", "SECURITY": "Security", "UPDATE_PASSWORD": "Update Password", diff --git a/tsconfig.json b/tsconfig.json index baba9fda5..e3bbeac91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,6 @@ "isolatedModules": true, "noEmit": false, "jsx": "react-jsx", - "rootDir": "src", "outDir": "build/js", "baseUrl": "./src", "paths": { From 2f7a7dcfeaa488a8166a32c09749b8f418a814d6 Mon Sep 17 00:00:00 2001 From: fede erbes Date: Thu, 11 Jan 2024 09:55:41 +0100 Subject: [PATCH 11/20] chore: add new auto lock period values and migrate users if needed (#736) * chore: add new auto lock period values and migrate users if needed * chore: improve ui around auto lock options * chore: use l10n in time labels --- src/app/hooks/useWalletReducer.ts | 5 +- src/app/hooks/useWalletSession.ts | 11 ++- .../screens/settings/lockCountdown/index.tsx | 94 +++++++++---------- src/app/stores/wallet/actions/types.ts | 7 +- src/assets/img/settings/Timer.svg | 3 - src/assets/img/settings/Timer15m.svg | 6 ++ src/assets/img/settings/Timer1h.svg | 6 ++ src/assets/img/settings/Timer30m.svg | 6 ++ src/assets/img/settings/Timer3h.svg | 6 ++ src/assets/img/settings/TimerFull.svg | 4 - src/assets/img/settings/TimerHalf.svg | 4 - src/locales/en.json | 3 + 12 files changed, 91 insertions(+), 64 deletions(-) delete mode 100644 src/assets/img/settings/Timer.svg create mode 100644 src/assets/img/settings/Timer15m.svg create mode 100644 src/assets/img/settings/Timer1h.svg create mode 100644 src/assets/img/settings/Timer30m.svg create mode 100644 src/assets/img/settings/Timer3h.svg delete mode 100644 src/assets/img/settings/TimerFull.svg delete mode 100644 src/assets/img/settings/TimerHalf.svg diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 366173d14..d8cf87752 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -51,7 +51,8 @@ const useWalletReducer = () => { const dispatch = useDispatch(); const { refetch: refetchStxData } = useStxWalletData(); const { refetch: refetchBtcData } = useBtcWalletData(); - const { setSessionStartTime, clearSessionTime } = useWalletSession(); + const { setSessionStartTime, clearSessionTime, setSessionStartTimeAndMigrate } = + useWalletSession(); const queryClient = useQueryClient(); const loadActiveAccounts = async ( @@ -144,7 +145,7 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(accountsList[0], accountsList)); dispatch(getActiveAccountsAction(accountsList)); } finally { - setSessionStartTime(); + setSessionStartTimeAndMigrate(); } }; diff --git a/src/app/hooks/useWalletSession.ts b/src/app/hooks/useWalletSession.ts index 95ceb0d5d..730d77b41 100644 --- a/src/app/hooks/useWalletSession.ts +++ b/src/app/hooks/useWalletSession.ts @@ -1,9 +1,9 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { setWalletLockPeriodAction } from '@stores/wallet/actions/actionCreators'; import { WalletSessionPeriods } from '@stores/wallet/actions/types'; +import { chromeSessionStorage } from '@utils/chromeStorage'; import { addMinutes } from 'date-fns'; import { useDispatch } from 'react-redux'; -import { chromeSessionStorage } from '@utils/chromeStorage'; import useSeedVault from './useSeedVault'; const SESSION_START_TIME_KEY = 'sessionStartTime'; @@ -36,11 +36,20 @@ const useWalletSession = () => { setSessionStartTime(); }; + const setSessionStartTimeAndMigrate = () => { + if (walletLockPeriod < WalletSessionPeriods.LOW) { + return setWalletLockPeriod(WalletSessionPeriods.LOW); + } + + setSessionStartTime(); + }; + return { setSessionStartTime, setWalletLockPeriod, shouldLock, clearSessionTime, + setSessionStartTimeAndMigrate, }; }; diff --git a/src/app/screens/settings/lockCountdown/index.tsx b/src/app/screens/settings/lockCountdown/index.tsx index cabb8a8e9..18a329775 100644 --- a/src/app/screens/settings/lockCountdown/index.tsx +++ b/src/app/screens/settings/lockCountdown/index.tsx @@ -1,13 +1,14 @@ -import Timer from '@assets/img/settings/Timer.svg'; -import TimerFull from '@assets/img/settings/TimerFull.svg'; -import TimerHalf from '@assets/img/settings/TimerHalf.svg'; +import Timer15 from '@assets/img/settings/Timer15m.svg'; +import Timer1 from '@assets/img/settings/Timer1h.svg'; +import Timer30 from '@assets/img/settings/Timer30m.svg'; +import Timer3 from '@assets/img/settings/Timer3h.svg'; import ActionButton from '@components/button'; import TopRow from '@components/topRow'; import useWalletSelector from '@hooks/useWalletSelector'; import useWalletSession from '@hooks/useWalletSession'; import { WalletSessionPeriods } from '@stores/wallet/actions/types'; import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -40,27 +41,34 @@ interface TimeSelectionBoxProps { } const TimeSelectionBox = styled.button((props) => ({ - ...props.theme.body_medium_m, - backgroundColor: 'transparent', - border: `1px solid ${props.selected ? props.theme.colors.white_0 : props.theme.colors.grey}`, - color: props.theme.colors.white_0, + ...props.theme.typography.body_medium_m, + backgroundColor: props.selected ? props.theme.colors.white_900 : 'transparent', + border: `1px solid ${props.theme.colors.white_800}`, + color: props.selected ? props.theme.colors.white_0 : props.theme.colors.white_200, borderRadius: props.theme.radius(1), - height: 44, display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', - marginBottom: props.theme.spacing(12), - paddingLeft: props.theme.spacing(6), - paddingRight: props.theme.spacing(6), + padding: props.theme.space.m, + marginBottom: props.theme.space.s, })); -const TimerIcon = styled.img((props) => ({ +const TimerIcon = styled.img((props) => ({ width: 18, height: 21, - marginRight: props.theme.spacing(12), + marginRight: props.theme.space.l, + opacity: props.selected ? 1 : 0.8, })); +const getLabel = (period: number, t: TFunction<'translation', 'SETTING_SCREEN'>) => { + if (period < 60) { + return t('LOCK_COUNTDOWN_MIN', { count: period }); + } + const hours = period / 60; + return t('LOCK_COUNTDOWN_HS', { count: hours }); +}; + function LockCountdown() { const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); @@ -72,49 +80,41 @@ function LockCountdown() { navigate(-1); }; - const onChooseLow = () => { - setSelectedTime(WalletSessionPeriods.LOW); - }; - - const onChooseStandard = () => { - setSelectedTime(WalletSessionPeriods.STANDARD); - }; - - const onChooseLong = () => { - setSelectedTime(WalletSessionPeriods.LONG); - }; - const onSave = () => { setWalletLockPeriod(selectedTime); navigate(-1); }; + const periodOptions: number[] = Object.keys(WalletSessionPeriods) + .filter((key) => !Number.isNaN(Number(WalletSessionPeriods[key]))) + .map((key) => WalletSessionPeriods[key]); + + const iconsByPeriod = { + [WalletSessionPeriods.LOW]: Timer15, + [WalletSessionPeriods.STANDARD]: Timer30, + [WalletSessionPeriods.LONG]: Timer1, + [WalletSessionPeriods.VERY_LONG]: Timer3, + }; + return ( <> {t('LOCK_COUNTDOWN_TITLE')} - - - {`${WalletSessionPeriods.LOW} minute`} - - - - {`${WalletSessionPeriods.STANDARD} minutes`} - - - - {`${WalletSessionPeriods.LONG} minutes`} - + {periodOptions.map((period) => ( + setSelectedTime(period)} + > + + {getLabel(period, t)} + + ))} diff --git a/src/app/stores/wallet/actions/types.ts b/src/app/stores/wallet/actions/types.ts index e252a4034..bec02f74e 100644 --- a/src/app/stores/wallet/actions/types.ts +++ b/src/app/stores/wallet/actions/types.ts @@ -39,9 +39,10 @@ export const SetWalletLockPeriodKey = 'SetWalletLockPeriod'; export const SetWalletUnlockedKey = 'SetWalletUnlocked'; export enum WalletSessionPeriods { - LOW = 1, - STANDARD = 10, - LONG = 30, + LOW = 15, + STANDARD = 30, + LONG = 60, + VERY_LONG = 180, } export interface WalletState { diff --git a/src/assets/img/settings/Timer.svg b/src/assets/img/settings/Timer.svg deleted file mode 100644 index 504fd5ff2..000000000 --- a/src/assets/img/settings/Timer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/img/settings/Timer15m.svg b/src/assets/img/settings/Timer15m.svg new file mode 100644 index 000000000..b7bbd5c34 --- /dev/null +++ b/src/assets/img/settings/Timer15m.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer1h.svg b/src/assets/img/settings/Timer1h.svg new file mode 100644 index 000000000..dfdc84e87 --- /dev/null +++ b/src/assets/img/settings/Timer1h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer30m.svg b/src/assets/img/settings/Timer30m.svg new file mode 100644 index 000000000..333786f7d --- /dev/null +++ b/src/assets/img/settings/Timer30m.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/Timer3h.svg b/src/assets/img/settings/Timer3h.svg new file mode 100644 index 000000000..687b5e489 --- /dev/null +++ b/src/assets/img/settings/Timer3h.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/img/settings/TimerFull.svg b/src/assets/img/settings/TimerFull.svg deleted file mode 100644 index 5a8b51d50..000000000 --- a/src/assets/img/settings/TimerFull.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/assets/img/settings/TimerHalf.svg b/src/assets/img/settings/TimerHalf.svg deleted file mode 100644 index 914f48708..000000000 --- a/src/assets/img/settings/TimerHalf.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/locales/en.json b/src/locales/en.json index 4f67aa36d..4f87ed20b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -746,6 +746,9 @@ "RECOVER_ASSETS": "Recover assets", "LOCK_COUNTDOWN": "Auto-lock Timer", "LOCK_COUNTDOWN_TITLE": "Select the time duration before the wallet locks automatically.", + "LOCK_COUNTDOWN_MIN": "{{count}} minutes", + "LOCK_COUNTDOWN_HS_one": "{{count}} hour", + "LOCK_COUNTDOWN_HS_other": "{{count}} hours", "ENTER_YOUR_NEW_PASSWORD": "Enter your new password", "CONFIRM_YOUR_NEW_PASSWORD": "Confirm your new password", "TEXT_INPUT_NEW_PASSWORD_LABEL": "New Password", From 7898e483524008d69957fef6cb4911de46e7b5f3 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Thu, 11 Jan 2024 11:00:47 +0200 Subject: [PATCH 12/20] Allow testnet ledger accounts (#688) * Allow testnet ledger accounts * Fix merge issue * Enable account switch on ledger * pr review fix * Fix ledger device account index * Fix ledger filtering on select account screen * fix ledger account ID derivation --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> Co-authored-by: Tim Man --- src/app/hooks/useWalletReducer.ts | 16 ++++++++++++-- src/app/screens/accountList/index.tsx | 21 ++++++++----------- .../screens/btcSelectAddressScreen/index.tsx | 5 ++++- .../ledger/importLedgerAccount/index.tsx | 2 ++ src/app/screens/settings/index.tsx | 13 +++++------- src/app/screens/speedUpTransaction/index.tsx | 2 +- src/common/utils/ledger.ts | 20 +++++++++++++++--- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index d8cf87752..883a20ea5 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -1,4 +1,4 @@ -import { getDeviceAccountIndex } from '@common/utils/ledger'; +import { filterLedgerAccounts, getDeviceAccountIndex } from '@common/utils/ledger'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; import useStxWalletData from '@hooks/queries/useStxWalletData'; import useNetworkSelector from '@hooks/useNetwork'; @@ -85,7 +85,16 @@ const useWalletReducer = () => { if (!selectedAccount) { [selectedAccountData] = walletAccounts; } else if (isLedgerAccount(selectedAccount)) { - selectedAccountData = ledgerAccountsList.find((a) => a.id === selectedAccount.id); + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, currentNetwork.type); + const selectedAccountDataInNetwork = networkLedgerAccounts.find( + (a) => a.id === selectedAccount.id, + ); + + // we try find the specific matching ledger account + // If we can't find it, we default to the first ledger account in the selected network + // If we can't find that, we default to the first software account in the wallet + selectedAccountData = + selectedAccountDataInNetwork ?? networkLedgerAccounts[0] ?? walletAccounts[0]; } else { selectedAccountData = walletAccounts.find((a) => a.id === selectedAccount.id); } @@ -106,6 +115,9 @@ const useWalletReducer = () => { dispatch(fetchAccountAction(selectedAccountData, walletAccounts)); + // ledger accounts initially didn't have a deviceAccountIndex + // this is a migration to add the deviceAccountIndex to the ledger accounts without them + // it should only fire once if ever if (ledgerAccountsList.some((account) => account.deviceAccountIndex === undefined)) { const newLedgerAccountsList = ledgerAccountsList.map((account) => ({ ...account, diff --git a/src/app/screens/accountList/index.tsx b/src/app/screens/accountList/index.tsx index f86106f2d..9e50a792c 100644 --- a/src/app/screens/accountList/index.tsx +++ b/src/app/screens/accountList/index.tsx @@ -1,5 +1,6 @@ import ConnectLedger from '@assets/img/dashboard/connect_ledger.svg'; import Plus from '@assets/img/dashboard/plus.svg'; +import { filterLedgerAccounts } from '@common/utils/ledger'; import AccountRow from '@components/accountRow'; import Separator from '@components/separator'; import TopRow from '@components/topRow'; @@ -85,10 +86,8 @@ function AccountList(): JSX.Element { const { createAccount, switchAccount } = useWalletReducer(); const displayedAccountsList = useMemo(() => { - if (network.type === 'Mainnet') { - return [...ledgerAccountsList, ...accountsList]; - } - return accountsList; + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); + return [...networkLedgerAccounts, ...accountsList]; }, [accountsList, ledgerAccountsList, network]); const handleAccountSelect = async (account: Account, goBack = true) => { @@ -140,14 +139,12 @@ function AccountList(): JSX.Element { {t('NEW_ACCOUNT')} - {network.type === 'Mainnet' && ( - - - - - {t('LEDGER_ACCOUNT')} - - )} + + + + + {t('LEDGER_ACCOUNT')} + ); diff --git a/src/app/screens/btcSelectAddressScreen/index.tsx b/src/app/screens/btcSelectAddressScreen/index.tsx index bf2ce535c..5eb6b86a3 100644 --- a/src/app/screens/btcSelectAddressScreen/index.tsx +++ b/src/app/screens/btcSelectAddressScreen/index.tsx @@ -2,6 +2,7 @@ import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; import XverseLogo from '@assets/img/settings/logo.svg'; import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; import DappPlaceholderIcon from '@assets/img/webInteractions/authPlaceholder.svg'; +import { filterLedgerAccounts } from '@common/utils/ledger'; import AccountRow from '@components/accountRow'; import ActionButton from '@components/button'; import Separator from '@components/separator'; @@ -234,6 +235,8 @@ function BtcSelectAddressScreen() { switchAccountBasedOnRequest(); }, []); + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network.type); + return ( <> @@ -262,7 +265,7 @@ function BtcSelectAddressScreen() { {showAccountList ? ( - {[...ledgerAccountsList, ...accountsList].map((account) => ( + {[...networkLedgerAccounts, ...accountsList].map((account) => ( - {!isLedgerAccount(selectedAccount) && ( - - )} - + new Promise((res) => { setTimeout(res, ms); }); +export const filterLedgerAccounts = (accounts: Account[], network: NetworkType) => + accounts.filter((account) => + account.ordinalsAddress?.startsWith(network === 'Mainnet' ? 'bc1' : 'tb1'), + ); + +// this is used for migrating the old ledger accounts to the new format +// it returns the index of the account in the list, which now maps to the deviceAccountIndex export const getDeviceAccountIndex = ( ledgerAccountsList: Account[], id: number, @@ -31,10 +38,17 @@ export const getNewAccountId = (ledgerAccountsList: Account[]) => { return ledgerAccountsList[ledgerAccountsList.length - 1].id + 1; }; -export const getDeviceNewAccountIndex = (ledgerAccountsList: Account[], masterKey?: string) => { - const ledgerAccountsIndexList = ledgerAccountsList +export const getDeviceNewAccountIndex = ( + ledgerAccountsList: Account[], + network: NetworkType, + masterKey?: string, +) => { + const networkLedgerAccounts = filterLedgerAccounts(ledgerAccountsList, network); + + const ledgerAccountsIndexList = networkLedgerAccounts .filter((account) => masterKey === account.masterPubKey) .map((account, key) => + // ledger accounts initially didn't have deviceAccountIndex, so we map to their list index as as the initial behaviour account.deviceAccountIndex !== undefined ? account.deviceAccountIndex : key, ) .sort((a, b) => a - b); From 73461bfe27eb105bc8259dcda98dc1eb090f2b0b Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 11 Jan 2024 10:26:52 +0000 Subject: [PATCH 13/20] release: v0.28.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09f7772f2..4ab9c0e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.27.0", + "version": "0.28.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.27.0", + "version": "0.28.0", "dependencies": { "@ledgerhq/hw-transport-webusb": "^6.27.13", "@phosphor-icons/react": "^2.0.10", diff --git a/package.json b/package.json index 6364bfe4c..2a7f1ddf0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.27.0", + "version": "0.28.0", "private": true, "engines": { "node": "^18.18.2" From 748bb2934e13d7de47f9d2eb97ffae34bb36926a Mon Sep 17 00:00:00 2001 From: Tim Man Date: Thu, 11 Jan 2024 19:04:26 +0800 Subject: [PATCH 14/20] chore: fix build-rc.yml workflow file --- .github/workflows/build-rc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-rc.yml b/.github/workflows/build-rc.yml index cfe31363e..3bc833f0b 100644 --- a/.github/workflows/build-rc.yml +++ b/.github/workflows/build-rc.yml @@ -61,10 +61,10 @@ jobs: -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/{owner}/{repo}/releases > releases.json # get $TAG from branch name, e.g. v0.25.0 - TAG=$(echo $SOURCE_BRANCH | sed 's/release\/\(.*\)/\1/') + export TAG=$(echo $SOURCE_BRANCH | sed 's/release\/\(.*\)/\1/') # export $NEXT_TAG using releases.json and $TAG, e.g. v0.25.0-rc.0 cd scripts - ./find-tag.sh + source ./find-tag.sh # publish the release as prerelease rc gh api \ --method POST \ From 526886368a9bcb96bd89225468d4a5dbe115a1cd Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:54:37 +0100 Subject: [PATCH 15/20] [ENG-3474] feat: Make UI for STX RBF in the extension (#727) * [ENG-3436] chore: Make some code refactoring for RBF in the extension * Make some code fixes * Make some code fixes * fix: put speed up screen in another scrollable container * Make some code tweaks according to PR review comments * Remove the unused import * fix nonce not being set correctly for tx requests * fix nonce calc persisted * [ENG-3455] fix: Fix the sharp fee change on tx confirmation * release v0.26.1 * package-lock update * fix: cap stx contract call fees (#722) * fix: cap stx contract call fees * chore: update core version * chore: bump to core 6.0.1 * [ENG-3474] feat: Make UI for STX RBF in the extension * Add the `useRbfTransactionData` hook * Add some logic for stx rbf * Add some logic for stx rbf * Improve the stx rbf logic * Improve the stx rbf logic * Add the insufficient funds check * Add ledger signing and broadcasting for stx rbf * Add the fee cap * Allow decimal fee for custom stx rbf * Fix the loading state in the rbf hook * Remove the todo comment * Remove the `convertStringHexToBufferReader` helper func * Round up the minimum rbf fee for stx txs * Create separate btc and stx rbf components * Remove the old todo comment * improve the stx rbf logic * Make some small code tweaks after the PR review * Pass the seedVault.getSeed func * Update the useRbfTransactionData hook * Update the rbf logic --------- Co-authored-by: Tim Man Co-authored-by: Mahmoud Aboelenein Co-authored-by: Yukan --- .../itemRow/rareSats.tsx | 2 +- .../txInOutput/transactionOutput.tsx | 2 +- src/app/components/speedUpTransaction/btc.tsx | 228 ++++++++++ .../speedUpTransaction/index.styled.ts | 113 +++++ src/app/components/speedUpTransaction/stx.tsx | 206 +++++++++ .../transactions/btcTransaction.tsx | 6 +- .../transactions/stxTransaction.tsx | 49 +-- .../transactions/stxTransferTransaction.tsx | 76 +++- src/app/hooks/queries/useTransaction.ts | 3 +- src/app/hooks/useRbfTransactionData.ts | 271 ++++++++++++ src/app/hooks/useWalletReducer.ts | 10 +- .../ledger/importLedgerAccount/index.tsx | 2 +- .../customFee/index.styled.ts | 2 +- .../speedUpTransaction/customFee/index.tsx | 36 +- .../speedUpTransaction/index.styled.ts | 122 +----- src/app/screens/speedUpTransaction/index.tsx | 414 +++++++----------- src/assets/img/transactions/increaseFee.svg | 8 - src/locales/en.json | 2 +- 18 files changed, 1088 insertions(+), 464 deletions(-) create mode 100644 src/app/components/speedUpTransaction/btc.tsx create mode 100644 src/app/components/speedUpTransaction/index.styled.ts create mode 100644 src/app/components/speedUpTransaction/stx.tsx create mode 100644 src/app/hooks/useRbfTransactionData.ts delete mode 100644 src/assets/img/transactions/increaseFee.svg diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx index f6c8de8e3..ee362347c 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -10,7 +10,7 @@ import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import Theme from 'theme'; import Avatar from '../../../ui-library/avatar'; -import { mapTxSatributeInfoToBundleInfo, SatRangeTx } from '../utils'; +import { SatRangeTx, mapTxSatributeInfoToBundleInfo } from '../utils'; const SatsBundleContainer = styled.div` display: flex; diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index 6128e9bcd..43c8ab099 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -1,5 +1,5 @@ -import OutputIcon from '@assets/img/transactions/output.svg'; import ScriptIcon from '@assets/img/transactions/ScriptIcon.svg'; +import OutputIcon from '@assets/img/transactions/output.svg'; import TransferDetailView from '@components/transferDetailView'; import useWalletSelector from '@hooks/useWalletSelector'; import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; diff --git a/src/app/components/speedUpTransaction/btc.tsx b/src/app/components/speedUpTransaction/btc.tsx new file mode 100644 index 000000000..1504438dd --- /dev/null +++ b/src/app/components/speedUpTransaction/btc.tsx @@ -0,0 +1,228 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; +import { EMPTY_LABEL } from '@utils/constants'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { useTheme } from 'styled-components'; +import { + ButtonContainer, + Container, + ControlsContainer, + CustomFeeIcon, + DetailText, + FeeButton, + FeeButtonLeft, + FeeButtonRight, + HighlightedText, + SecondaryText, + StyledActionButton, + Title, + WarningText, +} from './index.styled'; + +type TierFees = { + enoughFunds: boolean; + fee?: number; + feeRate: number; +}; + +interface Props { + rbfTxSummary?: { + currentFee: number; + currentFeeRate: number; + minimumRbfFee: number; + minimumRbfFeeRate: number; + }; + rbfRecommendedFees?: { + medium?: TierFees; + high?: TierFees; + higher?: TierFees; + highest?: TierFees; + }; + selectedOption?: string; + customFeeRate?: string; + customTotalFee?: string; + feeButtonMapping: { + [key: string]: { + title: string; + icon: React.ReactNode; + }; + }; + handleGoBack: () => void; + handleClickFeeButton: (e: React.MouseEvent) => void; + handleClickSubmit: () => void; + getEstimatedCompletionTime: (feeRate?: number) => string; + isBroadcasting: boolean; +} + +function SpeedUpBtcTransaction({ + rbfTxSummary, + rbfRecommendedFees, + selectedOption, + customFeeRate, + customTotalFee, + feeButtonMapping, + handleGoBack, + handleClickFeeButton, + handleClickSubmit, + getEstimatedCompletionTime, + isBroadcasting, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'SPEED_UP_TRANSACTION' }); + const { btcFiatRate, fiatCurrency } = useWalletSelector(); + const theme = useTheme(); + + return ( + + {t('TITLE')} + {t('FEE_INFO')} + + {t('CURRENT_FEE')}{' '} + + + + + + + {t('ESTIMATED_COMPLETION_TIME')}{' '} + + {getEstimatedCompletionTime(rbfTxSummary?.currentFeeRate)} + + + + {rbfRecommendedFees && + Object.entries(rbfRecommendedFees).map(([key, obj]) => { + const isDisabled = !obj.enoughFunds; + + return ( + + + {feeButtonMapping[key].icon} +
+ {feeButtonMapping[key].title} + {getEstimatedCompletionTime(obj.feeRate)} + + + +
+
+ +
+ {obj.fee ? ( + + ) : ( + EMPTY_LABEL + )} +
+ + {obj.fee ? ( + + ) : ( + `${EMPTY_LABEL} ${fiatCurrency}` + )} + + {isDisabled && {t('INSUFFICIENT_FUNDS')}} +
+
+ ); + })} + + + +
+ {t('CUSTOM')} + {customFeeRate && ( + <> + {getEstimatedCompletionTime(Number(customFeeRate))} + + + + + )} +
+
+ + {customFeeRate && customTotalFee ? ( + <> + {value}} + /> + + + + + ) : ( + t('MANUAL_SETTING') + )} + +
+
+ + + + +
+ ); +} + +export default SpeedUpBtcTransaction; diff --git a/src/app/components/speedUpTransaction/index.styled.ts b/src/app/components/speedUpTransaction/index.styled.ts new file mode 100644 index 000000000..a9fba674e --- /dev/null +++ b/src/app/components/speedUpTransaction/index.styled.ts @@ -0,0 +1,113 @@ +import ActionButton from '@components/button'; +import { Faders } from '@phosphor-icons/react'; +import styled from 'styled-components'; + +export const Title = styled.h1((props) => ({ + ...props.theme.typography.headline_s, + color: props.theme.colors.white_0, + marginTop: props.theme.space.m, + marginBottom: props.theme.space.m, +})); + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + ...props.theme.scrollbar, +})); + +export const DetailText = styled.span((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_200, + marginBottom: props.theme.space.xs, +})); + +export const HighlightedText = styled.span((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, +})); + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + margin-top: ${(props) => props.theme.space.s}; + gap: ${(props) => props.theme.space.xs}; +`; + +export const FeeButton = styled.button<{ + isSelected: boolean; + centered?: boolean; +}>((props) => ({ + ...props.theme.typography.body_medium_m, + textAlign: 'left', + color: props.theme.colors.white_0, + backgroundColor: `${props.isSelected ? props.theme.colors.elevation6_600 : 'transparent'}`, + border: `1px solid ${ + props.isSelected ? props.theme.colors.white_800 : props.theme.colors.white_850 + }`, + borderRadius: props.theme.radius(2), + height: 'auto', + display: 'flex', + justifyContent: 'space-between', + alignItems: props.centered ? 'center' : 'flex-start', + transition: 'background-color 0.1s ease-in-out, border 0.1s ease-in-out', + padding: props.theme.space.m, + paddingTop: props.theme.space.s, + paddingBottom: props.theme.space.s, + ':not(:disabled):hover': { + borderColor: props.theme.colors.white_800, + }, + ':disabled': { + cursor: 'not-allowed', + color: props.theme.colors.white_400, + div: { + color: 'inherit', + }, + svg: { + fill: props.theme.colors.white_600, + }, + }, +})); + +export const ControlsContainer = styled.div` + display: flex; + column-gap: ${(props) => props.theme.space.s}; + margin: ${(props) => props.theme.space.xxl} 0px ${(props) => props.theme.space.xxl}; +`; + +export const CustomFeeIcon = styled(Faders)({ + transform: 'rotate(90deg)', +}); + +export const FeeButtonLeft = styled.div((props) => ({ + display: 'flex', + alignItems: 'center', + gap: props.theme.space.s, +})); + +export const FeeButtonRight = styled.div({ + textAlign: 'right', +}); + +export const SecondaryText = styled.div<{ + alignRight?: boolean; +}>((props) => ({ + ...props.theme.typography.body_medium_s, + color: props.theme.colors.white_200, + marginTop: props.theme.space.xxs, + textAlign: props.alignRight ? 'right' : 'left', +})); + +export const StyledActionButton = styled(ActionButton)((props) => ({ + 'div, h1': { + ...props.theme.typography.body_medium_m, + }, +})); + +export const WarningText = styled.span((props) => ({ + ...props.theme.typography.body_medium_s, + display: 'block', + color: props.theme.colors.danger_light, + marginTop: props.theme.space.xxs, +})); diff --git a/src/app/components/speedUpTransaction/stx.tsx b/src/app/components/speedUpTransaction/stx.tsx new file mode 100644 index 000000000..7c7b62ceb --- /dev/null +++ b/src/app/components/speedUpTransaction/stx.tsx @@ -0,0 +1,206 @@ +import FiatAmountText from '@components/fiatAmountText'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { getStxFiatEquivalent, stxToMicrostacks } from '@secretkeylabs/xverse-core'; +import { EMPTY_LABEL } from '@utils/constants'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import { useTheme } from 'styled-components'; +import { + ButtonContainer, + Container, + ControlsContainer, + CustomFeeIcon, + DetailText, + FeeButton, + FeeButtonLeft, + FeeButtonRight, + HighlightedText, + SecondaryText, + StyledActionButton, + Title, + WarningText, +} from './index.styled'; + +type TierFees = { + enoughFunds: boolean; + fee?: number; + feeRate: number; +}; + +interface Props { + rbfTxSummary?: { + currentFee: number; + currentFeeRate: number; + minimumRbfFee: number; + minimumRbfFeeRate: number; + }; + rbfRecommendedFees?: { + medium?: TierFees; + high?: TierFees; + higher?: TierFees; + highest?: TierFees; + }; + selectedOption?: string; + customFeeRate?: string; + customTotalFee?: string; + feeButtonMapping: { + [key: string]: { + title: string; + icon: React.ReactNode; + }; + }; + handleGoBack: () => void; + handleClickFeeButton: (e: React.MouseEvent) => void; + handleClickSubmit: () => void; + getEstimatedCompletionTime: (feeRate?: number) => string; + isBroadcasting: boolean; +} + +function SpeedUpStxTransaction({ + rbfTxSummary, + rbfRecommendedFees, + selectedOption, + customFeeRate, + customTotalFee, + feeButtonMapping, + handleGoBack, + handleClickFeeButton, + handleClickSubmit, + getEstimatedCompletionTime, + isBroadcasting, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'SPEED_UP_TRANSACTION' }); + const { btcFiatRate, stxBtcRate, fiatCurrency } = useWalletSelector(); + const theme = useTheme(); + + return ( + + {t('TITLE')} + {t('FEE_INFO')} + + {t('CURRENT_FEE')}{' '} + + + + + + {t('ESTIMATED_COMPLETION_TIME')}{' '} + + {getEstimatedCompletionTime(rbfTxSummary?.currentFeeRate)} + + + + {rbfRecommendedFees && + Object.entries(rbfRecommendedFees).map(([key, obj]) => { + const isDisabled = !obj.enoughFunds; + + return ( + + + {feeButtonMapping[key].icon} +
+ {feeButtonMapping[key].title} + {getEstimatedCompletionTime(obj.feeRate)} +
+
+ +
+ {obj.fee ? ( + + ) : ( + EMPTY_LABEL + )} +
+ + {obj.fee ? ( + + ) : ( + `${EMPTY_LABEL} ${fiatCurrency}` + )} + + {isDisabled && {t('INSUFFICIENT_FUNDS')}} +
+
+ ); + })} + + + +
+ {t('CUSTOM')} + {customFeeRate && ( + {getEstimatedCompletionTime(Number(customFeeRate))} + )} +
+
+ + {customFeeRate && customTotalFee ? ( + <> + {value}} + /> + + + + + ) : ( + t('MANUAL_SETTING') + )} + +
+
+ + + + +
+ ); +} + +export default SpeedUpStxTransaction; diff --git a/src/app/components/transactions/btcTransaction.tsx b/src/app/components/transactions/btcTransaction.tsx index 40194084b..ffec7015c 100644 --- a/src/app/components/transactions/btcTransaction.tsx +++ b/src/app/components/transactions/btcTransaction.tsx @@ -23,8 +23,8 @@ const TransactionContainer = styled.button((props) => ({ alignItems: 'center', width: '100%', padding: props.theme.spacing(5), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, background: 'none', ':hover': { background: props.theme.colors.white_900, @@ -45,7 +45,7 @@ const TransactionAmountContainer = styled.div({ const TransactionInfoContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.spacing(6), + marginLeft: props.theme.space.s, flex: 1, })); diff --git a/src/app/components/transactions/stxTransaction.tsx b/src/app/components/transactions/stxTransaction.tsx index e1563a7b8..13abf73de 100644 --- a/src/app/components/transactions/stxTransaction.tsx +++ b/src/app/components/transactions/stxTransaction.tsx @@ -3,55 +3,30 @@ import { parseStxTransactionData } from '@secretkeylabs/xverse-core'; import { AddressTransactionWithTransfers } from '@stacks/stacks-blockchain-api-types'; import { CurrencyTypes } from '@utils/constants'; import { isAddressTransactionWithTransfers, Tx } from '@utils/transactions/transactions'; -// import IncreaseFeeIcon from '@assets/img/transactions/increaseFee.svg'; -// import styled from 'styled-components'; -// import { useTranslation } from 'react-i18next'; import StxTransferTransaction from './stxTransferTransaction'; import TxTransfers from './txTransfers'; -// const IncreaseFeeButton = styled.button((props) => ({ -// ...props.theme.body_xs, -// display: 'flex', -// justifyContent: 'center', -// alignItems: 'center', -// alignSelf: 'flex-start', -// background: 'none', -// paddingLeft: props.theme.spacing(8), -// paddingRight: props.theme.spacing(8), -// color: props.theme.colors.white_0, -// border: `0.5px solid ${props.theme.colors.elevation3}`, -// height: 34, -// borderRadius: props.theme.radius(3), -// img: { -// marginRight: props.theme.spacing(3), -// }, -// })); - interface TransactionHistoryItemProps { transaction: AddressTransactionWithTransfers | Tx; transactionCoin: CurrencyTypes; txFilter: string | null; } -export default function StxTransactionHistoryItem(props: TransactionHistoryItemProps) { - const { transaction, transactionCoin, txFilter } = props; +export default function StxTransactionHistoryItem({ + transaction, + transactionCoin, + txFilter, +}: TransactionHistoryItemProps) { const { selectedAccount } = useWalletSelector(); - // const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); if (!isAddressTransactionWithTransfers(transaction)) { return ( - <> - - {/* - fee - {t('INCREASE_FEE_BUTTON')} - */} - + ); } // This is a normal Transaction or MempoolTransaction diff --git a/src/app/components/transactions/stxTransferTransaction.tsx b/src/app/components/transactions/stxTransferTransaction.tsx index 51db9c995..92e116bb2 100644 --- a/src/app/components/transactions/stxTransferTransaction.tsx +++ b/src/app/components/transactions/stxTransferTransaction.tsx @@ -1,20 +1,24 @@ +import ActionButton from '@components/button'; import TransactionAmount from '@components/transactions/transactionAmount'; import TransactionRecipient from '@components/transactions/transactionRecipient'; import TransactionStatusIcon from '@components/transactions/transactionStatusIcon'; import TransactionTitle from '@components/transactions/transactionTitle'; import useWalletSelector from '@hooks/useWalletSelector'; +import { FastForward } from '@phosphor-icons/react'; import { StxTransactionData } from '@secretkeylabs/xverse-core'; import { CurrencyTypes } from '@utils/constants'; import { getStxTxStatusUrl } from '@utils/helper'; -import styled from 'styled-components'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import styled, { useTheme } from 'styled-components'; const TransactionContainer = styled.button((props) => ({ - display: 'flex', width: '100%', - paddingTop: props.theme.spacing(5), - paddingBottom: props.theme.spacing(5), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), + display: 'flex', + alignItems: 'center', + padding: props.theme.spacing(5), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, background: 'none', ':hover': { background: props.theme.colors.white_900, @@ -27,15 +31,16 @@ const TransactionContainer = styled.button((props) => ({ const TransactionInfoContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'column', - marginLeft: props.theme.spacing(6), + marginLeft: props.theme.space.s, flex: 1, })); const TransactionAmountContainer = styled.div({ + width: '100%', display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', flex: 1, - width: '100%', - justifyContent: 'flex-end', }); const TransactionRow = styled.div((props) => ({ @@ -46,14 +51,38 @@ const TransactionRow = styled.div((props) => ({ ...props.theme.body_bold_m, })); +const StyledButton = styled(ActionButton)((props) => ({ + padding: 0, + border: 'none', + width: 'auto', + height: 'auto', + div: { + ...props.theme.typography.body_medium_m, + color: props.theme.colors.tangerine, + }, + ':hover:enabled': { + backgroundColor: 'transparent', + }, + ':active:enabled': { + backgroundColor: 'transparent', + }, +})); + interface StxTransferTransactionProps { transaction: StxTransactionData; transactionCoin: CurrencyTypes; } -export default function StxTransferTransaction(props: StxTransferTransactionProps) { - const { transaction, transactionCoin } = props; - const { network } = useWalletSelector(); +export default function StxTransferTransaction({ + transaction, + transactionCoin, +}: StxTransferTransactionProps) { + const { network, hasActivatedRBFKey } = useWalletSelector(); + const theme = useTheme(); + const { t } = useTranslation('translation', { keyPrefix: 'COIN_DASHBOARD_SCREEN' }); + const navigate = useNavigate(); + const showAccelerateButton = + hasActivatedRBFKey && transaction.txStatus === 'pending' && !transaction.incoming; const openTxStatusUrl = () => { window.open(getStxTxStatusUrl(transaction.txid, network), '_blank', 'noopener,noreferrer'); @@ -63,12 +92,31 @@ export default function StxTransferTransaction(props: StxTransferTransactionProp - +
+ + +
+ {showAccelerateButton && ( + { + e.stopPropagation(); + + navigate(`/speed-up-tx/${transaction.txid}`, { + state: { + transaction, + }, + }); + }} + icon={} + iconPosition="right" + /> + )}
-
); diff --git a/src/app/hooks/queries/useTransaction.ts b/src/app/hooks/queries/useTransaction.ts index f1dd82be5..30396baf1 100644 --- a/src/app/hooks/queries/useTransaction.ts +++ b/src/app/hooks/queries/useTransaction.ts @@ -3,7 +3,7 @@ import useWalletSelector from '@hooks/useWalletSelector'; import { fetchBtcTransaction } from '@secretkeylabs/xverse-core'; import { useQuery } from '@tanstack/react-query'; -export default function useTransaction(id: string) { +export default function useTransaction(id?: string) { const { selectedAccount } = useWalletSelector(); const btcClient = useBtcClient(); @@ -25,5 +25,6 @@ export default function useTransaction(id: string) { return useQuery({ queryKey: ['transaction', id], queryFn: fetchTransaction, + enabled: id !== undefined, }); } diff --git a/src/app/hooks/useRbfTransactionData.ts b/src/app/hooks/useRbfTransactionData.ts new file mode 100644 index 000000000..01d95782b --- /dev/null +++ b/src/app/hooks/useRbfTransactionData.ts @@ -0,0 +1,271 @@ +import { + BtcTransactionData, + RecommendedFeeResponse, + SettingsNetwork, + StacksTransaction, + StxTransactionData, + mempoolApi, + microstacksToStx, + rbf, +} from '@secretkeylabs/xverse-core'; +import { deserializeTransaction, estimateTransaction } from '@stacks/transactions'; +import { isLedgerAccount } from '@utils/helper'; +import axios from 'axios'; +import BigNumber from 'bignumber.js'; +import { useCallback, useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import useBtcClient from './useBtcClient'; +import useNetworkSelector from './useNetwork'; +import useSeedVault from './useSeedVault'; +import useWalletSelector from './useWalletSelector'; + +// TODO: move the types and helper functions below to xverse-core + +type TierFees = { + enoughFunds: boolean; + fee?: number; + feeRate: number; +}; + +type RbfRecommendedFees = { + medium?: TierFees; + high?: TierFees; + higher?: TierFees; + highest?: TierFees; +}; + +type RbfData = { + rbfTransaction?: InstanceType; + rbfTxSummary?: { + currentFee: number; + currentFeeRate: number; + minimumRbfFee: number; + minimumRbfFeeRate: number; + }; + rbfRecommendedFees?: RbfRecommendedFees; + mempoolFees?: RecommendedFeeResponse; + isLoading?: boolean; +}; + +export const isBtcTransaction = ( + transaction: BtcTransactionData | StxTransactionData, +): transaction is BtcTransactionData => transaction?.txType === 'bitcoin'; + +interface LatestNonceResponse { + last_mempool_tx_nonce: number; + last_executed_tx_nonce: number; + possible_next_nonce: number; + detected_missing_nonces: Array; +} + +export async function getLatestNonce( + stxAddress: string, + network: SettingsNetwork, +): Promise { + const baseUrl = network?.address; + const apiUrl = `${baseUrl}/extended/v1/address/${stxAddress}/nonces`; + return axios.get(apiUrl).then((response) => response.data); +} + +interface RawTransactionResponse { + raw_tx: string; +} + +export async function getRawTransaction(txId: string, network: SettingsNetwork): Promise { + const baseUrl = network?.address; + const apiUrl = `${baseUrl}/extended/v1/tx/${txId}/raw`; + + return axios.get(apiUrl).then((response) => response.data.raw_tx); +} + +const constructRecommendedFees = ( + lowerName: keyof RbfRecommendedFees, + lowerFeeRate: number, + higherName: keyof RbfRecommendedFees, + higherFeeRate: number, + stxAvailableBalance: string, +): RbfRecommendedFees => { + const bigNumLowerFee = BigNumber(lowerFeeRate); + const bigNumHigherFee = BigNumber(higherFeeRate); + + return { + [lowerName]: { + enoughFunds: bigNumLowerFee.lte(BigNumber(stxAvailableBalance)), + feeRate: microstacksToStx(bigNumLowerFee).toNumber(), + fee: microstacksToStx(bigNumLowerFee).toNumber(), + }, + [higherName]: { + enoughFunds: bigNumHigherFee.lte(BigNumber(stxAvailableBalance)), + feeRate: microstacksToStx(bigNumHigherFee).toNumber(), + fee: microstacksToStx(bigNumHigherFee).toNumber(), + }, + }; +}; + +const sortFees = (fees: RbfRecommendedFees) => + Object.fromEntries( + Object.entries(fees).sort((a, b) => { + const priorityOrder = ['highest', 'higher', 'high', 'medium']; + return priorityOrder.indexOf(a[0]) - priorityOrder.indexOf(b[0]); + }), + ); + +const useRbfTransactionData = (transaction?: BtcTransactionData | StxTransactionData): RbfData => { + const [isLoading, setIsLoading] = useState(true); + const [rbfData, setRbfData] = useState({}); + const { accountType, network, selectedAccount, stxAvailableBalance, feeMultipliers } = + useWalletSelector(); + const seedVault = useSeedVault(); + const btcClient = useBtcClient(); + const selectedNetwork = useNetworkSelector(); + const { t } = useTranslation('translation', { keyPrefix: 'SPEED_UP_TRANSACTION' }); + const navigate = useNavigate(); + + // TODO: move the STX RBF calculations to xverse-core and add unit tests + const fetchStxData = useCallback(async () => { + if (!transaction || isBtcTransaction(transaction)) { + return; + } + + try { + setIsLoading(true); + + const { fee } = transaction; + const txRaw: string = await getRawTransaction(transaction.txid, network); + const unsignedTx: StacksTransaction = deserializeTransaction(txRaw); + + const [slow, medium, high] = await estimateTransaction( + unsignedTx.payload, + undefined, + selectedNetwork, + ); + + let feePresets: RbfRecommendedFees = {}; + let mediumFee = medium.fee; + let highFee = high.fee; + const higherFee = fee.multipliedBy(1.25).toNumber(); + const highestFee = fee.multipliedBy(1.5).toNumber(); + + if (feeMultipliers?.thresholdHighStacksFee) { + if (high.fee > feeMultipliers.thresholdHighStacksFee) { + // adding a fee cap + highFee = feeMultipliers.thresholdHighStacksFee * 1.5; + mediumFee = feeMultipliers.thresholdHighStacksFee; + } + } + + let minimumFee = fee.multipliedBy(1.25).toNumber(); + if (!Number.isSafeInteger(minimumFee)) { + // round up the fee to the nearest integer + minimumFee = Math.ceil(minimumFee); + } + + if (fee.lt(BigNumber(mediumFee))) { + feePresets = constructRecommendedFees( + 'medium', + mediumFee, + 'high', + highFee, + stxAvailableBalance, + ); + } else { + feePresets = constructRecommendedFees( + 'higher', + higherFee, + 'highest', + highestFee, + stxAvailableBalance, + ); + } + + setRbfData({ + rbfTransaction: undefined, + rbfTxSummary: { + currentFee: microstacksToStx(fee).toNumber(), + currentFeeRate: microstacksToStx(fee).toNumber(), + minimumRbfFee: microstacksToStx(BigNumber(minimumFee)).toNumber(), + minimumRbfFeeRate: microstacksToStx(BigNumber(minimumFee)).toNumber(), + }, + rbfRecommendedFees: sortFees(feePresets), + mempoolFees: { + fastestFee: microstacksToStx(BigNumber(high.fee)).toNumber(), + halfHourFee: microstacksToStx(BigNumber(medium.fee)).toNumber(), + hourFee: microstacksToStx(BigNumber(slow.fee)).toNumber(), + economyFee: microstacksToStx(BigNumber(slow.fee)).toNumber(), + minimumFee: microstacksToStx(BigNumber(slow.fee)).toNumber(), + }, + }); + } catch (err: any) { + toast.error(t('SOMETHING_WENT_WRONG')); + navigate(-1); + console.error(err); + } finally { + setIsLoading(false); + } + }, [transaction, network, selectedNetwork, feeMultipliers, stxAvailableBalance, t, navigate]); + + const fetchRbfData = useCallback(async () => { + if (!selectedAccount || !transaction) { + setIsLoading(false); + return; + } + + if (!isBtcTransaction(transaction)) { + return fetchStxData(); + } + + try { + setIsLoading(true); + + const rbfTx = new rbf.RbfTransaction(transaction, { + ...selectedAccount, + accountType: accountType || 'software', + accountId: + isLedgerAccount(selectedAccount) && selectedAccount.deviceAccountIndex + ? selectedAccount.deviceAccountIndex + : selectedAccount.id, + network: network.type, + esploraProvider: btcClient, + getSeedPhrase: seedVault.getSeed, + }); + + const mempoolFees = await mempoolApi.getRecommendedFees(network.type); + const rbfRecommendedFeesResponse = await rbfTx.getRbfRecommendedFees(mempoolFees); + + const rbfTransactionSummary = await rbf.getRbfTransactionSummary(btcClient, transaction.txid); + + setRbfData({ + rbfTransaction: rbfTx, + rbfTxSummary: rbfTransactionSummary, + rbfRecommendedFees: sortFees(rbfRecommendedFeesResponse), + mempoolFees, + }); + } catch (err: any) { + toast.error(t('SOMETHING_WENT_WRONG')); + navigate(-1); + console.error(err); + } finally { + setIsLoading(false); + } + }, [ + selectedAccount, + transaction, + accountType, + network.type, + seedVault, + btcClient, + fetchStxData, + t, + navigate, + ]); + + useEffect(() => { + fetchRbfData(); + }, [fetchRbfData]); + + return { ...rbfData, isLoading }; +}; + +export default useRbfTransactionData; diff --git a/src/app/hooks/useWalletReducer.ts b/src/app/hooks/useWalletReducer.ts index 883a20ea5..a0a1ad269 100644 --- a/src/app/hooks/useWalletReducer.ts +++ b/src/app/hooks/useWalletReducer.ts @@ -5,20 +5,20 @@ import useNetworkSelector from '@hooks/useNetwork'; import { Account, AnalyticsEvents, + SettingsNetwork, + StacksMainnet, + StacksNetwork, + StacksTestnet, createWalletAccount, decryptSeedPhraseCBC, getBnsName, newWallet, restoreWalletWithAccounts, - SettingsNetwork, - StacksMainnet, - StacksNetwork, - StacksTestnet, walletFromSeedPhrase, } from '@secretkeylabs/xverse-core'; import { - addAccountAction, ChangeNetworkAction, + addAccountAction, fetchAccountAction, getActiveAccountsAction, resetWalletAction, diff --git a/src/app/screens/ledger/importLedgerAccount/index.tsx b/src/app/screens/ledger/importLedgerAccount/index.tsx index b1841143b..ff928558a 100644 --- a/src/app/screens/ledger/importLedgerAccount/index.tsx +++ b/src/app/screens/ledger/importLedgerAccount/index.tsx @@ -6,11 +6,11 @@ import Transport from '@ledgerhq/hw-transport-webusb'; import { useTransition } from '@react-spring/web'; import { Account, + LedgerErrors, getMasterFingerPrint, importNativeSegwitAccountFromLedger, importStacksAccountFromLedger, importTaprootAccountFromLedger, - LedgerErrors, } from '@secretkeylabs/xverse-core'; import { DEFAULT_TRANSITION_OPTIONS } from '@utils/constants'; import { useEffect, useState } from 'react'; diff --git a/src/app/screens/speedUpTransaction/customFee/index.styled.ts b/src/app/screens/speedUpTransaction/customFee/index.styled.ts index 1bcfcf066..f822a83a1 100644 --- a/src/app/screens/speedUpTransaction/customFee/index.styled.ts +++ b/src/app/screens/speedUpTransaction/customFee/index.styled.ts @@ -45,7 +45,7 @@ export const InputField = styled.input((props) => ({ backgroundColor: 'transparent', color: props.theme.colors.white_200, border: 'transparent', - width: '80%', + width: '70%', '&::-webkit-outer-spin-button': { '-webkit-appearance': 'none', margin: 0, diff --git a/src/app/screens/speedUpTransaction/customFee/index.tsx b/src/app/screens/speedUpTransaction/customFee/index.tsx index 0435509fd..b0c88a61d 100644 --- a/src/app/screens/speedUpTransaction/customFee/index.tsx +++ b/src/app/screens/speedUpTransaction/customFee/index.tsx @@ -1,6 +1,10 @@ import BottomModal from '@components/bottomModal'; import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; +import { + getBtcFiatEquivalent, + getStxFiatEquivalent, + stxToMicrostacks, +} from '@secretkeylabs/xverse-core'; import { handleKeyDownFeeRateInput } from '@utils/helper'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; @@ -32,6 +36,7 @@ export default function CustomFee({ minimumFeeRate, isFeeLoading, error, + isBtc, }: { visible: boolean; onClose: () => void; @@ -43,11 +48,12 @@ export default function CustomFee({ initialTotalFee: string; isFeeLoading: boolean; error: string; + isBtc: boolean; }) { const { t } = useTranslation('translation', { keyPrefix: 'TRANSACTION_SETTING', }); - const { btcFiatRate, fiatCurrency } = useWalletSelector(); + const { btcFiatRate, stxBtcRate, fiatCurrency } = useWalletSelector(); const [feeRateInput, setFeeRateInput] = useState(feeRate || minimumFeeRate); const [totalFee, setTotalFee] = useState(fee || initialTotalFee); @@ -72,9 +78,17 @@ export default function CustomFee({ onClickApply(feeRateInput, totalFee); }; - const fiatFee = totalFee - ? getBtcFiatEquivalent(BigNumber(totalFee), BigNumber(btcFiatRate)) - : BigNumber(0); + let fiatFee = BigNumber(0); + + if (totalFee) { + fiatFee = isBtc + ? getBtcFiatEquivalent(BigNumber(totalFee), BigNumber(btcFiatRate)) + : getStxFiatEquivalent( + stxToMicrostacks(BigNumber(totalFee)), + BigNumber(stxBtcRate), + BigNumber(btcFiatRate), + ); + } return ( @@ -84,15 +98,21 @@ export default function CustomFee({ - Sats /vB + + {isBtc ? ( + 'Sats /vB' + ) : ( + + )} + {error && } - {!error && minimumFeeRate && Number(feeRateInput) >= Number(minimumFeeRate) && ( + {!error && isBtc && minimumFeeRate && Number(feeRateInput) >= Number(minimumFeeRate) && ( <> {t('TOTAL_FEE')} diff --git a/src/app/screens/speedUpTransaction/index.styled.ts b/src/app/screens/speedUpTransaction/index.styled.ts index 7da1f58ee..85f76dbb6 100644 --- a/src/app/screens/speedUpTransaction/index.styled.ts +++ b/src/app/screens/speedUpTransaction/index.styled.ts @@ -1,14 +1,5 @@ -import ActionButton from '@components/button'; -import { Faders } from '@phosphor-icons/react'; import styled from 'styled-components'; -export const Title = styled.h1((props) => ({ - ...props.theme.typography.headline_s, - color: props.theme.colors.white_0, - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - export const LoaderContainer = styled.div({ display: 'flex', justifyContent: 'center', @@ -16,116 +7,13 @@ export const LoaderContainer = styled.div({ height: 'inherit', }); -export const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - ...props.theme.scrollbar, -})); - -export const DetailText = styled.span((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, - marginBottom: props.theme.spacing(4), -})); - -export const HighlightedText = styled.span((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_0, -})); - -export const ButtonContainer = styled.div` - display: flex; - flex-direction: column; - margin-top: ${(props) => props.theme.spacing(6)}px; - gap: ${(props) => props.theme.spacing(4)}px; -`; - -export const FeeButton = styled.button<{ - isSelected: boolean; - centered?: boolean; -}>((props) => ({ - ...props.theme.body_medium_m, - textAlign: 'left', - color: props.theme.colors.white_0, - backgroundColor: `${props.isSelected ? props.theme.colors.elevation6_600 : 'transparent'}`, - border: `1px solid ${ - props.isSelected ? props.theme.colors.white_800 : props.theme.colors.white_850 - }`, - borderRadius: props.theme.radius(2), - height: 'auto', - display: 'flex', - justifyContent: 'space-between', - alignItems: props.centered ? 'center' : 'flex-start', - transition: 'background-color 0.1s ease-in-out, border 0.1s ease-in-out', - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(6), - paddingBottom: props.theme.spacing(6), - ':not(:disabled):hover': { - borderColor: props.theme.colors.white_800, - }, - ':disabled': { - cursor: 'not-allowed', - color: props.theme.colors.white_400, - div: { - color: 'inherit', - }, - svg: { - fill: props.theme.colors.white_600, - }, - }, -})); - -export const ControlsContainer = styled.div` - display: flex; - column-gap: 12px; - margin: 38px 0px 40px; -`; - -export const CustomFeeIcon = styled(Faders)({ - transform: 'rotate(90deg)', -}); - -export const FeeButtonLeft = styled.div((props) => ({ - display: 'flex', - alignItems: 'center', - gap: props.theme.spacing(6), -})); - -export const FeeButtonRight = styled.div({ - textAlign: 'right', -}); - -export const SecondaryText = styled.div<{ - alignRight?: boolean; -}>((props) => ({ - ...props.theme.typography.body_medium_s, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(2), - textAlign: props.alignRight ? 'right' : 'left', -})); - -export const StyledActionButton = styled(ActionButton)((props) => ({ - 'div, h1': { - ...props.theme.typography.body_medium_m, - }, -})); - -export const WarningText = styled.span((props) => ({ - ...props.theme.typography.body_medium_s, - display: 'block', - color: props.theme.colors.danger_light, - marginTop: props.theme.spacing(2), -})); - export const SuccessActionsContainer = styled.div((props) => ({ width: '100%', display: 'flex', flexDirection: 'column', - gap: props.theme.spacing(6), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(20), - marginTop: props.theme.spacing(20), + 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, })); diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index 4d31631cc..a618754fb 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -1,89 +1,61 @@ import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; +import ledgerConnectStxIcon from '@assets/img/ledger/ledger_import_connect_stx.svg'; import { delay } from '@common/utils/ledger'; import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; -import FiatAmountText from '@components/fiatAmountText'; import LedgerConnectionView from '@components/ledger/connectLedgerView'; +import SpeedUpBtcTransaction from '@components/speedUpTransaction/btc'; +import SpeedUpStxTransaction from '@components/speedUpTransaction/stx'; import TopRow from '@components/topRow'; import useTransaction from '@hooks/queries/useTransaction'; import useBtcClient from '@hooks/useBtcClient'; +import useNetworkSelector from '@hooks/useNetwork'; +import useRbfTransactionData, { + getLatestNonce, + getRawTransaction, + isBtcTransaction, +} from '@hooks/useRbfTransactionData'; import useSeedVault from '@hooks/useSeedVault'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; import { CarProfile, Lightning, RocketLaunch, ShootingStar } from '@phosphor-icons/react'; import { - RecommendedFeeResponse, + StacksTransaction, Transport as TransportType, - getBtcFiatEquivalent, - mempoolApi, - rbf, + broadcastSignedTransaction, + signLedgerStxTransaction, + signTransaction, + stxToMicrostacks, } from '@secretkeylabs/xverse-core'; +import { deserializeTransaction } from '@stacks/transactions'; import { EMPTY_LABEL } from '@utils/constants'; import { isLedgerAccount } from '@utils/helper'; import BigNumber from 'bignumber.js'; -import { useCallback, useEffect, useState } from 'react'; +import { useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { MoonLoader } from 'react-spinners'; import { useTheme } from 'styled-components'; import CustomFee from './customFee'; -import { - ButtonContainer, - Container, - ControlsContainer, - CustomFeeIcon, - DetailText, - FeeButton, - FeeButtonLeft, - FeeButtonRight, - HighlightedText, - LoaderContainer, - SecondaryText, - StyledActionButton, - SuccessActionsContainer, - Title, - WarningText, -} from './index.styled'; - -type TierFees = { - enoughFunds: boolean; - fee?: number; - feeRate: number; -}; - -type RbfRecommendedFees = { - medium?: TierFees; - high?: TierFees; - higher?: TierFees; - highest?: TierFees; -}; +import { LoaderContainer, SuccessActionsContainer } from './index.styled'; function SpeedUpTransactionScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SPEED_UP_TRANSACTION' }); const theme = useTheme(); const navigate = useNavigate(); const [showCustomFee, setShowCustomFee] = useState(false); - const { selectedAccount, accountType, network, btcFiatRate, fiatCurrency } = useWalletSelector(); - const seedVault = useSeedVault(); + const { selectedAccount, stxAddress, network, stxAvailableBalance } = useWalletSelector(); const { id } = useParams(); + const location = useLocation(); const btcClient = useBtcClient(); - const [rbfTxSummary, setRbfTxSummary] = useState<{ - currentFee: number; - currentFeeRate: number; - minimumRbfFee: number; - minimumRbfFeeRate: number; - }>(); const [feeRateInput, setFeeRateInput] = useState(); const [selectedOption, setSelectedOption] = useState(); - const [recommendedFees, setRecommendedFees] = useState(); - const [rbfRecommendedFees, setRbfRecommendedFees] = useState(); - const { data: transaction } = useTransaction(id!); - const [rbfTransaction, setRbfTransaction] = useState< - InstanceType | undefined - >(); + const { transaction: stxTransaction } = location.state || {}; + const { data: btcTransaction } = useTransaction(stxTransaction ? undefined : id); + const { isLoading, rbfTransaction, rbfRecommendedFees, rbfTxSummary, mempoolFees } = + useRbfTransactionData(stxTransaction || btcTransaction); const { t: signatureRequestTranslate } = useTranslation('translation', { keyPrefix: 'SIGNATURE_REQUEST', }); @@ -93,68 +65,13 @@ function SpeedUpTransactionScreen() { const [isConnectSuccess, setIsConnectSuccess] = useState(false); const [isConnectFailed, setIsConnectFailed] = useState(false); const [isTxRejected, setIsTxRejected] = useState(false); - const [isLoading, setIsLoading] = useState(true); const [customFeeRate, setCustomFeeRate] = useState(); const [customTotalFee, setCustomTotalFee] = useState(); const [customFeeError, setCustomFeeError] = useState(); - - /* TODO: Move `fetchRbfData` function logic to a separate hook like - const useRbfTransactionData: (transaction: ReturnType) => { - rbfTransaction?: rbf.RbfTransaction, - rbfTxSummary?: { - currentFee: number; - currentFeeRate: number; - minimumRbfFee: number; - minimumRbfFeeRate: number; - }, - rbfRecommendedFees?: RbfRecommendedFees - } => { // logic here } - */ - const fetchRbfData = useCallback(async () => { - if (!selectedAccount || !id || !transaction) { - return; - } - - try { - setIsLoading(true); - const rbfTx = new rbf.RbfTransaction(transaction, { - ...selectedAccount, - accountType: accountType || 'software', - accountId: - isLedgerAccount(selectedAccount) && typeof selectedAccount.deviceAccountIndex === 'number' - ? selectedAccount.deviceAccountIndex - : selectedAccount.id, - network: network.type, - esploraProvider: btcClient, - getSeedPhrase: seedVault.getSeed, - }); - setRbfTransaction(rbfTx); - - const rbfTransactionSummary = await rbf.getRbfTransactionSummary(btcClient, transaction.txid); - setRbfTxSummary(rbfTransactionSummary); - - const mempoolFees = await mempoolApi.getRecommendedFees(network.type); - setRecommendedFees(mempoolFees); - - const rbfRecommendedFeesResponse = await rbfTx.getRbfRecommendedFees(mempoolFees); - setRbfRecommendedFees( - Object.fromEntries( - Object.entries(rbfRecommendedFeesResponse).sort((a, b) => { - const priorityOrder = ['highest', 'higher', 'high', 'medium']; - return priorityOrder.indexOf(a[0]) - priorityOrder.indexOf(b[0]); - }), - ), - ); - } catch (err: any) { - console.error(err); - } finally { - setIsLoading(false); - } - }, [selectedAccount, id, transaction, accountType, network.type, seedVault, btcClient]); - - useEffect(() => { - fetchRbfData(); - }, [fetchRbfData]); + const { getSeed } = useSeedVault(); + const selectedStacksNetwork = useNetworkSelector(); + const isBtc = isBtcTransaction(stxTransaction || btcTransaction); + const [isBroadcasting, setIsBroadcasting] = useState(false); const handleClickFeeButton = (e: React.MouseEvent) => { if (e.currentTarget.value === 'custom') { @@ -178,7 +95,26 @@ function SpeedUpTransactionScreen() { navigate('/'); }; + const calculateStxTotalFee = async (feeRate: string) => { + if (rbfTxSummary && Number(feeRate) < rbfTxSummary.minimumRbfFeeRate) { + setCustomFeeError(t('FEE_TOO_LOW', { minimumFee: rbfTxSummary.minimumRbfFeeRate })); + return; + } + + if (stxToMicrostacks(BigNumber(feeRate)).gt(BigNumber(stxAvailableBalance))) { + setCustomFeeError(t('INSUFFICIENT_FUNDS')); + } else { + setCustomFeeError(undefined); + } + + return Number(feeRate); + }; + const calculateTotalFee = async (feeRate: string) => { + if (!isBtc) { + return calculateStxTotalFee(feeRate); + } + if (!rbfTransaction) { return; } @@ -203,7 +139,66 @@ function SpeedUpTransactionScreen() { return feeSummary.fee; }; + const signAndBroadcastStxTx = async (transport?: TransportType) => { + if (!feeRateInput || !selectedAccount) { + return; + } + + try { + setIsBroadcasting(true); + const fee = stxToMicrostacks(BigNumber(feeRateInput)).toString(); + const txRaw: string = await getRawTransaction(stxTransaction.txid, network); + const unsignedTx: StacksTransaction = deserializeTransaction(txRaw); + + // check if the transaction exists in microblock + const latestNonceData = await getLatestNonce(stxAddress, network); + if (stxTransaction.nonce > latestNonceData.last_executed_tx_nonce) { + unsignedTx.setFee(BigInt(fee)); + unsignedTx.setNonce(BigInt(stxTransaction.nonce)); + + const seedPhrase = await getSeed(); + + if (isLedgerAccount(selectedAccount)) { + if (!transport || selectedAccount.deviceAccountIndex === undefined) { + return; + } + + const result = await signLedgerStxTransaction({ + transport, + transactionBuffer: Buffer.from(unsignedTx.serialize()), + addressIndex: selectedAccount.deviceAccountIndex, + }); + await delay(1500); + await broadcastSignedTransaction(result, selectedStacksNetwork); + } else { + const signedTx: StacksTransaction = await signTransaction( + unsignedTx, + seedPhrase, + selectedAccount.id, + selectedStacksNetwork, + ); + await broadcastSignedTransaction(signedTx, selectedStacksNetwork); + } + + toast.success(t('TX_FEE_UPDATED')); + handleGoBack(); + return; + } + + toast.error('This transaction has already been confirmed in a microblock.'); + return; + } catch (err: any) { + console.error(err); + } finally { + setIsBroadcasting(false); + } + }; + const signAndBroadcastTx = async (transport?: TransportType) => { + if (!isBtc) { + return signAndBroadcastStxTx(transport); + } + if (!rbfTransaction) { return; } @@ -213,6 +208,7 @@ function SpeedUpTransactionScreen() { } try { + setIsBroadcasting(true); const signedTx = await rbfTransaction.getReplacementTransaction({ feeRate: Number(feeRateInput), ledgerTransport: transport, @@ -230,11 +226,13 @@ function SpeedUpTransactionScreen() { toast.error(t('INSUFFICIENT_FEE')); } } + } finally { + setIsBroadcasting(false); } }; const handleClickSubmit = async () => { - if (!selectedAccount || !id) { + if (!selectedAccount || (!btcTransaction && !stxTransaction)) { return; } @@ -312,19 +310,19 @@ function SpeedUpTransactionScreen() { }; const getEstimatedCompletionTime = (feeRate?: number) => { - if (!feeRate || !recommendedFees) { + if (!feeRate || !mempoolFees) { return EMPTY_LABEL; } - if (feeRate < recommendedFees.hourFee) { + if (feeRate < mempoolFees.hourFee) { return t('TIME.SEVERAL_HOURS_OR_MORE'); } - if (feeRate === recommendedFees.hourFee) { + if (feeRate === mempoolFees.hourFee) { return `~1 ${t('TIME.HOUR')}`; } - if (feeRate > recommendedFees.hourFee && feeRate <= recommendedFees.halfHourFee) { + if (feeRate > mempoolFees.hourFee && feeRate <= mempoolFees.halfHourFee) { return `~30 ${t('TIME.MINUTES')}`; } @@ -364,155 +362,36 @@ function SpeedUpTransactionScreen() { ) : ( <> - - {t('TITLE')} - {t('FEE_INFO')} - - {t('CURRENT_FEE')}{' '} - - - - - - - {t('ESTIMATED_COMPLETION_TIME')}{' '} - - {getEstimatedCompletionTime(rbfTxSummary?.currentFeeRate)} - - - - {rbfRecommendedFees && - Object.entries(rbfRecommendedFees).map(([key, obj]) => { - const isDisabled = !obj.enoughFunds; - - return ( - - - {feeButtonMapping[key].icon} -
- {feeButtonMapping[key].title} - {getEstimatedCompletionTime(obj.feeRate)} - - - -
-
- -
- {obj.fee ? ( - - ) : ( - EMPTY_LABEL - )} -
- - {obj.fee ? ( - - ) : ( - `${EMPTY_LABEL} ${fiatCurrency}` - )} - - {isDisabled && {t('INSUFFICIENT_FUNDS')}} -
-
- ); - })} - - - -
- {t('CUSTOM')} - {customFeeRate && ( - <> - - {getEstimatedCompletionTime(Number(customFeeRate))} - - - - - - )} -
-
- {customFeeRate && customTotalFee ? ( -
-
- -
- - - -
- ) : ( -
{t('MANUAL_SETTING')}
- )} -
-
- - - - -
+ {isBtc ? ( + + ) : ( + + )} - {/* TODO: Move this modal and the custom option info above to a separate component */} {rbfTxSummary && showCustomFee && ( )} @@ -532,10 +412,12 @@ function SpeedUpTransactionScreen() { {currentStepIndex === 0 && ( diff --git a/src/assets/img/transactions/increaseFee.svg b/src/assets/img/transactions/increaseFee.svg deleted file mode 100644 index 6437b801f..000000000 --- a/src/assets/img/transactions/increaseFee.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/locales/en.json b/src/locales/en.json index 4f87ed20b..e5bfdf656 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -805,7 +805,6 @@ "FT_CONTRACT_PREFIX": "Token contract", "OPEN_FT_CONTRACT_DEPLOYMENT": "View the contract on", "STACKS_EXPLORER": "Stacks Explorer", - "INCREASE_FEE_BUTTON": "Increase fee", "BALANCE": "Balance", "TRANSACTIONS": "TRANSACTIONS", "CONTRACT": "CONTRACT", @@ -831,6 +830,7 @@ "MANUAL_SETTING": "Manual setting", "TX_FEE_UPDATED": "Transaction fee updated", "FEE_TOO_LOW": "The minimum fee is {{minimumFee}}", + "SOMETHING_WENT_WRONG": "Something went wrong", "TIME": { "SEVERAL_HOURS_OR_MORE": "several hours or more", "HOUR": "hour", From 57a50fe6b404041ec378671ffb0450bfd9b7556d Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Fri, 12 Jan 2024 04:58:02 +0100 Subject: [PATCH 16/20] [ENG-3564] fix: Speed up transaction ui issue displaying and loading (#749) --- src/app/hooks/useRbfTransactionData.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/hooks/useRbfTransactionData.ts b/src/app/hooks/useRbfTransactionData.ts index 01d95782b..8df8b543f 100644 --- a/src/app/hooks/useRbfTransactionData.ts +++ b/src/app/hooks/useRbfTransactionData.ts @@ -208,7 +208,6 @@ const useRbfTransactionData = (transaction?: BtcTransactionData | StxTransaction const fetchRbfData = useCallback(async () => { if (!selectedAccount || !transaction) { - setIsLoading(false); return; } From 8f0e3acb9790d5aca015d554a502db50aabc53de Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 12 Jan 2024 14:45:38 +0800 Subject: [PATCH 17/20] fix: skip server validation of api urls if value didn't change (#752) --- .../screens/settings/changeNetwork/index.tsx | 18 +++++++++++++++--- .../settings/changeNetwork/nodeInput.tsx | 8 +++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/screens/settings/changeNetwork/index.tsx b/src/app/screens/settings/changeNetwork/index.tsx index 98485d348..782d3b6a0 100644 --- a/src/app/screens/settings/changeNetwork/index.tsx +++ b/src/app/screens/settings/changeNetwork/index.tsx @@ -70,6 +70,9 @@ function ChangeNetworkScreen() { }; const onNetworkSelected = (networkSelected: SettingsNetwork) => { + if (networkSelected.type === formInputs.type) { + return; + } setFormInputs(networkSelected); setFormErrors(initialNodeErrors); }; @@ -111,6 +114,8 @@ function ChangeNetworkScreen() { const onSubmit = async () => { setIsChangingNetwork(true); + // TODO use formik/yup for all validation if form gets more complex + // validate required fields if (!formInputs.address) { setFormErrors((prevErrors) => ({ ...prevErrors, @@ -129,10 +134,17 @@ function ChangeNetworkScreen() { return; } + const isChangedStacksUrl = formInputs.address !== network.address; + const isChangedBtcApiUrl = formInputs.btcApiUrl !== network.btcApiUrl; + const isChangedFallbackBtcApiUrl = formInputs.fallbackBtcApiUrl !== network.fallbackBtcApiUrl; + + // validate against server if inputs were changed const [isValidStacksUrl, isValidBtcApiUrl, isValidFallbackBtcApiUrl] = await Promise.all([ - isValidStacksApi(formInputs.address, formInputs.type), - isValidBtcApi(formInputs.btcApiUrl, formInputs.type), - !formInputs.fallbackBtcApiUrl || isValidBtcApi(formInputs.fallbackBtcApiUrl, formInputs.type), + !isChangedStacksUrl || isValidStacksApi(formInputs.address, formInputs.type), + !isChangedBtcApiUrl || isValidBtcApi(formInputs.btcApiUrl, formInputs.type), + !formInputs.fallbackBtcApiUrl || + !isChangedFallbackBtcApiUrl || + isValidBtcApi(formInputs.fallbackBtcApiUrl, formInputs.type), ]); if (isValidStacksUrl && isValidBtcApiUrl && isValidFallbackBtcApiUrl) { diff --git a/src/app/screens/settings/changeNetwork/nodeInput.tsx b/src/app/screens/settings/changeNetwork/nodeInput.tsx index ce1a7b32b..f23992727 100644 --- a/src/app/screens/settings/changeNetwork/nodeInput.tsx +++ b/src/app/screens/settings/changeNetwork/nodeInput.tsx @@ -78,9 +78,11 @@ function NodeInput({ - + {value && ( + + )} From cd7c7984262e538b92f2c0907ef705a15b51bd61 Mon Sep 17 00:00:00 2001 From: Tim Man Date: Fri, 12 Jan 2024 14:46:08 +0800 Subject: [PATCH 18/20] [END 3565] fix: catch and display failed to broadcast error (#751) * fix: update stx speed up error messages * fix: error toast styling and shorten the speed up stx failure message --- src/app/App.tsx | 34 +++++++++++++++++--- src/app/screens/speedUpTransaction/index.tsx | 3 +- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index c11c784c1..95a65b586 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,5 @@ import LoadingScreen from '@components/loadingScreen'; -import { CheckCircle } from '@phosphor-icons/react'; +import { CheckCircle, XCircle } from '@phosphor-icons/react'; import rootStore from '@stores/index'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; @@ -11,13 +11,20 @@ import { Toaster } from 'react-hot-toast'; import { Provider } from 'react-redux'; import { RouterProvider } from 'react-router-dom'; import { PersistGate } from 'redux-persist/integration/react'; -import { ThemeProvider } from 'styled-components'; +import styled, { ThemeProvider } from 'styled-components'; import '../locales'; import Theme from '../theme'; import GlobalStyle from '../theme/global'; import SessionGuard from './components/guards/session'; import router from './routes'; +// needed to keep the svg icon scale for toasts over multiple lines +const StyledIcon = styled.div` + display: flex; + align-items: center; + justify-content: center; +`; + function App(): JSX.Element { useEffect(() => { if (!MIX_PANEL_TOKEN) { @@ -46,16 +53,33 @@ function App(): JSX.Element { containerStyle={{ bottom: 80 }} toastOptions={{ success: { - icon: , + icon: ( + + + + ), style: { ...Theme.typography.body_medium_m, backgroundColor: Theme.colors.success_medium, borderRadius: Theme.radius(2), - padding: Theme.spacing(4), - paddingLeft: Theme.spacing(6), + 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, + }, + }, }} /> diff --git a/src/app/screens/speedUpTransaction/index.tsx b/src/app/screens/speedUpTransaction/index.tsx index a618754fb..d1842d925 100644 --- a/src/app/screens/speedUpTransaction/index.tsx +++ b/src/app/screens/speedUpTransaction/index.tsx @@ -185,10 +185,11 @@ function SpeedUpTransactionScreen() { return; } - toast.error('This transaction has already been confirmed in a microblock.'); + toast.error('This transaction has already been confirmed in a block.'); return; } catch (err: any) { console.error(err); + toast.error('Failed to broadcast transaction.'); } finally { setIsBroadcasting(false); } From 497b3b774a498e5f79770a1c25355b41aa629944 Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:07:52 +0100 Subject: [PATCH 19/20] [ENG-3563] fix: BTC fee infinite loader when sending ordinals (#748) * [ENG-3563] fix: BTC fee infinite loader when sending ordinals * fix: handle btc fee error --------- Co-authored-by: Abdul Haseeb --- .../transactionSetting/editBtcFee.tsx | 6 +- .../components/transactionSetting/feeItem.tsx | 55 +++++++++++++++---- src/app/hooks/useBtcFees.ts | 21 ++++--- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/app/components/transactionSetting/editBtcFee.tsx b/src/app/components/transactionSetting/editBtcFee.tsx index 5489f56b6..77509d524 100644 --- a/src/app/components/transactionSetting/editBtcFee.tsx +++ b/src/app/components/transactionSetting/editBtcFee.tsx @@ -55,7 +55,7 @@ const InputContainer = styled.div((props) => ({ })); const InputField = styled.input((props) => ({ - ...props.theme.body_m, + ...props.theme.typography.body_m, backgroundColor: 'transparent', color: props.theme.colors.white_0, border: 'transparent', @@ -187,7 +187,7 @@ function EditBtcFee({ const { ordinals } = useOrdinalsByAddress(btcAddress); const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); const btcClient = useBtcClient(); - const feeData = useBtcFees({ + const { feeData, highFeeError, mediumFeeError } = useBtcFees({ isRestoreFlow: !!isRestoreFlow, nonOrdinalUtxos, btcRecipients, @@ -341,6 +341,7 @@ function EditBtcFee({ setFeeMode('high'); }} selected={feeMode === 'high'} + error={highFeeError} /> props.theme.colors.white_200}; margin-bottom: ${(props) => props.theme.space.xxs}; `; @@ -75,9 +75,19 @@ interface FeeItemProps { fiat: string | JSX.Element; selected: boolean; onClick?: () => void; + error?: string; } -function FeeItem({ priority, time, feeRate, totalFee, fiat, selected, onClick }: FeeItemProps) { +function FeeItem({ + priority, + time, + feeRate, + totalFee, + fiat, + selected, + error, + onClick, +}: FeeItemProps) { const { t } = useTranslation('translation'); const getIcon = () => { switch (priority) { @@ -105,25 +115,50 @@ function FeeItem({ priority, time, feeRate, totalFee, fiat, selected, onClick }: } }; + 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/ vByte`} + + {time} + + {`${feeRate} Sats/ vByte`} - {totalFee ? ( - + + + {totalFee && ( {`${totalFee} Sats`} - {fiat} - - ) : ( + )} + + {fiat} + + {error && ( + + {getErrorMessage(error)} + + )} + + + {!totalFee && !error && ( diff --git a/src/app/hooks/useBtcFees.ts b/src/app/hooks/useBtcFees.ts index 5a118bf24..976b26308 100644 --- a/src/app/hooks/useBtcFees.ts +++ b/src/app/hooks/useBtcFees.ts @@ -31,14 +31,16 @@ const useBtcFees = ({ btcRecipients, type, ordinalTxUtxo, -}: Params): FeeData => { +}: Params): { feeData: FeeData; highFeeError?: string; mediumFeeError?: string } => { const [feeData, setFeeData] = useState({ standardFeeRate: '', standardTotalFee: '', highFeeRate: '', highTotalFee: '', }); - const { network, btcAddress, selectedAccount, ordinalsAddress } = useWalletSelector(); + const [highFeeError, setHighFeeError] = useState(''); + const [standardFeeError, setStandardFeeError] = useState(''); + const { network, btcAddress, ordinalsAddress } = useWalletSelector(); const btcClient = useBtcClient(); const { ordinals } = useOrdinalsByAddress(btcAddress); const ordinalsUtxos = useMemo(() => ordinals?.map((ord) => ord.utxo), [ordinals]); @@ -46,16 +48,18 @@ const useBtcFees = ({ useEffect(() => { async function fetchFees(mode: 'standard' | 'high') { try { + setStandardFeeError(''); + setHighFeeError(''); let feeInfo; if (isRestoreFlow) { feeInfo = await getBtcFeesForNonOrdinalBtcSend( btcAddress, - nonOrdinalUtxos!, + nonOrdinalUtxos || [], ordinalsAddress, network.type, mode, ); - } else if (btcRecipients && selectedAccount) { + } else if (type === 'BTC' && btcRecipients) { feeInfo = await getBtcFees(btcRecipients, btcAddress, btcClient, network.type, mode); } else if (type === 'Ordinals' && btcRecipients && ordinalTxUtxo) { feeInfo = await getBtcFeesForOrdinalSend( @@ -68,12 +72,13 @@ const useBtcFees = ({ mode, ); } - return { fee: feeInfo?.fee.toString() || '', feeRate: feeInfo?.selectedFeeRate.toString() || '', }; - } catch (error) { + } catch (error: any) { + if (mode === 'standard') setStandardFeeError(error.toString()); + else if (mode === 'high') setHighFeeError(error.toString()); return { fee: '', feeRate: '' }; } } @@ -102,10 +107,8 @@ const useBtcFees = ({ network, ordinalsUtxos, ordinalsAddress, - selectedAccount, ]); - - return feeData; + return { feeData, highFeeError, mediumFeeError: standardFeeError }; }; export default useBtcFees; From f0458140624952578c20ed42c46f261f21b25fb3 Mon Sep 17 00:00:00 2001 From: fede erbes Date: Fri, 12 Jan 2024 16:55:16 +0100 Subject: [PATCH 20/20] chore: remove mock data for psbt ui (#753) --- src/app/screens/signPsbtRequest/index.tsx | 20 +-- .../signPsbtRequest/tempMockDataUtil.ts | 143 ------------------ 2 files changed, 3 insertions(+), 160 deletions(-) delete mode 100644 src/app/screens/signPsbtRequest/tempMockDataUtil.ts diff --git a/src/app/screens/signPsbtRequest/index.tsx b/src/app/screens/signPsbtRequest/index.tsx index 3e662ce82..fccafc694 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -6,7 +6,6 @@ import { btcTransaction, Transport } from '@secretkeylabs/xverse-core'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import getPsbtDataWithMocks from './tempMockDataUtil'; import useSignPsbtValidationGate from './useSignPsbtValidationGate'; function SignPsbtRequest() { @@ -39,22 +38,9 @@ function SignPsbtRequest() { .getSummary() .then((summary) => { const { feeOutput: psbtFeeOutput, inputs: psbtInputs, outputs: psbtOutputs } = summary; - // TODO: remove this section, this is only for testing - const { inputsWithMocks, outputsWithMocks, feeOutputWithMocks } = getPsbtDataWithMocks( - btcAddress, - ordinalsAddress, - psbtInputs, - psbtOutputs, - !psbtFeeOutput, - psbtFeeOutput, - ); - setFeeOutput(feeOutputWithMocks); - setInputs(inputsWithMocks); - setOutputs(outputsWithMocks); - - // setFeeOutput(psbtFeeOutput); - // setInputs(psbtInputs); - // setOutputs(psbtOutputs); + setFeeOutput(psbtFeeOutput); + setInputs(psbtInputs); + setOutputs(psbtOutputs); setIsLoading(false); }) .catch((error) => { diff --git a/src/app/screens/signPsbtRequest/tempMockDataUtil.ts b/src/app/screens/signPsbtRequest/tempMockDataUtil.ts deleted file mode 100644 index b2af92428..000000000 --- a/src/app/screens/signPsbtRequest/tempMockDataUtil.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { btcTransaction } from '@secretkeylabs/xverse-core'; - -// TODO: remove after testing -const getPsbtDataWithMocks = ( - btcAddress: string, - ordinalsAddress: string, - inputs: btcTransaction.EnhancedInput[], - outputs: btcTransaction.EnhancedOutput[], - isPartialTransaction: boolean, - feeOutput?: btcTransaction.TransactionFeeOutput, -) => { - const outputsWithMocks = [...outputs]; - const inputsWithMocks = [...inputs]; - const feeOutputWithMocks = feeOutput ? { ...feeOutput } : undefined; - - if (localStorage.getItem('assetsInPayment') === 'true') { - // TODO: mock data for items spend in payment address - if (isPartialTransaction) { - inputsWithMocks.push({ - // @ts-ignore - extendedUtxo: { - address: btcAddress, - outpoint: '9851e0a32f6fd352dd763624025cb55cead8954c7bdde4430c290f7f9e3bcfeb:0', - // @ts-ignore - utxo: { - value: 100000, - status: { - confirmed: false, // to test the unconfirmed utxo warning callout - }, - }, - }, - inscriptions: [ - { - contentType: 'image/png', - fromAddress: btcAddress, - id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', - number: 10951686, - offset: 0, - }, - ], - satributes: [], - sigHash: 131, - }); - } - outputsWithMocks.push( - { - address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', - amount: 10000, - inscriptions: [ - { - contentType: 'image/png', - fromAddress: btcAddress, - id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', - number: 10951686, - offset: 0, - }, - ], - satributes: [], - }, - { - address: 'bc1p6rh39e6s6utyc8adtlt3q09d9tnrwlwynngdwj9jse2uysekynxscnwfh7', - amount: 546, - inscriptions: [], - satributes: [ - { - amount: 1, - fromAddress: btcAddress, - offset: 0, - types: ['PALINDROME'], - }, - ], - }, - ); - } - - if (localStorage.getItem('bundleInOrdinal') === 'true') { - // TODO: mock data for bundle item in ordinal address - outputsWithMocks.push({ - address: ordinalsAddress, - amount: 20, - inscriptions: [ - { - contentType: 'image/png', - fromAddress: 'bc1prnplwl27eedudpvl9cjhd2pysudk0gzze08wjhsyy0998mfcgmcsaz4nse', - id: '9f0a1ea3ca2e2431242350b63cf53708f0f3e560638eb26b1255d4e5dd766fc4i0', - number: 10987226, - offset: 0, - }, - { - contentType: 'image/png', - fromAddress: 'bc1pmz88ylp258alrgeqsy7jn99u20ylkc4fuqcgwva3eef8s92ye9squunk5r', - id: '2237248523bc923a7844b47cb7e2552c1666032ed54ab153a00fba1f5c3e1e22i0', - number: 10878824, - offset: 2, - }, - ], - satributes: [ - { - amount: 1, - fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', - offset: 3, - types: ['FIRST_TRANSACTION', 'VINTAGE', 'BLOCK9', 'NAKAMOTO'], - }, - { - amount: 1, - fromAddress: 'bc1pugy3kp2zeuntlw649vse3eyy9zr6rwd2lfchdasx9pa7nvm2555qfeepyt', - offset: 0, - types: ['PIZZA'], - }, - ], - }); - } - - if (localStorage.getItem('assetsInFees') === 'true' && feeOutputWithMocks) { - feeOutputWithMocks.satributes = [ - ...feeOutputWithMocks.satributes, - { - amount: 1, - fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', - offset: 0, - types: ['PALINDROME'], - }, - ]; - feeOutputWithMocks.inscriptions = [ - ...feeOutputWithMocks.inscriptions, - { - contentType: 'image/png', - fromAddress: '38NMchWMVXBokHicGrs9nWimzJPfjJYhZ8', - id: 'f115397bbaf139f0daa954bf26e9a986b5468a2332628b0c0a9a9f6af34d1b3di0', - number: 10951686, - offset: 0, - }, - ]; - } - - return { - inputsWithMocks, - outputsWithMocks, - feeOutputWithMocks, - }; -}; - -export default getPsbtDataWithMocks;