diff --git a/.easignore b/.easignore index cf5bf1eca0e6..6c8abd06ca81 100644 --- a/.easignore +++ b/.easignore @@ -8,7 +8,6 @@ packages/suite-analytics/ packages/suite-build/ packages/suite-data/ packages/suite-desktop/ -packages/suite-desktop-api/ packages/suite-desktop-core/ packages/suite-desktop-ui/ packages/suite-storage/ diff --git a/.github/workflows/check-project-assignment.yml b/.github/workflows/check-project-assignment.yml new file mode 100644 index 000000000000..72856f1d12bf --- /dev/null +++ b/.github/workflows/check-project-assignment.yml @@ -0,0 +1,61 @@ +name: "[Check] Project/Issue Assignment" + +on: + pull_request: + types: + - opened + - ready_for_review + - labeled + - synchronize + +jobs: + check-project-or-issue: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check if PR is assigned to a project or an issue + env: + GITHUB_TOKEN: ${{ secrets.TREZOR_BOT_TOKEN }} + run: | + # Fetch PR labels + PR_LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name') + + # Check for "no-project" label + if echo "$PR_LABELS" | grep -q "^no-project$"; then + echo "Pass: The PR has the 'no-project' label." + exit 0 + fi + + # Check for linked issues using GraphQL + LINKED_ISSUES=$(gh api graphql -f query=' + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 10) { + nodes { + id + } + } + } + } + }' -F owner=${{ github.repository_owner }} -F repo=${{ github.event.repository.name }} -F number=${{ github.event.pull_request.number }} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes | length') + + if [ "$LINKED_ISSUES" -gt 0 ]; then + echo "Pass: The PR is linked to $LINKED_ISSUES issue(s)." + exit 0 + fi + + # Check for associated projects + PROJECT_COUNT=$(gh pr view ${{ github.event.pull_request.number }} --json projectItems --jq '.projectItems | length') + + if [ "$PROJECT_COUNT" -gt 0 ]; then + echo "Pass: This PR is assigned to a project." + exit 0 + fi + + # If no condition passes + echo "Error: This PR is not assigned to any project, not linked to a valid issue, and does not have the 'no-project' label." + echo "Please assign the PR to a project or link it to an issue. Alternatively, add the 'no-project' label if not applicable." + exit 1 diff --git a/.github/workflows/release-suite-definitions.yml b/.github/workflows/release-suite-definitions.yml index 82fbfa3fc0c7..0bb210b6428a 100644 --- a/.github/workflows/release-suite-definitions.yml +++ b/.github/workflows/release-suite-definitions.yml @@ -47,12 +47,14 @@ jobs: yarn nfts simple binance-smart-chain jws yarn nfts simple optimistic-ethereum jws yarn nfts simple base jws + yarn nfts simple arbitrum-one jws yarn coins simple ethereum jws yarn coins simple ethereum-classic jws yarn coins simple polygon-pos jws yarn coins simple binance-smart-chain jws yarn coins simple optimistic-ethereum jws yarn coins simple base jws + yarn coins simple arbitrum-one jws yarn coins simple cardano jws yarn coins simple solana jws yarn coins advanced solana json @@ -70,12 +72,14 @@ jobs: yarn nfts simple binance-smart-chain jws yarn nfts simple optimistic-ethereum jws yarn nfts simple base jws + yarn nfts simple arbitrum-one jws yarn coins simple ethereum jws yarn coins simple ethereum-classic jws yarn coins simple polygon-pos jws yarn coins simple binance-smart-chain jws yarn coins simple optimistic-ethereum jws yarn coins simple base jws + yarn coins simple arbitrum-one jws yarn coins simple cardano jws yarn coins simple solana jws yarn coins advanced solana json diff --git a/.github/workflows/release-suite-native-production.yml b/.github/workflows/release-suite-native-production.yml index 91f682b2f4c0..7fd205c5258b 100644 --- a/.github/workflows/release-suite-native-production.yml +++ b/.github/workflows/release-suite-native-production.yml @@ -95,9 +95,9 @@ jobs: - name: Install libs run: yarn workspaces focus @suite-native/app - name: Build on EAS Android - run: eas build - --platform android - --profile productionAPK - --non-interactive - --message ${{ github.sha }} + run: | + BUILD_ID=$(eas build --platform android --profile productionAPK --non-interactive --message ${{ github.sha }} --wait --json | jq -r '.[0].id') + echo "BUILD_ID: $BUILD_ID" + BUILD_URL=$(eas build:view "$BUILD_ID" --json | jq -r '.artifacts.buildUrl') + echo "BUILD_URL: $BUILD_URL" working-directory: suite-native/app diff --git a/.github/workflows/test-blockchain-link.yml b/.github/workflows/test-blockchain-link.yml index 097c2f98656b..7e16a71e17e9 100644 --- a/.github/workflows/test-blockchain-link.yml +++ b/.github/workflows/test-blockchain-link.yml @@ -7,6 +7,10 @@ on: pull_request: paths: - "packages/blockchain-link" + - "packages/blockchain-link-utils" + - "packages/blockchain-link-types" + - "packages/e2e-utils/src/fixtures/blockbook.ts" + - "packages/e2e-utils/src/mocks/backendServer.ts" # dependencies of packages/blockchain-link - "packages/utxo-lib" - "packages/utils" diff --git a/.github/workflows/test-misc.yml b/.github/workflows/test-misc.yml index 88f2ee4796c8..252a872d357d 100644 --- a/.github/workflows/test-misc.yml +++ b/.github/workflows/test-misc.yml @@ -68,3 +68,19 @@ jobs: - run: yarn install --immutable - run: yarn message-system-sign-config - run: yarn test:unit + + utility-scripts: + runs-on: ubuntu-latest + if: github.repository == 'trezor/trezor-suite' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - run: yarn install --immutable + - run: yarn generate-package @trezor/meow-package + - run: rm -rf packages/meow-package diff --git a/.github/workflows/test-suite-desktop-e2e.yml b/.github/workflows/test-suite-desktop-e2e.yml index 49d9bc376d3c..4d45421c731f 100644 --- a/.github/workflows/test-suite-desktop-e2e.yml +++ b/.github/workflows/test-suite-desktop-e2e.yml @@ -22,23 +22,23 @@ on: jobs: run-desktop-tests: if: github.repository == 'trezor/trezor-suite' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: include: - # - TEST_GROUP: "@group=suite" - # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=device-management" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=suite" + CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=device-management" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=settings" CONTAINERS: "trezor-user-env-unix electrum-regtest" # - TEST_GROUP: "@group=metadata" # CONTAINERS: "trezor-user-env-unix" # - TEST_GROUP: "@group=passphrase" # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=other" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=other" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=wallet" CONTAINERS: "trezor-user-env-unix" diff --git a/.github/workflows/test-suite-web-e2e-pw.yml b/.github/workflows/test-suite-web-e2e-pw.yml index 5c14ba1f5e9b..34b1a743e5cc 100644 --- a/.github/workflows/test-suite-web-e2e-pw.yml +++ b/.github/workflows/test-suite-web-e2e-pw.yml @@ -32,6 +32,8 @@ on: - ".github/workflows/release*" - ".github/workflows/template*" - ".github/actions/release*/**" + schedule: + - cron: "0 0 * * *" workflow_dispatch: env: @@ -93,18 +95,18 @@ jobs: fail-fast: false matrix: include: - # - TEST_GROUP: "@group=suite" - # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=device-management" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=suite" + CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=device-management" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=settings" CONTAINERS: "trezor-user-env-unix" # - TEST_GROUP: "@group=metadata" # CONTAINERS: "trezor-user-env-unix" # - TEST_GROUP: "@group=passphrase" # CONTAINERS: "trezor-user-env-unix" - # - TEST_GROUP: "@group=other" - # CONTAINERS: "trezor-user-env-unix" + - TEST_GROUP: "@group=other" + CONTAINERS: "trezor-user-env-unix" - TEST_GROUP: "@group=wallet" CONTAINERS: "trezor-user-env-unix bitcoin-regtest" diff --git a/.github/workflows/test-suite-web-nightly.yml b/.github/workflows/test-suite-web-nightly.yml index ca6235488d24..6f4014cdd586 100644 --- a/.github/workflows/test-suite-web-nightly.yml +++ b/.github/workflows/test-suite-web-nightly.yml @@ -21,9 +21,6 @@ jobs: - TEST_GROUP: "@group_device-management" CONTAINERS: "trezor-user-env-unix" CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" - - TEST_GROUP: "@group_settings" - CONTAINERS: "trezor-user-env-unix" - CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" - TEST_GROUP: "@group_metadata" CONTAINERS: "trezor-user-env-unix" CYPRESS_USE_TREZOR_USER_ENV_BRIDGE: "1" diff --git a/.husky/eslint.sh b/.husky/eslint.sh index 4a29193f3622..e2fb5cba2bda 100644 --- a/.husky/eslint.sh +++ b/.husky/eslint.sh @@ -1,5 +1,13 @@ #!/bin/bash +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +if [ "$TREZOR_PRE_COMMIT_ESLINT_SKIP" == "true" ]; then + echo "Skipping eslint pre-commit hook, do: 'export TREZOR_PRE_COMMIT_ESLINT_SKIP=false' to re-enable it." + exit 0 +fi + # This check will get all staged files that are *.js(x)/*.ts(x) files # and run eslint on them and tries to fix them and re-add them to be committed. # If auto-fix is possible it shall be transparent for the user. @@ -7,11 +15,12 @@ STAGED_FILES=$(git diff --cached --name-only --diff-filter=d | grep '(\.js\|\.jsx$\|\.ts\|\.tsx)$') +echo -e "${GREEN}Running Eslint pre-commit hook, to disable it do: 'export TREZOR_PRE_COMMIT_ESLINT_SKIP=true'.${NC}" + # Exit if no files. Passing no arguments would trigger eslint for whole repo if [ -z "$STAGED_FILES" ]; then echo "No staged JavaScript/TypeScript files to lint." else - echo "Linting JavaScript/TypeScript files..." echo "$STAGED_FILES" echo "" diff --git a/jest.config.native.js b/jest.config.native.js index 66cf9fa3196c..a62817379a56 100644 --- a/jest.config.native.js +++ b/jest.config.native.js @@ -41,6 +41,5 @@ module.exports = { '/../../node_modules/react-native-gesture-handler/jestSetup.js', '/../../suite-native/test-utils/src/atomsMock.js', '/../../suite-native/test-utils/src/expoMock.js', - '/../../suite-native/test-utils/src/walletSdkMock.js', ], }; diff --git a/package.json b/package.json index 0f9aecf57d67..132bcab1df65 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,8 @@ "node-gyp": "10.2.0", "reselect": "5.1.1", "### We need promise to be same version in mobile unless it produces undefined behavior, check PR #15815": "1.0.0", - "promise": "8.3.0" + "promise": "8.3.0", + "@everstake/wallet-sdk/@solana/web3.js": "1.95.8" }, "devDependencies": { "@babel/cli": "^7.23.9", diff --git a/packages/address-validator/src/currencies.js b/packages/address-validator/src/currencies.js index 9d74114ce5ea..879d16be254f 100644 --- a/packages/address-validator/src/currencies.js +++ b/packages/address-validator/src/currencies.js @@ -1448,7 +1448,12 @@ var CURRENCIES = [ }, { name: 'BNB Smart Chain', - symbol: 'bnb', + symbol: 'bsc', + validator: ETHValidator, + }, + { + name: 'Arbitrum One', + symbol: 'arb', // TODO validator: ETHValidator, }, { diff --git a/packages/address-validator/tests/wallet_address_validator.test.js b/packages/address-validator/tests/wallet_address_validator.test.js index 341818c49d6c..8f17989681f4 100644 --- a/packages/address-validator/tests/wallet_address_validator.test.js +++ b/packages/address-validator/tests/wallet_address_validator.test.js @@ -786,8 +786,8 @@ describe('WAValidator.validate()', function () { isValidAddressType('aaaaaaaaaaaaaaa000000000000000', 'siacoin', 'prod', undefined); }); - it('should return true for correct BNB addresses', function () { - valid('0x0590396689ee1d287147e9383fb8dd24532f2006', 'bnb'); + it('should return true for correct BSC addresses', function () { + valid('0x0590396689ee1d287147e9383fb8dd24532f2006', 'bsc'); valid('0x07fc5c2bcaa0fa6bdaa4fff897490312c8f33c27', 'BNB smart chain'); }); @@ -1116,12 +1116,12 @@ describe('WAValidator.validate()', function () { }); it('should return true for correct BNB smart chain address', function () { - valid('0x7ae2f5b9e386cd1b50a4550696d957cb4900f03a', 'bnb'); + valid('0x7ae2f5b9e386cd1b50a4550696d957cb4900f03a', 'bsc'); valid('0x0000000000000000000000000000000000001000', 'BNB Smart Chain'); }); it('should return false for incorrect BNB smart chain address', function () { - invalid('bnb1xlvns0n2mxh77mzaspn2hgav4rr4m8eerfju38', 'bnb'); + invalid('bnb1xlvns0n2mxh77mzaspn2hgav4rr4m8eerfju38', 'bsc'); }); it('should return true for correct xtz(tezos) address', function () { @@ -1567,13 +1567,13 @@ describe('WAValidator.validate()', function () { invalid('g4VPBPrHZkfE8CsjuG2S4yBQNd455UWmk', 'stellar'); }); - it('should return false for incorrect bnb addresses', function () { + it('should return false for incorrect bsc addresses', function () { commonTests('bnb smart chain'); - commonTests('bnb'); - invalid('xrb_1f5e4w33ndqbkx4bw5jtp13kp5xghebfxcmw9hdt1f7goid1s4373w6tjdgu', 'bnb'); - invalid('nano_1f5e4w33ndqbkx4bw5jtp13kp5xghebfxcmw9hdt1f7goid1s4373w6tjdgu', 'bnb'); - invalid('xrb_1111111112111111111111111111111111111111111111111111hifc8npp', 'bnb'); - invalid('nano_111111111111111111111111111111111111111111111111111hifc8npp', 'bnb'); + commonTests('bsc'); + invalid('xrb_1f5e4w33ndqbkx4bw5jtp13kp5xghebfxcmw9hdt1f7goid1s4373w6tjdgu', 'bsc'); + invalid('nano_1f5e4w33ndqbkx4bw5jtp13kp5xghebfxcmw9hdt1f7goid1s4373w6tjdgu', 'bsc'); + invalid('xrb_1111111112111111111111111111111111111111111111111111hifc8npp', 'bsc'); + invalid('nano_111111111111111111111111111111111111111111111111111hifc8npp', 'bsc'); }); it('should return false for incorrect xtz(tezos) address', function () { diff --git a/packages/blockchain-link-types/package.json b/packages/blockchain-link-types/package.json index 4807a55f6d08..ac7d85206461 100644 --- a/packages/blockchain-link-types/package.json +++ b/packages/blockchain-link-types/package.json @@ -19,6 +19,7 @@ "prepublish": "yarn tsx ../../scripts/prepublish.js" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@solana/web3.js": "^2.0.0", "@trezor/type-utils": "workspace:*", "@trezor/utxo-lib": "workspace:*" diff --git a/packages/blockchain-link-types/src/blockbook-api.ts b/packages/blockchain-link-types/src/blockbook-api.ts index 2debd21191f1..5bf7f31bb3f0 100644 --- a/packages/blockchain-link-types/src/blockbook-api.ts +++ b/packages/blockchain-link-types/src/blockbook-api.ts @@ -146,8 +146,6 @@ export interface Token { secondaryValue?: number; ids?: string[]; multiTokenValues?: MultiTokenValue[]; - totalReceived?: string; - totalSent?: string; } export interface Address { page?: number; diff --git a/packages/blockchain-link-types/src/blockbook.ts b/packages/blockchain-link-types/src/blockbook.ts index 34973f812e07..a88ecf1a1173 100644 --- a/packages/blockchain-link-types/src/blockbook.ts +++ b/packages/blockchain-link-types/src/blockbook.ts @@ -69,9 +69,7 @@ type BlockFiltersBatch = `${string}:${string}:${string}`[]; // XPUBAddress, ERC20, ERC721, ERC1155 - blockbook generated type (Token) is not strict enough export type XPUBAddress = { type: 'XPUBAddress'; -} & Required< - Pick -> & +} & Required> & Pick; type BaseERC = Required> & diff --git a/packages/blockchain-link-types/src/common.ts b/packages/blockchain-link-types/src/common.ts index 4bf76b3b682a..9b0015d21f4f 100644 --- a/packages/blockchain-link-types/src/common.ts +++ b/packages/blockchain-link-types/src/common.ts @@ -6,7 +6,9 @@ import type { TokenTransfer as BlockbookTokenTransfer, ContractInfo, StakingPool, + Token, } from './blockbook-api'; +import type { SolanaStakingAccount } from './solana'; /* Common types used in both params and responses */ @@ -41,6 +43,17 @@ export interface BlockchainSettings { throttleBlockEvent?: number; } +/** + * Discrepancy between `ServerInfo` and `CoinInfo` + * + * `ServerInfo` type + * - `shortcut` is a label for a network (e.g. for BASE `shortcut` has value ETH) + * - `network` is unique symbol for a network (e.g. for BASE `network` has value BASE) + * + * `CoinInfo` type + * - `shortcut` is a unique network symbol + * - `network` are data about network + */ export interface ServerInfo { url: string; name: string; @@ -51,6 +64,7 @@ export interface ServerInfo { blockHeight: number; blockHash: string; consensusBranchId?: number; // zcash current branch id + network: string; } export type TokenStandard = 'ERC20' | 'ERC1155' | 'ERC721' | 'SPL' | 'SPL-2022' | 'BEP20'; @@ -178,7 +192,7 @@ export interface TokenAccount { balance: string; } -export interface TokenInfo { +export interface TokenInfo extends Partial> { type: string; // token type: ERC20... contract: string; // token address, token unit for ADA balance?: string; // token balance @@ -197,6 +211,7 @@ export interface AccountInfo { availableBalance: string; empty: boolean; tokens?: TokenInfo[]; // ethereum and blockfrost tokens + addresses?: AccountAddresses; // bitcoin and blockfrost addresses history: { total: number; // total transactions (unknown in ripple) @@ -211,6 +226,7 @@ export interface AccountInfo { nonce?: string; contractInfo?: ContractInfo; stakingPools?: StakingPool[]; + solStakingAccounts?: SolanaStakingAccount[]; // solana staking accounts addressAliases?: { [key: string]: AddressAlias }; // XRP sequence?: number; diff --git a/packages/blockchain-link-types/src/solana.ts b/packages/blockchain-link-types/src/solana.ts index 330c6e5d700a..cc40d53b6634 100644 --- a/packages/blockchain-link-types/src/solana.ts +++ b/packages/blockchain-link-types/src/solana.ts @@ -7,6 +7,7 @@ import type { GetTransactionApi, Signature, } from '@solana/web3.js'; +import type { SolDelegation } from '@everstake/wallet-sdk'; import type { GetObjectWithKey, @@ -70,4 +71,6 @@ export type AccountInfo< export type { Address } from '@solana/web3.js'; +export type SolanaStakingAccount = SolDelegation; + export type TokenDetailByMint = { [mint: string]: { name: string; symbol: string } }; diff --git a/packages/blockchain-link-utils/package.json b/packages/blockchain-link-utils/package.json index d9e3bfd37ecf..159f6aa349d1 100644 --- a/packages/blockchain-link-utils/package.json +++ b/packages/blockchain-link-utils/package.json @@ -20,6 +20,7 @@ "prepublish": "yarn tsx ../../scripts/prepublish.js" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@mobily/ts-belt": "^3.13.1", "@trezor/env-utils": "workspace:*", "@trezor/utils": "workspace:*" diff --git a/packages/blockchain-link-utils/src/blockbook.ts b/packages/blockchain-link-utils/src/blockbook.ts index 470ffbd9cea4..9f5a91f975ec 100644 --- a/packages/blockchain-link-utils/src/blockbook.ts +++ b/packages/blockchain-link-utils/src/blockbook.ts @@ -1,3 +1,5 @@ +import { ETH_NETWORK_ADDRESSES } from '@everstake/wallet-sdk'; + import { BigNumber } from '@trezor/utils/src/bigNumber'; import type { Utxo, @@ -22,6 +24,7 @@ import { Addresses, filterTargets, enhanceVinVout, sumVinVout, transformTarget } export const transformServerInfo = (payload: ServerInfo) => ({ name: payload.name, shortcut: payload.shortcut, + network: payload.network ?? payload.shortcut, // some instances don't send network (e.g. regtest) testnet: payload.testnet, version: payload.version, decimals: payload.decimals, @@ -87,21 +90,15 @@ export const filterTokenTransfers = ( }); }; -const ethereumStakingAddresses = { - poolInstance: [ - '0xD523794C879D9eC028960a231F866758e405bE34', - '0xAFA848357154a6a624686b348303EF9a13F63264', - ], - withdrawTreasury: [ - '0x19449f0f696703Aa3b1485DfA2d855F33659397a', - '0x66cb3AeD024740164EBcF04e292dB09b5B63A2e1', - ], -}; - export const isEthereumStakingInternalTransfer = (from: string, to: string) => { - const { poolInstance, withdrawTreasury } = ethereumStakingAddresses; + const poolInstances = Object.values(ETH_NETWORK_ADDRESSES).map( + network => network.addressContractPool, + ); + const withdrawTreasuries = Object.values(ETH_NETWORK_ADDRESSES).map( + network => network.addressContractWithdrawTreasury, + ); - return poolInstance.includes(from) && withdrawTreasury.includes(to); + return poolInstances.includes(from) && withdrawTreasuries.includes(to); }; export const filterEthereumInternalTransfers = ( @@ -338,8 +335,6 @@ export const transformAddresses = ( path: t.path, transfers: t.transfers, balance: t.balance, - sent: t.totalSent, - received: t.totalReceived, }, ]); }, [] as Address[]); diff --git a/packages/blockchain-link-utils/src/ripple.ts b/packages/blockchain-link-utils/src/ripple.ts index 4de17fbb07ec..2129f3cbb081 100644 --- a/packages/blockchain-link-utils/src/ripple.ts +++ b/packages/blockchain-link-utils/src/ripple.ts @@ -4,6 +4,7 @@ import type { Transaction } from '@trezor/blockchain-link-types'; export const transformServerInfo = (payload: any) => ({ name: 'Ripple', shortcut: 'xrp', + network: 'xrp', testnet: false, version: payload.buildVersion, decimals: 6, diff --git a/packages/blockchain-link/package.json b/packages/blockchain-link/package.json index 751d645506eb..9f933a02e57b 100644 --- a/packages/blockchain-link/package.json +++ b/packages/blockchain-link/package.json @@ -76,6 +76,7 @@ "worker-loader": "^3.0.8" }, "dependencies": { + "@everstake/wallet-sdk": "^1.0.5", "@solana-program/token": "^0.4.1", "@solana-program/token-2022": "^0.3.1", "@solana/web3.js": "^2.0.0", diff --git a/packages/blockchain-link/src/ui/config.ts b/packages/blockchain-link/src/ui/config.ts index d3721a620c9b..3282b1566b0a 100644 --- a/packages/blockchain-link/src/ui/config.ts +++ b/packages/blockchain-link/src/ui/config.ts @@ -114,6 +114,28 @@ export default [ subscribe: '', }, }, + { + blockchain: { + name: 'Arbitrum One', + worker: 'js/blockbook-worker.js', + server: ['https://arb1.trezor.io', 'https://arb2.trezor.io'], + debug: true, + }, + data: { + address: '', + accountInfoOptions: { + page: 1, + pageSize: 25, + contractFilter: undefined, + }, + estimateFeeOptions: { + blocks: [1, 2, 10], + }, + txid: '', + tx: '', + subscribe: '', + }, + }, { blockchain: { name: 'Base', diff --git a/packages/blockchain-link/src/workers/blockfrost/index.ts b/packages/blockchain-link/src/workers/blockfrost/index.ts index 7b6f2b5ca69d..b240f4722497 100644 --- a/packages/blockchain-link/src/workers/blockfrost/index.ts +++ b/packages/blockchain-link/src/workers/blockfrost/index.ts @@ -27,6 +27,7 @@ const getInfo = async (request: Request) => { type: RESPONSES.GET_INFO, payload: { url: api.options.url, + network: info.shortcut, ...info, }, } as const; diff --git a/packages/blockchain-link/src/workers/electrum/methods/getInfo.ts b/packages/blockchain-link/src/workers/electrum/methods/getInfo.ts index 7b553cc8d6c2..16fa84891cf5 100644 --- a/packages/blockchain-link/src/workers/electrum/methods/getInfo.ts +++ b/packages/blockchain-link/src/workers/electrum/methods/getInfo.ts @@ -19,6 +19,7 @@ const getInfo: Api = client => { blockHash: blockheaderToBlockhash(hex), name: 'Bitcoin', shortcut: coin, + network: coin, testnet: coin === 'REGTEST', decimals: 8, }); diff --git a/packages/blockchain-link/src/workers/solana/index.ts b/packages/blockchain-link/src/workers/solana/index.ts index 5be487b0c776..174a2ace3b97 100644 --- a/packages/blockchain-link/src/workers/solana/index.ts +++ b/packages/blockchain-link/src/workers/solana/index.ts @@ -61,6 +61,7 @@ import { IntervalId } from '@trezor/type-utils'; import { getBaseFee, getPriorityFee } from './fee'; import { BaseWorker, ContextType, CONTEXT } from '../baseWorker'; +// import { getSolanaStakingAccounts } from '../utils'; export type SolanaAPI = Readonly<{ clusterUrl: ClusterUrl; @@ -205,7 +206,11 @@ const pushTransaction = async (request: Request) = } }; -const getAccountInfo = async (request: Request) => { +const getAccountInfo = async ( + request: Request, + // TODO: uncomment when solana staking accounts are supported + // isTestnet: boolean, +) => { const { payload } = request; const { details = 'basic' } = payload; const api = await request.connect(); @@ -346,9 +351,12 @@ const getAccountInfo = async (request: Request) => const accountDataBytes = getBase64Encoder().encode(accountDataEncoded); const accountDataLength = BigInt(accountDataBytes.byteLength); const rent = await api.rpc.getMinimumBalanceForRentExemption(accountDataLength).send(); + // TODO: uncomment when solana staking accounts are supported + // const stakingAccounts = await getSolanaStakingAccounts(payload.descriptor, isTestnet); misc = { owner: accountInfo?.owner, rent: Number(rent), + solStakingAccounts: [], }; } } @@ -403,6 +411,7 @@ const getInfo = async (request: Request, isTestnet: boolea blockHeight: Number(blockHeight), blockHash, shortcut: isTestnet ? 'dsol' : 'sol', + network: isTestnet ? 'dsol' : 'sol', url: api.clusterUrl, name: 'Solana', version: (await api.rpc.getVersion().send())['solana-core'], diff --git a/packages/blockchain-link/src/workers/utils.ts b/packages/blockchain-link/src/workers/utils.ts index a180fd498341..ace5622825ee 100644 --- a/packages/blockchain-link/src/workers/utils.ts +++ b/packages/blockchain-link/src/workers/utils.ts @@ -1,5 +1,9 @@ +import { Solana, SolNetwork } from '@everstake/wallet-sdk'; + import { parseHostname } from '@trezor/utils'; +import config from './../ui/config'; + /** * Sorts array of backend urls so the localhost addresses are first, * then onion addresses and then the rest. Apart from that it will @@ -20,3 +24,21 @@ export const prioritizeEndpoints = (urls: string[]) => }) .sort(([, a], [, b]) => b - a) .map(([url]) => url); + +export const getSolanaStakingAccounts = async (descriptor: string, isTestnet: boolean) => { + const blockchainEnvironment = isTestnet ? 'devnet' : 'mainnet'; + + // Find the blockchain configuration for the specified chain and environment + const blockchainConfig = config.find(c => + c.blockchain.name.toLowerCase().includes(`solana ${blockchainEnvironment}`), + ); + const serverUrl = blockchainConfig?.blockchain.server[0]; + const network = isTestnet ? SolNetwork.Devnet : SolNetwork.Mainnet; + + const solanaClient = new Solana(network, serverUrl); + + const delegations = await solanaClient.getDelegations(descriptor); + const { result: stakingAccounts } = delegations; + + return stakingAccounts; +}; diff --git a/packages/blockchain-link/tests/integration/blockbook.test.ts b/packages/blockchain-link/tests/integration/blockbook.test.ts index 4dd92f020496..b722fd12509c 100644 --- a/packages/blockchain-link/tests/integration/blockbook.test.ts +++ b/packages/blockchain-link/tests/integration/blockbook.test.ts @@ -45,6 +45,7 @@ backends.forEach(b => { expect(result).toEqual({ name: 'TestMock', shortcut: 'test', + network: 'test', decimals: 9, blockHeight: 1, url: expect.any(String), diff --git a/packages/blockchain-link/tests/integration/blockfrost.test.ts b/packages/blockchain-link/tests/integration/blockfrost.test.ts index 9f03bd8f1d78..6248506d1931 100644 --- a/packages/blockchain-link/tests/integration/blockfrost.test.ts +++ b/packages/blockchain-link/tests/integration/blockfrost.test.ts @@ -45,6 +45,7 @@ backends.forEach(b => { expect(result).toEqual({ name: 'BlockfrostMock', shortcut: 'ada', + network: 'ada', decimals: 6, blockHeight: 1, blockHash: 'test_block_hash-hash', diff --git a/packages/blockchain-link/tests/unit/fixtures/getInfo.ts b/packages/blockchain-link/tests/unit/fixtures/getInfo.ts index b4ac41765f9b..7744ba864a7f 100644 --- a/packages/blockchain-link/tests/unit/fixtures/getInfo.ts +++ b/packages/blockchain-link/tests/unit/fixtures/getInfo.ts @@ -7,6 +7,7 @@ export default { decimals: 9, name: 'TestMock', shortcut: 'test', + network: 'test', }, }, { @@ -18,6 +19,7 @@ export default { data: { name: 'Zcash', shortcut: 'zec', + network: 'zec', decimals: 8, bestHeight: 1, backend: { @@ -34,9 +36,37 @@ export default { decimals: 8, name: 'Zcash', shortcut: 'zec', + network: 'zec', consensusBranchId: 3268858036, }, }, + { + description: 'BASE L2 network', + serverFixtures: [ + { + method: 'getInfo', + response: { + data: { + name: 'Base Archive', + shortcut: 'ETH', + network: 'BASE', + decimals: 18, + bestHeight: 23603976, + backend: { + version: 'Geth/v1.101411.2-stable-3dd9b027/linux-amd64/go1.23.3', + }, + }, + }, + }, + ], + response: { + name: 'Base Archive', + shortcut: 'ETH', + network: 'BASE', + decimals: 18, + blockHeight: 23603976, + }, + }, { description: 'Error', serverFixtures: [ @@ -59,6 +89,7 @@ export default { decimals: 6, name: 'Ripple', shortcut: 'xrp', + network: 'xrp', testnet: false, version: '1.4.0', }, @@ -87,6 +118,7 @@ export default { decimals: 6, name: 'BlockfrostMock', shortcut: 'ada', + network: 'ada', testnet: false, version: '1.4.0', }, diff --git a/packages/blockchain-link/webpack/workers.web.js b/packages/blockchain-link/webpack/workers.web.js index 4197bfb4ada9..2af9626057d6 100644 --- a/packages/blockchain-link/webpack/workers.web.js +++ b/packages/blockchain-link/webpack/workers.web.js @@ -42,6 +42,12 @@ module.exports = { stream: require.resolve('stream-browserify'), }, }, + externals: [ + { + // Replace cross-fetch with native fetch, otherwise it will use node-fetch and fails to build + 'cross-fetch': 'fetch', + }, + ], performance: { hints: false, }, diff --git a/packages/components/src/components/Flex/Flex.tsx b/packages/components/src/components/Flex/Flex.tsx index f1018bb1e543..12d9093e287c 100644 --- a/packages/components/src/components/Flex/Flex.tsx +++ b/packages/components/src/components/Flex/Flex.tsx @@ -137,6 +137,10 @@ const Container = styled.div` ${({ $hasDivider, ...props }) => $hasDivider && withDivider(props)} ${withFrameProps} + + &:empty { + display: none; + } `; export type FlexProps = AllowedFrameProps & { diff --git a/packages/components/src/components/IconCircle/IconCircle.tsx b/packages/components/src/components/IconCircle/IconCircle.tsx index 96fd3e6672bf..9478d84a00a2 100644 --- a/packages/components/src/components/IconCircle/IconCircle.tsx +++ b/packages/components/src/components/IconCircle/IconCircle.tsx @@ -1,13 +1,8 @@ import styled, { css } from 'styled-components'; -import { ExclusiveColorOrVariant, Icon, IconName, IconSize, getIconSize } from '../Icon/Icon'; +import { Icon, IconName, IconSize, getIconSize } from '../Icon/Icon'; import { TransientProps } from '../../utils/transientProps'; -import { - IconCircleExclusiveColorOrVariant, - IconCircleVariant, - IconCircleColors, - IconCirclePaddingType, -} from './types'; +import { IconCircleVariant, IconCirclePaddingType } from './types'; import { mapVariantToIconBackground, mapVariantToIconBorderColor, @@ -23,12 +18,11 @@ import { export const allowedIconCircleFrameProps = ['margin'] as const satisfies FramePropsKeys[]; type AllowedFrameProps = Pick; -type IconCircleWrapperProps = TransientProps< - IconCircleExclusiveColorOrVariant & AllowedFrameProps -> & { +type IconCircleWrapperProps = TransientProps & { $size: number; $hasBorder: boolean; $paddingType: IconCirclePaddingType; + $variant: IconCircleVariant; }; const IconCircleWrapper = styled.div` @@ -58,26 +52,17 @@ export type IconCircleProps = { size?: IconSize | number; paddingType?: IconCirclePaddingType; hasBorder?: boolean; -} & IconCircleExclusiveColorOrVariant & - AllowedFrameProps; + variant?: IconCircleVariant; +} & AllowedFrameProps; export const IconCircle = ({ name, size = 60, hasBorder = true, paddingType = 'large', - iconColor, - variant, + variant = 'primary', ...rest }: IconCircleProps) => { - const wrapperColorOrVariant: TransientProps = - iconColor === undefined ? { $variant: variant ?? 'primary' } : { $iconColor: iconColor }; - - const iconColorOrVariant: ExclusiveColorOrVariant = - iconColor === undefined - ? { variant: variant ?? 'primary' } - : { color: iconColor.foreground }; - const iconSize = getIconSize(size); const frameProps = pickAndPrepareFrameProps(rest, allowedIconCircleFrameProps); @@ -86,12 +71,12 @@ export const IconCircle = ({ $size={iconSize} $paddingType={paddingType} $hasBorder={hasBorder} - {...wrapperColorOrVariant} + $variant={variant} {...frameProps} > - + ); }; -export type { IconCircleVariant, IconCircleColors }; +export type { IconCircleVariant }; diff --git a/packages/components/src/components/IconCircle/types.tsx b/packages/components/src/components/IconCircle/types.tsx index e480c159cc86..d91dab335b98 100644 --- a/packages/components/src/components/IconCircle/types.tsx +++ b/packages/components/src/components/IconCircle/types.tsx @@ -1,5 +1,3 @@ -import { CSSColor } from '@trezor/theme'; - import { UIVariant, UISize } from '../../config/types'; export const iconCircleVariants = [ @@ -12,11 +10,5 @@ export const iconCircleVariants = [ export type IconCircleVariant = Extract; -export type IconCircleColors = { foreground: CSSColor; background: CSSColor }; - -export type IconCircleExclusiveColorOrVariant = - | { variant?: IconCircleVariant; iconColor?: undefined } - | { variant?: undefined; iconColor?: IconCircleColors }; - export const iconCirclePaddingTypes = ['small', 'medium', 'large'] as const; export type IconCirclePaddingType = Extract; diff --git a/packages/components/src/components/IconCircle/utils.tsx b/packages/components/src/components/IconCircle/utils.tsx index 9d5721ff9498..e5ab966542bd 100644 --- a/packages/components/src/components/IconCircle/utils.tsx +++ b/packages/components/src/components/IconCircle/utils.tsx @@ -2,32 +2,20 @@ import { DefaultTheme } from 'styled-components'; import { Color, CSSColor } from '@trezor/theme'; -import { - IconCircleVariant, - IconCircleExclusiveColorOrVariant, - IconCirclePaddingType, -} from './types'; -import { TransientProps } from '../../utils/transientProps'; +import { IconCircleVariant, IconCirclePaddingType } from './types'; type VariantMapArgs = { theme: DefaultTheme; $hasBorder: boolean; -} & TransientProps; + $variant: IconCircleVariant; +}; type PaddingTypeMap = { $paddingType: IconCirclePaddingType; $size: number; }; -export const mapVariantToIconBorderColor = ({ - $variant, - theme, - $iconColor, -}: VariantMapArgs): CSSColor => { - if ($variant === undefined) { - return $iconColor?.foreground ?? 'transparent'; - } - +export const mapVariantToIconBorderColor = ({ $variant, theme }: VariantMapArgs): CSSColor => { const colorMap: Record = { primary: 'backgroundPrimarySubtleOnElevation0', warning: 'backgroundAlertYellowSubtleOnElevation0', @@ -42,13 +30,8 @@ export const mapVariantToIconBorderColor = ({ export const mapVariantToIconBackground = ({ theme, $hasBorder, - $iconColor, $variant, }: VariantMapArgs): CSSColor => { - if ($variant === undefined) { - return $iconColor?.background ?? 'transparent'; - } - const noBorderColorMap: Record = { primary: 'backgroundPrimarySubtleOnElevation0', warning: 'backgroundAlertYellowSubtleOnElevation0', diff --git a/packages/components/src/components/InfoItem/InfoItem.tsx b/packages/components/src/components/InfoItem/InfoItem.tsx index d79728dd7b12..6e661fe623e3 100644 --- a/packages/components/src/components/InfoItem/InfoItem.tsx +++ b/packages/components/src/components/InfoItem/InfoItem.tsx @@ -52,6 +52,7 @@ export type InfoItemProps = AllowedFrameProps & labelWidth?: string | number; verticalAlignment?: InfoItemVerticalAlignment; gap?: SpacingValues; + 'data-testid'?: string; }; export const InfoItem = ({ @@ -64,13 +65,14 @@ export const InfoItem = ({ gap, labelWidth, verticalAlignment = 'center', + 'data-testid': dataTestId, ...rest }: InfoItemProps) => { const frameProps = pickAndPrepareFrameProps(rest, allowedInfoItemFrameProps); const isRow = direction === 'row'; return ( - + = { args: { variant: 'primary', iconName: undefined, - iconComponent: undefined, heading: 'Modal heading', description: 'Modal description', children: @@ -69,19 +67,6 @@ export const NewModal: StoryObj = { }, options: [...newModalVariants, undefined], }, - iconComponent: { - options: ['nothing', 'purple'], - mapping: { - nothing: undefined, - purple: ( - - ), - }, - }, size: { control: { type: 'select', diff --git a/packages/components/src/components/NewModal/NewModal.tsx b/packages/components/src/components/NewModal/NewModal.tsx index d130561b60ef..6142dbcc1f12 100644 --- a/packages/components/src/components/NewModal/NewModal.tsx +++ b/packages/components/src/components/NewModal/NewModal.tsx @@ -20,7 +20,7 @@ import { ElevationContext, ElevationUp, useElevation } from '../ElevationContext import { useScrollShadow } from '../../utils/useScrollShadow'; import { IconCircle } from '../IconCircle/IconCircle'; import { IconName } from '../Icon/Icon'; -import { Row } from '../Flex/Flex'; +import { Box } from '../Box/Box'; import { NewModalButton } from './NewModalButton'; import { NewModalContext } from './NewModalContext'; import { NewModalBackdrop } from './NewModalBackdrop'; @@ -99,10 +99,6 @@ const Footer = styled.footer` border-top: 1px solid ${({ theme }) => theme.borderElevation0}; `; -type ExclusiveIconNameOrComponent = - | { iconName?: IconName; iconComponent?: undefined } - | { iconName?: undefined; iconComponent?: ReactNode }; - type NewModalProps = AllowedFrameProps & { variant?: NewModalVariant; children?: ReactNode; @@ -114,8 +110,9 @@ type NewModalProps = AllowedFrameProps & { isBackdropCancelable?: boolean; alignment?: NewModalAlignment; size?: NewModalSize; + iconName?: IconName; 'data-testid'?: string; -} & ExclusiveIconNameOrComponent; +}; const InnerNewModalBase = ({ children, @@ -125,7 +122,6 @@ const InnerNewModalBase = ({ description, bottomContent, iconName, - iconComponent, onBackClick, onCancel, isBackdropCancelable, @@ -193,18 +189,15 @@ const InnerNewModalBase = ({ - {(iconComponent || iconName) && ( - - {iconComponent ?? - (iconName && ( - - ))} - + + )} {children} @@ -235,7 +228,7 @@ const NewModal = ({ isBackdropCancelable = true, ...rest }: NewModalProps) => { onClick={isBackdropCancelable ? onCancel : undefined} alignment={alignment} > - + ); }; diff --git a/packages/components/src/components/Table/TableCell.tsx b/packages/components/src/components/Table/TableCell.tsx index d3f73f724623..8f7fc10f771f 100644 --- a/packages/components/src/components/Table/TableCell.tsx +++ b/packages/components/src/components/Table/TableCell.tsx @@ -63,6 +63,7 @@ export type TableCellProps = AllowedFrameProps & { children?: ReactNode; colSpan?: number; align?: UIHorizontalAlignment; + 'data-testid'?: string; }; export const TableCell = ({ @@ -71,6 +72,7 @@ export const TableCell = ({ align = 'left', padding, maxWidth = 300, + 'data-testid': dataTestId, }: TableCellProps) => { const isHeader = useTableHeader(); const { hasBorders, typographyStyle } = useTable(); @@ -91,6 +93,7 @@ export const TableCell = ({ $padding={padding ?? defaultPadding} $maxWidth={maxWidth} $hasBorder={hasBorders} + data-testid={dataTestId} > ))} - + {children} diff --git a/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx b/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx index 96b610b273e5..4643011df049 100644 --- a/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx +++ b/packages/components/src/components/loaders/ProgressBar/ProgressBar.tsx @@ -1,41 +1,34 @@ import styled from 'styled-components'; -import { borders } from '@trezor/theme'; - const Wrapper = styled.div` background: ${({ theme }) => theme.backgroundNeutralSubdued}; width: 100%; - border-radius: ${borders.radii.full}; + overflow: hidden; `; type ValueProps = { $max: number; $value: number; - $isRed: boolean; }; -const Value = styled.div.attrs(({ $max, $value }) => ({ - style: { - width: `${(100 / $max) * $value}%`, - }, -}))` - background: ${({ theme, $isRed }) => ($isRed ? theme.borderAlertRed : theme.borderSecondary)}; +const Value = styled.div` + background: ${({ theme }) => theme.iconPrimaryDefault}; height: 5px; max-width: 100%; - transition: width 0.5s; + width: 1%; + transform: ${({ $max, $value }) => `scaleX(${(100 / $max) * $value})`}; + transform-origin: left; + transition: transform 0.5s; `; -export interface ProgressBarProps { +export type ProgressBarProps = { max?: number; value: number; - isRed?: boolean; -} +}; -// HTML progress element is not used because styling is browser-dependent (no consistent way to override styles -// from parent component, no straightforward way to add width transition in Firefox) -export const ProgressBar = ({ max = 100, value, isRed = false, ...props }: ProgressBarProps) => ( +export const ProgressBar = ({ max = 100, value, ...props }: ProgressBarProps) => ( - + ); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 2c6b485f642f..29f149d06471 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -52,7 +52,6 @@ export { IconCircle, type IconCircleProps, type IconCircleVariant, - type IconCircleColors, } from './components/IconCircle/IconCircle'; export { InfoSegments, type InfoSegmentsProps } from './components/InfoSegments/InfoSegments'; export { InfoItem, type InfoItemProps } from './components/InfoItem/InfoItem'; diff --git a/packages/connect-common/files/coins-eth.json b/packages/connect-common/files/coins-eth.json index eaa1afd6fa48..d787c18d0673 100644 --- a/packages/connect-common/files/coins-eth.json +++ b/packages/connect-common/files/coins-eth.json @@ -11,6 +11,7 @@ "is_testnet": false, "name": "Ethereum", "shortcut": "ETH", + "label": "ETH", "slip44": 60, "support": { "T1B1": "1.6.2", @@ -30,7 +31,8 @@ "coingecko_id": "binance-smart-chain", "is_testnet": false, "name": "BNB Smart Chain", - "shortcut": "BNB", + "shortcut": "BSC", + "label": "BNB", "slip44": 714, "support": { "T1B1": "1.9.4", @@ -51,6 +53,7 @@ "is_testnet": false, "name": "Ethereum Classic", "shortcut": "ETC", + "label": "ETC", "slip44": 61, "support": { "T1B1": "1.6.2", @@ -71,6 +74,7 @@ "is_testnet": false, "name": "Polygon", "shortcut": "POL", + "label": "POL", "slip44": 966, "support": { "T1B1": "1.9.4", @@ -80,6 +84,27 @@ "T3T1": "2.6.1" } }, + { + "blockchain_link": { + "type": "blockbook", + "url": ["https://arb1.trezor.io", "https://arb2.trezor.io"] + }, + "chain": "arb", + "chain_id": 42161, + "coingecko_id": "arbitrum-one", + "is_testnet": false, + "name": "Arbitrum One", + "shortcut": "ARB", + "label": "ETH", + "slip44": 9001, + "support": { + "T1B1": "1.9.4", + "T2B1": "2.6.1", + "T2T1": "2.3.5", + "T3B1": "2.8.1", + "T3T1": "2.6.1" + } + }, { "blockchain_link": { "type": "blockbook", @@ -91,7 +116,8 @@ "is_testnet": false, "name": "Base", "shortcut": "BASE", - "slip44": 1, + "label": "ETH", + "slip44": 8453, "support": { "T1B1": "1.9.4", "T2B1": "2.6.1", @@ -111,7 +137,8 @@ "is_testnet": false, "name": "Optimism", "shortcut": "OP", - "slip44": 1, + "label": "ETH", + "slip44": 614, "support": { "T1B1": "1.9.4", "T2B1": "2.6.1", @@ -130,6 +157,7 @@ "is_testnet": true, "name": "Holesky", "shortcut": "tHOL", + "label": "tHOL", "slip44": 1, "support": { "T1B1": "1.11.3", @@ -149,6 +177,7 @@ "is_testnet": true, "name": "Sepolia", "shortcut": "tSEP", + "label": "tSEP", "slip44": 1, "support": { "T1B1": "1.11.3", diff --git a/packages/connect-common/files/firmware/t3b1/releases.json b/packages/connect-common/files/firmware/t3b1/releases.json index 8da492edbd3c..bf1dca002880 100644 --- a/packages/connect-common/files/firmware/t3b1/releases.json +++ b/packages/connect-common/files/firmware/t3b1/releases.json @@ -3,7 +3,7 @@ "required": false, "version": [2, 8, 3], "min_firmware_version": [2, 8, 3], - "min_bootloader_version": [2, 1, 8], + "min_bootloader_version": [2, 1, 7], "bootloader_version": [2, 1, 8], "translations": ["cs-CZ", "de-DE", "es-ES", "fr-FR", "it-IT", "pt-BR"], "url": "firmware/t3b1/trezor-t3b1-2.8.3.bin", diff --git a/packages/connect-explorer-theme/src/components/menu.tsx b/packages/connect-explorer-theme/src/components/menu.tsx index d6c688673653..95540f66cf7e 100644 --- a/packages/connect-explorer-theme/src/components/menu.tsx +++ b/packages/connect-explorer-theme/src/components/menu.tsx @@ -111,7 +111,7 @@ export function Menu({ const prevRoute = useRef(route); const coinSymbols = { - binance: 'bnb', + binance: 'bsc', bitcoin: 'btc', cardano: 'ada', eos: 'eos', diff --git a/packages/connect-explorer/src/components/ApiPlayground.tsx b/packages/connect-explorer/src/components/ApiPlayground.tsx index 687ca5106993..c5dfc94ed4f7 100644 --- a/packages/connect-explorer/src/components/ApiPlayground.tsx +++ b/packages/connect-explorer/src/components/ApiPlayground.tsx @@ -45,7 +45,6 @@ const ContentWrapper = styled.div` `; const OptionsRow = styled(MethodContent)` - margin-top: -${spacingsPx.sm}; margin-bottom: ${spacingsPx.md}; align-items: center; diff --git a/packages/connect-explorer/src/reducers/methodReducer.ts b/packages/connect-explorer/src/reducers/methodReducer.ts index 83d2b425e486..f155d448051c 100644 --- a/packages/connect-explorer/src/reducers/methodReducer.ts +++ b/packages/connect-explorer/src/reducers/methodReducer.ts @@ -54,7 +54,7 @@ const onFieldChange = (state: MethodState, _field: Field, value: any) => { setAffectedValues(newState, field); } - return updateParams(newState); + return updateParams({ ...state, fields: newState.fields }); }; // Update field data diff --git a/packages/connect-iframe/webpack/base.webpack.config.ts b/packages/connect-iframe/webpack/base.webpack.config.ts index 92fdba35a73f..95fcea747ce8 100644 --- a/packages/connect-iframe/webpack/base.webpack.config.ts +++ b/packages/connect-iframe/webpack/base.webpack.config.ts @@ -111,10 +111,11 @@ export const config: webpack.Configuration = { }), // provide fallback for global objects. // resolve.fallback will not work since those objects are not imported as modules. + // process/browser needs explicit .js extension new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'], Promise: ['es6-promise', 'Promise'], - process: 'process/browser', + process: 'process/browser.js', }), // resolve @trezor/connect modules as "browser" new webpack.NormalModuleReplacementPlugin(/\/workers\/workers$/, resource => { diff --git a/packages/connect-web/src/module/index.ts b/packages/connect-web/src/module/index.ts index 22778d4b4299..bf9ba157c1b1 100644 --- a/packages/connect-web/src/module/index.ts +++ b/packages/connect-web/src/module/index.ts @@ -1,3 +1,4 @@ +import { cloneObject } from '@trezor/utils'; import { factory } from '@trezor/connect/src/factory'; import { config } from '@trezor/connect/src/data/config'; import { TrezorConnectDynamic } from '@trezor/connect/src/impl/dynamic'; @@ -31,8 +32,8 @@ const impl = new TrezorConnectDynamic< impl: new CoreInModule((message: CoreEventMessage) => { if (message.event === TRANSPORT_EVENT) { const platform = getInstallerPackage(); - message.payload.bridge = suggestBridgeInstaller(platform); - message.payload.udev = suggestUdevInstaller(platform); + message.payload.bridge = cloneObject(suggestBridgeInstaller(platform)); + message.payload.udev = cloneObject(suggestUdevInstaller(platform)); } return message; diff --git a/packages/connect/e2e/__wscache__/blockbook.js b/packages/connect/e2e/__wscache__/blockbook.js index de6ace0ec33b..f5ec6a719487 100644 --- a/packages/connect/e2e/__wscache__/blockbook.js +++ b/packages/connect/e2e/__wscache__/blockbook.js @@ -10,6 +10,7 @@ const blockbookFixtures = { bestHeight: 7000000, // high block to make sure that utxos have enough confirmations (composeTransaction test) bestHash: '', block0Hash: '', + network: params.network, testnet: true, version: '0.0.0-mocked', }, diff --git a/packages/connect/e2e/__wscache__/blockfrost.js b/packages/connect/e2e/__wscache__/blockfrost.js index d00ae35541e2..5fab7007e362 100644 --- a/packages/connect/e2e/__wscache__/blockfrost.js +++ b/packages/connect/e2e/__wscache__/blockfrost.js @@ -6,6 +6,7 @@ const blockfrostFixtures = { data: { name: 'Blockfrost', shortcut: params.shortcut, + network: params.network, decimals: 6, testnet: false, version: '1.4.0', diff --git a/packages/connect/e2e/common.setup.ts b/packages/connect/e2e/common.setup.ts index 9313681c72af..674bae1d0f9c 100644 --- a/packages/connect/e2e/common.setup.ts +++ b/packages/connect/e2e/common.setup.ts @@ -93,11 +93,13 @@ export const setup = async ( throw new Error('Unknown emulator start type'); } + const { settings, ...restOptions } = options; + if (!options.wiped) { const mnemonic = options.mnemonic || MNEMONICS.mnemonic_all; await TrezorUserEnvLink.setupEmu({ - ...options, + ...restOptions, mnemonic, pin: options.pin || '', passphrase_protection: !!options.passphrase_protection, @@ -106,7 +108,7 @@ export const setup = async ( }); } - if (options.settings) { + if (settings) { // allow apply-settings to fail, older FW may not know some flags yet try { await TrezorUserEnvLink.send({ type: 'emulator-apply-settings', ...options.settings }); diff --git a/packages/connect/src/__mocks__/@trezor/blockchain-link.ts b/packages/connect/src/__mocks__/@trezor/blockchain-link.ts index d8297334864a..f0251867bf32 100644 --- a/packages/connect/src/__mocks__/@trezor/blockchain-link.ts +++ b/packages/connect/src/__mocks__/@trezor/blockchain-link.ts @@ -37,6 +37,7 @@ class BlockchainLink { url: this.name, name: this.name, shortcut: this.name, + network: this.name, consensusBranchId: 1001, }); } diff --git a/packages/connect/src/api/composeTransaction.ts b/packages/connect/src/api/composeTransaction.ts index f72749c222d0..818e6b65b1a2 100644 --- a/packages/connect/src/api/composeTransaction.ts +++ b/packages/connect/src/api/composeTransaction.ts @@ -1,6 +1,7 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/ComposeTransaction.js import { BigNumber } from '@trezor/utils/src/bigNumber'; +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; import type { ComposeOutput, TransactionInputOutputSortingStrategy } from '@trezor/utxo-lib'; import { AbstractMethod } from '../core/AbstractMethod'; @@ -9,7 +10,6 @@ import { UI, createUiMessage } from '../events'; import { Discovery } from './common/Discovery'; import { validateParams, getFirmwareRange } from './common/paramsValidator'; import * as pathUtils from '../utils/pathUtils'; -import { resolveAfter } from '../utils/promiseUtils'; import { formatAmount } from '../utils/formatUtils'; import { getBitcoinNetwork, fixCoinInfoNetwork } from '../data/coinInfo'; import { isBackendSupported, initBlockchain } from '../backend/BlockchainLink'; diff --git a/packages/connect/src/api/ethereum/api/ethereumGetAddress.ts b/packages/connect/src/api/ethereum/api/ethereumGetAddress.ts index da81a682ac3a..526b4823130f 100644 --- a/packages/connect/src/api/ethereum/api/ethereumGetAddress.ts +++ b/packages/connect/src/api/ethereum/api/ethereumGetAddress.ts @@ -10,7 +10,7 @@ import { getEthereumNetwork, getUniqueNetworks } from '../../../data/coinInfo'; import { stripHexPrefix } from '../../../utils/formatUtils'; import { PROTO, ERRORS } from '../../../constants'; import { UI, createUiMessage } from '../../../events'; -import type { EthereumNetworkInfo } from '../../../types'; +import type { EthereumNetworkInfoDefinitionValues } from '../../../types'; import { getEthereumDefinitions, decodeEthereumDefinition, @@ -21,7 +21,7 @@ import { GetAddress as GetAddressSchema } from '../../../types/api/getAddress'; type Params = PROTO.EthereumGetAddress & { address?: string; - network?: EthereumNetworkInfo; + network?: EthereumNetworkInfoDefinitionValues; encoded_network?: ArrayBuffer; }; diff --git a/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts b/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts index 571e3b79eba3..ac5ea8f27206 100644 --- a/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts +++ b/packages/connect/src/api/ethereum/api/ethereumSignTransaction.ts @@ -17,13 +17,13 @@ import { } from '../ethereumDefinitions'; import type { EthereumTransaction, EthereumTransactionEIP1559 } from '../../../types/api/ethereum'; import { - EthereumNetworkInfo, + EthereumNetworkInfoDefinitionValues, EthereumSignTransaction as EthereumSignTransactionSchema, } from '../../../types'; type Params = { path: number[]; - network?: EthereumNetworkInfo; + network?: EthereumNetworkInfoDefinitionValues; definitions?: MessagesSchema.EthereumDefinitions; chunkify: boolean; } & ( diff --git a/packages/connect/src/api/ethereum/ethereumDefinitions.ts b/packages/connect/src/api/ethereum/ethereumDefinitions.ts index 01881bc84734..9e5cde1bdb3e 100644 --- a/packages/connect/src/api/ethereum/ethereumDefinitions.ts +++ b/packages/connect/src/api/ethereum/ethereumDefinitions.ts @@ -5,7 +5,7 @@ import { trzd } from '@trezor/protocol'; import { Type, Static, Assert } from '@trezor/schema-utils'; import { DataManager } from '../../data/DataManager'; -import { EthereumNetworkInfo } from '../../types'; +import { EthereumNetworkInfoDefinitionValues } from '../../types'; import { ethereumNetworkInfoBase } from '../../data/coinInfo'; interface GetEthereumDefinitions { @@ -135,14 +135,10 @@ export const decodeEthereumDefinition = ( return decoded; }; -/** - * Converts protobuf decoded eth definitions to EthereumNetworkInfo type - */ export const ethereumNetworkInfoFromDefinition = ( definition: EthereumNetworkDefinitionDecoded, -): EthereumNetworkInfo => ({ +): EthereumNetworkInfoDefinitionValues => ({ ...ethereumNetworkInfoBase, - chainId: definition.chain_id, label: definition.name, name: definition.name, diff --git a/packages/connect/src/api/getAccountInfo.ts b/packages/connect/src/api/getAccountInfo.ts index b317bb6c7b21..099117af6172 100644 --- a/packages/connect/src/api/getAccountInfo.ts +++ b/packages/connect/src/api/getAccountInfo.ts @@ -1,11 +1,12 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/core/methods/GetAccountInfo.js +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; + import { AbstractMethod, MethodReturnType, DEFAULT_FIRMWARE_RANGE } from '../core/AbstractMethod'; import { Discovery } from './common/Discovery'; import { validateParams, getFirmwareRange } from './common/paramsValidator'; import { validatePath, getSerializedPath } from '../utils/pathUtils'; import { getAccountLabel, isUtxoBased } from '../utils/accountUtils'; -import { resolveAfter } from '../utils/promiseUtils'; import { getCoinInfo } from '../data/coinInfo'; import { PROTO, ERRORS } from '../constants'; import { UI, createUiMessage } from '../events'; diff --git a/packages/connect/src/backend/Blockchain.ts b/packages/connect/src/backend/Blockchain.ts index 6ce17fff5256..b3fe864bfb28 100644 --- a/packages/connect/src/backend/Blockchain.ts +++ b/packages/connect/src/backend/Blockchain.ts @@ -40,10 +40,6 @@ const getNormalizedTrezorShortcut = (shortcut: string) => { return 'XRP'; } - if (['OP', 'BASE'].includes(shortcut)) { - return 'ETH'; - } - return shortcut; }; @@ -132,10 +128,10 @@ export class Blockchain { this.serverInfo = info; - const trezorShortcut = getNormalizedTrezorShortcut(this.coinInfo.shortcut); - const backendShortcut = this.serverInfo.shortcut; + const trezorNetworkShortcut = getNormalizedTrezorShortcut(this.coinInfo.shortcut); + const backendNetworkShortcut = this.serverInfo.network; - if (trezorShortcut.toLowerCase() !== backendShortcut.toLowerCase()) { + if (backendNetworkShortcut.toLowerCase() !== trezorNetworkShortcut.toLowerCase()) { throw ERRORS.TypedError('Backend_Invalid'); } diff --git a/packages/connect/src/backend/__tests__/Blockchain.test.ts b/packages/connect/src/backend/__tests__/Blockchain.test.ts index 3d8e4086bd07..ae5cb66c31b0 100644 --- a/packages/connect/src/backend/__tests__/Blockchain.test.ts +++ b/packages/connect/src/backend/__tests__/Blockchain.test.ts @@ -42,7 +42,34 @@ describe('backend/Blockchain', () => { }); it('cache estimated fees (ethereum-like)', async () => { - const coinInfo = getEthereumNetwork('Ethereum'); + const coinInfo = getEthereumNetwork('ETH'); + if (!coinInfo) throw new Error('coinInfo is missing'); + + const spy = jest.spyOn(BlockchainLink.prototype, 'estimateFee'); + + const backend = await initBlockchain(coinInfo, () => {}); + + // blocks: 1 was not requested before + await backend.estimateFee({ blocks: [1] }); + expect(spy.mock.calls.length).toEqual(1); + + // blocks: 1 is requested again, returned from cache + await backend.estimateFee({ blocks: [1] }); + expect(spy.mock.calls.length).toEqual(1); + + // blocks: 2 was not requested before + await backend.estimateFee({ blocks: [1, 2] }); + expect(spy.mock.calls.length).toEqual(2); + + // request with "specific" field + await backend.estimateFee({ blocks: [1, 2], specific: { value: '0x0' } }); + expect(spy.mock.calls.length).toEqual(3); + + spy.mockClear(); + }); + + it('cache estimated fees (ethereum-like) - L2 network', async () => { + const coinInfo = getEthereumNetwork('BASE'); if (!coinInfo) throw new Error('coinInfo is missing'); const spy = jest.spyOn(BlockchainLink.prototype, 'estimateFee'); diff --git a/packages/connect/src/data/__tests__/defaultFeeLevels.test.ts b/packages/connect/src/data/__tests__/defaultFeeLevels.test.ts new file mode 100644 index 000000000000..5396b36d74cf --- /dev/null +++ b/packages/connect/src/data/__tests__/defaultFeeLevels.test.ts @@ -0,0 +1,90 @@ +import { getEthereumFeeLevels } from '../defaultFeeLevels'; + +describe('getEthereumFeeLevels', () => { + const fixtures = { + eth: { + defaultGas: 15, + minFee: 1, + maxFee: 10000, + expected: { + blockTime: -1, + defaultFees: [ + { + label: 'normal', + feePerUnit: '15000000000', // 15 Gwei * 1e9 = 15000000000 Wei + feeLimit: '21000', + blocks: -1, + }, + ], + minFee: 1, + maxFee: 10000, + dustLimit: -1, + }, + }, + pol: { + defaultGas: 200, + minFee: 1, + maxFee: 10000000, + expected: { + blockTime: -1, + defaultFees: [ + { + label: 'normal', + feePerUnit: '200000000000', // 200 Gwei * 1e9 = 200000000000 Wei + feeLimit: '21000', + blocks: -1, + }, + ], + minFee: 1, + maxFee: 10000000, + dustLimit: -1, + }, + }, + base: { + defaultGas: 0.01, + minFee: 0.000000001, + maxFee: 100, + expected: { + blockTime: -1, + defaultFees: [ + { + label: 'normal', + feePerUnit: '10000000', // 0.01 Gwei * 1e9 = 10000000 Wei + feeLimit: '21000', + blocks: -1, + }, + ], + minFee: 0.0000001, + maxFee: 1000, + dustLimit: -1, + }, + }, + unknown: { + defaultGas: 5, + minFee: 0.000000001, + maxFee: 10000, + expected: { + blockTime: -1, + defaultFees: [ + { + label: 'normal', + feePerUnit: '5000000000', // 5 Gwei * 1e9 = 5000000000 Wei + feeLimit: '21000', + blocks: -1, + }, + ], + minFee: 0.000000001, + maxFee: 10000, + dustLimit: -1, + }, + }, + }; + + Object.entries(fixtures).forEach(([chain, { expected }]) => { + it(`should return correct fee levels for ${chain}`, () => { + const result = getEthereumFeeLevels(chain); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/packages/connect/src/data/coinInfo.ts b/packages/connect/src/data/coinInfo.ts index 777a605cdb80..abc841b9375b 100644 --- a/packages/connect/src/data/coinInfo.ts +++ b/packages/connect/src/data/coinInfo.ts @@ -5,7 +5,6 @@ import { getBitcoinFeeLevels, getEthereumFeeLevels, getMiscFeeLevels } from './d import { ERRORS } from '../constants'; import { toHardened, fromHardened } from '../utils/pathUtils'; import type { - CoinInfo, BitcoinNetworkInfo, EthereumNetworkInfo, MiscNetworkInfo, @@ -33,16 +32,16 @@ export const getBitcoinNetwork = (pathOrName: DerivationPath) => { return networks.find(n => n.slip44 === slip44); }; -export const getEthereumNetwork = (pathOrName: DerivationPath) => { +export const getEthereumNetwork = (pathOrNetworkSymbol: DerivationPath) => { const networks = cloneObject(ethereumNetworks); - if (typeof pathOrName === 'string') { - const name = pathOrName.toLowerCase(); - return networks.find( - n => n.name.toLowerCase() === name || n.shortcut.toLowerCase() === name, - ); + if (typeof pathOrNetworkSymbol === 'string') { + const networkSymbol = pathOrNetworkSymbol.toLowerCase(); + + return networks.find(network => network.shortcut.toLowerCase() === networkSymbol); } - const slip44 = fromHardened(pathOrName[1]); + + const slip44 = fromHardened(pathOrNetworkSymbol[1]); return networks.find(n => n.slip44 === slip44); }; @@ -249,8 +248,7 @@ const parseBitcoinNetworksJson = (json: any) => { export const ethereumNetworkInfoBase = { type: 'ethereum' as const, - decimals: 16, - ...getEthereumFeeLevels(), + decimals: 18, }; const parseEthereumNetworksJson = (json: any) => { @@ -259,9 +257,10 @@ const parseEthereumNetworksJson = (json: any) => { ethereumNetworks.push({ ...ethereumNetworkInfoBase, + ...getEthereumFeeLevels(network.chain), blockchainLink: network.blockchain_link, chainId: network.chain_id, - label: network.name, + label: network.label, name: network.name, shortcut: network.shortcut, slip44: network.slip44, @@ -304,8 +303,8 @@ export const parseCoinsJson = (json: any) => { }); }; -export const getUniqueNetworks = (networks: (CoinInfo | undefined)[]) => - networks.reduce((result: CoinInfo[], info?: CoinInfo) => { +export const getUniqueNetworks = (networks: (T | undefined)[]) => + networks.reduce((result: T[], info?: T) => { if (!info || result.find(i => i.shortcut === info.shortcut)) return result; return result.concat(info); diff --git a/packages/connect/src/data/defaultFeeLevels.ts b/packages/connect/src/data/defaultFeeLevels.ts index 7a402fc0b2e1..5e14fe697c9f 100644 --- a/packages/connect/src/data/defaultFeeLevels.ts +++ b/packages/connect/src/data/defaultFeeLevels.ts @@ -1,3 +1,5 @@ +import { BigNumber } from '@trezor/utils'; + import { FeeLevel, FeeInfo } from '../types'; // this is workaround for the lack of information from 'trezor-common' @@ -18,6 +20,25 @@ const getDefaultBlocksForFeeLevel = (shortcut: string, label: string) => ? BLOCKS_FOR_FEE_LEVEL[shortcut][label] : -1; // -1 for unknown +const EVM_GAS_PRICE_PER_CHAIN_IN_GWEI: Record< + string, + { min: number; max: number; defaultGas: number } +> = { + eth: { min: 1, max: 10000, defaultGas: 15 }, + pol: { min: 1, max: 10000000, defaultGas: 200 }, + bsc: { min: 1, max: 100000, defaultGas: 3 }, + base: { min: 0.0000001, max: 1000, defaultGas: 0.01 }, + arb: { min: 0.001, max: 1000, defaultGas: 0.01 }, + op: { min: 0.000000001, max: 1000, defaultGas: 0.01 }, +}; + +const getEvmChainGweiGasPrice = (chain: string) => + EVM_GAS_PRICE_PER_CHAIN_IN_GWEI[chain] ?? { + min: 0.000000001, + max: 10000, + defaultGas: 5, + }; + // partial data from coins.jon interface CoinsJsonData { shortcut: string; // uppercase shortcut @@ -58,20 +79,24 @@ export const getBitcoinFeeLevels = (coin: CoinsJsonData): FeeInfoWithLevels => { }; }; -export const getEthereumFeeLevels = (): FeeInfoWithLevels => ({ - blockTime: -1, // unknown - defaultFees: [ - { - label: 'normal' as const, - feePerUnit: '5000000000', - feeLimit: '21000', // unlike the other networks ethereum have additional value "feeLimit" (Gas limit) - blocks: -1, // unknown - }, - ], - minFee: 1, - maxFee: 10000, - dustLimit: -1, // unknown/unused -}); +export const getEthereumFeeLevels = (chain: string): FeeInfoWithLevels => { + const { min, max, defaultGas } = getEvmChainGweiGasPrice(chain); + + return { + blockTime: -1, // unknown + defaultFees: [ + { + label: 'normal' as const, + feePerUnit: new BigNumber(defaultGas).multipliedBy('1e+9').toString(), // defined in wei 1 Gwei = 10^9 Wei + feeLimit: '21000', // default transfer gas limit + blocks: -1, // unknown + }, + ], + minFee: min, + maxFee: max, + dustLimit: -1, // unknown/unused + }; +}; const RIPPLE_FEE_INFO: FeeInfoWithLevels = { blockTime: -1, // unknown diff --git a/packages/connect/src/device/DeviceList.ts b/packages/connect/src/device/DeviceList.ts index 81e8e34bd7a2..aca31c954dd2 100644 --- a/packages/connect/src/device/DeviceList.ts +++ b/packages/connect/src/device/DeviceList.ts @@ -11,6 +11,7 @@ import { isTransportInstance, } from '@trezor/transport'; import { Descriptor, PathPublic } from '@trezor/transport/src/types'; +import { resolveAfter } from '@trezor/utils/src/resolveAfter'; import { ERRORS } from '../constants'; import { DEVICE, TransportInfo } from '../events'; @@ -23,7 +24,6 @@ import { } from '../types'; import { getBridgeInfo } from '../data/transportInfo'; import { initLog } from '../utils/debug'; -import { resolveAfter } from '../utils/promiseUtils'; import { typedObjectKeys } from '../types/utils'; // custom log diff --git a/packages/connect/src/device/__tests__/resolveDescriptorForTaproot.test.ts b/packages/connect/src/device/__tests__/resolveDescriptorForTaproot.test.ts index b7e2a936a0a2..4c3cc606504f 100644 --- a/packages/connect/src/device/__tests__/resolveDescriptorForTaproot.test.ts +++ b/packages/connect/src/device/__tests__/resolveDescriptorForTaproot.test.ts @@ -85,7 +85,7 @@ describe(resolveDescriptorForTaproot.name, () => { }); expect(response).toEqual({ - checksum: undefined, // code is defensive, it will work but it wont provide checksum + checksum: undefined, // code is defensive, it will work but it won't provide checksum xpub: "tr([71d98c03/86'/0'/0']xpub6CXYpDGLuWpjqFXRTbo8LMYVsiiRjwWiDY7iwDkq1mk4GDYE7TWmSBCnNmbcVYQK4T56RZRRwhCAG7ucTBHAG2rhWHpXdMQtkZVDeVuv33p/<0;1>/*)", }); }); diff --git a/packages/connect/src/device/resolveDescriptorForTaproot.ts b/packages/connect/src/device/resolveDescriptorForTaproot.ts index 28bb8ec2e745..67e28dd63189 100644 --- a/packages/connect/src/device/resolveDescriptorForTaproot.ts +++ b/packages/connect/src/device/resolveDescriptorForTaproot.ts @@ -1,32 +1,26 @@ import { MessagesSchema as Messages } from '@trezor/protobuf'; +import { convertTaprootXpub } from '@trezor/utils'; import { HDNodeResponse } from '../types/api/getPublicKey'; -interface Params { +interface ResolveDescriptorForTaprootParams { response: HDNodeResponse; publicKey: Messages.PublicKey; } -export const resolveDescriptorForTaproot = ({ response, publicKey }: Params) => { +export const resolveDescriptorForTaproot = ({ + response, + publicKey, +}: ResolveDescriptorForTaprootParams) => { if (publicKey.descriptor !== null && publicKey.descriptor !== undefined) { const [xpub, checksum] = publicKey.descriptor.split('#'); - // This is here to keep backwards compatibility, suite and blockbooks are still using `'` over `h` - const openingSquareBracketSplit = xpub.split('['); - if (openingSquareBracketSplit.length === 2) { - const [beforeOpeningBracket, afterOpeningBracket] = openingSquareBracketSplit; + // This is here to keep backwards compatibility, suite and block-books + // are still using `'` over `h`. + const correctedXpub = convertTaprootXpub({ xpub, direction: 'h-to-apostrophe' }); - const closingSquareBracketSplit = afterOpeningBracket.split(']'); - if (closingSquareBracketSplit.length === 2) { - const [path, afterClosingBracket] = closingSquareBracketSplit; - - const correctedPath = path.replace(/h/g, "'"); // .replaceAll() - - return { - xpub: `${beforeOpeningBracket}[${correctedPath}]${afterClosingBracket}`, - checksum, - }; - } + if (correctedXpub !== null) { + return { xpub: correctedXpub, checksum }; } } diff --git a/packages/connect/src/exports.ts b/packages/connect/src/exports.ts index cffd8891f832..4664f6bb43f1 100644 --- a/packages/connect/src/exports.ts +++ b/packages/connect/src/exports.ts @@ -3,3 +3,6 @@ export * from './events'; export * from './types'; export { parseConnectSettings } from './data/connectSettings'; + +// Do NOT add any code exports here. Only TrezorConnect and types shall be exported from +// `@trezor/connect` package. diff --git a/packages/connect/src/types/coinInfo.ts b/packages/connect/src/types/coinInfo.ts index d4a03c9e71c7..9fa3aaec12ec 100644 --- a/packages/connect/src/types/coinInfo.ts +++ b/packages/connect/src/types/coinInfo.ts @@ -94,6 +94,16 @@ export const EthereumNetworkInfo = Type.Intersect([ }), ]); +export type EthereumNetworkInfoDefinitionValues = Static< + typeof EthereumNetworkInfoDefinitionValues +>; +export const EthereumNetworkInfoDefinitionValues = Type.Omit(EthereumNetworkInfo, [ + 'minFee', + 'maxFee', + 'defaultFees', + 'blockTime', +]); + export type MiscNetworkInfo = Static; export const MiscNetworkInfo = Type.Intersect([ Common, diff --git a/packages/connect/src/utils/__tests__/deviceFeaturesUtils.test.ts b/packages/connect/src/utils/__tests__/deviceFeaturesUtils.test.ts index ae0147e1a753..319a27cd1c61 100644 --- a/packages/connect/src/utils/__tests__/deviceFeaturesUtils.test.ts +++ b/packages/connect/src/utils/__tests__/deviceFeaturesUtils.test.ts @@ -129,7 +129,9 @@ describe('utils/deviceFeaturesUtils', () => { expect(getUnavailableCapabilities(featT1B1, coins2)).toEqual({ ada: 'no-support', tada: 'no-support', - bnb: 'update-required', + bnb: 'no-support', + bsc: 'update-required', + arb: 'update-required', base: 'update-required', crw: 'update-required', eos: 'no-support', @@ -171,8 +173,9 @@ describe('utils/deviceFeaturesUtils', () => { expect(getUnavailableCapabilities(featT2T1, coins2)).toEqual({ replaceTransaction: 'update-required', amountUnit: 'update-required', + arb: 'update-required', base: 'update-required', - bnb: 'update-required', + bsc: 'update-required', decreaseOutput: 'update-required', eip1559: 'update-required', 'eip712-domain-only': 'update-required', @@ -278,7 +281,7 @@ describe('utils/deviceFeaturesUtils', () => { it('handles duplicated shortcuts correctly, ', () => { const customCoins = [ - { shortcut: 'BNB', type: 'ethereum', support: { T2T1: '2.4.4' } }, + { shortcut: 'BSC', type: 'ethereum', support: { T2T1: '2.4.4' } }, { shortcut: 'BNB', type: 'misc', support: { T2T1: '2.3.3' } }, { shortcut: 'ETH', type: 'ethereum', support: { T2T1: false } }, ]; @@ -294,7 +297,7 @@ describe('utils/deviceFeaturesUtils', () => { expect(result).toEqual({ eth: 'no-support', - bnb: 'update-required', + bsc: 'update-required', amountUnit: 'update-required', chunkify: 'update-required', coinjoin: 'update-required', @@ -307,9 +310,9 @@ describe('utils/deviceFeaturesUtils', () => { }); }); - it('handles duplicated shortcuts correctly, does not include bnb: no-support', () => { + it('handles duplicated shortcuts correctly, does not include bsc: no-support', () => { const customCoins = [ - { shortcut: 'BNB', type: 'ethereum', support: { T1B1: '1.1.3' } }, + { shortcut: 'BSC', type: 'ethereum', support: { T1B1: '1.1.3' } }, { shortcut: 'BNB', type: 'misc', support: { T1B1: false } }, { shortcut: 'ETH', type: 'ethereum', support: { T1B1: false } }, ]; @@ -325,6 +328,7 @@ describe('utils/deviceFeaturesUtils', () => { expect(result).toEqual({ eth: 'no-support', + bnb: 'no-support', amountUnit: 'update-required', chunkify: 'no-support', coinjoin: 'update-required', @@ -339,7 +343,7 @@ describe('utils/deviceFeaturesUtils', () => { it('handles duplicated shortcuts correctly, includes no-support because none is supported', () => { const customCoins = [ - { shortcut: 'BNB', type: 'ethereum', support: { T1B1: false } }, + { shortcut: 'BSC', type: 'ethereum', support: { T1B1: false } }, { shortcut: 'BNB', type: 'misc', support: { T1B1: false } }, { shortcut: 'ETH', type: 'ethereum', support: { T1B1: false } }, ]; @@ -356,6 +360,7 @@ describe('utils/deviceFeaturesUtils', () => { expect(result).toEqual({ eth: 'no-support', bnb: 'no-support', + bsc: 'no-support', amountUnit: 'update-required', chunkify: 'no-support', coinjoin: 'update-required', diff --git a/packages/connect/src/utils/deviceFeaturesUtils.ts b/packages/connect/src/utils/deviceFeaturesUtils.ts index 5b12b6db6bcc..dd1d03eb9918 100644 --- a/packages/connect/src/utils/deviceFeaturesUtils.ts +++ b/packages/connect/src/utils/deviceFeaturesUtils.ts @@ -47,8 +47,6 @@ export const getUnavailableCapabilities = (features: Features, coins: CoinInfo[] const fw = [features.major_version, features.minor_version, features.patch_version].join('.'); const key = features.internal_model; - const duplicatedShortcuts = ['bnb']; // relevant duplicated shortcuts from duplicity_overrides.json from fw repo - // 1. check if firmware version is supported by CoinInfo.support const supported = coins.filter(info => { // info.support[key] possible types: @@ -57,22 +55,17 @@ export const getUnavailableCapabilities = (features: Features, coins: CoinInfo[] // - string for supported models (version) if (!info.support || info.support[key] === false) { const shortcut = info.shortcut.toLowerCase(); - if (!duplicatedShortcuts.includes(shortcut)) { - list[shortcut] = 'no-support'; - - return false; - } else { - const occurrences = coins.filter(coin => shortcut == coin.shortcut.toLowerCase()); - const allUnsupported = occurrences.every( - info2 => !info2.support || info2.support[key] === false, - ); - if (allUnsupported) { - list[shortcut] = 'no-support'; - } + const occurrences = coins.filter(coin => shortcut == coin.shortcut.toLowerCase()); + const allUnsupported = occurrences.every( + info2 => !info2.support || info2.support[key] === false, + ); - return false; + if (allUnsupported) { + list[shortcut] = 'no-support'; } + + return false; } return true; diff --git a/packages/connect/src/utils/ethereumUtils.ts b/packages/connect/src/utils/ethereumUtils.ts index fecccde58a52..6e71adb18f8b 100644 --- a/packages/connect/src/utils/ethereumUtils.ts +++ b/packages/connect/src/utils/ethereumUtils.ts @@ -1,8 +1,11 @@ // origin: https://github.com/trezor/connect/blob/develop/src/js/utils/ethereumUtils.js -import type { CoinInfo } from '../types'; - -export const getNetworkLabel = (label: string, network?: CoinInfo) => { +export const getNetworkLabel = ( + label: string, + network?: { + name: string; + }, +) => { if (network) { const name = network.name.toLowerCase().indexOf('testnet') >= 0 ? 'Testnet' : network.name; diff --git a/packages/e2e-utils/src/fixtures/blockbook.ts b/packages/e2e-utils/src/fixtures/blockbook.ts index 64d9465646ab..80f47951ada4 100644 --- a/packages/e2e-utils/src/fixtures/blockbook.ts +++ b/packages/e2e-utils/src/fixtures/blockbook.ts @@ -3,6 +3,7 @@ export const blockbook = { data: { name: 'TestMock', shortcut: 'test', + network: 'test', decimals: 9, bestHeight: 1, }, diff --git a/packages/e2e-utils/src/fixtures/blockfrost.ts b/packages/e2e-utils/src/fixtures/blockfrost.ts index 3bd15305418e..4a8d90c7e852 100644 --- a/packages/e2e-utils/src/fixtures/blockfrost.ts +++ b/packages/e2e-utils/src/fixtures/blockfrost.ts @@ -3,6 +3,7 @@ export const blockfrost = { data: { name: 'BlockfrostMock', shortcut: 'ada', + network: 'ada', decimals: 6, testnet: false, version: '1.4.0', diff --git a/packages/eslint/src/typescriptConfig.mjs b/packages/eslint/src/typescriptConfig.mjs index da4bde7410f3..ac636859c7cc 100644 --- a/packages/eslint/src/typescriptConfig.mjs +++ b/packages/eslint/src/typescriptConfig.mjs @@ -48,7 +48,6 @@ export const typescriptConfig = [ }, ], '@typescript-eslint/no-empty-object-type': 'off', // Todo: we shall solve this, this is bad practice - '@typescript-eslint/triple-slash-reference': 'off', // Todo: solve before merge }, }, ]; diff --git a/packages/node-utils/src/checkSocks5Proxy.ts b/packages/node-utils/src/checkSocks5Proxy.ts new file mode 100644 index 000000000000..e6af1f4e138e --- /dev/null +++ b/packages/node-utils/src/checkSocks5Proxy.ts @@ -0,0 +1,35 @@ +import net from 'net'; + +export const checkSocks5Proxy = (host: string, port: number): Promise => { + return new Promise((resolve, reject) => { + const socket = new net.Socket(); + + socket.setTimeout(2_000); + + socket.on('connect', () => { + // Version 5, 1 method, no authentication + const handshakeRequest = Buffer.from([0x05, 0x01, 0x00]); + socket.write(handshakeRequest); + }); + + socket.on('data', data => { + if (data[0] === 0x05 && data[1] === 0x00) { + resolve(true); + } else { + resolve(false); + } + socket.destroy(); + }); + + socket.on('error', err => { + reject(err); + }); + + socket.on('timeout', () => { + socket.destroy(); + reject(new Error('Connection timed out')); + }); + + socket.connect(port, host); + }); +}; diff --git a/packages/node-utils/src/index.ts b/packages/node-utils/src/index.ts index 0201677b904a..951c43c5d5ec 100644 --- a/packages/node-utils/src/index.ts +++ b/packages/node-utils/src/index.ts @@ -12,3 +12,4 @@ export { type Response, } from './http'; export { checkFileExists } from './checkFileExists'; +export { checkSocks5Proxy } from './checkSocks5Proxy'; diff --git a/packages/node-utils/src/tests/checkSocks5Proxy.test.ts b/packages/node-utils/src/tests/checkSocks5Proxy.test.ts new file mode 100644 index 000000000000..7a90e28ae788 --- /dev/null +++ b/packages/node-utils/src/tests/checkSocks5Proxy.test.ts @@ -0,0 +1,62 @@ +import net from 'net'; + +import { checkSocks5Proxy } from '../checkSocks5Proxy'; + +jest.mock('net'); + +describe('checkSocks5Proxy', () => { + const host = '127.0.0.1'; + const port = 9050; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return true for a valid SOCKS5 proxy', async () => { + const mockSocket = { + connect: jest.fn(), + write: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'connect') { + callback(); + } + if (event === 'data') { + // Valid SOCKS5 response. + callback(Buffer.from([0x05, 0x00])); + } + }), + setTimeout: jest.fn(), + destroy: jest.fn(), + }; + + // @ts-expect-error + net.Socket.mockImplementation(() => mockSocket); + + const result = await checkSocks5Proxy(host, port); + expect(result).toBe(true); + }); + + it('should return false for an invalid SOCKS5 proxy', async () => { + const mockSocket = { + connect: jest.fn(), + write: jest.fn(), + on: jest.fn((event, callback) => { + if (event === 'connect') { + callback(); + } + if (event === 'data') { + // Not valid SOCKS5 response + callback(Buffer.from([0x05, 0x01])); + } + }), + setTimeout: jest.fn(), + destroy: jest.fn(), + }; + + // @ts-expect-error + net.Socket.mockImplementation(() => mockSocket); + + const result = await checkSocks5Proxy(host, port); + expect(result).toBe(false); + }); +}); diff --git a/packages/product-components/src/components/CoinLogo/coins.ts b/packages/product-components/src/components/CoinLogo/coins.ts index aa0fc7458748..787c4329ce5a 100644 --- a/packages/product-components/src/components/CoinLogo/coins.ts +++ b/packages/product-components/src/components/CoinLogo/coins.ts @@ -5,9 +5,10 @@ export type LegacyNetworkSymbol = 'eos' | 'nem' | 'xlm' | 'xtz'; export const COINS: Record = { ada: require('../../images/coins/ada.svg'), + arb: require('../../images/coins/arb.svg'), base: require('../../images/coins/base.svg'), bch: require('../../images/coins/bch.svg'), - bnb: require('../../images/coins/bnb.svg'), + bsc: require('../../images/coins/bsc.svg'), btc: require('../../images/coins/btc.svg'), btg: require('../../images/coins/btg.svg'), dash: require('../../images/coins/dash.svg'), diff --git a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDevice.tsx b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDevice.tsx index 15cbb77f71ba..6439bbf92e27 100644 --- a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDevice.tsx +++ b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDevice.tsx @@ -33,15 +33,13 @@ export const SLIDE_DOWN = keyframes` } `; -const Wrapper = styled.div<{ $animation?: AnimationDirection }>` - display: flex; - width: 300px; - height: 62px; - padding: 0 ${spacingsPx.md} 0 ${spacingsPx.xxl}; +const Wrapper = styled.div<{ $animation?: AnimationDirection; $isCancelable?: boolean }>` + padding: ${spacingsPx.sm} ${spacingsPx.sm} ${spacingsPx.sm} ${spacingsPx.xxl}; border-radius: ${borders.radii.full}; background: ${({ theme }) => theme.backgroundSurfaceElevation0}; box-shadow: ${({ theme }) => theme.boxShadowBase}; - align-items: center; + + ${({ $isCancelable }) => !$isCancelable && `padding-right: ${spacingsPx.xxl};`} ${({ $animation }) => $animation === AnimationDirection.Up && @@ -70,6 +68,7 @@ export interface ConfirmOnDeviceProps { export const ConfirmOnDevice = ({ isConfirmed, ...rest }: ConfirmOnDeviceProps) => ( e.stopPropagation()} > diff --git a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx index f3f09898b19e..fa9086e37e1b 100644 --- a/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx +++ b/packages/product-components/src/components/ConfirmOnDevice/ConfirmOnDeviceContent.tsx @@ -1,82 +1,17 @@ import { ReactNode } from 'react'; -import styled, { css, useTheme } from 'styled-components'; +import styled, { css } from 'styled-components'; -import { Icon, useElevation } from '@trezor/components'; +import { Column, Row, IconButton, Text } from '@trezor/components'; import { DeviceModelInternal } from '@trezor/connect'; -import { - Elevation, - borders, - mapElevationToBackground, - spacingsPx, - typography, -} from '@trezor/theme'; +import { borders, spacings, spacingsPx } from '@trezor/theme'; import { RotateDeviceImage } from '../RotateDeviceImage/RotateDeviceImage'; -const Column = styled.div` - display: flex; -`; - -const Title = styled.div` - display: flex; - justify-content: center; - ${typography.body}; - color: ${({ theme }) => theme.textDefault}; -`; - -const Left = styled(Column)``; - -const Middle = styled(Column)` - flex: 1; - justify-content: center; - flex-direction: column; -`; - -const Right = styled(Column)``; - -const Steps = styled.div` - display: flex; - margin-top: ${spacingsPx.sm}; - max-width: 200px; - padding: 0 ${spacingsPx.sm}; - justify-content: center; -`; - -const CloseWrapper = styled.div` - margin-left: ${spacingsPx.xs}; -`; - -const Close = styled.div<{ $elevation: Elevation }>` - border-radius: 100%; - cursor: pointer; - background: ${mapElevationToBackground}; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - transition: opacity 0.1s; - - :hover { - opacity: 0.7; - } -`; - -const Success = styled.div` - display: flex; - flex: 1; - ${typography.callout} - color: ${({ theme }) => theme.textPrimaryDefault}; - text-align: center; - justify-content: center; -`; - const Step = styled.div<{ $isActive: boolean }>` - width: 18px; - height: 4px; + flex: 1; + height: ${spacingsPx.xxs}; border-radius: ${borders.radii.xxs}; - margin-right: ${spacingsPx.xxs}; background: ${({ theme }) => theme.backgroundNeutralSubdued}; ${({ $isActive }) => @@ -86,10 +21,6 @@ const Step = styled.div<{ $isActive: boolean }>` `} `; -const StyledRotateDeviceImage = styled(RotateDeviceImage)` - height: 34px; -`; - const isStepActive = (index: number, activeStep?: number) => { if (!activeStep) { return false; @@ -114,39 +45,38 @@ export interface ConfirmOnDeviceProps { export const ConfirmOnDeviceContent = ({ title, - steps, + steps = 3, activeStep, onCancel, successText, deviceModelInternal, deviceUnitColor, }: ConfirmOnDeviceProps) => { - const { elevation } = useElevation(); const hasSteps = steps && activeStep !== undefined; - const theme = useTheme(); return ( - <> - - - + + - - {title} + + {title} {successText && hasSteps && activeStep > steps && ( - + {successText} - + )} {hasSteps && activeStep <= steps && ( - + {Array.from(Array(steps).keys()).map((step, index) => ( ))} - + )} - - - - - {onCancel && ( - - - - )} - - - + + + {onCancel && ( + + )} + ); }; diff --git a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx index 7c6407a3dc0b..e9f49c0014bd 100644 --- a/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx +++ b/packages/product-components/src/components/RotateDeviceImage/RotateDeviceImage.tsx @@ -48,7 +48,13 @@ export const RotateDeviceImage = ({ width={animationWidth} /> ) : ( - + )} ); diff --git a/packages/product-components/src/components/SelectAssetModal/AssetItem.tsx b/packages/product-components/src/components/SelectAssetModal/AssetItem.tsx index b05596072c52..6ec66cbb9da3 100644 --- a/packages/product-components/src/components/SelectAssetModal/AssetItem.tsx +++ b/packages/product-components/src/components/SelectAssetModal/AssetItem.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { spacings, spacingsPx } from '@trezor/theme'; import { AssetLogo, Badge, Column, Row, Text } from '@trezor/components'; import { getContractAddressForNetworkSymbol } from '@suite-common/wallet-utils'; +import { getNetworkDisplaySymbol, isNetworkSymbol } from '@suite-common/wallet-config'; import { CoinLogo } from '../CoinLogo/CoinLogo'; import { AssetOptionBaseProps } from './SelectAssetModal'; @@ -43,6 +44,9 @@ export const AssetItem = ({ }: AssetItemProps) => { const getCoinLogo = () => isCoinSymbol(symbol) ? : null; + const displaySymbol = isNetworkSymbol(ticker) + ? getNetworkDisplaySymbol(ticker) + : ticker.toUpperCase(); return ( ) : ( @@ -86,7 +90,7 @@ export const AssetItem = ({ )} - {ticker.toUpperCase()} + {displaySymbol} diff --git a/packages/product-components/src/components/SelectAssetModal/NetworkTabs.tsx b/packages/product-components/src/components/SelectAssetModal/NetworkTabs.tsx index 724e8543a211..6c7324c649e6 100644 --- a/packages/product-components/src/components/SelectAssetModal/NetworkTabs.tsx +++ b/packages/product-components/src/components/SelectAssetModal/NetworkTabs.tsx @@ -4,7 +4,11 @@ import styled from 'styled-components'; import { AssetLogo, Row, Tooltip, useElevation } from '@trezor/components'; import { Elevation, mapElevationToBorder, spacings, spacingsPx } from '@trezor/theme'; -import { Network } from '@suite-common/wallet-config'; +import { + getNetworkDisplaySymbol, + type NetworkSymbol, + type Network, +} from '@suite-common/wallet-config'; import { CheckableTag } from './CheckableTag'; @@ -18,7 +22,7 @@ const NetworkTabsWrapper = styled.div<{ $elevation: Elevation }>` export type NetworkFilterCategory = { name: Network['name']; - symbol: Network['symbol']; + symbol: NetworkSymbol; coingeckoId: Network['coingeckoId']; coingeckoNativeId?: Network['coingeckoNativeId']; }; @@ -95,7 +99,7 @@ export const NetworkTabs = ({ tabs, activeTab, setActiveTab, networkCount }: Net )} {network.name} diff --git a/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.storiesData.ts b/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.storiesData.ts index 434614962fce..24cf6b33b32f 100644 --- a/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.storiesData.ts +++ b/packages/product-components/src/components/SelectAssetModal/SelectAssetModal.storiesData.ts @@ -474,7 +474,7 @@ export const selectAssetModalNetworks: NetworkFilterCategory[] = [ }, { name: 'BNB Smart Chain', - symbol: 'bnb', + symbol: 'bsc', coingeckoId: 'binance-smart-chain', coingeckoNativeId: 'binancecoin', }, diff --git a/packages/product-components/src/images/coins/arb.svg b/packages/product-components/src/images/coins/arb.svg new file mode 100644 index 000000000000..b4f0d1cfa97c --- /dev/null +++ b/packages/product-components/src/images/coins/arb.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/product-components/src/images/coins/bnb.svg b/packages/product-components/src/images/coins/bsc.svg similarity index 100% rename from packages/product-components/src/images/coins/bnb.svg rename to packages/product-components/src/images/coins/bsc.svg diff --git a/packages/request-manager/e2e/identities-stress.ts b/packages/request-manager/e2e/identities-stress.ts index 5f1289c8c53e..faad13c90c0a 100644 --- a/packages/request-manager/e2e/identities-stress.ts +++ b/packages/request-manager/e2e/identities-stress.ts @@ -37,9 +37,8 @@ const intervalBetweenRequests = 1000 * 20; port, controlPort, torDataDir, - snowflakeBinaryPath: '', }); - const torParams = await torController.getTorConfiguration(processId); + const torParams = torController.getTorConfiguration(processId); // Starting Tor process from binary. torRunner({ torParams, diff --git a/packages/request-manager/e2e/interceptor.test.ts b/packages/request-manager/e2e/interceptor.test.ts index dd12649c1412..5186a783a831 100644 --- a/packages/request-manager/e2e/interceptor.test.ts +++ b/packages/request-manager/e2e/interceptor.test.ts @@ -10,7 +10,6 @@ const hostIp = '127.0.0.1'; const port = 38835; const controlPort = 35527; const processId = process.pid; -const snowflakeBinaryPath = ''; // 1 minute before timeout, because Tor might be slow to start. jest.setTimeout(60000); @@ -29,7 +28,7 @@ describe('Interceptor', () => { let torController: TorController; let torIdentities: TorIdentities; - const torSettings = { running: true, host: hostIp, port, snowflakeBinaryPath }; + const torSettings = { running: true, host: hostIp, port }; const INTERCEPTOR = { handler: () => {}, @@ -45,9 +44,8 @@ describe('Interceptor', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }); - const torParams = await torController.getTorConfiguration(processId); + const torParams = torController.getTorConfiguration(processId); // Starting Tor process from binary. torProcess = torRunner({ torParams, diff --git a/packages/request-manager/e2e/torControlPort.test.ts b/packages/request-manager/e2e/torControlPort.test.ts index d341d062f13e..864764c11ad0 100644 --- a/packages/request-manager/e2e/torControlPort.test.ts +++ b/packages/request-manager/e2e/torControlPort.test.ts @@ -18,7 +18,6 @@ const controlAuthCookiePath = `${torDataDir}/control_auth_cookie`; const host = 'localhost'; const port = 9998; const controlPort = 9999; -const snowflakeBinaryPath = ''; describe('TorControlPort', () => { beforeAll(async () => { @@ -40,7 +39,6 @@ describe('TorControlPort', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }; const fakeListener = () => {}; const torControlPort = new TorControlPort(options, fakeListener); @@ -105,7 +103,6 @@ describe('TorControlPort', () => { port, controlPort, torDataDir, - snowflakeBinaryPath, }; const fakeListener = () => {}; const torControlPort = new TorControlPort(options, fakeListener); diff --git a/packages/request-manager/src/controller.ts b/packages/request-manager/src/controller.ts index ac42aeb48c85..ee58e4af0bf1 100644 --- a/packages/request-manager/src/controller.ts +++ b/packages/request-manager/src/controller.ts @@ -1,8 +1,7 @@ import { EventEmitter } from 'events'; import path from 'path'; -import { createTimeoutPromise } from '@trezor/utils'; -import { checkFileExists } from '@trezor/node-utils'; +import { ScheduleActionParams, ScheduledAction, scheduleAction } from '@trezor/utils'; import { TorControlPort } from './torControlPort'; import { @@ -13,15 +12,15 @@ import { } from './types'; import { bootstrapParser, BOOTSTRAP_EVENT_PROGRESS } from './events/bootstrap'; +const WAITING_TIME = 1000; +const MAX_TRIES_WAITING = 200; +const BOOTSTRAP_SLOW_TRESHOLD = 1000 * 5; // 5 seconds. + export class TorController extends EventEmitter { options: TorConnectionOptions; controlPort: TorControlPort; bootstrapSlownessChecker?: NodeJS.Timeout; status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped; - // Configurations - waitingTime = 1000; - maxTriesWaiting = 200; - bootstrapSlowThreshold = 1000 * 5; // 5 seconds. constructor(options: TorConnectionOptions) { super(); @@ -57,23 +56,37 @@ export class TorController extends EventEmitter { if (this.bootstrapSlownessChecker) { clearTimeout(this.bootstrapSlownessChecker); } - // When Bootstrap starts we wait time defined in bootstrapSlowThreshold and if after that time, + // When Bootstrap starts we wait time defined in BOOTSTRAP_SLOW_TRESHOLD and if after that time, // it has not being finalized, then we send slow event. We know that Bootstrap is going on since // we received, at least, first Bootstrap events from ControlPort. this.bootstrapSlownessChecker = setTimeout(() => { this.emit('bootstrap/event', { type: 'slow', }); - }, this.bootstrapSlowThreshold); + }, BOOTSTRAP_SLOW_TRESHOLD); } - public async getTorConfiguration( - processId: number, - snowflakeBinaryPath?: string, - ): Promise { + private onMessageReceived(message: string) { + const bootstrap: BootstrapEvent[] = bootstrapParser(message); + bootstrap.forEach(event => { + if (event.type !== 'progress') return; + if (event.progress && !this.getIsBootstrapping()) { + // We consider that bootstrap has started when we receive any bootstrap event and + // Tor is not bootstrapping yet. + // If we do not receive any bootstrapping event, we can consider there is something going wrong and + // an error will be thrown when `MAX_TRIES_WAITING` is reached in `waitUntilAlive`. + this.startBootstrap(); + } + if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) { + this.successfullyBootstrapped(); + } + this.emit('bootstrap/event', event); + }); + } + + public getTorConfiguration(processId: number): string[] { const { torDataDir } = this.options; const controlAuthCookiePath = path.join(torDataDir, 'control_auth_cookie'); - const snowflakeLogPath = path.join(torDataDir, 'snowflake.log'); // https://github.com/torproject/tor/blob/bf30943cb75911d70367106af644d4273baaa85d/doc/man/tor.1.txt const config: string[] = [ @@ -136,96 +149,31 @@ export class TorController extends EventEmitter { this.options.torDataDir, ]; - let existsSnowflakeBinary = false; - if (snowflakeBinaryPath && snowflakeBinaryPath.trim() !== '') { - // If provided snowflake file does not exists, do not use it. - existsSnowflakeBinary = await checkFileExists(snowflakeBinaryPath); - } - - if (existsSnowflakeBinary) { - // Snowflake is a WebRTC pluggable transport for Tor (client) - // More info: - // https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/tree/main/client - // https://packages.debian.org/bookworm/snowflake-client - - const SNOWFLAKE_PLUGIN = 'snowflake exec'; - const SNOWFLAKE_SERVER = 'snowflake 192.0.2.3:80'; - const SNOWFLAKE_FINGERPRINT = '2B280B23E1107BB62ABFC40DDCC8824814F80A72'; - const SNOWFLAKE_URL = 'https://snowflake-broker.torproject.net.global.prod.fastly.net/'; - const SNOWFLAKE_FRONT = 'fronts=foursquare.com,github.githubassets.com'; - const SNOWFLAKE_ICE = - 'ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478'; - const SNOWFLAKE_UTLS = 'utls-imitate=hellorandomizedalpn'; - - const snowflakeCommand = `${SNOWFLAKE_PLUGIN} ${snowflakeBinaryPath} -log ${snowflakeLogPath}`; - const snowflakeBridge = `${SNOWFLAKE_SERVER} ${SNOWFLAKE_FINGERPRINT} fingerprint=${SNOWFLAKE_FINGERPRINT} url=${SNOWFLAKE_URL} ${SNOWFLAKE_FRONT} ${SNOWFLAKE_ICE} ${SNOWFLAKE_UTLS}`; - - config.push( - '--UseBridges', - '1', - '--ClientTransportPlugin', - snowflakeCommand, - '--Bridge', - snowflakeBridge, - ); - } - return config; } - public onMessageReceived(message: string) { - const bootstrap: BootstrapEvent[] = bootstrapParser(message); - bootstrap.forEach(event => { - if (event.type !== 'progress') return; - if (event.progress && !this.getIsBootstrapping()) { - // We consider that bootstrap has started when we receive any bootstrap event and - // Tor is not bootstrapping yet. - // If we do not receive any bootstrapping event, we can consider there is something going wrong and - // an error will be thrown when `maxTriesWaiting` is reached in `waitUntilAlive`. - this.startBootstrap(); - } - if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) { - this.successfullyBootstrapped(); - } - this.emit('bootstrap/event', event); - }); - } - - public waitUntilAlive(): Promise { - const errorMessages: string[] = []; + public async waitUntilAlive(): Promise { this.status = TOR_CONTROLLER_STATUS.Bootstrapping; - const waitUntilResponse = async (triesCount: number): Promise => { - if (this.getIsStopped()) { - // If TOR is starting and we want to cancel it. - return; - } - if (triesCount >= this.maxTriesWaiting) { - throw new Error( - `Timeout waiting for TOR control port: \n${errorMessages.join('\n')}`, - ); - } - try { - const isConnected = await this.controlPort.connect(); - const isAlive = this.controlPort.ping(); - if (isConnected && isAlive && this.getIsCircuitEstablished()) { - // It is running so let's not wait anymore. - return; - } - } catch (error) { - // Some error here is expected when waiting but - // we do not want to throw until maxTriesWaiting is reach. - // Instead we want to log it to know what causes the error. - if (error && error.message) { - console.warn('request-manager:', error.message); - errorMessages.push(error.message); - } + + const checkConnection: ScheduledAction = async () => { + if (this.getIsStopped()) return false; + const isConnected = await this.controlPort.connect(); + const isAlive = this.controlPort.ping(); + const isCircuitEstablished = this.getIsCircuitEstablished(); + // It is running so let's not wait anymore. + if (isConnected && isAlive && isCircuitEstablished) { + return true; } - await createTimeoutPromise(this.waitingTime); + throw new Error('Tor not alive'); + }; - return waitUntilResponse(triesCount + 1); + const params: ScheduleActionParams = { + attempts: MAX_TRIES_WAITING, + timeout: WAITING_TIME, + gap: WAITING_TIME, }; - return waitUntilResponse(1); + await scheduleAction(checkConnection, params); } public getStatus(): Promise { diff --git a/packages/request-manager/src/controllerExternal.ts b/packages/request-manager/src/controllerExternal.ts new file mode 100644 index 000000000000..e020e3e5f6c6 --- /dev/null +++ b/packages/request-manager/src/controllerExternal.ts @@ -0,0 +1,87 @@ +import { EventEmitter } from 'events'; + +import { ScheduleActionParams, ScheduledAction, scheduleAction } from '@trezor/utils'; +import { checkSocks5Proxy } from '@trezor/node-utils'; + +import { TOR_CONTROLLER_STATUS, TorControllerStatus, TorExternalConnectionOptions } from './types'; + +const WAITING_TIME = 1_000; +const MAX_TRIES_WAITING = 200; + +export class TorControllerExternal extends EventEmitter { + status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped; + options: TorExternalConnectionOptions; + + constructor(options: TorExternalConnectionOptions) { + super(); + this.options = options; + } + + private getIsStopped() { + return this.status === TOR_CONTROLLER_STATUS.Stopped; + } + + private async getIsExternalTorRunning() { + let isSocks5ProxyPort = false; + try { + isSocks5ProxyPort = await checkSocks5Proxy(this.options.host, this.options.port); + } catch { + // Ignore errors. + } + + return isSocks5ProxyPort; + } + + private startBootstrap() { + this.status = TOR_CONTROLLER_STATUS.Bootstrapping; + } + + private successfullyBootstrapped() { + this.status = TOR_CONTROLLER_STATUS.ExternalTorRunning; + } + + public getTorConfiguration() { + return ''; + } + + public async waitUntilAlive() { + this.startBootstrap(); + + const checkConnection: ScheduledAction = async () => { + if (this.getIsStopped()) return false; + const isRunning = await this.getIsExternalTorRunning(); + if (isRunning) { + this.successfullyBootstrapped(); + + return true; + } + + throw new Error('Tor external not alive'); + }; + + const params: ScheduleActionParams = { + attempts: MAX_TRIES_WAITING, + timeout: WAITING_TIME, + gap: WAITING_TIME, + }; + + await scheduleAction(checkConnection, params); + } + + public async getStatus() { + const isExternalTorRunning = await this.getIsExternalTorRunning(); + if (isExternalTorRunning) { + return TOR_CONTROLLER_STATUS.ExternalTorRunning; + } + + return TOR_CONTROLLER_STATUS.Stopped; + } + + public closeActiveCircuits() { + // Do nothing. Not possible in External Tor without ControlPort. + } + + public stop() { + this.status = TOR_CONTROLLER_STATUS.Stopped; + } +} diff --git a/packages/request-manager/src/index.ts b/packages/request-manager/src/index.ts index aef9257d3391..2e198e5fe49c 100644 --- a/packages/request-manager/src/index.ts +++ b/packages/request-manager/src/index.ts @@ -1,4 +1,5 @@ export { TorController } from './controller'; +export { TorControllerExternal } from './controllerExternal'; export { createInterceptor } from './interceptor'; export type { InterceptedEvent, BootstrapEvent, TorControllerStatus } from './types'; export { TOR_CONTROLLER_STATUS } from './types'; diff --git a/packages/request-manager/src/types.ts b/packages/request-manager/src/types.ts index ac9fff309854..052a84251d8f 100644 --- a/packages/request-manager/src/types.ts +++ b/packages/request-manager/src/types.ts @@ -3,7 +3,11 @@ export interface TorConnectionOptions { port: number; controlPort: number; torDataDir: string; - snowflakeBinaryPath: string; +} + +export interface TorExternalConnectionOptions { + host: string; + port: number; } export type TorCommandResponse = @@ -72,5 +76,6 @@ export const TOR_CONTROLLER_STATUS = { Bootstrapping: 'Bootstrapping', Stopped: 'Stopped', CircuitEstablished: 'CircuitEstablished', + ExternalTorRunning: 'ExternalTorRunning', } as const; export type TorControllerStatus = keyof typeof TOR_CONTROLLER_STATUS; diff --git a/packages/suite-data/files/translations/en.json b/packages/suite-data/files/translations/en.json index 06407c527fd4..5192aaa2c161 100644 --- a/packages/suite-data/files/translations/en.json +++ b/packages/suite-data/files/translations/en.json @@ -1709,14 +1709,14 @@ "TR_SOUTH": "South", "TR_STAKE_ACKNOWLEDGE_ENTRY_PERIOD": "I acknowledge the above entry period", "TR_STAKE_ADDING_TO_POOL": "Adding to staking pool", - "TR_STAKE_ANY_AMOUNT_ETH": "Stake a minimum amount of {amount} {symbol} and start earning rewards. With our current APY rate of {apyPercent}%, your rewards earn too!", + "TR_STAKE_ANY_AMOUNT_ETH": "Stake a minimum amount of {amount} {networkSymbol} and start earning rewards. With our current APY rate of {apyPercent}%, your rewards earn too!", "TR_STAKE_APY": "Annual Percentage Yield", "TR_STAKE_APY_ABBR": "APY", "TR_STAKE_APY_DESC": "*Annual Percentage Yield", "TR_STAKE_AVAILABLE": "Available", "TR_STAKE_CAN_CLAIM_WARNING": "You can already claim {amount} {symbol}. {br}Claim now or wait until new unstake is processed.", "TR_STAKE_CLAIM": "Claim", - "TR_STAKE_CLAIMED_AMOUNT_TRANSFERRED": "The claimed amount is transferred to your {symbol} account.", + "TR_STAKE_CLAIMED_AMOUNT_TRANSFERRED": "The claimed amount is transferred to your {networkSymbol} account.", "TR_STAKE_CLAIMING_PERIOD": "Claiming period", "TR_STAKE_CLAIM_AFTER_UNSTAKING": "You can claim once the unstaking period is complete.", "TR_STAKE_CLAIM_IN_NEXT_BLOCK": "in the next block", @@ -1804,7 +1804,7 @@ "TR_STAKING_DEPOSIT_FEE_DECRIPTION": "The deposit fee is {feeAmount} ADA and is required to register your address to start staking. If you choose to unstake your Cardano you will get the deposit back.", "TR_STAKING_ESTIMATED_GAINS": "Estimated gains", "TR_STAKING_FEE": "Fee", - "TR_STAKING_GETTING_READY": "Your {symbol} is getting ready to work", + "TR_STAKING_GETTING_READY": "Your {networkSymbol} is getting ready to work", "TR_STAKING_INSTANTLY_STAKED": "You've instantly staked {amount} {symbol}. {days, plural, =0 {} one {The remaining {symbol} will be staked within # day.} other { The remaining {symbol} will be staked within # days}}", "TR_STAKING_IS_NOT_SUPPORTED": "Staking is not supported on this network.", "TR_STAKING_NOT_ENOUGH_FUNDS": "You don't have enough funds on your account.", diff --git a/packages/suite-desktop-api/src/factory.ts b/packages/suite-desktop-api/src/factory.ts index 17269e44fa17..baa14bfd23fb 100644 --- a/packages/suite-desktop-api/src/factory.ts +++ b/packages/suite-desktop-api/src/factory.ts @@ -125,7 +125,7 @@ export const factory = >( getTorSettings: () => ipcRenderer.invoke('tor/get-settings'), changeTorSettings: payload => { - if (validation.isObject({ snowflakeBinaryPath: 'string' }, payload)) { + if (validation.isObject({ useExternalTor: 'boolean' }, payload)) { return ipcRenderer.invoke('tor/change-settings', payload); } diff --git a/packages/suite-desktop-api/src/messages.ts b/packages/suite-desktop-api/src/messages.ts index 6eeb54c95628..b8e5bc6cf2e1 100644 --- a/packages/suite-desktop-api/src/messages.ts +++ b/packages/suite-desktop-api/src/messages.ts @@ -54,7 +54,7 @@ export type HandshakeTorModule = { }; export type TorSettings = { - snowflakeBinaryPath: string; + useExternalTor: boolean; }; export type TraySettings = { diff --git a/packages/suite-desktop-connect-popup/package.json b/packages/suite-desktop-connect-popup/package.json new file mode 100644 index 000000000000..07c2e5002119 --- /dev/null +++ b/packages/suite-desktop-connect-popup/package.json @@ -0,0 +1,27 @@ +{ + "name": "@trezor/suite-desktop-connect-popup", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "type-check": "yarn g:tsc --build tsconfig.json" + }, + "dependencies": { + "@reduxjs/toolkit": "1.9.5", + "@suite-common/redux-utils": "workspace:*", + "@suite-common/suite-types": "workspace:*", + "@suite-common/test-utils": "workspace:*", + "@suite-common/wallet-core": "workspace:*", + "@trezor/connect": "workspace:*", + "@trezor/env-utils": "workspace:^", + "@trezor/suite-desktop-api": "workspace:*", + "@trezor/urls": "workspace:*", + "@trezor/utils": "workspace:*" + }, + "devDependencies": { + "redux-mock-store": "^1.5.4", + "redux-thunk": "^2.4.2" + } +} diff --git a/packages/suite-desktop-connect-popup/redux.d.ts b/packages/suite-desktop-connect-popup/redux.d.ts new file mode 100644 index 000000000000..df9a0c3f969a --- /dev/null +++ b/packages/suite-desktop-connect-popup/redux.d.ts @@ -0,0 +1,7 @@ +import { AsyncThunkAction } from '@reduxjs/toolkit'; + +declare module 'redux' { + export interface Dispatch { + >(thunk: TThunk): ReturnType; + } +} diff --git a/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts b/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts new file mode 100644 index 000000000000..fb93d6a1f189 --- /dev/null +++ b/packages/suite-desktop-connect-popup/src/connectPopupThunks.ts @@ -0,0 +1,99 @@ +import { createThunk } from '@suite-common/redux-utils'; +import TrezorConnect, { ERRORS } from '@trezor/connect'; +import { createDeferred } from '@trezor/utils'; +import { selectSelectedDevice } from '@suite-common/wallet-core'; +import { desktopApi } from '@trezor/suite-desktop-api'; +import { serializeError } from '@trezor/connect/src/constants/errors'; + +const CONNECT_POPUP_MODULE = '@common/connect-popup'; + +export const connectPopupCallThunk = createThunk( + `${CONNECT_POPUP_MODULE}/callThunk`, + async ( + { + id, + method, + payload, + processName, + origin, + }: { + id: number; + method: string; + payload: any; + processName?: string; + origin?: string; + }, + { dispatch, getState, extra }, + ) => { + try { + const device = selectSelectedDevice(getState()); + + if (!device) { + console.error('Device not found'); + + // TODO: wait for device selection and continue + throw ERRORS.TypedError('Device_NotFound'); + } + + // @ts-expect-error: method is dynamic + const methodInfo = await TrezorConnect[method]({ + ...payload, + __info: true, + }); + if (!methodInfo.success) { + throw methodInfo; + } + + const confirmation = createDeferred(); + dispatch(extra.actions.lockDevice(true)); + dispatch( + extra.actions.openModal({ + type: 'connect-popup', + onCancel: () => confirmation.reject(ERRORS.TypedError('Method_Cancel')), + onConfirm: () => confirmation.resolve(), + method: methodInfo.payload.info, + processName, + origin, + }), + ); + await confirmation.promise; + dispatch(extra.actions.lockDevice(false)); + + // @ts-expect-error: method is dynamic + const response = await TrezorConnect[method]({ + device: { + path: device.path, + instance: device.instance, + state: device.state, + }, + ...payload, + }); + + dispatch(extra.actions.onModalCancel()); + + desktopApi.connectPopupResponse({ + ...response, + id, + }); + } catch (error) { + console.error('connectPopupCallThunk', error); + desktopApi.connectPopupResponse({ + success: false, + payload: serializeError(error), + id, + }); + } + }, +); + +export const connectPopupInitThunk = createThunk( + `${CONNECT_POPUP_MODULE}/initPopupThunk`, + async (_, { dispatch }) => { + if (desktopApi.available && (await desktopApi.connectPopupEnabled())) { + desktopApi.on('connect-popup/call', params => { + dispatch(connectPopupCallThunk(params)); + }); + desktopApi.connectPopupReady(); + } + }, +); diff --git a/packages/suite-desktop-connect-popup/src/index.ts b/packages/suite-desktop-connect-popup/src/index.ts new file mode 100644 index 000000000000..e56779802e7a --- /dev/null +++ b/packages/suite-desktop-connect-popup/src/index.ts @@ -0,0 +1 @@ +export * from './connectPopupThunks'; diff --git a/packages/suite-desktop-connect-popup/tsconfig.json b/packages/suite-desktop-connect-popup/tsconfig.json new file mode 100644 index 000000000000..9de034a7f700 --- /dev/null +++ b/packages/suite-desktop-connect-popup/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { + "path": "../../suite-common/redux-utils" + }, + { + "path": "../../suite-common/suite-types" + }, + { + "path": "../../suite-common/test-utils" + }, + { + "path": "../../suite-common/wallet-core" + }, + { "path": "../connect" }, + { "path": "../env-utils" }, + { "path": "../suite-desktop-api" }, + { "path": "../urls" }, + { "path": "../utils" } + ] +} diff --git a/packages/suite-desktop-core/e2e/playwright.config.ts b/packages/suite-desktop-core/e2e/playwright.config.ts index a29755e82fdb..9ada29d527eb 100644 --- a/packages/suite-desktop-core/e2e/playwright.config.ts +++ b/packages/suite-desktop-core/e2e/playwright.config.ts @@ -5,8 +5,8 @@ export enum PlaywrightProjects { Web = 'web', Desktop = 'desktop', } -const timeoutCIRun = 1000 * 60; -const timeoutLocalRun = 1000 * 30; +const timeoutCIRun = 1000 * 180; +const timeoutLocalRun = 1000 * 60; const config: PlaywrightTestConfig = { projects: [ @@ -17,16 +17,21 @@ const config: PlaywrightTestConfig = { baseURL: process.env.BASE_URL || 'http://localhost:8000/', }, grepInvert: /@desktopOnly/, + //TODO: #16073 Instability on Web tests. Once solved, remove ignoreSnapshots + ignoreSnapshots: true, }, { name: PlaywrightProjects.Desktop, use: {}, grepInvert: /@webOnly/, + //TODO: #16073 We cannot set resolution for Electron. Once solved, remove ignoreSnapshots + ignoreSnapshots: true, }, ], testDir: 'tests', workers: 1, // to disable parallelism between test files use: { + viewport: { width: 1280, height: 720 }, headless: process.env.HEADLESS === 'true', trace: 'on', video: 'on', @@ -39,6 +44,12 @@ const config: PlaywrightTestConfig = { : [['list'], ['html', { open: 'never' }]], timeout: process.env.GITHUB_ACTION ? timeoutCIRun : timeoutLocalRun, outputDir: path.join(__dirname, 'test-results'), + snapshotPathTemplate: 'snapshots/{projectName}/{testFilePath}/{arg}{ext}', + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.001, + }, + }, }; // eslint-disable-next-line import/no-default-export diff --git a/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/best-offer-buy-confirmation.png b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/best-offer-buy-confirmation.png new file mode 100644 index 000000000000..f175e9177830 Binary files /dev/null and b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/best-offer-buy-confirmation.png differ diff --git a/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/buy-coins-layout.png b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/buy-coins-layout.png new file mode 100644 index 000000000000..5bbdc0fa4c0b Binary files /dev/null and b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/buy-coins-layout.png differ diff --git a/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/compared-offers-buy-confirmation.png b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/compared-offers-buy-confirmation.png new file mode 100644 index 000000000000..f175e9177830 Binary files /dev/null and b/packages/suite-desktop-core/e2e/snapshots/web/coin-market/buy-coins.test.ts/compared-offers-buy-confirmation.png differ diff --git a/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-grid.png b/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-grid.png new file mode 100644 index 000000000000..1c179820dc96 Binary files /dev/null and b/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-grid.png differ diff --git a/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-table.png b/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-table.png new file mode 100644 index 000000000000..07adafbaddfd Binary files /dev/null and b/packages/suite-desktop-core/e2e/snapshots/web/dashboard/assets.test.ts/new-asset-table.png differ diff --git a/packages/suite-desktop-core/e2e/support/bridge.ts b/packages/suite-desktop-core/e2e/support/bridge.ts index 8d917a1bac36..e5ecd31ac5ea 100644 --- a/packages/suite-desktop-core/e2e/support/bridge.ts +++ b/packages/suite-desktop-core/e2e/support/bridge.ts @@ -20,6 +20,6 @@ export const expectBridgeToBeStopped = async (request: APIRequestContext) => { // Bridge should be ready to check `/status` endpoint. export const waitForAppToBeInitialized = async (suite: any) => await Promise.race([ - expect(suite.page.getByTestId('@welcome/title')).toBeVisible(), - expect(suite.page.getByTestId('@dashboard/graph')).toBeVisible(), + expect(suite.window.getByTestId('@welcome/title')).toBeVisible(), + expect(suite.window.getByTestId('@dashboard/graph')).toBeVisible(), ]); diff --git a/packages/suite-desktop-core/e2e/support/common.ts b/packages/suite-desktop-core/e2e/support/common.ts index e02d2d809584..c80df0d7e747 100644 --- a/packages/suite-desktop-core/e2e/support/common.ts +++ b/packages/suite-desktop-core/e2e/support/common.ts @@ -105,10 +105,15 @@ export const launchSuite = async (params: LaunchSuiteParams = {}) => { return { electronApp, window }; }; +export const isDesktopProject = (testInfo: TestInfo) => + testInfo.project.name === PlaywrightProjects.Desktop; + +export const isWebProject = (testInfo: TestInfo) => + testInfo.project.name === PlaywrightProjects.Web; + export const getApiUrl = (webBaseUrl: string | undefined, testInfo: TestInfo) => { const electronApiURL = 'file:///'; - const apiURL = - testInfo.project.name === PlaywrightProjects.Desktop ? electronApiURL : webBaseUrl; + const apiURL = isDesktopProject(testInfo) ? electronApiURL : webBaseUrl; if (!apiURL) { throw new Error('apiURL is not defined'); } diff --git a/packages/suite-desktop-core/e2e/support/fixtures.ts b/packages/suite-desktop-core/e2e/support/fixtures.ts index 16a3b0f3b87c..a0e7286732e8 100644 --- a/packages/suite-desktop-core/e2e/support/fixtures.ts +++ b/packages/suite-desktop-core/e2e/support/fixtures.ts @@ -9,16 +9,24 @@ import { } from '@trezor/trezor-user-env-link'; import { DashboardActions } from './pageActions/dashboardActions'; -import { getApiUrl, getElectronVideoPath, launchSuite } from './common'; +import { getApiUrl, getElectronVideoPath, isDesktopProject, launchSuite } from './common'; import { SettingsActions } from './pageActions/settingsActions'; import { SuiteGuide } from './pageActions/suiteGuideActions'; import { WalletActions } from './pageActions/walletActions'; import { OnboardingActions } from './pageActions/onboardingActions'; -import { PlaywrightProjects } from '../playwright.config'; import { AnalyticsFixture } from './analytics'; +import { BackupActions } from './pageActions/backupActions'; +import { DevicePromptActions } from './pageActions/devicePromptActions'; +import { AnalyticsActions } from './pageActions/analyticsActions'; +import { IndexedDbFixture } from './indexedDb'; +import { RecoveryActions } from './pageActions/recoveryActions'; +import { TrezorInputActions } from './pageActions/trezorInputActions'; +import { MarketActions } from './pageActions/marketActions'; +import { AssetsActions } from './pageActions/assetsActions'; type Fixtures = { startEmulator: boolean; + setupEmulator: boolean; emulatorStartConf: StartEmu; emulatorSetupConf: SetupEmu; apiURL: string; @@ -30,11 +38,20 @@ type Fixtures = { suiteGuidePage: SuiteGuide; walletPage: WalletActions; onboardingPage: OnboardingActions; + backupPage: BackupActions; + analyticsPage: AnalyticsActions; + devicePrompt: DevicePromptActions; + recoveryPage: RecoveryActions; + trezorInput: TrezorInputActions; analytics: AnalyticsFixture; + indexedDb: IndexedDbFixture; + marketPage: MarketActions; + assetsPage: AssetsActions; }; const test = base.extend({ startEmulator: true, + setupEmulator: true, emulatorStartConf: {}, emulatorSetupConf: {}, apiURL: async ({ baseURL }, use, testInfo) => { @@ -48,6 +65,7 @@ const test = base.extend({ { trezorUserEnvLink, startEmulator, + setupEmulator, emulatorStartConf, emulatorSetupConf, locale, @@ -62,10 +80,13 @@ const test = base.extend({ await trezorUserEnvLink.stopEmu(); await trezorUserEnvLink.connect(); await trezorUserEnvLink.startEmu(emulatorStartConf); + } + + if (startEmulator && setupEmulator) { await trezorUserEnvLink.setupEmu(emulatorSetupConf); } - if (testInfo.project.name === PlaywrightProjects.Desktop) { + if (isDesktopProject(testInfo)) { const suite = await launchSuite({ locale, colorScheme, @@ -122,18 +143,51 @@ const test = base.extend({ const walletPage = new WalletActions(page); await use(walletPage); }, - onboardingPage: async ({ page, emulatorStartConf }, use, testInfo) => { + onboardingPage: async ({ page, analyticsPage, emulatorStartConf }, use, testInfo) => { const onboardingPage = new OnboardingActions( page, + analyticsPage, emulatorStartConf.model ?? TrezorUserEnvLink.defaultModel, testInfo, ); await use(onboardingPage); }, + backupPage: async ({ page, devicePrompt }, use) => { + const backupPage = new BackupActions(page, devicePrompt); + await use(backupPage); + }, + analyticsPage: async ({ page }, use) => { + const analyticsPage = new AnalyticsActions(page); + await use(analyticsPage); + }, + devicePrompt: async ({ page }, use) => { + const devicePromptActions = new DevicePromptActions(page); + await use(devicePromptActions); + }, + recoveryPage: async ({ page }, use) => { + const recoveryPage = new RecoveryActions(page); + await use(recoveryPage); + }, + trezorInput: async ({ page }, use) => { + const trezorInput = new TrezorInputActions(page); + await use(trezorInput); + }, analytics: async ({ page }, use) => { const analytics = new AnalyticsFixture(page); await use(analytics); }, + indexedDb: async ({ page }, use) => { + const indexedDb = new IndexedDbFixture(page); + await use(indexedDb); + }, + marketPage: async ({ page }, use) => { + const marketPage = new MarketActions(page); + await use(marketPage); + }, + assetsPage: async ({ page }, use) => { + const assetPage = new AssetsActions(page); + await use(assetPage); + }, }); export { test }; diff --git a/packages/suite-desktop-core/e2e/support/indexedDb.ts b/packages/suite-desktop-core/e2e/support/indexedDb.ts new file mode 100644 index 000000000000..c40b0d9c66e3 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/indexedDb.ts @@ -0,0 +1,21 @@ +import { Page } from '@playwright/test'; + +export class IndexedDbFixture { + constructor(private page: Page) {} + + async reset() { + await this.page.evaluate(() => { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase('trezor-suite'); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject('Error resetting database'); + }; + }); + }); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/analyticsActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/analyticsActions.ts new file mode 100644 index 000000000000..6c7797c05fc5 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/analyticsActions.ts @@ -0,0 +1,16 @@ +import { Locator, Page } from '@playwright/test'; + +export class AnalyticsActions { + readonly heading: Locator; + readonly continueButton: Locator; + + constructor(page: Page) { + this.continueButton = page.getByTestId('@analytics/continue-button'); + this.heading = page.getByTestId('@analytics/consent/heading'); + } + + async passThroughAnalytics() { + await this.continueButton.click(); + await this.continueButton.click(); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/assetsActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/assetsActions.ts new file mode 100644 index 000000000000..c696d4e4537e --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/assetsActions.ts @@ -0,0 +1,31 @@ +import { Locator, Page } from '@playwright/test'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; + +export class AssetsActions { + readonly section: Locator; + readonly tableIcon: Locator; + readonly gridIcon: Locator; + readonly buyAssetButton = (symbol: NetworkSymbol) => + this.page.getByTestId(`@dashboard/asset/${symbol}/buy-button`); + readonly enableMoreCoins: Locator; + readonly assetCard = (symbol: NetworkSymbol) => + this.page.getByTestId(`@dashboard/asset-card/${symbol}`); + readonly assetRow = (symbol: NetworkSymbol) => + this.page.getByTestId(`@dashboard/asset-row/${symbol}`); + readonly assetFiatAmount = (symbol: NetworkSymbol) => + this.page.getByTestId(`@dashboard/asset/${symbol}/fiat-amount`); + readonly bottomInfo: Locator; + readonly assetExchangeRate: Locator; + readonly assetWeekChange: Locator; + + constructor(private readonly page: Page) { + this.section = page.getByTestId('@dashboard/assets'); + this.tableIcon = this.page.getByTestId('@dashboard/assets/table-icon'); + this.gridIcon = this.page.getByTestId('@dashboard/assets/grid-icon'); + this.enableMoreCoins = this.page.getByTestId('@dashboard/assets/enable-more-coins'); + this.bottomInfo = this.page.getByTestId('@dashboard/asset/bottom-info'); + this.assetExchangeRate = this.page.getByTestId('@dashboard/asset/exchange-rate'); + this.assetWeekChange = this.page.getByTestId('@dashboard/asset/week-change'); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/backupActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/backupActions.ts new file mode 100644 index 000000000000..029d0ff2d099 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/backupActions.ts @@ -0,0 +1,45 @@ +import { Locator, Page, expect } from '@playwright/test'; + +import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; + +import { DevicePromptActions } from './devicePromptActions'; + +export class BackupActions { + readonly backupStartButton: Locator; + readonly wroteSeedProperlyCheckbox: Locator; + readonly madeNoDigitalCopyCheckbox: Locator; + readonly willHideSeedCheckbox: Locator; + readonly backupCloseButton: Locator; + + constructor( + private page: Page, + private devicePrompt: DevicePromptActions, + ) { + this.backupStartButton = page.getByTestId('@backup/start-button'); + this.wroteSeedProperlyCheckbox = page.getByTestId('@backup/check-item/wrote-seed-properly'); + this.madeNoDigitalCopyCheckbox = page.getByTestId( + '@backup/check-item/made-no-digital-copy', + ); + this.willHideSeedCheckbox = page.getByTestId('@backup/check-item/will-hide-seed'); + this.backupCloseButton = page.getByTestId('@backup/close-button'); + } + + async passThroughShamirBackup(shares: number, threshold: number) { + // Backup button should be disabled until all checkboxes are checked + await expect(this.backupStartButton).toBeDisabled(); + + await this.wroteSeedProperlyCheckbox.click(); + await this.madeNoDigitalCopyCheckbox.click(); + await this.willHideSeedCheckbox.click(); + + // Create Shamir backup on device + await this.backupStartButton.click(); + await this.devicePrompt.confirmOnDevicePromptIsShown(); + + // Adding delay to mitigate race condition; avoids hitting the homescreen + await this.page.waitForTimeout(1000); + await TrezorUserEnvLink.readAndConfirmShamirMnemonicEmu({ shares, threshold }); + + await this.backupCloseButton.click(); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/dashboardActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/dashboardActions.ts index 9be72f4d3654..fdce17dc3957 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/dashboardActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/dashboardActions.ts @@ -1,26 +1,25 @@ import { Locator, Page, expect } from '@playwright/test'; -import { NetworkSymbol } from '@suite-common/wallet-config'; - export class DashboardActions { - private readonly page: Page; readonly dashboardMenuButton: Locator; readonly discoveryHeader: Locator; readonly discoveryBar: Locator; readonly dashboardGraph: Locator; readonly deviceSwitchingOpenButton: Locator; readonly modal: Locator; + //TODO: Refactor to wallet page object readonly walletAtIndex = (index: number) => this.page.getByTestId(`@switch-device/wallet-on-index/${index}`); readonly walletAtIndexEjectButton = (index: number) => this.page.getByTestId(`@switch-device/wallet-on-index/${index}/eject-button`); + readonly walletAtIndexFiatAmount = (index: number) => + this.page.getByTestId(`@switch-device/wallet-on-index/${index}/fiat-amount`); readonly confirmDeviceEjectButton: Locator; readonly addStandardWalletButton: Locator; - readonly balanceOfNetwork = (symbol: NetworkSymbol) => - this.page.getByTestId(`@wallet/coin-balance/value-${symbol}`); + readonly hideBalanceButton: Locator; + readonly portfolioFiatAmount: Locator; - constructor(page: Page) { - this.page = page; + constructor(private readonly page: Page) { this.dashboardMenuButton = this.page.getByTestId('@suite/menu/suite-index'); this.discoveryHeader = this.page.getByRole('heading', { name: 'Dashboard' }); this.discoveryBar = this.page.getByTestId('@wallet/discovery-progress-bar'); @@ -29,6 +28,8 @@ export class DashboardActions { this.modal = this.page.getByTestId('@modal'); this.confirmDeviceEjectButton = this.page.getByTestId('@switch-device/eject'); this.addStandardWalletButton = this.page.getByTestId('@switch-device/add-wallet-button'); + this.hideBalanceButton = this.page.getByTestId('@quickActions/hideBalances'); + this.portfolioFiatAmount = this.page.getByTestId('@dashboard/portfolio/fiat-amount'); } async navigateTo() { diff --git a/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts new file mode 100644 index 000000000000..61654df16dfa --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/devicePromptActions.ts @@ -0,0 +1,21 @@ +import { Locator, Page, expect } from '@playwright/test'; + +export class DevicePromptActions { + private readonly confirmOnDevicePrompt: Locator; + private readonly connectDevicePrompt: Locator; + readonly modal: Locator; + + constructor(page: Page) { + this.confirmOnDevicePrompt = page.getByTestId('@onboarding/confirm-on-device'); + this.connectDevicePrompt = page.getByTestId('@connect-device-prompt'); + this.modal = page.getByTestId('@modal'); + } + + async confirmOnDevicePromptIsShown() { + await expect(this.confirmOnDevicePrompt).toBeVisible(); + } + + async connectDevicePromptIsShown() { + await expect(this.connectDevicePrompt).toBeVisible(); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts new file mode 100644 index 000000000000..5abd75297596 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts @@ -0,0 +1,153 @@ +import { Locator, Page, expect } from '@playwright/test'; + +import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; +import { FiatCurrencyCode } from '@suite-common/suite-config'; +import regional from '@trezor/suite/src/constants/wallet/coinmarket/regional'; + +const getCountryLabel = (country: string) => { + const labelWithFlag = regional.countriesMap.get(country); + if (!labelWithFlag) { + throw new Error(`Country ${country} not found in the countries map`); + } + + return labelWithFlag.substring(labelWithFlag.indexOf(' ') + 1); +}; + +export class MarketActions { + readonly offerSpinner: Locator; + readonly section: Locator; + readonly form: Locator; + readonly bestOfferProvider: Locator; + readonly bestOfferYouGet: Locator; + readonly bestOfferAmount: Locator; + readonly buyBestOfferButton: Locator; + readonly youPayInput: Locator; + readonly youPayCurrencyDropdown: Locator; + readonly youPayCurrencyOption = (currency: FiatCurrencyCode) => + this.page.getByTestId(`@coinmarket/form/fiat-currency-select/option/${currency}`); + readonly countryOfResidenceDropdown: Locator; + readonly countryOfResidenceOption = (countryCode: string) => + this.page.getByTestId(`@coinmarket/form/country-select/option/${countryCode}`); + readonly buyOffersPage: Locator; + readonly compareButton: Locator; + readonly quotes: Locator; + readonly quoteOfProvider = (provider: string) => + this.page.getByTestId(`@coinmarket/offers/quote-${provider}`); + readonly quoteProvider: Locator; + readonly quoteAmount: Locator; + readonly selectThisQuoteButton: Locator; + readonly modal: Locator; + readonly buyTermsConfirmButton: Locator; + readonly confirmOnTrezorButton: Locator; + readonly confirmOnDevicePrompt: Locator; + readonly tradeConfirmation: Locator; + readonly tradeConfirmationCryptoAmount: Locator; + readonly tradeConfirmationProvider: Locator; + readonly tradeConfirmationContinueButton: Locator; + readonly exchangeFeeDetails: Locator; + + constructor(private page: Page) { + this.offerSpinner = this.page.getByTestId('@coinmarket/offers/loading-spinner'); + this.section = this.page.getByTestId('@coinmarket'); + this.form = this.page.getByTestId('@coinmarket/form'); + this.bestOfferProvider = this.page.getByTestId('@coinmarket/offers/quote/provider'); + this.bestOfferYouGet = this.page.getByTestId('@coinmarket/best-offer/amount'); + this.bestOfferAmount = this.page.getByTestId('@coinmarket/form/offer/crypto-amount'); + this.buyBestOfferButton = this.page.getByTestId('@coinmarket/form/buy-button'); + this.youPayInput = this.page.getByTestId('@coinmarket/form/fiat-input'); + this.youPayCurrencyDropdown = this.page.getByTestId( + '@coinmarket/form/fiat-currency-select/input', + ); + this.countryOfResidenceDropdown = this.page.getByTestId( + '@coinmarket/form/country-select/input', + ); + this.buyOffersPage = this.page.getByTestId('@coinmarket/buy-offers'); + this.compareButton = this.page.getByTestId('@coinmarket/form/compare-button'); + this.quotes = this.page.getByTestId('@coinmarket/offers/quote'); + this.quoteProvider = this.page.getByTestId('@coinmarket/offers/quote/provider'); + this.quoteAmount = this.page.getByTestId('@coinmarket/offers/quote/crypto-amount'); + this.selectThisQuoteButton = this.page.getByTestId( + '@coinmarket/offers/get-this-deal-button', + ); + this.modal = this.page.getByTestId('@modal'); + this.buyTermsConfirmButton = this.page.getByTestId( + '@coinmarket/buy/offers/buy-terms-confirm-button', + ); + this.confirmOnTrezorButton = this.page.getByTestId( + '@coinmarket/offer/confirm-on-trezor-button', + ); + this.confirmOnDevicePrompt = this.page.getByTestId('@prompts/confirm-on-device'); + this.tradeConfirmation = this.page.getByTestId('@coinmarket/selected-offer'); + this.tradeConfirmationCryptoAmount = this.page.getByTestId( + '@coinmarket/form/info/crypto-amount', + ); + this.tradeConfirmationProvider = this.page.getByTestId('@coinmarket/form/info/provider'); + this.tradeConfirmationContinueButton = this.page.getByTestId( + '@coinmarket/offer/continue-transaction-button', + ); + this.exchangeFeeDetails = this.page.getByTestId('@wallet/fee-details'); + } + + waitForOffersSyncToFinish = async () => { + await expect(this.offerSpinner).toBeHidden({ timeout: 30000 }); + //Even though the offer sync is finished, the best offer might not be displayed correctly yet and show 0 BTC + await expect(this.bestOfferAmount).not.toHaveText('0 BTC'); + await expect(this.buyBestOfferButton).toBeEnabled(); + }; + + selectCountryOfResidence = async (countryCode: string) => { + const countryLabel = getCountryLabel(countryCode); + const currentCountry = await this.countryOfResidenceDropdown.textContent(); + if (currentCountry === countryLabel) { + return; + } + await this.countryOfResidenceDropdown.click(); + await this.countryOfResidenceDropdown.getByRole('combobox').fill(countryLabel); + await this.countryOfResidenceOption(countryCode).click(); + }; + + selectFiatCurrency = async (currencyCode: FiatCurrencyCode) => { + const currentCurrency = await this.youPayCurrencyDropdown.textContent(); + if (currentCurrency === currencyCode.toUpperCase()) { + return; + } + await this.youPayCurrencyDropdown.click(); + await this.youPayCurrencyOption(currencyCode).click(); + }; + + setYouPayAmount = async ( + amount: string, + currency: FiatCurrencyCode = 'czk', + country: string = 'CZ', + ) => { + //Warning: the field is initialized empty and gets default value after the first offer sync + await expect(this.youPayInput).not.toHaveValue(''); + await this.selectCountryOfResidence(country); + await this.selectFiatCurrency(currency); + await this.youPayInput.fill(amount); + //Warning: Bug #16054, as a workaround we wait for offer sync after setting the amount + await this.waitForOffersSyncToFinish(); + }; + + confirmTrade = async () => { + await expect(this.modal).toBeVisible(); + await this.buyTermsConfirmButton.click(); + await this.confirmOnTrezorButton.click(); + await expect(this.confirmOnDevicePrompt).toBeVisible(); + await TrezorUserEnvLink.pressYes(); + await expect(this.confirmOnDevicePrompt).not.toBeVisible(); + }; + + readBestOfferValues = async () => { + await expect(this.bestOfferAmount).not.toHaveText('0 BTC'); + const amount = await this.bestOfferAmount.textContent(); + const provider = await this.bestOfferProvider.textContent(); + if (!amount || !provider) { + throw new Error( + `Test was not able to extract amount or provider from the page. Amount: ${amount}, Provider: ${provider}`, + ); + } + + return { amount, provider }; + }; +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/onboardingActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/onboardingActions.ts index 47317fc0350e..6b5ca9c96162 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/onboardingActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/onboardingActions.ts @@ -3,14 +3,11 @@ import { Locator, Page, TestInfo, expect } from '@playwright/test'; import { Model, TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; import { SUITE as SuiteActions } from '@trezor/suite/src/actions/suite/constants'; -import { PlaywrightProjects } from '../../playwright.config'; +import { AnalyticsActions } from './analyticsActions'; +import { isWebProject } from '../common'; export class OnboardingActions { - readonly model: Model; - readonly testInfo: TestInfo; readonly welcomeTitle: Locator; - readonly analyticsHeading: Locator; - readonly analyticsContinueButton: Locator; readonly onboardingContinueButton: Locator; readonly onboardingViewOnlySkipButton: Locator; readonly onboardingViewOnlyEnableButton: Locator; @@ -18,18 +15,27 @@ export class OnboardingActions { readonly connectDevicePrompt: Locator; readonly authenticityStartButton: Locator; readonly authenticityContinueButton: Locator; + readonly createBackupButton: Locator; + readonly recoverWalletButton: Locator; + readonly startRecoveryButton: Locator; + readonly continueRecoveryButton: Locator; + readonly retryRecoveryButton: Locator; + readonly firmwareContinueButton: Locator; + readonly skipFirmwareButton: Locator; + readonly skipConfirmButton: Locator; + readonly skipPinButton: Locator; + readonly continueCoinsButton: Locator; + readonly finalTitle: Locator; + isModelWithSecureElement = () => ['T2B1', 'T3T1'].includes(this.model); constructor( public page: Page, - model: Model, - testInfo: TestInfo, + private analyticsPage: AnalyticsActions, + private readonly model: Model, + private readonly testInfo: TestInfo, ) { - this.model = model; - this.testInfo = testInfo; this.welcomeTitle = this.page.getByTestId('@welcome/title'); - this.analyticsHeading = this.page.getByTestId('@analytics/consent/heading'); - this.analyticsContinueButton = this.page.getByTestId('@analytics/continue-button'); this.onboardingContinueButton = this.page.getByTestId('@onboarding/exit-app-button'); this.onboardingViewOnlySkipButton = this.page.getByTestId('@onboarding/viewOnly/skip'); this.onboardingViewOnlyEnableButton = this.page.getByTestId('@onboarding/viewOnly/enable'); @@ -39,6 +45,17 @@ export class OnboardingActions { this.authenticityContinueButton = this.page.getByTestId( '@authenticity-check/continue-button', ); + this.createBackupButton = this.page.getByTestId('@onboarding/create-backup-button'); + this.recoverWalletButton = this.page.getByTestId('@onboarding/path-recovery-button'); + this.startRecoveryButton = this.page.getByTestId('@onboarding/recovery/start-button'); + this.continueRecoveryButton = this.page.getByTestId('@onboarding/recovery/continue-button'); + this.retryRecoveryButton = this.page.getByTestId('@onboarding/recovery/retry-button'); + this.firmwareContinueButton = this.page.getByTestId('@firmware/continue-button'); + this.skipFirmwareButton = this.page.getByTestId('@firmware/skip-button'); + this.skipPinButton = this.page.getByTestId('@onboarding/skip-button'); + this.skipConfirmButton = this.page.getByTestId('@onboarding/skip-button-confirm'); + this.continueCoinsButton = this.page.getByTestId('@onboarding/coins/continue-button'); + this.finalTitle = this.page.getByTestId('@onboarding/final'); } async optionallyDismissFwHashCheckError() { @@ -50,11 +67,9 @@ export class OnboardingActions { } async completeOnboarding({ enableViewOnly = false } = {}) { - if (this.testInfo.project.name === PlaywrightProjects.Web) { - await this.disableFirmwareHashCheck(); - } + await this.disableFirmwareHashCheck(); await this.optionallyDismissFwHashCheckError(); - await this.analyticsContinueButton.click(); + await this.analyticsPage.continueButton.click(); await this.onboardingContinueButton.click(); if (this.isModelWithSecureElement()) { await this.authenticityStartButton.click(); @@ -71,6 +86,10 @@ export class OnboardingActions { async disableFirmwareHashCheck() { // Desktop starts with already disabled firmware hash check. Web needs to disable it. + if (!isWebProject(this.testInfo)) { + return; + } + await expect(this.welcomeTitle).toBeVisible({ timeout: 10000 }); // eslint-disable-next-line @typescript-eslint/no-shadow await this.page.evaluate(SuiteActions => { @@ -84,4 +103,14 @@ export class OnboardingActions { }); }, SuiteActions); } + + async skipFirmware() { + await this.skipFirmwareButton.click(); + await this.skipConfirmButton.click(); + } + + async skipPin() { + await this.skipPinButton.click(); + await this.skipConfirmButton.click(); + } } diff --git a/packages/suite-desktop-core/e2e/support/pageActions/recoveryActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/recoveryActions.ts new file mode 100644 index 000000000000..e955ff253fa2 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/recoveryActions.ts @@ -0,0 +1,26 @@ +import { Locator, Page } from '@playwright/test'; + +export class RecoveryActions { + readonly selectBasicRecoveryButton: Locator; + readonly userUnderstandsCheckbox: Locator; + readonly startButton: Locator; + readonly successTitle: Locator; + + constructor(private page: Page) { + this.selectBasicRecoveryButton = page.getByTestId('@recover/select-type/basic'); + this.userUnderstandsCheckbox = page.getByTestId('@recovery/user-understands-checkbox'); + this.startButton = page.getByTestId('@recovery/start-button'); + this.successTitle = page.getByTestId('@recovery/success-title'); + } + + async selectWordCount(number: 12 | 18 | 24) { + await this.page.getByTestId(`@recover/select-count/${number}`).click(); + } + + async initDryCheck(type: 'basic' | 'advanced', numberOfWords: 12 | 18 | 24) { + await this.userUnderstandsCheckbox.click(); + await this.startButton.click(); + await this.selectWordCount(numberOfWords); + await this.page.getByTestId(`@recover/select-type/${type}`).click(); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/settingsActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/settingsActions.ts index 586ff1795253..90fc5a33b1e8 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/settingsActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/settingsActions.ts @@ -36,8 +36,6 @@ const backgroundImages = { }; export class SettingsActions { - private readonly page: Page; - private readonly apiURL: string; private readonly TIMES_CLICK_TO_SET_DEBUG_MODE = 5; readonly settingsMenuButton: Locator; readonly settingsHeader: Locator; @@ -56,7 +54,6 @@ export class SettingsActions { readonly confirmOnDevicePrompt: Locator; readonly homescreenGalleryButton: Locator; readonly notificationSuccessToast: Locator; - readonly pinSubmitButton: Locator; //coin Advance settings readonly networkButton = (symbol: NetworkSymbol) => this.page.getByTestId(`@settings/wallet/network/${symbol}`); @@ -73,11 +70,12 @@ export class SettingsActions { readonly languageInput: Locator; readonly languageInputOption = (language: Language) => this.page.getByTestId(`@settings/language-select/option/${language}`); - readonly pinInput = (index: number) => this.page.getByTestId(`@pin/input/${index}`); + readonly checkSeedButton: Locator; - constructor(page: Page, apiURL: string) { - this.page = page; - this.apiURL = apiURL; + constructor( + private readonly page: Page, + private readonly apiURL: string, + ) { this.settingsMenuButton = this.page.getByTestId('@suite/menu/settings'); this.settingsHeader = this.page.getByTestId('@settings/menu/title'); this.debugTabButton = this.page.getByTestId('@settings/menu/debug'); @@ -104,7 +102,7 @@ export class SettingsActions { this.coinAdvanceSettingSaveButton = this.page.getByTestId('@settings/advance/button/save'); this.themeInput = this.page.getByTestId('@theme/color-scheme-select/input'); this.languageInput = this.page.getByTestId('@settings/language-select/input'); - this.pinSubmitButton = this.page.getByTestId('@pin/submit-button'); + this.checkSeedButton = this.page.getByTestId('@settings/device/check-seed-button'); } async navigateTo() { @@ -213,13 +211,4 @@ export class SettingsActions { await expect(this.notificationSuccessToast).toBeVisible(); }); } - - async enterPinOnBlindMatrix(pinEntryNumber: string) { - await test.step('Find number on blind matrix and click it', async () => { - const state = await TrezorUserEnvLink.getDebugState(); - const index = state.matrix.indexOf(pinEntryNumber) + 1; - await this.pinInput(index).click(); - await this.pinSubmitButton.click(); - }); - } } diff --git a/packages/suite-desktop-core/e2e/support/pageActions/suiteGuideActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/suiteGuideActions.ts index bde3d892c46d..e476524922e0 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/suiteGuideActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/suiteGuideActions.ts @@ -6,7 +6,6 @@ import { capitalizeFirstLetter } from '@trezor/utils'; const anyTestIdEndingWithClose = '[data-testid$="close"]'; export class SuiteGuide { - private readonly page: Page; readonly guideButton: Locator; readonly supportAndFeedbackButton: Locator; readonly bugFormButton: Locator; @@ -27,8 +26,7 @@ export class SuiteGuide { readonly feedbackSuccessToast: Locator; readonly articleHeader: Locator; - constructor(page: Page) { - this.page = page; + constructor(private readonly page: Page) { this.guideButton = this.page.getByTestId('@guide/button-open'); this.supportAndFeedbackButton = this.page.getByTestId('@guide/button-feedback'); this.bugFormButton = this.page.getByTestId('@guide/feedback/bug'); diff --git a/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts new file mode 100644 index 000000000000..993bab507a02 --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts @@ -0,0 +1,60 @@ +import { Locator, Page, test } from '@playwright/test'; + +import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; + +import { expect } from '../customMatchers'; + +export class TrezorInputActions { + readonly wordSelectInput: Locator; + readonly pinSubmitButton: Locator; + readonly pinInput = (index: number) => this.page.getByTestId(`@pin/input/${index}`); + + constructor(private page: Page) { + this.wordSelectInput = page.getByTestId('@word-input-select/input'); + this.pinSubmitButton = this.page.getByTestId('@pin/submit-button'); + } + + async inputWord(word: string) { + await this.wordSelectInput.type(word); + await this.page.getByTestId(`@word-input-select/option/${word}`).click(); + } + + async inputMnemonicT1B1(mnemonic: string) { + const arrayMnemonic = mnemonic.split(' '); + await test.step(`Inputting words ${arrayMnemonic.length}x ${arrayMnemonic}`, async () => { + for (let i = 0; i < 24; i++) { + await expect(this.wordSelectInput).toHaveText("Check your Trezor's screen"); + const state = await TrezorUserEnvLink.getDebugState(); + const position = state.recovery_word_pos - 1; + const isGivenFakeWord = position === -1; + if (isGivenFakeWord) { + await test.step(`Inputting fake word ${state.recovery_fake_word}`, async () => { + await this.inputWord(state.recovery_fake_word); + }); + } else { + await test.step(`Inputting word ${arrayMnemonic[position]} at position ${position}`, async () => { + await this.inputWord(arrayMnemonic[position]); + }); + } + } + }); + } + + //TODO: #16107 Not working with anything else than 12x 'all' - I will ask around + async inputMnemonicT2T1(mnemonic: string) { + for (const word of mnemonic.split(' ')) { + await test.step(`Inputting word ${word.slice(0, 4)}`, async () => { + await TrezorUserEnvLink.inputEmu(word.slice(0, 4)); + }); + } + } + + async enterPinOnBlindMatrix(pinEntryNumber: string) { + await test.step('Find number on blind matrix and click it', async () => { + const state = await TrezorUserEnvLink.getDebugState(); + const index = state.matrix.indexOf(pinEntryNumber) + 1; + await this.pinInput(index).click(); + await this.pinSubmitButton.click(); + }); + } +} diff --git a/packages/suite-desktop-core/e2e/support/pageActions/walletActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/walletActions.ts index e915846df25d..f9c24c8216f5 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/walletActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/walletActions.ts @@ -2,18 +2,22 @@ import { Locator, Page, expect } from '@playwright/test'; import { NetworkSymbol } from '@suite-common/wallet-config'; +type WalletParams = { symbol?: NetworkSymbol; atIndex?: number }; + export class WalletActions { - private readonly page: Page; - readonly walletMenuButton: Locator; readonly searchInput: Locator; readonly accountChevron: Locator; readonly cardanoAccountLabels: { [key: string]: Locator }; readonly walletStakingButton: Locator; readonly stakeAddress: Locator; + readonly walletExtraDropDown: Locator; + readonly coinMarketBuyButton: Locator; + readonly coinExchangeButton: Locator; + readonly coinMarketDropdownBuyButton: Locator; + readonly balanceOfNetwork = (symbol: NetworkSymbol) => + this.page.getByTestId(`@wallet/coin-balance/value-${symbol}`); - constructor(page: Page) { - this.page = page; - this.walletMenuButton = this.page.getByTestId('@suite/menu/wallet-index'); + constructor(private readonly page: Page) { this.searchInput = this.page.getByTestId('@wallet/accounts/search-icon'); this.accountChevron = this.page.getByTestId('@account-menu/arrow'); this.cardanoAccountLabels = { @@ -23,6 +27,12 @@ export class WalletActions { }; this.walletStakingButton = this.page.getByTestId('@wallet/menu/staking'); this.stakeAddress = this.page.getByTestId('@cardano/staking/address'); + this.walletExtraDropDown = this.page.getByTestId('@wallet/menu/extra-dropdown'); + this.coinMarketBuyButton = this.page.getByTestId('@wallet/menu/wallet-coinmarket-buy'); + this.coinExchangeButton = this.page.getByTestId('@wallet/menu/wallet-coinmarket-exchange'); + this.coinMarketDropdownBuyButton = this.page + .getByRole('list') + .getByTestId('@wallet/menu/wallet-coinmarket-buy'); } async filterTransactions(transaction: string) { @@ -49,4 +59,25 @@ export class WalletActions { .locator(`[data-testid*="@account-menu/${symbol}"][tabindex]`) .count(); } + + walletMenuButton = ({ symbol = 'btc', atIndex = 0 }: WalletParams = {}): Locator => { + return this.page.getByTestId(`@account-menu/${symbol}/normal/${atIndex}`); + }; + + async openCoinMarket(params: WalletParams = {}) { + await this.walletMenuButton(params).click(); + //TODO: #16073 We cannot set resolution for Electron. on CI button is hidden under dropdown due to a breakpoint + const isBuyButtonHidden = !(await this.coinMarketBuyButton.isVisible()); + if (isBuyButtonHidden) { + await this.walletExtraDropDown.click(); + await this.coinMarketDropdownBuyButton.click(); + } else { + await this.coinMarketBuyButton.click(); + } + } + + async openExchangeMarket(params: WalletParams = {}) { + await this.walletMenuButton(params).click(); + await this.coinExchangeButton.click(); + } } diff --git a/packages/suite-desktop-core/e2e/tests/spawn-bridge-daemon.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge-daemon.test.ts similarity index 88% rename from packages/suite-desktop-core/e2e/tests/spawn-bridge-daemon.test.ts rename to packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge-daemon.test.ts index 33c0d47af4f5..cfcc9e6ea025 100644 --- a/packages/suite-desktop-core/e2e/tests/spawn-bridge-daemon.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge-daemon.test.ts @@ -1,10 +1,10 @@ -import { test, expect } from '../support/fixtures'; -import { launchSuite, launchSuiteElectronApp } from '../support/common'; +import { test, expect } from '../../support/fixtures'; +import { launchSuite, launchSuiteElectronApp } from '../../support/common'; import { expectBridgeToBeRunning, expectBridgeToBeStopped, waitForAppToBeInitialized, -} from '../support/bridge'; +} from '../../support/bridge'; test.describe.serial('Bridge', { tag: ['@group=suite', '@desktopOnly'] }, () => { test.beforeAll(async ({ trezorUserEnvLink }) => { diff --git a/packages/suite-desktop-core/e2e/tests/spawn-bridge.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge.test.ts similarity index 86% rename from packages/suite-desktop-core/e2e/tests/spawn-bridge.test.ts rename to packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge.test.ts index 116dc8c9ee4f..2237d013a994 100644 --- a/packages/suite-desktop-core/e2e/tests/spawn-bridge.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-bridge.test.ts @@ -1,12 +1,13 @@ -import { test, expect } from '../support/fixtures'; -import { launchSuite, LEGACY_BRIDGE_VERSION } from '../support/common'; -import { OnboardingActions } from '../support/pageActions/onboardingActions'; +import { test, expect } from '../../support/fixtures'; +import { launchSuite, LEGACY_BRIDGE_VERSION } from '../../support/common'; +import { OnboardingActions } from '../../support/pageActions/onboardingActions'; import { BRIDGE_URL, expectBridgeToBeRunning, expectBridgeToBeStopped, waitForAppToBeInitialized, -} from '../support/bridge'; +} from '../../support/bridge'; +import { AnalyticsActions } from '../../support/pageActions/analyticsActions'; test.describe.serial('Bridge', { tag: ['@group=suite', '@desktopOnly'] }, () => { test.beforeEach(async ({ trezorUserEnvLink }) => { @@ -53,6 +54,7 @@ test.describe.serial('Bridge', { tag: ['@group=suite', '@desktopOnly'] }, () => await suite.window.title(); const onboardingPage = new OnboardingActions( suite.window, + new AnalyticsActions(suite.window), trezorUserEnvLink.defaultModel, testInfo, ); diff --git a/packages/suite-desktop-core/e2e/tests/spawn-tor.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-tor.test.ts similarity index 95% rename from packages/suite-desktop-core/e2e/tests/spawn-tor.test.ts rename to packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-tor.test.ts index 33ba871d4490..08b3a9d68c9a 100644 --- a/packages/suite-desktop-core/e2e/tests/spawn-tor.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-thor/spawn-tor.test.ts @@ -1,8 +1,8 @@ import { Page } from '@playwright/test'; -import { test, expect } from '../support/fixtures'; -import { launchSuite } from '../support/common'; -import { NetworkAnalyzer } from '../support/networkAnalyzer'; +import { test, expect } from '../../support/fixtures'; +import { launchSuite } from '../../support/common'; +import { NetworkAnalyzer } from '../../support/networkAnalyzer'; const timeout = 1000 * 60 * 5; // 5 minutes because it takes a while to start tor. diff --git a/packages/suite-desktop-core/e2e/tests/coin-market/buy-coins.test.ts b/packages/suite-desktop-core/e2e/tests/coin-market/buy-coins.test.ts new file mode 100644 index 000000000000..ca6800287319 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/coin-market/buy-coins.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from '../../support/fixtures'; + +const regexpBtcValue = /^\d+(\.\d+)? BTC$/; + +test.describe('Coin market buy', { tag: ['@group=other'] }, () => { + test.use({ emulatorStartConf: { wipe: true } }); + test.beforeEach(async ({ onboardingPage, dashboardPage, walletPage }) => { + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + await walletPage.openCoinMarket(); + }); + + // TOOD: #16041 Once solved, fix and uncomment, invity calls are not stable on CI + test.skip('Buy crypto from compared offers', async ({ marketPage }) => { + await test.step('Fill input amount and opens offer comparison', async () => { + await marketPage.setYouPayAmount('1234'); + await expect(marketPage.section).toHaveScreenshot('buy-coins-layout.png', { + mask: [marketPage.bestOfferYouGet, marketPage.bestOfferProvider], + }); + await marketPage.compareButton.click(); + }); + + await test.step('Check offers and chooses the first one', async () => { + // TOOD: #16041 Once solved, add verification of offer compare items + await expect(marketPage.buyOffersPage).toBeVisible(); + expect(await marketPage.quotes.count()).toBeGreaterThan(1); + await marketPage.selectThisQuoteButton.first().click(); + }); + + await test.step('Confirm trade and verifies confirmation summary', async () => { + await marketPage.confirmTrade(); + await expect(marketPage.tradeConfirmation).toHaveScreenshot( + 'compared-offers-buy-confirmation.png', + { + mask: [ + marketPage.tradeConfirmationCryptoAmount, + marketPage.tradeConfirmationProvider, + ], + }, + ); + // TOOD: #16041 Once solved, Assert mocked price + await expect(marketPage.tradeConfirmationCryptoAmount).toHaveText(regexpBtcValue); + await expect(marketPage.tradeConfirmationContinueButton).toBeEnabled(); + }); + }); + + // TOOD: #16041 Once solved, fix and uncomment, invity calls are not stable on CI + test.skip('Buy crypto from best offer', async ({ marketPage }) => { + await marketPage.setYouPayAmount('1234'); + const { amount, provider } = await marketPage.readBestOfferValues(); + await marketPage.buyBestOfferButton.click(); + await marketPage.confirmTrade(); + await expect(marketPage.tradeConfirmation).toHaveScreenshot( + 'best-offer-buy-confirmation.png', + { + mask: [ + marketPage.tradeConfirmationCryptoAmount, + marketPage.tradeConfirmationProvider, + ], + }, + ); + await expect(marketPage.tradeConfirmationCryptoAmount).toHaveText(amount); + await expect(marketPage.tradeConfirmationProvider).toHaveText(provider); + await expect(marketPage.tradeConfirmationContinueButton).toBeEnabled(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/coin-market/exchange-coins.test.ts b/packages/suite-desktop-core/e2e/tests/coin-market/exchange-coins.test.ts new file mode 100644 index 000000000000..da3645ca02ff --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/coin-market/exchange-coins.test.ts @@ -0,0 +1,94 @@ +import { test, expect } from '../../support/fixtures'; + +test.describe('Coinmarket Exchange', { tag: ['@group=other'] }, () => { + test.use({ + emulatorStartConf: { wipe: true }, + emulatorSetupConf: { + mnemonic: + 'alcohol woman abuse must during monitor noble actual mixed trade anger aisle', + }, + }); + test.beforeEach( + async ({ onboardingPage, dashboardPage, walletPage, settingsPage, trezorUserEnvLink }) => { + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + await settingsPage.navigateTo(); + await settingsPage.coinsTabButton.click(); + await settingsPage.enableNetwork('regtest'); + await trezorUserEnvLink.sendToAddressAndMineBlock({ + address: 'bcrt1qnspxpr2xj9s2jt6qlhuvdnxw6q55jvyg6q7g5r', + btc_amount: 25, + }); + await settingsPage.enableNetwork('eth'); + await dashboardPage.navigateTo(); + await walletPage.openExchangeMarket({ symbol: 'regtest' }); + }, + ); + + // TOOD: #16041 Once solved, fix and uncomment, We dont have enough founds to make the exchange + test.skip('Exchange flow', async ({ marketPage, page }) => { + await test.step('Wait for exchange form initialization and make visual comparison', async () => { + await expect(marketPage.bestOfferAmount).toHaveText('0 BTC'); + await expect(marketPage.form).toHaveScreenshot('exchange-form.png', { + mask: [marketPage.exchangeFeeDetails], + }); + }); + + await page + .getByTestId('@coinmarket/form/select-account/input') + .getByRole('combobox') + .fill('ETH'); + await page.getByTestId('@coinmarket/form/select-account/option/ethereum').first().click(); + + await page.getByTestId('@coinmarket/form/crypto-input').fill('0.005'); + await expect(page.getByText('Not enough funds')).toBeVisible(); + + // Custom fee setup + // await page.getByTestId('select-bar/custom').click(); + // await page.getByTestId('feePerUnit').fill('1'); + // await page.getByTestId('@coinmarket/exchange/compare-button').click(); + + // TOOD: #16041 Once solved, Verifies the offers displayed match the mock + + // pass through initial run and device auth check + // // Gets the deal + // await page.getByTestId('@coinmarket/exchange/offers/get-this-deal-button').first().click(); + // await page.getByTestId('@modal').isVisible(); + // await page.getByTestId('@coinmarket/exchange/offers/buy-terms-confirm-button').click(); + // // Verifies amounts, currencies and providers + // const wrapper = await page + // .locator('[class*="CoinmarketExchangeOfferInfo__Wrapper"]') + // .first(); + // await expect(wrapper.locator('[class*="FormattedCryptoAmount__Value"]').first()).toHaveText( + // testData.cryptoInput, + // ); + // await expect(wrapper.locator('[class*="FormattedCryptoAmount__Value"]').nth(1)).toHaveText( + // testData.ethValue, + // ); + // await expect( + // wrapper.locator('[class*="FormattedCryptoAmount__Container"]').first(), + // ).toContainText('REGTEST'); + // await expect( + // wrapper.locator('[class*="FormattedCryptoAmount__Container"]').last(), + // ).toContainText(testData.targetCrypto); + // await expect(wrapper.locator('[class*="CoinmarketProviderInfo__Text"]')).toHaveText( + // 'ChangeHero', + // ); + // // Verifies receiving address and its title + // const addressWrapper = await page.locator('[class*="VerifyAddress__Wrapper"]').first(); + // await expect(addressWrapper.locator('[class*="AccountLabeling__TabularNums"]')).toHaveText( + // 'Ethereum #1', + // ); + // await expect(addressWrapper.locator('[class*="Input__StyledInput"]')).toHaveValue( + // testData.ethAddress, + // ); + // // Confirming the transaction + // await page.getByTestId('@coinmarket/exchange/offers/confirm-on-trezor-button').click(); + // await page.getByTestId('@prompts/confirm-on-device'); + // await trezorUserEnvLink.pressYes(); + // await page.getByTestId('@coinmarket/exchange/offers/continue-transaction-button').click(); + // await page.getByTestId('@coinmarket/exchange/offers/confirm-on-trezor-and-send').click(); + // // Verification modal opens + // await page.locator('[class*="OutputElement__OutputWrapper"]').first(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/dashboard/assets.test.ts b/packages/suite-desktop-core/e2e/tests/dashboard/assets.test.ts new file mode 100644 index 000000000000..fa5e956ce749 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/dashboard/assets.test.ts @@ -0,0 +1,46 @@ +import { test, expect } from '../../support/fixtures'; + +test.describe('Assets', { tag: ['@group=suite'] }, () => { + test.use({ + emulatorStartConf: { wipe: true }, + emulatorSetupConf: { needs_backup: true }, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.completeOnboarding(); + }); + + test('User can initiate buy from Assets in table view', async ({ assetsPage, marketPage }) => { + await assetsPage.tableIcon.click(); + await assetsPage.buyAssetButton('btc').click(); + await expect(marketPage.section).toBeVisible(); + }); + + test('User can initiate buy from Assets in grid view', async ({ assetsPage, marketPage }) => { + await assetsPage.gridIcon.click(); + await assetsPage.buyAssetButton('btc').click(); + await expect(marketPage.section).toBeVisible(); + }); + + test('New asset is shown in both grid and row', async ({ + assetsPage, + dashboardPage, + settingsPage, + }) => { + await assetsPage.enableMoreCoins.click(); + await settingsPage.enableNetwork('eth'); + + await dashboardPage.navigateTo(); + await dashboardPage.discoveryShouldFinish(); + await expect(assetsPage.section).toHaveScreenshot('new-asset-grid.png', { + mask: [assetsPage.assetExchangeRate, assetsPage.assetWeekChange], + }); + + await assetsPage.tableIcon.click(); + await expect(assetsPage.section).toHaveScreenshot('new-asset-table.png', { + mask: [assetsPage.assetExchangeRate, assetsPage.assetWeekChange], + //The width of the table is not fixed -> higher maxDiffPixelRatio + maxDiffPixelRatio: 0.025, + }); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/dashboard/discreet-mode.test.ts b/packages/suite-desktop-core/e2e/tests/dashboard/discreet-mode.test.ts new file mode 100644 index 000000000000..8a196e64719e --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/dashboard/discreet-mode.test.ts @@ -0,0 +1,75 @@ +import { Locator } from '@playwright/test'; + +import { EventType } from '@trezor/suite-analytics'; +import { ExtractByEventType } from '@trezor/suite-web/e2e/support/types'; + +import { test, expect } from '../../support/fixtures'; + +const verifyHiddenAndRevealedValue = async ({ + locator, + hiddenValue = '$###', + revealedValue = '$0.00', +}: { + locator: Locator; + hiddenValue?: string; + revealedValue?: string; +}) => { + await expect.soft(locator).toHaveText(hiddenValue); + // Value is revealed on hover over text. But the locator might cover larger area then the text itself + // Text is centered to the left, so we click on 0,0 + await locator.hover({ position: { x: 0, y: 0 } }); + await expect.soft(locator).toHaveText(revealedValue); +}; + +test.describe('Discreet Mode', { tag: ['@group=suite'] }, () => { + test.use({ emulatorStartConf: { wipe: true } }); + test.beforeEach(async ({ analytics, dashboardPage, onboardingPage }) => { + await analytics.interceptAnalytics(); + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + }); + + test('Balances are hidden when user enables discreet mode', async ({ + analytics, + assetsPage, + dashboardPage, + walletPage, + }) => { + await dashboardPage.hideBalanceButton.click(); + + await test.step('Verify account value is hidden', async () => { + await verifyHiddenAndRevealedValue({ + locator: walletPage.balanceOfNetwork('btc'), + hiddenValue: '###', + revealedValue: '0', + }); + }); + + await test.step('Verify asset card value is hidden', async () => { + await verifyHiddenAndRevealedValue({ locator: assetsPage.assetFiatAmount('btc') }); + }); + + await test.step('Verify asset row value is hidden', async () => { + await assetsPage.tableIcon.click(); + await verifyHiddenAndRevealedValue({ locator: assetsPage.assetFiatAmount('btc') }); + }); + + await test.step('Verify Portfolio value is hidden', async () => { + await verifyHiddenAndRevealedValue({ locator: dashboardPage.portfolioFiatAmount }); + }); + + await test.step('Verify wallet value is hidden', async () => { + await dashboardPage.deviceSwitchingOpenButton.click(); + await verifyHiddenAndRevealedValue({ + locator: dashboardPage.walletAtIndexFiatAmount(0), + }); + }); + + test.step('Verify analytics event', () => { + const menuToggleDiscreetEvent = analytics.findAnalyticsEventByType< + ExtractByEventType + >(EventType.MenuToggleDiscreet); + expect(menuToggleDiscreetEvent.value).toBe('true'); + }); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts b/packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts similarity index 95% rename from packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts rename to packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts index ec13a1aa7811..2310b46caf87 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/custom-firmware.test.ts +++ b/packages/suite-desktop-core/e2e/tests/firmware/custom-firmware.test.ts @@ -4,7 +4,7 @@ import { test, expect } from '../../support/fixtures'; const firmwarePath = path.join(__dirname, '../../fixtures/trezor-2.5.1.bin'); -test.describe('Custom firmware', { tag: ['@group=settings'] }, () => { +test.describe('Custom firmware', { tag: ['@group=device-management'] }, () => { test.use({ emulatorStartConf: { wipe: true } }); test.beforeEach(async ({ onboardingPage, settingsPage }) => { await onboardingPage.completeOnboarding(); diff --git a/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts b/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts new file mode 100644 index 000000000000..b7bcb9e0cbb4 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/firmware/update-firmware.test.ts @@ -0,0 +1,39 @@ +import { test, expect } from '../../support/fixtures'; + +// Skip due to minimal value of this test at current state +test.describe.skip('Firmware update', { tag: ['@group=device-management'] }, () => { + test.use({ emulatorStartConf: { wipe: true, model: 'T2T1', version: '2.5.2' } }); + test.beforeEach(async ({ onboardingPage, dashboardPage }) => { + await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); + }); + + test('User triggers firmware update from a notification banner', async ({ page }) => { + await page.getByTestId('@notification/update-notification-banner').click(); + await page.getByTestId('@firmware/install-button').click(); + + await expect(page.getByTestId('@firmware-modal')).toBeVisible(); + // await expect(page.getByTestId('@firmware-modal')).toHaveScreenshot('check-seed.png'); + await page.getByTestId('@firmware/confirm-seed-checkbox').click(); + await page.getByTestId('@firmware/confirm-seed-button').click(); + + // we can't really test anything from this point since this https://github.com/trezor/trezor-suite/pull/12472 was merged + // in combination with not doing git lfs checkout in feature branches. Firmware will not be uploaded and an error is presented to user + // but only in feature branches, develop or production branches should display correct behavior. + + // one point to get over this would be to stub correct (bigger) firmware binary response here, but I don't know how to stub fetch that + // happens inside a nested iframe (connect-iframe). + // Update: Trezor env does not support bootloader atm https://github.com/trezor/trezor-user-env/issues/219 + + // reconnect in bootloader screen (disconnect) + // await expect(page.getByTestId('@firmware/disconnect-message')).toHaveText( + // 'Disconnect your Trezor', + // ); + // await trezorUserEnvLink.stopBridge(); + + // reconnect in bootloader screen (connect in bootloader) + // await expect(page.getByTestId('@firmware/connect-in-bootloader-message')).toHaveText( + // 'Swipe your finger across the touchscreen while connecting the USB cable.', + // ); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/general/suite-guide.test.ts b/packages/suite-desktop-core/e2e/tests/general/suite-guide.test.ts index a2b3c4e29e73..17efcf0ad2b5 100644 --- a/packages/suite-desktop-core/e2e/tests/general/suite-guide.test.ts +++ b/packages/suite-desktop-core/e2e/tests/general/suite-guide.test.ts @@ -1,6 +1,6 @@ import { test, expect } from '../../support/fixtures'; -test.describe('Suite Guide', { tag: '@suite' }, () => { +test.describe('Suite Guide', { tag: '@group=suite' }, () => { test.use({ startEmulator: false }); /** * Test case: diff --git a/packages/suite-desktop-core/e2e/tests/general/wallet-discovery.test.ts b/packages/suite-desktop-core/e2e/tests/general/wallet-discovery.test.ts index 3735d5be9126..6846257ac5c0 100644 --- a/packages/suite-desktop-core/e2e/tests/general/wallet-discovery.test.ts +++ b/packages/suite-desktop-core/e2e/tests/general/wallet-discovery.test.ts @@ -15,10 +15,10 @@ test.describe('Wallet discover tests', { tag: ['@group=wallet'] }, () => { * 1. Discover a standard wallet * 2. Verify discovery by checking a the first btc value under the graph */ - test('Discover a standard wallet', async ({ dashboardPage }) => { + test('Discover a standard wallet', async ({ dashboardPage, walletPage }) => { await dashboardPage.openDeviceSwitcher(); await dashboardPage.ejectWallet(); await dashboardPage.addStandardWallet(); - await expect(dashboardPage.balanceOfNetwork('btc').first()).toBeVisible(); + await expect(walletPage.balanceOfNetwork('btc').first()).toBeVisible(); }); }); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts new file mode 100644 index 000000000000..b558a8f97272 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts @@ -0,0 +1,78 @@ +import { test, expect } from '../../../support/fixtures'; + +test.describe('Onboarding - create wallet', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T1B1', version: '1-latest', wipe: true }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Success (basic)', async ({ + page, + analyticsPage, + onboardingPage, + devicePrompt, + trezorUserEnvLink, + }) => { + // Pass through analytics and firmware steps + await analyticsPage.passThroughAnalytics(); + await onboardingPage.firmwareContinueButton.click(); + + // Start wallet creation + await page.getByTestId('@onboarding/path-create-button').click(); + + // Confirm on device + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + // Skip backup + // It is possible to leave onboarding now + await expect(page.getByTestId('@onboarding/skip-backup')).toBeVisible(); + + // Start backup process + await page.getByTestId('@onboarding/create-backup-button').click(); + + // Check backup completion steps + await page.getByTestId('@backup/check-item/wrote-seed-properly').click(); + await page.getByTestId('@backup/check-item/made-no-digital-copy').click(); + await page.getByTestId('@backup/check-item/will-hide-seed').click(); + await expect(page.getByTestId('@onboarding/confirm-on-device')).not.toBeVisible(); + + await page.getByTestId('@backup/start-button').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + + for (let i = 0; i < 48; i++) { + await trezorUserEnvLink.pressYes(); + } + + await page.getByTestId('@backup/close-button').click(); + + // Proceed to PIN setup + // Now we are in PIN step, skip button is available + await expect(page.getByTestId('@onboarding/skip-button')).toBeVisible(); + + // Lets set PIN + await page.getByTestId('@onboarding/set-pin-button').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + // Simulate PIN mismatch + await page.getByTestId('@pin/input/1').click(); + await page.getByTestId('@pin/submit-button').click(); + await page.getByTestId('@pin/input/1').click(); + await page.getByTestId('@pin/input/1').click(); + await page.getByTestId('@pin/submit-button').click(); + await expect(page.getByTestId('@pin-mismatch')).toBeVisible(); + await page.getByTestId('@pin-mismatch/try-again-button').click(); + + // Retry PIN setup + await page.getByTestId('@onboarding/confirm-on-device').waitFor({ state: 'visible' }); + await trezorUserEnvLink.pressYes(); + + // Pin matrix appears again + await expect(page.getByTestId('@pin/input/1')).toBeVisible(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts new file mode 100644 index 000000000000..a9075088dbb8 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts @@ -0,0 +1,59 @@ +import { test, expect } from '../../../support/fixtures'; + +test.describe('Onboarding - recover wallet T1B1', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T1B1', version: '1-latest', wipe: true }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Incomplete run of advanced recovery', async ({ + onboardingPage, + analyticsPage, + devicePrompt, + recoveryPage, + page, + trezorUserEnvLink, + }) => { + // Navigate through onboarding steps + await analyticsPage.passThroughAnalytics(); + await onboardingPage.firmwareContinueButton.click(); + await onboardingPage.recoverWalletButton.click(); + + // Select advanced recovery + await recoveryPage.selectWordCount(24); + await page.getByTestId('@recover/select-type/advanced').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + // Simulate user input + for (let i = 0; i <= 4; i++) { + await page.getByTestId('@recovery/word-input-advanced/1').click({ force: true }); + } + + // Simulate device disconnection due to lack of cancel button + await page.waitForTimeout(501); + await trezorUserEnvLink.stopEmu(); + await devicePrompt.confirmOnDevicePromptIsShown(); + + // Restart emulator + await trezorUserEnvLink.startEmu({ model: 'T1B1', version: '1-latest' }); + + // Retry recovery with basic type + await onboardingPage.retryRecoveryButton.click(); + await recoveryPage.selectWordCount(12); + await page.getByTestId('@recover/select-type/basic').click(); + + // Confirm on device + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + // Ensure input field for basic recovery is visible + await expect(page.getByTestId('@word-input-select/input')).toBeVisible(); + + // Note: Completion of reading device data requires support in trezor-user-env + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts new file mode 100644 index 000000000000..eacda5350e60 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts @@ -0,0 +1,41 @@ +import { test } from '../../../support/fixtures'; + +test.describe('Onboarding - recover wallet T1B1', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T1B1', version: '1-latest', wipe: true }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Device disconnected during recovery offers retry', async ({ + onboardingPage, + analyticsPage, + recoveryPage, + devicePrompt, + trezorUserEnvLink, + }) => { + await analyticsPage.passThroughAnalytics(); + + // Start wallet recovery process + await onboardingPage.firmwareContinueButton.click(); + await onboardingPage.recoverWalletButton.click(); + await recoveryPage.selectWordCount(24); + await recoveryPage.selectBasicRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + // Disconnect the device + await trezorUserEnvLink.stopEmu(); + await devicePrompt.connectDevicePromptIsShown(); + await trezorUserEnvLink.startEmu({ model: 'T1B1', version: '1-latest', wipe: false }); + + // Retry recovery process + await onboardingPage.retryRecoveryButton.click(); + await recoveryPage.selectWordCount(24); + await recoveryPage.selectBasicRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts new file mode 100644 index 000000000000..e4901e8a37d8 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts @@ -0,0 +1,46 @@ +import { test, expect } from '../../../support/fixtures'; + +const mnemonic = + 'nasty answer gentle inform unaware abandon regret supreme dragon gravity behind lava dose pilot garden into dynamic outer hard speed luxury run truly armed'; + +test.describe('Onboarding - recover wallet T1B1', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T1B1', version: '1-latest', wipe: true }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Successfully recovers wallet from mnemonic', async ({ + page, + onboardingPage, + analyticsPage, + devicePrompt, + recoveryPage, + trezorInput, + trezorUserEnvLink, + }) => { + await analyticsPage.passThroughAnalytics(); + + // Start wallet recovery process + await onboardingPage.firmwareContinueButton.click(); + await onboardingPage.recoverWalletButton.click(); + await recoveryPage.selectWordCount(24); + await recoveryPage.selectBasicRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await page.waitForTimeout(1000); + await trezorUserEnvLink.pressYes(); + + // Input mnemonic + await trezorInput.inputMnemonicT1B1(mnemonic); + + // Finalize recovery, skip pin, and verify success + await onboardingPage.continueRecoveryButton.click(); + await onboardingPage.skipPin(); + await onboardingPage.continueCoinsButton.click(); + await expect(onboardingPage.finalTitle).toBeVisible(); + await expect(onboardingPage.finalTitle).toContainText('Setup complete!'); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts new file mode 100644 index 000000000000..7204c5d8ef8f --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts @@ -0,0 +1,50 @@ +import { test, expect } from '../../../support/fixtures'; + +test.describe('Onboarding - create wallet', { tag: ['@group=device-management'] }, () => { + // This test always needs to run the newest possible emulator version + // Emulator setup: wipe: true, model: T2T1, version: 2-latest + test.use({ + emulatorStartConf: { wipe: true, model: 'T2T1', version: '2-latest' }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Success (Shamir backup)', async ({ + page, + analyticsPage, + onboardingPage, + backupPage, + devicePrompt, + trezorUserEnvLink, + }) => { + await analyticsPage.passThroughAnalytics(); + await onboardingPage.firmwareContinueButton.click(); + + await page.getByTestId('@onboarding/path-create-button').click(); + + // Will be clicking on Shamir backup button + await page.getByTestId('@onboarding/select-seed-type-open-dialog').click(); + await page.getByTestId('@onboarding/select-seed-type-shamir-advanced').click(); + await page.getByTestId('@onboarding/select-seed-type-confirm').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + + await page.getByTestId('@onboarding/create-backup-button').click(); + + const shares = 3; + const threshold = 2; + await backupPage.passThroughShamirBackup(shares, threshold); + await page.getByTestId('@onboarding/set-pin-button').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.inputEmu('12'); + await trezorUserEnvLink.inputEmu('12'); + await expect(page.getByTestId('@prompts/confirm-on-device')).toBeVisible(); + await trezorUserEnvLink.pressYes(); + await expect(page.getByTestId('@onboarding/pin/continue-button')).toBeVisible(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts new file mode 100644 index 000000000000..13ebf5bdcf83 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts @@ -0,0 +1,41 @@ +import { test } from '../../../support/fixtures'; + +test.describe('Onboarding - recover wallet T2T1', { tag: ['@group=device-management'] }, () => { + // This test always needs to run the newest possible emulator version + // Emulator setup: wipe: true, model: T2T1, version: 2-latest + test.use({ + emulatorStartConf: { wipe: true, model: 'T2T1', version: '2-latest' }, + setupEmulator: false, + }); + + test.beforeEach(async ({ onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + }); + + test('Device disconnected during recovery offers retry', async ({ + page, + onboardingPage, + analyticsPage, + devicePrompt, + trezorUserEnvLink, + }) => { + await analyticsPage.passThroughAnalytics(); + + // Start wallet recovery process and confirm on device + await onboardingPage.firmwareContinueButton.click(); + await onboardingPage.recoverWalletButton.click(); + await onboardingPage.startRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + + // Disconnect device + await page.waitForTimeout(1000); + await trezorUserEnvLink.stopEmu(); + await page.waitForTimeout(500); + await devicePrompt.connectDevicePromptIsShown(); + await trezorUserEnvLink.startEmu({ model: 'T2T1', version: '2-latest', wipe: false }); + + // Check that you can retry + await onboardingPage.retryRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts new file mode 100644 index 000000000000..7d241d71feb0 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts @@ -0,0 +1,141 @@ +import { test } from '../../../support/fixtures'; + +const shareOneOfThree = [ + 'gesture', + 'necklace', + 'academic', + 'acid', + 'deadline', + 'width', + 'armed', + 'render', + 'filter', + 'bundle', + 'failure', + 'priest', + 'injury', + 'endorse', + 'volume', + 'terminal', + 'lunch', + 'drift', + 'diploma', + 'rainbow', +]; + +const shareTwoOfThree = [ + 'gesture', + 'necklace', + 'academic', + 'agency', + 'alpha', + 'ecology', + 'visitor', + 'raisin', + 'yelp', + 'says', + 'findings', + 'bulge', + 'rapids', + 'paper', + 'branch', + 'spelling', + 'cubic', + 'tactics', + 'formal', + 'disease', +]; + +test.describe('Onboarding - T2T1 in recovery mode', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { wipe: true, model: 'T2T1', version: '2.5.3' }, + setupEmulator: false, + }); + + test.beforeEach(async ({ page, onboardingPage, analyticsPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + + await analyticsPage.passThroughAnalytics(); + + await onboardingPage.skipFirmware(); + await page.getByTestId('@onboarding/path-recovery-button').click(); + }); + + test('Initial run with device that is already in recovery mode', async ({ + page, + trezorUserEnvLink, + onboardingPage, + analyticsPage, + devicePrompt, + indexedDb, + }) => { + // Start recovery with some device + await page.getByTestId('@onboarding/recovery/start-button').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.selectNumOfWordsEmu(20); + await trezorUserEnvLink.pressYes(); + await page.waitForTimeout(501); // Wait for device release + + // Disconnect device, reload application + await trezorUserEnvLink.stopEmu(); + await devicePrompt.connectDevicePromptIsShown(); + + await indexedDb.reset(); + await page.reload(); + + // Restart emulator and disable firmware hash check + await trezorUserEnvLink.startEmu({ wipe: false, model: 'T2T1', version: '2.5.3' }); + await onboardingPage.disableFirmwareHashCheck(); + + // Go through analytics opt-out again + await analyticsPage.passThroughAnalytics(); + + // Recovery device persisted after reload + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressNo(); + await trezorUserEnvLink.pressYes(); + }); + + test('Continue recovery after device is disconnected', async ({ + page, + trezorUserEnvLink, + devicePrompt, + }) => { + // Start recovery + await page.getByTestId('@onboarding/recovery/start-button').click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.selectNumOfWordsEmu(20); + await trezorUserEnvLink.pressYes(); + + // Enter first Shamir share + for (const word of shareOneOfThree) { + await trezorUserEnvLink.inputEmu(word); + } + + await devicePrompt.confirmOnDevicePromptIsShown(); + + // Disconnect and reconnect device + await trezorUserEnvLink.stopEmu(); + await devicePrompt.connectDevicePromptIsShown(); + await trezorUserEnvLink.startEmu({ wipe: false, model: 'T2T1', version: '2.5.3' }); + await devicePrompt.confirmOnDevicePromptIsShown(); + + // This is needed, because there seem to be some weird refreshes on the emu + // which means you confirm too early if you don't wait + await page.waitForTimeout(3000); + await trezorUserEnvLink.pressYes(); + + // Enter second Shamir share + for (const word of shareTwoOfThree) { + await trezorUserEnvLink.inputEmu(word); + } + + await trezorUserEnvLink.pressYes(); + await page.getByTestId('@onboarding/recovery/continue-button').click(); + await page.getByTestId('@onboarding/skip-button').click(); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts new file mode 100644 index 000000000000..d1e7a4879e72 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts @@ -0,0 +1,56 @@ +import { test, expect } from '../../../support/fixtures'; + +test.describe('Onboarding - recover wallet T2T1', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { wipe: true, model: 'T2T1', version: '2.5.3' }, + setupEmulator: false, + }); + test.beforeEach(async ({ analyticsPage, onboardingPage }) => { + await onboardingPage.disableFirmwareHashCheck(); + + analyticsPage.passThroughAnalytics(); + }); + + test('Successfully recovers wallet from mnemonic', async ({ + onboardingPage, + page, + devicePrompt, + trezorUserEnvLink, + }) => { + // Start wallet recovery process and confirm on device + await onboardingPage.skipFirmware(); + await onboardingPage.recoverWalletButton.click(); + await onboardingPage.startRecoveryButton.click(); + await devicePrompt.confirmOnDevicePromptIsShown(); + + await page.waitForTimeout(1000); + await trezorUserEnvLink.pressYes(); + + await devicePrompt.confirmOnDevicePromptIsShown(); + await page.waitForTimeout(1000); + await trezorUserEnvLink.pressYes(); + + await page.waitForTimeout(1000); + await trezorUserEnvLink.selectNumOfWordsEmu(12); + + await page.waitForTimeout(1000); + await trezorUserEnvLink.pressYes(); + + // Input mnemonic + await page.waitForTimeout(1000); + for (let i = 0; i < 12; i++) { + await trezorUserEnvLink.inputEmu('all'); + } + + // Confirm recovery success + await page.waitForTimeout(1000); + await trezorUserEnvLink.pressYes(); + + // Finalize recovery, skip pin, and check success + await onboardingPage.continueRecoveryButton.click(); + await onboardingPage.skipPin(); + await onboardingPage.continueCoinsButton.click(); + await expect(onboardingPage.finalTitle).toBeVisible(); + await expect(onboardingPage.finalTitle).toContainText('Setup complete!'); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/recovery/t1b1-dry-run.test.ts b/packages/suite-desktop-core/e2e/tests/recovery/t1b1-dry-run.test.ts new file mode 100644 index 000000000000..5bb91fb0fc46 --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/recovery/t1b1-dry-run.test.ts @@ -0,0 +1,36 @@ +import { test, expect } from '../../support/fixtures'; + +const mnemonic = + 'nasty answer gentle inform unaware abandon regret supreme dragon gravity behind lava dose pilot garden into dynamic outer hard speed luxury run truly armed'; +const pin = '1'; + +test.describe('Recovery T1B1 - dry run', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T1B1', version: '1-latest', wipe: true }, + emulatorSetupConf: { mnemonic, pin }, + }); + + test.beforeEach(async ({ onboardingPage, settingsPage }) => { + await onboardingPage.completeOnboarding(); + await settingsPage.navigateTo(); + await settingsPage.deviceTabButton.click(); + }); + + test('Standard recovery dry run', async ({ + settingsPage, + recoveryPage, + trezorInput, + trezorUserEnvLink, + devicePrompt, + }) => { + await settingsPage.checkSeedButton.click(); + await recoveryPage.initDryCheck('basic', 24); + await trezorInput.enterPinOnBlindMatrix(pin); + await trezorInput.inputMnemonicT1B1(mnemonic); + await expect(devicePrompt.modal).toContainText( + "Follow the instructions on your Trezor's screen", + ); + await trezorUserEnvLink.pressYes(); + await expect(recoveryPage.successTitle).toHaveText('Wallet backup checked successfully'); + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/recovery/t2t1-dry-run.test.ts b/packages/suite-desktop-core/e2e/tests/recovery/t2t1-dry-run.test.ts new file mode 100644 index 000000000000..35dfc886ca2a --- /dev/null +++ b/packages/suite-desktop-core/e2e/tests/recovery/t2t1-dry-run.test.ts @@ -0,0 +1,52 @@ +import { MNEMONICS } from '@trezor/trezor-user-env-link'; + +import { test, expect } from '../../support/fixtures'; + +const pin = '1'; + +test.describe('Recovery T2T1 - dry run', { tag: ['@group=device-management'] }, () => { + test.use({ + emulatorStartConf: { model: 'T2T1', wipe: true }, + emulatorSetupConf: { mnemonic: 'mnemonic_all', pin }, + }); + + test.beforeEach(async ({ onboardingPage, settingsPage }) => { + await onboardingPage.completeOnboarding(); + await settingsPage.navigateTo(); + await settingsPage.deviceTabButton.click(); + }); + + test('Standard recovery dry run', async ({ + settingsPage, + recoveryPage, + trezorUserEnvLink, + trezorInput, + }) => { + await settingsPage.checkSeedButton.click(); + await recoveryPage.userUnderstandsCheckbox.click(); + await recoveryPage.startButton.click(); + await expect(settingsPage.modal).toBeVisible(); + await expect(settingsPage.modal).toContainText( + 'Enter the words directly on your Trezor device in the correct order.', + ); + await trezorUserEnvLink.pressYes(); + await trezorUserEnvLink.inputEmu('1'); + await trezorUserEnvLink.selectNumOfWordsEmu(12); + await trezorUserEnvLink.pressYes(); + await trezorInput.inputMnemonicT2T1(MNEMONICS.mnemonic_all); + + await trezorUserEnvLink.pressYes(); + await expect(recoveryPage.successTitle).toHaveText('Wallet backup checked successfully'); + }); + + //TODO: #14987 Fix Recovery - dry run test for T2T1 + test.skip('Recovery with device reconnection', async () => { + // Start dry recovery check process + // First interrupt: Disconnect device and check that recovery process is paused + // Reinitialize process on device reconnect + // Now check that reconnecting device works and seed check procedure does reinitialize correctly + // Another interrupt: Reload page + // On app reload, recovery process should auto start if app detects initialized device in recovery mode + // Communication established, now finish the seed check process + }); +}); diff --git a/packages/suite-desktop-core/e2e/tests/settings/autodetect.test.ts b/packages/suite-desktop-core/e2e/tests/settings/autodetect.test.ts index 7dc5df850eb1..866572b96b62 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/autodetect.test.ts +++ b/packages/suite-desktop-core/e2e/tests/settings/autodetect.test.ts @@ -37,10 +37,10 @@ test.use({ startEmulator: false }); testCases.forEach(({ testName, userPreferences, text, textColor, bodyBackgroundColor }) => { test.describe.serial('Language and theme detection', { tag: ['@group=settings'] }, () => { test.use(userPreferences); - test(testName, async ({ onboardingPage }) => { + test(testName, async ({ onboardingPage, analyticsPage }) => { await onboardingPage.optionallyDismissFwHashCheckError(); - await expect(onboardingPage.analyticsHeading).toHaveText(text); - await expect(onboardingPage.analyticsHeading).toHaveCSS('color', textColor); + await expect(analyticsPage.heading).toHaveText(text); + await expect(analyticsPage.heading).toHaveCSS('color', textColor); await expect(onboardingPage.page.locator('body')).toHaveCSS( 'background-color', bodyBackgroundColor, diff --git a/packages/suite-desktop-core/e2e/tests/settings/electrum.test.ts b/packages/suite-desktop-core/e2e/tests/settings/electrum.test.ts index b795642358d5..1532c0731bfb 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/electrum.test.ts +++ b/packages/suite-desktop-core/e2e/tests/settings/electrum.test.ts @@ -17,6 +17,7 @@ test.describe.serial( test('Electrum completes discovery successfully', async ({ dashboardPage, settingsPage, + walletPage, }) => { test.info().annotations.push({ type: 'dependency', @@ -33,7 +34,7 @@ test.describe.serial( await dashboardPage.navigateTo(); await dashboardPage.discoveryShouldFinish(); - await expect(dashboardPage.balanceOfNetwork('regtest').first()).toBeVisible(); + await expect(walletPage.balanceOfNetwork('regtest').first()).toBeVisible(); }); }, ); diff --git a/packages/suite-desktop-core/e2e/tests/settings/t1b1-device-settings.test.ts b/packages/suite-desktop-core/e2e/tests/settings/t1b1-device-settings.test.ts index 3b9748fbedef..6a25ac78ff14 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/t1b1-device-settings.test.ts +++ b/packages/suite-desktop-core/e2e/tests/settings/t1b1-device-settings.test.ts @@ -10,15 +10,15 @@ test.describe('T1B1 - Device settings', { tag: ['@group=settings'] }, () => { await settingsPage.deviceTabButton.click(); }); - test('enable pin', async ({ page, trezorUserEnvLink, settingsPage }) => { + test('enable pin', async ({ page, trezorUserEnvLink, trezorInput }) => { await page.getByTestId('@settings/device/pin-switch').click(); await expect(page.getByTestId('@prompts/confirm-on-device')).toBeVisible(); await trezorUserEnvLink.pressYes(); const pinEntryNumber = '1'; - await settingsPage.enterPinOnBlindMatrix(pinEntryNumber); + await trezorInput.enterPinOnBlindMatrix(pinEntryNumber); await expect(page.getByTestId('@pin/input/1')).toBeVisible(); - await settingsPage.enterPinOnBlindMatrix(pinEntryNumber); + await trezorInput.enterPinOnBlindMatrix(pinEntryNumber); await expect(page.getByTestId('@toast/pin-changed')).toBeVisible(); }); diff --git a/packages/suite-desktop-core/e2e/tests/settings/t2b1-device-settings.test.ts b/packages/suite-desktop-core/e2e/tests/settings/t2b1-device-settings.test.ts index 484fcd5580e8..f58ffa8c3af4 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/t2b1-device-settings.test.ts +++ b/packages/suite-desktop-core/e2e/tests/settings/t2b1-device-settings.test.ts @@ -10,8 +10,9 @@ test.describe.serial('T2B1 - Device settings', { tag: ['@group=settings'] }, () emulatorStartConf: { version: '2-latest', model: 'T2B1', wipe: true }, }); - test.beforeEach(async ({ onboardingPage, settingsPage }) => { + test.beforeEach(async ({ onboardingPage, dashboardPage, settingsPage }) => { await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); await settingsPage.navigateTo(); await settingsPage.deviceTabButton.click(); }); @@ -39,12 +40,4 @@ test.describe.serial('T2B1 - Device settings', { tag: ['@group=settings'] }, () await trezorUserEnvLink.pressYes(); //TODO: Verification? }); - - test('Backup in settings', async ({ page }) => { - await expect(page.getByTestId('@settings/device/check-seed-button')).toBeEnabled(); - await page.getByTestId('@settings/device/failed-backup-row').waitFor({ state: 'detached' }); - await page.getByTestId('@settings/device/check-seed-button').click(); - await expect(page.getByTestId('@modal')).toBeVisible(); - //TODO: Verification? Should we actually do the backup? - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/settings/t2t1-device-settings.test.ts b/packages/suite-desktop-core/e2e/tests/settings/t2t1-device-settings.test.ts index 5839889787e2..fd1df4386e55 100644 --- a/packages/suite-desktop-core/e2e/tests/settings/t2t1-device-settings.test.ts +++ b/packages/suite-desktop-core/e2e/tests/settings/t2t1-device-settings.test.ts @@ -2,8 +2,9 @@ import { test, expect } from '../../support/fixtures'; test.describe('T2T1 - Device settings', { tag: ['@group=settings'] }, () => { test.use({ emulatorStartConf: { wipe: true, model: 'T2T1' } }); - test.beforeEach(async ({ onboardingPage, settingsPage }) => { + test.beforeEach(async ({ onboardingPage, dashboardPage, settingsPage }) => { await onboardingPage.completeOnboarding(); + await dashboardPage.discoveryShouldFinish(); await settingsPage.navigateTo(); await settingsPage.deviceTabButton.click(); }); @@ -41,14 +42,6 @@ test.describe('T2T1 - Device settings', { tag: ['@group=settings'] }, () => { //TODO: Any verification? }); - test('Backup in settings', async ({ page }) => { - await expect(page.getByTestId('@settings/device/check-seed-button')).toBeVisible(); - await page.getByTestId('@settings/device/failed-backup-row').waitFor({ state: 'detached' }); - await page.getByTestId('@settings/device/check-seed-button').click(); - await expect(page.getByTestId('@modal')).toBeVisible(); - //TODO: Verification? Should we actually do the backup? - }); - test('Can change homescreen background in firmware >= 2.5.4', async ({ settingsPage }) => { await settingsPage.changeDeviceBackground('original_t2t1'); }); diff --git a/packages/suite-desktop-core/src/index.d.ts b/packages/suite-desktop-core/src/index.d.ts index 3c39c93cfb37..b3b9bbcd9780 100644 --- a/packages/suite-desktop-core/src/index.d.ts +++ b/packages/suite-desktop-core/src/index.d.ts @@ -1,5 +1,8 @@ -// Include suite globals (as some dependencies from @suite can rely on them) -/// +interface Window { + // Needed for Cypress and Playwright + Playwright?: any; + store?: any; +} type LogMessage = { date: Date; @@ -13,30 +16,35 @@ declare interface ILogger { * Exit the Logger (will correctly end the log file) */ exit(); + /** * Error message (level: 1) * @param topic(string) - Log topic * @param message(string | string[]) - Message content(s) */ error(topic: string, message: string | string[]); + /** * Warning message (level: 2) * @param topic(string) - Log topic * @param message(string | string[]) - Message content(s) */ warn(topic: string, message: string | string[]); + /** * Info message (level: 3) * @param topic(string) - Log topic * @param message(string | string[]) - Message content(s) */ info(topic: string, message: string | string[]); + /** * Debug message (level: 4) * @param topic(string) - Log topic * @param message(string | string[]) - Message content(s) */ debug(topic: string, message: string | string[]); + /** * Log Level getter */ @@ -77,6 +85,7 @@ declare type BeforeRequestListener = ( declare interface RequestInterceptor { onBeforeRequest(listener: BeforeRequestListener): void; + offBeforeRequest(listener: BeforeRequestListener): void; } @@ -100,8 +109,10 @@ declare type UpdateSettings = { declare type TorSettings = { running: boolean; // Tor should be enabled host: string; // Hostname of the tor process through which traffic is routed - port: number; // Port of the tor process through which traffic is routed - snowflakeBinaryPath: string; // Path in user system to the snowflake binary + port: number; // Port of the Tor process through which traffic is routed + controlPort: number; // Port of the Tor Control Port + torDataDir: string; // Path of tor data directory + useExternalTor: boolean; // Tor should use external daemon instead of the one built-in suite. }; declare type BridgeSettings = { diff --git a/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts new file mode 100644 index 000000000000..a2e30fe459e7 --- /dev/null +++ b/packages/suite-desktop-core/src/libs/processes/TorExternalProcess.ts @@ -0,0 +1,42 @@ +import { TOR_CONTROLLER_STATUS, TorControllerExternal } from '@trezor/request-manager'; + +import { Status } from './BaseProcess'; + +export type TorProcessStatus = Status & { isBootstrapping?: boolean }; + +const DEFAULT_TOR_EXTERNAL_HOST = '127.0.0.1'; +const DEFAULT_TOR_EXTERNAL_PORT = 9050; + +export class TorExternalProcess { + isStopped = true; + torController: TorControllerExternal; + port = DEFAULT_TOR_EXTERNAL_PORT; + host = DEFAULT_TOR_EXTERNAL_HOST; + constructor() { + this.torController = new TorControllerExternal({ host: this.host, port: this.port }); + } + + public getPort() { + return this.port; + } + + public async status(): Promise { + const torControllerStatus = await this.torController.getStatus(); + + return { + service: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + process: torControllerStatus === TOR_CONTROLLER_STATUS.ExternalTorRunning, + isBootstrapping: false, // For Tor external we fake bootstrap process. + }; + } + + public async start(): Promise { + this.isStopped = false; + await this.torController.waitUntilAlive(); + } + + public stop() { + // We should not stop External Tor Process but ignore it. + this.isStopped = true; + } +} diff --git a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts index 6ac1d5b56225..2a543a7c5f8b 100644 --- a/packages/suite-desktop-core/src/libs/processes/TorProcess.ts +++ b/packages/suite-desktop-core/src/libs/processes/TorProcess.ts @@ -11,7 +11,6 @@ export class TorProcess extends BaseProcess { controlPort: number; torHost: string; torDataDir: string; - snowflakeBinaryPath: string; constructor(options: TorConnectionOptions) { super('tor', 'tor'); @@ -20,22 +19,20 @@ export class TorProcess extends BaseProcess { this.controlPort = options.controlPort; this.torHost = options.host; this.torDataDir = options.torDataDir; - this.snowflakeBinaryPath = ''; this.torController = new TorController({ host: this.torHost, port: this.port, controlPort: this.controlPort, torDataDir: this.torDataDir, - snowflakeBinaryPath: this.snowflakeBinaryPath, }); } - setTorConfig(torConfig: Pick) { - this.snowflakeBinaryPath = torConfig.snowflakeBinaryPath; + public getPort() { + return this.port; } - async status(): Promise { + public async status(): Promise { const torControllerStatus = await this.torController.getStatus(); return { @@ -45,12 +42,9 @@ export class TorProcess extends BaseProcess { }; } - async start(): Promise { + public async start(): Promise { const electronProcessId = process.pid; - const torConfiguration = await this.torController.getTorConfiguration( - electronProcessId, - this.snowflakeBinaryPath, - ); + const torConfiguration = this.torController.getTorConfiguration(electronProcessId); await super.start(torConfiguration); diff --git a/packages/suite-desktop-core/src/libs/store.ts b/packages/suite-desktop-core/src/libs/store.ts index f8a5cd9e02d0..42cd31a25af9 100644 --- a/packages/suite-desktop-core/src/libs/store.ts +++ b/packages/suite-desktop-core/src/libs/store.ts @@ -63,8 +63,10 @@ export class Store { return this.store.get('torSettings', { running: false, port: 9050, + controlPort: 9051, host: '127.0.0.1', - snowflakeBinaryPath: '', + useExternalTor: false, + torDataDir: '', }); } diff --git a/packages/suite-desktop-core/src/modules/tor.ts b/packages/suite-desktop-core/src/modules/tor.ts index 5c3ca1b5c3cc..3d2af751064b 100644 --- a/packages/suite-desktop-core/src/modules/tor.ts +++ b/packages/suite-desktop-core/src/modules/tor.ts @@ -12,28 +12,47 @@ import { getFreePort } from '@trezor/node-utils'; import { validateIpcMessage } from '@trezor/ipc-proxy'; import { TorProcess, TorProcessStatus } from '../libs/processes/TorProcess'; +import { TorExternalProcess } from '../libs/processes/TorExternalProcess'; import { app, ipcMain } from '../typed-electron'; import type { Dependencies } from './index'; const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) => { const { logger } = global; - const host = '127.0.0.1'; - const port = await getFreePort(); - const controlPort = await getFreePort(); - const torDataDir = path.join(app.getPath('userData'), 'tor'); const initialSettings = store.getTorSettings(); - store.setTorSettings({ ...initialSettings, host, port }); + store.setTorSettings({ + ...initialSettings, + port: await getFreePort(), + controlPort: await getFreePort(), + torDataDir: path.join(app.getPath('userData'), 'tor'), + }); + + const settings = store.getTorSettings(); - const tor = new TorProcess({ - host, - port, - controlPort, - torDataDir, - snowflakeBinaryPath: initialSettings.snowflakeBinaryPath, + const bundledTorProcess = new TorProcess({ + host: settings.host, + port: settings.port, + controlPort: settings.controlPort, + torDataDir: settings.torDataDir, }); + const externalTorProcess = new TorExternalProcess(); + + const getTarget = () => { + const { useExternalTor } = store.getTorSettings(); + + if (useExternalTor) { + return externalTorProcess; + } + + return bundledTorProcess; + }; + + const updateTorPort = (port: number) => { + store.setTorSettings({ ...store.getTorSettings(), port }); + }; + const setProxy = (rule: string) => { logger.info('tor', `Setting proxy rules to "${rule}"`); // Including network session of electron auto-updater in the Tor proxy. @@ -44,17 +63,26 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); }; - const getProxySettings = (shouldEnableTor: boolean) => - shouldEnableTor ? { proxy: `socks://${host}:${port}` } : { proxy: '' }; + const getProxySettings = (shouldEnableTor: boolean) => { + const { useExternalTor, port, host } = store.getTorSettings(); + return shouldEnableTor + ? { + proxy: `socks://${host}:${useExternalTor ? 9050 : port}`, + } + : { proxy: '' }; + }; const handleTorProcessStatus = (status: TorProcessStatus) => { + const { useExternalTor, running } = store.getTorSettings(); let type: TorStatus; if (!status.process) { type = TorStatus.Disabled; } else if (status.isBootstrapping) { type = TorStatus.Enabling; - } else if (status.service) { + } else if (status.service && !useExternalTor) { + type = TorStatus.Enabled; + } else if (useExternalTor && running) { type = TorStatus.Enabled; } else { type = TorStatus.Disabled; @@ -90,20 +118,51 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } }; + const createFakeBootstrapProcess = () => { + let progress = 0; + const duration = 3_000; + // update progress every 300ms. + const interval = 300; + + const increment = (100 / duration) * interval; + const intervalId = setInterval(() => { + progress += increment; + if (progress >= 100) { + progress = 100; + clearInterval(intervalId); + } + handleBootstrapEvent({ + type: 'progress', + progress: `${progress}`, + summary: 'Using External Tor fake progress', + }); + }, interval); + }; + const setupTor = async (shouldEnableTor: boolean) => { - const isTorRunning = (await tor.status()).process; - const { snowflakeBinaryPath } = store.getTorSettings(); + const { useExternalTor } = store.getTorSettings(); + + const isTorRunning = (await getTarget().status()).process; - if (shouldEnableTor === isTorRunning) { + if (shouldEnableTor === isTorRunning && !useExternalTor) { return; } if (shouldEnableTor === true) { - setProxy(`socks5://${host}:${port}`); - tor.torController.on('bootstrap/event', handleBootstrapEvent); + const { host } = store.getTorSettings(); + const port = getTarget().getPort(); + const proxyRule = `socks5://${host}:${port}`; + setProxy(proxyRule); + getTarget().torController.on('bootstrap/event', handleBootstrapEvent); + try { - tor.setTorConfig({ snowflakeBinaryPath }); - await tor.start(); + updateTorPort(port); + if (useExternalTor) { + await getTarget().start(); + createFakeBootstrapProcess(); + } else { + await getTarget().start(); + } } catch (error) { mainWindowProxy.getInstance()?.webContents.send('tor/bootstrap', { type: 'error', @@ -111,18 +170,19 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); // When there is error does not mean that the process is stop, // so we make sure to stop it so we are able to restart it. - tor.stop(); + getTarget().stop(); + throw error; } finally { - tor.torController.removeAllListeners(); + getTarget().torController.removeAllListeners(); } } else { mainWindowProxy.getInstance()?.webContents.send('tor/status', { type: TorStatus.Disabling, }); setProxy(''); - tor.torController.stop(); - await tor.stop(); + getTarget().torController.stop(); + await getTarget().stop(); } store.setTorSettings({ ...store.getTorSettings(), running: shouldEnableTor }); @@ -130,15 +190,13 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) ipcMain.handle( 'tor/change-settings', - (ipcEvent, { snowflakeBinaryPath }: { snowflakeBinaryPath: string }) => { + (ipcEvent, { useExternalTor }: { useExternalTor: boolean }) => { validateIpcMessage(ipcEvent); try { store.setTorSettings({ - running: store.getTorSettings().running, - host, - port, - snowflakeBinaryPath, + ...store.getTorSettings(), + useExternalTor, }); return { success: true }; @@ -175,6 +233,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) // correctly set in module trezor-connect-ipc. const proxySettings = getProxySettings(shouldEnableTor); + // Proxy is also set in packages/suite-desktop-core/src/modules/trezor-connect.ts await TrezorConnect.setProxy(proxySettings); logger.info( @@ -201,7 +260,8 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) } // Once Tor is toggled it renderer should know the new status. - const status = await tor.status(); + const status = await getTarget().status(); + handleTorProcessStatus(status); return { success: true }; @@ -211,11 +271,16 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) let lastCircuitResetTime = 0; const socksTimeout = 30000; // this value reflects --SocksTimeout flag set by TorController config mainThreadEmitter.on('module/reset-tor-circuits', event => { + if (store.getTorSettings().useExternalTor) { + logger.debug('tor', `Ignore circuit reset. Running External Tor without Control Port.`); + + return; + } const lastResetDiff = Date.now() - lastCircuitResetTime; if (lastResetDiff > socksTimeout) { logger.debug('tor', `Close active circuits. Triggered by identity ${event.identity}`); lastCircuitResetTime = Date.now(); - tor.torController.closeActiveCircuits(); + getTarget().torController.closeActiveCircuits(); } else { logger.debug( 'tor', @@ -225,8 +290,9 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) }); ipcMain.on('tor/get-status', async () => { - logger.debug('tor', `Getting status (${store.getTorSettings().running ? 'ON' : 'OFF'})`); - const status = await tor.status(); + const { running } = store.getTorSettings(); + logger.debug('tor', `Getting status (${running ? 'ON' : 'OFF'})`); + const status = await getTarget().status(); handleTorProcessStatus(status); }); @@ -235,7 +301,7 @@ const load = async ({ mainWindowProxy, store, mainThreadEmitter }: Dependencies) store.setTorSettings({ ...store.getTorSettings(), running: true }); } - return tor; + return getTarget; }; type TorModule = (dependencies: Dependencies) => { @@ -245,24 +311,24 @@ type TorModule = (dependencies: Dependencies) => { export const init: TorModule = dependencies => { let loaded = false; - let tor: TorProcess | undefined; + let getTarget: any; const onLoad = async () => { if (loaded) return { shouldRunTor: false }; loaded = true; - tor = await load(dependencies); - const torSettings = dependencies.store.getTorSettings(); + getTarget = await load(dependencies); + const { running } = dependencies.store.getTorSettings(); return { - shouldRunTor: torSettings.running, + shouldRunTor: running, }; }; const onQuit = async () => { const { logger } = global; logger.info('tor', 'Stopping (app quit)'); - await tor?.stop(); + await getTarget()?.stop(); }; return { onLoad, onQuit }; diff --git a/packages/suite-desktop-core/src/modules/trezor-connect.ts b/packages/suite-desktop-core/src/modules/trezor-connect.ts index eb8c1d375cf3..393e13896dc4 100644 --- a/packages/suite-desktop-core/src/modules/trezor-connect.ts +++ b/packages/suite-desktop-core/src/modules/trezor-connect.ts @@ -11,11 +11,10 @@ export const initBackground: ModuleInitBackground = ({ mainThreadEmitter, store const { logger } = global; logger.info(SERVICE_NAME, `Starting service`); - const setProxy = (ifRunning = false) => { - const tor = store.getTorSettings(); - if (ifRunning && !tor.running) return Promise.resolve(); - const payload = tor.running ? { proxy: `socks://${tor.host}:${tor.port}` } : { proxy: '' }; - logger.info(SERVICE_NAME, `${tor.running ? 'Enable' : 'Disable'} proxy ${payload.proxy}`); + const setProxy = () => { + const { running, host, port } = store.getTorSettings(); + const payload = running ? { proxy: `socks://${host}:${port}` } : { proxy: '' }; + logger.info(SERVICE_NAME, `${running ? 'Enable' : 'Disable'} proxy ${payload.proxy}`); return TrezorConnect.setProxy(payload); }; @@ -26,7 +25,7 @@ export const initBackground: ModuleInitBackground = ({ mainThreadEmitter, store logger.debug(SERVICE_NAME, `call ${method}`); if (method === 'init') { const response = await TrezorConnect[method](...params); - await setProxy(true); + await setProxy(); return response; } diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx index 8837ab00e8e4..a3538d94093e 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Available.tsx @@ -1,46 +1,15 @@ -import styled from 'styled-components'; - -import { - Card, - Checkbox, - Column, - Icon, - Markdown, - NewModal, - Paragraph, - Row, - Text, -} from '@trezor/components'; +import { Card, Checkbox, Column, Markdown, NewModal, Paragraph, H4 } from '@trezor/components'; import { desktopApi, UpdateInfo } from '@trezor/suite-desktop-api'; -import { borders, spacings, spacingsPx } from '@trezor/theme'; +import { spacings } from '@trezor/theme'; -import { FormattedDate, Translation } from 'src/components/suite'; +import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { download } from 'src/actions/suite/desktopUpdateActions'; import { selectSuiteFlags } from 'src/reducers/suite/suiteReducer'; import { setFlag } from 'src/actions/suite/suiteActions'; -import { Changelog } from './changelogComponents'; import { getVersionName } from './getVersionName'; -const GreenTag = styled.div` - display: flex; - align-items: center; - gap: ${spacingsPx.xxs}; - border-radius: ${borders.radii.full}; - background-color: ${({ theme }) => theme.backgroundPrimarySubtleOnElevation0}; - padding: ${spacingsPx.xxxs} ${spacingsPx.xs}; -`; - -const NewTag = () => ( - - - - - - -); - interface AvailableProps { onCancel: () => void; latest: UpdateInfo | undefined; @@ -85,47 +54,33 @@ export const Available = ({ onCancel, latest }: AvailableProps) => { } > - -
- - - - - - -
- - + +

+ +

+ + + + {latest?.changelog ? ( {latest?.changelog} ) : ( )} -
- - - {latest?.releaseDate && ( - - - - )} - - - - - - - - -
+ + + + + + ); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx index 475e61bfc522..104115f053f7 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Downloading.tsx @@ -1,34 +1,12 @@ import { useEffect, useState } from 'react'; -import styled from 'styled-components'; - import { UpdateProgress } from '@trezor/suite-desktop-api'; import { bytesToHumanReadable } from '@trezor/utils'; -import { H2, NewModal, ProgressBar, variables, Row, Column } from '@trezor/components'; +import { NewModal, ProgressBar, Paragraph, Column, Text, H3 } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation } from 'src/components/suite'; -const DownloadProgress = styled.span` - font-size: 20px; - font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD}; - color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY}; -`; - -const ReceivedData = styled.span` - color: ${({ theme }) => theme.legacy.TYPE_GREEN}; -`; - -const TotalData = styled.span` - color: ${({ theme }) => theme.legacy.TYPE_LIGHT_GREY}; -`; - -// eslint-disable-next-line local-rules/no-override-ds-component -const Text = styled(H2)` - color: ${({ theme }) => theme.legacy.TYPE_DARK_GREY}; - font-weight: ${variables.FONT_WEIGHT.MEDIUM}; -`; - interface DownloadingProps { hideWindow: () => void; progress?: UpdateProgress; @@ -52,30 +30,27 @@ export const Downloading = ({ hideWindow, progress }: DownloadingProps) => { } + iconName="download" > - - - {progress?.verifying ? ( - - - {ellipsisArray.filter((_, k) => k < step)} - - ) : ( - <> - - - - - - {bytesToHumanReadable(progress?.transferred || 0)} - - /{bytesToHumanReadable(progress?.total || 0)} - - - )} - - +

+ {progress?.verifying ? ( + <> + + {ellipsisArray.filter((_, k) => k < step)} + + ) : ( + + )} +

+ + + + {bytesToHumanReadable(progress?.transferred || 0)} + + {' / '} + {bytesToHumanReadable(progress?.total || 0)} + ); diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx index 10ca9f2cec21..fb89e6626018 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessDisable.tsx @@ -1,18 +1,9 @@ import { useCallback, useState } from 'react'; -import { useTheme } from 'styled-components'; - import { SUITE_URL } from '@trezor/urls'; import { analytics, EventType } from '@trezor/suite-analytics'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { - Button, - Column, - NewModal, - Paragraph, - IconCircleColors, - IconCircle, -} from '@trezor/components'; +import { NewModal, Paragraph, H3, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation, TrezorLink } from 'src/components/suite'; @@ -24,8 +15,6 @@ interface EarlyAccessDisableProps { export const EarlyAccessDisable = ({ hideWindow }: EarlyAccessDisableProps) => { const [enabled, setEnabled] = useState(true); - const theme = useTheme(); - const allowPrerelease = useCallback(() => { analytics.report({ type: EventType.SettingsGeneralEarlyAccess, @@ -37,63 +26,58 @@ export const EarlyAccessDisable = ({ hideWindow }: EarlyAccessDisableProps) => { setEnabled(false); }, []); - const purpleModalColorBranding: IconCircleColors = { - foreground: theme.iconAlertPurple, - background: theme.backgroundAlertPurpleSubtleOnElevationNegative, - }; - - const eapIconComponent = ( - - ); - return enabled ? ( } bottomContent={ <> - - + } > - - + +

+ +

+ - - +
) : ( } bottomContent={ <> - + - + } > - - + +

+ +

+ - - +
diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx index 4b2a33af6188..176c70414a83 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/EarlyAccessEnable.tsx @@ -1,19 +1,8 @@ import { useCallback, useState } from 'react'; -import { useTheme } from 'styled-components'; - import { analytics, EventType } from '@trezor/suite-analytics'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { - Button, - Paragraph, - Tooltip, - NewModal, - Card, - Column, - IconCircleColors, - IconCircle, -} from '@trezor/components'; +import { Paragraph, Tooltip, NewModal, H3, Column, Card } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { CheckItem, Translation } from 'src/components/suite'; @@ -26,8 +15,6 @@ export const EarlyAccessEnable = ({ hideWindow }: EarlyAccessEnableProps) => { const [understood, setUnderstood] = useState(false); const [enabled, setEnabled] = useState(false); - const theme = useTheme(); - const allowPrerelease = useCallback(() => { analytics.report({ type: EventType.SettingsGeneralEarlyAccess, @@ -41,78 +28,83 @@ export const EarlyAccessEnable = ({ hideWindow }: EarlyAccessEnableProps) => { const checkForUpdates = useCallback(() => desktopApi.checkForUpdates(true), []); - const purpleModalColorBranding: IconCircleColors = { - foreground: theme.iconAlertPurple, - background: theme.backgroundAlertPurpleSubtleOnElevationNegative, - }; - - const eapIconComponent = ( - - ); - return enabled ? ( } + iconName="eap" + variant="info" onCancel={hideWindow} bottomContent={ <> - - + } > - + +

+ +

+ + + +
) : ( } + iconName="eap" + variant="info" onCancel={hideWindow} bottomContent={ - - } - > - - + + + + + + + + } > - - + +

+ +

+ - - +
- - - } - description="" - isChecked={understood} - onClick={() => setUnderstood(!understood)} - /> -
+ + } + description="" + isChecked={understood} + onClick={() => setUnderstood(!understood)} + /> +
); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx index 7cd285da93c4..01f1702be7cc 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/JustUpdated.tsx @@ -1,12 +1,9 @@ import { useState, useCallback, useEffect } from 'react'; -import { Column, Markdown, NewModal, Paragraph } from '@trezor/components'; -import { spacings } from '@trezor/theme'; +import { Markdown, NewModal, Card } from '@trezor/components'; import { Translation } from 'src/components/suite'; -import { Changelog } from './changelogComponents'; - interface AvailableProps { onCancel: () => void; } @@ -29,14 +26,11 @@ export const JustUpdated = ({ onCancel }: AvailableProps) => { return ( - -
+ } - description={} onCancel={onCancel} bottomContent={ <> @@ -46,15 +40,17 @@ export const JustUpdated = ({ onCancel }: AvailableProps) => { } > - - - {changelog !== null ? ( - {changelog} - ) : ( - - )} - - + } + > + {changelog !== null ? ( + {changelog} + ) : ( + + )} +
); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx index 7f2d0a92a8ce..f8ac2c403cad 100644 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx +++ b/packages/suite-desktop-ui/src/support/DesktopUpdater/Ready.tsx @@ -1,4 +1,4 @@ -import { Button, NewModal, Paragraph, Row } from '@trezor/components'; +import { NewModal, Paragraph, H3, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { Translation } from 'src/components/suite'; @@ -20,25 +20,28 @@ export const Ready = ({ hideWindow }: ReadyProps) => { return ( } onCancel={installOnQuit} + iconName="download" bottomContent={ - - - - + + + + + } > - - - - - - + +

+ +

+ + {' '} + + +
); }; diff --git a/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx b/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx deleted file mode 100644 index ecc7a2be2b8d..000000000000 --- a/packages/suite-desktop-ui/src/support/DesktopUpdater/changelogComponents.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react'; - -import styled from 'styled-components'; - -import { borders, Elevation, mapElevationToBackground, spacingsPx } from '@trezor/theme'; -import { useElevation } from '@trezor/components'; - -const ChangelogWrapper = styled.div<{ $elevation: Elevation }>` - background-color: ${({ theme, $elevation }) => mapElevationToBackground({ theme, $elevation })}; - border-radius: ${borders.radii.md}; - max-height: 400px; - overflow-y: auto; - padding: ${spacingsPx.md} ${spacingsPx.xl}; -`; - -export const Changelog = ({ children }: { children: ReactNode }) => { - const { elevation } = useElevation(); - - return {children}; -}; diff --git a/packages/suite-desktop-ui/src/support/Router.tsx b/packages/suite-desktop-ui/src/support/Router.tsx index 4f954558a955..dd7e7a928d52 100644 --- a/packages/suite-desktop-ui/src/support/Router.tsx +++ b/packages/suite-desktop-ui/src/support/Router.tsx @@ -33,6 +33,7 @@ import { SettingsCoins } from 'src/views/settings/SettingsCoins/SettingsCoins'; import { SettingsDebug } from 'src/views/settings/SettingsDebug/SettingsDebug'; import { SettingsDevice } from 'src/views/settings/SettingsDevice/SettingsDevice'; import { Tokens } from 'src/views/wallet/tokens'; +import { Nfts } from 'src/views/wallet/nfts'; import PasswordManager from 'src/views/password-manager'; const components: { [key: string]: ComponentType } = { @@ -47,6 +48,7 @@ const components: { [key: string]: ComponentType } = { 'wallet-sign-verify': WalletSignVerify, 'wallet-anonymize': WalletAnonymize, 'wallet-tokens': Tokens, + 'wallet-nfts': Nfts, 'wallet-coinmarket-buy': CoinmarketBuyForm, 'wallet-coinmarket-buy-detail': CoinmarketBuyDetail, 'wallet-coinmarket-buy-offers': CoinmarketBuyOffers, diff --git a/packages/suite-web/e2e/fixtures/eth-account.ts b/packages/suite-web/e2e/fixtures/eth-account.ts index ae51aae5757f..bbe00cab4f6a 100644 --- a/packages/suite-web/e2e/fixtures/eth-account.ts +++ b/packages/suite-web/e2e/fixtures/eth-account.ts @@ -103,6 +103,7 @@ export const fixtures = [ bestHeight: 19960825, bestHash: '0x8339e411cd2f62b9493e36c444f7bd11ec716ceaa94f491e9726233d22abc024', block0Hash: '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3', + network: 'ETH', testnet: false, backend: { version: 'erigon/2.59.3/linux-amd64/go1.21.6', diff --git a/packages/suite-web/e2e/fixtures/send-form-doge.ts b/packages/suite-web/e2e/fixtures/send-form-doge.ts index 0c94e214f539..e17e33b8ac80 100644 --- a/packages/suite-web/e2e/fixtures/send-form-doge.ts +++ b/packages/suite-web/e2e/fixtures/send-form-doge.ts @@ -131,6 +131,7 @@ export const fixtures = [ bestHeight: 4484566, bestHash: '38fbb073e7eb4bfac9ed485dd0b3212247fb2a3d8b509dc9738c01a86ede33b3', block0Hash: '1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691', + network: 'DOGE', testnet: false, backend: { version: '1140600', subversion: '/Shibetoshi:1.14.6/' }, }, diff --git a/packages/suite-web/e2e/fixtures/send-form-ltc-mimble-wimble.ts b/packages/suite-web/e2e/fixtures/send-form-ltc-mimble-wimble.ts index c1c86db86711..cd7ac4240613 100644 --- a/packages/suite-web/e2e/fixtures/send-form-ltc-mimble-wimble.ts +++ b/packages/suite-web/e2e/fixtures/send-form-ltc-mimble-wimble.ts @@ -95,6 +95,7 @@ export const fixtures = [ bestHeight: 2373436, bestHash: '2c1bc2b99f8a4447a57dfc7b694b9c82bff1b3af7cce8ff151df01238dc07c8b', block0Hash: '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2', + network: 'LTC', testnet: false, backend: { version: '210201', subversion: '/LitecoinCore:0.21.2.1/' }, }, diff --git a/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts b/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts index 3afbf47cf146..84a9035defaf 100644 --- a/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts +++ b/packages/suite-web/e2e/support/pageObjects/switchDeviceObject.ts @@ -13,7 +13,10 @@ class SwitchDevice { clickAddLabel(index: number) { cy.wait(2000); // TODO fix waiting for animation - this.hoverOverWallet(index); + this.hoverAndCheck( + index, + `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/add-label-button"]`, + ); cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/add-label-button"]`, ).click(); @@ -21,7 +24,10 @@ class SwitchDevice { clickEditLabel(index: number) { cy.wait(2000); // TODO fix waiting for animation - this.hoverOverWallet(index); + this.hoverAndCheck( + index, + `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/edit-label-button"]`, + ); cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/edit-label-button"]`, ).click(); @@ -33,10 +39,20 @@ class SwitchDevice { cy.get('@metadataInput').type('{enter}'); } - private hoverOverWallet(index: number) { + private hoverAndCheck(index: number, checkSelector: string, retryCount = 2) { + if (retryCount === 0) { + throw new Error(`Failed to make the ${checkSelector} visible`); + } + cy.get( `${this.walletSelectorBeginPart}[data-testid$=":${index + 1}/hover-container"]`, ).realHover(); + cy.get(checkSelector).then($el => { + if (!$el.is(':visible')) { + cy.log(`Retrying hover and check for index ${index}`); + this.hoverAndCheck(index, checkSelector, retryCount - 1); + } + }); } } diff --git a/packages/suite-web/e2e/tests/coinmarket/buy.test.ts b/packages/suite-web/e2e/tests/coinmarket/buy.test.ts deleted file mode 100644 index af03dc5ae2b6..000000000000 --- a/packages/suite-web/e2e/tests/coinmarket/buy.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -// @group_other - -function setupAndAssertBuy(selectOffer: () => void) { - const testData = { - fiatInput: '500', - quoteBtcValue: '0.02073954', - tradeBtcValue: '0.02066953', // real situation: the final trade value may differ from the quote value - btcAddress: 'bc1q7ceqvaq7fqyywxqcx7qnfxkfk2ykpsla9pe80q', - }; - - // Tests all input windows are empty and country is set to Austria - cy.getTestElement('@coinmarket/form/country-select/input').should('contain.text', 'Austria'); - cy.getTestElement('@coinmarket/form/fiat-input').should('have.value', ''); - - // Tests set currencies are EUR and BTC - cy.getTestElement('@coinmarket/form/fiat-currency-select/input').should('contain.text', 'EUR'); - - cy.getTestElement('@coinmarket/form/select-crypto/input').should('contain.text', 'BTC'); - - // Fills out the form - cy.getTestElement('@coinmarket/form/fiat-input').type(testData.fiatInput, { - force: true, // The Fiat input contains the inner select box, that partially covers the input. - }); - - selectOffer(); - - cy.getTestElement('@modal').should('be.visible'); - cy.getTestElement('@coinmarket/buy/offers/buy-terms-confirm-button').click(); - cy.getTestElement('@coinmarket/offer/confirm-on-trezor-button').click(); - cy.getTestElement('@prompts/confirm-on-device'); - cy.task('pressYes'); - - // Verifies info on the confirmation page - cy.getTestElement('@coinmarket/form/info').should('exist'); - // Verifies fiat amount - cy.getTestElement('@coinmarket/form/info/fiat-amount') - .invoke('text') - .should('match', new RegExp(`€${testData.fiatInput}.[0-9][0-9]`)); - // Verifies crypto amount - cy.getTestElement('@coinmarket/form/info/crypto-amount').should( - 'contain.text', - testData.quoteBtcValue, - ); - // Verifies fiat amount ticker - cy.getTestElement('@coinmarket/form/info/crypto-amount').should('contain.text', 'BTC'); - // Verifies fiat provider - cy.getTestElement('@coinmarket/form/info/provider').invoke('text').should('be.equal', 'banxa'); - // Verifies fiat payment method - cy.getTestElement('@coinmarket/form/info/payment-method') - .invoke('text') - .should('be.equal', 'Bank Transfer'); - // Verifies receiving address - cy.getTestElement('@coinmarket/form/verify/address') - .invoke('text') - .should('be.equal', 'bc1q7ceqvaq7fqyywxqcx7qnfxkfk2ykpsla9pe80q'); - - // Moving on to the partner's site - cy.getTestElement('@coinmarket/offer/continue-transaction-button').click(); - - // Verifies that the buy trade was approved - cy.getTestElement('@coinmarket/detail/success').invoke('text').should('be.equal', 'Approved'); - - // Goes back, then on the Last transactions page after verifies the transaction is listed - cy.getTestElement('@coinmarket/menu/wallet-coinmarket-transactions').click(); - - // Verifies fiat amount - cy.getTestElement('@coinmarket/transaction/fiat-amount') - .invoke('text') - .should('match', new RegExp(`${testData.fiatInput} EUR`)); - // Verifies crypto amount - cy.getTestElement('@coinmarket/transaction/crypto-amount').should( - 'contain.text', - testData.tradeBtcValue, - ); - // Verifies fiat amount ticker - cy.getTestElement('@coinmarket/transaction/crypto-amount').should('contain', 'BTC'); - // Verifies fiat provider - cy.getTestElement('@coinmarket/form/info/provider').invoke('text').should('be.equal', 'banxa'); - // Verifies fiat payment method - cy.getTestElement('@coinmarket/form/info/payment-method') - .invoke('text') - .should('be.equal', 'Bank Transfer'); - - cy.getTestElement('@coinmarket/transaction/status') - .invoke('text') - .should('be.equal', 'Approved'); -} - -describe('Coinmarket buy', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu', { needs_backup: false }); - cy.task('startBridge'); - - cy.viewport(1200, 768).resetDb(); - cy.interceptInvityApi(); - cy.prefixedVisit('/', { - onBeforeLoad: (win: Window) => { - cy.stub(win, 'open').callsFake(() => { - let { href } = win.location; - // simulate redirect from partner back to Suite, prefix independent - href = href.replace( - new RegExp('/accounts/coinmarket/buy(/confirm)?#/btc/0'), - '/coinmarket-redirect#detail/btc/normal/0/mockedPaymentId3', - ); - win.location.href = href; - }); - }, - }); - cy.passThroughInitialRun(); - cy.discoveryShouldFinish(); - // navigate to buy - cy.getTestElement('@account-menu/btc/normal/0').click(); - cy.getTestElement('@wallet/menu/wallet-coinmarket-buy').click(); - }); - - /** - * 1. Navigates to Trade/Buy. - * 2. Verifies the mocked API response (country:AT). - * 3. Fills in an amount and clicks “Compare all offers”. - * 4. Verifies the mocked API response (only offers from the mocked file, e.g. banxa, btcdirect). - * 5. Picks one offer and clicks “Get this deal”. - * 6. Verifies that a modal opens. - * 7. Clicks the checkbox and “Confirm”. - * 8. Clicks “Confirm on Trezor” in Suite and on the emulator. - * 9. Verifies “Confirmed on Trezor” text. - * 10. Verifies the amount, currency, crypto, provider and payment method all match the mocked/given data. - * 11. Clicks “Finish transaction”. - * 12. Simulates interaction with the partner's site - * 13. Verifies that the buy trade was approved - * * 14. Goes back to the Buy tab and verifies the transaction is listed under "Trade transactions" - */ - it('Should buy crypto with comparing all offers successfully', () => { - setupAndAssertBuy(() => { - // Wait for input delay after pass the amount, delay is 500ms - cy.wait(1000); - - cy.getTestElement('@coinmarket/form/compare-button').click(); - - // Verifies the offers displayed match the mock - cy.fixture('./invity/buy/quotes').then((quotes: any) => { - const bankAccountQuotes = quotes.filter( - (quote: any) => quote.paymentMethod === 'bankTransfer', - ); - const exchangeProvider: string[] = ['banxa', 'btcdirect']; - - // Get all displayed quotes - cy.getTestElement('@coinmarket/offers/quote') - .should('exist') - // Loop all displayed quotes - .each(($el, elIndex) => { - // Test provider - cy.wrap($el) - .find('[data-testid="@coinmarket/offers/quote/provider"]') - .then($el2 => { - const text = $el2.text(); - - expect(exchangeProvider).to.include(text); - }); - // Test quote receive amount - cy.wrap($el) - .find('[data-testid="@coinmarket/offers/quote/crypto-amount"]') - .invoke('text') - .then((readValue: string) => { - const coinValueFromApp: number = parseFloat(readValue); - const coinValueFromQuote: number = parseFloat( - bankAccountQuotes[elIndex].receiveStringAmount, - ); - - expect(coinValueFromApp).to.be.eq(coinValueFromQuote); - }); - }); - }); - // Gets the deal - cy.getTestElement('@coinmarket/offers/get-this-deal-button').eq(0).click(); - }); - }); - - /** - * 1. Navigates to Trade/Buy. - * 2. Verifies the mocked API response (country:AT). - * 3. Fills in an amount and clicks “Buy”. - * 4. Verifies that a modal opens. - * 5. Clicks the checkbox and “Confirm”. - * 6. Clicks “Confirm on Trezor” in Suite and on the emulator. - * 7. Verifies “Confirmed on Trezor” text. - * 8. Verifies the amount, currency, crypto, provider and payment method all match the mocked/given data. - * 9. Clicks “Finish transaction”. - * 10. Simulates interaction with the partner's site - * 11. Verifies that the buy trade was approved - * * 12. Goes back to the Buy tab and verifies the transaction is listed under "Trade transactions" - */ - it('Should buy crypto best offer successfully', () => { - setupAndAssertBuy(() => { - cy.getTestElement('@coinmarket/form/buy-button').click(); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/coinmarket/exchange.test.ts b/packages/suite-web/e2e/tests/coinmarket/exchange.test.ts deleted file mode 100644 index 97c1ab06e309..000000000000 --- a/packages/suite-web/e2e/tests/coinmarket/exchange.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -// @group_coinmarket - -import { onNavBar } from '../../support/pageObjects/topBarObject'; - -describe.skip('Coinmarket exchange', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu', { - needs_backup: false, - mnemonic: - 'alcohol woman abuse must during monitor noble actual mixed trade anger aisle', - }); - cy.task('startBridge'); - - cy.viewport(1440, 2560).resetDb(); - cy.interceptInvityApi(); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - cy.discoveryShouldFinish(); - cy.enableRegtestAndGetCoins({ - payments: [ - { - address: 'bcrt1qnspxpr2xj9s2jt6qlhuvdnxw6q55jvyg6q7g5r', - amount: 1, - }, - ], - }); - - // Enables ETH account - onNavBar.openSettings(); - cy.getTestElement('@settings/menu/wallet').click(); - cy.getTestElement('@settings/wallet/network/eth').click(); - // cy.getTestElement('@settings/menu/close').click(); - - // Goes to Exchange - cy.getTestElement('@suite/menu/suite-index').click(); - cy.getTestElement('@account-menu/regtest/normal/0/label').click(); - cy.getTestElement('@wallet/menu/wallet-coinmarket-buy').click(); - cy.getTestElement('@suite/menu/wallet-coinmarket-exchange').click(); - }); - - /** - * Test case - * 1. Go to Accounts/REGTEST account/Trade/Exchange - * 2. Check whether all input windows are empty and crypto input is set on REGTEST - * 3. Exchange 0.005REGTEST for ETH with custom fee of 1 sat - * 4. Verifies the mocked offers are all fully and correctly displayed - * 5. Pick one offer - * 6. Verifies the amounts, currencies, addresses and providers are all in accordance with the mock - * 7. Confirms the transaction and verifies the same information in the modal - * 8. Return back to the Exchange tab in Trezor Suite - */ - it('Should exchange crypto successfully', () => { - const testData = { - cryptoInput: '0.005', - targetCrypto: 'ETH', - ethValue: '0.053845', - ethAddress: '0x3f2329C9ADFbcCd9A84f52c906E936A42dA18CB8', - }; - - cy.discoveryShouldFinish(); - // Tests all input windows are empty - cy.getTestElement('@coinmarket/exchange/crypto-input').should('have.value', ''); - cy.getTestElement('@coinmarket/exchange/fiat-input').should('have.value', ''); - - // Tests crypto input contains REGTEST - cy.getTestElement('@coinmarket/exchange/crypto-currency-select/input').should( - 'contain.text', - 'REGTEST', - ); - - // Fills out 0.005REGTEST and chooses ETH as target crypto */ - cy.getTestElement('@coinmarket/exchange/crypto-input').type(testData.cryptoInput); - cy.getTestElement('@coinmarket/exchange/receive-crypto-select/input').type('ETH{enter}'); - - // Custom fee setup - cy.getTestElement('select-bar/custom').click(); - cy.getTestElement('feePerUnit').clear(); - cy.getTestElement('feePerUnit').type('1'); - cy.getTestElement('@coinmarket/exchange/compare-button').click(); - - // Verifies the offers displayed match the mock - cy.fixture('./invity/exchange/quotes').then((quotes: any) => { - const exchangeProvider = [ - ['changehero', 'changehero'], - ['changenow', 'changenow'], - ['changelly', 'changelly'], - ]; - - exchangeProvider.forEach((provider: string[]) => { - // Tests offer accordance with the mocks - const valueFromFixtures = quotes.find( - (quote: any) => quote.exchange === provider[1], - ); - cy.contains('[class*="Quote__Wrapper"]', provider[0], { matchCase: false }) - .should('exist') - .find('[class*="CryptoAmount__Value"]') // Returns element handle - .invoke('text') - .then((readValue: string) => { - const ethValueFromApp: number = parseFloat(readValue); - const ethValueFromQuote: number = parseFloat( - valueFromFixtures.receiveStringAmount, - ); - expect(ethValueFromApp).to.be.eq(ethValueFromQuote); - }); - }); - }); - - // Gets the deal - cy.getTestElement('@coinmarket/exchange/offers/get-this-deal-button').eq(0).click(); - cy.getTestElement('@modal').should('be.visible'); - cy.getTestElement('@coinmarket/exchange/offers/buy-terms-confirm-button').click(); - - // Verifies amounts, currencies and providers - cy.get('[class*="CoinmarketExchangeOfferInfo__Wrapper"]') - .should('exist') - .then(wrapper => { - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Value"]') - .first() - .invoke('text') - .should('be.equal', testData.cryptoInput); - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Value"]') - .eq(1) - .invoke('text') - .should('be.equal', testData.ethValue); - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Container"]') - .first() - .should('contain.text', 'REGTEST'); - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Container"]') - .last() - .should('contain.text', testData.targetCrypto); - cy.wrap(wrapper) - .find('[class*="CoinmarketProviderInfo__Text"]') - .invoke('text') - .should('be.equal', 'ChangeHero'); - }); - - // Verifies receiving address and its title - cy.get('[class*="VerifyAddress__Wrapper"]') - .should('exist') - .then(wrapper => { - cy.wrap(wrapper) - .find('[class*="AccountLabeling__TabularNums"]') - .invoke('text') - .should('be.equal', 'Ethereum #1'); - cy.wrap(wrapper) - .find('[class*="Input__StyledInput"]') - .should('have.value', testData.ethAddress); - }); - - // Confirming the transaction - cy.getTestElement('@coinmarket/exchange/offers/confirm-on-trezor-button').click(); - cy.getTestElement('@prompts/confirm-on-device'); - cy.task('pressYes'); - cy.getTestElement('@coinmarket/exchange/offers/continue-transaction-button').click(); - cy.getTestElement('@coinmarket/exchange/offers/confirm-on-trezor-and-send').click(); - - // Verification modal opens - cy.get('[class*="OutputElement__OutputWrapper"]') - .should('exist') - .then(wrapper => { - cy.wrap(wrapper) - .find('[class*="OutputElement__OutputHeadline"]') - .first() - .invoke('text') - .should('be.equal', '2N4dH9yn4eYnnjHTYpN9xDmuMRS2k1AHWd8... '); - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Value"]') - .first() - .invoke('text') - .should('be.equal', testData.cryptoInput); - cy.wrap(wrapper) - .find('[class*="FormattedCryptoAmount__Symbol"]') - .first() - .should('contain.text', 'REGTEST'); - }); - cy.getTestElement('@modal').within(() => cy.contains('1 sat/vB')); - cy.task('pressYes'); - cy.task('pressYes'); - cy.getTestElement('@modal/send').should('not.be.disabled').click(); - - // Final check and return to the Exchange tab - cy.getTestElement('@toast/tx-sent').should('be.visible'); - cy.get('[class*="ToastNotification__Wrapper"]') - .should('exist') - .then(wrapper => { - cy.wrap(wrapper) - .find('[class*="HiddenPlaceholder__Wrapper"]') - .invoke('text') - .should('be.equal', '0.005 REGTEST'); - }); - cy.get('[class*="PaymentSuccessful__Wrapper"]') - .should('exist') - .then(wrapper => { - cy.wrap(wrapper) - .find('[class*="PaymentSuccessful__Title"]') - .invoke('text') - .should('be.equal', 'Approved'); - }); - cy.getTestElement('@coinmarket/exchange/payment/back-to-account').click(); - }); -}); diff --git a/packages/suite-web/e2e/tests/dashboard/assets.test.ts b/packages/suite-web/e2e/tests/dashboard/assets.test.ts deleted file mode 100644 index 1d2167c80bc9..000000000000 --- a/packages/suite-web/e2e/tests/dashboard/assets.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -// @group_suite -// @retry=2 - -import { EventType } from '@trezor/suite-analytics'; - -import { ExtractByEventType, Requests } from '../../support/types'; -import { onNavBar } from '../../support/pageObjects/topBarObject'; - -let requests: Requests; - -describe.skip('Assets', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu', { - needs_backup: true, - }); - cy.task('startBridge'); - - cy.viewport(1440, 2560).resetDb(); - - requests = []; - cy.interceptDataTrezorIo(requests); - }); - - it('checks that BTC and ETH accounts are available', () => { - cy.prefixedVisit('/'); - - cy.passThroughInitialRun(); - cy.discoveryShouldFinish(); - - // enable ethereum - onNavBar.openSettings(); - cy.getTestElement('@settings/menu/wallet').click(); - cy.getTestElement('@settings/wallet/network/eth').click(); - cy.getTestElement('@suite/menu/suite-index').click(); - - cy.get('[class^="AssetsView__Grid"]').then(grid => { - cy.wrap(grid).contains('Bitcoin').should('be.visible'); - cy.wrap(grid).contains('Ethereum').click(); - }); - - cy.findAnalyticsEventByType>( - requests, - EventType.SelectWalletType, - ).then(selectWalletTypeEvent => { - expect(selectWalletTypeEvent.type).to.equal('standard'); - }); - - cy.findAnalyticsEventByType>( - requests, - EventType.AccountsStatus, - ).then(() => { - // expect(parseInt(accountsStatusEvent.btc_normal.toString(), 10)).to.not.equal(NaN); - // expect(parseInt(accountsStatusEvent.btc_taproot.toString(), 10)).to.not.equal(NaN); - // expect(parseInt(accountsStatusEvent.btc_segwit.toString(), 10)).to.not.equal(NaN); - // expect(parseInt(accountsStatusEvent.btc_legacy.toString(), 10)).to.not.equal(NaN); - // expect(parseInt(accountsStatusEvent.eth_normal.toString(), 10)).to.not.equal(NaN); - }); - - // cy.findAnalyticsEventByType>( - // requests, - // EventType.AccountsNonZeroBalance, - // ).then(accountsNonZeroBalanceEvent => { - // // 0x73d0385F4d8E00C5e6504C6030F47BF6212736A8 has token and nobody will be able to move it without ETH - // expect(parseInt(accountsNonZeroBalanceEvent.eth_normal.toString(), 10)).to.not.equal( - // NaN, - // ); - // }); - - // cy.findAnalyticsEventByType>( - // requests, - // EventType.AccountsTokensStatus, - // ).then(accountsTokensStatusEvent => { - // // 0x73d0385F4d8E00C5e6504C6030F47BF6212736A8 has token and nobody will be able to move it without ETH - // expect(parseInt(accountsTokensStatusEvent.eth.toString(), 10)).to.not.equal(NaN); - // }); - }); -}); diff --git a/packages/suite-web/e2e/tests/dashboard/dashboard.test.ts b/packages/suite-web/e2e/tests/dashboard/dashboard.test.ts deleted file mode 100644 index 775b415f080e..000000000000 --- a/packages/suite-web/e2e/tests/dashboard/dashboard.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @group_suite -// @retry=2 - -describe('Dashboard', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu', { - needs_backup: true, - }); - cy.task('startBridge'); - - cy.viewport('macbook-13').resetDb(); - cy.interceptInvityApi(); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - }); - - const testCoinmarketInputs = () => { - cy.getTestElement('@coinmarket/form/select-crypto/input').should('exist'); - cy.getTestElement('@coinmarket/form/fiat-input').should('exist'); - cy.getTestElement('@coinmarket/form/country-select/input').should('exist'); - cy.getTestElement('@coinmarket/form/payment-method-select/input').should('exist'); - }; - - it('Assets table buy button', () => { - cy.getTestElement('@dashboard/assets/table-icon').click(); - cy.getTestElement('@dashboard/assets/table/btc/buy-button').click(); - - testCoinmarketInputs(); - }); - - it('Assets grid buy button', () => { - cy.getTestElement('@dashboard/assets/grid-icon').click(); - cy.getTestElement('@dashboard/assets/grid/btc/buy-button').click(); - - testCoinmarketInputs(); - }); - - // QA todo: test for graph - // QA todo: dashboard appearance for seed with tx history vs seed without tx history -}); diff --git a/packages/suite-web/e2e/tests/dashboard/discreet-mode.test.ts b/packages/suite-web/e2e/tests/dashboard/discreet-mode.test.ts deleted file mode 100644 index 7f88d021a3bc..000000000000 --- a/packages/suite-web/e2e/tests/dashboard/discreet-mode.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -// @group_suite -// @retry=2 - -import { EventType } from '@trezor/suite-analytics'; - -import { ExtractByEventType, Requests } from '../../support/types'; - -let requests: Requests; - -describe('Dashboard', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu'); - cy.task('startBridge'); - - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - - requests = []; - cy.interceptDataTrezorIo(requests); - }); - - /* - * 1. navigate to 'Dashboard' page - * 2. Enable discreet mode - * 3. check that status of Discreet mode - */ - it('Discreet mode checkbox', () => { - const discreetPartialClass = 'HiddenPlaceholder'; - - cy.discoveryShouldFinish(); - cy.getTestElement('@quickActions/hideBalances').click(); - cy.getTestElement('@wallet/coin-balance/value-btc') - .parent() - .parent() - .invoke('attr', 'class') - .then(className => { - console.log('className', className); - expect(className).to.contain(discreetPartialClass); - }); - - // could be that request was not yet intercepted. - // wait is not very nice, cy.findAnalyticsEventByType should implement some retry-ability mechanism internally - cy.wait(100); - - cy.findAnalyticsEventByType>( - requests, - EventType.MenuToggleDiscreet, - ).then(menuToggleDiscreetEvent => { - expect(menuToggleDiscreetEvent.value).to.equal('true'); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/firmware/firmware.test.ts b/packages/suite-web/e2e/tests/firmware/firmware.test.ts deleted file mode 100644 index 59a686c33c2a..000000000000 --- a/packages/suite-web/e2e/tests/firmware/firmware.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @group_device-management -// @retry=2 - -describe('Firmware', () => { - beforeEach(() => { - // use portrait mode monitor to prevent scrolling in settings - cy.viewport('macbook-13').resetDb(); - }); - - it(`Firmware 2.5.2 outdated notification banner should open firmware update modal`, () => { - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2.5.2' }); - cy.task('setupEmu'); - cy.task('startBridge'); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - cy.getTestElement('@notification/update-notification-banner').click(); - - // initial screen - cy.getTestElement('@firmware/install-button').click(); - - // check seed screen - cy.getTestElement('@modal/close-button').should('be.visible'); // modal is cancellable at this moment - cy.getTestElement('@firmware-modal').matchImageSnapshot('check-seed'); - cy.getTestElement('@firmware/confirm-seed-checkbox').click(); - cy.getTestElement('@firmware/confirm-seed-button').click(); - - // we can't really test anything from this point since this https://github.com/trezor/trezor-suite/pull/12472 was merged - // in combination with not doing git lfs checkout in feature branches. Firmware will not be uploaded and an error is presented to user - // but only in feature branches, develop or production branches should display correct behavior. - - // one point to get over this would be to stub correct (bigger) firmware binary response here, but I don't know how to stub fetch that - // happens inside a nested iframe (connect-iframe). - - // cy.prefixedVisit('/', { - // onBeforeLoad: (win: Window) => { - // cy.stub(win, 'fetch').callsFake( - // (uri: string, options: Parameters[1]) => { - // console.log('uri', uri); - // if (uri.includes('static/connect/data/firmware/t2t1/')) { - // return Promise.resolve( - // new Response(new ArrayBuffer(0), { status: 200 }), - // ); - // } - - // return fetch(uri, options); - // }, - // ); - // }, - // }); - - // // reconnect in bootloader screen (disconnect) - // cy.getTestElement('@firmware/disconnect-message', { timeout: 30000 }); - // cy.task('stopEmu'); - - // // reconnect in bootloader screen (connect in bootloader) - // cy.getTestElement('@firmware/connect-in-bootloader-message', { timeout: 20000 }); - // cy.log( - // 'And this is the end my friends. Emulator does not support bootloader, so we can not proceed with actual fw install', - // ); - }); -}); diff --git a/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts b/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts index d66a202611e8..64dbd77eeab9 100644 --- a/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts +++ b/packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts @@ -54,11 +54,11 @@ describe('Metadata - cancel metadata on device', () => { cy.task('pressNo'); // set wallet to remembered + cy.discoveryShouldFinish(); cy.getTestElement('@menu/switch-device').click(); cy.getTestElement('@viewOnlyStatus/disabled').click(); cy.getTestElement('@viewOnly/radios/enabled').click(); cy.safeReload(); - cy.discoveryShouldFinish(); // no dialogue, metadata keys survive together with remembered wallet! // but when user tries to add another wallet, there is enable labeling dialogue again cy.getTestElement('@menu/switch-device').click(); diff --git a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts b/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts deleted file mode 100644 index 0b8a097f9116..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-create-wallet.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// @group_device-management -// @retry=2 - -describe('Onboarding - create wallet', () => { - beforeEach(() => { - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: true }); - cy.task('startBridge'); - cy.viewport(1920, 1080).resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - }); - - // todo: skipping for it is too flaky.. - // after calling "resetDevice" we almost always receive "device disconnected during action" which is error sent by bridge. - it('Success (basic)', () => { - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@firmware/continue-button').click(); - - cy.getTestElement('@onboarding/path-create-button').click(); - // cy.getTestElement('@onboarding/only-backup-option-button').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.task('pressYes'); - - cy.getTestElement('@onboarding/skip-backup'); - cy.log('It is possible to leave onboarding now'); - - cy.getTestElement('@onboarding/create-backup-button').click(); - - // todo: these are "after checkboxes". is that correct? - cy.getTestElement('@backup/check-item/wrote-seed-properly').click(); - cy.getTestElement('@backup/check-item/made-no-digital-copy').click(); - cy.getTestElement('@backup/check-item/will-hide-seed').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('not.be.visible'); - - cy.getTestElement('@backup/start-button').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.wait(501); - - for (let i = 0; i < 48; i++) { - cy.task('pressYes'); - cy.wait(400); - } - - cy.getTestElement('@backup/close-button').click(); - - cy.log('Now we are in PIN step, skip button is available'); - cy.getTestElement('@onboarding/skip-button').should('be.visible'); - - cy.log('Lets set PIN'); - cy.getTestElement('@onboarding/set-pin-button').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - - cy.task('pressYes'); - - cy.log('PIN mismatch for now will be enough'); - cy.getTestElement('@pin/input/1').click(); - cy.getTestElement('@pin/submit-button').click(); - cy.getTestElement('@pin/input/1').click(); - cy.getTestElement('@pin/input/1').click(); - cy.getTestElement('@pin/submit-button').click(); - cy.getTestElement('@pin-mismatch'); - cy.getTestElement('@pin-mismatch/try-again-button').click(); - - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.task('pressYes'); - - cy.log('Pin matrix appears again'); - cy.getTestElement('@pin/input/1'); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts b/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts deleted file mode 100644 index 626dc4c3d5a4..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-advanced.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @group_bounty -// @retry=2 - -describe('Onboarding - recover wallet T1B1', () => { - beforeEach(() => { - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: true }); - cy.task('startBridge'); - - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - }); - - it('Incomplete run of advanced recovery', () => { - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@firmware/continue-button').click(); - cy.getTestElement('@onboarding/path-recovery-button').click(); - - // cy.getTestElement('@onboarding/button-continue').click(); - // cy.getTestElement('@firmware/continue-button').click(); - cy.getTestElement('@recover/select-count/24').click(); - cy.getTestElement('@recover/select-type/advanced').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.task('pressYes'); - - cy.log('typical user starts doing the T9 craziness'); - for (let i = 0; i <= 4; i++) { - cy.getTestElement('@recovery/word-input-advanced/1').click({ force: true }); - } - cy.log( - 'but after a while he finds he has no chance to finish it ever, so he disconnects device as there is no cancel button', - ); - cy.wait(501); - cy.task('stopEmu'); - cy.getTestElement('@connect-device-prompt', { timeout: 20000 }); - cy.task('startEmu', { model: 'T1B1', version: '1-latest' }); - - cy.getTestElement('@onboarding/recovery/retry-button').click(); - cy.getTestElement('@recover/select-count/12').click(); - cy.getTestElement('@recover/select-type/basic').click(); - - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.task('pressYes'); - cy.getTestElement('@word-input-select/input'); - - // todo: finish reading from device. needs support in trezor-user-env - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts b/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts deleted file mode 100644 index 3179cacbeb64..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-fail.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onOnboardingPage } from '../../../support/pageObjects/onboardingObject'; -import { onAnalyticsPage } from '../../../support/pageObjects/analyticsObject'; -import { onRecoverPage } from '../../../support/pageObjects/recoverObject'; -import { onConnectDevicePrompt } from '../../../support/pageObjects/connectDeviceObject'; - -describe('Onboarding - recover wallet T1B1', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: true }); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - - cy.step('Go through analytics', () => { - onAnalyticsPage.continue(); - onAnalyticsPage.continue(); - }); - }); - - it('Device disconnected during recovery offers retry', () => { - cy.step('Start wallet recovery process and confirm on device', () => { - onOnboardingPage.continueFirmware(); - onOnboardingPage.recoverWallet(); - onRecoverPage.selectWordCount(24); - onRecoverPage.selectBasicRecovery(); - onOnboardingPage.waitForConfirmationOnDevice(); - cy.wait(1000); - cy.task('pressYes'); - cy.wait(1000); - }); - - cy.step('Disconnect device', () => { - cy.task('stopEmu'); - cy.wait(500); - onConnectDevicePrompt.waitForConnectDevicePrompt(); - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: false }); - }); - - cy.step('Check that you can retry', () => { - onOnboardingPage.retryRecovery(); - onRecoverPage.selectWordCount(24); - onRecoverPage.selectBasicRecovery(); - onOnboardingPage.waitForConfirmationOnDevice(); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts b/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts deleted file mode 100644 index b8130303bfa1..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t1b1/t1b1-recovery-success.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onOnboardingPage } from '../../../support/pageObjects/onboardingObject'; -import { onWordInputPage } from '../../../support/pageObjects/wordInputObject'; -import { onAnalyticsPage } from '../../../support/pageObjects/analyticsObject'; -import { onRecoverPage } from '../../../support/pageObjects/recoverObject'; - -const mnemonic = [ - 'nasty', - 'answer', - 'gentle', - 'inform', - 'unaware', - 'abandon', - 'regret', - 'supreme', - 'dragon', - 'gravity', - 'behind', - 'lava', - 'dose', - 'pilot', - 'garden', - 'into', - 'dynamic', - 'outer', - 'hard', - 'speed', - 'luxury', - 'run', - 'truly', - 'armed', -]; - -describe('Onboarding - recover wallet T1B1', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: true }); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - - cy.step('Go through analytics', () => { - onAnalyticsPage.continue(); - onAnalyticsPage.continue(); - }); - }); - - it('Successfully recovers wallet from mnemonic', () => { - cy.step('Start wallet recovery process and confirm on device', () => { - onOnboardingPage.continueFirmware(); - onOnboardingPage.recoverWallet(); - onRecoverPage.selectWordCount(24); - onRecoverPage.selectBasicRecovery(); - onOnboardingPage.waitForConfirmationOnDevice(); - cy.wait(2000); - cy.task('pressYes'); - }); - - onWordInputPage.inputMnemonicT1B1(mnemonic); - - cy.step('Finalize recovery, skip pin and check success', () => { - onOnboardingPage.continueRecovery(); - onOnboardingPage.skipPin(); - onOnboardingPage.continueCoins(); - onOnboardingPage.checkOnboardingSuccess(); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts b/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts deleted file mode 100644 index 0da3ec306c42..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-create-wallet.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -// @group_device-management -// @retry=2 - -describe('Onboarding - create wallet', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - }); - - it('Success (Shamir backup)', () => { - // note: this is an example of test that can not be parametrized to be both integration (isolated) test and e2e test. - // the problem is that it always needs to run the newest possible emulator. If this was pinned to use emulator which is currently - // in production, and we locally bumped emulator version, we would get into a screen saying "update your firmware" and the test would fail. - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2-latest' }); - - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@firmware/continue-button').click(); - cy.getTestElement('@onboarding/path-create-button').click(); - - cy.log('Will be clicking on Shamir backup button'); - cy.getTestElement('@onboarding/select-seed-type-open-dialog').click(); - cy.getTestElement('@onboarding/select-seed-type-shamir-advanced').click(); - cy.getTestElement('@onboarding/select-seed-type-confirm').click(); - cy.getTestElement('@onboarding/confirm-on-device').should('be.visible'); - cy.task('pressYes'); - - cy.getTestElement('@onboarding/create-backup-button').click(); - - const shares = 3; - const threshold = 2; - cy.passThroughBackupShamir(shares, threshold); - cy.getTestElement('@onboarding/set-pin-button').click(); - cy.getTestElement('@onboarding/confirm-on-device'); - - cy.task('pressYes'); - cy.task('inputEmu', '12'); - cy.task('inputEmu', '12'); - cy.getTestElement('@prompts/confirm-on-device'); - cy.task('pressYes'); - cy.getTestElement('@onboarding/pin/continue-button'); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts b/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts deleted file mode 100644 index 2a87fab655d0..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-fail.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onAnalyticsPage } from '../../../support/pageObjects/analyticsObject'; -import { onConnectDevicePrompt } from '../../../support/pageObjects/connectDeviceObject'; -import { onOnboardingPage } from '../../../support/pageObjects/onboardingObject'; - -describe('Onboarding - recover wallet T2T1', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - // note: this is an example of test that can not be parametrized to be both integration (isolated) test and e2e test. - // the problem is that it always needs to run the newest possible emulator. If this was pinned to use emulator which is currently - // in production, and we locally bumped emulator version, we would get into a screen saying "update your firmware" and the test would fail. - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2-latest' }); - - cy.step('Go through analytics', () => { - onAnalyticsPage.continue(); - onAnalyticsPage.continue(); - }); - }); - - it('Device disconnected during recovery offers retry', () => { - cy.step('Start wallet recovery process and confirm on device', () => { - onOnboardingPage.continueFirmware(); - onOnboardingPage.recoverWallet(); - onOnboardingPage.startRecovery(); - - onOnboardingPage.waitForConfirmationOnDevice(); - }); - - cy.step('Disconnect device', () => { - cy.wait(1000); - cy.task('stopEmu'); - cy.wait(500); - onConnectDevicePrompt.waitForConnectDevicePrompt(); - cy.task('startEmu', { model: 'T2T1', version: '2-latest', wipe: false }); - }); - - cy.step('Check that you can retry', () => { - onOnboardingPage.retryRecovery(); - onOnboardingPage.waitForConfirmationOnDevice(); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts b/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts deleted file mode 100644 index 0909703ae87a..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-persistence.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onOnboardingPage } from '../../../support/pageObjects/onboardingObject'; - -const shareOneOfThree = [ - 'gesture', - 'necklace', - 'academic', - 'acid', - 'deadline', - 'width', - 'armed', - 'render', - 'filter', - 'bundle', - 'failure', - 'priest', - 'injury', - 'endorse', - 'volume', - 'terminal', - 'lunch', - 'drift', - 'diploma', - 'rainbow', -]; -const shareTwoOfThree = [ - 'gesture', - 'necklace', - 'academic', - 'agency', - 'alpha', - 'ecology', - 'visitor', - 'raisin', - 'yelp', - 'says', - 'findings', - 'bulge', - 'rapids', - 'paper', - 'branch', - 'spelling', - 'cubic', - 'tactics', - 'formal', - 'disease', -]; - -describe('Onboarding - T2T1 in recovery mode', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2.5.3' }); - cy.resetDb(); - cy.viewport('macbook-13'); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@analytics/continue-button').click(); - - onOnboardingPage.skipFirmware(); - - cy.getTestElement('@onboarding/path-recovery-button').click(); - }); - - it('Initial run with device that is already in recovery mode', () => { - // start recovery with some device - cy.getTestElement('@onboarding/recovery/start-button').click(); - cy.getTestElement('@onboarding/confirm-on-device'); - cy.task('pressYes'); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); - cy.task('selectNumOfWordsEmu', 20); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); // wait for device release - - // disconnect device, reload application - cy.task('stopEmu'); - cy.getTestElement('@connect-device-prompt', { timeout: 20000 }); - cy.wait(501); - cy.resetDb(); - cy.safeReload(); - - // now suite has reloaded. database is wiped. - cy.task('startEmu', { wipe: false, model: 'T2T1', version: '2.5.3' }); - cy.disableFirmwareHashCheck(); // need to disable again after reset - // analytics opt-out again - cy.getTestElement('@analytics/continue-button').click(); - cy.getTestElement('@analytics/continue-button').click(); - // recovery device persisted reload - cy.getTestElement('@onboarding/confirm-on-device'); - cy.wait(501); - cy.task('pressNo'); - cy.wait(501); - cy.task('pressYes'); - }); - - // https://github.com/trezor/trezor-suite/issues/2049 - it(` - 1. start recovery - 2. enter first shamir share - 3. reconnect device - 4. enter second shamir share - 5. recovery is finished - `, () => { - cy.getTestElement('@onboarding/recovery/start-button').click(); - cy.getTestElement('@onboarding/confirm-on-device'); - cy.task('pressYes'); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); - cy.task('selectNumOfWordsEmu', 20); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); - for (let i = 0; i < shareOneOfThree.length; i++) { - cy.task('inputEmu', shareOneOfThree[i]); - cy.wait(501); - } - cy.getTestElement('@onboarding/confirm-on-device'); - cy.wait(501); - cy.task('stopEmu'); - cy.wait(501); - cy.getTestElement('@connect-device-prompt', { timeout: 30000 }); - cy.task('startEmu', { wipe: false, model: 'T2T1', version: '2.5.3' }); - cy.getTestElement('@onboarding/confirm-on-device'); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); - cy.task('pressYes'); - cy.wait(501); - for (let i = 0; i < shareTwoOfThree.length; i++) { - cy.task('inputEmu', shareTwoOfThree[i]); - cy.wait(501); - } - cy.wait(501); - cy.task('pressYes'); - cy.getTestElement('@onboarding/recovery/continue-button').click(); - cy.getTestElement('@onboarding/skip-button').click(); - }); -}); diff --git a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts b/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts deleted file mode 100644 index d5ebe432a9de..000000000000 --- a/packages/suite-web/e2e/tests/onboarding/t2t1/t2t1-recovery-success.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onAnalyticsPage } from '../../../support/pageObjects/analyticsObject'; -import { onOnboardingPage } from '../../../support/pageObjects/onboardingObject'; - -describe('Onboarding - recover wallet T2T1', () => { - beforeEach(() => { - cy.task('startBridge'); - cy.task('startEmu', { wipe: true, model: 'T2T1', version: '2.5.3' }); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.disableFirmwareHashCheck(); - - cy.step('Go through analytics', () => { - onAnalyticsPage.continue(); - onAnalyticsPage.continue(); - }); - }); - - it('Successfully recovers wallet from mnemonic', () => { - cy.step('Start wallet recovery process and confirm on device', () => { - onOnboardingPage.skipFirmware(); - onOnboardingPage.recoverWallet(); - onOnboardingPage.startRecovery(); - - onOnboardingPage.waitForConfirmationOnDevice(); - cy.wait(1000); - cy.task('pressYes'); - - onOnboardingPage.waitForConfirmationOnDevice(); - cy.wait(1000); - cy.task('pressYes'); - - cy.wait(1000); - cy.task('selectNumOfWordsEmu', 12); - - cy.wait(1000); - cy.task('pressYes'); - }); - - cy.step('Input mnemonic', () => { - cy.wait(1000); - for (let i = 0; i < 12; i++) { - cy.task('inputEmu', 'all'); - } - }); - - cy.step('Confirm recovery success', () => { - cy.wait(1000); - cy.task('pressYes'); - }); - - cy.step('Finalize recovery, skip pin and check success', () => { - onOnboardingPage.continueRecovery(); - onOnboardingPage.skipPin(); - onOnboardingPage.continueCoins(); - onOnboardingPage.checkOnboardingSuccess(); - }); - }); -}); diff --git a/packages/suite-web/e2e/tests/recovery/t1b1-dry-run.test.ts b/packages/suite-web/e2e/tests/recovery/t1b1-dry-run.test.ts deleted file mode 100644 index 73480b85d928..000000000000 --- a/packages/suite-web/e2e/tests/recovery/t1b1-dry-run.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// @group_device-management - -import { SeedCheckType } from '../../support/enums/seedCheckType'; -import { onCheckSeedPage } from '../../support/pageObjects/checkSeedObject'; -import { onSettingsDevicePage } from '../../support/pageObjects/settings/settingsDeviceObject'; -import { onSettingsMenu } from '../../support/pageObjects/settings/settingsMenuObject'; -import { onNavBar } from '../../support/pageObjects/topBarObject'; -import { onWordInputPage } from '../../support/pageObjects/wordInputObject'; - -const mnemonic = [ - 'nasty', - 'answer', - 'gentle', - 'inform', - 'unaware', - 'abandon', - 'regret', - 'supreme', - 'dragon', - 'gravity', - 'behind', - 'lava', - 'dose', - 'pilot', - 'garden', - 'into', - 'dynamic', - 'outer', - 'hard', - 'speed', - 'luxury', - 'run', - 'truly', - 'armed', -]; - -function confirmSuccessOnDevice(): void { - cy.task('pressYes'); -} - -describe('Recovery T1B1 - dry run', () => { - const pin = '1'; - beforeEach(() => { - cy.task('startEmu', { model: 'T1B1', version: '1-latest', wipe: true }); - cy.wait(2000); - cy.task('setupEmu', { needs_backup: false, mnemonic: mnemonic.join(' '), pin }); - cy.task('startBridge'); - cy.viewport('macbook-13').resetDb(); - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - onNavBar.openSettings(); - onSettingsMenu.openDeviceSettings(); - }); - - it('Standard dry run', () => { - onSettingsDevicePage.openSeedCheck(); - onCheckSeedPage.initCheck(SeedCheckType.Standard, 24); - cy.enterPinOnBlindMatrix(pin); - onWordInputPage.inputMnemonicT1B1(mnemonic); - - confirmSuccessOnDevice(); - - onCheckSeedPage.verifyCheckSuccessful(); - }); -}); diff --git a/packages/suite-web/e2e/tests/recovery/t2t1-dry-run-persistence.test.ts b/packages/suite-web/e2e/tests/recovery/t2t1-dry-run-persistence.test.ts deleted file mode 100644 index 410b744e5c3f..000000000000 --- a/packages/suite-web/e2e/tests/recovery/t2t1-dry-run-persistence.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// @group_device-management -// @retry=2 - -import { onNavBar } from '../../support/pageObjects/topBarObject'; - -describe('Recovery - dry run', () => { - beforeEach(() => { - cy.task('startEmu', { wipe: true }); - cy.task('setupEmu', { mnemonic: 'mnemonic_all' }); - cy.task('startBridge'); - cy.viewport(1440, 2560).resetDb(); - }); - - // Test case skipped because it was unstable - // See the issue for more details - https://github.com/trezor/trezor-suite/issues/4128 - it.skip('Communication between device and application is automatically established whenever app detects device in recovery mode', () => { - cy.prefixedVisit('/'); - cy.passThroughInitialRun(); - onNavBar.openSettings(); - cy.getTestElement('@settings/menu/device').click(); - - cy.getTestElement('@settings/device/check-seed-button').click(); - cy.getTestElement('@recovery/user-understands-checkbox').click(); - cy.getTestElement('@recovery/start-button').click(); - cy.task('pressYes'); - cy.getTestElement('@prompts/confirm-on-device'); - - /* reinitialize process on device reconnect */ - cy.log( - 'Now check that reconnecting device works and seed check procedure does reinitialize correctly', - ); - cy.wait(501); - cy.task('stopEmu'); - cy.getTestElement('@recovery/close-button', { timeout: 30000 }).click(); - cy.getTestElement('@connect-device-prompt'); - cy.task('startEmu', { wipe: false }); - cy.getTestElement('@prompts/confirm-on-device', { timeout: 20000 }); - cy.task('pressYes'); - cy.log('At this moment, communication with device should be re-established'); - - /* reinitialize process on app reload */ - - cy.log( - 'On app reload, recovery process should auto start if app detects initialized device in recovery mode', - ); - - cy.safeReload().task('stopBridge').task('startBridge'); - cy.wait(2000); - - cy.getTestElement('@prompts/confirm-on-device'); - cy.task('pressYes'); - cy.task('selectNumOfWordsEmu', 12); - cy.task('pressYes'); - cy.log('Communication established, now finish the seed check process'); - - for (let i = 0; i < 12; i++) { - cy.task('inputEmu', 'all'); - } - cy.task('pressYes'); - - cy.getTestElement('@recovery/success-title'); - }); -}); diff --git a/packages/suite-web/src/global.d.ts b/packages/suite-web/src/global.d.ts index 1911ba2efacf..a3b5ee5e7702 100644 --- a/packages/suite-web/src/global.d.ts +++ b/packages/suite-web/src/global.d.ts @@ -1 +1,6 @@ -/// +interface Window { + // Needed for Cypress and Playwright + Playwright?: any; + TrezorConnect?: any; + store?: any; +} diff --git a/packages/suite-web/src/support/Router.tsx b/packages/suite-web/src/support/Router.tsx index 5e09600f4b35..77f67f69128f 100644 --- a/packages/suite-web/src/support/Router.tsx +++ b/packages/suite-web/src/support/Router.tsx @@ -31,6 +31,7 @@ const components: Record>> = { () => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/details'), ), 'wallet-tokens': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/tokens')), + 'wallet-nfts': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/nfts')), 'wallet-send': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/send')), 'wallet-staking': lazy(() => import(/* webpackChunkName: "wallet" */ 'src/views/wallet/staking/WalletStaking').then( diff --git a/packages/suite/global.d.ts b/packages/suite/global.d.ts index fc8993a795d2..a99316a3f99c 100644 --- a/packages/suite/global.d.ts +++ b/packages/suite/global.d.ts @@ -1,10 +1,3 @@ interface Window { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose; - chrome?: any; // Only in Chromium browsers - - // Needed for Cypress and Playwright - Cypress?: any; - Playwright?: any; - TrezorConnect?: any; - store?: any; } diff --git a/packages/suite/package.json b/packages/suite/package.json index 4d98b750653a..a8839304dc77 100644 --- a/packages/suite/package.json +++ b/packages/suite/package.json @@ -18,7 +18,7 @@ "test-unit:watch": "yarn g:jest -o --watch" }, "dependencies": { - "@everstake/wallet-sdk": "^0.3.66", + "@everstake/wallet-sdk": "^1.0.5", "@floating-ui/react": "^0.26.9", "@formatjs/intl": "2.10.0", "@hookform/resolvers": "3.9.1", @@ -27,6 +27,7 @@ "@sentry/core": "^7.100.1", "@solana/buffer-layout": "^4.0.1", "@solana/web3.js": "^2.0.0", + "@solana/web3.js-version1": "npm:@solana/web3.js@1.95.8", "@suite-common/analytics": "workspace:*", "@suite-common/assets": "workspace:*", "@suite-common/connect-init": "workspace:*", @@ -75,6 +76,7 @@ "@trezor/suite-analytics": "workspace:*", "@trezor/suite-data": "workspace:*", "@trezor/suite-desktop-api": "workspace:*", + "@trezor/suite-desktop-connect-popup": "workspace:*", "@trezor/suite-storage": "workspace:*", "@trezor/theme": "workspace:*", "@trezor/type-utils": "workspace:*", diff --git a/packages/suite/src/actions/suite/constants/suiteConstants.ts b/packages/suite/src/actions/suite/constants/suiteConstants.ts index c1d2a865368c..2839f30c44a6 100644 --- a/packages/suite/src/actions/suite/constants/suiteConstants.ts +++ b/packages/suite/src/actions/suite/constants/suiteConstants.ts @@ -1,3 +1,5 @@ +import type { NetworkSymbol } from '@suite-common/wallet-config'; + export const INIT = '@suite/init'; export const READY = '@suite/ready'; export const ERROR = '@suite/error'; @@ -35,3 +37,4 @@ export const LOCK_TYPE = { export const REQUEST_DEVICE_RECONNECT = '@suite/request-device-reconnect'; export const SET_EXPERIMENTAL_FEATURES = '@suite/set-experimental-features'; export const SET_SIDEBAR_WIDTH = '@suite/set-sidebar-width'; +export const EXPERIMENTAL_L2_NETWORKS: readonly NetworkSymbol[] = ['op', 'arb', 'base']; diff --git a/packages/suite/src/actions/suite/initAction.ts b/packages/suite/src/actions/suite/initAction.ts index 520723204eae..bf8bf0a9b916 100644 --- a/packages/suite/src/actions/suite/initAction.ts +++ b/packages/suite/src/actions/suite/initAction.ts @@ -9,6 +9,7 @@ import { } from '@suite-common/wallet-core'; import { periodicCheckTokenDefinitionsThunk } from '@suite-common/token-definitions'; import { desktopApi } from '@trezor/suite-desktop-api'; +import * as trezorConnectPopupActions from '@trezor/suite-desktop-connect-popup'; import { isDesktop } from '@trezor/env-utils'; import * as routerActions from 'src/actions/suite/routerActions'; @@ -115,7 +116,7 @@ export const init = () => async (dispatch: Dispatch, getState: GetState) => { // 14. init connect popup handler if (isDesktop()) { - dispatch(trezorConnectActions.connectPopupInitThunk()); + dispatch(trezorConnectPopupActions.connectPopupInitThunk()); } // 15. backend connected, suite is ready to use diff --git a/packages/suite/src/actions/suite/metadataActions.ts b/packages/suite/src/actions/suite/metadataActions.ts index 453df45bedc9..3ef7469cc8f4 100644 --- a/packages/suite/src/actions/suite/metadataActions.ts +++ b/packages/suite/src/actions/suite/metadataActions.ts @@ -3,6 +3,8 @@ import { createAction } from '@reduxjs/toolkit'; import { selectDevices } from '@suite-common/wallet-core'; import { Account } from '@suite-common/wallet-types'; import { StaticSessionId } from '@trezor/connect'; +import { createZip } from '@trezor/utils'; +import { notificationsActions } from '@suite-common/toast-notifications'; import { METADATA, METADATA_LABELING } from 'src/actions/suite/constants'; import { Dispatch, GetState } from 'src/types/suite'; @@ -18,6 +20,8 @@ import * as metadataUtils from 'src/utils/suite/metadata'; import { selectSelectedProviderForLabels } from 'src/reducers/suite/metadataReducer'; import type { AbstractMetadataProvider, PasswordManagerState } from 'src/types/suite/metadata'; +import { getProviderInstance } from './metadataProviderActions'; + export type MetadataAction = | { type: typeof METADATA.ENABLE } | { type: typeof METADATA.DISABLE } @@ -165,3 +169,55 @@ export const encryptAndSaveMetadata = async ({ return providerInstance.setFileContent(fileName, encrypted); }; + +export const exportMetadataToLocalFile = () => async (dispatch: Dispatch, getState: GetState) => { + const providerInstance = dispatch( + getProviderInstance({ + clientId: selectSelectedProviderForLabels(getState())!.clientId, + dataType: 'labels', + }), + ); + + if (!providerInstance) return; + + const filesListResult = await providerInstance.getFilesList(); + + if (!filesListResult.success || !filesListResult.payload?.length) { + dispatch( + notificationsActions.addToast({ type: 'error', error: 'Exporting labels failed' }), + ); + + return; + } + + const files = filesListResult.payload; + + return Promise.all( + files.map(file => { + return providerInstance.getFileContent(file).then(result => { + if (!result.success) throw new Error(result.error); + + return { name: file, content: result.payload }; + }); + }), + ) + .then(filesContent => { + const zipBlob = createZip(filesContent); + // Trigger download + const a = document.createElement('a'); + a.href = URL.createObjectURL(zipBlob); + a.download = 'archive.zip'; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(a.href); + }) + .catch(_err => { + dispatch( + notificationsActions.addToast({ type: 'error', error: 'Exporting labels failed' }), + ); + + return; + }); +}; diff --git a/packages/suite/src/actions/wallet/graphActions.ts b/packages/suite/src/actions/wallet/graphActions.ts index a58052b60881..225d4f9671fc 100644 --- a/packages/suite/src/actions/wallet/graphActions.ts +++ b/packages/suite/src/actions/wallet/graphActions.ts @@ -14,6 +14,7 @@ import { deviceGraphDataFilterFn, } from 'src/utils/wallet/graph'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; +import { State } from 'src/reducers/wallet/graphReducer'; import { ACCOUNT_GRAPH_SUCCESS, @@ -176,38 +177,42 @@ export const updateGraphData = }); }; -export const getGraphDataForInterval = - (options: { account?: Account; deviceState?: StaticSessionId }) => - (_dispatch: Dispatch, getState: GetState) => { - const { graph } = getState().wallet; - const { selectedRange } = graph; - - const data: GraphData[] = []; - graph.data.forEach(accountGraph => { - const accountFilter = options.account - ? accountGraphDataFilterFn(accountGraph, options.account) - : true; - const deviceFilter = options.deviceState - ? deviceGraphDataFilterFn(accountGraph, options.deviceState) - : true; - - if (accountFilter && deviceFilter) { - if (selectedRange.startDate && selectedRange.endDate) { - data.push({ - ...accountGraph, - data: - accountGraph.data?.filter(d => - isWithinInterval(fromUnixTime(d.time), { - start: selectedRange.startDate, - end: selectedRange.endDate, - }), - ) ?? [], - }); - } else { - data.push(accountGraph); - } +// TODO: should be in graphUtils +export const getGraphDataForInterval = ({ + account, + deviceState, + graph, +}: { + account?: Account; + deviceState?: StaticSessionId; + graph: State; +}) => { + const { selectedRange } = graph; + + const data: GraphData[] = []; + graph.data.forEach(accountGraph => { + const accountFilter = account ? accountGraphDataFilterFn(accountGraph, account) : true; + const deviceFilter = deviceState + ? deviceGraphDataFilterFn(accountGraph, deviceState) + : true; + + if (accountFilter && deviceFilter) { + if (selectedRange.startDate && selectedRange.endDate) { + data.push({ + ...accountGraph, + data: + accountGraph.data?.filter(d => + isWithinInterval(fromUnixTime(d.time), { + start: selectedRange.startDate, + end: selectedRange.endDate, + }), + ) ?? [], + }); + } else { + data.push(accountGraph); } - }); + } + }); - return data; - }; + return data; +}; diff --git a/packages/suite/src/actions/wallet/send/sendFormThunks.ts b/packages/suite/src/actions/wallet/send/sendFormThunks.ts index 84b8dff34bc2..b74127fb1eb7 100644 --- a/packages/suite/src/actions/wallet/send/sendFormThunks.ts +++ b/packages/suite/src/actions/wallet/send/sendFormThunks.ts @@ -17,6 +17,7 @@ import { signTransactionThunk, sendFormActions, selectPrecomposedSendForm, + cancelSignSendFormTransactionThunk, } from '@suite-common/wallet-core'; import { isCardanoTx, isRbfTransaction } from '@suite-common/wallet-utils'; import { MetadataAddPayload } from '@suite-common/metadata-types'; @@ -270,6 +271,7 @@ export const signAndPushSendFormTransactionThunk = createThunk( ); } + // This thunk uses precomposedForm so it must be called before cleanup. dispatch( applySendFormMetadataLabelsThunk({ selectedAccount, @@ -278,6 +280,9 @@ export const signAndPushSendFormTransactionThunk = createThunk( }), ); + // Clean send form state and close review modal. + dispatch(cancelSignSendFormTransactionThunk()); + return result; }, ); diff --git a/packages/suite/src/actions/wallet/stake/stakeFormActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts new file mode 100644 index 000000000000..f24e9054feb8 --- /dev/null +++ b/packages/suite/src/actions/wallet/stake/stakeFormActions.ts @@ -0,0 +1,156 @@ +import { BigNumber } from '@trezor/utils/src/bigNumber'; +import { FeeLevel } from '@trezor/connect'; +import { + calculateTotal, + calculateMax, + getExternalComposeOutput, + formatAmount, +} from '@suite-common/wallet-utils'; +import { + StakeFormState, + PrecomposedLevels, + PrecomposedTransaction, + ExternalOutput, +} from '@suite-common/wallet-types'; +import { ComposeActionContext } from '@suite-common/wallet-core'; +import { getNetworkDisplaySymbol, NetworkSymbol } from '@suite-common/wallet-config'; + +type StakingParams = { + feeInBaseUnits: string; + minBalanceForStakingInBaseUnits: string; + minAmountForStakingInBaseUnits: string; + minAmountForWithdrawalInBaseUnits: string; +}; + +export const calculate = ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount = true, + symbol: NetworkSymbol, + stakingParams: StakingParams, +): PrecomposedTransaction => { + const { + feeInBaseUnits, + minBalanceForStakingInBaseUnits, + minAmountForStakingInBaseUnits, + minAmountForWithdrawalInBaseUnits, + } = stakingParams; + + let amount: string; + let max: string | undefined; + + if (output.type === 'send-max' || output.type === 'send-max-noaddress') { + const minAmountWithFeeInBaseUnits = new BigNumber(minBalanceForStakingInBaseUnits).plus( + feeInBaseUnits, + ); + + if (new BigNumber(availableBalance).lt(minAmountWithFeeInBaseUnits)) { + max = minAmountForStakingInBaseUnits; + } else { + max = new BigNumber(calculateMax(availableBalance, feeInBaseUnits)) + .minus(minAmountForWithdrawalInBaseUnits) + .toString(); + } + + amount = max; + } else { + amount = output.amount; + } + + const totalSpent = new BigNumber(calculateTotal(amount, feeInBaseUnits)); + + if ( + new BigNumber(feeInBaseUnits).gt(availableBalance) || + (compareWithAmount && totalSpent.isGreaterThan(availableBalance)) + ) { + const error = 'TR_STAKE_NOT_ENOUGH_FUNDS'; + + // errorMessage declared later + return { + type: 'error', + error, + errorMessage: { id: error, values: { symbol: symbol.toUpperCase() } }, + } as const; + } + + const payloadData = { + type: 'nonfinal' as const, + totalSpent: totalSpent.toString(), + max, + fee: feeInBaseUnits, + feePerByte: feeLevel.feePerUnit, + feeLimit: feeLevel.feeLimit, + bytes: 0, + inputs: [], + }; + + if (output.type === 'send-max' || output.type === 'payment') { + return { + ...payloadData, + type: 'final', + // compatibility with BTC PrecomposedTransaction from @trezor/connect + inputs: [], + outputsPermutation: [0], + outputs: [ + { + address: output.address, + amount, + script_type: 'PAYTOADDRESS', + }, + ], + }; + } + + return payloadData; +}; + +export const composeStakingTransaction = ( + formValues: StakeFormState, + formState: ComposeActionContext, + predefinedLevels: FeeLevel[], + calculateTransaction: ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount: boolean, + symbol: NetworkSymbol, + ) => PrecomposedTransaction, + customFeeLimit?: string, +) => { + const { account, network } = formState; + const composeOutputs = getExternalComposeOutput(formValues, account, network); + if (!composeOutputs) return; // no valid Output + + const { output, decimals } = composeOutputs; + const { availableBalance } = account; + + // wrap response into PrecomposedLevels object where key is a FeeLevel label + const wrappedResponse: PrecomposedLevels = {}; + const compareWithAmount = formValues.stakeType === 'stake'; + const response = predefinedLevels.map(level => + calculateTransaction(availableBalance, output, level, compareWithAmount, account.symbol), + ); + response.forEach((tx, index) => { + const feeLabel = predefinedLevels[index].label as FeeLevel['label']; + wrappedResponse[feeLabel] = tx; + }); + + // format max (calculate sends it as satoshi) + // update errorMessage values (symbol) + Object.keys(wrappedResponse).forEach(key => { + const tx = wrappedResponse[key]; + if (tx.type !== 'error') { + tx.max = tx.max ? formatAmount(tx.max, decimals) : undefined; + tx.estimatedFeeLimit = customFeeLimit ?? tx.estimatedFeeLimit; + } + if (tx.type === 'error' && tx.error === 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE') { + tx.errorMessage = { + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE', + values: { networkSymbol: getNetworkDisplaySymbol(network.symbol) }, + }; + } + }); + + return wrappedResponse; +}; diff --git a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts index 9b8b7c2089fc..917b9698221f 100644 --- a/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts +++ b/packages/suite/src/actions/wallet/stake/stakeFormEthereumActions.ts @@ -3,18 +3,9 @@ import { toWei } from 'web3-utils'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import TrezorConnect, { FeeLevel } from '@trezor/connect'; import { notificationsActions } from '@suite-common/toast-notifications'; -import { - calculateTotal, - calculateMax, - calculateEthFee, - getExternalComposeOutput, - formatAmount, - isPending, - getAccountIdentity, -} from '@suite-common/wallet-utils'; +import { calculateEthFee, isPending, getAccountIdentity } from '@suite-common/wallet-utils'; import { StakeFormState, - PrecomposedLevels, PrecomposedTransaction, PrecomposedTransactionFinal, ExternalOutput, @@ -27,7 +18,7 @@ import { UNSTAKE_INTERCHANGES, } from '@suite-common/wallet-constants'; import { selectSelectedDevice, ComposeActionContext } from '@suite-common/wallet-core'; -import type { NetworkSymbol } from '@suite-common/wallet-config'; +import { type NetworkSymbol } from '@suite-common/wallet-config'; import { Dispatch, GetState } from 'src/types/suite'; import { selectAddressDisplayType } from 'src/reducers/suite/suiteReducer'; @@ -36,9 +27,11 @@ import { prepareClaimEthTx, prepareStakeEthTx, prepareUnstakeEthTx, -} from 'src/utils/suite/stake'; +} from 'src/utils/suite/ethereumStaking'; + +import { calculate, composeStakingTransaction } from './stakeFormActions'; -const calculate = ( +const calculateTransaction = ( availableBalance: string, output: ExternalOutput, feeLevel: FeeLevel, @@ -47,88 +40,27 @@ const calculate = ( ): PrecomposedTransaction => { const feeInWei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0'); - let amount: string; - let max: string | undefined; - - if (output.type === 'send-max' || output.type === 'send-max-noaddress') { - const minEthBalanceForStakingWei = toWei(MIN_ETH_BALANCE_FOR_STAKING.toString(), 'ether'); - const minAmountWithFeeWei = new BigNumber(minEthBalanceForStakingWei).plus(feeInWei); - - if (new BigNumber(availableBalance).lt(minAmountWithFeeWei)) { - max = toWei(MIN_ETH_AMOUNT_FOR_STAKING.toString(), 'ether'); - } else { - max = new BigNumber(calculateMax(availableBalance, feeInWei)) - .minus(toWei(MIN_ETH_FOR_WITHDRAWALS.toString(), 'ether')) - .toString(); - } - - amount = max; - } else { - amount = output.amount; - } - - // total ETH spent (amount + fee), in ERC20 only fee - const totalSpent = new BigNumber(calculateTotal(amount, feeInWei)); - - if ( - new BigNumber(feeInWei).gt(availableBalance) || - (compareWithAmount && totalSpent.isGreaterThan(availableBalance)) - ) { - const error = 'TR_STAKE_NOT_ENOUGH_FUNDS'; - - // errorMessage declared later - return { - type: 'error', - error, - errorMessage: { id: error, values: { symbol: symbol.toUpperCase() } }, - } as const; - } - - const payloadData = { - type: 'nonfinal' as const, - totalSpent: totalSpent.toString(), - max, - fee: feeInWei, - feePerByte: feeLevel.feePerUnit, - feeLimit: feeLevel.feeLimit, - bytes: 0, // TODO: calculate - inputs: [], + const stakingParams = { + feeInBaseUnits: feeInWei, + minBalanceForStakingInBaseUnits: toWei(MIN_ETH_BALANCE_FOR_STAKING.toString(), 'ether'), + minAmountForStakingInBaseUnits: toWei(MIN_ETH_AMOUNT_FOR_STAKING.toString(), 'ether'), + minAmountForWithdrawalInBaseUnits: toWei(MIN_ETH_FOR_WITHDRAWALS.toString(), 'ether'), }; - if (output.type === 'send-max' || output.type === 'payment') { - return { - ...payloadData, - type: 'final', - // compatibility with BTC PrecomposedTransaction from @trezor/connect - inputs: [], - outputsPermutation: [0], - outputs: [ - { - address: output.address, - amount, - script_type: 'PAYTOADDRESS', - }, - ], - }; - } - - return payloadData; + return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams); }; export const composeTransaction = (formValues: StakeFormState, formState: ComposeActionContext) => async () => { - const { account, network, feeInfo } = formState; - const composeOutputs = getExternalComposeOutput(formValues, account, network); - if (!composeOutputs) return; // no valid Output + const { account, feeInfo } = formState; + if (!account || !feeInfo) return; - const { output, decimals } = composeOutputs; - const { availableBalance } = account; const { amount } = formValues.outputs[0]; // gasLimit calculation based on account.descriptor and amount - const { ethereumStakeType } = formValues; + const { stakeType } = formValues; const stakeTxGasLimit = await getStakeTxGasLimit({ - ethereumStakeType, + stakeType, from: account.descriptor, amount, symbol: account.symbol, @@ -156,34 +88,13 @@ export const composeTransaction = }); } - // wrap response into PrecomposedLevels object where key is a FeeLevel label - const wrappedResponse: PrecomposedLevels = {}; - const compareWithAmount = formValues.ethereumStakeType === 'stake'; - const response = predefinedLevels.map(level => - calculate(availableBalance, output, level, compareWithAmount, account.symbol), + return composeStakingTransaction( + formValues, + formState, + predefinedLevels, + calculateTransaction, + customFeeLimit, ); - response.forEach((tx, index) => { - const feeLabel = predefinedLevels[index].label as FeeLevel['label']; - wrappedResponse[feeLabel] = tx; - }); - - // format max (calculate sends it as satoshi) - // update errorMessage values (symbol) - Object.keys(wrappedResponse).forEach(key => { - const tx = wrappedResponse[key]; - if (tx.type !== 'error') { - tx.max = tx.max ? formatAmount(tx.max, decimals) : undefined; - tx.estimatedFeeLimit = customFeeLimit; - } - if (tx.type === 'error' && tx.error === 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE') { - tx.errorMessage = { - id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE', - values: { symbol: network.symbol.toUpperCase() }, - }; - } - }); - - return wrappedResponse; }; export const signTransaction = @@ -227,9 +138,9 @@ export const signTransaction = const identity = getAccountIdentity(account); // transform to TrezorConnect.ethereumSignTransaction params - const { ethereumStakeType } = formValues; + const { stakeType } = formValues; let txData; - if (ethereumStakeType === 'stake') { + if (stakeType === 'stake') { txData = await prepareStakeEthTx({ symbol: account.symbol, from: account.descriptor, @@ -240,7 +151,7 @@ export const signTransaction = chainId: network.chainId, }); } - if (ethereumStakeType === 'unstake') { + if (stakeType === 'unstake') { txData = await prepareUnstakeEthTx({ symbol: account.symbol, from: account.descriptor, @@ -252,7 +163,7 @@ export const signTransaction = interchanges: UNSTAKE_INTERCHANGES, }); } - if (ethereumStakeType === 'claim') { + if (stakeType === 'claim') { txData = await prepareClaimEthTx({ symbol: account.symbol, from: account.descriptor, diff --git a/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts b/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts new file mode 100644 index 000000000000..6bae1e158c10 --- /dev/null +++ b/packages/suite/src/actions/wallet/stake/stakeFormSolanaActions.ts @@ -0,0 +1,161 @@ +import { BigNumber } from '@trezor/utils/src/bigNumber'; +import TrezorConnect, { FeeLevel } from '@trezor/connect'; +import { notificationsActions } from '@suite-common/toast-notifications'; +import { networkAmountToSmallestUnit } from '@suite-common/wallet-utils'; +import { + StakeFormState, + PrecomposedTransaction, + PrecomposedTransactionFinal, + ExternalOutput, + AddressDisplayOptions, +} from '@suite-common/wallet-types'; +import { ComposeActionContext, selectSelectedDevice } from '@suite-common/wallet-core'; +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { + MIN_SOL_AMOUNT_FOR_STAKING, + MIN_SOL_BALANCE_FOR_STAKING, + MIN_SOL_FOR_WITHDRAWALS, +} from '@suite-common/wallet-constants'; + +import { Dispatch, GetState } from 'src/types/suite'; +import { selectAddressDisplayType } from 'src/reducers/suite/suiteReducer'; +import { getPubKeyFromAddress, prepareStakeSolTx } from 'src/utils/suite/solanaStaking'; + +import { calculate, composeStakingTransaction } from './stakeFormActions'; + +const calculateTransaction = ( + availableBalance: string, + output: ExternalOutput, + feeLevel: FeeLevel, + compareWithAmount = true, + symbol: NetworkSymbol, +): PrecomposedTransaction => { + const feeInLamports = new BigNumber(feeLevel.feePerTx ?? '0').toString(); + + const stakingParams = { + feeInBaseUnits: feeInLamports, + minBalanceForStakingInBaseUnits: networkAmountToSmallestUnit( + MIN_SOL_BALANCE_FOR_STAKING.toString(), + symbol, + ), + minAmountForStakingInBaseUnits: networkAmountToSmallestUnit( + MIN_SOL_AMOUNT_FOR_STAKING.toString(), + symbol, + ), + minAmountForWithdrawalInBaseUnits: networkAmountToSmallestUnit( + MIN_SOL_FOR_WITHDRAWALS.toString(), + symbol, + ), + }; + + return calculate(availableBalance, output, feeLevel, compareWithAmount, symbol, stakingParams); +}; + +export const composeTransaction = + (formValues: StakeFormState, formState: ComposeActionContext) => () => { + const { feeInfo } = formState; + if (!feeInfo) return; + + const { levels } = feeInfo; + const predefinedLevels = levels.filter(l => l.label !== 'custom'); + + return composeStakingTransaction( + formValues, + formState, + predefinedLevels, + calculateTransaction, + undefined, + ); + }; + +export const signTransaction = + (formValues: StakeFormState, transactionInfo: PrecomposedTransactionFinal) => + async (dispatch: Dispatch, getState: GetState) => { + const { selectedAccount, blockchain } = getState().wallet; + + const device = selectSelectedDevice(getState()); + if ( + selectedAccount.status !== 'loaded' || + !device || + !transactionInfo || + transactionInfo.type !== 'final' + ) + return; + + const { account } = selectedAccount; + if (account.networkType !== 'solana') return; + + const selectedBlockchain = blockchain[account.symbol]; + const addressDisplayType = selectAddressDisplayType(getState()); + const { stakeType } = formValues; + + let txData; + if (stakeType === 'stake') { + txData = await prepareStakeSolTx({ + from: account.descriptor, + path: account.path, + amount: formValues.outputs[0].amount, + symbol: account.symbol, + selectedBlockchain, + }); + } + + if (!txData) { + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: 'Unknown stake action', + }), + ); + + return; + } + + if (!txData.success) { + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: txData.errorMessage, + }), + ); + + return; + } + + const signedTx = await TrezorConnect.solanaSignTransaction({ + device: { + path: device.path, + instance: device.instance, + state: device.state, + }, + useEmptyPassphrase: device.useEmptyPassphrase, + path: account.path, + serializedTx: txData.tx.serializedTx, + chunkify: addressDisplayType === AddressDisplayOptions.CHUNKED, + }); + + if (!signedTx.success) { + // catch manual error from TransactionReviewModal + if (signedTx.payload.error === 'tx-cancelled') return; + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: signedTx.payload.error, + }), + ); + + return; + } + + const signerPubKey = getPubKeyFromAddress(account.descriptor); + + txData.tx.versionedTx.addSignature( + signerPubKey, + Uint8Array.from(Buffer.from(signedTx.payload.signature, 'hex')), + ); + + const serializedVersiondeTx = txData.tx.versionedTx.serialize(); + const signedSerializedTx = Buffer.from(serializedVersiondeTx).toString('hex'); + + return signedSerializedTx; + }; diff --git a/packages/suite/src/actions/wallet/stakeActions.ts b/packages/suite/src/actions/wallet/stakeActions.ts index 611c582c4a24..8b5a374eaeae 100644 --- a/packages/suite/src/actions/wallet/stakeActions.ts +++ b/packages/suite/src/actions/wallet/stakeActions.ts @@ -11,6 +11,8 @@ import { notificationsActions } from '@suite-common/toast-notifications'; import { formatNetworkAmount, isRbfTransaction, + isSupportedEthStakingNetworkSymbol, + isSupportedSolStakingNetworkSymbol, tryGetAccountIdentity, } from '@suite-common/wallet-utils'; import { StakeFormState, PrecomposedTransactionFinal, StakeType } from '@suite-common/wallet-types'; @@ -19,15 +21,21 @@ import { Dispatch, GetState } from 'src/types/suite'; import * as modalActions from '../suite/modalActions'; import * as stakeFormEthereumActions from './stake/stakeFormEthereumActions'; +import * as stakeFormSolanaActions from './stake/stakeFormSolanaActions'; import { openModal } from '../suite/modalActions'; export const composeTransaction = (formValues: StakeFormState, formState: ComposeActionContext) => (dispatch: Dispatch) => { const { account } = formState; - if (account.networkType === 'ethereum') { + + if (isSupportedEthStakingNetworkSymbol(account.symbol)) { return dispatch(stakeFormEthereumActions.composeTransaction(formValues, formState)); } + if (isSupportedSolStakingNetworkSymbol(account.symbol)) { + return dispatch(stakeFormSolanaActions.composeTransaction(formValues, formState)); + } + return Promise.resolve(undefined); }; @@ -45,9 +53,9 @@ export const cancelSignTx = (isSuccessTx?: boolean) => (dispatch: Dispatch, getS // otherwise just close modal and open stake modal dispatch(modalActions.onCancel()); - const { ethereumStakeType } = precomposedForm ?? {}; - if (ethereumStakeType && !isSuccessTx) { - dispatch(openModal({ type: ethereumStakeType })); + const { stakeType } = precomposedForm ?? {}; + if (stakeType && !isSuccessTx) { + dispatch(openModal({ type: stakeType })); } }; @@ -171,19 +179,25 @@ export const signTransaction = // signTransaction by Trezor let serializedTx: string | undefined; - if (account.networkType === 'ethereum') { + if (isSupportedEthStakingNetworkSymbol(account.symbol)) { serializedTx = await dispatch( stakeFormEthereumActions.signTransaction(formValues, enhancedTxInfo), ); } + if (isSupportedSolStakingNetworkSymbol(account.symbol)) { + serializedTx = await dispatch( + stakeFormSolanaActions.signTransaction(formValues, enhancedTxInfo), + ); + } + if (!serializedTx) { // close modal manually since UI.CLOSE_UI.WINDOW was blocked dispatch(modalActions.onCancel()); - const { ethereumStakeType } = formValues; - if (ethereumStakeType) { - dispatch(openModal({ type: ethereumStakeType })); + const { stakeType } = formValues; + if (stakeType) { + dispatch(openModal({ type: stakeType })); } return; @@ -203,6 +217,6 @@ export const signTransaction = ); if (decision) { // push tx to the network - return dispatch(pushTransaction(formValues.ethereumStakeType)); + return dispatch(pushTransaction(formValues.stakeType)); } }; diff --git a/packages/suite/src/components/suite/AppNavigation/AppNavigationTooltip.tsx b/packages/suite/src/components/suite/AppNavigation/AppNavigationTooltip.tsx index 21c41ebeacaf..f5c6947065d2 100644 --- a/packages/suite/src/components/suite/AppNavigation/AppNavigationTooltip.tsx +++ b/packages/suite/src/components/suite/AppNavigation/AppNavigationTooltip.tsx @@ -11,7 +11,7 @@ interface AppNavigationTooltipProps { } export const AppNavigationTooltip = ({ children, isActiveTab }: AppNavigationTooltipProps) => { - const { selectedAccount } = useSelector(state => state.wallet); + const selectedAccount = useSelector(state => state.wallet.selectedAccount); const isAccountLoading = selectedAccount.status === 'loading'; diff --git a/packages/suite/src/components/suite/CoinList/Coin.tsx b/packages/suite/src/components/suite/CoinList/Coin.tsx index bef1a6c126ac..f846095f1793 100644 --- a/packages/suite/src/components/suite/CoinList/Coin.tsx +++ b/packages/suite/src/components/suite/CoinList/Coin.tsx @@ -7,6 +7,7 @@ import { variables, Icon } from '@trezor/components'; import { NetworkSymbol } from '@suite-common/wallet-config'; import { typography } from '@trezor/theme'; import { TranslationKey } from '@suite-common/intl-types'; +import { focusStyleTransition, getFocusShadowStyle } from '@trezor/components/src/utils/utils'; import { CoinLogo } from '@trezor/product-components'; import { Translation } from 'src/components/suite'; @@ -83,7 +84,9 @@ export const CoinWrapper = styled.button<{ font-weight: ${variables.FONT_WEIGHT.DEMI_BOLD}; color: ${({ theme }) => theme.legacy.TYPE_DARK_GREY}; cursor: pointer; - transition: 0.2s ease-in-out; + transition: + 0.2s ease-in-out, + ${focusStyleTransition}; overflow: hidden; &:disabled { @@ -92,6 +95,8 @@ export const CoinWrapper = styled.button<{ background: ${({ theme }) => theme.legacy.BG_GREY}; } + ${getFocusShadowStyle()} + &:hover { background: ${({ theme }) => theme.legacy.BG_GREY_ALT}; border-color: ${({ theme, $toggled }) => diff --git a/packages/suite/src/components/suite/FormFractionButtons.tsx b/packages/suite/src/components/suite/FormFractionButtons.tsx index 5a565888fed5..2e84f6367b08 100644 --- a/packages/suite/src/components/suite/FormFractionButtons.tsx +++ b/packages/suite/src/components/suite/FormFractionButtons.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { Button, Tooltip } from '@trezor/components'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { MIN_ETH_AMOUNT_FOR_STAKING } from '@suite-common/wallet-constants'; -import { NetworkSymbol } from '@suite-common/wallet-config'; +import { getNetworkDisplaySymbol, NetworkSymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; @@ -51,6 +51,8 @@ export const FormFractionButtons = ({ const isMaxDisabled = isDisabled || new BigNumber(totalAmount || '0').lt(MIN_ETH_AMOUNT_FOR_STAKING); + const displaySymbol = getNetworkDisplaySymbol(symbol); + return ( ) @@ -78,7 +80,7 @@ export const FormFractionButtons = ({ id="TR_STAKE_MIN_AMOUNT_TOOLTIP" values={{ amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(), - symbol: symbol.toUpperCase(), + networkSymbol: displaySymbol, }} /> ) @@ -96,7 +98,7 @@ export const FormFractionButtons = ({ id="TR_STAKE_MIN_AMOUNT_TOOLTIP" values={{ amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(), - symbol: symbol.toUpperCase(), + networkSymbol: displaySymbol, }} /> ) @@ -114,7 +116,7 @@ export const FormFractionButtons = ({ id="TR_STAKE_MIN_AMOUNT_TOOLTIP" values={{ amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(), - symbol: symbol.toUpperCase(), + networkSymbol: displaySymbol, }} /> ) diff --git a/packages/suite/src/components/suite/FormattedCryptoAmount.tsx b/packages/suite/src/components/suite/FormattedCryptoAmount.tsx index 0131ee8f9a9e..828230529580 100644 --- a/packages/suite/src/components/suite/FormattedCryptoAmount.tsx +++ b/packages/suite/src/components/suite/FormattedCryptoAmount.tsx @@ -1,6 +1,11 @@ import styled from 'styled-components'; -import { getNetworkOptional, type NetworkSymbolExtended } from '@suite-common/wallet-config'; +import { + getNetworkDisplaySymbol, + getNetworkOptional, + isNetworkSymbol, + type NetworkSymbolExtended, +} from '@suite-common/wallet-config'; import { SignValue } from '@suite-common/suite-types'; import { formatCoinBalance, @@ -64,7 +69,8 @@ export const FormattedCryptoAmount = ({ const areSatsSupported = !!networkFeatures?.includes('amount-unit'); let formattedValue = value; - let formattedSymbol = symbol?.toUpperCase(); + let formattedSymbol = + symbol && isNetworkSymbol(symbol) ? getNetworkDisplaySymbol(symbol) : symbol?.toUpperCase(); const isSatoshis = areSatsSupported && areSatsDisplayed; @@ -95,7 +101,7 @@ export const FormattedCryptoAmount = ({ } const content = ( - + {!!signValue && } @@ -115,5 +121,5 @@ export const FormattedCryptoAmount = ({ return content; } - return {content}; + return {content}; }; diff --git a/packages/suite/src/components/suite/FormattedNftAmount.tsx b/packages/suite/src/components/suite/FormattedNftAmount.tsx index 90a36e95a2fd..2bd138a8c08a 100644 --- a/packages/suite/src/components/suite/FormattedNftAmount.tsx +++ b/packages/suite/src/components/suite/FormattedNftAmount.tsx @@ -32,7 +32,7 @@ export const FormattedNftAmount = ({ }: FormattedNftAmountProps) => { const theme = useTheme(); const { translationString } = useTranslation(); - const { selectedAccount } = useSelector(state => state.wallet); + const selectedAccount = useSelector(state => state.wallet.selectedAccount); const { network } = selectedAccount; const symbolComponent = transfer.symbol ? ( @@ -63,7 +63,7 @@ export const FormattedNftAmount = ({ ) : ( {token.value}x - + )} @@ -95,7 +95,7 @@ export const FormattedNftAmount = ({ {signValue ? : null} - + {isWithLink ? ( { subheading: ( ), content: { diff --git a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx index a566819fd994..ae28b634b7b0 100644 --- a/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx +++ b/packages/suite/src/components/suite/StakingProcess/UnstakingInfo.tsx @@ -10,9 +10,10 @@ import { StakeRootState, AccountsRootState, } from '@suite-common/wallet-core'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; -import { getDaysToUnstake } from 'src/utils/suite/stake'; +import { getDaysToUnstake } from 'src/utils/suite/ethereumStaking'; import { CoinjoinRootState } from 'src/reducers/wallet/coinjoinReducer'; import { InfoRow } from './InfoRow'; @@ -34,7 +35,7 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => { if (!account) return null; const daysToUnstake = getDaysToUnstake(unstakeTxs, data); - const accountSymbol = account.symbol.toUpperCase(); + const displaySymbol = getNetworkDisplaySymbol(account.symbol); const infoRows = [ { @@ -49,7 +50,7 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => { subheading: ( ), content: { @@ -58,12 +59,15 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => { }, { heading: ( - + ), subheading: ( ), content: { @@ -72,7 +76,9 @@ export const UnstakingInfo = ({ isExpanded }: UnstakingInfoProps) => { }, }, { - heading: , + heading: ( + + ), }, ]; diff --git a/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx b/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx index 3e617e22492c..6e388a414c64 100644 --- a/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx +++ b/packages/suite/src/components/suite/TorLoader/TorProgressBar.tsx @@ -123,7 +123,7 @@ export const TorProgressBar = ({ - + {isTorError ? ( diff --git a/packages/suite/src/components/suite/labeling/WalletLabeling.tsx b/packages/suite/src/components/suite/labeling/WalletLabeling.tsx index e3eb3b21e856..3f0b93523cdf 100644 --- a/packages/suite/src/components/suite/labeling/WalletLabeling.tsx +++ b/packages/suite/src/components/suite/labeling/WalletLabeling.tsx @@ -2,8 +2,6 @@ import { useCallback } from 'react'; import styled from 'styled-components'; -import { selectSelectedDeviceLabelOrName } from '@suite-common/wallet-core'; - import { TrezorDevice } from 'src/types/suite'; import { useTranslation } from 'src/hooks/suite/useTranslation'; import { useSelector } from 'src/hooks/suite/useSelector'; @@ -38,7 +36,6 @@ export const useWalletLabeling = () => { export const useGetWalletLabel = ({ device, shouldUseDeviceLabel }: WalletLabellingProps) => { const { defaultAccountLabelString } = useWalletLabeling(); - const deviceLabel = useSelector(selectSelectedDeviceLabelOrName); const { walletLabel } = useSelector(state => selectLabelingDataForWallet(state, device.state)); let label: string | undefined; @@ -49,6 +46,8 @@ export const useGetWalletLabel = ({ device, shouldUseDeviceLabel }: WalletLabell } if (shouldUseDeviceLabel) { + const deviceLabel = device?.features?.label || device?.name || ''; + return <>{`${deviceLabel} ${label}`}; } diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx index 2a10e176917c..6f31b2ce056d 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceSelector.tsx @@ -6,16 +6,17 @@ import { selectDevicesCount, selectSelectedDevice } from '@suite-common/wallet-c import type { TimerId } from '@trezor/type-utils'; import { borders, spacingsPx } from '@trezor/theme'; import { focusStyleTransition, getFocusShadowStyle } from '@trezor/components/src/utils/utils'; -import { Icon } from '@trezor/components'; +import { Icon, Tooltip } from '@trezor/components'; import { SHAKE } from 'src/support/suite/styles/animations'; import { goto } from 'src/actions/suite/routerActions'; -import { useDispatch, useSelector } from 'src/hooks/suite'; +import { useDiscovery, useDispatch, useSelector } from 'src/hooks/suite'; import { ViewOnlyTooltip } from 'src/views/view-only/ViewOnlyTooltip'; import { SidebarDeviceStatus } from './SidebarDeviceStatus'; import { ExpandedSidebarOnly } from '../Sidebar/ExpandedSidebarOnly'; import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; +import { Translation } from '../../../Translation'; const CaretContainer = styled.div` background: transparent; @@ -57,15 +58,16 @@ const Wrapper = styled.div<{ $isAnimationTriggered?: boolean; $isSidebarCollapse `} `; -const InnerContainer = styled.div` +const InnerContainer = styled.div<{ $isDisabled?: boolean }>` position: relative; width: 100%; display: flex; align-items: center; - cursor: pointer; gap: ${spacingsPx.md}; min-height: 42px; -webkit-app-region: no-drag; + + cursor: ${({ $isDisabled }) => ($isDisabled ? 'not-allowed' : 'pointer')}; `; export const DeviceSelector = () => { @@ -73,6 +75,9 @@ export const DeviceSelector = () => { const deviceCount = useSelector(selectDevicesCount); const dispatch = useDispatch(); + const { getDiscoveryStatus } = useDiscovery(); + const discoveryStatus = getDiscoveryStatus(); + const discoveryInProgress = discoveryStatus && discoveryStatus.status === 'loading'; const [localCount, setLocalCount] = useState(null); const [isAnimationTriggered, setIsAnimationTriggered] = useState(false); @@ -108,14 +113,17 @@ export const DeviceSelector = () => { } }, [countChanged]); - const handleSwitchDeviceClick = () => - dispatch( - goto('suite-switch-device', { - params: { - cancelable: true, - }, - }), - ); + const handleSwitchDeviceClick = () => { + if (!discoveryInProgress) { + dispatch( + goto('suite-switch-device', { + params: { + cancelable: true, + }, + }), + ); + } + }; const { isSidebarCollapsed } = useResponsiveContext(); @@ -125,21 +133,32 @@ export const DeviceSelector = () => { $isSidebarCollapsed={isSidebarCollapsed} > - + ) : undefined + } > - - - - {selectedDevice && selectedDevice.state && ( - - - - )} - - + + + + + {selectedDevice && selectedDevice.state && ( + + + + )} + + + ); diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx index f95ed6f132b4..0423e483085c 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/DeviceSelector/DeviceStatus.tsx @@ -24,7 +24,6 @@ type DeviceStatusProps = { const DeviceWrapper = styled.div<{ $isLowerOpacity: boolean }>` display: flex; - width: 24px; opacity: ${({ $isLowerOpacity }) => $isLowerOpacity && 0.4}; `; @@ -44,7 +43,6 @@ export const DeviceStatus = ({ deviceModel={deviceModel} deviceColor={device?.features?.unit_color} animationHeight="34px" - animationWidth="24px" /> ); @@ -67,7 +65,7 @@ export const DeviceStatus = ({ {content} ) : ( - + {image} )} diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/NavBackends.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/NavBackends.tsx index a15d6b91162d..7b8095ae3f96 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/NavBackends.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/QuickActions/NavBackends.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useRef, useState } from 'react'; +import React, { ReactNode, useRef } from 'react'; import styled from 'styled-components'; @@ -89,13 +89,20 @@ type NavBackendsProps = { * The issue here is that `Dropdown` component expects child with `isDisabled` prop, * as it passes this props via `cloneElement()`. */ -// @ts-expect-error `isDisabled` is needed here despite not being used -const WrapperDiv = ({ children, isDisabled }: { isDisabled?: boolean; children: ReactNode }) => ( -
{children}
+const WrapperDiv = ({ + children, + isDisabled: _, // `isDisabled` is needed here despite not being used + onClick, +}: { + isDisabled?: boolean; + children: ReactNode; + onClick?: () => void; +}) => ( + // eslint-disable-next-line +
{children}
); export const NavBackends = ({ customBackends, children }: NavBackendsProps) => { - const [open, setOpen] = useState(false); const dropdownRef = useRef(); const blockchain = useSelector(state => state.wallet.blockchain); const dispatch = useDispatch(); @@ -136,7 +143,6 @@ export const NavBackends = ({ customBackends, children }: NavBackendsProps) => { return ( setOpen(!open)} ref={dropdownRef} alignMenu="top-right" addon={{ diff --git a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx index 5cc54d9d3821..e0b5de1fa692 100644 --- a/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx +++ b/packages/suite/src/components/suite/layouts/SuiteLayout/Sidebar/Sidebar.tsx @@ -18,6 +18,7 @@ import { setSidebarWidth as setSidebarWidthInRedux } from '../../../../../action import { useResponsiveContext } from '../../../../../support/suite/ResponsiveContext'; const Container = styled.nav<{ $elevation: Elevation }>` + overflow-x: hidden; display: flex; container-type: inline-size; flex-direction: column; @@ -39,7 +40,7 @@ const Content = styled.div` export const Sidebar = () => { const [closedNotificationDevice, setClosedNotificationDevice] = useState(false); const [closedNotificationSuite, setClosedNotificationSuite] = useState(false); - const { isSidebarCollapsed } = useResponsiveContext(); + const { isSidebarCollapsed, setSidebarWidth, sidebarWidth } = useResponsiveContext(); const { elevation } = useElevation(); const { updateStatusDevice, updateStatusSuite } = useUpdateStatus(); @@ -48,9 +49,8 @@ export const Sidebar = () => { setSidebarWidth: (width: number) => setSidebarWidthInRedux({ width }), }); - const { setSidebarWidth, sidebarWidth } = useResponsiveContext(); - const handleSidebarWidthChanged = (width: number) => { + setSidebarWidth(width); actions.setSidebarWidth(width); }; const handleSidebarWidthUpdate = (width: number) => { diff --git a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmAddressModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmAddressModal.tsx index 667c7a7457b6..079925ecd0b8 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmAddressModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmAddressModal.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { selectSelectedDevice } from '@suite-common/wallet-core'; +import { getNetworkDisplaySymbol, isNetworkSymbol } from '@suite-common/wallet-config'; import { showAddress } from 'src/actions/wallet/receiveActions'; import { Translation } from 'src/components/suite'; @@ -38,16 +39,16 @@ export const ConfirmAddressModal = ({ addressPath, value, ...props }: ConfirmAdd const getHeading = () => { if (modalCryptoId) { - const coinSymbol = cryptoIdToCoinSymbol(modalCryptoId)?.toUpperCase(); - const symbol = cryptoIdToSymbol(modalCryptoId)?.toUpperCase(); + const coinSymbol = cryptoIdToCoinSymbol(modalCryptoId)?.toLowerCase(); + const symbol = cryptoIdToSymbol(modalCryptoId); if (symbol && coinSymbol !== symbol) { return ( ); @@ -57,7 +58,10 @@ export const ConfirmAddressModal = ({ addressPath, value, ...props }: ConfirmAdd ); @@ -67,7 +71,7 @@ export const ConfirmAddressModal = ({ addressPath, value, ...props }: ConfirmAdd ); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmValueModal/ConfirmValueModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmValueModal/ConfirmValueModal.tsx index c414395c4ecf..e6ccaf0ddc03 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmValueModal/ConfirmValueModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmValueModal/ConfirmValueModal.tsx @@ -114,7 +114,7 @@ export const ConfirmValueModal = ({ description={showTokensSubheading && } onCancel={isCancelable ? onCancel : undefined} > - + {!device?.connected && ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmXpubModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmXpubModal.tsx index 766f6c293ab1..86dfea63d68a 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/ConfirmXpubModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/ConfirmXpubModal.tsx @@ -1,4 +1,6 @@ import { selectSelectedDevice } from '@suite-common/wallet-core'; +import { convertTaprootXpub } from '@trezor/utils'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; import { showXpub } from 'src/actions/wallet/publicKeyActions'; @@ -26,6 +28,13 @@ export const ConfirmXpubModal = ( ? `${account.descriptor}#${account.descriptorChecksum}` : account.descriptor; + // Suite internally uses apostrophe, but FW uses 'h' for taproot descriptors, + // and we want to show it correctly to the user + const xpubWithReplacedApostropheWithH = convertTaprootXpub({ + xpub, + direction: 'apostrophe-to-h', + }); + return ( @@ -46,7 +55,7 @@ export const ConfirmXpubModal = ( confirmStepLabel={} validateOnDevice={showXpub} copyButtonText={} - value={xpub} + value={xpubWithReplacedApostropheWithH ?? xpub} displayMode={DisplayMode.PAGINATED_TEXT} {...props} /> diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx index 02077fb292dc..7ad012d947ac 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewEvmExplanation.tsx @@ -8,16 +8,16 @@ import { Translation } from 'src/components/suite'; type TransactionReviewEvmExplanationProps = { account: Account; - ethereumStakeType: StakeType | null; + stakeType: StakeType | null; }; export const TransactionReviewEvmExplanation = ({ account, - ethereumStakeType, + stakeType, }: TransactionReviewEvmExplanationProps) => { const network = networks[account.symbol]; - if (network.networkType !== 'ethereum' || ethereumStakeType) { + if (network.networkType !== 'ethereum' || stakeType) { return null; } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx index adfbb2ac2461..7bfece56c72b 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx @@ -50,7 +50,7 @@ const isStakeState = (state: SendState | StakeState): state is StakeState => { }; const isStakeForm = (form: FormState | StakeFormState): form is StakeFormState => { - return 'ethereumStakeType' in form; + return 'stakeType' in form; }; interface TransactionReviewModalContentProps { @@ -108,8 +108,8 @@ export const TransactionReviewModalContent = ({ }); // for bump fee we have to analyze tx data which are in outputs[0] - const ethereumStakeType = isStakeForm(precomposedForm) - ? precomposedForm.ethereumStakeType + const stakeType = isStakeForm(precomposedForm) + ? precomposedForm.stakeType : getTxStakeNameByDataHex(outputs[0]?.value); // get estimate mining time @@ -153,9 +153,9 @@ export const TransactionReviewModalContent = ({ broadcast={precomposedForm.options.includes('broadcast')} detailsOpen={detailsOpen} onDetailsClick={() => setDetailsOpen(!detailsOpen)} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} actionText={getTransactionReviewModalActionText({ - ethereumStakeType, + stakeType, isRbfAction, })} /> @@ -170,18 +170,15 @@ export const TransactionReviewModalContent = ({ buttonRequestsCount={buttonRequestsCount} isRbfAction={isRbfAction} actionText={getTransactionReviewModalActionText({ - ethereumStakeType, + stakeType, isRbfAction, isSending, })} isSending={isSending} setIsSending={() => setIsSending(true)} - ethereumStakeType={ethereumStakeType || undefined} - /> - + ); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx index aca152887686..1ebd681211ab 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutput.tsx @@ -3,7 +3,7 @@ import { ReactNode, forwardRef } from 'react'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { formatNetworkAmount, formatAmount, isTestnet } from '@suite-common/wallet-utils'; import { BTC_LOCKTIME_VALUE } from '@suite-common/wallet-constants'; -import { NetworkSymbol, NetworkSymbolExtended } from '@suite-common/wallet-config'; +import { getNetworkDisplaySymbol, NetworkSymbol } from '@suite-common/wallet-config'; import { ReviewOutput, StakeType } from '@suite-common/wallet-types'; import { TranslationKey } from '@suite-common/intl-types'; @@ -54,17 +54,16 @@ export type TransactionReviewOutputProps = { symbol: NetworkSymbol; account: Account; isRbf: boolean; - ethereumStakeType?: StakeType; + stakeType?: StakeType; } & ReviewOutput; export const TransactionReviewOutput = forwardRef( (props, ref) => { - const { type, state, label, value, symbol, token, account, ethereumStakeType, isRbf } = - props; + const { type, state, label, value, symbol, token, account, stakeType, isRbf } = props; let outputLabel: ReactNode = label; const { networkType } = account; const { translationString } = useTranslation(); - const displayMode = useDisplayMode({ ethereumStakeType, type }); + const displayMode = useDisplayMode({ stakeType, type }); if (type === 'locktime') { const isTimestamp = new BigNumber(value).gte(BTC_LOCKTIME_VALUE); @@ -96,17 +95,13 @@ export const TransactionReviewOutput = forwardRef diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx index 0e40b28d7459..8cc992907cf8 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputElement.tsx @@ -3,7 +3,7 @@ import { forwardRef, ReactNode } from 'react'; import styled from 'styled-components'; import { variables } from '@trezor/components'; -import type { NetworkSymbol, NetworkSymbolExtended } from '@suite-common/wallet-config'; +import { type NetworkSymbol } from '@suite-common/wallet-config'; import { TokenInfo } from '@trezor/connect'; import { amountToSmallestUnit } from '@suite-common/wallet-utils'; import { zIndices } from '@trezor/theme'; @@ -145,7 +145,6 @@ export type TransactionReviewOutputElementProps = { indicator?: JSX.Element; lines: OutputElementLine[]; symbol?: NetworkSymbol; - displaySymbol?: NetworkSymbolExtended; fiatVisible?: boolean; token?: TokenInfo; account?: Account; @@ -156,107 +155,90 @@ export type TransactionReviewOutputElementProps = { export const TransactionReviewOutputElement = forwardRef< HTMLDivElement, TransactionReviewOutputElementProps ->( - ( - { - indicator, - lines, - token, - symbol, - displaySymbol, - fiatVisible = false, - account, - state, - displayMode, - }, - ref, - ) => { - const network = account?.networkType; - const cardanoFingerprint = getFingerprint(account?.tokens, token?.symbol); - const isActive = state === 'active'; - - const showMultiIndicator = lines.length > 1; - - return ( - - - {showMultiIndicator ? ( - - {indicator} - - ) : ( - <>{indicator} - )} - - - {lines.map(line => ( - - - {isActive && (line.id === 'address' || line.id === 'regular_legacy') - ? line.confirmLabel - : line.label} - - - {isActive && - displayMode && - TYPES_TO_BE_DISPLAYED_IN_SCREEN_BOX.includes(line.id) ? ( - - ) : ( +>(({ indicator, lines, token, symbol, fiatVisible = false, account, state, displayMode }, ref) => { + const network = account?.networkType; + const cardanoFingerprint = getFingerprint(account?.tokens, token?.symbol); + const isActive = state === 'active'; + + const showMultiIndicator = lines.length > 1; + + return ( + + + {showMultiIndicator ? ( + + {indicator} + + ) : ( + <>{indicator} + )} + + + {lines.map(line => ( + + + {isActive && (line.id === 'address' || line.id === 'regular_legacy') + ? line.confirmLabel + : line.label} + + + {isActive && + displayMode && + TYPES_TO_BE_DISPLAYED_IN_SCREEN_BOX.includes(line.id) ? ( + + ) : ( + + {line.plainValue ? ( + line.value + ) : ( + + )} + + )} + {/* temporary solution until fiat value for ERC20 tokens will be fixed */} + {symbol && fiatVisible && !(line.id !== 'fee' && token) && ( + <> + + + - {line.plainValue ? ( - line.value - ) : ( - - )} + - )} - {/* temporary solution until fiat value for ERC20 tokens will be fixed */} - {symbol && fiatVisible && !(line.id !== 'fee' && token) && ( - <> - - - - - - - - )} - - {network === 'cardano' && cardanoFingerprint && ( - - - - - {cardanoFingerprint} - - )} - {network === 'cardano' && token && token.decimals !== 0 && ( - - - - - - {amountToSmallestUnit(line.value, token.decimals)} - - + )} - - ))} - - - ); - }, -); + + {network === 'cardano' && cardanoFingerprint && ( + + + + + {cardanoFingerprint} + + )} + {network === 'cardano' && token && token.decimals !== 0 && ( + + + + + + {amountToSmallestUnit(line.value, token.decimals)} + + + )} + + ))} + + + ); +}); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx index 604881c17120..0c6ea927d3ec 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewOutputList.tsx @@ -99,7 +99,7 @@ export interface TransactionReviewOutputListProps { actionText: TranslationKey; isSending?: boolean; setIsSending?: () => void; - ethereumStakeType?: StakeType; + stakeType?: StakeType; } export const TransactionReviewOutputList = ({ @@ -115,7 +115,7 @@ export const TransactionReviewOutputList = ({ actionText, isSending, setIsSending, - ethereumStakeType, + stakeType, }: TransactionReviewOutputListProps) => { const dispatch = useDispatch(); const { networkType } = account; @@ -225,7 +225,7 @@ export const TransactionReviewOutputList = ({ symbol={symbol} account={account} isRbf={isRbfAction} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} /> ); })} @@ -237,7 +237,7 @@ export const TransactionReviewOutputList = ({ outputs={outputs} buttonRequestsCount={buttonRequestsCount} precomposedTx={precomposedTx} - ethereumStakeType={ethereumStakeType} + stakeType={stakeType} isRbfAction={isRbfAction} /> )} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx index 725af91e98bf..a9f771312bf7 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewOutputList/TransactionReviewTotalOutput.tsx @@ -47,7 +47,7 @@ const getLines = ( symbol: TransactionReviewOutputListProps['account']['symbol'], precomposedTx: TransactionReviewOutputListProps['precomposedTx'], isRbfAction?: boolean, - ethereumStakeType?: StakeType, + stakeType?: StakeType, ): Array => { const isUpdatedSendFlow = getIsUpdatedSendFlow(device); const isUpdatedEthereumSendFlow = getIsUpdatedEthereumSendFlow(device, networkType); @@ -70,7 +70,7 @@ const getLines = ( .toString(); if (isUpdatedEthereumSendFlow) { - const isUnknownStakingClaimValue = isRbfAction && ethereumStakeType === 'claim'; + const isUnknownStakingClaimValue = isRbfAction && stakeType === 'claim'; const amountLine = { id: 'amount', // In updated ethereum send flow there is no total amount shown, only amount without fee label: , @@ -120,15 +120,7 @@ export const TransactionReviewTotalOutput = forwardRef< TransactionReviewTotalOutputProps >( ( - { - account, - signedTx, - outputs, - buttonRequestsCount, - precomposedTx, - ethereumStakeType, - isRbfAction, - }, + { account, signedTx, outputs, buttonRequestsCount, precomposedTx, stakeType, isRbfAction }, ref, ) => { const device = useSelector(selectSelectedDevice); @@ -139,14 +131,7 @@ export const TransactionReviewTotalOutput = forwardRef< const { symbol, networkType } = account; - const lines = getLines( - device, - networkType, - symbol, - precomposedTx, - isRbfAction, - ethereumStakeType, - ); + const lines = getLines(device, networkType, symbol, precomposedTx, isRbfAction, stakeType); return ( } lines={lines} - displaySymbol={symbol} symbol={symbol} fiatVisible={!isTestnet(symbol)} ref={ref} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx index 097f68341afe..d019be484dec 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewSummary.tsx @@ -209,7 +209,7 @@ interface TransactionReviewSummaryProps { broadcast?: boolean; detailsOpen: boolean; onDetailsClick: () => void; - ethereumStakeType?: StakeType | null; + stakeType?: StakeType | null; actionText: TranslationKey; } @@ -221,7 +221,7 @@ export const TransactionReviewSummary = ({ broadcast, detailsOpen, onDetailsClick, - ethereumStakeType, + stakeType, actionText, }: TransactionReviewSummaryProps) => { const drafts = useSelector(state => state.wallet.send.drafts); @@ -327,7 +327,7 @@ export const TransactionReviewSummary = ({ )} - {!ethereumStakeType && ( + {!stakeType && ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AddAccountModal/AddAccountModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AddAccountModal/AddAccountModal.tsx index f9403af8ea02..afcca31765d3 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AddAccountModal/AddAccountModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/AddAccountModal/AddAccountModal.tsx @@ -82,7 +82,7 @@ export const AddAccountModal = ({ const [enabledNetworks, disabledNetworks] = arrayPartition(supportedNetworks, network => enabledNetworkSymbols.includes(network.symbol), ); - const [disabledMainnetNetworks, disabledTestnetNetworks] = arrayPartition( + const [, disabledTestnetNetworks] = arrayPartition( disabledNetworks, network => !network?.testnet, ); @@ -99,6 +99,15 @@ export const AddAccountModal = ({ ) : []; + const filterNetworksBySymbol = (networks: Network[], symbol?: NetworkSymbol) => + symbol ? networks.filter(network => network.symbol === symbol) : networks; + + const filteredDisabledNetworks = filterNetworksBySymbol(disabledNetworks, symbol); + const filteredEnabledNetworks = filterNetworksBySymbol(enabledNetworks, symbol); + + const visibleNetworks = + emptyAccounts.length > 0 ? filteredEnabledNetworks : filteredDisabledNetworks; + const isCoinjoinVisible = (isCoinjoinPublic || isDebug) && !isCoinjoinDisabled; const getAvailableAccountTypes = (network: Network) => { @@ -208,20 +217,22 @@ export const AddAccountModal = ({ children: ( <> - } - networks={enabledNetworks} - selectedNetworks={selectedNetworks} - handleNetworkSelection={selectNetwork} - /> + {!symbol && ( + } + networks={enabledNetworks} + selectedNetworks={selectedNetworks} + handleNetworkSelection={selectNetwork} + /> + )} } - networks={disabledMainnetNetworks} + networks={visibleNetworks} selectedNetworks={selectedNetworks} handleNetworkSelection={selectNetwork} /> - {!!disabledTestnetNetworks.length && ( + {!symbol && !!disabledTestnetNetworks.length && ( )} - {showUnsupportedCoins && ( + {!symbol && showUnsupportedCoins && ( useMemo( () => - ['default', ...network.customBackends] + ['default', ...network.backendTypes] .filter(backend => { switch (backend) { case 'default': diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx index 004ad7c90787..4464776d0704 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ClaimModal/ClaimModal.tsx @@ -4,6 +4,7 @@ import { Paragraph, Tooltip, Banner, Card, Column, InfoItem, NewModal } from '@t import { spacings } from '@trezor/theme'; import { getAccountEverstakeStakingPool } from '@suite-common/wallet-utils'; import type { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Fees } from 'src/components/wallet/Fees/Fees'; import { Translation, FiatValue, FormattedCryptoAmount } from 'src/components/suite'; @@ -61,7 +62,7 @@ export const ClaimModal = ({ onCancel }: ClaimModalModalProps) => { description={ } size="small" @@ -122,7 +123,6 @@ export const ClaimModal = ({ onCancel }: ClaimModalModalProps) => { composedLevels={composedLevels} changeFeeLevel={changeFeeLevel} helperText={} - showFeeWhilePending={true} /> diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx index 80cf302b1ed4..7ed151df01ac 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ConfirmUnverifiedModal.tsx @@ -83,7 +83,7 @@ export const ConfirmUnverifiedModal = ({ )} } diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ExampleCSV.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ExampleCSV.tsx index de8f11fdd349..57545227c97e 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ExampleCSV.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ExampleCSV.tsx @@ -5,6 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import { Paragraph, Icon, motionAnimation } from '@trezor/components'; import { borders, spacingsPx, typography } from '@trezor/theme'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; import { useSelector, useTranslation } from 'src/hooks/suite'; @@ -81,7 +82,8 @@ export const ExampleCSV = () => { address,amount,currency{isLabelingAvailable && ',label'} - {addresses[0]},0.31337,{account.symbol.toUpperCase()} + {addresses[0]},0.31337, + {getNetworkDisplaySymbol(account.symbol)} {isLabelingAvailable && `,${translationString('TR_SENDFORM_LABELING_EXAMPLE_1')}`} diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ImportTransactionModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ImportTransactionModal.tsx index a616945e2baa..02e42190ac4e 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ImportTransactionModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/ImportTransactionModal/ImportTransactionModal.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { UserContextPayload } from '@suite-common/suite-types'; import { parseCSV } from '@suite-common/wallet-utils'; +import { networksCollection } from '@suite-common/wallet-config'; import { Translation, Modal } from 'src/components/suite'; import type { ExtendedMessageDescriptor } from 'src/types/suite'; @@ -29,6 +30,17 @@ export const ImportTransactionModal = ({ onCancel, decision }: ImportTransaction const onCsvResult = (result: string) => { const parsed = parseCSV(result, ['address', 'amount', 'currency', 'label'], delimiter); + + parsed.forEach(item => { + const network = networksCollection.find( + network => network.displaySymbol === item.currency, + ); + + if (network) { + item.currency = network.symbol; + } + }); + decision.resolve(parsed); onCancel(); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx index dc78f08d1502..6039015f9c06 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeEthInANutshellModal/StakeEthInANutshellModal.tsx @@ -16,12 +16,13 @@ import { import { TranslationKey } from '@suite-common/intl-types'; import { spacings } from '@trezor/theme'; import { selectValidatorsQueueData } from '@suite-common/wallet-core'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getUnstakingPeriodInDays } from 'src/utils/suite/stake'; +import { getUnstakingPeriodInDays } from 'src/utils/suite/ethereumStaking'; import { StakingInfo } from 'src/components/suite/StakingProcess/StakingInfo'; import { UnstakingInfo } from 'src/components/suite/StakingProcess/UnstakingInfo'; @@ -85,6 +86,8 @@ export const StakeEthInANutshellModal = ({ onCancel }: StakeEthInANutshellModalP }, ]; + if (!account) return null; + return ( } @@ -108,7 +111,7 @@ export const StakeEthInANutshellModal = ({ onCancel }: StakeEthInANutshellModalP diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx index 3917e5b1348b..7cc6a145d414 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/ConfirmStakeEthModal.tsx @@ -4,12 +4,19 @@ import { Checkbox, NewModal, Column, Banner, Card } from '@trezor/components'; import { spacings } from '@trezor/theme'; import { selectValidatorsQueueData } from '@suite-common/wallet-core'; import { HELP_CENTER_ETH_STAKING } from '@trezor/urls'; +import { getNetworkDisplaySymbol, type NetworkType } from '@suite-common/wallet-config'; import { Translation, TrezorLink } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { openModal } from 'src/actions/suite/modalActions'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getDaysToAddToPoolInitial } from 'src/utils/suite/stake'; +import { getDaysToAddToPoolInitial } from 'src/utils/suite/ethereumStaking'; + +const getStakeEnteringMessage = (networkType?: NetworkType) => { + if (networkType === 'ethereum') return 'TR_STAKE_ENTERING_POOL_MAY_TAKE'; + + return 'TR_STAKE_ACTIVATION_COULD_TAKE'; +}; interface ConfirmStakeEthModalProps { isLoading: boolean; @@ -40,6 +47,8 @@ export const ConfirmStakeEthModal = ({ onConfirm(); }; + if (!account) return null; + return ( } @@ -60,7 +69,7 @@ export const ConfirmStakeEthModal = ({ ), - symbol: account?.symbol.toUpperCase(), + networkSymbol: getNetworkDisplaySymbol(account.symbol), }} /> diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx index d0ce15ecc3d4..16c9525d1ca9 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/Inputs.tsx @@ -4,6 +4,7 @@ import { useFormatters } from '@suite-common/formatters'; import { formInputsMaxLength } from '@suite-common/validators'; import { MIN_ETH_FOR_WITHDRAWALS } from '@suite-common/wallet-constants'; import { spacings } from '@trezor/theme'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { NumberInput, Translation } from 'src/components/suite'; import { useTranslation } from 'src/hooks/suite'; @@ -15,7 +16,7 @@ import { validateReserveOrBalance, } from 'src/utils/suite/validation'; import { FIAT_INPUT, CRYPTO_INPUT } from 'src/types/wallet/stakeForms'; -import { validateStakingMax } from 'src/utils/suite/stake'; +import { validateStakingMax } from 'src/utils/suite/ethereumStaking'; import { FormFractionButtons } from 'src/components/suite/FormFractionButtons'; export const Inputs = () => { @@ -71,6 +72,8 @@ export const Inputs = () => { const shouldShowAmountForWithdrawalWarning = isLessAmountForWithdrawalWarningShown || isAmountForWithdrawalWarningShown; + const displaySymbol = getNetworkDisplaySymbol(account.symbol); + return ( { control={control} rules={cryptoInputRules} maxLength={formInputsMaxLength.amount} - innerAddon={{account.symbol.toUpperCase()}} + innerAddon={{displaySymbol}} bottomText={errors[CRYPTO_INPUT]?.message ?? null} inputState={getInputState(cryptoError || fiatError)} onChange={value => { @@ -130,7 +133,7 @@ export const Inputs = () => { } values={{ amount: MIN_ETH_FOR_WITHDRAWALS.toString(), - symbol: account.symbol.toUpperCase(), + networkSymbol: displaySymbol, }} /> @@ -142,7 +145,7 @@ export const Inputs = () => { id="TR_STAKE_RECOMMENDED_AMOUNT_FOR_WITHDRAWALS" values={{ amount: MIN_ETH_FOR_WITHDRAWALS.toString(), - symbol: account.symbol.toUpperCase(), + networkSymbol: displaySymbol, }} /> diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/StakeEthForm.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/StakeEthForm.tsx index aea033b6a82b..5818b0cdc4b9 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/StakeEthForm.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeEthForm/StakeEthForm.tsx @@ -55,7 +55,6 @@ export const StakeEthForm = () => { composedLevels={composedLevels} changeFeeLevel={changeFeeLevel} helperText={} - showFeeWhilePending={false} /> diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx index 01f910f0056d..6aeec5ae102d 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakeModal.tsx @@ -29,7 +29,12 @@ export const StakeModal = ({ onCancel }: StakeModalModalProps) => { } + heading={ + + } onCancel={onCancel} bottomContent={} > diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx index 9f65c58fa24c..e172a8371cad 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/StakeModal/StakingInfoCards/EstimatedGains.tsx @@ -10,7 +10,7 @@ import { Translation } from 'src/components/suite/Translation'; import { useStakeEthFormContext } from 'src/hooks/wallet/useStakeEthForm'; import { CRYPTO_INPUT } from 'src/types/wallet/stakeForms'; import { FiatValue, FormattedCryptoAmount, TrezorLink } from 'src/components/suite'; -import { calculateGains } from 'src/utils/suite/stake'; +import { calculateGains } from 'src/utils/suite/ethereumStaking'; export const EstimatedGains = () => { const { account, getValues, formState } = useStakeEthFormContext(); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/AmountDetails.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/AmountDetails.tsx index e1e3113d8f44..751aa4ee93c6 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/AmountDetails.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/AmountDetails.tsx @@ -43,7 +43,7 @@ export const AmountDetails = ({ tx, isTestnet }: AmountDetailsProps) => { const fee = formatNetworkAmount(tx.fee, tx.symbol); const cardanoWithdrawal = formatCardanoWithdrawal(tx); const cardanoDeposit = formatCardanoDeposit(tx); - const { selectedAccount } = useSelector(state => state.wallet); + const selectedAccount = useSelector(state => state.wallet.selectedAccount); const txSignature = tx.ethereumSpecific?.parsedData?.methodId; const isStakeTypeTxNoAmount = isStakeTypeTx(txSignature) && amount.eq(0); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/IODetails/IODetails.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/IODetails/IODetails.tsx index f22540056a17..934a80eb0abc 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/IODetails/IODetails.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/AdvancedTxDetails/IODetails/IODetails.tsx @@ -101,7 +101,7 @@ const IOGroup = ({ hasHeadings = true, isUtxoBased = false, }: IOGroupProps) => { - const { selectedAccount } = useSelector(state => state.wallet); + const selectedAccount = useSelector(state => state.wallet.selectedAccount); const anonymitySet = selectedAccount?.account?.addresses?.anonymitySet; const hasInputs = !!inputs?.length; @@ -316,8 +316,7 @@ type IODetailsProps = { // Not ready for Cardano tokens, they will not be visible, probably export const IODetails = ({ tx, isPhishingTransaction }: IODetailsProps) => { - const { selectedAccount } = useSelector(state => state.wallet); - const { network } = selectedAccount; + const network = useSelector(state => state.wallet.selectedAccount.network); const getContent = () => { if (network?.networkType === 'ethereum') { @@ -376,8 +375,6 @@ export const IODetails = ({ tx, isPhishingTransaction }: IODetailsProps) => { } }; - console.log(tx); - return ( diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx index 0723cf992628..ce407ab5b443 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/BasicTxDetails.tsx @@ -1,8 +1,8 @@ -import styled, { useTheme } from 'styled-components'; +import styled from 'styled-components'; import { fromWei } from 'web3-utils'; import { - IconCircle, + Icon, Text, H3, useElevation, @@ -31,10 +31,13 @@ const IconWrapper = styled.div<{ $elevation: Elevation }>` border-radius: ${borders.radii.full}; `; -const NestedIconWrapper = styled.div` +const NestedIconWrapper = styled.div<{ $elevation: Elevation }>` position: absolute; top: -${spacingsPx.xxs}; right: -${spacingsPx.xxs}; + background: ${mapElevationToBorder}; + border-radius: ${borders.radii.full}; + padding: ${spacingsPx.xxxs}; `; const Item = ({ label, iconName, children }: Partial) => ( @@ -67,7 +70,6 @@ export const BasicTxDetails = ({ explorerUrlQueryString, }: BasicTxDetailsProps) => { const { elevation } = useElevation(); - const theme = useTheme(); // all solana txs which are fetched are already confirmed const isConfirmed = confirmations > 0 || tx.solanaSpecific?.status === 'confirmed'; @@ -76,16 +78,10 @@ export const BasicTxDetails = ({ - - + diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx index 3ad6494275a6..8ae7ec741202 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ChangeFee.tsx @@ -7,7 +7,7 @@ import { spacings } from '@trezor/theme'; import { Translation, FiatValue, FormattedCryptoAmount } from 'src/components/suite'; import { useSelector } from 'src/hooks/suite'; -import { useRbf, RbfContext, UseRbfProps } from 'src/hooks/wallet/useRbfForm'; +import { useRbfContext, UseRbfProps } from 'src/hooks/wallet/useRbfForm'; import { RbfFees } from './RbfFees'; import { AffectedTransactions } from './AffectedTransactions'; @@ -21,53 +21,53 @@ interface ChangeFeeProps extends UseRbfProps { } const ChangeFeeLoaded = (props: ChangeFeeProps) => { - const contextValues = useRbf(props); const { tx, showChained, children } = props; - const { networkType } = contextValues.account; + const { + account: { networkType }, + } = useRbfContext(); + const feeRate = networkType === 'bitcoin' ? `${tx.rbfParams?.feeRate} ${getFeeUnits(networkType)}` : null; const fee = formatNetworkAmount(tx.fee, tx.symbol); return ( - - - - -  ({feeRate}) - - } - typographyStyle="body" - > - - + + +  ({feeRate}) + + } + typographyStyle="body" + > + + + + - - - - - +
+ + - + - + - - + + - {children} - - + {children} + ); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ReplaceTxButton.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ReplaceTxButton.tsx index af74735de9a9..5a3d67b70627 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ReplaceTxButton.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ReplaceTxButton.tsx @@ -1,21 +1,13 @@ import { NewModal } from '@trezor/components'; -import { SelectedAccountLoaded, RbfTransactionParams } from '@suite-common/wallet-types'; import { Translation } from 'src/components/suite'; import { useDevice } from 'src/hooks/suite'; -import { useRbf } from 'src/hooks/wallet/useRbfForm'; +import { useRbfContext } from 'src/hooks/wallet/useRbfForm'; -type ReplaceTxButtonProps = { - rbfParams: RbfTransactionParams; - selectedAccount: SelectedAccountLoaded; -}; - -export const ReplaceTxButton = ({ rbfParams, selectedAccount }: ReplaceTxButtonProps) => { +export const ReplaceTxButton = () => { const { device, isLocked } = useDevice(); - const { isLoading, signTransaction, getValues, composedLevels } = useRbf({ - selectedAccount, - rbfParams, - }); + + const { isLoading, signTransaction, getValues, composedLevels } = useRbfContext(); const values = getValues(); const composedTx = composedLevels ? composedLevels[values.selectedFee || 'normal'] : undefined; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx index 62ad5ade2802..6eb7fc6801a2 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModal.tsx @@ -1,25 +1,118 @@ import { useState, useMemo } from 'react'; -import { NewModal, Column, Banner } from '@trezor/components'; -import { HELP_CENTER_ZERO_VALUE_ATTACKS } from '@trezor/urls'; +import { NewModal } from '@trezor/components'; import { isPending, findChainedTransactions, getAccountKey } from '@suite-common/wallet-utils'; import { getNetwork } from '@suite-common/wallet-config'; import { selectAccountByKey, - selectTransactionConfirmations, selectAllPendingTransactions, selectIsPhishingTransaction, } from '@suite-common/wallet-core'; -import { spacings } from '@trezor/theme'; +import { + ChainedTransactions, + SelectedAccountLoaded, + WalletAccountTransactionWithRequiredRbfParams, +} from '@suite-common/wallet-types'; import { useSelector } from 'src/hooks/suite'; -import { Translation, TrezorLink } from 'src/components/suite'; +import { Translation } from 'src/components/suite'; import { Account, WalletAccountTransaction } from 'src/types/wallet'; +import { RbfContext, useRbf } from 'src/hooks/wallet/useRbfForm'; -import { BasicTxDetails } from './BasicTxDetails'; import { AdvancedTxDetails, TabID } from './AdvancedTxDetails/AdvancedTxDetails'; import { ChangeFee } from './ChangeFee/ChangeFee'; import { ReplaceTxButton } from './ChangeFee/ReplaceTxButton'; +import { TxDetailModalBase } from './TxDetailModalBase'; + +const hasRbfParams = ( + tx: WalletAccountTransaction, +): tx is WalletAccountTransactionWithRequiredRbfParams => tx.rbfParams !== undefined; + +type DetailModalProps = { + tx: WalletAccountTransaction; + onCancel: () => void; + tab: TabID | undefined; + onChangeFeeClick: () => void; + chainedTxs?: ChainedTransactions; + canBumpFee: boolean; +}; + +const DetailModal = ({ + tx, + onCancel, + tab, + onChangeFeeClick, + chainedTxs, + canBumpFee, +}: DetailModalProps) => { + const accountKey = getAccountKey(tx.descriptor, tx.symbol, tx.deviceState); + const account = useSelector(state => selectAccountByKey(state, accountKey)) as Account; + const network = getNetwork(account.symbol); + const isPhishingTransaction = useSelector(state => + selectIsPhishingTransaction(state, tx.txid, accountKey), + ); + const blockchain = useSelector(state => state.wallet.blockchain[tx.symbol]); + + return ( + } + bottomContent={ + canBumpFee ? ( + + + + ) : null + } + onBackClick={undefined} + > + + + ); +}; + +type BumpFeeModalProps = { + tx: WalletAccountTransactionWithRequiredRbfParams; + onCancel: () => void; + onBackClick: () => void; + onShowChained: () => void; + chainedTxs?: ChainedTransactions; + selectedAccount: SelectedAccountLoaded; +}; + +const BumpFeeModal = ({ + tx, + onCancel, + onBackClick, + onShowChained, + chainedTxs, + selectedAccount, +}: BumpFeeModalProps) => { + const contextValues = useRbf({ rbfParams: tx.rbfParams, chainedTxs, selectedAccount }); + + return ( + + } + bottomContent={} + onBackClick={onBackClick} + > + + + + ); +}; type TxDetailModalProps = { tx: WalletAccountTransaction; @@ -28,14 +121,18 @@ type TxDetailModalProps = { }; export const TxDetailModal = ({ tx, rbfForm, onCancel }: TxDetailModalProps) => { - const blockchain = useSelector(state => state.wallet.blockchain[tx.symbol]); - const transactions = useSelector(selectAllPendingTransactions); - const [section, setSection] = useState<'CHANGE_FEE' | 'DETAILS'>( rbfForm ? 'CHANGE_FEE' : 'DETAILS', ); const [tab, setTab] = useState(undefined); + const accountKey = getAccountKey(tx.descriptor, tx.symbol, tx.deviceState); + const account = useSelector(state => selectAccountByKey(state, accountKey)) as Account; + const network = getNetwork(account.symbol); + const networkFeatures = network.accountTypes[account.accountType]?.features ?? network.features; + const selectedAccount = useSelector(state => state.wallet.selectedAccount); + + const transactions = useSelector(selectAllPendingTransactions); // const confirmations = getConfirmations(tx, blockchain.blockHeight); // TODO: replace this part will be refactored after blockbook implementation: // https://github.com/trezor/blockbook/issues/555 @@ -44,114 +141,49 @@ export const TxDetailModal = ({ tx, rbfForm, onCancel }: TxDetailModalProps) => return findChainedTransactions(tx.descriptor, tx.txid, transactions); }, [tx, transactions]); - const accountKey = getAccountKey(tx.descriptor, tx.symbol, tx.deviceState); - const confirmations = useSelector(state => - selectTransactionConfirmations(state, tx.txid, accountKey), - ); - const account = useSelector(state => selectAccountByKey(state, accountKey)) as Account; - const selectedAccount = useSelector(state => state.wallet.selectedAccount); - const network = getNetwork(account.symbol); - const networkFeatures = network.accountTypes[account.accountType]?.features ?? network.features; - - const isPhishingTransaction = useSelector(state => - selectIsPhishingTransaction(state, tx.txid, accountKey), - ); const onBackClick = () => { setSection('DETAILS'); setTab(undefined); }; - const getBottomContent = () => { - if ( - networkFeatures?.includes('rbf') && - tx.rbfParams && - !tx.deadline && - selectedAccount.status === 'loaded' - ) { - if (section === 'CHANGE_FEE') { - return ( - - ); - } else { - return ( - { - setSection('CHANGE_FEE'); - setTab(undefined); - }} - > - - - ); - } - } + const onShowChained = () => { + setSection('DETAILS'); + setTab('chained'); }; + const onChangeFeeClick = () => { + setSection('CHANGE_FEE'); + setTab(undefined); + }; + + const canBumpFee = + hasRbfParams(tx) && + networkFeatures?.includes('rbf') && + !tx.deadline && + selectedAccount.status === 'loaded'; + + if (section === 'CHANGE_FEE' && canBumpFee) { + return ( + + ); + } + return ( - - ) : ( - - ) - } - size="large" - bottomContent={getBottomContent()} - onBackClick={section === 'CHANGE_FEE' ? onBackClick : undefined} - > - - - - {isPhishingTransaction && ( - - ( - - {chunks} - - ), - }} - /> - - )} - - {section === 'CHANGE_FEE' ? ( - { - setSection('DETAILS'); - setTab('chained'); - }} - /> - ) : ( - - )} - - + tab={tab} + onChangeFeeClick={onChangeFeeClick} + chainedTxs={chainedTxs} + canBumpFee={canBumpFee} + /> ); }; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModalBase.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModalBase.tsx new file mode 100644 index 000000000000..1466d3a1c68b --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/TxDetailModalBase.tsx @@ -0,0 +1,90 @@ +import { ReactNode } from 'react'; + +import { NewModal, Column, Banner } from '@trezor/components'; +import { HELP_CENTER_ZERO_VALUE_ATTACKS } from '@trezor/urls'; +import { getAccountKey } from '@suite-common/wallet-utils'; +import { getNetwork } from '@suite-common/wallet-config'; +import { + selectAccountByKey, + selectTransactionConfirmations, + selectIsPhishingTransaction, +} from '@suite-common/wallet-core'; +import { spacings } from '@trezor/theme'; + +import { useSelector } from 'src/hooks/suite'; +import { Translation, TrezorLink } from 'src/components/suite'; +import { Account, WalletAccountTransaction } from 'src/types/wallet'; + +import { BasicTxDetails } from './BasicTxDetails'; + +type TxDetailModalProps = { + tx: WalletAccountTransaction; + onCancel: () => void; + onBackClick: (() => void) | undefined; + heading: ReactNode; + bottomContent: ReactNode | undefined; + children: ReactNode; +}; + +export const TxDetailModalBase = ({ + tx, + onCancel, + onBackClick, + heading, + bottomContent, + children, +}: TxDetailModalProps) => { + const blockchain = useSelector(state => state.wallet.blockchain[tx.symbol]); + + const accountKey = getAccountKey(tx.descriptor, tx.symbol, tx.deviceState); + const confirmations = useSelector(state => + selectTransactionConfirmations(state, tx.txid, accountKey), + ); + const account = useSelector(state => selectAccountByKey(state, accountKey)) as Account; + const network = getNetwork(account.symbol); + + const isPhishingTransaction = useSelector(state => + selectIsPhishingTransaction(state, tx.txid, accountKey), + ); + + return ( + + + + + {isPhishingTransaction && ( + + ( + + {chunks} + + ), + }} + /> + + )} + + {children} + + + ); +}; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx index 18aab999bb48..dc16f1272520 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/EverstakeModal.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; -import { Checkbox, NewModal, Column, Banner, Card } from '@trezor/components'; +import { Checkbox, NewModal, Column, Banner, Card, IconName } from '@trezor/components'; import { spacings } from '@trezor/theme'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; @@ -22,9 +23,50 @@ export const EverstakeModal = ({ onCancel }: EverstakeModalProps) => { dispatch(openModal({ type: 'stake' })); }; + if (!account) return null; + + const displaySymbol = getNetworkDisplaySymbol(account.symbol); + + const banners: { + icon: IconName; + message: JSX.Element; + }[] = [ + { + icon: 'fileFilled', + message: ( + {text}, + }} + /> + ), + }, + { + icon: 'shieldWarningFilled', + message: ( + + ), + }, + ]; + return ( } + heading={} description={} onCancel={onCancel} size="small" @@ -40,18 +82,11 @@ export const EverstakeModal = ({ onCancel }: EverstakeModalProps) => { } > - - {text}, - }} - /> - - - - + {banners.map(({ icon, message }, index) => ( + + {message} + + ))} { composedLevels={composedLevels} changeFeeLevel={changeFeeLevel} helperText={} - showFeeWhilePending={false} /> ); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx index 377a2d426017..88fd15abbe69 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UnstakeModal/UnstakeEthForm/UnstakeEthForm.tsx @@ -10,7 +10,7 @@ import { Fees } from 'src/components/wallet/Fees/Fees'; import { useUnstakeEthFormContext } from 'src/hooks/wallet/useUnstakeEthForm'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; import { CRYPTO_INPUT, FIAT_INPUT } from 'src/types/wallet/stakeForms'; -import { getUnstakingPeriodInDays } from 'src/utils/suite/stake'; +import { getUnstakingPeriodInDays } from 'src/utils/suite/ethereumStaking'; import { ApproximateInstantEthAmount } from 'src/views/wallet/staking/components/EthStakingDashboard/components/ApproximateInstantEthAmount'; import { Options } from './Options'; @@ -80,7 +80,6 @@ export const UnstakeEthForm = () => { composedLevels={composedLevels} changeFeeLevel={changeFeeLevel} helperText={} - showFeeWhilePending={false} /> diff --git a/packages/suite/src/components/suite/notifications/NotificationRenderer/NotificationRenderer.tsx b/packages/suite/src/components/suite/notifications/NotificationRenderer/NotificationRenderer.tsx index b249cb702dc9..8947c8256bbe 100644 --- a/packages/suite/src/components/suite/notifications/NotificationRenderer/NotificationRenderer.tsx +++ b/packages/suite/src/components/suite/notifications/NotificationRenderer/NotificationRenderer.tsx @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux'; import { AUTH_DEVICE, type NotificationEntry } from '@suite-common/toast-notifications'; import { selectSelectedDeviceLabelOrName } from '@suite-common/wallet-core'; import { DEVICE } from '@trezor/connect'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { NotificationViewProps } from 'src/components/suite'; import type { ExtendedMessageDescriptor } from 'src/types/suite'; @@ -291,7 +292,7 @@ export const NotificationRenderer = ({ ); case 'successful-claim': return success(render, notification, 'TOAST_SUCCESSFUL_CLAIM', 'check', { - symbol: notification.symbol, + networkSymbol: getNetworkDisplaySymbol(notification.symbol), }); case 'firmware-language-changed': return success(render, notification, 'TR_FIRMWARE_LANGUAGE_CHANGED'); diff --git a/packages/suite/src/components/wallet/DiscoveryProgress.tsx b/packages/suite/src/components/wallet/DiscoveryProgress.tsx index 0123aca49a26..26c82c6f3d05 100644 --- a/packages/suite/src/components/wallet/DiscoveryProgress.tsx +++ b/packages/suite/src/components/wallet/DiscoveryProgress.tsx @@ -1,25 +1,19 @@ -import styled from 'styled-components'; - -import { ProgressBar } from '@trezor/components'; +import { ProgressBar, Box } from '@trezor/components'; import { zIndices } from '@trezor/theme'; import { useDiscovery } from 'src/hooks/suite'; -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledProgressBar = styled(ProgressBar)` - height: 0; - z-index: ${zIndices.discoveryProgress}; -`; - export const DiscoveryProgress = () => { const { discovery, isDiscoveryRunning, calculateProgress } = useDiscovery(); if (!discovery || !isDiscoveryRunning) return null; return ( - + + + ); }; diff --git a/packages/suite/src/components/wallet/Fees/CustomFee.tsx b/packages/suite/src/components/wallet/Fees/CustomFee.tsx index 80557bce6d36..65168983224a 100644 --- a/packages/suite/src/components/wallet/Fees/CustomFee.tsx +++ b/packages/suite/src/components/wallet/Fees/CustomFee.tsx @@ -119,9 +119,13 @@ export const CustomFee = ({ except: networkType !== 'ethereum', }), range: (value: string) => { - const feeBig = new BigNumber(value); - if (feeBig.isGreaterThan(maxFee) || feeBig.isLessThan(minFee)) { - return translationString('CUSTOM_FEE_NOT_IN_RANGE', { minFee, maxFee }); + const customFee = new BigNumber(value); + + if (customFee.isGreaterThan(maxFee) || customFee.isLessThan(minFee)) { + return translationString('CUSTOM_FEE_NOT_IN_RANGE', { + minFee: new BigNumber(minFee).toString(), + maxFee: new BigNumber(maxFee).toString(), + }); } }, }, @@ -166,7 +170,7 @@ export const CustomFee = ({ feeLimitError?.message ? ( ) : null } diff --git a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx b/packages/suite/src/components/wallet/Fees/FeeDetails.tsx index 201b3a43e548..0482a3645f46 100644 --- a/packages/suite/src/components/wallet/Fees/FeeDetails.tsx +++ b/packages/suite/src/components/wallet/Fees/FeeDetails.tsx @@ -110,7 +110,7 @@ export const FeeDetails = (props: DetailsProps) => { const { networkType } = props; return ( - + {networkType === 'bitcoin' && } {networkType === 'ethereum' && } diff --git a/packages/suite/src/components/wallet/Fees/Fees.tsx b/packages/suite/src/components/wallet/Fees/Fees.tsx index 824691fb4407..d3035396f939 100644 --- a/packages/suite/src/components/wallet/Fees/Fees.tsx +++ b/packages/suite/src/components/wallet/Fees/Fees.tsx @@ -66,7 +66,6 @@ export interface FeesProps { label?: TranslationKey; rbfForm?: boolean; helperText?: React.ReactNode; - showFeeWhilePending?: boolean; } export const Fees = ({ @@ -79,7 +78,6 @@ export const Fees = ({ label, rbfForm, helperText, - showFeeWhilePending = true, ...props }: FeesProps) => { // Type assertion allowing to make the component reusable, see https://stackoverflow.com/a/73624072. @@ -95,8 +93,7 @@ export const Fees = ({ // Solana has only `normal` fee level, so we do not display any feeOptions since there is nothing to choose from const feeOptions = networkType === 'solana' ? [] : buildFeeOptions(feeInfo.levels); - const showNormalFee = showFeeWhilePending || transactionInfo?.type === 'final'; - const shouldAnimateNormalFee = showNormalFee && !isCustomLevel; + const shouldAnimateNormalFee = !isCustomLevel; return ( @@ -164,7 +161,7 @@ export const Fees = ({ feeInfo={feeInfo} selectedLevel={selectedLevel} transactionInfo={transactionInfo} - showFee={showNormalFee} + showFee={true} /> )} diff --git a/packages/suite/src/components/wallet/FiatHeader.tsx b/packages/suite/src/components/wallet/FiatHeader.tsx index 11705e86d439..27b9d70ebca4 100644 --- a/packages/suite/src/components/wallet/FiatHeader.tsx +++ b/packages/suite/src/components/wallet/FiatHeader.tsx @@ -37,6 +37,7 @@ type UseFiatAmountProps = { amount: string; symbol?: NetworkSymbol }; type FiatHeaderProps = { size: 'large' | 'medium'; localCurrency: string; + 'data-testid'?: string; } & UseFiatAmountProps; // redacted value placeholder doesn't have to be displayed twice, display it only for whole value @@ -59,7 +60,13 @@ const useFiatAmount = ({ amount, symbol }: UseFiatAmountProps) => { /** * If `symbol` is not provided, `amount` is returned as is, otherwise it is converted to fiat currency. */ -export const FiatHeader = ({ amount, symbol, size, localCurrency }: FiatHeaderProps) => { +export const FiatHeader = ({ + amount, + symbol, + size, + localCurrency, + 'data-testid': dataTestId, +}: FiatHeaderProps) => { const language = useSelector(selectLanguage); const fiatAmount = useFiatAmount({ amount, symbol }); const { FiatAmountFormatter } = useFormatters(); @@ -75,7 +82,7 @@ export const FiatHeader = ({ amount, symbol, size, localCurrency }: FiatHeaderPr return ( - + diff --git a/packages/suite/src/components/wallet/InputError.tsx b/packages/suite/src/components/wallet/InputError.tsx index 3d0c1e717292..31cce366e101 100644 --- a/packages/suite/src/components/wallet/InputError.tsx +++ b/packages/suite/src/components/wallet/InputError.tsx @@ -7,23 +7,23 @@ import { spacings } from '@trezor/theme'; import { LearnMoreButton } from '../suite/LearnMoreButton'; type ButtonProps = { onClick: MouseEventHandler; text: string }; -type LinkProps = { url: Url }; export type InputErrorProps = { - button?: ButtonProps | LinkProps; + buttonProps?: ButtonProps; + learnMoreUrl?: Url; message?: string; }; -export const InputError = ({ button, message }: InputErrorProps) => ( +export const InputError = ({ buttonProps, learnMoreUrl, message }: InputErrorProps) => ( - {message} - {button && - ('url' in button ? ( - - ) : ( - - ))} + + {message} + {learnMoreUrl && } + + {buttonProps?.text && ( + + )} ); diff --git a/packages/suite/src/components/wallet/TokenIconSetWrapper.tsx b/packages/suite/src/components/wallet/TokenIconSetWrapper.tsx index e4f97bb0fbc1..55398ee729eb 100644 --- a/packages/suite/src/components/wallet/TokenIconSetWrapper.tsx +++ b/packages/suite/src/components/wallet/TokenIconSetWrapper.tsx @@ -30,8 +30,11 @@ export const TokenIconSetWrapper = ({ accounts, symbol }: TokenIconSetWrapperPro if (!allTokensWithRates.length) return null; - const tokens = getTokens(allTokensWithRates, symbol, coinDefinitions) - .shownWithBalance as TokensWithRates[]; + const tokens = getTokens({ + tokens: allTokensWithRates, + symbol, + tokenDefinitions: coinDefinitions, + })?.shownWithBalance as TokensWithRates[]; const aggregatedTokens = Object.values( tokens.reduce((acc: Record, token) => { diff --git a/packages/suite/src/components/wallet/TransactionItem/InstantStakeBadge.tsx b/packages/suite/src/components/wallet/TransactionItem/InstantStakeBadge.tsx index 1698f9e90512..8688434e0248 100644 --- a/packages/suite/src/components/wallet/TransactionItem/InstantStakeBadge.tsx +++ b/packages/suite/src/components/wallet/TransactionItem/InstantStakeBadge.tsx @@ -12,7 +12,7 @@ import { spacings, spacingsPx } from '@trezor/theme'; import { Translation, FormattedCryptoAmount } from 'src/components/suite'; import { WalletAccountTransaction } from 'src/types/wallet'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getInstantStakeType } from 'src/utils/suite/stake'; +import { getInstantStakeType } from 'src/utils/suite/ethereumStaking'; const Wrapper = styled.div` display: flex; diff --git a/packages/suite/src/components/wallet/TransactionItem/TransactionHeader.tsx b/packages/suite/src/components/wallet/TransactionItem/TransactionHeader.tsx index 4d437580ce7c..d43d564fa81d 100644 --- a/packages/suite/src/components/wallet/TransactionItem/TransactionHeader.tsx +++ b/packages/suite/src/components/wallet/TransactionItem/TransactionHeader.tsx @@ -2,6 +2,7 @@ import { getTxHeaderSymbol, isSupportedEthStakingNetworkSymbol } from '@suite-co import { AccountTransaction } from '@trezor/connect'; import { Row } from '@trezor/components'; import { spacings } from '@trezor/theme'; +import { getNetworkDisplaySymbol, isNetworkSymbol } from '@suite-common/wallet-config'; import { useTranslation } from 'src/hooks/suite'; import { WalletAccountTransaction } from 'src/types/wallet'; @@ -79,7 +80,11 @@ export const TransactionHeader = ({ transaction, isPending }: TransactionHeaderP } const isMultiTokenTransaction = transaction.tokens.length > 1; - const symbol = getTxHeaderSymbol(transaction)?.toUpperCase(); + const transactionSymbol = getTxHeaderSymbol(transaction); + const symbol = + transactionSymbol && isNetworkSymbol(transactionSymbol) + ? getNetworkDisplaySymbol(transactionSymbol) + : transactionSymbol?.toUpperCase(); return ( )} - + {isSupportedEthStakingNetworkSymbol(transaction.symbol) && ( diff --git a/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx b/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx index f0fbeb27b517..0138a0ecde04 100644 --- a/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx +++ b/packages/suite/src/components/wallet/TransactionItem/TransactionItem.tsx @@ -25,7 +25,7 @@ import { AccountTransactionBaseAnchor } from 'src/constants/suite/anchors'; import { TransactionTimestamp } from 'src/components/wallet/TransactionTimestamp'; import { SUBPAGE_NAV_HEIGHT } from 'src/constants/suite/layout'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; -import { getInstantStakeType } from 'src/utils/suite/stake'; +import { getInstantStakeType } from 'src/utils/suite/ethereumStaking'; import { OutlineHighlight } from 'src/components/OutlineHighlight'; import { TransactionTypeIcon } from './TransactionTypeIcon'; diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/AccountBanners.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/AccountBanners.tsx index 54a5b23ce094..131d21cea484 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/AccountBanners.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/AccountBanners.tsx @@ -1,5 +1,7 @@ import { Context } from '@suite-common/message-system'; import { isSupportedEthStakingNetworkSymbol } from '@suite-common/wallet-utils'; +import { Column } from '@trezor/components'; +import { spacings } from '@trezor/theme'; import { Account } from 'src/types/wallet'; import { useSelector } from 'src/hooks/suite'; @@ -24,7 +26,7 @@ export const AccountBanners = ({ account }: AccountBannersProps) => { const { route } = useSelector(state => state.router); return ( - <> + {account?.accountType === 'coinjoin' && } {account?.symbol && isSupportedEthStakingNetworkSymbol(account.symbol) && @@ -39,6 +41,6 @@ export const AccountBanners = ({ account }: AccountBannersProps) => { {account?.symbol && } - + ); }; diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/EvmExplanationBanner.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/EvmExplanationBanner.tsx index ee0325ece724..7f211191a702 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/EvmExplanationBanner.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/EvmExplanationBanner.tsx @@ -1,5 +1,4 @@ import { networks } from '@suite-common/wallet-config'; -import { spacings } from '@trezor/theme'; import { Account } from 'src/types/wallet'; import { Translation } from 'src/components/suite'; @@ -53,7 +52,6 @@ export const EvmExplanationBanner = ({ account }: EvmExplanationBannerProps) => /> } hasIcon={points.length === 1} - margin={{ bottom: spacings.sm }} > diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx index 520df0f05d71..0f51f767252d 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountBanners/StakeEthBanner.tsx @@ -6,6 +6,7 @@ import { Account } from '@suite-common/wallet-types'; import { selectPoolStatsApyData } from '@suite-common/wallet-core'; import { MIN_ETH_AMOUNT_FOR_STAKING } from '@suite-common/wallet-constants'; import { isSupportedEthStakingNetworkSymbol } from '@suite-common/wallet-utils'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Translation } from 'src/components/suite'; import { goto } from 'src/actions/suite/routerActions'; @@ -65,7 +66,7 @@ export const StakeEthBanner = ({ account }: StakeEthBannerProps) => { id="TR_STAKE_ANY_AMOUNT_ETH" values={{ apyPercent: ethApy, - symbol: account?.symbol.toUpperCase(), + networkSymbol: getNetworkDisplaySymbol(account.symbol), amount: MIN_ETH_AMOUNT_FOR_STAKING.toString(), }} /> diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx index 9a61238f7b9e..2a223d66e40d 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountTopPanel/AccountNavigation.tsx @@ -8,20 +8,28 @@ import { useDispatch, useSelector } from 'src/hooks/suite'; import { goto } from 'src/actions/suite/routerActions'; import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; import { NavigationItem, SubpageNavigation } from 'src/components/suite/layouts/SuiteLayout'; +import { + selectIsDebugModeActive, + selectHasExperimentalFeature, +} from 'src/reducers/suite/suiteReducer'; export const ACCOUNT_TABS = [ 'wallet-index', 'wallet-details', 'wallet-tokens', + 'wallet-nfts', + 'wallet-nfts-hidden', 'wallet-tokens-hidden', 'wallet-staking', ]; export const AccountNavigation = () => { + const isDebugModeActive = useSelector(selectIsDebugModeActive); + const account = useSelector(selectSelectedAccount); const routerParams = useSelector(state => state.router.params) as WalletParams; const dispatch = useDispatch(); - + const enabledNftSection = useSelector(selectHasExperimentalFeature('nft-section')); const network = getNetworkOptional(routerParams?.symbol); const networkType = account?.networkType || network?.networkType || ''; @@ -54,13 +62,26 @@ export const AccountNavigation = () => { activeRoutes: ['wallet-tokens', 'wallet-tokens-hidden'], 'data-testid': '@wallet/menu/wallet-tokens', }, + { + id: 'wallet-nfts', + callback: () => { + goToWithAnalytics('wallet-nfts', { preserveParams: true }); + }, + title: , + isHidden: !hasNetworkFeatures(account, 'nfts') || !enabledNftSection, + activeRoutes: ['wallet-nfts', 'wallet-nfts-hidden'], + 'data-testid': '@wallet/menu/wallet-nfts', + }, { id: 'wallet-staking', callback: () => { goToWithAnalytics('wallet-staking', { preserveParams: true }); }, title: , - isHidden: !hasNetworkFeatures(account, 'staking'), + // TODO: remove 'solana' and debug mode check from the condition when staking will be ready for launch + isHidden: + !hasNetworkFeatures(account, 'staking') || + (!isDebugModeActive && 'solana' === networkType), 'data-testid': '@wallet/menu/staking', }, { diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx index b2f3e9e48906..5bfc2917292d 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountItemSkeleton.tsx @@ -3,8 +3,24 @@ import { spacings } from '@trezor/theme'; import { useLoadingSkeleton } from 'src/hooks/suite'; +import { useResponsiveContext } from '../../../../support/suite/ResponsiveContext'; + export const AccountItemSkeleton = () => { const { shouldAnimate } = useLoadingSkeleton(); + const { isSidebarCollapsed } = useResponsiveContext(); + + if (isSidebarCollapsed) { + return ( + + + + ); + } return ( ` - flex: 1; -`; +import { useAccountSearch, useTranslation } from 'src/hooks/suite'; // eslint-disable-next-line local-rules/no-override-ds-component const StyledInput = styled(Input)` @@ -27,13 +21,6 @@ export const AccountSearchBox = () => { const theme = useTheme(); const { translationString } = useTranslation(); const { setCoinFilter, searchString, setSearchString } = useAccountSearch(); - const enabledNetworks = useSelector(selectEnabledNetworks); - const device = useSelector(selectSelectedDevice); - - const unavailableCapabilities = device?.unavailableCapabilities ?? {}; - const supportedNetworks = enabledNetworks.filter(symbol => !unavailableCapabilities[symbol]); - - const showCoinFilter = supportedNetworks.length > 1; const onClear = () => { setSearchString(undefined); @@ -41,20 +28,18 @@ export const AccountSearchBox = () => { }; return ( - - { - setSearchString(e.target.value); - }} - innerAddon={} - innerAddonAlign="left" - size="small" - placeholder={translationString('TR_SEARCH')} - showClearButton="always" - onClear={onClear} - data-testid="@account-menu/search-input" - /> - + { + setSearchString(e.target.value); + }} + innerAddon={} + innerAddonAlign="left" + size="small" + placeholder={translationString('TR_SEARCH')} + showClearButton="always" + onClear={onClear} + data-testid="@account-menu/search-input" + /> ); }; diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx index 06dc023a1d5c..28b1b92baeed 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/AccountSection.tsx @@ -1,10 +1,11 @@ import { Account } from '@suite-common/wallet-types'; import { selectCoinDefinitions } from '@suite-common/token-definitions'; -import { selectAccountHasStaked } from '@suite-common/wallet-core'; -import { isSupportedEthStakingNetworkSymbol } from '@suite-common/wallet-utils'; +import { selectAccountHasStaked, selectStakingAccounts } from '@suite-common/wallet-core'; +import { isSupportedStakingNetworkSymbol } from '@suite-common/wallet-utils'; import { useSelector } from 'src/hooks/suite'; import { getTokens } from 'src/utils/wallet/tokenUtils'; +import { selectIsDebugModeActive } from 'src/reducers/suite/suiteReducer'; import { AccountItem } from './AccountItem/AccountItem'; import { AccountItemsGroup } from './AccountItemsGroup'; @@ -32,14 +33,24 @@ export const AccountSection = ({ tokens: accountTokens = [], } = account; + const isDebugModeActive = useSelector(selectIsDebugModeActive); + const coinDefinitions = useSelector(state => selectCoinDefinitions(state, symbol)); const hasStaked = useSelector(state => selectAccountHasStaked(state, account.key)); + const stakingAccounts = useSelector(state => selectStakingAccounts(state, account.key)); + // TODO: remove isDebugModeActive when staking will be ready for launch + const hasStakingAccount = !!stakingAccounts?.length && isDebugModeActive; // for solana - const isStakeShown = isSupportedEthStakingNetworkSymbol(symbol) && hasStaked; + const isStakeShown = + isSupportedStakingNetworkSymbol(symbol) && (hasStaked || hasStakingAccount); const showGroup = ['ethereum', 'solana', 'cardano'].includes(networkType); - const tokens = getTokens(accountTokens, account.symbol, coinDefinitions); + const tokens = getTokens({ + tokens: accountTokens, + symbol: account.symbol, + tokenDefinitions: coinDefinitions, + }); const dataTestKey = `@account-menu/${symbol}/${accountType}/${index}`; diff --git a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/CoinsFilter.tsx b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/CoinsFilter.tsx index d81e5dd2e2ef..7314c5d1bda9 100644 --- a/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/CoinsFilter.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/AccountsMenu/CoinsFilter.tsx @@ -1,13 +1,13 @@ import styled from 'styled-components'; import { motion, AnimatePresence, MotionProps } from 'framer-motion'; -import { selectSelectedDevice } from '@suite-common/wallet-core'; import { TOOLTIP_DELAY_NORMAL, Tooltip, motionEasing } from '@trezor/components'; import { CoinLogo } from '@trezor/product-components'; import { borders, spacingsPx } from '@trezor/theme'; import { useSelector, useAccountSearch } from 'src/hooks/suite'; import { selectEnabledNetworks } from 'src/reducers/wallet/settingsReducer'; +import { useNetworkSupport } from 'src/hooks/settings/useNetworkSupport'; // eslint-disable-next-line local-rules/no-override-ds-component const StyledCoinLogo = styled(CoinLogo)<{ $isSelected?: boolean }>` @@ -44,12 +44,15 @@ const Container = styled.div` export const CoinsFilter = () => { const { coinFilter, setCoinFilter } = useAccountSearch(); const enabledNetworks = useSelector(selectEnabledNetworks); - const device = useSelector(selectSelectedDevice); + const { supportedMainnets, supportedTestnets } = useNetworkSupport(); - const unavailableCapabilities = device?.unavailableCapabilities ?? {}; - const supportedNetworks = enabledNetworks.filter(symbol => !unavailableCapabilities[symbol]); + const supportedNetworks = [...supportedMainnets, ...supportedTestnets].map( + network => network.symbol, + ); + + const availableNetworks = enabledNetworks.filter(symbol => supportedNetworks.includes(symbol)); - const showCoinFilter = supportedNetworks.length > 1; + const showCoinFilter = availableNetworks.length > 1; const coinAnimcationConfig: MotionProps = { initial: { @@ -80,7 +83,7 @@ export const CoinsFilter = () => { }} > - {supportedNetworks.map(network => { + {availableNetworks.map(network => { const isSelected = coinFilter === network; return ( diff --git a/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx b/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx index 4aa3d1852dd4..2e0059dd690a 100644 --- a/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx +++ b/packages/suite/src/components/wallet/WalletLayout/WalletLayout.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react'; import { SkeletonRectangle, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; +import { PrimitiveType } from '@trezor/type-utils'; import { AppState } from 'src/types/suite'; import { useTranslation, useLayout } from 'src/hooks/suite'; @@ -30,14 +31,21 @@ const WalletPageHeader = ({ isSubpage }: WalletPageHeaderProps) => { type WalletLayoutProps = { title: TranslationKey; + titleValues?: Record; account: AppState['wallet']['selectedAccount']; isSubpage?: boolean; children?: ReactNode; }; -export const WalletLayout = ({ title, account, isSubpage, children }: WalletLayoutProps) => { +export const WalletLayout = ({ + title, + titleValues, + account, + isSubpage, + children, +}: WalletLayoutProps) => { const { translationString } = useTranslation(); - const l10nTitle = translationString(title); + const l10nTitle = translationString(title, titleValues); useLayout(l10nTitle, ); diff --git a/packages/suite/src/constants/suite/experimental.ts b/packages/suite/src/constants/suite/experimental.ts index 45e6721e889f..0d9e19cc4a82 100644 --- a/packages/suite/src/constants/suite/experimental.ts +++ b/packages/suite/src/constants/suite/experimental.ts @@ -1,11 +1,16 @@ import { TranslationKey } from '@suite-common/intl-types'; import { desktopApi } from '@trezor/suite-desktop-api'; -import { EXPERIMENTAL_PASSWORD_MANAGER_KB_URL, TOR_SNOWFLAKE_KB_URL, Url } from '@trezor/urls'; +import { EXPERIMENTAL_PASSWORD_MANAGER_KB_URL, Url } from '@trezor/urls'; import { Route } from '@suite-common/suite-types'; +import { isDesktop } from '@trezor/env-utils'; import { Dispatch } from '../../types/suite'; -export type ExperimentalFeature = 'password-manager' | 'tor-snowflake'; +export type ExperimentalFeature = + | 'password-manager' + | 'tor-external' + | 'nft-section' + | 'ethereum-l2-support'; export type ExperimentalFeatureConfig = { title: TranslationKey; @@ -23,20 +28,28 @@ export const EXPERIMENTAL_FEATURES: Record !isDesktop(), onToggle: async ({ newValue }) => { - if (!newValue) { - const result = await desktopApi.getTorSettings(); - if (result.success && result.payload.snowflakeBinaryPath !== '') { - await desktopApi.changeTorSettings({ - ...result.payload, - snowflakeBinaryPath: '', - }); - } + const result = await desktopApi.getTorSettings(); + if (result.success && result.payload.useExternalTor !== newValue) { + await desktopApi.changeTorSettings({ + ...result.payload, + useExternalTor: newValue, + }); } }, }, + 'ethereum-l2-support': { + title: 'TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT', + description: 'TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT_DESCRIPTION', + }, + 'nft-section': { + title: 'TR_EXPERIMENTAL_NFT_SECTION', + description: 'TR_EXPERIMENTAL_NFT_SECTION_DESCRIPTION', + }, }; diff --git a/packages/suite/src/hooks/settings/useNetworkSupport.ts b/packages/suite/src/hooks/settings/useNetworkSupport.ts index a29d4008bf1c..8d2082a8cf54 100644 --- a/packages/suite/src/hooks/settings/useNetworkSupport.ts +++ b/packages/suite/src/hooks/settings/useNetworkSupport.ts @@ -5,14 +5,35 @@ import { hasBitcoinOnlyFirmware } from '@trezor/device-utils'; import { arrayPartition } from '@trezor/utils'; import { useSelector } from 'src/hooks/suite'; -import { selectIsDebugModeActive } from 'src/reducers/suite/suiteReducer'; +import { + selectIsDebugModeActive, + selectHasExperimentalFeature, +} from 'src/reducers/suite/suiteReducer'; +import { EXPERIMENTAL_L2_NETWORKS } from 'src/actions/suite/constants/suiteConstants'; +import { selectEnabledNetworks } from 'src/reducers/wallet/settingsReducer'; export const useNetworkSupport = () => { const device = useSelector(selectSelectedDevice); const isDebug = useSelector(selectIsDebugModeActive); const deviceSupportedNetworkSymbols = useSelector(selectDeviceSupportedNetworks); + const isEthereumL2SupportEnabled = useSelector( + selectHasExperimentalFeature('ethereum-l2-support'), + ); + const enabledNetworks = useSelector(selectEnabledNetworks); + + const mainnets = getMainnets(isDebug).filter(network => { + if (isEthereumL2SupportEnabled) { + return true; // no filtering needed if L2 support is enabled + } + + // if L2 support is not enabled + const isExperimentalL2 = EXPERIMENTAL_L2_NETWORKS.includes(network.symbol); + const isEnabled = enabledNetworks.includes(network.symbol); + + // filter out experimental L2 networks unless they are in the enabled networks + return !(isExperimentalL2 && !isEnabled); + }); - const mainnets = getMainnets(isDebug); const testnets = getTestnets(isDebug); const isNetworkSupported = (network: Network) => diff --git a/packages/suite/src/hooks/suite/useDisplayMode.ts b/packages/suite/src/hooks/suite/useDisplayMode.ts index eb5918363338..b02c4f3d06ed 100644 --- a/packages/suite/src/hooks/suite/useDisplayMode.ts +++ b/packages/suite/src/hooks/suite/useDisplayMode.ts @@ -9,15 +9,15 @@ import { useSelector } from './useSelector'; type UseDisplayModeProps = { type: ReviewOutput['type']; - ethereumStakeType?: StakeType; + stakeType?: StakeType; }; -export const useDisplayMode = ({ type, ethereumStakeType }: UseDisplayModeProps) => { +export const useDisplayMode = ({ type, stakeType }: UseDisplayModeProps) => { const account = useSelector(selectSelectedAccount); const unavailableCapabilities = useSelector(selectDeviceUnavailableCapabilities); const addressDisplayType = useSelector(selectAddressDisplayType); - if (ethereumStakeType || ['data', 'opreturn'].includes(type)) { + if (stakeType || ['data', 'opreturn'].includes(type)) { return DisplayMode.SINGLE_WRAPPED_TEXT; } diff --git a/packages/suite/src/hooks/wallet/__fixtures__/useRbfForm.ts b/packages/suite/src/hooks/wallet/__fixtures__/useRbfForm.ts index 1d718b927ea6..c39c9006352f 100644 --- a/packages/suite/src/hooks/wallet/__fixtures__/useRbfForm.ts +++ b/packages/suite/src/hooks/wallet/__fixtures__/useRbfForm.ts @@ -1,3 +1,10 @@ +import { + ChainedTransactions, + WalletAccountTransaction, + WalletAccountTransactionWithRequiredRbfParams, +} from '@suite-common/wallet-types'; +import { AccountUtxo } from '@trezor/connect'; + export { getRootReducer } from './useSendForm'; const ABCD = 'abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd'; @@ -71,7 +78,41 @@ const BTC_CJ_ACCOUNT = { // // script_type: 'PAYTOADDRESS', // }, -const PREPARE_TX = (params = {}) => ({ +const txDummyData = { + deviceState: 'A@B:1', + descriptor: '', + type: 'sent', + txid: '', + amount: '', + fee: '', + targets: [], + tokens: [], + internalTransfers: [], + details: { + vin: [], + vout: [], + size: 0, + totalInput: '', + totalOutput: '', + }, +} satisfies Partial; + +// This type-magic here is for 2 reasons: +// +// 1. WalletAccountTransaction has rbfParams as optional, but we want to +// enforce it in this fixture as we test only this case here +// +// 2. We need to add `required` into AccountUtxo because this is then passed +// down into utxo-lib where it is present on ComposeInput. This is not +// ideal and pretty magic, maybe subject of future refactor. +// +type HackedTxType = WalletAccountTransactionWithRequiredRbfParams & { + rbfParams: WalletAccountTransactionWithRequiredRbfParams['rbfParams'] & { + utxo: Array; + }; +}; + +const PREPARE_TX = (params: Partial = {}): HackedTxType => ({ symbol: 'btc', rbfParams: { txid: 'ABCD', @@ -110,9 +151,21 @@ const PREPARE_TX = (params = {}) => ({ baseFee: 175, ...params, }, + ...txDummyData, }); -export const composeAndSign = [ +type ComposeAndSignFixture = { + description: string; + store: any; + tx: WalletAccountTransactionWithRequiredRbfParams; + composedLevels: any; + composeTransactionCalls: number; + chainedTxs?: ChainedTransactions; + signedTx?: any; + decreasedOutputs?: boolean | string; +}; + +export const composeAndSign: ComposeAndSignFixture[] = [ { description: 'change-output reduced by fee. outputs order not affected. change was at the end of original tx.', @@ -152,7 +205,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { outputs: [ { @@ -178,10 +231,10 @@ export const composeAndSign = [ }, }, chainedTxs: { - own: [{ txid: 'aaaa', fee: '500' }], + own: [{ symbol: 'btc', ...txDummyData, txid: 'aaaa', fee: '500' }], others: [ - { txid: 'bbbb', fee: '500' }, - { txid: 'cccc', fee: '5000' }, + { symbol: 'btc', ...txDummyData, txid: 'bbbb', fee: '500' }, + { symbol: 'btc', ...txDummyData, txid: 'cccc', fee: '5000' }, ], }, tx: PREPARE_TX({ @@ -207,7 +260,7 @@ export const composeAndSign = [ feePerByte: '34.34', // 3.79 (old) + 4 (new) + 26.55 for chainedTxs }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { outputs: [ { @@ -218,7 +271,7 @@ export const composeAndSign = [ }, { address_n: [2147483692, 2147483648, 2147483648, 1, 0], - amount: '9239', + amount: '3239', orig_index: 1, orig_hash: 'ABCD', }, @@ -249,7 +302,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { outputs: [ { @@ -304,7 +357,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { outputs: [ // change-output is gone @@ -358,7 +411,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 4, // 1. normal fee, 2. custom fee + composeTransactionCalls: 2, // 1. normal fee, 2. custom fee signedTx: { outputs: [ // change-output is gone @@ -432,7 +485,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { outputs: [ { @@ -503,7 +556,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 4, // 1. normal fee, 2. custom fee + composeTransactionCalls: 2, // 1. normal fee, 2. custom fee signedTx: { inputs: [{ prev_hash: DCBA }, { prev_hash: ABCD }], outputs: [ @@ -578,7 +631,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 4, // 1. normal fee, 2. custom fee + composeTransactionCalls: 2, // 1. normal fee, 2. custom fee signedTx: { inputs: [{ prev_hash: DCBA }, { prev_hash: ABCD }], outputs: [ @@ -648,7 +701,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 6, // 1. normal fee, 2. custom fee, 3. send-max + composeTransactionCalls: 3, // 1. normal fee, 2. custom fee, 3. send-max decreasedOutputs: true, signedTx: { inputs: [{ prev_hash: DCBA }], @@ -695,7 +748,7 @@ export const composeAndSign = [ feeRate: '1.37', changeAddress: undefined, }), - composeTransactionCalls: 2, // 1. immediate send-max + composeTransactionCalls: 1, // 1. immediate send-max decreasedOutputs: true, composedLevels: { normal: { @@ -754,7 +807,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 6, // 1. normal fee, 2. custom fee, 3 send-max + composeTransactionCalls: 3, // 1. normal fee, 2. custom fee, 3 send-max decreasedOutputs: true, signedTx: { inputs: [{ prev_hash: DCBA }], @@ -816,7 +869,7 @@ export const composeAndSign = [ ], }, }, - composeTransactionCalls: 2, + composeTransactionCalls: 1, signedTx: { // outputs are restored outputs: [ @@ -878,7 +931,7 @@ export const composeAndSign = [ error: 'NOT-ENOUGH-FUNDS', }, }, - composeTransactionCalls: 8, // 1. normal fee, 2. custom fee, 3. send-max normal fee, 4. send-max custom fee + composeTransactionCalls: 4, // 1. normal fee, 2. custom fee, 3. send-max normal fee, 4. send-max custom fee // tx is not signed }, { @@ -943,7 +996,7 @@ export const composeAndSign = [ feePerByte: '15.33', // 11.33 (old) + 4 (new) }, }, - composeTransactionCalls: 2, // 1. immediate send-max + composeTransactionCalls: 1, // 1. immediate send-max decreasedOutputs: 'TR_NOT_ENOUGH_ANONYMIZED_FUNDS_RBF_WARNING', signedTx: { inputs: [{ prev_hash: DCBA }], @@ -1029,7 +1082,7 @@ export const composeAndSign = [ feePerByte: '15.33', // 11.33 (old) + 4 (new) }, }, - composeTransactionCalls: 2, // 1. immediate send-max + composeTransactionCalls: 1, // 1. immediate send-max decreasedOutputs: 'TR_UTXO_REGISTERED_IN_COINJOIN_RBF_WARNING', signedTx: { inputs: [{ prev_hash: DCBA }], diff --git a/packages/suite/src/hooks/wallet/__tests__/useRbfForm.test.tsx b/packages/suite/src/hooks/wallet/__tests__/useRbfForm.test.tsx index 657595f8bb9a..3f5661de2ab6 100644 --- a/packages/suite/src/hooks/wallet/__tests__/useRbfForm.test.tsx +++ b/packages/suite/src/hooks/wallet/__tests__/useRbfForm.test.tsx @@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'; import TrezorConnect from '@trezor/connect'; import { configureMockStore, initPreloadedState } from '@suite-common/test-utils'; -import { SelectedAccountLoaded, RbfTransactionParams } from '@suite-common/wallet-types'; +import { ServerInfo } from '@trezor/blockchain-link-types'; import { renderWithProviders, @@ -14,7 +14,7 @@ import { ChangeFee } from 'src/components/suite/modals/ReduxModal/UserContextMod import { ReplaceTxButton } from 'src/components/suite/modals/ReduxModal/UserContextModal/TxDetailModal/ChangeFee/ReplaceTxButton'; import * as fixtures from '../__fixtures__/useRbfForm'; -import { useRbfContext } from '../useRbfForm'; +import { RbfContext, useRbf, useRbfContext } from '../useRbfForm'; // do not mock jest.unmock('@trezor/connect'); @@ -39,33 +39,43 @@ jest.mock('@trezor/blockchain-link', () => ({ default: class BlockchainLink { name = 'jest-mocked-module'; listeners: Record void> = {}; + constructor(args: any) { this.name = args.name; } + on(...args: any[]) { const [type, fn] = args; this.listeners[type] = fn; } + listenerCount() { return 0; } + connect() { return true; } + disconnect() {} + removeAllListeners() {} + dispose() {} - getInfo() { + getInfo(): ServerInfo { return { - url: this, + url: this.name, name: this.name, shortcut: this.name, + network: this.name, version: '0.0.0', decimals: 0, blockHeight: 10000000, blockHash: 'abcd', + testnet: false, }; } + estimateFee(params: { blocks: number[] }) { return params.blocks.map(() => ({ feePerUnit: '-1' })); } @@ -73,6 +83,7 @@ jest.mock('@trezor/blockchain-link', () => ({ })); type RootReducerState = ReturnType>; + interface Args { send?: Partial; fees?: any; @@ -97,6 +108,7 @@ const initStore = ({ send, fees, selectedAccount, coinjoin }: Args = {}) => { interface TestCallback { getContextValues?: () => any; } + // component rendered inside of SendIndex // callback prop is an object passed from single test case // getContextValues returns actual state of SendFormContext @@ -130,17 +142,25 @@ describe('useRbfForm hook', () => { it(`composeAndSign: ${f.description}`, async () => { const store = initStore(f.store); const callback: TestCallback = {}; - const { unmount } = renderWithProviders( - store, - // @ts-expect-error f.tx is not exact - {}}> - - - , - ); + + const TestComponent = () => { + const contextValues = useRbf({ + rbfParams: f.tx.rbfParams, + chainedTxs: f.chainedTxs, + selectedAccount: f.store.selectedAccount, + }); + + return ( + + {}}> + + + + + ); + }; + + const { unmount } = renderWithProviders(store, ); const composeTransactionSpy = jest.spyOn(TrezorConnect, 'composeTransaction'); @@ -165,13 +185,11 @@ describe('useRbfForm hook', () => { expect(composedLevels).toMatchObject(f.composedLevels); // validate number of calls to '@trezor/connect' - if (typeof f.composeTransactionCalls === 'number') { - expect(composeTransactionSpy).toHaveBeenCalledTimes(f.composeTransactionCalls); - } + expect(composeTransactionSpy).toHaveBeenCalledTimes(f.composeTransactionCalls); - if (f.decreasedOutputs) { + if (f.decreasedOutputs !== undefined) { if (typeof f.decreasedOutputs === 'string') { - expect(() => screen.getByText(f.decreasedOutputs)).not.toThrow(); + expect(() => screen.getByText(f.decreasedOutputs as string)).not.toThrow(); } else { expect(() => findByTestId('@send/decreased-outputs')).not.toThrow(); } diff --git a/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketBuildAccountGroups.ts b/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketBuildAccountGroups.ts index 4651eb9e9abe..7547d7deca17 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketBuildAccountGroups.ts +++ b/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketBuildAccountGroups.ts @@ -18,7 +18,7 @@ export const useCoinmarketBuildAccountGroups = ( const accountLabels = useSelector(selectAccountLabels); const device = useSelector(selectSelectedDevice); const { getDefaultAccountLabel } = useDefaultAccountLabel(); - const { tokenDefinitions } = useSelector(state => state); + const tokenDefinitions = useSelector(state => state.tokenDefinitions); const supportedSymbols = useSelector(selectSupportedSymbols(type)); const groups = useMemo( diff --git a/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketComposeTransaction.ts b/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketComposeTransaction.ts index 739fc5f67b2b..9bd6ef93f4da 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketComposeTransaction.ts +++ b/packages/suite/src/hooks/wallet/coinmarket/form/common/useCoinmarketComposeTransaction.ts @@ -4,7 +4,7 @@ import { UseFormReturn } from 'react-hook-form'; import { COMPOSE_ERROR_TYPES } from '@suite-common/wallet-constants'; import { selectAccounts, selectSelectedDevice } from '@suite-common/wallet-core'; import { AddressDisplayOptions } from '@suite-common/wallet-types'; -import { getFeeLevels } from '@suite-common/wallet-utils'; +import { getFeeInfo } from '@suite-common/wallet-utils'; import { saveComposedTransactionInfo } from 'src/actions/wallet/coinmarket/coinmarketCommonActions'; import { FORM_OUTPUT_ADDRESS, FORM_OUTPUT_AMOUNT } from 'src/constants/wallet/coinmarket/form'; @@ -41,9 +41,14 @@ export const useCoinmarketComposeTransaction = ; const chunkify = addressDisplayType === AddressDisplayOptions.CHUNKED; const { symbol, networkType } = account; - const coinFees = fees[symbol]; - const levels = getFeeLevels(networkType, coinFees); - const feeInfo = useMemo(() => ({ ...coinFees, levels }), [coinFees, levels]); + const feeInfo = useMemo( + () => + getFeeInfo({ + networkType, + feeInfo: fees[symbol], + }), + [networkType, symbol, fees], + ); const initState = useMemo(() => ({ account, network, feeInfo }), [account, network, feeInfo]); const outputAddress = values?.outputs?.[0].address; const [state, setState] = useState(initState); diff --git a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketExchangeForm.ts b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketExchangeForm.ts index fdce997a18a6..d09774f84978 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketExchangeForm.ts +++ b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketExchangeForm.ts @@ -554,7 +554,6 @@ export const useCoinmarketExchangeForm = ({ address: sendAddress, amount: sendStringAmount, destinationTag: sendPaymentExtraId, - setMaxOutputId: values.setMaxOutputId, }); // in case of not success, recomposeAndSign shows notification if (result?.success) { diff --git a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketSellForm.ts b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketSellForm.ts index 008fdc1a0646..7a55e3870092 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketSellForm.ts +++ b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketSellForm.ts @@ -498,7 +498,6 @@ export const useCoinmarketSellForm = ({ address: destinationAddress, amount: cryptoStringAmount, destinationTag: destinationPaymentExtraId, - setMaxOutputId: values.setMaxOutputId, }); if (result?.success) { // send txid to the server as confirmation diff --git a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketVerifyAccount.tsx b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketVerifyAccount.tsx index 9c64ece393a9..e2893f080f55 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketVerifyAccount.tsx +++ b/packages/suite/src/hooks/wallet/coinmarket/form/useCoinmarketVerifyAccount.tsx @@ -1,7 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { Network, networksCollection } from '@suite-common/wallet-config'; import { selectSelectedDevice } from '@suite-common/wallet-core'; import { Account } from '@suite-common/wallet-types'; import { TrezorDevice } from '@suite-common/suite-types'; @@ -17,7 +16,6 @@ import { } from 'src/utils/wallet/coinmarket/coinmarketUtils'; import { CoinmarketAccountType, - CoinmarketGetSuiteReceiveAccountsProps, CoinmarketGetTranslationIdsProps, CoinmarketVerifyAccountProps, CoinmarketVerifyAccountReturnProps, @@ -25,22 +23,22 @@ import { CoinmarketVerifyFormProps, } from 'src/types/coinmarket/coinmarketVerify'; import { useAccountAddressDictionary } from 'src/hooks/wallet/useAccounts'; +import { useNetworkSupport } from 'src/hooks/settings/useNetworkSupport'; const getSelectAccountOptions = ( suiteReceiveAccounts: Account[] | undefined, device: TrezorDevice | undefined, + isSupportedNetwork: boolean, ): CoinmarketVerifyFormAccountOptionProps[] => { const selectAccountOptions: CoinmarketVerifyFormAccountOptionProps[] = []; - if (suiteReceiveAccounts) { - suiteReceiveAccounts.forEach(account => { - selectAccountOptions.push({ type: 'SUITE', account }); - }); + suiteReceiveAccounts?.forEach(account => { + selectAccountOptions.push({ type: 'SUITE', account }); + }); - // have to be signed by private key - if (device?.connected) { - selectAccountOptions.push({ type: 'ADD_SUITE' }); - } + // have to be signed by private key + if (device?.connected && isSupportedNetwork) { + selectAccountOptions.push({ type: 'ADD_SUITE' }); } selectAccountOptions.push({ type: 'NON_SUITE' }); @@ -64,38 +62,8 @@ const getTranslationIds = ( }; }; -const getSuiteReceiveAccounts = ({ - currency, - device, - symbol, - isDebug, - accounts, -}: CoinmarketGetSuiteReceiveAccountsProps): Account[] | undefined => { - if (currency) { - const unavailableCapabilities = device?.unavailableCapabilities ?? {}; - - // Is the symbol supported by the suite and the device natively? - const receiveNetworks = networksCollection.filter( - (n: Network) => - n.symbol === symbol && - !unavailableCapabilities[n.symbol] && - ((n.isDebugOnlyNetwork && isDebug) || !n.isDebugOnlyNetwork), - ); - - return filterReceiveAccounts({ - accounts, - deviceState: device?.state?.staticSessionId, - symbol, - isDebug, - receiveNetworks, - }); - } - - return undefined; -}; - const useCoinmarketVerifyAccount = ({ - currency, + cryptoId, }: CoinmarketVerifyAccountProps): CoinmarketVerifyAccountReturnProps => { const selectedAccount = useSelector(state => state.wallet.selectedAccount); const accounts = useSelector(state => state.wallet.accounts); @@ -104,6 +72,8 @@ const useCoinmarketVerifyAccount = ({ const dispatch = useDispatch(); const [isMenuOpen, setIsMenuOpen] = useState(undefined); + const { supportedMainnets, supportedTestnets } = useNetworkSupport(); + const methods = useForm({ mode: 'onChange', }); @@ -112,23 +82,29 @@ const useCoinmarketVerifyAccount = ({ CoinmarketVerifyFormAccountOptionProps | undefined >(); - const networkId = currency && parseCryptoId(currency).networkId; - const symbol = currency && cryptoIdToSymbol(currency); - const suiteReceiveAccounts = useMemo( - () => - getSuiteReceiveAccounts({ - currency, - device, + const networkId = cryptoId && parseCryptoId(cryptoId).networkId; + const symbol = cryptoId && cryptoIdToSymbol(cryptoId); + + const isSupportedNetwork = [...supportedMainnets, ...supportedTestnets].some( + network => network.symbol === symbol, + ); + + const suiteReceiveAccounts = useMemo(() => { + if (cryptoId) { + return filterReceiveAccounts({ + accounts, + deviceState: device?.state?.staticSessionId, symbol, isDebug, - accounts, - }), - [accounts, currency, device, isDebug, symbol], - ); + }); + } + + return undefined; + }, [accounts, cryptoId, device, isDebug, symbol]); const selectAccountOptions = useMemo( - () => getSelectAccountOptions(suiteReceiveAccounts, device), - [device, suiteReceiveAccounts], + () => getSelectAccountOptions(suiteReceiveAccounts, device, isSupportedNetwork), + [device, suiteReceiveAccounts, isSupportedNetwork], ); const preselectedAccount = useMemo( () => @@ -156,7 +132,6 @@ const useCoinmarketVerifyAccount = ({ const onChangeAccount = (account: CoinmarketVerifyFormAccountOptionProps) => { if (account.type === 'ADD_SUITE' && device) { - setIsMenuOpen(true); dispatch( openModal({ type: 'add-account', @@ -181,7 +156,7 @@ const useCoinmarketVerifyAccount = ({ selectAccountOption(preselectedAccount); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [accounts]); useEffect(() => { methods.trigger(); diff --git a/packages/suite/src/hooks/wallet/coinmarket/useCoinmarketInfo.ts b/packages/suite/src/hooks/wallet/coinmarket/useCoinmarketInfo.ts index d31634b591cd..c7eff422699d 100644 --- a/packages/suite/src/hooks/wallet/coinmarket/useCoinmarketInfo.ts +++ b/packages/suite/src/hooks/wallet/coinmarket/useCoinmarketInfo.ts @@ -2,7 +2,11 @@ import { useCallback } from 'react'; import { CoinInfo, CryptoId } from 'invity-api'; -import { getNetwork, getNetworkByCoingeckoNativeId } from '@suite-common/wallet-config'; +import { + getNetwork, + getNetworkByCoingeckoNativeId, + isNetworkSymbol, +} from '@suite-common/wallet-config'; import addressValidator from '@trezor/address-validator'; import { useSelector } from 'src/hooks/suite/useSelector'; @@ -17,19 +21,26 @@ const supportedAddressValidatorSymbols = new Set( addressValidator.getCurrencies().map(c => c.symbol), ); -function toCryptoOption(cryptoId: CryptoId, coinInfo: CoinInfo): CoinmarketCryptoSelectItemProps { +const toCryptoOption = ( + cryptoId: CryptoId, + coinInfo: CoinInfo, +): CoinmarketCryptoSelectItemProps => { const { networkId, contractAddress } = parseCryptoId(cryptoId); + const coinInfoSymbol = coinInfo.symbol.toLowerCase(); + const displaySymbol = isNetworkSymbol(coinInfoSymbol) + ? getNetwork(coinInfoSymbol)?.displaySymbol + : coinInfoSymbol.toUpperCase(); return { type: 'currency', value: cryptoId, - label: coinInfo.symbol.toUpperCase(), + label: displaySymbol, cryptoName: coinInfo.name, coingeckoId: networkId, contractAddress: contractAddress || null, symbol: coinInfo.symbol, }; -} +}; const sortPopularCurrencies = ( a: CoinmarketCryptoSelectItemProps, diff --git a/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts b/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts index acc2940e200d..ddd7681dd8ea 100644 --- a/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts +++ b/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts @@ -1,9 +1,13 @@ import { useEffect, useMemo } from 'react'; import { UseFormReturn } from 'react-hook-form'; -import { ExcludedUtxos, FormState } from '@suite-common/wallet-types'; +import { ExcludedUtxos, FormState, UtxoSorting } from '@suite-common/wallet-types'; import type { AccountUtxo, PROTO } from '@trezor/connect'; import { getUtxoOutpoint, isSameUtxo } from '@suite-common/wallet-utils'; +import { selectAccountTransactionsWithNulls } from '@suite-common/wallet-core'; + +import { useSelector } from 'src/hooks/suite'; +import { sortUtxos } from 'src/utils/wallet/utxoSortingUtils'; import { useCoinjoinRegisteredUtxos } from './useCoinjoinRegisteredUtxos'; import { @@ -28,19 +32,26 @@ export const useUtxoSelection = ({ setValue, watch, }: UtxoSelectionContextProps): UtxoSelectionContext => { + const accountTransactions = useSelector(state => + selectAccountTransactionsWithNulls(state, account.key), + ); + // register custom form field (without HTMLElement) useEffect(() => { register('isCoinControlEnabled'); register('selectedUtxos'); register('anonymityWarningChecked'); + register('utxoSorting'); }, [register]); const coinjoinRegisteredUtxos = useCoinjoinRegisteredUtxos({ account }); - // has coin control been enabled manually? - const isCoinControlEnabled = watch('isCoinControlEnabled'); - // fee level - const selectedFee = watch('selectedFee'); + const [isCoinControlEnabled, options, selectedFee, utxoSorting] = watch([ + 'isCoinControlEnabled', + 'options', + 'selectedFee', + 'utxoSorting', + ]); // confirmation of spending low-anonymity UTXOs - only relevant for coinjoin account const anonymityWarningChecked = !!watch('anonymityWarningChecked'); // manually selected UTXOs @@ -79,20 +90,29 @@ export const useUtxoSelection = ({ const spendableUtxos: AccountUtxo[] = []; const lowAnonymityUtxos: AccountUtxo[] = []; const dustUtxos: AccountUtxo[] = []; - account?.utxo?.forEach(utxo => { - switch (excludedUtxos[getUtxoOutpoint(utxo)]) { - case 'low-anonymity': - lowAnonymityUtxos.push(utxo); - - return; - case 'dust': - dustUtxos.push(utxo); - - return; - default: - spendableUtxos.push(utxo); - } - }); + + // Skip sorting and categorizing UTXOs if coin control is not enabled. + const utxos = + options?.includes('utxoSelection') && account?.utxo + ? sortUtxos(account?.utxo, utxoSorting, accountTransactions) + : account?.utxo; + + if (utxos?.length) { + utxos?.forEach(utxo => { + switch (excludedUtxos[getUtxoOutpoint(utxo)]) { + case 'low-anonymity': + lowAnonymityUtxos.push(utxo); + + return; + case 'dust': + dustUtxos.push(utxo); + + return; + default: + spendableUtxos.push(utxo); + } + }); + } // category displayed on top and controlled by the check-all checkbox const topCategory = @@ -139,6 +159,8 @@ export const useUtxoSelection = ({ setValue('anonymityWarningChecked', false); } + const selectUtxoSorting = (sorting: UtxoSorting) => setValue('utxoSorting', sorting); + const toggleAnonymityWarning = () => setValue('anonymityWarningChecked', !anonymityWarningChecked); @@ -204,6 +226,8 @@ export const useUtxoSelection = ({ selectedUtxos, spendableUtxos, coinjoinRegisteredUtxos, + utxoSorting, + selectUtxoSorting, toggleAnonymityWarning, toggleCheckAllUtxos, toggleCoinControl, diff --git a/packages/suite/src/hooks/wallet/useClaimEthForm.ts b/packages/suite/src/hooks/wallet/useClaimEthForm.ts index 646afd342927..a494ab82404e 100644 --- a/packages/suite/src/hooks/wallet/useClaimEthForm.ts +++ b/packages/suite/src/hooks/wallet/useClaimEthForm.ts @@ -1,16 +1,17 @@ import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; - -import { getFeeLevels } from '@suite-common/wallet-utils'; +import { getFeeInfo } from '@suite-common/wallet-utils'; import { PrecomposedTransactionFinal } from '@suite-common/wallet-types'; import { useDispatch, useSelector } from 'src/hooks/suite'; import { CRYPTO_INPUT, OUTPUT_AMOUNT, UseStakeFormsProps } from 'src/types/wallet/stakeForms'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; -import { getEthNetworkForWalletSdk, getStakeFormsDefaultValues } from 'src/utils/suite/stake'; +import { + getEthNetworkAddresses, + getStakeFormsDefaultValues, +} from 'src/utils/suite/ethereumStaking'; import { ClaimContextValues, ClaimFormState } from 'src/types/wallet/claimForm'; import { useFees } from './form/useFees'; @@ -28,21 +29,21 @@ export const useClaimEthForm = ({ selectedAccount }: UseStakeFormsProps): ClaimC const symbolFees = useSelector(state => state.wallet.fees[account.symbol]); const defaultValues = useMemo(() => { - const { address_accounting: accountingAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + const { addressContractAccounting } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: accountingAddress, - ethereumStakeType: 'claim', + address: addressContractAccounting, + stakeType: 'claim', }), } as ClaimFormState; }, [account.symbol]); const state = useMemo(() => { - const levels = getFeeLevels(account.networkType, symbolFees); - const feeInfo = { ...symbolFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: symbolFees, + }); return { account, @@ -102,9 +103,10 @@ export const useClaimEthForm = ({ selectedAccount }: UseStakeFormsProps): ClaimC // sub-hook, FeeLevels handler const fees = useSelector(state => state.wallet.fees); - const coinFees = fees[account.symbol]; - const levels = getFeeLevels(account.networkType, coinFees); - const feeInfo = { ...coinFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: fees[account.symbol], + }); const { changeFeeLevel, selectedFee: _selectedFee } = useFees({ defaultValue: 'normal', feeInfo, diff --git a/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts b/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts index 0c6980c23a16..7c1886b686ee 100644 --- a/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts +++ b/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import { notificationsActions } from '@suite-common/toast-notifications'; import { DEFAULT_VALUES, DEFAULT_PAYMENT } from '@suite-common/wallet-constants'; import { FormState } from '@suite-common/wallet-types'; -import { getFeeLevels } from '@suite-common/wallet-utils'; +import { getFeeInfo } from '@suite-common/wallet-utils'; import { networks } from '@suite-common/wallet-config'; import type { Account, FormOptions } from '@suite-common/wallet-types'; import { composeSendFormTransactionFeeLevelsThunk } from '@suite-common/wallet-core'; @@ -80,9 +80,10 @@ export const useCoinmarketRecomposeAndSign = () => { }; // prepare form state for composeAction - const coinFees = fees[account.symbol]; - const levels = getFeeLevels(account.networkType, coinFees); - const feeInfo = { ...coinFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: fees[account.symbol], + }); const composeContext = { account, network, feeInfo }; // recalcCustomLimit is used in case of custom fee level, when we want to keep the feePerUnit defined by the user diff --git a/packages/suite/src/hooks/wallet/useRbfForm.ts b/packages/suite/src/hooks/wallet/useRbfForm.ts index a07120ed6a5a..6c958b7da8c5 100644 --- a/packages/suite/src/hooks/wallet/useRbfForm.ts +++ b/packages/suite/src/hooks/wallet/useRbfForm.ts @@ -1,19 +1,17 @@ import { createContext, useContext, useState, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; -import { fromWei } from 'web3-utils'; - import { BigNumber } from '@trezor/utils/src/bigNumber'; import { DEFAULT_PAYMENT, DEFAULT_OPRETURN, DEFAULT_VALUES } from '@suite-common/wallet-constants'; -import { getFeeLevels } from '@suite-common/wallet-utils'; +import { getFeeInfo } from '@suite-common/wallet-utils'; import { SelectedAccountLoaded, - Account, RbfTransactionParams, ChainedTransactions, FormState, FeeInfo, } from '@suite-common/wallet-types'; +import type { NetworkType } from '@suite-common/wallet-config'; import { useSelector } from 'src/hooks/suite'; import { selectCurrentTargetAnonymity } from 'src/reducers/wallet/coinjoinReducer'; @@ -31,25 +29,33 @@ export type UseRbfProps = { const getBitcoinFeeInfo = (info: FeeInfo, feeRate: string) => { // increase FeeLevels (old rate + defined rate) - const levels = getFeeLevels('bitcoin', info).map(l => ({ - ...l, - feePerUnit: new BigNumber(l.feePerUnit).plus(feeRate).toString(), + const feeInfo = getFeeInfo({ + networkType: 'bitcoin', + feeInfo: info, + }); + const levels = feeInfo.levels.map(level => ({ + ...level, + feePerUnit: new BigNumber(level.feePerUnit).plus(feeRate).toString(), })); return { - ...info, + ...feeInfo, levels, - minFee: new BigNumber(feeRate).plus(info.minFee).toNumber(), // increase required minFee rate + minFee: new BigNumber(feeRate).plus(feeInfo.minFee).toNumber(), // increase required minFee rate }; }; const getEthereumFeeInfo = (info: FeeInfo, gasPrice: string) => { const current = new BigNumber(gasPrice); - const minFeeFromNetwork = new BigNumber(fromWei(info.levels[0].feePerUnit, 'gwei')); + const feeInfo = getFeeInfo({ + networkType: 'ethereum', + feeInfo: info, + }); + const minFeeFromNetwork = new BigNumber(feeInfo.levels[0].feePerUnit); const getFee = () => { - if (minFeeFromNetwork.lte(current.plus(1))) { - return current.plus(1); + if (minFeeFromNetwork.lte(current.plus(feeInfo.minFee))) { + return current.plus(feeInfo.minFee); } return minFeeFromNetwork; @@ -58,20 +64,20 @@ const getEthereumFeeInfo = (info: FeeInfo, gasPrice: string) => { const fee = getFee(); // increase FeeLevel only if it's lower than predefined - const levels = getFeeLevels('ethereum', info).map(l => ({ - ...l, + const levels = feeInfo.levels.map(level => ({ + ...level, feePerUnit: fee.toString(), })); return { - ...info, + ...feeInfo, levels, - minFee: current.plus(1).toNumber(), // increase required minFee rate + minFee: current.plus(feeInfo.minFee).toNumber(), // increase required minFee rate }; }; -const getFeeInfo = ( - networkType: Account['networkType'], +const getRbfFeeInfo = ( + networkType: NetworkType, info: FeeInfo, rbfParams: RbfTransactionParams, ) => { @@ -91,7 +97,7 @@ const useRbfState = ({ selectedAccount, rbfParams, chainedTxs }: UseRbfProps) => const { shouldSendInSats } = useBitcoinAmountUnit(account.symbol); return useMemo(() => { - const feeInfo = getFeeInfo(account.networkType, symbolFees, rbfParams); + const feeInfo = getRbfFeeInfo(account.networkType, symbolFees, rbfParams); // filter out utxos generated by this transaction const otherUtxo = (account.utxo || []).filter(input => input.txid !== rbfParams.txid); // filter out utxos with anonymity level below target and currently registered diff --git a/packages/suite/src/hooks/wallet/useSendForm.ts b/packages/suite/src/hooks/wallet/useSendForm.ts index 3f386dc6b3dd..6bfd9f8db6dc 100644 --- a/packages/suite/src/hooks/wallet/useSendForm.ts +++ b/packages/suite/src/hooks/wallet/useSendForm.ts @@ -4,10 +4,10 @@ import { useForm, useFieldArray } from 'react-hook-form'; import { useDidUpdate } from '@trezor/react-utils'; import { FormState } from '@suite-common/wallet-types'; import { - getFeeLevels, getDefaultValues, amountToSmallestUnit, formatAmount, + getFeeInfo, } from '@suite-common/wallet-utils'; import { getNetworkSymbolForProtocol } from '@suite-common/suite-utils'; import { selectCurrentFiatRates } from '@suite-common/wallet-core'; @@ -62,9 +62,10 @@ export interface UseSendFormProps extends SendFormProps { const getStateFromProps = (props: UseSendFormProps) => { const { account, network } = props.selectedAccount; const { symbol, networkType } = account; - const coinFees = props.fees[symbol]; - const levels = getFeeLevels(networkType, coinFees); - const feeInfo = { ...coinFees, levels }; + const feeInfo = getFeeInfo({ + networkType, + feeInfo: props.fees[symbol], + }); const currencyCode = props.localCurrency; const localCurrencyOption = { value: currencyCode, @@ -74,7 +75,6 @@ const getStateFromProps = (props: UseSendFormProps) => { return { account, network, - coinFees, feeInfo, localCurrencyOption, online: props.online, @@ -152,7 +152,7 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => { const excludedUtxos = useExcludedUtxos({ account: state.account, - dustLimit: state.coinFees.dustLimit, + dustLimit: state.feeInfo.dustLimit, targetAnonymity: props.targetAnonymity, }); @@ -226,6 +226,10 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => { setState(getStateFromProps(props)); // resetting state will trigger "loadDraft" useEffect block, which will reset FormState to default }, [dispatch, props, setComposedLevels]); + const resetDraft = useCallback(() => { + dispatch(removeSendFormDraftThunk()); + }, [dispatch]); + // declare useSendFormImport, sub-hook of useSendForm const { importTransaction, validateImportedTransaction } = useSendFormImport({ network: state.network, @@ -393,6 +397,7 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => { loadTransaction, signTransaction: sign, setDraftSaveRequest, + resetDraft, utxoSelection, ...sendFormUtils, ...sendFormOutputs, diff --git a/packages/suite/src/hooks/wallet/useStakeEthForm.ts b/packages/suite/src/hooks/wallet/useStakeEthForm.ts index 67315892f970..892f2b268017 100644 --- a/packages/suite/src/hooks/wallet/useStakeEthForm.ts +++ b/packages/suite/src/hooks/wallet/useStakeEthForm.ts @@ -3,12 +3,11 @@ import { useForm, useWatch } from 'react-hook-form'; import useDebounce from 'react-use/lib/useDebounce'; import { fromWei } from 'web3-utils'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { fromFiatCurrency, - getFeeLevels, + getFeeInfo, getFiatRateKey, toFiatCurrency, } from '@suite-common/wallet-utils'; @@ -31,7 +30,10 @@ import { } from 'src/types/wallet/stakeForms'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; -import { getEthNetworkForWalletSdk, getStakeFormsDefaultValues } from 'src/utils/suite/stake'; +import { + getEthNetworkAddresses, + getStakeFormsDefaultValues, +} from 'src/utils/suite/ethereumStaking'; import type { CryptoAmountLimitProps } from 'src/utils/suite/validation'; import { useStakeCompose } from './form/useStakeCompose'; @@ -41,6 +43,7 @@ import { useFees } from './form/useFees'; export const StakeEthFormContext = createContext(null); StakeEthFormContext.displayName = 'StakeEthFormContext'; +// TODO: refactor this hook to support both ethereum and solana export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeContextValues => { const dispatch = useDispatch(); @@ -61,14 +64,13 @@ export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeC }; const defaultValues = useMemo(() => { - const { address_pool: poolAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + // TODO: get the address for solana here + const { addressContractPool } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: poolAddress, - ethereumStakeType: 'stake', + address: addressContractPool, + stakeType: 'stake', }), setMaxOutputId: undefined, } as StakeFormState; @@ -79,8 +81,10 @@ export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeC const isDraft = !!draft; const state = useMemo(() => { - const levels = getFeeLevels(account.networkType, symbolFees); - const feeInfo = { ...symbolFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: symbolFees, + }); return { account, @@ -131,9 +135,10 @@ export const useStakeEthForm = ({ selectedAccount }: UseStakeFormsProps): StakeC // sub-hook, FeeLevels handler const fees = useSelector(state => state.wallet.fees); - const coinFees = fees[account.symbol]; - const levels = getFeeLevels(account.networkType, coinFees); - const feeInfo = { ...coinFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: fees[account.symbol], + }); const { changeFeeLevel, selectedFee: _selectedFee } = useFees({ defaultValue: 'normal', feeInfo, diff --git a/packages/suite/src/hooks/wallet/useTotalFiatBalance.ts b/packages/suite/src/hooks/wallet/useTotalFiatBalance.ts new file mode 100644 index 000000000000..7df87d3fcb66 --- /dev/null +++ b/packages/suite/src/hooks/wallet/useTotalFiatBalance.ts @@ -0,0 +1,32 @@ +import { FiatCurrencyCode } from '@suite-common/suite-config/src/fiat'; +import { Account, RatesByKey } from '@suite-common/wallet-types'; +import { getTotalFiatBalance } from '@suite-common/wallet-utils/src/accountUtils'; + +import { useSelector } from 'src/hooks/suite'; +import { getTokens } from 'src/utils/wallet/tokenUtils'; + +export const useTotalFiatBalance = ( + accounts: Account[], + localCurrency: FiatCurrencyCode, + rates?: RatesByKey, +) => { + const tokenDefinitions = useSelector(state => state.tokenDefinitions); + const deviceAccounts: Account[] = accounts.map(account => { + const coinDefinitions = tokenDefinitions?.[account.symbol]?.coin; + const tokens = getTokens({ + tokens: account.tokens ?? [], + symbol: account.symbol, + tokenDefinitions: coinDefinitions, + }); + + return { ...account, tokens: tokens.shownWithBalance }; + }); + + const totalFiatBalance = getTotalFiatBalance({ + deviceAccounts, + localCurrency, + rates, + }).toString(); + + return totalFiatBalance; +}; diff --git a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts index 67018e01ad4a..3cda72da0aaf 100644 --- a/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts +++ b/packages/suite/src/hooks/wallet/useUnstakeEthForm.ts @@ -2,12 +2,11 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState } import { useForm, useWatch } from 'react-hook-form'; import useDebounce from 'react-use/lib/useDebounce'; -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; import { fromFiatCurrency, getAccountAutocompoundBalance, - getFeeLevels, + getFeeInfo, getFiatRateKey, toFiatCurrency, } from '@suite-common/wallet-utils'; @@ -30,10 +29,10 @@ import { import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { signTransaction } from 'src/actions/wallet/stakeActions'; import { - getEthNetworkForWalletSdk, + getEthNetworkAddresses, getStakeFormsDefaultValues, simulateUnstake, -} from 'src/utils/suite/stake'; +} from 'src/utils/suite/ethereumStaking'; import type { AmountLimitProps } from 'src/utils/suite/validation'; import { useStakeCompose } from './form/useStakeCompose'; @@ -78,14 +77,12 @@ export const useUnstakeEthForm = ({ }; const defaultValues = useMemo(() => { - const { address_pool: poolAddress } = selectNetwork( - getEthNetworkForWalletSdk(account.symbol), - ); + const { addressContractPool } = getEthNetworkAddresses(account.symbol); return { ...getStakeFormsDefaultValues({ - address: poolAddress, - ethereumStakeType: 'unstake', + address: addressContractPool, + stakeType: 'unstake', amount: autocompoundBalance, }), } as UnstakeFormState; @@ -96,8 +93,10 @@ export const useUnstakeEthForm = ({ const isDraft = !!draft; const state = useMemo(() => { - const levels = getFeeLevels(account.networkType, symbolFees); - const feeInfo = { ...symbolFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: symbolFees, + }); return { account, @@ -171,9 +170,10 @@ export const useUnstakeEthForm = ({ // sub-hook, FeeLevels handler const fees = useSelector(state => state.wallet.fees); - const coinFees = fees[account.symbol]; - const levels = getFeeLevels(account.networkType, coinFees); - const feeInfo = { ...coinFees, levels }; + const feeInfo = getFeeInfo({ + networkType: account.networkType, + feeInfo: fees[account.symbol], + }); const { changeFeeLevel, selectedFee: _selectedFee } = useFees({ defaultValue: 'normal', feeInfo, diff --git a/packages/suite/src/middlewares/wallet/__fixtures__/walletMiddleware.ts b/packages/suite/src/middlewares/wallet/__fixtures__/walletMiddleware.ts index da041fd9474d..34943b6bfe3f 100644 --- a/packages/suite/src/middlewares/wallet/__fixtures__/walletMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/__fixtures__/walletMiddleware.ts @@ -44,7 +44,7 @@ export const blockchainSubscription = [ coin: 'eth', }, disconnect: { - called: 0, + called: 1, }, }, }, @@ -62,7 +62,7 @@ export const blockchainSubscription = [ called: 0, }, disconnect: { - called: 1, + called: 2, coin: 'eth', }, }, diff --git a/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts b/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts index 34ceb080ce8f..d537d18ee5f5 100644 --- a/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/discoveryMiddleware.ts @@ -1,3 +1,4 @@ +import { connectPopupCallThunk } from '@trezor/suite-desktop-connect-popup'; import { authorizeDeviceThunk, deviceActions, @@ -15,7 +16,6 @@ import { UI } from '@trezor/connect'; import { isDeviceAcquired } from '@suite-common/suite-utils'; import { DiscoveryStatus } from '@suite-common/wallet-constants'; import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils'; -import { connectPopupCallThunk } from '@suite-common/connect-init'; import { SUITE, ROUTER, MODAL } from 'src/actions/suite/constants'; import * as walletSettingsActions from 'src/actions/settings/walletSettingsActions'; diff --git a/packages/suite/src/reducers/store.ts b/packages/suite/src/reducers/store.ts index 27a652447bb8..753ea90be5d9 100644 --- a/packages/suite/src/reducers/store.ts +++ b/packages/suite/src/reducers/store.ts @@ -9,6 +9,7 @@ import { isCodesignBuild } from '@trezor/env-utils'; import { mergeDeepObject } from '@trezor/utils'; import { prepareTokenDefinitionsReducer } from '@suite-common/token-definitions'; import { prepareFirmwareReducer } from '@suite-common/firmware'; +import { accountsActions } from '@suite-common/wallet-core'; import suiteMiddlewares from 'src/middlewares/suite'; import walletMiddlewares from 'src/middlewares/wallet'; @@ -54,7 +55,7 @@ const middleware = [ ...recoveryMiddlewares, ]; -const excludedActions = [addLog.type]; +const excludedActions = [addLog.type, accountsActions.updateAccountRefreshTimestamp.type]; if (!isCodesignBuild()) { const excludeLogger = (_getState: any, action: any): boolean => diff --git a/packages/suite/src/storage/CHANGELOG.md b/packages/suite/src/storage/CHANGELOG.md index 6780a9991308..90027c4218d7 100644 --- a/packages/suite/src/storage/CHANGELOG.md +++ b/packages/suite/src/storage/CHANGELOG.md @@ -1,5 +1,9 @@ # Storage changelog +## 51 + +- Changed `metadata.key` on non-eth EVM networks accounts to be `descriptor-chainId` + ## 49 - networks now have same order everywhere diff --git a/packages/suite/src/storage/index.ts b/packages/suite/src/storage/index.ts index 13d955302360..8db8d304aa02 100644 --- a/packages/suite/src/storage/index.ts +++ b/packages/suite/src/storage/index.ts @@ -5,7 +5,7 @@ import { reloadApp } from 'src/utils/suite/reload'; import { migrate } from './migrations'; import type { SuiteDBSchema } from './definitions'; -const VERSION = 49; // don't forget to add migration and CHANGELOG when changing versions! +const VERSION = 51; // don't forget to add migration and CHANGELOG when changing versions! /** * If the object stores don't already exist then creates them. diff --git a/packages/suite/src/storage/migrations/index.ts b/packages/suite/src/storage/migrations/index.ts index c551d1b16a14..66c21faeb98a 100644 --- a/packages/suite/src/storage/migrations/index.ts +++ b/packages/suite/src/storage/migrations/index.ts @@ -3,6 +3,7 @@ import { toWei } from 'web3-utils'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { isDesktop } from '@trezor/env-utils'; import { + getNetwork, isNetworkSymbol, type NetworkSymbol, networkSymbolCollection, @@ -20,6 +21,7 @@ import { PartialRecord } from '@trezor/type-utils'; import type { CustomBackend, BlockbookUrl } from 'src/types/wallet/backend'; import type { State } from 'src/reducers/wallet/settingsReducer'; +import { migrationOfBnbNetwork } from 'src/storage/migrations/networks/bnb'; import { updateAll } from './utils'; import type { DBWalletAccountTransaction, SuiteDBSchema } from '../definitions'; @@ -1151,6 +1153,10 @@ export const migrate: OnUpgradeFunc = async ( } if (oldVersion < 50) { + await migrationOfBnbNetwork(db, oldVersion, newVersion, transaction); + } + + if (oldVersion < 51) { await updateAll(transaction, 'accounts', account => { if (account.networkType === 'cardano') { account.misc.staking.drep = null; @@ -1158,5 +1164,16 @@ export const migrate: OnUpgradeFunc = async ( return account; } }); + + await updateAll(transaction, 'accounts', account => { + if (account.networkType === 'ethereum' && account.symbol !== 'eth') { + const { chainId } = getNetwork(account.symbol); + account.metadata.key = `${account.descriptor}-${chainId}`; + + return account; + } + + return account; + }); } }; diff --git a/packages/suite/src/storage/migrations/networks/bnb.ts b/packages/suite/src/storage/migrations/networks/bnb.ts new file mode 100644 index 000000000000..c3d39615dfbe --- /dev/null +++ b/packages/suite/src/storage/migrations/networks/bnb.ts @@ -0,0 +1,201 @@ +import type { OnUpgradeFunc } from '@trezor/suite-storage'; + +import { updateAll } from '../utils'; +import type { SuiteDBSchema } from '../../definitions'; + +export const migrationOfBnbNetwork: OnUpgradeFunc = async ( + _db, + _oldVersion, + _newVersion, + transaction, +) => { + // migrate bnb to bsc + + await updateAll(transaction, 'walletSettings', walletSettings => { + // @ts-expect-error + const indexOfBnb = walletSettings.enabledNetworks.indexOf('bnb'); + if (indexOfBnb !== -1) { + walletSettings.enabledNetworks[indexOfBnb] = 'bsc'; + } + + return walletSettings; + }); + + await updateAll(transaction, 'suiteSettings', suiteSettings => { + if ( + // @ts-expect-error + typeof suiteSettings.evmSettings?.confirmExplanationModalClosed?.bnb == 'boolean' + ) { + suiteSettings.evmSettings.confirmExplanationModalClosed.bsc = + // @ts-expect-error + suiteSettings.evmSettings.confirmExplanationModalClosed.bnb; + // @ts-expect-error + delete suiteSettings.evmSettings.confirmExplanationModalClosed.bnb; + } + + if ( + // @ts-expect-error + typeof suiteSettings.evmSettings?.explanationBannerClosed?.bnb == 'boolean' + ) { + suiteSettings.evmSettings.explanationBannerClosed.bsc = + // @ts-expect-error + suiteSettings.evmSettings.explanationBannerClosed.bnb; + // @ts-expect-error + delete suiteSettings.evmSettings.explanationBannerClosed.bnb; + } + + return suiteSettings; + }); + + const backendSettings = transaction.objectStore('backendSettings'); + // @ts-expect-error + const bnbBackendSettings = await backendSettings.get('bnb'); + if (bnbBackendSettings) { + backendSettings.add(bnbBackendSettings, 'bsc'); + // @ts-expect-error + backendSettings.delete('bnb'); + } + + const tokenManagement = transaction.objectStore('tokenManagement'); + const bnbTokenManagementShow = await tokenManagement.get('bnb-coin-show'); + if (bnbTokenManagementShow) { + tokenManagement.add(bnbTokenManagementShow, 'bsc-coin-show'); + tokenManagement.delete('bnb-coin-show'); + } + + const bnbTokenManagementHide = await tokenManagement.get('bnb-coin-hide'); + if (bnbTokenManagementHide) { + tokenManagement.add(bnbTokenManagementHide, 'bsc-coin-hide'); + tokenManagement.delete('bnb-coin-hide'); + } + + await updateAll(transaction, 'discovery', discovery => { + discovery.networks = discovery.networks.map(network => + // @ts-expect-error + network === 'bnb' ? 'bsc' : network, + ); + + discovery.failed = discovery.failed.map(network => { + // @ts-expect-error + if (network.symbol === 'bnb') { + network = { ...network, symbol: 'bsc' }; + } + + return network; + }); + + return discovery; + }); + + const accounts = transaction.objectStore('accounts'); + let accountsCursor = await accounts.openCursor(); + while (accountsCursor) { + const account = accountsCursor.value; + // @ts-expect-error + if (account.symbol === 'bnb') { + const newAccount = { + ...account, + symbol: 'bsc' as const, + key: account.key.replace('bnb', 'bsc'), + }; + await accountsCursor.delete(); + await accounts.add(newAccount); + } + + accountsCursor = await accountsCursor.continue(); + } + + await updateAll(transaction, 'walletSettings', walletSettings => { + if (walletSettings.lastUsedFeeLevel['bnb']) { + walletSettings.lastUsedFeeLevel = { + ...walletSettings.lastUsedFeeLevel, + bsc: { ...walletSettings.lastUsedFeeLevel['bnb'] }, + }; + + delete walletSettings.lastUsedFeeLevel['bnb']; + } + + return walletSettings; + }); + + await updateAll(transaction, 'txs', tx => { + // @ts-expect-error + if (tx.tx.symbol === 'bnb') { + tx.tx = { ...tx.tx, symbol: 'bsc' }; + } + + return tx; + }); + + const graphs = transaction.objectStore('graph'); + let graphCursor = await graphs.openCursor(); + while (graphCursor) { + const graph = graphCursor.value; + //@ts-expect-error + if (graph.account.symbol === 'bnb') { + const newGraph = { + ...graph, + account: { ...graph.account, symbol: 'bsc' as const }, + }; + await graphCursor.delete(); + await graphs.add(newGraph); + } + + graphCursor = await graphCursor.continue(); + } + + await updateAll(transaction, 'historicRates', rates => { + const rate = Object.keys(rates).reduce((newRates, key) => { + const newKey = key.replace('bnb', 'bsc'); + // @ts-expect-error + newRates[newKey] = rates[key]; + + return newRates; + }, {}); + + return rate; + }); + + const historicRates = transaction.objectStore('historicRates'); + const historicRatesKeys = await historicRates.getAllKeys(); + const historicRatesKeysWithBnb = historicRatesKeys.filter(key => key.includes('bnb')); + + historicRatesKeysWithBnb.forEach(async key => { + const rate = await historicRates.get(key); + if (rate) { + historicRates.add(rate, key.replace('bnb', 'bsc')); + } + historicRates.delete(key); + }); + + const sendFormDrafts = transaction.objectStore('sendFormDrafts'); + const sendFormDraftsKeys = await sendFormDrafts.getAllKeys(); + const sendFormDraftsKeysWithBnb = sendFormDraftsKeys.filter(key => key.includes('bnb')); + + sendFormDraftsKeysWithBnb.forEach(async key => { + const draft = await sendFormDrafts.get(key); + if (draft) { + sendFormDrafts.add(draft, key.replace('bnb', 'bsc')); + } + sendFormDrafts.delete(key); + }); + + const formDrafts = transaction.objectStore('formDrafts'); + const formDraftsKeys = await formDrafts.getAllKeys(); + const formDraftsKeysWithBnb = formDraftsKeys.filter(key => key.includes('bnb')); + + formDraftsKeysWithBnb.forEach(async key => { + const draft = await formDrafts.get(key); + if (draft) { + formDrafts.add(draft, key.replace('bnb', 'bsc')); + } + formDrafts.delete(key); + }); + + await updateAll(transaction, 'coinmarketTrades', trade => { + // @ts-expect-error + if (trade.account.symbol === 'bnb') { + trade.account.symbol = 'bsc'; + } + }); +}; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 2a1e3b0f0a8d..7441b1597928 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -1425,11 +1425,11 @@ export default defineMessages({ id: 'TR_COPY_TO_CLIPBOARD', }, TR_ADDRESS_MODAL_TITLE: { - defaultMessage: '{networkName} receive address', + defaultMessage: '{networkSymbol} receive address', id: 'TR_ADDRESS_MODAL_TITLE', }, TR_ADDRESS_MODAL_TITLE_EXCHANGE: { - defaultMessage: '{networkCurrencyName} receive address on {networkName} network', + defaultMessage: '{networkCurrencyName} receive address on {networkSymbol} network', id: 'TR_ADDRESS_MODAL_TITLE_EXCHANGE', }, TR_XPUB_MODAL_CLIPBOARD: { @@ -1437,7 +1437,7 @@ export default defineMessages({ id: 'TR_XPUB_MODAL_CLIPBOARD', }, TR_XPUB_MODAL_TITLE: { - defaultMessage: '{networkName} Account {accountIndex} public key (XPUB)', + defaultMessage: '{networkSymbol} Account {accountIndex} public key (XPUB)', id: 'TR_XPUB_MODAL_TITLE', }, TR_XPUB_MODAL_TITLE_METADATA: { @@ -2036,6 +2036,14 @@ export default defineMessages({ defaultMessage: 'Unable to verify address history. Check that the address is correct.', id: 'TR_ETH_ADDRESS_CANT_VERIFY_HISTORY', }, + TR_EVM_ADDRESS_IS_CONTRACT: { + defaultMessage: 'You are sending funds to a contract address.', + id: 'TR_EVM_ADDRESS_IS_CONTRACT', + }, + TR_I_UNDERSTAND_THE_RISK: { + defaultMessage: 'I understand', + id: 'TR_I_UNDERSTAND_THE_RISK', + }, TR_NEEDS_ATTENTION_BOOTLOADER: { defaultMessage: 'Trezor is in Bootloader mode.', id: 'TR_NEEDS_ATTENTION_BOOTLOADER', @@ -2595,6 +2603,14 @@ export default defineMessages({ defaultMessage: 'Unrecognized tokens pose potential risks. Use caution.', id: 'TR_TOKEN_UNRECOGNIZED_BY_TREZOR_TOOLTIP', }, + TR_COLLECTIONS_UNRECOGNIZED_BY_TREZOR: { + defaultMessage: 'Unrecognized collections', + id: 'TR_COLLECTIONS_UNRECOGNIZED_BY_TREZOR', + }, + TR_NFT_UNRECOGNIZED_BY_TREZOR_TOOLTIP: { + defaultMessage: 'Unrecognized NFTs pose potential risks. Use caution.', + id: 'TR_NFT_UNRECOGNIZED_BY_TREZOR_TOOLTIP', + }, TR_LEARN: { defaultMessage: 'Learn', description: 'Link to Suite Guide.', @@ -2718,6 +2734,14 @@ export default defineMessages({ defaultMessage: 'Tokens', id: 'TR_NAV_TOKENS', }, + TR_NAV_COLLECTIONS: { + defaultMessage: 'Collections', + id: 'TR_NAV_COLLECTIONS', + }, + TR_NAV_NFTS: { + defaultMessage: 'NFTs', + id: 'TR_NAV_NFTS', + }, TR_NAV_SIGN_AND_VERIFY: { defaultMessage: 'Sign & verify', description: @@ -2798,6 +2822,10 @@ export default defineMessages({ defaultMessage: 'BNB Smart Chain', id: 'TR_NETWORK_BNB', }, + TR_NETWORK_ARBITRUM_ONE: { + defaultMessage: 'Arbitrum One', + id: 'TR_NETWORK_ARBITRUM_ONE', + }, TR_NETWORK_BASE: { defaultMessage: 'Base', id: 'TR_NETWORK_BASE', @@ -3003,11 +3031,11 @@ export default defineMessages({ id: 'TR_RECEIVE', }, TR_RECEIVE_NETWORK: { - defaultMessage: 'Receive {network}', + defaultMessage: 'Receive {networkSymbol}', id: 'TR_RECEIVE_NETWORK', }, TR_BUY_NETWORK: { - defaultMessage: 'Buy {network}', + defaultMessage: 'Buy {networkSymbol}', id: 'TR_BUY_NETWORK', }, TR_TAPROOT_BANNER_TITLE: { @@ -3307,6 +3335,11 @@ export default defineMessages({ defaultMessage: 'Details', id: 'TR_TRANSACTION_DETAILS', }, + TR_TOKEN_ID_COLON: { + defaultMessage: 'Token ID:', + id: 'TR_TOKEN_ID_COLON', + }, + TR_TOKEN_ID: { defaultMessage: 'Token ID', id: 'TR_TOKEN_ID', @@ -4052,27 +4085,6 @@ export default defineMessages({ id: 'TR_ONION_LINKS_TITLE', defaultMessage: 'Open trezor.io links as .onion links', }, - TR_TOR_CONFIG_SNOWFLAKE_TITLE: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_TITLE', - defaultMessage: 'Tor Snowflake Binary Path', - }, - TR_TOR_CONFIG_SNOWFLAKE_DESCRIPTION: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_DESCRIPTION', - defaultMessage: - 'Enter the path to the Tor Snowflake binary on your system. Make sure Tor is disabled before making this change.', - }, - TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH', - defaultMessage: 'Must be a valid full path.', - }, - TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL', - defaultMessage: 'Update path', - }, - TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL: { - id: 'TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL', - defaultMessage: 'Disable Tor Snowflake', - }, TR_TOR_ENABLE_TITLE: { id: 'TR_TOR_ENABLE_TITLE', defaultMessage: 'Enable Tor', @@ -4373,7 +4385,7 @@ export default defineMessages({ }, RECEIVE_TITLE: { id: 'RECEIVE_TITLE', - defaultMessage: 'Receive {symbol}', + defaultMessage: 'Receive {networkSymbol}', }, RECEIVE_DESC_BITCOIN: { id: 'RECEIVE_DESC_BITCOIN', @@ -4979,6 +4991,24 @@ export default defineMessages({ defaultMessage: 'Experimental', description: 'Section title for Early Access program so far', }, + TR_EXPERIMENTAL_NFT_SECTION: { + id: 'TR_EXPERIMENTAL_NFT_SECTION', + defaultMessage: 'NFTs (non-fungible tokens)', + }, + TR_EXPERIMENTAL_NFT_SECTION_DESCRIPTION: { + id: 'TR_EXPERIMENTAL_NFT_SECTION_DESCRIPTION', + defaultMessage: + 'Adds an NFT section to EVM-based chain accounts. Currently, viewing is supported, but sending is not available.', + }, + TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT: { + id: 'TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT', + defaultMessage: 'Support for Ethereum L2 networks (Arbitrum One, Base, Optimism)', + }, + TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT_DESCRIPTION: { + id: 'TR_EXPERIMENTAL_ETHEREUM_L2_SUPPORT_DESCRIPTION', + defaultMessage: + 'Allows to enable Arbitrum One, Base, and Optimism networks in the coin settings.', + }, TR_EXPERIMENTAL_FEATURES_ALLOW: { id: 'TR_EXPERIMENTAL_FEATURES_ALLOW', defaultMessage: 'Experimental features', @@ -5001,14 +5031,14 @@ export default defineMessages({ defaultMessage: 'Use this utility to retrieve passwords stored on Dropbox and secured by Trezor. Designed for former users of the Trezor Password Manager Chrome extension.', }, - TR_EXPERIMENTAL_TOR_SNOWFLAKE: { - id: 'TR_EXPERIMENTAL_TOR_SNOWFLAKE', - defaultMessage: 'Tor Snowflake', + TR_EXPERIMENTAL_TOR_EXTERNAL: { + id: 'TR_EXPERIMENTAL_TOR_EXTERNAL', + defaultMessage: 'Tor external', }, - TR_EXPERIMENTAL_TOR_SNOWFLAKE_DESCRIPTION: { - id: 'TR_EXPERIMENTAL_TOR_SNOWFLAKE_DESCRIPTION', + TR_EXPERIMENTAL_TOR_EXTERNAL_DESCRIPTION: { + id: 'TR_EXPERIMENTAL_TOR_EXTERNAL_DESCRIPTION', defaultMessage: - 'Access censored websites and apps using Tor Snowflake, a system designed to bypass restrictions.', + 'Allows you to use Tor daemon running in a external process on port 9050 instead of the one bundled with Trezor Suite.', }, TR_EARLY_ACCESS: { id: 'TR_EARLY_ACCESS', @@ -5294,18 +5324,34 @@ export default defineMessages({ id: 'TR_TOKENS', defaultMessage: 'Tokens', }, + TR_COLLECTIONS: { + id: 'TR_COLLECTIONS', + defaultMessage: 'Collections', + }, TR_TOKENS_EMPTY: { id: 'TR_TOKENS_EMPTY', defaultMessage: 'No tokens... yet.', }, + TR_NFT_EMPTY: { + id: 'TR_NFT_EMPTY', + defaultMessage: 'No NFT collections... yet.', + }, TR_TOKENS_EMPTY_CHECK_HIDDEN: { id: 'TR_TOKENS_EMPTY_CHECK_HIDDEN', defaultMessage: 'No tokens. They may be hidden.', }, + TR_NFT_EMPTY_CHECK_HIDDEN: { + id: 'TR_NFT_EMPTY_CHECK_HIDDEN', + defaultMessage: 'No NFT collections. They may be hidden.', + }, TR_HIDDEN_TOKENS_EMPTY: { id: 'TR_HIDDEN_TOKENS_EMPTY', defaultMessage: 'You have no hidden tokens.', }, + TR_HIDDEN_NFT_EMPTY: { + id: 'TR_HIDDEN_NFT_EMPTY', + defaultMessage: 'You have no hidden NFT collections.', + }, TR_ADD_TOKEN_TITLE: { id: 'TR_ADD_TOKEN_TITLE', defaultMessage: 'Add ERC20 token', @@ -5395,6 +5441,10 @@ export default defineMessages({ defaultMessage: 'Amount', id: 'AMOUNT', }, + TR_QUANTITY: { + defaultMessage: 'Quantity', + id: 'TR_QUANTITY', + }, AMOUNT_SEND_MAX: { id: 'AMOUNT_SEND_MAX', defaultMessage: 'Send max', @@ -5436,9 +5486,14 @@ export default defineMessages({ id: 'AMOUNT_IS_LESS_THAN_RESERVE', }, AMOUNT_NOT_ENOUGH_CURRENCY_FEE: { - defaultMessage: 'Not enough {symbol} to cover transaction fee', + defaultMessage: 'Not enough {networkSymbol} to cover transaction fee', id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE', }, + + AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT: { + defaultMessage: 'Not enough {symbol} to cover transaction fee ({feeAmount} {symbol})', + id: 'AMOUNT_NOT_ENOUGH_CURRENCY_FEE_WITH_ETH_AMOUNT', + }, REMAINING_BALANCE_LESS_THAN_RENT: { defaultMessage: 'After sending this amount, your account will have {remainingSolBalance} SOL remaining. A non-empty account must maintain a balance of more than {rent} SOL.', @@ -5717,6 +5772,22 @@ export default defineMessages({ defaultMessage: 'There are no spendable UTXOs in your account.', description: 'Message showing in Coin control section', }, + TR_LARGEST_FIRST: { + id: 'TR_LARGEST_FIRST', + defaultMessage: 'Largest first', + }, + TR_SMALLEST_FIRST: { + id: 'TR_SMALLEST_FIRST', + defaultMessage: 'Smallest first', + }, + TR_OLDEST_FIRST: { + id: 'TR_OLDEST_FIRST', + defaultMessage: 'Oldest first', + }, + TR_NEWEST_FIRST: { + id: 'TR_NEWEST_FIRST', + defaultMessage: 'Newest first', + }, TR_LOADING_TRANSACTION_DETAILS: { id: 'TR_LOADING_TRANSACTION_DETAILS', defaultMessage: 'Loading transaction details', @@ -5747,6 +5818,11 @@ export default defineMessages({ defaultMessage: 'Rejected by coordinator', description: 'Tooltip over an icon in Coin control section', }, + TR_UTXO_NOT_MATURED_COINBASE: { + id: 'TR_UTXO_NOT_MATURED_COINBASE', + defaultMessage: + 'Coinbase transaction has to have at least {confirmations} confirmations to be spendable', + }, TR_CHANGE_ADDRESS_TOOLTIP: { id: 'TR_CHANGE_ADDRESS_TOOLTIP', defaultMessage: 'This is a change address created from a previous send.', @@ -6364,6 +6440,10 @@ export default defineMessages({ id: 'TR_UNHIDE_TOKEN', defaultMessage: 'Unhide token', }, + TR_HIDE_COLLECTION: { + id: 'TR_HIDE_COLLECTION', + defaultMessage: 'Hide collection', + }, TR_UNHIDE: { id: 'TR_UNHIDE', defaultMessage: 'Unhide', @@ -6448,10 +6528,18 @@ export default defineMessages({ id: 'TR_SEARCH_TOKENS', defaultMessage: 'Search tokens', }, + TR_SEARCH_COLLECTIONS: { + id: 'TR_SEARCH_COLLECTIONS', + defaultMessage: 'Search collections', + }, TR_TOKENS_SEARCH_TOOLTIP: { id: 'TR_TOKENS_SEARCH_TOOLTIP', defaultMessage: 'Search by token, symbol, or contract address.', }, + TR_COLLECTIONS_SEARCH_TOOLTIP: { + id: 'TR_COLLECTIONS_SEARCH_TOOLTIP', + defaultMessage: 'Search by collection name, symbol, or contract address.', + }, TR_SEARCH_TRANSACTIONS: { id: 'TR_SEARCH_TRANSACTIONS', defaultMessage: 'Search transactions', @@ -7436,7 +7524,7 @@ export default defineMessages({ }, TR_STAKING_GETTING_READY: { id: 'TR_STAKING_GETTING_READY', - defaultMessage: 'Your {symbol} is getting ready to work', + defaultMessage: 'Your {networkSymbol} is getting ready to work', }, TR_STAKING_REWARDS_ARE_RESTAKED: { id: 'TR_STAKING_REWARDS_ARE_RESTAKED', @@ -7448,11 +7536,11 @@ export default defineMessages({ }, TR_STAKING_CONSOLIDATING_FUNDS: { id: 'TR_STAKING_CONSOLIDATING_FUNDS', - defaultMessage: 'Consolidating your {symbol} for you', + defaultMessage: 'Consolidating your {networkSymbol} for you', }, TR_STAKING_YOUR_UNSTAKED_FUNDS: { id: 'TR_STAKING_YOUR_UNSTAKED_FUNDS', - defaultMessage: 'Your unstaked {symbol} is ready', + defaultMessage: 'Your unstaked {networkSymbol} is ready', }, TR_RECEIVING_SYMBOL: { id: 'TR_RECEIVING_SYMBOL', @@ -8585,9 +8673,9 @@ export default defineMessages({ id: 'TR_TO', defaultMessage: 'To', }, - TR_STAKE_ETH: { - id: 'TR_STAKE_ETH', - defaultMessage: 'Stake Ethereum', + TR_STAKE_NETWORK: { + id: 'TR_STAKE_NETWORK', + defaultMessage: 'Stake {symbol}', }, TR_STAKE_RESTAKED_BADGE: { id: 'TR_STAKE_RESTAKED_BADGE', @@ -8605,9 +8693,9 @@ export default defineMessages({ id: 'TR_STAKE_ETH_SEE_MONEY_DANCE', defaultMessage: 'Watch your money dance', }, - TR_STAKE_ETH_SEE_MONEY_DANCE_DESC: { - id: 'TR_STAKE_ETH_SEE_MONEY_DANCE_DESC', - defaultMessage: 'Earn {apyPercent}% APY by staking your Ethereum with Trezor.', + TR_STAKE_NETWORK_SEE_MONEY_DANCE_DESC: { + id: 'TR_STAKE_NETWORK_SEE_MONEY_DANCE_DESC', + defaultMessage: 'Earn {apyPercent}% APY by staking your {symbol} with Trezor.', }, TR_STAKE_APY_DESC: { id: 'TR_STAKE_APY_DESC', @@ -8653,15 +8741,15 @@ export default defineMessages({ id: 'TR_STAKE_WHAT_IS_STAKING', defaultMessage: 'What is staking?', }, - TR_STAKE_STAKING_IS: { + TR_STAKE_NETWORK_STAKING_IS: { id: 'TR_STAKE_STAKING_IS', defaultMessage: - "Staking involves temporarily locking your Ethereum assets to support the blockchain's operation. In return, you'll earn additional Ethereum as a reward.", + "Staking involves temporarily locking your {symbol} to support the blockchain's operation. In return, you'll earn additional {symbol} as a reward.", }, TR_STAKE_ANY_AMOUNT_ETH: { id: 'TR_STAKE_ANY_AMOUNT_ETH', defaultMessage: - 'Stake a minimum amount of {amount} {symbol} and start earning rewards. With our current APY rate of {apyPercent}%, your rewards earn too!', + 'Stake a minimum amount of {amount} {networkSymbol} and start earning rewards. With our current APY rate of {apyPercent}%, your rewards earn too!', }, TR_STAKE_LEARN_MORE: { id: 'TR_STAKE_LEARN_MORE', @@ -8701,15 +8789,16 @@ export default defineMessages({ }, TR_STAKE_CLAIM_UNSTAKED: { id: 'TR_STAKE_CLAIM_UNSTAKED', - defaultMessage: 'Claim unstaked {symbol}', + defaultMessage: 'Claim unstaked {networkSymbol}', }, TR_STAKE_IN_ACCOUNT: { id: 'TR_STAKE_IN_ACCOUNT', - defaultMessage: '{symbol} in account', + defaultMessage: '{networkSymbol} in account', }, TR_STAKE_STAKED_ETH_AMOUNT_LOCKED: { id: 'TR_STAKE_STAKED_ETH_AMOUNT_LOCKED', - defaultMessage: 'The staked amount of {symbol} is locked and can’t be traded or sent.', + defaultMessage: + 'The staked amount of {networkSymbol} is locked and can’t be traded or sent.', }, TR_STAKE_UNSTAKING_TAKES: { id: 'TR_STAKE_UNSTAKING_TAKES', @@ -8719,7 +8808,7 @@ export default defineMessages({ TR_STAKE_ETH_REWARDS_EARN: { id: 'TR_STAKE_ETH_REWARDS_EARN', defaultMessage: - 'Your rewards also earn. Keep them staked and watch your {symbol} rewards soar.', + 'Your rewards also earn. Keep them staked and watch your {networkSymbol} rewards soar.', }, TR_STAKE_AVAILABLE: { id: 'TR_STAKE_AVAILABLE', @@ -8736,17 +8825,18 @@ export default defineMessages({ }, TR_STAKE_LEFT_AMOUNT_FOR_WITHDRAWAL: { id: 'TR_STAKE_LEFT_AMOUNT_FOR_WITHDRAWAL', - defaultMessage: 'We’ve left {amount} {symbol} out so you can pay for withdrawal fees.', + defaultMessage: + 'We’ve left {amount} {networkSymbol} out so you can pay for withdrawal fees.', }, TR_STAKE_LEFT_SMALL_AMOUNT_FOR_WITHDRAWAL: { id: 'TR_STAKE_LEFT_SMALL_AMOUNT_FOR_WITHDRAWAL', defaultMessage: - 'We’ve left a small amount of {symbol} out so you can pay for withdrawal fees.', + 'We’ve left a small amount of {networkSymbol} out so you can pay for withdrawal fees.', }, TR_STAKE_RECOMMENDED_AMOUNT_FOR_WITHDRAWALS: { id: 'TR_STAKE_RECOMMENDED_AMOUNT_FOR_WITHDRAWALS', defaultMessage: - "It's recommended to leave {amount} {symbol} so you can pay for withdrawal fees.", + "It's recommended to leave {amount} {networkSymbol} so you can pay for withdrawal fees.", }, TR_STAKE_CONFIRM_ENTRY_PERIOD: { id: 'TR_STAKE_CONFIRM_ENTRY_PERIOD', @@ -8761,10 +8851,14 @@ export default defineMessages({ defaultMessage: 'Entering the staking pool may take up to {count, plural, one {# day} other {# days}}', }, + TR_STAKE_ACTIVATION_COULD_TAKE: { + id: 'TR_STAKE_ACTIVATION_COULD_TAKE', + defaultMessage: 'Stake activation usually takes 1 epoch (~3 days)', + }, TR_STAKE_ETH_WILL_BE_BLOCKED: { id: 'TR_STAKE_ETH_WILL_BE_BLOCKED', defaultMessage: - 'Your {symbol} will be blocked during this period, and you can’t cancel this. Learn more', + 'Your {networkSymbol} will be blocked during this period, and you can’t cancel this. Learn more', }, TR_STAKE_ACKNOWLEDGE_ENTRY_PERIOD: { id: 'TR_STAKE_ACKNOWLEDGE_ENTRY_PERIOD', @@ -8829,7 +8923,7 @@ export default defineMessages({ TR_STAKE_ETH_REWARDS_EARN_APY: { id: 'TR_STAKE_ETH_REWARDS_EARN_APY', defaultMessage: - 'Your {symbol} rewards also earn the APY rate. Keep your funds staked or add more to increase your rewards.', + 'Your {networkSymbol} rewards also earn the APY rate. Keep your funds staked or add more to increase your rewards.', }, TR_STAKE_REWARDS: { id: 'TR_STAKE_REWARDS', @@ -8847,6 +8941,10 @@ export default defineMessages({ id: 'ZERO_BALANCE_TOKENS', defaultMessage: 'Zero-balance tokens', }, + EMPTY_NFT_COLLECTIONS: { + id: 'EMPTY_NFT_COLLECTIONS', + defaultMessage: 'Empty collections', + }, TR_STAKE_ADDING_TO_POOL: { id: 'TR_STAKE_ADDING_TO_POOL', defaultMessage: 'Adding to staking pool', @@ -8910,7 +9008,7 @@ export default defineMessages({ }, TR_STAKE_CLAIMED_AMOUNT_TRANSFERRED: { id: 'TR_STAKE_CLAIMED_AMOUNT_TRANSFERRED', - defaultMessage: 'The claimed amount is transferred to your {symbol} account.', + defaultMessage: 'The claimed amount is transferred to your {networkSymbol} account.', }, TR_STAKE_CLAIMING_PERIOD: { id: 'TR_STAKE_CLAIMING_PERIOD', @@ -8918,7 +9016,7 @@ export default defineMessages({ }, TR_STAKE_MIN_AMOUNT_TOOLTIP: { id: 'TR_STAKE_MIN_AMOUNT_TOOLTIP', - defaultMessage: 'Minimum amount to stake is {amount} {symbol}', + defaultMessage: 'Minimum amount to stake is {amount} {networkSymbol}', }, TOAST_TX_STAKED: { id: 'TOAST_TX_STAKED', @@ -8934,7 +9032,7 @@ export default defineMessages({ }, TOAST_SUCCESSFUL_CLAIM: { id: 'TOAST_SUCCESSFUL_CLAIM', - defaultMessage: '{symbol} claimed successfully', + defaultMessage: '{networkSymbol} claimed successfully', }, TOAST_ESTIMATED_FEE_ERROR: { id: 'TOAST_ESTIMATED_FEE_ERROR', @@ -8972,13 +9070,23 @@ export default defineMessages({ TR_STAKE_EVERSTAKE_MANAGES: { id: 'TR_STAKE_EVERSTAKE_MANAGES', defaultMessage: - 'Everstake maintains and protects your staked {symbol} with their smart contracts, infrastructure, and technology.', + 'Everstake maintains and protects your staked {networkSymbol} with their smart contracts, infrastructure, and technology.', }, TR_STAKE_TREZOR_NO_LIABILITY: { id: 'TR_STAKE_TREZOR_NO_LIABILITY', defaultMessage: "When staking, the responsibility for your funds' security transitions from your Trezor to Everstake.", }, + TR_STAKE_BY_STAKING_YOU_CAN_EARN_REWARDS: { + id: 'TR_STAKE_BY_STAKING_YOU_CAN_EARN_REWARDS', + defaultMessage: + 'By staking your {networkSymbol}, you can earn rewards while contributing to the security and stability of the network.', + }, + TR_STAKE_SECURELY_DELEGATE_TO_EVERSTAKE: { + id: 'TR_STAKE_SECURELY_DELEGATE_TO_EVERSTAKE', + defaultMessage: + 'With Trezor Suite, you can effortlessly and securely delegate your {symbol} to Everstake validator node for staking. Enjoy competitive rewards, rely on a trusted validator, and maintain full ownership of your coins.', + }, TR_STAKE_CONSENT_TO_STAKING_WITH_EVERSTAKE: { id: 'TR_STAKE_CONSENT_TO_STAKING_WITH_EVERSTAKE', defaultMessage: 'I acknowledge and consent to staking with Everstake', diff --git a/packages/suite/src/types/coinmarket/coinmarketVerify.ts b/packages/suite/src/types/coinmarket/coinmarketVerify.ts index 21fbcfeb3257..268ecf94965c 100644 --- a/packages/suite/src/types/coinmarket/coinmarketVerify.ts +++ b/packages/suite/src/types/coinmarket/coinmarketVerify.ts @@ -3,11 +3,10 @@ import { UseFormReturn } from 'react-hook-form'; import { CryptoId } from 'invity-api'; -import { NetworkSymbol } from '@suite-common/wallet-config'; import { AccountAddress } from '@trezor/connect'; import type { Account } from 'src/types/wallet'; -import { ExtendedMessageDescriptor, TrezorDevice } from 'src/types/suite'; +import { ExtendedMessageDescriptor } from 'src/types/suite'; export interface CoinmarketVerifyFormProps { address?: string; @@ -21,7 +20,7 @@ export interface CoinmarketVerifyFormAccountOptionProps { } export interface CoinmarketVerifyAccountProps { - currency: CryptoId | undefined; + cryptoId: CryptoId | undefined; } export interface CoinmarketGetTranslationIdsProps { @@ -51,11 +50,3 @@ export interface CoinmarketVerifyOptionsItemProps { option: CoinmarketVerifyFormAccountOptionProps; receiveNetwork: CryptoId; } - -export interface CoinmarketGetSuiteReceiveAccountsProps { - currency: CryptoId | undefined; - device: TrezorDevice | undefined; - symbol: NetworkSymbol | undefined; - isDebug: boolean; - accounts: Account[]; -} diff --git a/packages/suite/src/types/suite/index.ts b/packages/suite/src/types/suite/index.ts index b8a8cc4e2331..d5739278aec0 100644 --- a/packages/suite/src/types/suite/index.ts +++ b/packages/suite/src/types/suite/index.ts @@ -117,11 +117,6 @@ export interface TorBootstrap { isSlow?: boolean; } -export type TorConfig = { - enableSnowflake: boolean; - snowflakeBinaryPath: string; -}; - export enum DisplayMode { CHUNKS = 1, PAGINATED_TEXT, diff --git a/packages/suite/src/types/wallet/sendForm.ts b/packages/suite/src/types/wallet/sendForm.ts index 1719cef74ed8..d78be699b041 100644 --- a/packages/suite/src/types/wallet/sendForm.ts +++ b/packages/suite/src/types/wallet/sendForm.ts @@ -14,6 +14,7 @@ import { PrecomposedLevels, PrecomposedLevelsCardano, Rate, + UtxoSorting, WalletAccountTransaction, } from '@suite-common/wallet-types'; import { FiatCurrencyCode } from '@suite-common/suite-config'; @@ -30,7 +31,6 @@ export type ExportFileType = 'csv' | 'pdf' | 'json'; export type UseSendFormState = { account: Account; network: Network; - coinFees: FeeInfo; localCurrencyOption: { value: FiatCurrencyCode; label: Uppercase }; feeInfo: FeeInfo; composedLevels?: PrecomposedLevels | PrecomposedLevelsCardano; @@ -50,6 +50,8 @@ export interface UtxoSelectionContext { coinjoinRegisteredUtxos: AccountUtxo[]; isLowAnonymityUtxoSelected: boolean; anonymityWarningChecked: boolean; + utxoSorting?: UtxoSorting; + selectUtxoSorting: (ordering: UtxoSorting) => void; toggleAnonymityWarning: () => void; toggleCheckAllUtxos: () => void; toggleCoinControl: () => void; @@ -73,6 +75,7 @@ export type SendContextValues = outputs: Partial[]; // useFieldArray fields updateContext: (value: Partial) => void; resetContext: () => void; + resetDraft: () => void; composeTransaction: (field?: FieldPath) => void; loadTransaction: () => Promise; signTransaction: () => void; diff --git a/packages/suite/src/utils/suite/__fixtures__/stake.ts b/packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts similarity index 99% rename from packages/suite/src/utils/suite/__fixtures__/stake.ts rename to packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts index cb56784bb95e..9e41a2002700 100644 --- a/packages/suite/src/utils/suite/__fixtures__/stake.ts +++ b/packages/suite/src/utils/suite/__fixtures__/ethereumStaking.ts @@ -404,7 +404,7 @@ export const getStakeFormsDefaultValuesFixture = [ description: 'should return default values for stake forms', args: { address: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', - ethereumStakeType: 'stake', + stakeType: 'stake', amount: '0.1', }, result: { @@ -422,7 +422,7 @@ export const getStakeFormsDefaultValuesFixture = [ }, ], options: ['broadcast'], - ethereumStakeType: 'stake', + stakeType: 'stake', ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -440,7 +440,7 @@ export const getStakeFormsDefaultValuesFixture = [ 'should return default values for stake forms with empty amount when amount is invalid', args: { address: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', - ethereumStakeType: 'stake', + stakeType: 'stake', amount: undefined, }, result: { @@ -458,7 +458,7 @@ export const getStakeFormsDefaultValuesFixture = [ }, ], options: ['broadcast'], - ethereumStakeType: 'stake', + stakeType: 'stake', ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -477,7 +477,7 @@ export const getStakeTxGasLimitFixture = [ { description: 'should return correct gasLimit', args: { - ethereumStakeType: 'stake', + stakeType: 'stake', from: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', amount: '0.1', // eth symbol: 'eth', @@ -497,7 +497,7 @@ export const getStakeTxGasLimitFixture = [ { description: 'should throw an error when stake type is empty', args: { - ethereumStakeType: '', + stakeType: '', from: '0xfB0bc552ab5Fa1971E8530852753c957e29eEEFC', amount: '0.1', // eth symbol: 'eth', diff --git a/packages/suite/src/utils/suite/__tests__/stake.test.ts b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts similarity index 99% rename from packages/suite/src/utils/suite/__tests__/stake.test.ts rename to packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts index c21e68d1f36c..e3cc1ffc9df3 100644 --- a/packages/suite/src/utils/suite/__tests__/stake.test.ts +++ b/packages/suite/src/utils/suite/__tests__/ethereumStaking.test.ts @@ -31,7 +31,7 @@ import { getInstantStakeType, getChangedInternalTx, simulateUnstake, -} from '../stake'; +} from '../ethereumStaking'; import { transformTxFixtures, stakeFixture, @@ -51,7 +51,7 @@ import { getInstantStakeTypeFixture, getChangedInternalTxFixture, simulateUnstakeFixture, -} from '../__fixtures__/stake'; +} from '../__fixtures__/ethereumStaking'; describe('transformTx', () => { transformTxFixtures.forEach(test => { diff --git a/packages/suite/src/utils/suite/stake.ts b/packages/suite/src/utils/suite/ethereumStaking.ts similarity index 88% rename from packages/suite/src/utils/suite/stake.ts rename to packages/suite/src/utils/suite/ethereumStaking.ts index f7760e512410..cc4678199749 100644 --- a/packages/suite/src/utils/suite/stake.ts +++ b/packages/suite/src/utils/suite/ethereumStaking.ts @@ -1,4 +1,4 @@ -import { selectNetwork } from '@everstake/wallet-sdk/ethereum'; +import { Ethereum, ETH_NETWORK_ADDRESSES, EthNetworkAddresses } from '@everstake/wallet-sdk'; import { fromWei, numberToHex, toWei } from 'web3-utils'; import { @@ -13,6 +13,8 @@ import { MIN_ETH_AMOUNT_FOR_STAKING, MAX_ETH_AMOUNT_FOR_STAKING, UNSTAKE_INTERCHANGES, + WALLET_SDK_SOURCE, + UNSTAKING_ETH_PERIOD, } from '@suite-common/wallet-constants'; import type { NetworkSymbol } from '@suite-common/wallet-config'; import { getEthereumEstimateFeeParams, isPending, sanitizeHex } from '@suite-common/wallet-utils'; @@ -24,15 +26,6 @@ import { PartialRecord } from '@trezor/type-utils'; import { TranslationFunction } from 'src/hooks/suite/useTranslation'; -// source is a required parameter for some functions in the Everstake Wallet SDK. -// This parameter is used for some contract calls. -// It is a constant which allows the SDK to define which app calls its functions. -// Each app which integrates the SDK has its own source, e.g. source for Trezor Suite is '1'. -export const WALLET_SDK_SOURCE = '1'; - -// Used when Everstake unstaking period is not available from the API. -export const UNSTAKING_ETH_PERIOD = 3; - const secondsToDays = (seconds: number) => Math.round(seconds / 60 / 60 / 24); type EthNetwork = 'holesky' | 'mainnet'; @@ -49,6 +42,15 @@ export const getEthNetworkForWalletSdk = ( return network ?? 'mainnet'; }; +export const getEthNetworkAddresses = (symbol: NetworkSymbol): EthNetworkAddresses => { + const defaultAddresses = ETH_NETWORK_ADDRESSES['mainnet']; + const ethNetwork = getEthNetworkForWalletSdk(symbol); + + if (!ethNetwork) return defaultAddresses; + + return ETH_NETWORK_ADDRESSES[ethNetwork] ?? defaultAddresses; +}; + export const getAdjustedGasLimitConsumption = (estimatedFee: Success) => new BigNumber(estimatedFee.payload.levels[0].feeLimit || '') .plus(STAKE_GAS_LIMIT_RESERVE) @@ -77,9 +79,11 @@ export const stake = async ({ try { const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_pool: contractPool } = selectNetwork(ethNetwork); - const contractPoolAddress = contractPool.options.address; - const data = contractPool.methods.stake(WALLET_SDK_SOURCE).encodeABI(); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol); + + const contractPoolAddress = ethereumClient.contractPool.options.address; + const data = ethereumClient.contractPool.methods.stake(WALLET_SDK_SOURCE).encodeABI(); // gasLimit calculation based on address, amount and data size // amount is essential for a proper calculation of gasLimit (via blockbook/geth) @@ -90,7 +94,7 @@ export const stake = async ({ blocks: [2], specific: { from, - ...getEthereumEstimateFeeParams(contractPoolAddress, amount, undefined, data), + ...getEthereumEstimateFeeParams(addressContractPool, amount, undefined, data), }, }, }); @@ -151,9 +155,10 @@ export const unstake = async ({ const amountWei = toWei(amount, 'ether'); const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_pool: contractPool } = selectNetwork(ethNetwork); - const contractPoolAddress = contractPool.options.address; - const data = contractPool.methods + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol); + const contractPoolAddress = ethereumClient.contractPool.options.address; + const data = ethereumClient.contractPool.methods .unstake(amountWei, interchanges, WALLET_SDK_SOURCE) .encodeABI(); @@ -166,7 +171,7 @@ export const unstake = async ({ blocks: [2], specific: { from, - ...getEthereumEstimateFeeParams(contractPoolAddress, '0', undefined, data), + ...getEthereumEstimateFeeParams(addressContractPool, '0', undefined, data), }, }, }); @@ -213,9 +218,11 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa if (!readyForClaim.eq(requested)) throw new Error('Unstake request not filled yet'); const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { contract_accounting: contractAccounting } = selectNetwork(ethNetwork); - const contractAccountingAddress = contractAccounting.options.address; - const data = contractAccounting.methods.claimWithdrawRequest().encodeABI(); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractAccounting } = getEthNetworkAddresses(symbol); + + const contractAccountingAddress = ethereumClient.contractAccounting.options.address; + const data = ethereumClient.contractAccounting.methods.claimWithdrawRequest().encodeABI(); // gasLimit calculation based on address, amount and data size // amount is essential for a proper calculation of gasLimit (via blockbook/geth) @@ -227,7 +234,7 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa specific: { from, ...getEthereumEstimateFeeParams( - contractAccountingAddress, + addressContractAccounting, '0', undefined, data, @@ -253,13 +260,13 @@ export const claimWithdrawRequest = async ({ from, symbol, identity }: StakeTxBa export interface GetStakeFormsDefaultValuesParams { address: string; - ethereumStakeType: StakeFormState['ethereumStakeType']; + stakeType: StakeFormState['stakeType']; amount?: string; } export const getStakeFormsDefaultValues = ({ address, - ethereumStakeType, + stakeType, amount, }: GetStakeFormsDefaultValuesParams) => ({ fiatInput: '', @@ -273,7 +280,7 @@ export const getStakeFormsDefaultValues = ({ ], options: ['broadcast'], - ethereumStakeType, + stakeType, ethereumNonce: '', ethereumDataAscii: '', ethereumDataHex: '', @@ -427,7 +434,7 @@ export const prepareClaimEthTx = async ({ }; export interface GetStakeTxGasLimitParams { - ethereumStakeType: StakeType | undefined; + stakeType: StakeType | undefined; from: string; amount: string; symbol: NetworkSymbol; @@ -445,7 +452,7 @@ export type GetStakeTxGasLimitResponse = }; export const getStakeTxGasLimit = async ({ - ethereumStakeType, + stakeType, from, amount, symbol, @@ -459,7 +466,7 @@ export const getStakeTxGasLimit = async ({ }, }; - if (!ethereumStakeType) { + if (!stakeType) { return { success: false, error: genericError, @@ -468,10 +475,10 @@ export const getStakeTxGasLimit = async ({ try { let txData; - if (ethereumStakeType === 'stake') { + if (stakeType === 'stake') { txData = await stake({ from, amount, symbol, identity }); } - if (ethereumStakeType === 'unstake') { + if (stakeType === 'unstake') { // Increase allowedInterchangeNum to enable instant unstaking. txData = await unstake({ from, @@ -481,7 +488,7 @@ export const getStakeTxGasLimit = async ({ identity, }); } - if (ethereumStakeType === 'claim') { + if (stakeType === 'claim') { txData = await claimWithdrawRequest({ from, symbol, identity }); } @@ -578,19 +585,17 @@ export const getInstantStakeType = ( ): StakeType | null => { if (!address || !symbol) return null; const { from, to } = internalTransfer; - const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { address_pool: poolAddress, address_withdraw_treasury: withdrawTreasuryAddress } = - selectNetwork(ethNetwork); + const { addressContractPool, addressContractWithdrawTreasury } = getEthNetworkAddresses(symbol); - if (from === poolAddress && to === withdrawTreasuryAddress) { + if (from === addressContractPool && to === addressContractWithdrawTreasury) { return 'stake'; } - if (from === poolAddress && to === address) { + if (from === addressContractPool && to === address) { return 'unstake'; } - if (from === withdrawTreasuryAddress && to === address) { + if (from === addressContractWithdrawTreasury && to === address) { return 'claim'; } @@ -645,13 +650,14 @@ export const simulateUnstake = async ({ symbol, }: StakeTxBaseArgs & { amount: string }) => { const ethNetwork = getEthNetworkForWalletSdk(symbol); - const { address_pool: poolAddress, contract_pool: contractPool } = selectNetwork(ethNetwork); + const ethereumClient = new Ethereum(ethNetwork); + const { addressContractPool } = getEthNetworkAddresses(symbol); if (!amount || !from || !symbol) return null; const amountWei = toWei(amount, 'ether'); - const data = contractPool.methods + const data = ethereumClient.contractPool.methods .unstake(amountWei, UNSTAKE_INTERCHANGES, WALLET_SDK_SOURCE) .encodeABI(); if (!data) return null; @@ -659,7 +665,7 @@ export const simulateUnstake = async ({ const ethereumData = await TrezorConnect.blockchainEvmRpcCall({ coin: symbol, from, - to: poolAddress, + to: addressContractPool, data, }); diff --git a/packages/suite/src/utils/suite/solanaStaking.ts b/packages/suite/src/utils/suite/solanaStaking.ts new file mode 100644 index 000000000000..09643c38e2b9 --- /dev/null +++ b/packages/suite/src/utils/suite/solanaStaking.ts @@ -0,0 +1,84 @@ +import { VersionedTransaction, PublicKey } from '@solana/web3.js-version1'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { LAMPORTS_PER_SOL, WALLET_SDK_SOURCE } from '@suite-common/wallet-constants'; +import { selectSolanaWalletSdkNetwork } from '@suite-common/wallet-utils'; +import { BigNumber } from '@trezor/utils'; +import type { SolanaSignTransaction } from '@trezor/connect'; +import { Blockchain } from '@suite-common/wallet-types'; + +type SolanaTx = SolanaSignTransaction & { + versionedTx: VersionedTransaction; +}; + +export const transformTx = ( + tx: VersionedTransaction, + path: string | number[], + tokenAccountsInfos?: { + baseAddress: string; + tokenProgram: string; + tokenMint: string; + tokenAccount: string; + }[], +): SolanaTx => { + const serializedMessage = new Uint8Array(tx.message.serialize()); + const serializedTxHex = Buffer.from(serializedMessage).toString('hex'); + + const transformedTx = { + path, + serializedTx: serializedTxHex, + additionalInfo: tokenAccountsInfos ? { tokenAccountsInfos } : undefined, + versionedTx: tx, + }; + + return transformedTx; +}; + +export const getPubKeyFromAddress = (address: string) => { + return new PublicKey(address); +}; + +interface PrepareStakeSolTxParams { + from: string; + path: string | number[]; + amount: string; + symbol: NetworkSymbol; + selectedBlockchain: Blockchain; +} +export type PrepareStakeSolTxResponse = + | { + success: true; + tx: SolanaTx; + } + | { + success: false; + errorMessage: string; + }; + +export const prepareStakeSolTx = async ({ + from, + path, + amount, + symbol, + selectedBlockchain, +}: PrepareStakeSolTxParams): Promise => { + try { + const solanaClient = selectSolanaWalletSdkNetwork(symbol, selectedBlockchain.url); + + const lamports = new BigNumber(LAMPORTS_PER_SOL).multipliedBy(amount).toNumber(); // stake method expects lamports as a number + const tx = await solanaClient.stake(from, lamports, WALLET_SDK_SOURCE); + const transformedTx = transformTx(tx.result, path); + + return { + success: true, + tx: transformedTx, + }; + } catch (e) { + console.error(e); + + return { + success: false, + errorMessage: e.message, + }; + } +}; diff --git a/packages/suite/src/utils/suite/transactionReview.ts b/packages/suite/src/utils/suite/transactionReview.ts index ffe85a9e729a..8aa29ac9e565 100644 --- a/packages/suite/src/utils/suite/transactionReview.ts +++ b/packages/suite/src/utils/suite/transactionReview.ts @@ -2,17 +2,17 @@ import { TranslationKey } from '@suite-common/intl-types'; import { StakeFormState } from '@suite-common/wallet-types'; interface getTransactionReviewModalActionTextParams { - ethereumStakeType: StakeFormState['ethereumStakeType'] | null; + stakeType: StakeFormState['stakeType'] | null; isRbfAction: boolean; isSending?: boolean; } export const getTransactionReviewModalActionText = ({ - ethereumStakeType, + stakeType, isRbfAction, isSending, }: getTransactionReviewModalActionTextParams): TranslationKey => { - switch (ethereumStakeType) { + switch (stakeType) { case 'stake': return 'TR_STAKE_STAKE'; case 'unstake': diff --git a/packages/suite/src/utils/wallet/__tests__/tokenUtils.test.ts b/packages/suite/src/utils/wallet/__tests__/tokenUtils.test.ts index bacfb3e7cdcd..9a3e2a57ca84 100644 --- a/packages/suite/src/utils/wallet/__tests__/tokenUtils.test.ts +++ b/packages/suite/src/utils/wallet/__tests__/tokenUtils.test.ts @@ -5,9 +5,14 @@ describe('getTokens', () => { getTokensFixtures.forEach( ({ testName, tokens, symbol, coinDefinitions, searchQuery, result }) => { test(testName, () => { - expect(getTokens(tokens, symbol, coinDefinitions, searchQuery)).toStrictEqual( - result, - ); + expect( + getTokens({ + tokens, + symbol, + tokenDefinitions: coinDefinitions, + searchQuery, + }), + ).toStrictEqual(result); }); }, ); diff --git a/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts b/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts new file mode 100644 index 000000000000..ca753e77b591 --- /dev/null +++ b/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts @@ -0,0 +1,74 @@ +import { testMocks } from '@suite-common/test-utils'; + +import { sortUtxos } from '../utxoSortingUtils'; + +const UTXOS = [ + testMocks.getUtxo({ amount: '1', blockHeight: undefined, txid: 'txid1', vout: 0 }), + testMocks.getUtxo({ amount: '2', blockHeight: undefined, txid: 'txid2', vout: 1 }), + testMocks.getUtxo({ amount: '2', blockHeight: 1, txid: 'txid2', vout: 0 }), + testMocks.getUtxo({ amount: '2', blockHeight: 2, txid: 'txid3', vout: 0 }), +]; + +const ACCOUNT_TRANSACTIONS = [ + testMocks.getWalletTransaction({ txid: 'txid1', blockTime: undefined }), + testMocks.getWalletTransaction({ txid: 'txid2', blockTime: 1 }), + testMocks.getWalletTransaction({ txid: 'txid3', blockTime: 2 }), +]; + +const findTx = (txid: string) => ACCOUNT_TRANSACTIONS.find(tx => tx.txid === txid); + +describe(sortUtxos.name, () => { + it('sorts UTXOs by newest first', () => { + const sortedUtxos = sortUtxos(UTXOS, 'newestFirst', ACCOUNT_TRANSACTIONS); + expect( + sortedUtxos.map(it => [ + it.blockHeight ?? findTx(it.txid)?.blockTime, + `${it.txid}:${it.vout}`, // for stable sorting + ]), + ).toEqual([ + [2, 'txid3:0'], + [1, 'txid2:1'], + [1, 'txid2:0'], + [undefined, 'txid1:0'], + ]); + }); + + it('sorts UTXOs by oldest first', () => { + const sortedUtxos = sortUtxos(UTXOS, 'oldestFirst', ACCOUNT_TRANSACTIONS); + expect( + sortedUtxos.map(it => [ + it.blockHeight ?? findTx(it.txid)?.blockTime, + `${it.txid}:${it.vout}`, // for stable sorting + ]), + ).toEqual([ + [undefined, 'txid1:0'], + [1, 'txid2:0'], + [1, 'txid2:1'], + [2, 'txid3:0'], + ]); + }); + + it('sorts by size, largest first', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(0, 2), 'largestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => it.amount)).toEqual(['2', '1']); + }); + + it('sorts by size, smallest first', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(0, 2), 'smallestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => it.amount)).toEqual(['1', '2']); + }); + + it('sorts by secondary sorting by `txid` and `vout` in case of same values', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(1, 4), 'smallestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => `${it.txid}:${it.vout}`)).toEqual([ + 'txid2:0', + 'txid2:1', + 'txid3:0', + ]); + }); + + it('returns the original array if utxoSorting is undefined', () => { + const sortedUtxos = sortUtxos(UTXOS, undefined, ACCOUNT_TRANSACTIONS); + expect(sortedUtxos).toEqual(UTXOS); + }); +}); diff --git a/packages/suite/src/utils/wallet/coinmarket/coinmarketUtils.ts b/packages/suite/src/utils/wallet/coinmarket/coinmarketUtils.ts index 93919646c773..e07d765bd736 100644 --- a/packages/suite/src/utils/wallet/coinmarket/coinmarketUtils.ts +++ b/packages/suite/src/utils/wallet/coinmarket/coinmarketUtils.ts @@ -8,8 +8,10 @@ import { getNetwork, getNetworkByCoingeckoId, getNetworkByCoingeckoNativeId, + getNetworkDisplaySymbol, getNetworkFeatures, getNetworkType, + isNetworkSymbol, } from '@suite-common/wallet-config'; import TrezorConnect from '@trezor/connect'; import { DefinitionType, isTokenDefinitionKnown } from '@suite-common/token-definitions'; @@ -78,6 +80,14 @@ export const getNetworkName = (symbol: NetworkSymbol) => { return getNetwork(symbol).name; }; +export const getCoinmarketNetworkDisplaySymbol = (symbol: string) => { + const symbolLowered = symbol.toLowerCase(); + + return isNetworkSymbol(symbolLowered) + ? getNetworkDisplaySymbol(symbolLowered) + : symbol.toUpperCase(); +}; + interface CoinmarketGetDecimalsProps { sendCryptoSelect?: CoinmarketAccountOptionsGroupOptionProps; network?: Network | null; @@ -341,6 +351,12 @@ export const coinmarketBuildAccountOptions = ({ deviceState, }); + /** + * TODO: allow second layer ETH coins to trade, now it is not working -> skip them + * Temporary solution to skip not native network symbols + */ + const skipNotNativeNetworkSymbols: readonly NetworkSymbol[] = ['op', 'base', 'arb']; + const groups: CoinmarketAccountsOptionsGroupProps[] = []; accountsSorted.forEach(account => { @@ -353,7 +369,9 @@ export const coinmarketBuildAccountOptions = ({ accountType, } = account; - if (!getNetwork(accountSymbol).coingeckoNativeId) { + const network = getNetwork(accountSymbol); + + if (!network.coingeckoNativeId) { return; } @@ -365,18 +383,18 @@ export const coinmarketBuildAccountOptions = ({ index, }); - const accountDecimals = getNetwork(accountSymbol).decimals; - const options: CoinmarketAccountOptionsGroupOptionProps[] = [ - { - value: getNetwork(accountSymbol).coingeckoNativeId as CryptoId, - label: accountSymbol.toUpperCase(), - cryptoName: getNetworkName(accountSymbol), - descriptor, - balance: formattedBalance ?? '', - accountType: account.accountType, - decimals: accountDecimals, - }, - ]; + const accountDecimals = network.decimals; + const option: CoinmarketAccountOptionsGroupOptionProps = { + value: network.coingeckoNativeId as CryptoId, + label: getNetworkDisplaySymbol(accountSymbol), + cryptoName: network.name, + descriptor, + balance: formattedBalance ?? '', + accountType: account.accountType, + decimals: accountDecimals, + }; + const options: CoinmarketAccountOptionsGroupOptionProps[] = + !skipNotNativeNetworkSymbols.includes(network.symbol) ? [option] : []; // add crypto tokens to options if (tokens && tokens.length > 0) { @@ -426,7 +444,7 @@ export const coinmarketBuildAccountOptions = ({ }); }); - return groups; + return groups.filter(group => group.options.length > 0); }; export const coinmarketGetAmountLabels = ({ diff --git a/packages/suite/src/utils/wallet/exportTransactionsUtils.ts b/packages/suite/src/utils/wallet/exportTransactionsUtils.ts index 2907473bda83..3e0a716b826d 100644 --- a/packages/suite/src/utils/wallet/exportTransactionsUtils.ts +++ b/packages/suite/src/utils/wallet/exportTransactionsUtils.ts @@ -5,7 +5,7 @@ import { fromWei } from 'web3-utils'; import { FiatCurrencyCode } from '@suite-common/suite-config'; import { trezorLogo } from '@suite-common/suite-constants'; import { TokenDefinitions, getIsPhishingTransaction } from '@suite-common/token-definitions'; -import { NetworkSymbol } from '@suite-common/wallet-config'; +import { getNetworkDisplaySymbol, NetworkSymbol } from '@suite-common/wallet-config'; import { ExportFileType, RatesByTimestamps, @@ -186,11 +186,13 @@ const prepareContent = ( const targetData = { ...sharedData, fee: !hasFeeBeenAlreadyUsed ? t.fee : '', // fee only once per tx - feeSymbol: !hasFeeBeenAlreadyUsed ? symbol.toUpperCase() : '', + feeSymbol: !hasFeeBeenAlreadyUsed + ? getNetworkDisplaySymbol(data.symbol) + : '', address: target.isAddress ? target.addresses[0] : '', // SENT - it is destination address, RECV - it is MY address label: target.isAddress && target.metadataLabel ? target.metadataLabel : '', amount: target.isAddress ? target.amount : '', - symbol: target.isAddress ? symbol.toUpperCase() : '', + symbol: target.isAddress ? getNetworkDisplaySymbol(data.symbol) : '', fiat: target.isAddress && target.amount && historicRate ? localizeNumber( @@ -227,7 +229,9 @@ const prepareContent = ( const tokenData = { ...sharedData, fee: !hasFeeBeenAlreadyUsed ? t.fee : '', // fee only once per tx - feeSymbol: !hasFeeBeenAlreadyUsed ? symbol.toUpperCase() : '', + feeSymbol: !hasFeeBeenAlreadyUsed + ? getNetworkDisplaySymbol(data.symbol) + : '', address: token.to || '', // SENT - it is destination address, RECV - it is MY address label: '', // token transactions do not have labels amount: token.amount, // TODO: what to show if token.decimals missing so amount is not formatted correctly? @@ -256,11 +260,13 @@ const prepareContent = ( const internalTransferData = { ...sharedData, fee: !hasFeeBeenAlreadyUsed ? t.fee : '', // fee only once per tx - feeSymbol: !hasFeeBeenAlreadyUsed ? symbol.toUpperCase() : '', + feeSymbol: !hasFeeBeenAlreadyUsed + ? getNetworkDisplaySymbol(data.symbol) + : '', address: internal.to || '', // SENT - it is destination address, RECV - it is MY address label: '', // internal transactions do not have labels amount: internal.amount, - symbol: symbol.toUpperCase(), // if symbol not available, use contract address + symbol: getNetworkDisplaySymbol(data.symbol), // if symbol not available, use contract address fiat: internal.amount && historicRate ? localizeNumber( diff --git a/packages/suite/src/utils/wallet/tokenUtils.ts b/packages/suite/src/utils/wallet/tokenUtils.ts index 137411668bff..3e39ba0e360f 100644 --- a/packages/suite/src/utils/wallet/tokenUtils.ts +++ b/packages/suite/src/utils/wallet/tokenUtils.ts @@ -1,7 +1,12 @@ import { BigNumber } from '@trezor/utils/src/bigNumber'; import { Account, Rate, TokenAddress, RatesByKey } from '@suite-common/wallet-types'; import { TokenInfo } from '@trezor/connect'; -import { getFiatRateKey, isNftToken, isTokenMatchesSearch } from '@suite-common/wallet-utils'; +import { + getFiatRateKey, + isNftMatchesSearch, + isNftToken, + isTokenMatchesSearch, +} from '@suite-common/wallet-utils'; import { NetworkSymbol, getNetworkFeatures } from '@suite-common/wallet-config'; import { FiatCurrencyCode } from '@suite-common/suite-config'; import { @@ -65,16 +70,37 @@ export const formatTokenSymbol = (symbol: string) => { return isTokenSymbolLong ? `${upperCasedSymbol.slice(0, 7)}...` : upperCasedSymbol; }; -export const getTokens = ( - tokens: EnhancedTokenInfo[] | TokenInfo[], - symbol: NetworkSymbol, - coinDefinitions?: TokenDefinition, - searchQuery?: string, -) => { - // filter out NFT tokens until we implement them - const tokensWithoutNFTs = tokens.filter(token => !isNftToken(token)); +type GetTokens = { + tokens: EnhancedTokenInfo[] | TokenInfo[]; + symbol: NetworkSymbol; + tokenDefinitions?: TokenDefinition; + searchQuery?: string; + isNft?: boolean; +}; + +export type GetTokensOutputType = { + shownWithBalance: EnhancedTokenInfo[]; + shownWithoutBalance: EnhancedTokenInfo[]; + hiddenWithBalance: EnhancedTokenInfo[]; + hiddenWithoutBalance: EnhancedTokenInfo[]; + unverifiedWithBalance: EnhancedTokenInfo[]; + unverifiedWithoutBalance: EnhancedTokenInfo[]; +}; - const hasCoinDefinitions = getNetworkFeatures(symbol).includes('coin-definitions'); +export const getTokens = ({ + tokens = [], + symbol, + tokenDefinitions, + searchQuery, + isNft = false, +}: GetTokens): GetTokensOutputType => { + const filteredTokens = isNft + ? tokens.filter(token => isNftToken(token)) + : tokens.filter(token => !isNftToken(token)); + + const hasDefinitions = getNetworkFeatures(symbol).includes( + isNft ? 'nft-definitions' : 'coin-definitions', + ); const shownWithBalance: EnhancedTokenInfo[] = []; const shownWithoutBalance: EnhancedTokenInfo[] = []; @@ -83,16 +109,22 @@ export const getTokens = ( const unverifiedWithBalance: EnhancedTokenInfo[] = []; const unverifiedWithoutBalance: EnhancedTokenInfo[] = []; - tokensWithoutNFTs.forEach(token => { - const isKnown = isTokenDefinitionKnown(coinDefinitions?.data, symbol, token.contract); - const isHidden = coinDefinitions?.hide.includes(token.contract); - const isShown = coinDefinitions?.show.includes(token.contract); + filteredTokens.forEach(token => { + const isKnown = isTokenDefinitionKnown(tokenDefinitions?.data, symbol, token.contract); + const isHidden = tokenDefinitions?.hide.includes(token.contract); + const isShown = tokenDefinitions?.show.includes(token.contract); const query = searchQuery ? searchQuery.trim().toLowerCase() : ''; - if (searchQuery && !isTokenMatchesSearch(token, query)) return; + if ( + searchQuery && + !(isNft ? isNftMatchesSearch(token, query) : isTokenMatchesSearch(token, query)) + ) + return; - const hasBalance = new BigNumber(token?.balance || '0').gt(0); + const hasBalance = + new BigNumber(token?.balance || '0').gt(0) || + (isNft && (token?.multiTokenValues?.length || token?.ids?.length || 0) > 0); const pushToArray = ( arrayWithBalance: EnhancedTokenInfo[], @@ -107,7 +139,7 @@ export const getTokens = ( if (isShown) { pushToArray(shownWithBalance, shownWithoutBalance); - } else if (hasCoinDefinitions && !isKnown) { + } else if (hasDefinitions && !isKnown) { pushToArray(unverifiedWithBalance, unverifiedWithoutBalance); } else if (isHidden) { pushToArray(hiddenWithBalance, hiddenWithoutBalance); diff --git a/packages/suite/src/utils/wallet/utxoSortingUtils.ts b/packages/suite/src/utils/wallet/utxoSortingUtils.ts new file mode 100644 index 000000000000..465bfcc8b221 --- /dev/null +++ b/packages/suite/src/utils/wallet/utxoSortingUtils.ts @@ -0,0 +1,79 @@ +import { UtxoSorting, WalletAccountTransaction } from '@suite-common/wallet-types'; +import type { AccountUtxo } from '@trezor/connect'; +import { BigNumber } from '@trezor/utils'; + +type UtxoSortingFunction = (a: AccountUtxo, b: AccountUtxo) => number; + +type UtxoSortingFunctionWithContext = (context: { + accountTransactions: WalletAccountTransaction[]; +}) => UtxoSortingFunction; + +const performSecondarySorting: UtxoSortingFunction = (a, b) => { + const secondaryComparison = b.txid.localeCompare(a.txid); + if (secondaryComparison === 0) { + return b.vout - a.vout; + } + + return secondaryComparison; +}; + +const wrapSecondarySorting = + (sortFunction: UtxoSortingFunctionWithContext): UtxoSortingFunctionWithContext => + context => + (a, b) => { + const result = sortFunction(context)(a, b); + + if (result !== 0) { + return result; + } + + return performSecondarySorting(a, b); + }; + +const sortFromLargestToSmallest: UtxoSortingFunctionWithContext = () => (a, b) => + new BigNumber(b.amount).comparedTo(new BigNumber(a.amount)); + +const sortFromNewestToOldest: UtxoSortingFunctionWithContext = + ({ accountTransactions }) => + (a, b) => { + if (a.blockHeight > 0 && b.blockHeight > 0) { + return b.blockHeight - a.blockHeight; + } else { + // Pending transactions do not have blockHeight, so we must use blockTime of the transaction instead. + const getBlockTime = (txid: string) => { + const transaction = accountTransactions.find( + transaction => transaction.txid === txid, + ); + + return transaction?.blockTime ?? 0; + }; + + return getBlockTime(b.txid) - getBlockTime(a.txid); + } + }; + +const utxoSortMap: Record = { + largestFirst: wrapSecondarySorting(sortFromLargestToSmallest), + smallestFirst: + context => + (...params) => + wrapSecondarySorting(sortFromLargestToSmallest)(context)(...params) * -1, + + newestFirst: wrapSecondarySorting(sortFromNewestToOldest), + oldestFirst: + context => + (...params) => + wrapSecondarySorting(sortFromNewestToOldest)(context)(...params) * -1, +}; + +export const sortUtxos = ( + utxos: AccountUtxo[], + utxoSorting: UtxoSorting | undefined, + accountTransactions: WalletAccountTransaction[], +): AccountUtxo[] => { + if (utxoSorting === undefined) { + return utxos; + } + + return [...utxos].sort(utxoSortMap[utxoSorting]({ accountTransactions })); +}; diff --git a/packages/suite/src/views/dashboard/AssetsView/AssetCard/AssetCard.tsx b/packages/suite/src/views/dashboard/AssetsView/AssetCard/AssetCard.tsx index 024501232433..2c29351440da 100644 --- a/packages/suite/src/views/dashboard/AssetsView/AssetCard/AssetCard.tsx +++ b/packages/suite/src/views/dashboard/AssetsView/AssetCard/AssetCard.tsx @@ -138,7 +138,11 @@ export const AssetCard = ({ ); return ( - + {!failed ? ( - + )} {!isTestnet(symbol) && ( - + - } flex="0"> + } + flex="0" + > - } flex="0"> + } + flex="0" + > diff --git a/packages/suite/src/views/dashboard/AssetsView/AssetCoinName.tsx b/packages/suite/src/views/dashboard/AssetsView/AssetCoinName.tsx index 5fd26a231994..01fff5346323 100644 --- a/packages/suite/src/views/dashboard/AssetsView/AssetCoinName.tsx +++ b/packages/suite/src/views/dashboard/AssetsView/AssetCoinName.tsx @@ -17,7 +17,7 @@ export const AssetCoinName = ({ network }: AssetCoinNameProps) => { return ( - {name} + {name} {selectedAccounts.length} ); diff --git a/packages/suite/src/views/dashboard/AssetsView/AssetTable/AssetRow.tsx b/packages/suite/src/views/dashboard/AssetsView/AssetTable/AssetRow.tsx index ef90a0696e02..11e0adbf50e6 100644 --- a/packages/suite/src/views/dashboard/AssetsView/AssetTable/AssetRow.tsx +++ b/packages/suite/src/views/dashboard/AssetsView/AssetTable/AssetRow.tsx @@ -101,7 +101,7 @@ export const AssetRow = memo( return ( <> - +
@@ -151,17 +151,19 @@ export const AssetRow = memo( )} - + {!isTestnet(symbol) && } - {!isTestnet(symbol) && } + + {!isTestnet(symbol) && } + {!isTestnet(symbol) && ( )} diff --git a/packages/suite/src/views/dashboard/AssetsView/AssetsView.tsx b/packages/suite/src/views/dashboard/AssetsView/AssetsView.tsx index f5133732a328..201159568b3a 100644 --- a/packages/suite/src/views/dashboard/AssetsView/AssetsView.tsx +++ b/packages/suite/src/views/dashboard/AssetsView/AssetsView.tsx @@ -167,6 +167,7 @@ export const AssetsView = () => { return ( diff --git a/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts b/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts index 3dae8884a231..014e91c8f70a 100644 --- a/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts +++ b/packages/suite/src/views/dashboard/AssetsView/assetsViewUtils.ts @@ -21,9 +21,13 @@ export const handleTokensAndStakingData = ( currentFiatRates?: RatesByKey, ) => { const assetStakingBalance = accountsThatStaked.reduce((total, account) => { - return total.plus(getAccountTotalStakingBalance(account)); + return total.plus(getAccountTotalStakingBalance(account) ?? '0'); }, new BigNumber(0)); - const tokens = getTokens(assetTokens ?? [], symbol, coinDefinitions); + const tokens = getTokens({ + tokens: assetTokens ?? [], + symbol, + tokenDefinitions: coinDefinitions, + }); const tokensWithRates = enhanceTokensWithRates( tokens.shownWithBalance ?? [], localCurrency, diff --git a/packages/suite/src/views/dashboard/PortfolioCard/DashboardGraph.tsx b/packages/suite/src/views/dashboard/PortfolioCard/DashboardGraph.tsx index 5a2f2c741038..49b22414d12c 100644 --- a/packages/suite/src/views/dashboard/PortfolioCard/DashboardGraph.tsx +++ b/packages/suite/src/views/dashboard/PortfolioCard/DashboardGraph.tsx @@ -46,7 +46,7 @@ type DashboardGraphProps = { }; export const DashboardGraph = memo(({ accounts }: DashboardGraphProps) => { - const { error, isLoading, selectedRange } = useSelector(state => state.wallet.graph); + const graph = useSelector(state => state.wallet.graph); const selectedDevice = useSelector(selectSelectedDevice); const localCurrency = useSelector(selectLocalCurrency); const dispatch = useDispatch(); @@ -56,7 +56,7 @@ export const DashboardGraph = memo(({ accounts }: DashboardGraphProps) => { const [xTicks, setXticks] = useState([]); const selectedDeviceState = selectedDevice?.state?.staticSessionId; - const failedAccounts = error?.filter(a => a.deviceState === selectedDeviceState); + const failedAccounts = graph.error?.filter(a => a.deviceState === selectedDeviceState); const allFailed = failedAccounts && failedAccounts.every(fa => accounts.some(a => a.descriptor === fa.descriptor)); @@ -89,23 +89,23 @@ export const DashboardGraph = memo(({ accounts }: DashboardGraphProps) => { ); useEffect(() => { - if (!isLoading) { + if (!graph.isLoading) { const worker = new GraphWorker(); setIsProcessing(true); - const rawData = dispatch(getGraphDataForInterval({ deviceState: selectedDeviceState })); + const rawData = getGraphDataForInterval({ deviceState: selectedDeviceState, graph }); worker.postMessage({ history: rawData, - groupBy: selectedRange.groupBy, + groupBy: graph.selectedRange.groupBy, type: 'dashboard', }); const handleMessage = (event: MessageEvent) => { const aggregatedData = event.data; const graphTicks = - selectedRange.label === 'all' + graph.selectedRange.label === 'all' ? calcTicksFromData(aggregatedData).map(getUnixTime) - : calcTicks(selectedRange.startDate, selectedRange.endDate).map( + : calcTicks(graph.selectedRange.startDate, graph.selectedRange.endDate).map( getUnixTime, ); @@ -121,7 +121,7 @@ export const DashboardGraph = memo(({ accounts }: DashboardGraphProps) => { worker.terminate(); }; } - }, [dispatch, isLoading, selectedDeviceState, selectedRange]); + }, [graph, selectedDeviceState]); return ( @@ -138,12 +138,12 @@ export const DashboardGraph = memo(({ accounts }: DashboardGraphProps) => { hideToolbar variant="all-assets" onRefresh={onRefresh} - isLoading={isLoading || isProcessing} + isLoading={graph.isLoading || isProcessing} localCurrency={localCurrency} xTicks={xTicks} minMaxValues={minMaxValues} data={data} - selectedRange={selectedRange} + selectedRange={graph.selectedRange} receivedValueFn={receivedValueFn} sentValueFn={sentValueFn} balanceValueFn={balanceValueFn} diff --git a/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCard.tsx b/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCard.tsx index 126e5508509e..b91fcada5bdc 100644 --- a/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCard.tsx +++ b/packages/suite/src/views/dashboard/PortfolioCard/PortfolioCard.tsx @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { Dropdown, Card, Tooltip, Column } from '@trezor/components'; import { spacings } from '@trezor/theme'; -import { getTotalFiatBalance } from '@suite-common/wallet-utils'; import { selectCurrentFiatRates } from '@suite-common/wallet-core'; import { hasBitcoinOnlyFirmware } from '@trezor/device-utils'; @@ -15,6 +14,7 @@ import { useFastAccounts } from 'src/hooks/wallet'; import { goto } from 'src/actions/suite/routerActions'; import { setFlag } from 'src/actions/suite/suiteActions'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; +import { useTotalFiatBalance } from 'src/hooks/wallet/useTotalFiatBalance'; import { PortfolioCardHeader } from './PortfolioCardHeader'; import { PortfolioCardException } from './PortfolioCardException'; @@ -48,11 +48,7 @@ export const PortfolioCard = memo(() => { const { device } = useDevice(); const isDeviceEmpty = useMemo(() => accounts.every(a => a.empty), [accounts]); - const fiatAmount = getTotalFiatBalance({ - deviceAccounts: accounts, - localCurrency, - rates: currentFiatRates, - }).toString(); + const walletBalance = useTotalFiatBalance(accounts, localCurrency, currentFiatRates); const discoveryStatus = getDiscoveryStatus(); @@ -148,7 +144,7 @@ export const PortfolioCard = memo(() => { {actions.map(a => ( + + + ); +}; diff --git a/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx b/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx index a49284d76413..19c0836dfe54 100644 --- a/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx +++ b/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx @@ -21,6 +21,7 @@ import { TriggerHighlight } from './TriggerHighlight'; import { Backends } from './Backends'; import { PreField } from './PreField'; import { Tor } from './Tor'; +import { Metadata } from './Metadata'; export const SettingsDebug = () => { const flags = useSelector(selectSuiteFlags); @@ -76,6 +77,9 @@ export const SettingsDebug = () => { {JSON.stringify(flags)} + + + ); }; diff --git a/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx b/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx index d13a60e79edf..2972d683f3f7 100644 --- a/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx +++ b/packages/suite/src/views/settings/SettingsGeneral/SettingsGeneral.tsx @@ -5,7 +5,6 @@ import { SettingsLayout, SettingsSection } from 'src/components/settings'; import { Translation } from 'src/components/suite'; import { useLayoutSize, useSelector } from 'src/hooks/suite'; import { - selectHasExperimentalFeature, selectIsSettingsDesktopAppPromoBannerShown, selectTorState, } from 'src/reducers/suite/suiteReducer'; @@ -30,7 +29,6 @@ import { DesktopSuiteBanner } from './DesktopSuiteBanner'; import { AddressDisplay } from './AddressDisplay'; import { EnableViewOnly } from './EnableViewOnly'; import { Experimental } from './Experimental'; -import { TorSnowflake } from './TorSnowflake'; import { AutomaticUpdate } from './AutomaticUpdate'; import { AutoStart } from './AutoStart'; import { ShowOnTray } from './ShowOnTray'; @@ -45,9 +43,6 @@ export const SettingsGeneral = () => { const desktopUpdate = useSelector(state => state.desktopUpdate); const metadata = useSelector(state => state.metadata); const { isMobileLayout } = useLayoutSize(); - const torSnowflakeExperimentalFeature = useSelector( - selectHasExperimentalFeature('tor-snowflake'), - ); const hasBitcoinNetworks = enabledNetworks.some(symbol => { const networkFeatures = getNetwork(symbol).features; @@ -85,7 +80,6 @@ export const SettingsGeneral = () => { } icon="torBrowser"> {isDesktop() && } {isTorEnabled && } - {isDesktop() && torSnowflakeExperimentalFeature && } )} diff --git a/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx b/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx deleted file mode 100644 index acb15cff5dd1..000000000000 --- a/packages/suite/src/views/settings/SettingsGeneral/TorSnowflake.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ChangeEventHandler, useEffect, useState } from 'react'; - -import styled from 'styled-components'; - -import { TorSettings } from '@trezor/suite-desktop-api/src/messages'; -import { TOR_SNOWFLAKE_KB_URL } from '@trezor/urls'; -import { breakpointMediaQueries } from '@trezor/styles'; -import { desktopApi } from '@trezor/suite-desktop-api'; -import { Button, Input } from '@trezor/components'; -import { isFullPath } from '@trezor/utils'; -import { spacingsPx } from '@trezor/theme'; - -import { selectTorState } from 'src/reducers/suite/suiteReducer'; -import { useSelector, useTranslation } from 'src/hooks/suite'; -import { ActionColumn, SectionItem, TextColumn, Translation } from 'src/components/suite'; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - gap: ${spacingsPx.sm}; - min-width: 200px; - - ${breakpointMediaQueries.below_sm} { - min-width: 100%; - } -`; - -export const TorSnowflake = () => { - const { isTorEnabled } = useSelector(selectTorState); - const [torSettings, setTorSettings] = useState(null); - const [hasPathChanged, setHasPathChanged] = useState(false); - const [error, setError] = useState(null); - const { translationString } = useTranslation(); - - useEffect(() => { - const fetchTorSettings = async () => { - const result = await desktopApi.getTorSettings(); - if (result.success) { - setTorSettings(result.payload); - } else { - setError(result.error); - } - }; - - fetchTorSettings(); - - const handleTorSettingsChange = (settings: TorSettings) => setTorSettings(settings); - desktopApi.on('tor/settings', handleTorSettingsChange); - - return () => { - desktopApi.removeAllListeners('tor/settings'); - }; - }, []); - - const handleChange: ChangeEventHandler = ({ target: { value } }) => { - if (!torSettings) return; - - setHasPathChanged(true); - if (!isFullPath(value) && value !== '') { - setError(translationString('TR_TOR_CONFIG_SNOWFLAKE_ERROR_PATH')); - } else { - setError(null); - } - setTorSettings(prevSettings => ({ - ...prevSettings!, - snowflakeBinaryPath: value, - })); - }; - - const handleClick = async () => { - if (!torSettings || error) return; - - await desktopApi.changeTorSettings({ - ...torSettings, - snowflakeBinaryPath: torSettings.snowflakeBinaryPath, - }); - setHasPathChanged(false); - }; - - const isUpdateDisabled = - !torSettings || - !!error || - (!isFullPath(torSettings.snowflakeBinaryPath) && torSettings.snowflakeBinaryPath !== '') || - isTorEnabled || - !hasPathChanged; - - if (!torSettings) return null; - - const buttonTranslationId = - torSettings.snowflakeBinaryPath === '' && hasPathChanged - ? 'TR_TOR_CONFIG_SNOWFLAKE_DISABLE_LABEL' - : 'TR_TOR_CONFIG_SNOWFLAKE_UPDATE_LABEL'; - - return ( - - } - description={} - buttonLink={TOR_SNOWFLAKE_KB_URL} - /> - - - - - - - - ); -}; diff --git a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx index 554438a52f22..8e1746bce3c4 100644 --- a/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx +++ b/packages/suite/src/views/suite/SwitchDevice/DeviceItem/WalletInstance.tsx @@ -7,8 +7,8 @@ import { selectSelectedDevice, createDiscoveryThunk, } from '@suite-common/wallet-core'; -import { Card, Icon, Tooltip, Row, Column, Text, Divider } from '@trezor/components'; -import { getAllAccounts, getTotalFiatBalance } from '@suite-common/wallet-utils'; +import { Card, Icon, Tooltip, Row, Column, Text, Divider, Box } from '@trezor/components'; +import { getAllAccounts } from '@suite-common/wallet-utils'; import { spacings, negativeSpacings } from '@trezor/theme'; import { WalletLabeling, Translation, MetadataLabeling } from 'src/components/suite'; @@ -19,6 +19,7 @@ import { METADATA_LABELING } from 'src/actions/suite/constants'; import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer'; import { FiatHeader } from 'src/components/wallet/FiatHeader'; import { redirectAfterWalletSelectedThunk } from 'src/actions/wallet/addWalletThunk'; +import { useTotalFiatBalance } from 'src/hooks/wallet/useTotalFiatBalance'; import { useWalletLabeling } from '../../../../components/suite/labeling/WalletLabeling'; import { EjectConfirmation, EjectConfirmationDisableViewOnly } from './EjectConfirmation'; @@ -55,11 +56,9 @@ export const WalletInstance = ({ const { defaultAccountLabelString } = useWalletLabeling(); const deviceAccounts = getAllAccounts(instance.state, accounts); - const instanceBalance = getTotalFiatBalance({ - deviceAccounts, - localCurrency, - rates: currentFiatRates, - }); + + const walletBalance = useTotalFiatBalance(deviceAccounts, localCurrency, currentFiatRates); + const isSelected = enabled && selected && !!discoveryProcess; const { walletLabel } = useSelector(state => selectLabelingDataForWallet(state, instance.state), @@ -102,93 +101,112 @@ export const WalletInstance = ({ contentType === 'disabling-view-only-ejects-wallet'; return ( - - - - - {discoveryProcess ? ( - - {!instance.useEmptyPassphrase && ( - } - > - - - )} - {instance.state?.staticSessionId ? ( - - ) : ( - - )} - - ) : ( - - )} - - - - - - - - {(isViewOnlyRendered || - isEjectConfirmationRendered || - isDisablingViewOnlyEjectsWalletRendered) && ( - - )} - - {isViewOnlyRendered && } - {isEjectConfirmationRendered && ( - - )} - {isDisablingViewOnlyEjectsWalletRendered && ( - - )} - + + + + + + {discoveryProcess ? ( + + {!instance.useEmptyPassphrase && ( + + } + > + + + )} + {instance.state?.staticSessionId ? ( + + ) : ( + + )} + + ) : ( + + )} + + + + + + + + + + {(isViewOnlyRendered || + isEjectConfirmationRendered || + isDisablingViewOnlyEjectsWalletRendered) && ( + + )} + + {isViewOnlyRendered && ( + + )} + {isEjectConfirmationRendered && ( + + )} + {isDisablingViewOnlyEjectsWalletRendered && ( + + )} + + ); }; diff --git a/packages/suite/src/views/wallet/coinmarket/buy/CoinmarketBuyOffers.tsx b/packages/suite/src/views/wallet/coinmarket/buy/CoinmarketBuyOffers.tsx index d1b57f64805d..c27a5d0f323d 100644 --- a/packages/suite/src/views/wallet/coinmarket/buy/CoinmarketBuyOffers.tsx +++ b/packages/suite/src/views/wallet/coinmarket/buy/CoinmarketBuyOffers.tsx @@ -18,5 +18,7 @@ const CoinmarketBuyOffersComponent = ({ selectedAccount }: UseCoinmarketProps) = }; export const CoinmarketBuyOffers = () => ( - + + + ); diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketAddressOptions.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketAddressOptions.tsx index 6d0785b722ac..7c2a07b9ea7b 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketAddressOptions.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketAddressOptions.tsx @@ -15,7 +15,10 @@ import { useAccountAddressDictionary } from 'src/hooks/wallet/useAccounts'; import { selectLabelingDataForAccount } from 'src/reducers/suite/metadataReducer'; import { useSelector } from 'src/hooks/suite'; import { CoinmarketBalance } from 'src/views/wallet/coinmarket/common/CoinmarketBalance'; -import { getCoinmarketNetworkDecimals } from 'src/utils/wallet/coinmarket/coinmarketUtils'; +import { + getCoinmarketNetworkDecimals, + getCoinmarketNetworkDisplaySymbol, +} from 'src/utils/wallet/coinmarket/coinmarketUtils'; import { useCoinmarketInfo } from 'src/hooks/wallet/coinmarket/useCoinmarketInfo'; import { isCoinmarketExchangeContext } from 'src/utils/wallet/coinmarket/coinmarketTypingUtils'; import { useCoinmarketFormContext } from 'src/hooks/wallet/coinmarket/form/useCoinmarketCommonForm'; @@ -111,6 +114,10 @@ export const CoinmarketAddressOptions =
@@ -120,7 +127,7 @@ export const CoinmarketAddressOptions = diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketBalance.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketBalance.tsx index 457de287800a..2ce099db5e07 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketBalance.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketBalance.tsx @@ -14,8 +14,8 @@ import { interface CoinmarketBalanceProps { balance: string | undefined; - cryptoSymbolLabel: string | undefined; symbol: NetworkSymbol; + displaySymbol: string | undefined; tokenAddress?: TokenAddress | undefined; showOnlyAmount?: boolean; amountInCrypto?: boolean; @@ -24,15 +24,15 @@ interface CoinmarketBalanceProps { export const CoinmarketBalance = ({ balance, // expects a value in full units (BTC not sats) - cryptoSymbolLabel, symbol, + displaySymbol, tokenAddress, showOnlyAmount, amountInCrypto, sendCryptoSelect, }: CoinmarketBalanceProps) => { const { shouldSendInSats } = useBitcoinAmountUnit(symbol); - const balanceCurrency = coinmarketGetAccountLabel(cryptoSymbolLabel ?? '', shouldSendInSats); + const balanceCurrency = coinmarketGetAccountLabel(displaySymbol ?? '', shouldSendInSats); const networkDecimals = getCoinmarketNetworkDecimals({ sendCryptoSelect, network: getNetwork(symbol), @@ -56,7 +56,7 @@ export const CoinmarketBalance = ({ ≈{' '} {!amountInCrypto ? ( - {formattedBalance} {cryptoSymbolLabel} + {formattedBalance} {balanceCurrency} ) : ( stringBalance && diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketCryptoAmount.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketCryptoAmount.tsx index 9b202339058d..e97453847156 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketCryptoAmount.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketCryptoAmount.tsx @@ -8,6 +8,7 @@ import { FormattedCryptoAmount } from 'src/components/suite'; import { CoinmarketCoinLogo } from 'src/views/wallet/coinmarket/common/CoinmarketCoinLogo'; import { useCoinmarketInfo } from 'src/hooks/wallet/coinmarket/useCoinmarketInfo'; import { CoinmarketTestWrapper } from 'src/views/wallet/coinmarket'; +import { getCoinmarketNetworkDisplaySymbol } from 'src/utils/wallet/coinmarket/coinmarketUtils'; const LogoWrapper = styled.div` line-height: 0; @@ -25,7 +26,7 @@ export const CoinmarketCryptoAmount = ({ displayLogo, }: CoinmarketCryptoAmountProps) => { const { cryptoIdToCoinSymbol } = useCoinmarketInfo(); - const symbol = cryptoIdToCoinSymbol(cryptoId); + const symbol = cryptoIdToCoinSymbol(cryptoId)?.toLowerCase(); if (!amount || amount === '') { return ( @@ -35,7 +36,7 @@ export const CoinmarketCryptoAmount = ({ )} - {symbol} + {symbol ? getCoinmarketNetworkDisplaySymbol(symbol) : ''} ); } diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputAccount.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputAccount.tsx index 4b78d3b6eaca..5e8ba0a1aa64 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputAccount.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputAccount.tsx @@ -96,7 +96,7 @@ export const CoinmarketFormInputAccount = < balance={fiatValues.accountBalance} symbol={fiatValues.symbol} tokenAddress={fiatValues.tokenAddress} - cryptoSymbolLabel={selectedOption?.label} + displaySymbol={selectedOption?.label} sendCryptoSelect={selectedOption} /> ) diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputCryptoSelect.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputCryptoSelect.tsx index fc332a908b3c..0a2168c45ee2 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputCryptoSelect.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputCryptoSelect.tsx @@ -155,7 +155,7 @@ export const CoinmarketFormInputCryptoSelect = < }; const getNetworks = () => { - const networksToSelect: NetworkSymbol[] = ['eth', 'sol', 'pol', 'bnb']; + const networksToSelect: NetworkSymbol[] = ['eth', 'sol', 'pol', 'bsc']; const networkKeys = networkSymbolCollection.filter(item => networksToSelect.includes(item)); const networksSelected: NetworkFilterCategory[] = networkKeys.map(networkKey => { const network = getNetwork(networkKey); diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputCryptoAmount.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputCryptoAmount.tsx index 71f0726c727a..fb94417b6897 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputCryptoAmount.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputCryptoAmount.tsx @@ -26,6 +26,7 @@ import { import { coinmarketGetAccountLabel, getCoinmarketNetworkDecimals, + getCoinmarketNetworkDisplaySymbol, } from 'src/utils/wallet/coinmarket/coinmarketUtils'; import { FORM_OUTPUT_AMOUNT, @@ -73,6 +74,10 @@ export const CoinmarketFormInputCryptoAmount = )?.outputs?.[0]?.amount : (errors as FieldErrors).cryptoInput; const symbol = cryptoSelect?.value && cryptoIdToCoinSymbol(cryptoSelect?.value); + const displaySymbol = coinmarketGetAccountLabel( + getCoinmarketNetworkDisplaySymbol(symbol ?? ''), + shouldSendInSats, + ); const decimals = getCoinmarketNetworkDecimals({ sendCryptoSelect: !isCoinmarketBuyContext(context) ? context.getValues()[FORM_SEND_CRYPTO_CURRENCY_SELECT] @@ -129,14 +134,7 @@ export const CoinmarketFormInputCryptoAmount = - {coinmarketGetAccountLabel( - cryptoSelect?.value && symbol ? symbol : '', - shouldSendInSats, - )} - - } + innerAddon={<>{displaySymbol}} data-testid="@coinmarket/form/crypto-input" /> ); diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputFiatCrypto.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputFiatCrypto.tsx index 048c748b2ebf..e79d6bbf7a5f 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputFiatCrypto.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputFiatCrypto.tsx @@ -6,7 +6,10 @@ import { CoinmarketSellFormProps, } from 'src/types/coinmarket/coinmarketForm'; import { CoinmarketFormSwitcherCryptoFiat } from 'src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormSwitcherCryptoFiat'; -import { coinmarketGetAmountLabels } from 'src/utils/wallet/coinmarket/coinmarketUtils'; +import { + coinmarketGetAmountLabels, + getCoinmarketNetworkDisplaySymbol, +} from 'src/utils/wallet/coinmarket/coinmarketUtils'; import { CoinmarketFormInputCryptoAmount } from 'src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputCryptoAmount'; import { useCoinmarketInfo } from 'src/hooks/wallet/coinmarket/useCoinmarketInfo'; import { CoinmarketFormInputFiat } from 'src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormInputFiatCrypto/CoinmarketFormInputFiat'; @@ -38,6 +41,11 @@ export const CoinmarketFormInputFiatCrypto = < } = formProps; const { amountInCrypto } = methods.getValues(); const amountLabels = coinmarketGetAmountLabels({ type, amountInCrypto }); + const coinSymbol = + !amountInCrypto && cryptoCurrencyLabel + ? cryptoIdToCoinSymbol(cryptoCurrencyLabel) + : currencySelectLabel ?? ''; + const displaySymbol = coinSymbol && getCoinmarketNetworkDisplaySymbol(coinSymbol); const inputProps = { cryptoInputName, @@ -47,11 +55,7 @@ export const CoinmarketFormInputFiatCrypto = < labelLeft: showLabel ? : undefined, labelRight: showLabel ? ( diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormSwitcherCryptoFiat.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormSwitcherCryptoFiat.tsx index 69429d30cb6f..caed420ba86c 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormSwitcherCryptoFiat.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInput/CoinmarketFormSwitcherCryptoFiat.tsx @@ -3,13 +3,14 @@ import { TextButton } from '@trezor/components'; import { Translation } from 'src/components/suite'; interface CoinmarketFormSwitcherCryptoFiatProps { - symbol?: string; + // displaySymbol or fiat currency + currency?: string; isDisabled: boolean; toggleAmountInCrypto: () => void; } export const CoinmarketFormSwitcherCryptoFiat = ({ - symbol, + currency, isDisabled, toggleAmountInCrypto, }: CoinmarketFormSwitcherCryptoFiatProps) => ( @@ -24,7 +25,7 @@ export const CoinmarketFormSwitcherCryptoFiat = ({ diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx index 00314d88f341..433c02c1f17b 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormInputs.tsx @@ -85,7 +85,7 @@ export const CoinmarketFormInputs = () => { /> { /> ( - + diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOffer.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOffer.tsx index 20a628267849..cf99cc847b97 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOffer.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOffer.tsx @@ -76,7 +76,7 @@ export const CoinmarketFormOffer = () => { return ( - + {shouldDisplayFiatAmount ? ( { const { cryptoIdToCoinSymbol } = useCoinmarketInfo(); - const symbol = cryptoIdToCoinSymbol(cryptoId); + const coinSymbol = cryptoIdToCoinSymbol(cryptoId)?.toLowerCase(); // lowercase - possible can be a NetworkSymbol - if (!symbol) { + if (!coinSymbol) { return; } return ( - + diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOfferItem.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOfferItem.tsx index fa605f4c6457..f21be1c77dd7 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOfferItem.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketForm/CoinmarketFormOfferItem.tsx @@ -30,6 +30,7 @@ export const CoinmarketFormOfferItem = ({ justifyContent="center" margin={{ vertical: spacings.xs }} gap={spacings.sm} + data-testid="@coinmarket/offers/loading-spinner" > diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketLayout/CoinmarketLayout.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketLayout/CoinmarketLayout.tsx index a39f2c67c1ae..dacdfb7c698e 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketLayout/CoinmarketLayout.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketLayout/CoinmarketLayout.tsx @@ -13,7 +13,7 @@ export const CoinmarketLayout = ({ children }: CoinmarketLayoutProps) => { const routeName = useSelector(selectRouteName); return ( - + {!routeName?.includes(`wallet-coinmarket-exchange`) && ( )} diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketOffers/CoinmarketOffersItem.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketOffers/CoinmarketOffersItem.tsx index a78fa78e524a..c9257b8e3020 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketOffers/CoinmarketOffersItem.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketOffers/CoinmarketOffersItem.tsx @@ -107,7 +107,10 @@ export const CoinmarketOffersItem = ({ quote }: CoinmarketOffersItemProps) => { if (!cryptoAmountProps) return null; return ( - + @@ -128,7 +131,7 @@ export const CoinmarketOffersItem = ({ quote }: CoinmarketOffersItemProps) => { /> - + {isCoinmarketExchangeContext(context) && ( {receiveCurrency && } - {contractAddress && network ? ( + {contractAddress && displaySymbol ? ( ) : ( - cryptoIdToCoinSymbol(receiveCurrency!) + tokenName )} diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferBuy/CoinmarketOfferBuy.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferBuy/CoinmarketOfferBuy.tsx index d10e8151f6f8..45d8e66fd3f1 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferBuy/CoinmarketOfferBuy.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferBuy/CoinmarketOfferBuy.tsx @@ -14,16 +14,16 @@ export const CoinmarketOfferBuy = ({ paymentMethod, paymentMethodName, }: CoinmarketOfferBuyProps) => { - const currency = selectedQuote?.receiveCurrency; - const coinmarketVerifyAccount = useCoinmarketVerifyAccount({ currency }); + const cryptoId = selectedQuote?.receiveCurrency; + const coinmarketVerifyAccount = useCoinmarketVerifyAccount({ cryptoId }); return ( <> - {currency && ( + {cryptoId && ( )} diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferExchange/CoinmarketOfferExchange.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferExchange/CoinmarketOfferExchange.tsx index c79ec7ec1e13..2679542444c7 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferExchange/CoinmarketOfferExchange.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketOfferExchange/CoinmarketOfferExchange.tsx @@ -25,18 +25,18 @@ export const CoinmarketOfferExchange = ({ quoteAmounts, }: CoinmarketOfferExchangeProps) => { const { exchangeStep } = useCoinmarketFormContext(); - const currency = selectedQuote?.receive; - const coinmarketVerifyAccount = useCoinmarketVerifyAccount({ currency }); + const cryptoId = selectedQuote?.receive; + const coinmarketVerifyAccount = useCoinmarketVerifyAccount({ cryptoId }); const steps: CoinmarketSelectedOfferStepperItemProps[] = [ { step: 'RECEIVING_ADDRESS', translationId: 'TR_EXCHANGE_VERIFY_ADDRESS_STEP', isActive: exchangeStep === 'RECEIVING_ADDRESS', - component: currency ? ( + component: cryptoId ? ( ) : null, }, diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketSelectedOffer.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketSelectedOffer.tsx index 3cbba82b8466..a958c6c1a2c5 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketSelectedOffer.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketSelectedOffer.tsx @@ -29,7 +29,7 @@ export const CoinmarketSelectedOffer = () => { const paymentMethod = getPaymentMethod(context.selectedQuote, context); return ( - + {isCoinmarketBuyContext(context) && ( { +export const CoinmarketVerify = ({ coinmarketVerifyAccount, cryptoId }: CoinmarketVerifyProps) => { const dispatch = useDispatch(); const { translationString } = useTranslation(); const { cryptoIdToCoinSymbol, cryptoIdToNativeCoinSymbol } = useCoinmarketInfo(); @@ -59,12 +60,13 @@ export const CoinmarketVerify = ({ coinmarketVerifyAccount, currency }: Coinmark const { accountTooltipTranslationId, addressTooltipTranslationId } = getTranslationIds( selectedAccountOption?.type, ); + const coinSymbol = getCoinmarketNetworkDisplaySymbol(cryptoIdToCoinSymbol(cryptoId) ?? ''); const { ref: networkRef, ...networkField } = form.register('address', { required: translationString('TR_EXCHANGE_RECEIVING_ADDRESS_REQUIRED'), validate: value => { - if (selectedAccountOption?.type === 'NON_SUITE' && currency) { - const symbol = cryptoIdToNativeCoinSymbol(currency); + if (selectedAccountOption?.type === 'NON_SUITE' && cryptoId) { + const symbol = cryptoIdToNativeCoinSymbol(cryptoId); if (value && !addressValidator.validate(value, symbol)) { return translationString('TR_EXCHANGE_RECEIVING_ADDRESS_INVALID'); } @@ -105,11 +107,11 @@ export const CoinmarketVerify = ({ coinmarketVerifyAccount, currency }: Coinmark } diff --git a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketVerify/CoinmarketVerifyOptionsItem.tsx b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketVerify/CoinmarketVerifyOptionsItem.tsx index 54312b8caae9..bc6e13a134b3 100644 --- a/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketVerify/CoinmarketVerifyOptionsItem.tsx +++ b/packages/suite/src/views/wallet/coinmarket/common/CoinmarketSelectedOffer/CoinmarketVerify/CoinmarketVerifyOptionsItem.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { Column, Icon, Row, variables } from '@trezor/components'; import { CoinLogo } from '@trezor/product-components'; import { spacings } from '@trezor/theme'; +import { getNetwork } from '@suite-common/wallet-config'; import { AccountLabeling, Translation } from 'src/components/suite'; import { FORM_SEND_CRYPTO_CURRENCY_SELECT } from 'src/constants/wallet/coinmarket/form'; @@ -44,7 +45,7 @@ export const CoinmarketVerifyOptionsItem = ({ void; + isEmptyCollectionsOpen?: boolean; +}; + +const NftsRow = ({ + nft, + network, + isShown, + selectedAccount, + isEmptyCollection = false, + isEmptyCollectionsOpen = false, +}: NftsRowProps) => { + const dispatch = useDispatch(); + const [isCollectionOpen, setIsCollectionOpen] = useState(false); + const shouldShowCopyAddressModal = useSelector(selectIsCopyAddressModalShown); + const { account } = selectedAccount; + const nftItemsCount = nft.ids?.length || nft.multiTokenValues?.length || 0; + const NftName = ; + + return ( + <> + setIsCollectionOpen(!isCollectionOpen) + } + isCollapsed={isEmptyCollection && !isEmptyCollectionsOpen} + isHighlightedOnHover={!isEmptyCollection} + > + + + + + + , + icon: 'hide', + onClick: () => + dispatch( + tokenDefinitionsActions.setTokenStatus({ + symbol: network.symbol, + contractAddress: nft.contract || '', + status: TokenManagementAction.HIDE, + type: DefinitionType.NFT, + }), + ), + isHidden: !isShown, + }, + { + label: , + icon: 'newspaper', + onClick: () => { + dispatch({ + type: SUITE.SET_TRANSACTION_HISTORY_PREFILL, + payload: nft.contract || '', + }); + if (account) { + dispatch( + goto('wallet-index', { + params: { + symbol: account.symbol, + accountIndex: account.index, + accountType: account.accountType, + }, + }), + ); + } + }, + }, + { + label: , + icon: 'arrowUpRight', + onClick: () => { + window.open( + getNftContractExplorerUrl(network, nft), + '_blank', + ); + }, + }, + ], + }, + { + key: 'contract-address', + label: , + options: [ + { + label: ( + + {nft.contract} + + + ), + onClick: () => { + dispatch( + shouldShowCopyAddressModal + ? showCopyAddressModal( + nft.contract || '', + 'contract', + ) + : copyAddressToClipboard(nft.contract), + ); + }, + }, + ], + }, + ]} + /> + {!isShown && ( + + )} + + + + {nft.type === 'ERC721' && + nft.ids?.map((id, index) => ( + + + + + + + {NftName} + # + + + + e.stopPropagation()} + > + + + + + + + + ))} + {nft.type === 'ERC1155' && + nft.multiTokenValues?.map((value, index) => ( + + + + + + + {NftName} + # + + x + + e.stopPropagation()} + > + + + + + + + + ))} + + ); +}; + +export default NftsRow; diff --git a/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx new file mode 100644 index 000000000000..5bb74a163dc7 --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/NftsTable/NftsTable.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; + +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import { Card, Column, Table } from '@trezor/components'; +import { getNetwork } from '@suite-common/wallet-config'; +import { spacings } from '@trezor/theme'; + +import { GetTokensOutputType } from 'src/utils/wallet/tokenUtils'; +import { Translation } from 'src/components/suite/Translation'; + +import NftsRow from './NftsRow'; +import { DropdownRow } from '../../tokens/DropdownRow'; + +type NftsTableProps = { + selectedAccount: SelectedAccountLoaded; + isShown?: boolean; + verified?: boolean; + nfts: GetTokensOutputType; +}; + +const NftsTable = ({ selectedAccount, isShown, verified, nfts }: NftsTableProps) => { + const { account } = selectedAccount; + const network = getNetwork(account.symbol); + const [isEmptyCollectionsOpen, setIsEmptyCollectionsOpen] = useState(false); + + const getNftsToShow = () => { + if (isShown) { + return nfts.shownWithBalance; + } + + return verified ? nfts.hiddenWithBalance : nfts.unverifiedWithBalance; + }; + + const getNftsWithoutBalance = () => { + if (isShown) { + return nfts.shownWithoutBalance; + } + + return verified ? nfts.hiddenWithoutBalance : nfts.unverifiedWithoutBalance; + }; + + const nftsToShow = getNftsToShow(); + const nftsWithoutBalance = getNftsWithoutBalance(); + + return nftsToShow.length > 0 || nftsWithoutBalance.length > 0 ? ( + + + + + + + + + + + + {nftsToShow.map(nft => ( + + ))} + {nftsWithoutBalance.length !== 0 && ( + <> + + setIsEmptyCollectionsOpen(!isEmptyCollectionsOpen) + } + > + + + + + {nftsWithoutBalance.map(nft => ( + + ))} + + )} + +
+
+
+ ) : null; +}; + +export default NftsTable; diff --git a/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx b/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx new file mode 100644 index 000000000000..344eb14c7196 --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/NftsTablesSection.tsx @@ -0,0 +1,99 @@ +import { Banner, H3, Column } from '@trezor/components'; +import { SelectedAccountLoaded } from '@suite-common/wallet-types'; +import { selectNftDefinitions } from '@suite-common/token-definitions'; +import { spacings } from '@trezor/theme'; + +import { Translation } from 'src/components/suite'; +import { useSelector } from 'src/hooks/suite'; +import { getTokens } from 'src/utils/wallet/tokenUtils'; + +import { NoTokens } from '../tokens/common/NoTokens'; +import NftsTable from './NftsTable/NftsTable'; +import { NoSearchResultsWrapped } from '../tokens/common/TokensTable/TokensTable'; + +type EvmNftsTablesProps = { + selectedAccount: SelectedAccountLoaded; + searchQuery: string; + isShown: boolean; +}; + +export const NftsTablesSection = ({ + selectedAccount, + searchQuery, + isShown = true, +}: EvmNftsTablesProps) => { + const nftDefinitions = useSelector(state => + selectNftDefinitions(state, selectedAccount.account.symbol), + ); + const nfts = getTokens({ + tokens: selectedAccount.account.tokens || [], + symbol: selectedAccount.account.symbol, + tokenDefinitions: nftDefinitions, + isNft: true, + searchQuery, + }); + + const areNoShownNfts = !nfts?.shownWithBalance.length && !nfts?.shownWithoutBalance.length; + + const areNoHiddenNfts = !nfts?.hiddenWithBalance.length && !nfts?.hiddenWithoutBalance.length; + + const areNoUnverifiedNfts = + !nfts?.unverifiedWithBalance.length && !nfts?.unverifiedWithoutBalance.length; + + const hiddenEvmNfts = ( + + +

+ +

+ + + + +
+ ); + + const getNoTokensTitle = () => { + const hasHiddenOrUnverified = + nfts?.hiddenWithBalance.length || + nfts?.hiddenWithoutBalance.length || + nfts?.unverifiedWithBalance.length || + nfts?.unverifiedWithoutBalance.length; + + return hasHiddenOrUnverified ? 'TR_NFT_EMPTY_CHECK_HIDDEN' : 'TR_NFT_EMPTY'; + }; + + if (isShown) { + if (areNoShownNfts) { + if (searchQuery) { + return ; + } + + return } />; + } + + return ( + + ); + } + + if (areNoHiddenNfts && areNoUnverifiedNfts) { + if (searchQuery) { + return ; + } + + return } />; + } + + return hiddenEvmNfts; +}; diff --git a/packages/suite/src/views/wallet/nfts/index.tsx b/packages/suite/src/views/wallet/nfts/index.tsx new file mode 100644 index 000000000000..cdb23a7cd59e --- /dev/null +++ b/packages/suite/src/views/wallet/nfts/index.tsx @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { Column } from '@trezor/components'; +import { spacings } from '@trezor/theme'; + +import { WalletLayout } from 'src/components/wallet'; +import { useDispatch, useSelector } from 'src/hooks/suite'; +import { goto } from 'src/actions/suite/routerActions'; + +import { TokensNavigation } from '../tokens/TokensNavigation'; +import { NftsTablesSection } from './NftsTablesSection'; + +export const Nfts = () => { + const [searchQuery, setSearchQuery] = useState(''); + + const selectedAccount = useSelector(state => state.wallet.selectedAccount); + + const dispatch = useDispatch(); + + useEffect(() => { + if ( + selectedAccount.status === 'loaded' && + !selectedAccount.network?.features.includes('nfts') + ) { + dispatch(goto('wallet-index', { preserveParams: true })); + } + }, [selectedAccount, dispatch]); + + if (selectedAccount.status !== 'loaded') { + return ; + } + + return ( + + + + + + + + + + + + + + ); +}; + +export default Nfts; diff --git a/packages/suite/src/views/wallet/receive/components/Header.tsx b/packages/suite/src/views/wallet/receive/components/Header.tsx index 4d7073d851da..0fb57a19255f 100644 --- a/packages/suite/src/views/wallet/receive/components/Header.tsx +++ b/packages/suite/src/views/wallet/receive/components/Header.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { H2, Paragraph } from '@trezor/components'; +import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; import { Account } from 'src/types/wallet'; import { Translation } from 'src/components/suite'; @@ -15,7 +16,10 @@ interface HeaderProps { export const Header = ({ account }: HeaderProps) => { const title = ( - + ); if (account.networkType === 'bitcoin') { return ( diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx index a7a7d0aadf74..fc3512276d38 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import styled, { useTheme } from 'styled-components'; -import { typography } from '@trezor/theme'; import { COMPOSE_ERROR_TYPES } from '@suite-common/wallet-constants'; import { fetchAllTransactionsForAccountThunk } from '@suite-common/wallet-core'; import { getTxsPerPage } from '@suite-common/suite-utils'; import { amountToSmallestUnit, formatNetworkAmount } from '@suite-common/wallet-utils'; -import { Card, Checkbox, Icon, Switch, variables } from '@trezor/components'; +import { Card, Checkbox, Column, Icon, Row, Switch, Text } from '@trezor/components'; +import { spacings, spacingsPx } from '@trezor/theme'; import { FormattedCryptoAmount, Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; @@ -18,41 +18,13 @@ import { selectCurrentTargetAnonymity } from 'src/reducers/wallet/coinjoinReduce import { selectLabelingDataForSelectedAccount } from 'src/reducers/suite/metadataReducer'; import { filterAndCategorizeUtxos } from 'src/utils/wallet/filterAndCategorizeUtxosUtils'; -import { UtxoSelectionList } from './UtxoSelectionList'; +import { UtxoSortingSelect } from './UtxoSortingSelect'; +import { UtxoSelectionList } from './UtxoSelectionList/UtxoSelectionList'; import { UtxoSearch } from './UtxoSearch'; -const Row = styled.div` - align-items: center; - display: flex; - font-weight: ${variables.FONT_WEIGHT.MEDIUM}; -`; - -const SecondRow = styled(Row)` - border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; - font-size: ${variables.FONT_SIZE.SMALL}; - margin-top: 20px; - padding-bottom: 14px; -`; - -const GreyText = styled.div` - ${typography.hint} - color: ${({ theme }) => theme.textSubdued}; -`; - -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledSwitch = styled(Switch)` - margin: 0 14px 0 auto; -`; - -const AmountWrapper = styled.div` - display: flex; - flex-direction: column; - margin-left: auto; - text-align: right; -`; - -const SearchWrapper = styled.div` - margin-top: 20px; +const Header = styled.header` + border-bottom: 1px solid ${({ theme }) => theme.borderElevation1}; + padding-bottom: ${spacingsPx.sm}; `; const MissingToInput = styled.div<{ $isVisible: boolean }>` @@ -61,18 +33,18 @@ const MissingToInput = styled.div<{ $isVisible: boolean }>` `; const Empty = styled.div` - border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; - margin-bottom: 12px; - padding: 14px 0; + border-bottom: 1px solid ${({ theme }) => theme.borderElevation1}; + margin-bottom: ${spacingsPx.sm}; + padding: ${spacingsPx.sm} 0; `; const StyledPagination = styled(Pagination)` - margin: 20px 0; + margin: ${spacingsPx.lg} 0; `; -interface CoinControlProps { +type CoinControlProps = { close: () => void; -} +}; export const CoinControl = ({ close }: CoinControlProps) => { const [currentPage, setSelectedPage] = useState(1); @@ -197,41 +169,46 @@ export const CoinControl = ({ close }: CoinControlProps) => { return ( - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{hasEligibleUtxos && ( - + - + +
)} {!!spendableUtxosOnPage.length && ( ` `}; `; -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledCheckbox = styled(Checkbox)` - margin-top: ${spacingsPx.xxxs}; - margin-right: ${spacingsPx.xs}; -`; - -const Wrapper = styled.div<{ $isDisabled: boolean }>` +const Wrapper = styled.div<{ $isChecked: boolean; $isDisabled: boolean }>` align-items: flex-start; border-radius: ${borders.radii.xs}; display: flex; @@ -74,19 +69,18 @@ const Wrapper = styled.div<{ $isDisabled: boolean }>` &:hover, &:focus-within { - ${({ $isDisabled }) => + background: ${({ $isDisabled, theme }) => + !$isDisabled && theme.backgroundSurfaceElevation2}; + + ${({ $isChecked, $isDisabled }) => + !$isChecked && !$isDisabled && css` - background: ${({ theme }) => theme.backgroundSurfaceElevation2}; - - ${StyledCheckbox} > :first-child { + ${CheckContainer} { + background: ${({ theme }) => theme.backgroundSurfaceElevation0}; border-color: ${({ theme }) => theme.borderFocus}; } `}; - - ${DetailPartVisibleOnHover} { - opacity: 1; - } } `; @@ -97,17 +91,6 @@ const Body = styled.div` min-width: 0; `; -const Row = styled.div` - align-items: center; - display: flex; - gap: ${spacingsPx.xs}; -`; - -const BottomRow = styled(Row)` - margin-top: 6px; - min-height: 24px; -`; - const Address = styled.div` overflow: hidden; font-variant-numeric: tabular-nums slashed-zero; @@ -130,22 +113,47 @@ const TransactionDetailButton = styled(TextButton)` } `; -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledFluidSpinner = styled(Spinner)` - margin-right: ${spacingsPx.xs}; -`; - const StyledFiatValue = styled(FiatValue)` margin-left: auto; - padding-left: 4px; + padding-left: ${spacingsPx.xxs}; color: ${({ theme }) => theme.textSubdued}; ${typography.hint} `; -interface UtxoSelectionProps { +type ResolveUtxoSpendableProps = { + utxo: AccountUtxo; + coinjoinRegisteredUtxos: AccountUtxo[]; +}; + +// Same as MINIMAL_COINBASE_CONFIRMATIONS in '@trezor/utxo-lib'; It is redeclared here to avoid +// some magic import/export errors. This is very niche stuff and probably never changes. +// Also, this most probably bothers only developers on Regtest. +const MINIMAL_COINBASE_CONFIRMATIONS = 100; + +const resolveUtxoSpendable = ({ + utxo, + coinjoinRegisteredUtxos, +}: ResolveUtxoSpendableProps): ReactNode | null => { + if (utxo.coinbase === true && utxo.confirmations < MINIMAL_COINBASE_CONFIRMATIONS) { + return ( + + ); + } + + if (coinjoinRegisteredUtxos.includes(utxo)) { + return ; + } + + return null; +}; + +type UtxoSelectionProps = { transaction?: WalletAccountTransaction; utxo: AccountUtxo; -} +}; export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { const { @@ -177,7 +185,10 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { const isChecked = isCoinControlEnabled ? selectedUtxos.some(selected => isSameUtxo(selected, utxo)) : composedInputs.some(u => u.prev_hash === utxo.txid && u.prev_index === utxo.vout); - const isDisabled = coinjoinRegisteredUtxos.includes(utxo); + + const unspendableTooltip = resolveUtxoSpendable({ utxo, coinjoinRegisteredUtxos }); + const isDisabled = unspendableTooltip !== null; + const utxoTagIconColor = isDisabled ? theme.legacy.TYPE_LIGHT_GREY : theme.legacy.TYPE_DARK_GREY; @@ -191,17 +202,22 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { }; return ( - - }> - + + - + {isPendingTransaction && ( } @@ -234,7 +250,7 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { /> - + {transaction ? ( ) : ( @@ -242,7 +258,7 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { cursor="pointer" content={} > - + )} @@ -283,7 +299,7 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => { amount={formatNetworkAmount(utxo.amount, account.symbol, false)} symbol={network.symbol} /> - +
); diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoTag.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoTag.tsx similarity index 100% rename from packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoTag.tsx rename to packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoTag.tsx diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx similarity index 88% rename from packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx rename to packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx index e4850aa1b82f..33dfc3b9c7c6 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import styled from 'styled-components'; import { transparentize } from 'polished'; -import { selectAccountTransactionsWithNulls } from '@suite-common/wallet-core'; +import { selectAccountTransactions } from '@suite-common/wallet-core'; import { Icon, variables, IconName } from '@trezor/components'; import type { AccountUtxo } from '@trezor/connect'; import { CSSColor } from '@trezor/theme'; @@ -11,7 +11,7 @@ import { CSSColor } from '@trezor/theme'; import { useSelector } from 'src/hooks/suite'; import { useSendFormContext } from 'src/hooks/wallet'; -import { UtxoSelection } from './UtxoSelection'; +import { UtxoSelection } from './UtxoSelection/UtxoSelection'; const Wrapper = styled.section` border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; @@ -65,9 +65,7 @@ export const UtxoSelectionList = ({ }: UtxoSelectionListProps) => { const { account } = useSendFormContext(); - const accountTransactions = useSelector(state => - selectAccountTransactionsWithNulls(state, account.key), - ); + const accountTransactions = useSelector(state => selectAccountTransactions(state, account.key)); return ( @@ -89,7 +87,7 @@ export const UtxoSelectionList = ({ transaction?.txid === utxo.txid, + transaction => transaction.txid === utxo.txid, )} utxo={utxo} /> diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSortingSelect.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSortingSelect.tsx new file mode 100644 index 000000000000..dbc17db4d165 --- /dev/null +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSortingSelect.tsx @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; + +import { UtxoSorting } from '@suite-common/wallet-types'; +import { Option, Select } from '@trezor/components'; + +import { Translation } from 'src/components/suite'; +import { useSendFormContext } from 'src/hooks/wallet'; + +const sortingOptions: { value: UtxoSorting; label: ReactNode }[] = [ + { value: 'newestFirst', label: }, + { value: 'oldestFirst', label: }, + { value: 'smallestFirst', label: }, + { value: 'largestFirst', label: }, +]; + +export const UtxoSortingSelect = () => { + const { + utxoSelection: { utxoSorting, selectUtxoSorting }, + } = useSendFormContext(); + + const selectedOption = sortingOptions.find(option => option.value === utxoSorting); + + const handleChange = ({ value }: Option) => selectUtxoSorting(value); + + return ( +