diff --git a/.eslintrc.js b/.eslintrc.js index b0f687c23..4efee2f60 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,11 +30,10 @@ module.exports = { 'no-invalid-this': 0, 'react/prop-types': 'off', 'max-len': 'off', // prettier is already handling this automatically, - // note: prod webpack config strips console logs anyway, nevertheless - // we don't want the dev build to be spammed by needless logging '@typescript-eslint/no-explicit-any': ['error'], 'no-console': ['error', { allow: ['warn', 'error', 'info', 'debug'] }], - 'lodash/import-scope': ['error', 'method'] + 'lodash/import-scope': ['error', 'method'], + 'promise/avoid-new': 'off', }, overrides: [ { diff --git a/apps/browser-extension-wallet/.env.developerpreview b/apps/browser-extension-wallet/.env.developerpreview index 59bf74f4b..b92bf9963 100644 --- a/apps/browser-extension-wallet/.env.developerpreview +++ b/apps/browser-extension-wallet/.env.developerpreview @@ -59,6 +59,7 @@ PRODUCTION_MODE_TRACKING=false POSTHOG_DEV_TOKEN_MAINNET=phc_gH96Lx5lEVXTTWEyytSdTFPDk3Xsxwi4BqG88mKObd1 POSTHOG_DEV_TOKEN_PREPROD=phc_Xlmldm6EYSfQVgB9Uxm3b2xC1noDlgFFXpF9AJ6SMfJ POSTHOG_DEV_TOKEN_PREVIEW=phc_e8SaOOWpXpNE59TnpLumeUjWm4iv024AWjhQqU406jr +POSTHOG_DEV_TOKEN_SANCHONET=phc_OUu6sPucDu5S6skRmYbWN5Jn8TpggWTQu1Y1ETkm3xt # Cardano Services CARDANO_SERVICES_URL_MAINNET=https://dev-mainnet.lw.iog.io diff --git a/apps/browser-extension-wallet/manifest.json b/apps/browser-extension-wallet/manifest.json index 2b671e9de..18c96b95b 100644 --- a/apps/browser-extension-wallet/manifest.json +++ b/apps/browser-extension-wallet/manifest.json @@ -1,7 +1,7 @@ { "name": "$WALLET_MANIFEST_NAME", "description": "One fast, accessible, and secure platform for digital assets, DApps, NFTs, and DeFi.", - "version": "1.10.0", + "version": "1.10.2", "manifest_version": 3, "key": "$LACE_EXTENSION_KEY", "icons": { diff --git a/apps/browser-extension-wallet/package.json b/apps/browser-extension-wallet/package.json index 5da2607af..d9fafa054 100644 --- a/apps/browser-extension-wallet/package.json +++ b/apps/browser-extension-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@lace/browser-extension-wallet", - "version": "1.10.0", + "version": "1.10.2", "description": "A fully capable wallet packaged as browser extensions for Chrome, Firefox, and Edge", "homepage": "https://github.com/input-output-hk/lace/blob/master/apps/browser-extension-wallet/README.md", "bugs": { @@ -39,14 +39,14 @@ }, "dependencies": { "@ant-design/icons": "^4.7.0", - "@cardano-sdk/cardano-services-client": "0.18.0", + "@cardano-sdk/cardano-services-client": "0.19.0", "@cardano-sdk/core": "0.30.0", "@cardano-sdk/dapp-connector": "0.12.14", - "@cardano-sdk/input-selection": "0.12.26", - "@cardano-sdk/tx-construction": "0.18.2", + "@cardano-sdk/input-selection": "0.12.27", + "@cardano-sdk/tx-construction": "0.18.3", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.37.0", + "@cardano-sdk/web-extension": "0.27.0", "@emurgo/cip14-js": "~3.0.1", "@koralabs/handles-public-api-interfaces": "^1.6.6", "@lace/cardano": "0.1.0", @@ -59,7 +59,7 @@ "@vespaiach/axios-fetch-adapter": "^0.3.0", "antd": "^4.24.10", "are-you-es5": "^2.1.2", - "axios": "0.21.4", + "axios": "0.28.0", "bignumber.js": "9.0.1", "bip39": "^3.0.4", "blake2b-no-wasm": "2.1.4", diff --git a/apps/browser-extension-wallet/src/features/dapp/components/Connect.tsx b/apps/browser-extension-wallet/src/features/dapp/components/Connect.tsx index fd2e18014..bbc8150db 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/Connect.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/Connect.tsx @@ -67,7 +67,6 @@ const NonSSLBanner = () => { const authorize = (authorization: 'deny' | 'just-once' | 'allow', url: string) => { const api$ = of({ allowOrigin(origin: cip30.Origin): Promise<'deny' | 'just-once' | 'allow'> { - /* eslint-disable-next-line promise/avoid-new */ if (!url.startsWith(origin)) { return Promise.reject(); } diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss index a884c336c..9e5da7e97 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss @@ -6,7 +6,7 @@ padding-top: size_unit(0); } -.transactionContainer { +.transactionContainer { display: flex; flex-direction: column; flex: 1; @@ -15,13 +15,13 @@ .actions { @extend %flex-column; - height: size_unit(17.12); justify-content: space-between; - padding: size_unit(2) size_unit(3) size_unit(2) size_unit(3); + padding: size_unit(3) 0; border-top: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey)); position: sticky; bottom: 0; z-index: 10; + gap: size_unit(1); background-color: var(--light-mode-body, var(--dark-mode-bg-black)); .actionBtn { width: 100%; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx index 5654c2393..8f7c5a175 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx @@ -5,7 +5,7 @@ import { Flex } from '@lace/ui'; import { useViewsFlowContext } from '@providers/ViewFlowProvider'; import { Wallet } from '@lace/cardano'; -import { withAddressBookContext } from '@src/features/address-book/context'; +import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context'; import { useWalletStore } from '@stores'; import { useFetchCoinPrice, useChainHistoryProvider } from '@hooks'; import { @@ -20,9 +20,11 @@ import { createWalletAssetProvider } from '@cardano-sdk/wallet'; import { Skeleton } from 'antd'; import { useCurrencyStore, useAppSettingsContext } from '@providers'; -import { logger } from '@lib/wallet-api-ui'; +import { logger, walletRepository } from '@lib/wallet-api-ui'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; import { utxoAndBackendChainHistoryResolver } from '@src/utils/utxo-chain-history-resolver'; +import { AddressBookSchema, useDbStateValue } from '@lib/storage'; +import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses'; interface DappTransactionContainerProps { errorMessage?: string; @@ -43,6 +45,10 @@ export const DappTransactionContainer = withAddressBookContext( walletState } = useWalletStore(); + const ownAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address); + const { list: addressBook } = useAddressBookContext() as useDbStateValue; + const addressToNameMap = new Map(addressBook?.map((entry) => [entry.address as string, entry.name])); + const { fiatCurrency } = useCurrencyStore(); const { priceResult } = useFetchCoinPrice(); @@ -77,6 +83,7 @@ export const DappTransactionContainer = withAddressBookContext( const userRewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); const rewardAccountsAddresses = useMemo(() => userRewardAccounts?.map((key) => key.address), [userRewardAccounts]); const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$); + const allWalletsAddresses = getAllWalletsAddresses(useObservable(walletRepository.wallets$)); useEffect(() => { if (!req || !protocolParameters) { @@ -146,6 +153,8 @@ export const DappTransactionContainer = withAddressBookContext( errorMessage={errorMessage} toAddress={toAddressTokens} collateral={txCollateral} + ownAddresses={allWalletsAddresses.length > 0 ? allWalletsAddresses : ownAddresses} + addressToNameMap={addressToNameMap} /> ) : ( diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx index bf927ebba..ac55acc54 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx @@ -33,7 +33,9 @@ const hash = Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden') const getPubDRepKey = async () => await hash; const inMemoryWallet = { - getPubDRepKey, + governance: { + getPubDRepKey + }, assetInfo$, balance: { utxo: { diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx index 98afa976f..029399959 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx @@ -30,12 +30,13 @@ import { DappTransactionContainer } from '../DappTransactionContainer'; import '@testing-library/jest-dom'; import { BehaviorSubject } from 'rxjs'; import { act } from 'react-dom/test-utils'; -import { buildMockTx } from '@src/utils/mocks/tx'; +import { buildMockTx, sendingAddress } from '@src/utils/mocks/tx'; import { Wallet } from '@lace/cardano'; import { SignTxData } from '../types'; import { getWrapper } from '../testing.utils'; import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; import { cardanoCoin } from '@src/utils/constants'; +import { AddressBookSchema } from '@lib/storage'; const { Cardano, Crypto } = Wallet; @@ -51,6 +52,7 @@ const mockedAssetsInfo = new Map([['id', 'data']]); const assetInfo$ = new BehaviorSubject(mockedAssetsInfo); const available$ = new BehaviorSubject([]); const signed$ = new BehaviorSubject([]); +const addresses$ = new BehaviorSubject([sendingAddress]); const rewardAccounts$ = new BehaviorSubject([ { // eslint-disable-next-line unicorn/consistent-destructuring @@ -65,6 +67,7 @@ const protocolParameters$ = new BehaviorSubject({ }); const inMemoryWallet = { + addresses$, assetInfo$, balance: { utxo: { @@ -133,12 +136,12 @@ jest.mock('react-i18next', () => { }; }); -const addressList = ['addressList']; +const addressBook: AddressBookSchema[] = []; jest.mock('@src/features/address-book/context', () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any ...jest.requireActual('@src/features/address-book/context'), withAddressBookContext: mockWithAddressBookContext, - useAddressBookContext: () => ({ list: addressList }) + useAddressBookContext: () => ({ list: addressBook }) })); jest.mock('antd', () => { @@ -332,7 +335,9 @@ describe('Testing DappTransactionContainer component', () => { errorMessage, coinSymbol: 'ADA', collateral: BigInt(1_000_000), - txInspectionDetails + txInspectionDetails, + ownAddresses: [sendingAddress.address], + addressToNameMap: new Map() }, {} ); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx index 56f0beb42..0ce143d7e 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx @@ -307,7 +307,9 @@ describe('Testing hooks', () => { mockUseWalletStore.mockReset(); mockUseWalletStore.mockReturnValue({ inMemoryWallet: { - getPubDRepKey: jest.fn(async () => await ed25519PublicKeyHexMock) + governance: { + getPubDRepKey: jest.fn(async () => await ed25519PublicKeyHexMock) + } } }); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts index 99785804c..7e40ce065 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts @@ -205,7 +205,7 @@ export const useGetOwnPubDRepKeyHash = (): UseGetOwnPubDRepKeyHash => { useEffect(() => { if (!inMemoryWallet) return; const get = async () => { - const ownPubDRepKey = await inMemoryWallet.getPubDRepKey(); + const ownPubDRepKey = await inMemoryWallet.governance.getPubDRepKey(); const ownDRepKeyHash = await pubDRepKeyToHash(ownPubDRepKey); setOwnPubDRepKeyHash(ownDRepKeyHash); diff --git a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx index fdf8e7a83..3f05828cb 100644 --- a/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx +++ b/apps/browser-extension-wallet/src/features/delegation/components/MultiDelegationStakingPopup.tsx @@ -1,4 +1,4 @@ -import { OutsideHandlesProvider, StakingPopup } from '@lace/staking'; +import { DEFAULT_STAKING_BROWSER_PREFERENCES, OutsideHandlesProvider, StakingPopup } from '@lace/staking'; import React, { useCallback, useEffect } from 'react'; import { useAnalyticsContext, @@ -89,7 +89,7 @@ export const MultiDelegationStakingPopup = (): JSX.Element => { ] = useLocalStorage(MULTIDELEGATION_FIRST_VISIT_SINCE_PORTFOLIO_PERSISTENCE_LS_KEY, true); const [stakingBrowserPreferencesPersistence, { updateLocalStorage: setStakingBrowserPreferencesPersistence }] = - useLocalStorage(STAKING_BROWSER_PREFERENCES_LS_KEY); + useLocalStorage(STAKING_BROWSER_PREFERENCES_LS_KEY, DEFAULT_STAKING_BROWSER_PREFERENCES); const walletAddress = walletInfo.addresses?.[0].address?.toString(); const analytics = useAnalyticsContext(); diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useBuildDelegation.test.ts b/apps/browser-extension-wallet/src/hooks/__tests__/useBuildDelegation.test.ts index 8e1dfc48a..e6d8a4540 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useBuildDelegation.test.ts +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useBuildDelegation.test.ts @@ -49,7 +49,6 @@ jest.mock('../../stores', () => ({ }) })); -// eslint-disable-next-line promise/avoid-new const flushPromises = () => new Promise(setImmediate); describe('Testing useBuildDelegation hook', () => { diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx index 62d6c0510..102decfc5 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx @@ -13,6 +13,7 @@ import SpyInstance = jest.SpyInstance; const mockEmip3decrypt = jest.fn(); const mockEmip3encrypt = jest.fn(); const mockConnectDevice = jest.fn(); +const mockGetHwExtendedAccountPublicKey = jest.fn(); const mockRestoreWalletFromKeyAgent = jest.fn(); const mockSwitchKeyAgents = jest.fn(); const mockLedgerCheckDeviceConnection = jest.fn(); @@ -80,6 +81,7 @@ jest.mock('@lace/cardano', () => { restoreWalletFromKeyAgent: mockRestoreWalletFromKeyAgent, switchKeyAgents: mockSwitchKeyAgents, connectDevice: mockConnectDevice, + getHwExtendedAccountPublicKey: mockGetHwExtendedAccountPublicKey, KeyManagement: { ...actual.Wallet.KeyManagement, emip3decrypt: mockEmip3decrypt, @@ -382,7 +384,7 @@ describe('Testing useWalletManager hook', () => { describe('createHardwareWallet', () => { test('should use cardano manager to create wallet', async () => { const walletId = 'walletId'; - mockLedgerGetXpub.mockResolvedValue('pubkey'); + mockGetHwExtendedAccountPublicKey.mockResolvedValue('pubkey'); (walletApiUi.walletRepository as any).addWallet = jest.fn().mockResolvedValue(walletId); (walletApiUi.walletRepository as any).addAccount = jest.fn().mockResolvedValue(undefined); (walletApiUi.walletManager as any).activate = jest.fn().mockResolvedValue(undefined); @@ -696,11 +698,11 @@ describe('Testing useWalletManager hook', () => { }, { type: WalletType.Trezor, - prepare: () => mockTrezorGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + prepare: () => mockGetHwExtendedAccountPublicKey.mockResolvedValueOnce(extendedAccountPublicKey) }, { type: WalletType.Ledger, - prepare: () => mockLedgerGetXpub.mockResolvedValueOnce(extendedAccountPublicKey) + prepare: () => mockGetHwExtendedAccountPublicKey.mockResolvedValueOnce(extendedAccountPublicKey) } ]; diff --git a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts index 7d230bdff..27b3c8d87 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletManager.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletManager.ts @@ -44,12 +44,6 @@ export interface CreateWallet { chainId?: Wallet.Cardano.ChainId; } -export interface SetWallet { - walletInstance: Wallet.CardanoWallet; - chainName?: Wallet.ChainName; - mnemonicVerificationFrequency?: string; -} - export interface CreateHardwareWallet { accountIndex?: number; name: string; @@ -66,6 +60,14 @@ type WalletManagerAddAccountProps = { type ActivateWalletProps = Omit; +type CreateHardwareWalletRevampedParams = { + accountIndex: number; + name: string; + connection: Wallet.HardwareWalletConnection; +}; + +type CreateHardwareWalletRevamped = (params: CreateHardwareWalletRevampedParams) => Promise; + export interface UseWalletManager { walletManager: WalletManagerApi; walletRepository: WalletRepositoryApi; @@ -78,7 +80,9 @@ export interface UseWalletManager { createWallet: (args: CreateWallet) => Promise; activateWallet: (args: Omit) => Promise; createHardwareWallet: (args: CreateHardwareWallet) => Promise; + createHardwareWalletRevamped: CreateHardwareWalletRevamped; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; + connectHardwareWalletRevamped: typeof connectHardwareWalletRevamped; saveHardwareWallet: (wallet: Wallet.CardanoWallet, chainName?: Wallet.ChainName) => Promise; /** * @returns active wallet id after deleting the wallet; undefined if deleted the last wallet @@ -95,27 +99,6 @@ const clearBytes = (bytes: Uint8Array) => { } }; -const getHwExtendedAccountPublicKey = async ( - walletType: Wallet.HardwareWallets, - accountIndex: number, - deviceConnection?: Wallet.DeviceConnection -) => { - switch (walletType) { - case WalletType.Ledger: - await Wallet.Ledger.LedgerKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web); - return Wallet.Ledger.LedgerKeyAgent.getXpub({ - communicationType: Wallet.KeyManagement.CommunicationType.Web, - deviceConnection: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined, - accountIndex - }); - case WalletType.Trezor: - return Wallet.Trezor.TrezorKeyAgent.getXpub({ - communicationType: Wallet.KeyManagement.CommunicationType.Web, - accountIndex - }); - } -}; - const getExtendedAccountPublicKey = async ( wallet: AnyBip32Wallet, accountIndex: number, @@ -143,7 +126,7 @@ const getExtendedAccountPublicKey = async ( } case WalletType.Ledger: case WalletType.Trezor: - return getHwExtendedAccountPublicKey(wallet.type, accountIndex); + return Wallet.getHwExtendedAccountPublicKey(wallet.type, accountIndex); } }; @@ -210,6 +193,9 @@ const encryptMnemonic = async (mnemonic: string[], passphrase: Uint8Array) => { export const connectHardwareWallet = async (model: Wallet.HardwareWallets): Promise => await Wallet.connectDevice(model); +const connectHardwareWalletRevamped = async (usbDevice: USBDevice): Promise => + Wallet.connectDeviceRevamped(usbDevice); + export const useWalletManager = (): UseWalletManager => { const { walletLock, @@ -241,25 +227,21 @@ export const useWalletManager = (): UseWalletManager => { return (storedChain?.chainName && chainIdFromName(storedChain.chainName)) || DEFAULT_CHAIN_ID; }, [currentChain]); - /** - * Creates a Ledger or Trezor hardware wallet - * and saves it in browser storage with the data to lock/unlock it - */ - const createHardwareWallet = useCallback( - async ({ - accountIndex = 0, - deviceConnection, - name, - connectedDevice - }: CreateHardwareWallet): Promise => { - const extendedAccountPublicKey = await getHwExtendedAccountPublicKey( - connectedDevice, - accountIndex, - deviceConnection - ); + const createHardwareWalletRevamped = useCallback( + async ({ accountIndex, connection, name }) => { + let extendedAccountPublicKey; + try { + extendedAccountPublicKey = await Wallet.getHwExtendedAccountPublicKey( + connection.type, + accountIndex, + connection.type === WalletType.Ledger ? connection.value : undefined + ); + } catch (error: unknown) { + throw error; + } const addWalletProps: AddWalletProps = { metadata: { name, lastActiveAccountIndex: accountIndex }, - type: connectedDevice, + type: connection.type, accounts: [ { extendedAccountPublicKey, @@ -291,6 +273,28 @@ export const useWalletManager = (): UseWalletManager => { [getCurrentChainId] ); + /** + * Creates a Ledger or Trezor hardware wallet + * and saves it in browser storage with the data to lock/unlock it + */ + const createHardwareWallet = useCallback( + async ({ + accountIndex = 0, + deviceConnection, + name, + connectedDevice + }: CreateHardwareWallet): Promise => + createHardwareWalletRevamped({ + accountIndex, + connection: { + type: connectedDevice, + value: typeof deviceConnection !== 'boolean' ? deviceConnection : undefined + }, + name + }), + [createHardwareWalletRevamped] + ); + const tryMigrateToWalletRepository = useCallback(async (): Promise< AnyWallet[] | undefined > => { @@ -742,7 +746,9 @@ export const useWalletManager = (): UseWalletManager => { loadWallet, createWallet, createHardwareWallet, + createHardwareWalletRevamped, connectHardwareWallet, + connectHardwareWalletRevamped, saveHardwareWallet, deleteWallet, switchNetwork, diff --git a/apps/browser-extension-wallet/src/hooks/useWalletState.ts b/apps/browser-extension-wallet/src/hooks/useWalletState.ts index e4a6aaac5..ea9f95dd5 100644 --- a/apps/browser-extension-wallet/src/hooks/useWalletState.ts +++ b/apps/browser-extension-wallet/src/hooks/useWalletState.ts @@ -52,6 +52,7 @@ const combineObservable = ({ wallet }: Wallet.CardanoWallet): Observable { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should not trigger subscription for no active wallet', () => { + const mockWalletManager = { + activeWallet$: of(undefined), + activeWalletId$: of(undefined) + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$: of([]), + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + expect(mockWalletRepository.updateWalletMetadata).not.toHaveBeenCalled(); + }); + + it('should subscribe and update metadata', () => { + const mockWalletManager = { + activeWallet$: of({ addresses$: of([{ address: 'address1' }]) }), + activeWalletId$: of({ walletId: 'walletId' }) + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$: of([ + { + walletId: 'walletId', + metadata: {} + } + ]), + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenCalledWith({ + walletId: 'walletId', + metadata: { + walletAddresses: ['address1'] + } + }); + }); + + it('should subscribe and update metadata when a new wallet is added and activated', () => { + const activeWallet$ = new BehaviorSubject({ + addresses$: of([{ address: 'address1' }]) + }); + const activeWalletId$ = new BehaviorSubject({ + walletId: 'walletId1' + }); + const wallets$ = new BehaviorSubject<{ walletId: string; metadata: { walletAddresses?: string[] } }[]>([ + { + walletId: 'walletId1', + metadata: { walletAddresses: ['address1'] } + } + ]); + const mockWalletManager = { + activeWallet$, + activeWalletId$ + } as unknown as WalletManager; + + const mockWalletRepository = { + wallets$, + updateWalletMetadata: jest.fn() + } as unknown as WalletRepository; + + cacheActivatedWalletAddressSubscription(mockWalletManager, mockWalletRepository); + + wallets$.next([ + { + walletId: 'walletId1', + metadata: { walletAddresses: ['address1'] } + }, + { + walletId: 'walletId2', + metadata: {} + } + ]); + activeWalletId$.next({ walletId: 'walletId2' }); + activeWallet$.next({ addresses$: of([{ address: 'address2' }, { address: 'address3' }]) }); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenNthCalledWith(1, { + walletId: 'walletId1', + metadata: { + walletAddresses: ['address1'] + } + }); + + expect(mockWalletRepository.updateWalletMetadata).toHaveBeenNthCalledWith(2, { + walletId: 'walletId2', + metadata: { + walletAddresses: ['address2', 'address3'] + } + }); + }); +}); diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts b/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts new file mode 100644 index 000000000..54f9857d1 --- /dev/null +++ b/apps/browser-extension-wallet/src/lib/scripts/background/cache-wallets-address.ts @@ -0,0 +1,31 @@ +import { WalletManager, WalletRepository } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; +import { filter, switchMap, withLatestFrom, zip } from 'rxjs'; + +export const cacheActivatedWalletAddressSubscription = ( + walletManager: WalletManager, + walletRepository: WalletRepository +): void => { + zip([ + walletManager.activeWalletId$.pipe(filter((activeWalletId) => Boolean(activeWalletId))), + walletManager.activeWallet$.pipe( + filter((wallet) => Boolean(wallet)), + switchMap((wallet) => wallet.addresses$) + ) + ]) + .pipe(withLatestFrom(walletRepository.wallets$)) + .subscribe(([[activeWallet, walletAddresses], wallets]) => { + const wallet = wallets.find(({ walletId }) => walletId === activeWallet.walletId); + const uniqueAddresses = [ + ...new Set([...(wallet.metadata.walletAddresses || []), ...walletAddresses.map(({ address }) => address)]) + ]; + + walletRepository.updateWalletMetadata({ + walletId: activeWallet.walletId, + metadata: { + ...wallet.metadata, + walletAddresses: uniqueAddresses + } + }); + }); +}; diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/util.ts b/apps/browser-extension-wallet/src/lib/scripts/background/util.ts index a317f75f5..1579e512a 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/util.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/util.ts @@ -86,7 +86,6 @@ export const launchCip30Popup = async (url: string, windowType: Windows.CreateTy }; const waitForTabLoad = (tab: Tabs.Tab) => - // eslint-disable-next-line promise/avoid-new new Promise((resolve) => { const listener = (tabId: number, changeInfo: Tabs.OnUpdatedChangeInfoType) => { // make sure the status is 'complete' and it's the right tab diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts index 2c64fa9d1..0f58377d9 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts @@ -23,6 +23,7 @@ import { import { Wallet } from '@lace/cardano'; import { ADA_HANDLE_POLICY_ID, HANDLE_SERVER_URLS } from '@src/features/ada-handle/config'; import { Cardano, NotImplementedError } from '@cardano-sdk/core'; +import { cacheActivatedWalletAddressSubscription } from './cache-wallets-address'; const logger = console; @@ -198,4 +199,6 @@ walletManager logger.error('Failed to initialize wallet manager', error); }); +cacheActivatedWalletAddressSubscription(walletManager, walletRepository); + export const wallet$ = walletManager.activeWallet$; diff --git a/apps/browser-extension-wallet/src/lib/scripts/migrations/migrations.ts b/apps/browser-extension-wallet/src/lib/scripts/migrations/migrations.ts index 46d5496b8..6e87218ae 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/migrations/migrations.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/migrations/migrations.ts @@ -43,7 +43,7 @@ export type Migration = { downgrade?: (password?: string) => MigrationPersistance | Promise; }; -const migrations: Migration[] = [versions.v_1_0_0]; +const migrations: Migration[] = [versions.v_1_10_2]; /** * Applies all migrations in order between the two version provided @@ -137,7 +137,6 @@ export const migrationsRequirePassword = async ( */ export const checkMigrations = async (previousVersion: string, migrationsArray = migrations): Promise => { const currentVersion = runtime.getManifest().version; - // Return if a downgrade is occurring if (isVersionOlderThanOrEqual(currentVersion, previousVersion)) { // TODO: allow migrations if downgrading versions too [LW-5595] diff --git a/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/index.ts b/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/index.ts index c0d8c76da..0dcc43d33 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/index.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/index.ts @@ -1 +1,2 @@ export * from './v1_0_0'; +export * from './v1_10_2'; diff --git a/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/v1_10_2.ts b/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/v1_10_2.ts new file mode 100644 index 000000000..8983bfe51 --- /dev/null +++ b/apps/browser-extension-wallet/src/lib/scripts/migrations/versions/v1_10_2.ts @@ -0,0 +1,31 @@ +/* eslint-disable camelcase, @typescript-eslint/no-empty-function, no-console */ +import { getItemFromLocalStorage, removeItemFromLocalStorage } from '../util'; +import { Migration } from '../migrations'; + +const MIGRATION_VERSION = '1.10.2'; + +export const v_1_10_2: Migration = { + version: MIGRATION_VERSION, + upgrade: async () => ({ + prepare: () => { + try { + /** + * Between the v1.9.0 and v1.10.0 releases, the 'stakingBrowserPreferences' + * localStorage was updated, and contained a change in spelling, removing this + * object allows it to be saved again and any issues related to spelling + * are mitigated + * */ + removeItemFromLocalStorage('stakingBrowserPreferences'); + } catch (error) { + console.log(`error executing migration ${MIGRATION_VERSION}: ${error}`); + throw error; + } + }, + assert: () => { + const stakingBrowserPreferences = getItemFromLocalStorage('stakingBrowserPreferences'); + return !!stakingBrowserPreferences; + }, + persist: () => {}, + rollback: () => {} + }) +}; diff --git a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-content-script.ts b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-content-script.ts index 523adc068..54ce65a6c 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-content-script.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-content-script.ts @@ -1,4 +1,5 @@ import { runtime } from 'webextension-polyfill'; +import { AllowedOrigins } from './types'; // Communicate from background script to popup let port = runtime.connect({ name: 'trezor-connect' }); @@ -12,6 +13,7 @@ port.onDisconnect.addListener(() => { // communicate from popup to background script window.addEventListener('message', (event) => { + if (event.origin !== AllowedOrigins.TREZOR_CONNECT) throw new Error('Origin not allowed'); if (port && event.source === window && event.data) { port.postMessage({ data: event.data }); } diff --git a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts index 4fbfff55e..06423a09f 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts @@ -1,8 +1,5 @@ import { runtime, tabs } from 'webextension-polyfill'; - -// Handling messages from usb permissions iframe - -const url = 'https://connect.trezor.io/8/'; +import { AllowedOrigins } from './types'; /* Handling messages from usb permissions iframe */ const switchToPopupTab = async (event?: BeforeUnloadEvent) => { @@ -21,13 +18,15 @@ const switchToPopupTab = async (event?: BeforeUnloadEvent) => { // find tab by popup pattern and switch to it const currentTabs = await tabs.query({ - url: `${url}popup.html` + url: `${AllowedOrigins.TREZOR_CONNECT_POPUP_BASE_URL}/popup.html` }); if (currentTabs.length < 0) return; tabs.update(currentTabs[0].id, { active: true }); }; window.addEventListener('message', async (event) => { + if (event.origin !== AllowedOrigins.TREZOR_CONNECT) throw new Error('Origin not allowed'); + if (event.data === 'usb-permissions-init') { const iframe = document.querySelector('#trezor-usb-permissions'); if (!iframe || !(iframe instanceof HTMLIFrameElement)) { @@ -55,7 +54,7 @@ window.addEventListener('load', () => { instance.style.border = '0px'; instance.style.width = '100%'; instance.style.height = '100%'; - instance.setAttribute('src', `${url}extension-permissions.html`); + instance.setAttribute('src', `${AllowedOrigins.TREZOR_CONNECT_POPUP_BASE_URL}/extension-permissions.html`); instance.setAttribute('allow', 'usb'); if (document.body) { diff --git a/apps/browser-extension-wallet/src/lib/scripts/trezor/types.ts b/apps/browser-extension-wallet/src/lib/scripts/trezor/types.ts new file mode 100644 index 000000000..e4690c226 --- /dev/null +++ b/apps/browser-extension-wallet/src/lib/scripts/trezor/types.ts @@ -0,0 +1,4 @@ +export enum AllowedOrigins { + TREZOR_CONNECT = 'https://connect.trezor.io', + TREZOR_CONNECT_POPUP_BASE_URL = 'https://connect.trezor.io/8' +} diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index e13663264..4cbaed881 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -494,9 +494,6 @@ "browserView.walletSetup.mnemonicResetModal.content": "In order to keep you safe, we'll show you a new set of 24 words.", "browserView.walletSetup.mnemonicResetModal.cancel": "Cancel", "browserView.walletSetup.mnemonicResetModal.confirm": "OK", - "browserView.walletSetup.confirmExperimentalHwDapp.header": "Limited support for Dapp connection", - "browserView.walletSetup.confirmExperimentalHwDapp.content": "This current version does not support signing transactions through the Dapp connection feature with hardware wallets. Please stay tuned for upcoming releases and new features through @lace on Twitter.", - "browserView.walletSetup.confirmExperimentalHwDapp.confirm": "OK", "browserView.crypto.emptyDashboard.welcome": "Welcome", "browserView.crypto.emptyDashboard.addSomeFundsYoStartYourJourney": "Add some funds to start your journey", "browserView.crypto.emptyDashboard.useThisAddressOrScanTheQRCodeToTransferFunds": "Use this address or scan the QR code to transfer funds", @@ -647,13 +644,11 @@ "browserView.staking.details.noFundsModal.buttons.confirm": "Add funds", "browserView.staking.details.errors.utxoFullyDepleted": "UTxO has been fully depleted", "browserView.staking.details.errors.utxoBalanceInsufficient": "Balance Insufficient", - "browserView.onboarding.commonError.title": "Oops! Something went wrong", - "browserView.onboarding.commonError.description": "Please check your hardware device.", - "browserView.onboarding.commonError.ok": "OK", - "browserView.onboarding.notDetectedError.title": "Failed to detect device", - "browserView.onboarding.notDetectedError.description": "Please make sure your device is unlocked and the Cardano app is open.", - "browserView.onboarding.notDetectedError.agree": "Agree", - "browserView.onboarding.notDetectedError.trezorDescription": "Please make sure your device is unlocked.", + "browserView.onboarding.errorDialog.title": "Opps! Something went wrong", + "browserView.onboarding.errorDialog.cta": "OK", + "browserView.onboarding.errorDialog.messageDeviceDisconnected": "Please check your hardware device connection and for Ledger, if Cardano App is open", + "browserView.onboarding.errorDialog.messagePublicKeyExportRejected": "Public key export unsuccessful. User declined action on hardware wallet device.", + "browserView.onboarding.errorDialog.messageGeneric": "Try connecting you device again", "browserView.onboarding.startOver.title": "Are you sure you want to start again?", "browserView.onboarding.startOver.description": "Connection to this device will be cancelled and you will need to re-connect.", "browserView.onboarding.startOver.cancel": "Cancel", @@ -1263,6 +1258,18 @@ "core.walletSetupConnectHardwareWalletStep.supportedDevices": "Lace is supporting Ledger Nano X, Nano S and Nano S Plus", "core.walletSetupConnectHardwareWalletStep.connectDevice": "Just connect your device to your computer, unlock and open the Cardano app to hit continue.", "core.walletSetupConnectHardwareWalletStep.connectDeviceFull": "Just connect your device to your computer and unlock it to continue. If you're using a Ledger device, make sure you open the Cardano App.", + "core.walletSetupConnectHardwareWalletStepRevamp.title": "Connect your device", + "core.walletSetupConnectHardwareWalletStepRevamp.subTitleLedgerOnly": "Lace supports Ledger Nano X, Nano S, Nano S Plus. Unlock your device and open the Cardano app.", + "core.walletSetupConnectHardwareWalletStepRevamp.subTitle": "Lace supports Ledger Nano X, Nano S, Nano S Plus, Trezor Model T. Unlock your device and for Ledger, open the Cardano app.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.userGestureRequired": "It appears the page reload interrupted the search for connected hardware wallet devices.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.devicePickerRejected": "No hardware wallet device was chosen.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceLocked": "Your hardware wallet device seems to be locked. Please unlock it to proceed.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceBusy": "Your hardware wallet device seems to be busy with some other App. Please ensure it is free to connect.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.cardanoAppNotOpen": "Cardano App is not open on your hardware wallet device. Please open it to proceed.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.generic": "Something went wrong. Please try again.", + "core.walletSetupConnectHardwareWalletStepRevamp.errorCta": "Try again", + "core.walletSetupCreateStep.title": "Creating your wallet", + "core.walletSetupCreateStep.description": "Confirm exporting your public key on your hardware wallet device to create your Lace wallet.", "core.walletSetupRestoreStep.title": "Restoring your wallet", "core.walletSetupMnemonicStep.passphraseError": "Make sure the words of your recovery phrase are in the right order and spelled correctly.", "core.walletSetupMnemonicStepRevamp.writePassphraseTitle": "Start by saving your recovery phrase", @@ -1348,6 +1355,8 @@ "core.receive.usedAddresses.copy": "Copy Address", "core.receive.usedAddresses.addressCopied": "Address copied", "core.receive.showUsedAddresses": "Show used addresses", + "core.addressTags.own": "own", + "core.addressTags.foreign": "foreign", "addressesDiscovery.overlay.title": "Your wallet is syncing, this might take a few minutes", "addressesDiscovery.toast.errorText": "Wallet failed to sync", "addressesDiscovery.toast.successText": "Wallet synced successfully", @@ -1379,6 +1388,13 @@ "multiWallet.confirmationDialog.description": "You'll have to start over.", "multiWallet.confirmationDialog.cancel": "Go back", "multiWallet.confirmationDialog.confirm": "Proceed", + "multiWallet.errorDialog.commonError.title": "Oops! Something went wrong", + "multiWallet.errorDialog.commonError.description": "Please check your hardware device.", + "multiWallet.errorDialog.commonError.ok": "OK", + "multiWallet.errorDialog.notDetectedError.title": "Failed to detect device", + "multiWallet.errorDialog.notDetectedError.description": "Please make sure your device is unlocked and the Cardano app is open.", + "multiWallet.errorDialog.notDetectedError.agree": "Agree", + "multiWallet.errorDialog.notDetectedError.trezorDescription": "Please make sure your device is unlocked.", "multiWallet.activated.wallet": "Wallet \"{{walletName}}\" activated", "multiWallet.activated.account": "Account \"{{accountName}}\" activated", "multiWallet.popupHwAccountEnable": "Hardware wallets require the <0>expanded view to enable accounts", diff --git a/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/events.ts b/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/events.ts index 56053b8ce..7fb4c7136 100644 --- a/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/events.ts +++ b/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/events.ts @@ -26,11 +26,12 @@ export const postHogOnboardingActions: PostHogOnboardingActionsType = { PostHogAction.OnboardingRestoreEnterRecoveryPhrasePasteFromClipboardClick }, hw: { - WALLET_NAME_NEXT_CLICK: PostHogAction.OnboardingHWNameNextClick, - CONNECT_HW_NEXT_CLICK: PostHogAction.OnboardingHWConnectNextClick, - SETUP_HW_WALLET_NEXT_CLICK: PostHogAction.OnboardingHWSelectAccountNextClick, SETUP_OPTION_CLICK: PostHogAction.OnboardingHWClick, - DONE_GO_TO_WALLET: PostHogAction.OnboardingHWDoneGoToWallet + CONNECT_HW_VIEW: PostHogAction.OnboardingHWConnectView, + HW_POPUP_CONNECT_CLICK: PostHogAction.OnboardingHWPopupConnectClick, + CONNECT_HW_TRY_AGAIN_CLICK: PostHogAction.OnboardingHWConnectTryAgainClick, + SETUP_HW_ACCOUNT_NO_CLICK: PostHogAction.OnboardingHWSetupWalletAccountNoClick, + ENTER_WALLET: PostHogAction.OnboardingHWEnterWalletClick }, // eslint-disable-next-line camelcase forgot_password: { diff --git a/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/types.ts b/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/types.ts index 1e9f72dc9..0d8ae5e8c 100644 --- a/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/types.ts +++ b/apps/browser-extension-wallet/src/providers/AnalyticsProvider/analyticsTracker/types.ts @@ -44,15 +44,15 @@ export type PostHogActionsKeys = | 'ANALYTICS_AGREE_CLICK' | 'LEARN_MORE_CLICK' | 'ANALYTICS_REJECT_CLICK' - | 'WALLET_NAME_NEXT_CLICK' | 'SAVE_RECOVERY_PHRASE_NEXT_CLICK' | 'ENTER_RECOVERY_PHRASE_NEXT_CLICK' | 'ENTER_WALLET' | 'GOT_IT_CLICK' | 'PIN_EXTENSION_CLICK' - | 'CONNECT_HW_NEXT_CLICK' - | 'SETUP_HW_WALLET_NEXT_CLICK' - | 'DONE_GO_TO_WALLET' + | 'CONNECT_HW_VIEW' + | 'HW_POPUP_CONNECT_CLICK' + | 'CONNECT_HW_TRY_AGAIN_CLICK' + | 'SETUP_HW_ACCOUNT_NO_CLICK' | 'WALLET_NAME_PASSWORD_NEXT_CLICK' | 'RECOVERY_PHRASE_INTRO_WATCH_VIDEO_CLICK' | 'RECOVERY_PHRASE_INTRO_VIDEO_GOTIT_CLICK' diff --git a/apps/browser-extension-wallet/src/utils/__tests__/createQueue.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/createQueue.test.ts index 758afeaf2..e6a4942fb 100644 --- a/apps/browser-extension-wallet/src/utils/__tests__/createQueue.test.ts +++ b/apps/browser-extension-wallet/src/utils/__tests__/createQueue.test.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable promise/avoid-new */ import { createQueue, TaskQueue } from '../taskQueue'; describe('createQueue', () => { diff --git a/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts new file mode 100644 index 000000000..c2ade5698 --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/__tests__/get-all-wallets-addresses.test.ts @@ -0,0 +1,36 @@ +import { AnyWallet } from '@cardano-sdk/web-extension'; +import { getAllWalletsAddresses } from '../get-all-wallets-addresses'; +import { Wallet } from '@lace/cardano'; + +describe('getAllWalletsAddresses', () => { + it('should return an empty array if undefined is provided', () => { + const addresses = getAllWalletsAddresses(); + + expect(addresses).toEqual([]); + }); + + it('should return an empty array if no wallets are provided', () => { + const addresses = getAllWalletsAddresses([]); + + expect(addresses).toEqual([]); + }); + + it('should return an array of payment addresses', () => { + const mockWallets = [ + { + metadata: { + walletAddresses: ['addr1', 'addr2'] + } + }, + { + metadata: { + walletAddresses: ['addr2', 'addr3'] + } + } + ] as AnyWallet[]; + + const addresses = getAllWalletsAddresses(mockWallets); + + expect(addresses).toEqual(['addr1', 'addr2', 'addr3']); + }); +}); diff --git a/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts b/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts new file mode 100644 index 000000000..690c1c32b --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/get-all-wallets-addresses.ts @@ -0,0 +1,9 @@ +import { AnyWallet } from '@cardano-sdk/web-extension'; +import { Wallet } from '@lace/cardano'; +import flatMap from 'lodash/flatMap'; + +export const getAllWalletsAddresses = ( + wallets: AnyWallet[] = [] +): Wallet.Cardano.PaymentAddress[] => [ + ...new Set(flatMap(wallets.map(({ metadata: { walletAddresses = [] } }) => walletAddresses))) +]; diff --git a/apps/browser-extension-wallet/src/utils/mocks/fake-api-request.ts b/apps/browser-extension-wallet/src/utils/mocks/fake-api-request.ts index bf078cafb..e7902a039 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/fake-api-request.ts +++ b/apps/browser-extension-wallet/src/utils/mocks/fake-api-request.ts @@ -1,7 +1,6 @@ const DEFAULT_TIMEOUT = 1000; export const fakeApiRequest = (response: TResponse, timeout = DEFAULT_TIMEOUT): Promise => - // eslint-disable-next-line promise/avoid-new new Promise((resolve) => { setTimeout(() => resolve(response), timeout); }); diff --git a/apps/browser-extension-wallet/src/utils/mocks/tx.ts b/apps/browser-extension-wallet/src/utils/mocks/tx.ts index 8ea60d13c..a41522f82 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/tx.ts +++ b/apps/browser-extension-wallet/src/utils/mocks/tx.ts @@ -1,11 +1,13 @@ /* eslint-disable no-magic-numbers */ import { Wallet } from '@lace/cardano'; -const sendingAddress = Wallet.Cardano.PaymentAddress( - 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' -); +export const sendingAddress = { + address: Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' + ) +} as Wallet.KeyManagement.GroupedAddress; -const receivingAddress = Wallet.Cardano.PaymentAddress( +export const receivingAddress = Wallet.Cardano.PaymentAddress( 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' ); @@ -34,7 +36,7 @@ export const buildMockTx = ( ]), inputs: args.inputs ?? [ { - address: sendingAddress, + address: sendingAddress.address, index: 0, txId: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') } @@ -64,7 +66,7 @@ export const buildMockTx = ( } }, { - address: sendingAddress, + address: sendingAddress.address, value: { assets: new Map([ [Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'), BigInt(1)] diff --git a/apps/browser-extension-wallet/src/utils/taskQueue.ts b/apps/browser-extension-wallet/src/utils/taskQueue.ts index 67bdfacf9..0337630a5 100644 --- a/apps/browser-extension-wallet/src/utils/taskQueue.ts +++ b/apps/browser-extension-wallet/src/utils/taskQueue.ts @@ -37,7 +37,6 @@ export const createQueue = (batchTasks: number, intervalBetweenBatch: number): T if (sent >= batchTasks) { // Reset the sent count sent = 0; - // eslint-disable-next-line promise/avoid-new await new Promise((resolve) => setTimeout(resolve, intervalBetweenBatch)); } execute(); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx index 412cb9a52..e5bab6689 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx @@ -9,6 +9,9 @@ import { TxDirections } from '@src/types'; import { APP_MODE_POPUP } from '@src/utils/constants'; import { config } from '@src/config'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; +import { useObservable } from '@lace/common'; +import { getAllWalletsAddresses } from '@src/utils/get-all-wallets-addresses'; +import { walletRepository } from '@lib/wallet-api-ui'; type TransactionDetailsProxyProps = { name: string; @@ -21,13 +24,24 @@ export const TransactionDetailsProxy = withAddressBookContext( ({ name, activityInfo, direction, status, amountTransformer }: TransactionDetailsProxyProps): ReactElement => { const analytics = useAnalyticsContext(); const { + inMemoryWallet, walletInfo, environmentName, walletUI: { cardanoCoin, appMode } } = useWalletStore(); const isPopupView = appMode === APP_MODE_POPUP; const openExternalLink = useExternalLinkOpener(); + + // Prepare own addresses of active account + const allWalletsAddresses = getAllWalletsAddresses(useObservable(walletRepository.wallets$)); + const walletAddresses = useObservable(inMemoryWallet.addresses$)?.map((a) => a.address); + + // Prepare address book data as Map const { list: addressList } = useAddressBookContext(); + const addressToNameMap = useMemo( + () => new Map(addressList?.map((item: AddressListType) => [item.address, item.name])), + [addressList] + ); const { CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS } = config(); const explorerBaseUrl = useMemo( @@ -72,11 +86,6 @@ export const TransactionDetailsProxy = withAddressBookContext( externalLink && status === ActivityStatus.SUCCESS && openExternalLink(externalLink); }; - const addressToNameMap = useMemo( - () => new Map(addressList?.map((item: AddressListType) => [item.address, item.name])), - [addressList] - ); - return ( // eslint-disable-next-line react/jsx-pascal-case 0 ? allWalletsAddresses : walletAddresses} addressToNameMap={addressToNameMap} coinSymbol={cardanoCoin.symbol} isPopupView={isPopupView} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx index 1674ddf71..10865d408 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/MultiWallet.tsx @@ -42,7 +42,7 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): const { t } = useTranslation(); const { connectHardwareWallet, createHardwareWallet, walletRepository } = useWalletManager(); const analytics = useAnalyticsContext(); - const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); + const disconnectHardwareWallet$ = useMemo(() => new Subject(), []); const hardwareWalletProviders = useMemo( (): Providers => ({ @@ -83,14 +83,14 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): ); useEffect(() => { - const onHardwareWalletDisconnect = (event: HIDConnectionEvent) => { + const onHardwareWalletDisconnect = (event: USBConnectionEvent) => { disconnectHardwareWallet$.next(event); }; - navigator.hid.addEventListener('disconnect', onHardwareWalletDisconnect); + navigator.usb.addEventListener('disconnect', onHardwareWalletDisconnect); return () => { - navigator.hid.removeEventListener('disconnect', onHardwareWalletDisconnect); + navigator.usb.removeEventListener('disconnect', onHardwareWalletDisconnect); disconnectHardwareWallet$.complete(); }; }, [disconnectHardwareWallet$]); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx index 175f0eb64..2f36a529f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/HardwareWallet.test.tsx @@ -59,7 +59,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { let providers = {} as { connectHardwareWallet: jest.Mock; createWallet: jest.Mock; - disconnectHardwareWallet$: Subject; + disconnectHardwareWallet$: Subject; shouldShowDialog$: Subject; }; @@ -67,7 +67,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { providers = { connectHardwareWallet: jest.fn(), createWallet: jest.fn(), - disconnectHardwareWallet$: new Subject(), + disconnectHardwareWallet$: new Subject(), shouldShowDialog$: new Subject() }; }); @@ -101,7 +101,7 @@ describe('Multi Wallet Setup/Hardware Wallet', () => { await selectAccountStep(); act(() => { - providers.disconnectHardwareWallet$.next({ device: { opened: true } } as HIDConnectionEvent); + providers.disconnectHardwareWallet$.next({ device: { opened: true } } as USBConnectionEvent); }); await waitFor(() => expect(screen.queryByText('Oops! Something went wrong')).toBeInTheDocument()); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx index f8a9eb89e..767db218a 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/context.tsx @@ -15,7 +15,7 @@ interface State { setAccount: (account: number) => void; resetConnection: () => void; createWallet: () => Promise; - disconnectHardwareWallet$: Observable; + disconnectHardwareWallet$: Observable; } // eslint-disable-next-line unicorn/no-null diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx index 542f35c05..fdf64c8ab 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/Connect.tsx @@ -2,7 +2,6 @@ import { WalletSetupConnectHardwareWalletStep } from '@lace/core'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; -import { isTrezorHWSupported } from '../../../wallet-setup/helpers'; import { Wallet } from '@lace/cardano'; import { useHardwareWallet } from '../context'; import { walletRoutePaths } from '@routes'; @@ -11,6 +10,8 @@ import { WalletType } from '@cardano-sdk/web-extension'; import { useAnalyticsContext } from '@providers'; import { PostHogAction } from '@lace/common'; +export const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; + interface State { error?: 'notDetectedLedger' | 'notDetectedTrezor'; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx index 957dfe276..de4b88589 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/steps/ErrorHandling.tsx @@ -1,11 +1,29 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import { useHardwareWallet } from '../context'; -import { ErrorDialog } from '../../../wallet-setup/components/ErrorDialog'; +import { makeErrorDialog } from '../../../wallet-setup/components/HardwareWalletFlow'; import { walletRoutePaths } from '@routes'; type Errors = 'notDetectedLedger' | 'notDetectedTrezor' | 'common'; +const ErrorDialog = makeErrorDialog({ + common: { + title: 'multiWallet.errorDialog.commonError.title', + description: 'multiWallet.errorDialog.commonError.description', + confirm: 'multiWallet.errorDialog.commonError.ok' + }, + notDetectedLedger: { + title: 'multiWallet.errorDialog.notDetectedError.title', + description: 'multiWallet.errorDialog.notDetectedError.description', + confirm: 'multiWallet.errorDialog.notDetectedError.agree' + }, + notDetectedTrezor: { + title: 'multiWallet.errorDialog.notDetectedError.title', + description: 'multiWallet.errorDialog.notDetectedError.trezorDescription', + confirm: 'multiWallet.errorDialog.notDetectedError.agree' + } +}); + interface State { error?: Errors; } @@ -21,7 +39,7 @@ export const ErrorHandling = ({ error, onRetry }: Props): JSX.Element => { const [state, setState] = useState({}); useEffect(() => { - const subscription = disconnectHardwareWallet$.subscribe((event: HIDConnectionEvent) => { + const subscription = disconnectHardwareWallet$.subscribe((event: USBConnectionEvent) => { if (event.device.opened) { setState({ error: 'common' }); } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts index c8105b223..9c9a130a6 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/multi-wallet/hardware-wallet/types.ts @@ -11,6 +11,6 @@ export interface Data { export interface Providers { createWallet: (params: Data) => Promise; connectHardwareWallet: (model: Wallet.HardwareWallets) => Promise; - disconnectHardwareWallet$: Observable; + disconnectHardwareWallet$: Observable; shouldShowDialog$: Subject; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts new file mode 100644 index 000000000..f64d9d889 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/__tests__/isMultidelegationSupportedByDevice.test.ts @@ -0,0 +1,71 @@ +/* eslint-disable import/imports-first */ +const initConnectionAndGetSoftwareVersionMock = jest.fn(); + +import { + LedgerMultidelegationMinAppVersion, + isMultidelegationSupportedByDevice +} from '../isMultidelegationSupportedByDevice'; +import { WalletType } from '@cardano-sdk/web-extension'; + +jest.mock('@lace/cardano', () => ({ + Wallet: { + initConnectionAndGetSoftwareVersion: initConnectionAndGetSoftwareVersionMock + } +})); + +describe('isMultidelegationSupportedByDevice', () => { + it('calls Wallet.isMultidelegationSupportedByDevice to get a version', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: 0, + minor: 0, + patch: 0 + }); + await isMultidelegationSupportedByDevice(WalletType.Ledger); + expect(initConnectionAndGetSoftwareVersionMock).toHaveBeenCalledWith(WalletType.Ledger); + }); + + it('returns false if the version is lower than expected', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: 0, + minor: 0, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Trezor)).toEqual(false); + }); + + it('returns true if the version is greater on the major level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR + 1, + minor: 0, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is greater on the minor level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR + 1, + patch: 0 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is greater on the patch level', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR, + patch: LedgerMultidelegationMinAppVersion.PATCH + 1 + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Ledger)).toEqual(true); + }); + + it('returns true if the version is equal to the minimal required', async () => { + initConnectionAndGetSoftwareVersionMock.mockResolvedValue({ + major: LedgerMultidelegationMinAppVersion.MAJOR, + minor: LedgerMultidelegationMinAppVersion.MINOR, + patch: LedgerMultidelegationMinAppVersion.PATCH + }); + expect(await isMultidelegationSupportedByDevice(WalletType.Trezor)).toEqual(true); + }); +}); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolsTable/StakePoolsTable.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolsTable/StakePoolsTable.tsx index 626fa89dd..b3992ae2f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolsTable/StakePoolsTable.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolsTable/StakePoolsTable.tsx @@ -9,7 +9,9 @@ import { StakePoolsListRowSkeleton, StakePoolSortOptions, stakePoolTableConfig, - TranslationsFor + TranslationsFor, + getDefaultSortOrderByField, + DEFAULT_SORT_OPTIONS } from '@lace/staking'; import { Typography } from 'antd'; import { Search } from '@lace/common'; @@ -31,11 +33,6 @@ type stakePoolsTableProps = { type LoadMoreDataParam = Parameters[0]['loadMoreData']; -const DEFAULT_SORT_OPTIONS: StakePoolSortOptions = { - field: 'ticker', - order: 'desc' -}; - const searchDebounce = 300; export const StakePoolsTable = ({ scrollableTargetId }: stakePoolsTableProps): React.ReactElement => { @@ -109,7 +106,8 @@ export const StakePoolsTable = ({ scrollableTargetId }: stakePoolsTableProps): R }; const onSortChange = (sortField: SortField) => { - const order = sortField === sort?.field && sort?.order === 'asc' ? 'desc' : 'asc'; + const inverseOrder = sort?.order === 'asc' ? 'desc' : 'asc'; + const order = sortField !== sort?.field ? getDefaultSortOrderByField(sortField) : inverseOrder; setSort({ field: sortField, order }); }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx index c9d8d1bd7..3df3675da 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingContainer.tsx @@ -10,7 +10,7 @@ import { compactNumberWithUnit } from '@utils/format-number'; import { isMultidelegationSupportedByDevice } from '@views/browser/features/staking'; import { useWalletStore } from '@stores'; import { useAnalyticsContext, useCurrencyStore, useExternalLinkOpener } from '@providers'; -import { OutsideHandlesProvider } from '@lace/staking'; +import { DEFAULT_STAKING_BROWSER_PREFERENCES, OutsideHandlesProvider } from '@lace/staking'; import { useBalances, useFetchCoinPrice, useLocalStorage } from '@hooks'; import { MULTIDELEGATION_FIRST_VISIT_LS_KEY, @@ -27,7 +27,7 @@ export const StakingContainer = (): React.ReactElement => { const analytics = useAnalyticsContext(); const [stakingBrowserPreferencesPersistence, { updateLocalStorage: setStakingBrowserPreferencesPersistence }] = - useLocalStorage(STAKING_BROWSER_PREFERENCES_LS_KEY); + useLocalStorage(STAKING_BROWSER_PREFERENCES_LS_KEY, DEFAULT_STAKING_BROWSER_PREFERENCES); const [multidelegationFirstVisit, { updateLocalStorage: setMultidelegationFirstVisit }] = useLocalStorage( MULTIDELEGATION_FIRST_VISIT_LS_KEY, true diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts deleted file mode 100644 index 77452c9f9..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/helpers.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import { Wallet } from '@lace/cardano'; -import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; -import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; -import { WalletType } from '@cardano-sdk/web-extension'; - -export enum LedgerMultidelegationMinAppVersion { - MAJOR = 6, - MINOR = 1, - PATCH = 2 -} - -export enum TrezorMultidelegationFirmwareMinVersion { - MAJOR = 2, - MINOR = 6, - PATCH = 4 -} - -export const isMultidelegationSupportedByDevice = async ( - walletType: Exclude -): Promise => { - switch (walletType) { - case WalletType.Ledger: { - const ledgerInfo = await HardwareLedger.LedgerKeyAgent.getAppVersion(Wallet.KeyManagement.CommunicationType.Web); - return ( - ledgerInfo.version.major >= LedgerMultidelegationMinAppVersion.MAJOR && - ledgerInfo.version.minor >= LedgerMultidelegationMinAppVersion.MINOR && - ledgerInfo.version.patch >= LedgerMultidelegationMinAppVersion.PATCH - ); - } - case WalletType.Trezor: { - // To allow checks once the app is refreshed. It won't affect the user flow - // TODO: Smarter Trezor initialization logic after onboarding revamp LW-9808 - await HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ - manifest: Wallet.manifest, - communicationType: Wallet.KeyManagement.CommunicationType.Web - }); - const trezorInfo = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection( - Wallet.KeyManagement.CommunicationType.Web - ); - return ( - trezorInfo.major_version >= TrezorMultidelegationFirmwareMinVersion.MAJOR && - trezorInfo.minor_version >= TrezorMultidelegationFirmwareMinVersion.MINOR && - trezorInfo.patch_version >= TrezorMultidelegationFirmwareMinVersion.PATCH - ); - } - default: - return true; - } -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts index 2ac30533f..5d897b296 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/index.ts @@ -1,2 +1,2 @@ export * from './components/StakingContainer'; -export * from './helpers'; +export * from './isMultidelegationSupportedByDevice'; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts new file mode 100644 index 000000000..c31f2fda0 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/isMultidelegationSupportedByDevice.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-magic-numbers */ +import { Wallet } from '@lace/cardano'; +import { WalletType } from '@cardano-sdk/web-extension'; + +export enum LedgerMultidelegationMinAppVersion { + MAJOR = 6, + MINOR = 1, + PATCH = 2 +} + +export enum TrezorMultidelegationFirmwareMinVersion { + MAJOR = 2, + MINOR = 6, + PATCH = 4 +} + +export const isMultidelegationSupportedByDevice = async (walletType: Wallet.HardwareWallets): Promise => { + const version = await Wallet.initConnectionAndGetSoftwareVersion(walletType); + const expectedVersion = + walletType === WalletType.Ledger ? LedgerMultidelegationMinAppVersion : TrezorMultidelegationFirmwareMinVersion; + + const higherOnMajor = version.major > expectedVersion.MAJOR; + const higherOnMinor = version.major === expectedVersion.MAJOR && version.minor > expectedVersion.MINOR; + const higherOrEqualOnPatch = + version.major === expectedVersion.MAJOR && + version.minor === expectedVersion.MINOR && + version.patch >= expectedVersion.PATCH; + + return higherOnMajor || higherOnMinor || higherOrEqualOnPatch; +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx deleted file mode 100644 index ea823c23a..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { Modal, Typography } from 'antd'; -import { Button } from '@lace/common'; -import styles from './ErrorDialog.module.scss'; -import { useTranslation } from 'react-i18next'; - -const { Title, Text } = Typography; - -export type HWErrorCode = 'common' | 'notDetectedLedger' | 'notDetectedTrezor'; - -interface ErrorDialogProps { - visible: boolean; - onRetry: () => void; - errorCode: HWErrorCode; -} - -export const ErrorDialog = ({ visible, onRetry, errorCode = 'common' }: ErrorDialogProps): React.ReactElement => { - const { t } = useTranslation(); - - const ERROR_MESSAGES = { - common: { - title: t('browserView.onboarding.commonError.title'), - description: t('browserView.onboarding.commonError.description'), - confirm: t('browserView.onboarding.commonError.ok') - }, - notDetectedLedger: { - title: t('browserView.onboarding.notDetectedError.title'), - description: t('browserView.onboarding.notDetectedError.description'), - confirm: t('browserView.onboarding.notDetectedError.agree') - }, - notDetectedTrezor: { - title: t('browserView.onboarding.notDetectedError.title'), - description: t('browserView.onboarding.notDetectedError.trezorDescription'), - confirm: t('browserView.onboarding.notDetectedError.agree') - } - }; - - const error = ERROR_MESSAGES[errorCode]; - - return ( - - - {error.title} - - {error.description} - - - ); -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx deleted file mode 100644 index 8d0edd615..000000000 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow.tsx +++ /dev/null @@ -1,225 +0,0 @@ -// / -/* eslint-disable max-statements */ -/* eslint-disable react/no-multi-comp */ -import { useWalletManager, useTimeSpentOnPage, useLocalStorage } from '@hooks'; -import { WalletSetupSelectAccountsStepRevamp, WalletSetupConnectHardwareWalletStepRevamp } from '@lace/core'; -import React, { useState, useCallback, useEffect } from 'react'; -import { Switch, Route, useHistory, useLocation, Redirect } from 'react-router-dom'; -import { Wallet } from '@lace/cardano'; -import { WalletSetupLayout } from '@src/views/browser-view/components/Layout'; -import { ErrorDialog, HWErrorCode } from './ErrorDialog'; -import { StartOverDialog } from '@views/browser/features/wallet-setup/components/StartOverDialog'; -import { useTranslation } from 'react-i18next'; -import { EnhancedAnalyticsOptInStatus, postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; -import { config } from '@src/config'; -import { walletRoutePaths } from '@routes/wallet-paths'; -import { getHWPersonProperties, isTrezorHWSupported } from '../helpers'; -import { useAnalyticsContext } from '@providers'; -import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config'; -import { SendOnboardingAnalyticsEvent } from '../types'; -import { WalletType } from '@cardano-sdk/web-extension'; - -const { CHAIN } = config(); -const { AVAILABLE_WALLETS } = Wallet; -export interface HardwareWalletFlowProps { - onCancel: () => void; - onAppReload: () => void; - sendAnalytics: SendOnboardingAnalyticsEvent; -} - -type HardwareWalletStep = 'connect' | 'setup'; - -const TOTAL_ACCOUNTS = 50; - -const route = (path: string) => `${walletRoutePaths.setup.hardware}/${path}`; - -export const HardwareWalletFlow = ({ - onCancel, - onAppReload, - sendAnalytics -}: HardwareWalletFlowProps): React.ReactElement => { - const history = useHistory(); - const location = useLocation(); - const { t } = useTranslation(); - const [isErrorDialogVisible, setIsErrorDialogVisible] = useState(false); - const [hardwareWalletErrorCode, setHardwareWalletErrorCode] = useState('common'); - const [isStartOverDialogVisible, setIsStartOverDialogVisible] = useState(false); - const showStartOverDialog = () => setIsStartOverDialogVisible(true); - const [walletCreated, setWalletCreated] = useState(); - const [deviceConnection, setDeviceConnection] = useState(); - const [connectedDevice, setConnectedDevice] = useState(); - const [accountIndex, setAccountIndex] = useState(0); - const [isSubmitting, setIsSubmitting] = useState(false); - const { createHardwareWallet, connectHardwareWallet, saveHardwareWallet } = useWalletManager(); - const { updateEnteredAtTime } = useTimeSpentOnPage(); - const analytics = useAnalyticsContext(); - - useEffect(() => { - updateEnteredAtTime(); - }, [location.pathname, updateEnteredAtTime]); - - const showHardwareWalletError = (errorCode: HWErrorCode) => { - setHardwareWalletErrorCode(errorCode); - setIsErrorDialogVisible(true); - }; - - const walletSetupConnectHardwareWalletStepTranslations = { - title: t('core.walletSetupConnectHardwareWalletStep.title'), - subTitle: t(`core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'subTitleFull' : 'subTitle'}`), - supportedDevices: t( - `core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'supportedDevicesFull' : 'supportedDevices'}` - ), - connectDevice: t( - `core.walletSetupConnectHardwareWalletStep.${isTrezorHWSupported() ? 'connectDeviceFull' : 'connectDevice'}` - ) - }; - - const navigateTo = useCallback( - (nexthPath: string) => { - history.replace(route(nexthPath)); - }, - [history] - ); - - const [enhancedAnalyticsStatus] = useLocalStorage( - ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY, - EnhancedAnalyticsOptInStatus.OptedOut - ); - - const handleCreateWallet = async (name: string) => { - try { - const cardanoWallet = await createHardwareWallet({ - accountIndex, - deviceConnection, - name, - connectedDevice - }); - setWalletCreated(cardanoWallet); - } catch (error) { - console.error('ERROR creating hardware wallet', { error }); - showHardwareWalletError('common'); - } - }; - - const handleConnect = async (model: Wallet.HardwareWallets) => { - try { - const connection = await connectHardwareWallet(model); - setDeviceConnection(connection); - setConnectedDevice(model); - } catch (error) { - console.error('ERROR connecting hardware wallet', error); - if (error.innerError?.innerError?.message === 'The device is already open.') { - setDeviceConnection(deviceConnection); - } else { - showHardwareWalletError(model === WalletType.Trezor ? 'notDetectedTrezor' : 'notDetectedLedger'); - } - } - }; - - const handleFinishCreation = () => saveHardwareWallet(walletCreated, CHAIN); - - const handleGoToMyWalletClick = async () => { - try { - const posthogProperties = await getHWPersonProperties(connectedDevice, deviceConnection); - await sendAnalytics(postHogOnboardingActions.hw.DONE_GO_TO_WALLET, { - ...posthogProperties, - // eslint-disable-next-line camelcase - $set: { wallet_accounts_quantity: '1' } - }); - } catch { - console.error('We were not able to send the analytics event'); - } finally { - await handleFinishCreation(); - if (enhancedAnalyticsStatus === EnhancedAnalyticsOptInStatus.OptedIn) { - await analytics.sendAliasEvent(); - } - } - }; - - const onHardwareWalletDisconnect = useCallback((event: HIDConnectionEvent) => { - if (event.device.opened) showHardwareWalletError('common'); - }, []); - - useEffect(() => { - navigator.hid.addEventListener('disconnect', onHardwareWalletDisconnect); - return () => { - navigator.hid.removeEventListener('disconnect', onHardwareWalletDisconnect); - }; - }, [onHardwareWalletDisconnect]); - - const handleEnterWallet = async (account: number, name: string) => { - sendAnalytics(postHogOnboardingActions.hw.SETUP_HW_WALLET_NEXT_CLICK); - setAccountIndex(account); - setIsSubmitting(true); - await handleCreateWallet(name); - setIsSubmitting(false); - await handleGoToMyWalletClick(); - }; - - const hardwareWalletStepRenderFunctions: Record JSX.Element> = { - connect: () => ( - { - analytics.sendEventToPostHog(postHogOnboardingActions.hw.CONNECT_HW_NEXT_CLICK); - navigateTo('setup'); - }} - isNextEnable={!!deviceConnection} - translations={walletSetupConnectHardwareWalletStepTranslations} - isHardwareWallet - /> - ), - setup: () => ( - - ) - }; - - const goBackToConnect = () => { - /* eslint-disable unicorn/no-useless-undefined */ - setDeviceConnection(undefined); - setConnectedDevice(undefined); - setAccountIndex(0); - setWalletCreated(undefined); - history.replace(route('connect')); - }; - - const onRetry = () => { - setIsErrorDialogVisible(false); - goBackToConnect(); - // TODO: Remove this workaround with full app reload when SDK allows to connect Hardware Wallet for the 2nd time. - onAppReload(); - }; - - const handleStartOver = () => { - setIsStartOverDialogVisible(false); - goBackToConnect(); - // TODO: Remove this workaround with full app reload when SDK allows to connect Hardware Wallet for the 2nd time. - onAppReload(); - }; - - return ( - <> - - setIsStartOverDialogVisible(false)} - /> - - - {hardwareWalletStepRenderFunctions.connect()} - {hardwareWalletStepRenderFunctions.setup()} - - - - - ); -}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx new file mode 100644 index 000000000..2fcdcf58f --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/HardwareWalletFlow.tsx @@ -0,0 +1,178 @@ +/* eslint-disable unicorn/no-useless-undefined */ +import { useTimeSpentOnPage } from '@hooks'; +import { WalletSetupSelectAccountsStepRevamp } from '@lace/core'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Redirect, Route, Switch, useHistory } from 'react-router-dom'; +import { Wallet } from '@lace/cardano'; +import { WalletSetupLayout } from '@src/views/browser-view/components/Layout'; +import { makeErrorDialog } from './makeErrorDialog'; +import { StartOverDialog } from '@views/browser/features/wallet-setup/components/StartOverDialog'; +import { walletRoutePaths } from '@routes/wallet-paths'; +import { StepConnect } from './StepConnect'; +import { WalletType } from '@cardano-sdk/web-extension'; +import { StepCreate, WalletData } from './StepCreate'; +import { useAnalyticsContext } from '@providers'; +import { postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; + +export interface HardwareWalletFlowProps { + onCancel: () => void; +} + +const TOTAL_ACCOUNTS = 50; +const route = (path: FlowStep) => `${walletRoutePaths.setup.hardware}/${path}`; + +enum FlowStep { + Connect = 'Connect', + Setup = 'Setup', + Create = 'Create' +} + +enum ErrorDialogCode { + DeviceDisconnected = 'DeviceDisconnected', + PublicKeyExportRejected = 'PublicKeyExportRejected', + Generic = 'Generic' +} + +const commonErrorDialogTranslationKeys = { + title: 'browserView.onboarding.errorDialog.title' as const, + confirm: 'browserView.onboarding.errorDialog.cta' as const +}; +const ErrorDialog = makeErrorDialog({ + [ErrorDialogCode.DeviceDisconnected]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messageDeviceDisconnected' + }, + [ErrorDialogCode.PublicKeyExportRejected]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messagePublicKeyExportRejected' + }, + [ErrorDialogCode.Generic]: { + ...commonErrorDialogTranslationKeys, + description: 'browserView.onboarding.errorDialog.messageGeneric' + } +}); + +export const HardwareWalletFlow = ({ onCancel }: HardwareWalletFlowProps): React.ReactElement => { + const history = useHistory(); + const [connectedUsbDevice, setConnectedUsbDevice] = useState(); + const [errorDialogCode, setErrorDialogCode] = useState(); + const [isStartOverDialogVisible, setIsStartOverDialogVisible] = useState(false); + const [connection, setConnection] = useState(); + const [walletData, setWalletData] = useState(); + const { updateEnteredAtTime } = useTimeSpentOnPage(); + const analytics = useAnalyticsContext(); + + useEffect(() => { + updateEnteredAtTime(); + }, [history.location.pathname, updateEnteredAtTime]); + + useEffect(() => { + const onHardwareWalletDisconnect = (event: USBConnectionEvent) => { + if (event.device !== connectedUsbDevice || !connection) return; + setErrorDialogCode(ErrorDialogCode.DeviceDisconnected); + }; + + navigator.usb.addEventListener('disconnect', onHardwareWalletDisconnect); + return () => { + navigator.usb.removeEventListener('disconnect', onHardwareWalletDisconnect); + }; + }, [connectedUsbDevice, connection]); + + const navigateTo = useCallback( + (nexthPath: FlowStep) => { + history.replace(route(nexthPath)); + }, + [history] + ); + + const onConnected = useCallback( + (result?: Wallet.HardwareWalletConnection) => { + if (result) { + setConnection(result); + } + navigateTo(FlowStep.Setup); + }, + [navigateTo] + ); + + const closeConnection = () => { + if (connection.type === WalletType.Ledger) { + void connection.value.transport.close(); + } + }; + + const onAccountAndNameSubmit = async (accountIndex: number, name: string) => { + setWalletData({ + accountIndex, + name + }); + navigateTo(FlowStep.Create); + }; + + const cleanupConnectionState = () => { + setConnection(undefined); + navigateTo(FlowStep.Connect); + closeConnection(); + }; + + const onRetry = () => { + setErrorDialogCode(undefined); + cleanupConnectionState(); + }; + + const handleStartOver = () => { + setIsStartOverDialogVisible(false); + cleanupConnectionState(); + }; + + const onWalletCreateError = (error: Error) => { + let errorCode: ErrorDialogCode = ErrorDialogCode.Generic; + + const ledgerPkRejection = + error.message.includes('Failed to export extended account public key') && + error.message.includes('Action rejected by user'); + const trezorPkRejection = error.message.includes('Trezor transport failed'); + if (ledgerPkRejection || trezorPkRejection) { + errorCode = ErrorDialogCode.PublicKeyExportRejected; + } + + setErrorDialogCode(errorCode); + closeConnection(); + }; + + return ( + <> + {!!errorDialogCode && } + setIsStartOverDialogVisible(false)} + /> + + + + + + {!!connection && ( + <> + + setIsStartOverDialogVisible(true)} + onSubmit={onAccountAndNameSubmit} + onSelectedAccountChange={() => { + void analytics.sendEventToPostHog(postHogOnboardingActions.hw?.SETUP_HW_ACCOUNT_NO_CLICK); + }} + /> + + + + + + )} + + + + + ); +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx new file mode 100644 index 000000000..d852733eb --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepConnect.tsx @@ -0,0 +1,144 @@ +/* eslint-disable unicorn/no-null */ +import { UseWalletManager, useWalletManager } from '@hooks'; +import { Wallet } from '@lace/cardano'; +import { WalletSetupConnectHardwareWalletStepRevamp } from '@lace/core'; +import { TranslationKey } from '@lib/translations/types'; +import { TFunction } from 'i18next'; +import React, { useCallback, useEffect, useState, VFC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAnalyticsContext } from '@providers'; +import { postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; + +export const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; + +const requestHardwareWalletConnection = (): Promise => + navigator.usb.requestDevice({ + filters: isTrezorHWSupported() ? Wallet.supportedHwUsbDescriptors : Wallet.ledgerDescriptors + }); + +const threeSecondsTimeout = 3000; +const timeoutErrorMessage = 'Timeout. Connecting too long.'; + +const isTimeoutError = (error: Error): boolean => error.message === timeoutErrorMessage; + +const useConnectHardwareWalletWithTimeout = (connect: UseWalletManager['connectHardwareWalletRevamped']) => + useCallback( + async (usbDevice: USBDevice) => { + const result = await Promise.race([ + connect(usbDevice), + new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), threeSecondsTimeout)) + ]); + + if (result === 'timeout') { + throw new Error(timeoutErrorMessage); + } + + return result; + }, + [connect] + ); + +type ConnectionError = + | 'userGestureRequired' + | 'devicePickerRejected' + | 'deviceLocked' + | 'deviceBusy' + | 'cardanoAppNotOpen' + | 'generic'; + +const connectionSubtitleErrorTranslationsMap: Record = { + cardanoAppNotOpen: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.cardanoAppNotOpen', + deviceLocked: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceLocked', + deviceBusy: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.deviceBusy', + devicePickerRejected: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.devicePickerRejected', + userGestureRequired: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.userGestureRequired', + generic: 'core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.generic' +}; + +const makeTranslations = ({ connectionError, t }: { connectionError: ConnectionError; t: TFunction }) => ({ + title: t('core.walletSetupConnectHardwareWalletStepRevamp.title'), + subTitle: isTrezorHWSupported() + ? t('core.walletSetupConnectHardwareWalletStepRevamp.subTitle') + : t('core.walletSetupConnectHardwareWalletStepRevamp.subTitleLedgerOnly'), + errorMessage: connectionError ? t(connectionSubtitleErrorTranslationsMap[connectionError]) : '', + errorCta: t('core.walletSetupConnectHardwareWalletStepRevamp.errorCta') +}); + +const parseConnectionError = (error: Error): ConnectionError | null => { + if (error instanceof DOMException) { + if (error.message.includes('user gesture')) return 'userGestureRequired'; + if (error.message.includes('No device selected')) return 'devicePickerRejected'; + } + if (isTimeoutError(error)) return 'deviceBusy'; + if (error.message.includes('Cannot communicate with Ledger Cardano App')) { + if (error.message.includes('General error 0x5515')) return 'deviceLocked'; + if (error.message.includes('General error 0x6e01')) return 'cardanoAppNotOpen'; + } + return 'generic'; +}; + +enum DiscoveryState { + Idle = 'Idle', + Requested = 'Requested', + Running = 'Running' +} + +type StepConnectProps = { + onBack: () => void; + onConnected: (result?: Wallet.HardwareWalletConnection) => void; + onUsbDeviceChange: (usbDevice: USBDevice) => void; +}; + +export const StepConnect: VFC = ({ onBack, onConnected, onUsbDeviceChange }) => { + const { t } = useTranslation(); + const [discoveryState, setDiscoveryState] = useState(DiscoveryState.Requested); + const [connectionError, setConnectionError] = useState(null); + const { connectHardwareWalletRevamped } = useWalletManager(); + const analytics = useAnalyticsContext(); + + const translations = makeTranslations({ connectionError, t }); + const connect = useConnectHardwareWalletWithTimeout(connectHardwareWalletRevamped); + + const onRetry = useCallback(() => { + setDiscoveryState(DiscoveryState.Requested); + setConnectionError(null); + void analytics.sendEventToPostHog(postHogOnboardingActions.hw?.CONNECT_HW_TRY_AGAIN_CLICK); + }, [analytics]); + + useEffect(() => { + (async () => { + if (discoveryState !== DiscoveryState.Requested) return; + + setDiscoveryState(DiscoveryState.Running); + let connectionResult: Wallet.HardwareWalletConnection; + try { + void analytics.sendEventToPostHog(postHogOnboardingActions.hw?.CONNECT_HW_VIEW); + const usbDevice = await requestHardwareWalletConnection(); + onUsbDeviceChange(usbDevice); + connectionResult = await connect(usbDevice); + onConnected(connectionResult); + void analytics.sendEventToPostHog(postHogOnboardingActions.hw?.HW_POPUP_CONNECT_CLICK); + setDiscoveryState(DiscoveryState.Idle); + } catch (error) { + setDiscoveryState(DiscoveryState.Idle); + console.error('ERROR connecting hardware wallet', error); + + if (error.innerError?.innerError?.message === 'The device is already open.') { + onConnected(); + return; + } + + setConnectionError(parseConnectionError(error)); + } + })(); + }, [connect, discoveryState, onUsbDeviceChange, onConnected, analytics]); + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx new file mode 100644 index 000000000..1b7ffc593 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/StepCreate.tsx @@ -0,0 +1,93 @@ +import { Wallet } from '@lace/cardano'; +import { WalletSetupHWCreationStep } from '@lace/core'; +import { EnhancedAnalyticsOptInStatus, postHogOnboardingActions } from '@providers/AnalyticsProvider/analyticsTracker'; +import { TFunction } from 'i18next'; +import React, { VFC, useMemo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocalStorage, useWalletManager } from '@hooks'; +import { config } from '@src/config'; +import { useAnalyticsContext } from '@providers'; +import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config'; + +const { CHAIN } = config(); + +const makeWalletSetupCreateStepTranslations = (t: TFunction) => ({ + title: t('core.walletSetupCreateStep.title'), + description: t('core.walletSetupCreateStep.description') +}); + +enum CreationState { + Idle = 'Idle', + Working = 'Working' +} + +export type WalletData = { + accountIndex: number; + name: string; +}; + +type StepCreateProps = { + connection: Wallet.HardwareWalletConnection; + onError: (error: Error) => void; + walletData: WalletData; +}; + +export const StepCreate: VFC = ({ connection, onError, walletData }) => { + const { t } = useTranslation(); + const [status, setStatus] = useState(CreationState.Idle); + const { createHardwareWalletRevamped, saveHardwareWallet } = useWalletManager(); + const analytics = useAnalyticsContext(); + const [enhancedAnalyticsStatus] = useLocalStorage( + ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY, + EnhancedAnalyticsOptInStatus.OptedOut + ); + + const walletSetupCreateStepTranslations = useMemo(() => makeWalletSetupCreateStepTranslations(t), [t]); + + useEffect(() => { + (async () => { + if (status !== CreationState.Idle) return; + setStatus(CreationState.Working); + + let cardanoWallet: Wallet.CardanoWallet; + try { + cardanoWallet = await createHardwareWalletRevamped({ + connection, + ...walletData + }); + } catch (error) { + console.error('ERROR creating hardware wallet', { error }); + onError(error); + throw error; + } + + const deviceSpec = await Wallet.getDeviceSpec(connection); + void analytics.sendEventToPostHog(postHogOnboardingActions.hw.ENTER_WALLET, { + /* eslint-disable camelcase */ + $set_once: { + initial_hardware_wallet_model: deviceSpec.model, + initial_firmware_version: deviceSpec?.firmwareVersion, + initial_cardano_app_version: deviceSpec?.cardanoAppVersion + }, + $set: { wallet_accounts_quantity: '1' } + /* eslint-enable camelcase */ + }); + + await saveHardwareWallet(cardanoWallet, CHAIN); + if (enhancedAnalyticsStatus === EnhancedAnalyticsOptInStatus.OptedIn) { + await analytics.sendAliasEvent(); + } + })(); + }, [ + analytics, + connection, + createHardwareWalletRevamped, + enhancedAnalyticsStatus, + onError, + saveHardwareWallet, + status, + walletData + ]); + + return ; +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts new file mode 100644 index 000000000..c7bf754c0 --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/index.ts @@ -0,0 +1,2 @@ +export { HardwareWalletFlow } from './HardwareWalletFlow'; +export { makeErrorDialog } from './makeErrorDialog'; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss similarity index 84% rename from apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss rename to apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss index 5e7985546..79059e6cb 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/ErrorDialog.module.scss +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.module.scss @@ -1,5 +1,5 @@ -@import '../../../../../../../../packages/common/src/ui/styles/theme.scss'; -@import '../../../../../../../../packages/common/src/ui/styles/abstracts/typography'; +@import '../../../../../../../../../packages/common/src/ui/styles/theme'; +@import '../../../../../../../../../packages/common/src/ui/styles/abstracts/typography'; .errorDialog { :global(.ant-modal-content) { diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx new file mode 100644 index 000000000..b22f2ca2a --- /dev/null +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/HardwareWalletFlow/makeErrorDialog.tsx @@ -0,0 +1,43 @@ +import { TranslationKey } from '@lib/translations/types'; +import React from 'react'; +import { Modal, Typography } from 'antd'; +import { Button } from '@lace/common'; +import styles from './makeErrorDialog.module.scss'; +import { useTranslation } from 'react-i18next'; + +const { Title, Text } = Typography; + +type TranslationKeys = Record<'title' | 'description' | 'confirm', TranslationKey>; + +interface ErrorDialogProps { + visible: boolean; + onRetry: () => void; + errorCode?: ErrorCode; +} + +export const makeErrorDialog = + (translationsMap: Record) => + ({ visible, onRetry, errorCode }: ErrorDialogProps): React.ReactElement => { + const { t } = useTranslation(); + const errorTranslationKeys = translationsMap[errorCode]; + + return ( + + + {t(errorTranslationKeys.title)} + + {t(errorTranslationKeys.description)} + + + ); + }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx index 9e7b7635b..4c6bff0ef 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetup.tsx @@ -107,11 +107,7 @@ export const WalletSetup = ({ initialStep = WalletSetupSteps.Mnemonic }: WalletS /> - location.reload()} - sendAnalytics={sendAnalyticsHandler} - /> + )} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx index 52ac42238..085fb99d5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/wallet-setup/components/WalletSetupMainPage.tsx @@ -22,9 +22,8 @@ import { useHistory } from 'react-router-dom'; export const WalletSetupMainPage = (): ReactElement => { const history = useHistory(); - const [isDappConnectorWarningOpen, setIsDappConnectorWarningOpen] = useState(false); const [isAnalyticsModalOpen, setIsAnalyticsModalOpen] = useState(false); - const { t: translate, Trans } = useTranslate(); + const { t: translate } = useTranslate(); const analytics = useAnalyticsContext(); const [enhancedAnalyticsStatus, { updateLocalStorage: setDoesUserAllowAnalytics }] = useLocalStorage( @@ -53,7 +52,7 @@ export const WalletSetupMainPage = (): ReactElement => { }; const handleStartHardwareOnboarding = () => { - setIsDappConnectorWarningOpen(true); + history.push(walletRoutePaths.setup.hardware); analytics.sendEventToPostHog(postHogOnboardingActions.hw?.SETUP_OPTION_CLICK); }; @@ -100,23 +99,6 @@ export const WalletSetupMainPage = (): ReactElement => { onRestoreWalletRequest={handleRestoreWallet} translations={walletSetupOptionsStepTranslations} /> - -

- -

- - } - visible={isDappConnectorWarningOpen} - confirmLabel={translate('browserView.walletSetup.confirmExperimentalHwDapp.confirm')} - onCancel={() => setIsDappConnectorWarningOpen(false)} - onConfirm={() => { - setIsDappConnectorWarningOpen(false); - history.push(walletRoutePaths.setup.hardware); - }} - /> process.env.USE_TREZOR_HW === 'true'; -export const isHardwareWalletAvailable = (wallet: Wallet.HardwareWallets): boolean => - wallet !== WalletType.Trezor || isTrezorHWSupported(); -type HardwareWalletPersonProperties = { - model: string; - firmwareVersion?: string; - cardanoAppVersion?: string; -}; - -export const getTrezorSpecifications = async (): Promise => { - const { model, major_version, minor_version, patch_version } = - await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(Wallet.KeyManagement.CommunicationType.Web); - return { - model: `${WalletType.Trezor} model ${model}`, - firmwareVersion: `${major_version}.${minor_version}.${patch_version}` - }; -}; - -export const getLedgerSpecifications = async ( - deviceConnection: HardwareLedger.LedgerKeyAgent['deviceConnection'] -): Promise => { - const cardanoApp = await deviceConnection.getVersion(); - return { - model: deviceConnection.transport.deviceModel.id, - cardanoAppVersion: `${cardanoApp.version.major}.${cardanoApp.version.minor}.${cardanoApp.version.patch}` - }; -}; - -export const getHWPersonProperties = async ( - connectedDevice: Wallet.HardwareWallets, - deviceConnection: Wallet.DeviceConnection -): Promise => { - // TODO: Remove these hardcoded specs once we have a logic that will prevent additional interaction with 3rd party Trezor Connect popup - const trezorSpecificationsHC: HardwareWalletPersonProperties = { - // We are only accepting Model T for now - model: 'Trezor model T' - }; - const HWSpecifications = - connectedDevice === WalletType.Trezor - ? trezorSpecificationsHC - : await getLedgerSpecifications(deviceConnection as HardwareLedger.LedgerKeyAgent['deviceConnection']); - return { - $set_once: { - initial_hardware_wallet_model: HWSpecifications.model, - initial_firmware_version: HWSpecifications?.firmwareVersion, - initial_cardano_app_version: HWSpecifications?.cardanoAppVersion - } - }; -}; diff --git a/apps/browser-extension-wallet/webpack.common.dev.js b/apps/browser-extension-wallet/webpack.common.dev.js index ef6db8300..35825150a 100644 --- a/apps/browser-extension-wallet/webpack.common.dev.js +++ b/apps/browser-extension-wallet/webpack.common.dev.js @@ -48,7 +48,7 @@ module.exports = path: '.env', safe: false, silent: false, - defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.devpreview' : true, + defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.developerpreview' : true, systemvars: true, allowEmptyValues: true }), diff --git a/apps/browser-extension-wallet/webpack.common.js b/apps/browser-extension-wallet/webpack.common.js index fdd930229..8c4d06e13 100644 --- a/apps/browser-extension-wallet/webpack.common.js +++ b/apps/browser-extension-wallet/webpack.common.js @@ -86,7 +86,7 @@ module.exports = () => { path: '.env', safe: false, silent: false, - defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.devpreview' : true, + defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.developerpreview' : true, systemvars: true, allowEmptyValues: true }), diff --git a/apps/browser-extension-wallet/webpack.common.prod.js b/apps/browser-extension-wallet/webpack.common.prod.js index 98a4e4e58..73cff79a0 100644 --- a/apps/browser-extension-wallet/webpack.common.prod.js +++ b/apps/browser-extension-wallet/webpack.common.prod.js @@ -16,7 +16,7 @@ module.exports = () => ({ path: '.env', safe: false, silent: false, - defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.devpreview' : true, + defaults: process.env.BUILD_DEV_PREVIEW === 'true' ? '.env.developerpreview' : true, systemvars: true, allowEmptyValues: true }), diff --git a/packages/cardano/package.json b/packages/cardano/package.json index 484414c1b..4d9f04903 100644 --- a/packages/cardano/package.json +++ b/packages/cardano/package.json @@ -27,7 +27,7 @@ ], "scripts": { "build": "run -T rollup -c rollup.config.js", - "cleanup": "yarn exec rm -rf dist node_modules .rollup.cache src/dist", + "cleanup": "yarn exec rm -rf dist node_modules", "lint": "cd ../.. && yarn cardano:lint", "prepack": "yarn build", "prepare": "ts-patch install -s", @@ -38,17 +38,19 @@ "watch": "yarn build --watch" }, "dependencies": { - "@cardano-sdk/cardano-services-client": "0.18.0", + "@cardano-sdk/cardano-services-client": "0.19.0", "@cardano-sdk/core": "0.30.0", "@cardano-sdk/crypto": "0.1.22", - "@cardano-sdk/hardware-ledger": "0.8.19", - "@cardano-sdk/hardware-trezor": "0.4.19", + "@cardano-sdk/hardware-ledger": "0.9.1", + "@cardano-sdk/hardware-trezor": "0.4.20", "@cardano-sdk/key-management": "0.20.1", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.37.0", + "@cardano-sdk/web-extension": "0.27.0", "@lace/common": "0.1.0", + "@ledgerhq/devices": "^8.2.1", "@stablelib/chacha20poly1305": "1.0.1", + "@trezor/transport": "^1.1.18", "bignumber.js": "9.0.1", "buffer": "6.0.3", "classnames": "2.3.1", diff --git a/packages/cardano/src/wallet/lib/__tests__/get-block-info-by-hash.test.ts b/packages/cardano/src/wallet/lib/__tests__/get-block-info-by-hash.test.ts index 7f75cf83a..e10093999 100644 --- a/packages/cardano/src/wallet/lib/__tests__/get-block-info-by-hash.test.ts +++ b/packages/cardano/src/wallet/lib/__tests__/get-block-info-by-hash.test.ts @@ -1,7 +1,6 @@ /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable promise/avoid-new */ import '@testing-library/jest-dom'; import { getBlockInfoByHash } from '../get-block-info-by-hash'; import { stakepoolSearchProviderStub, mockedStakePools } from '../../test/mocks/StakepoolSearchProviderStub'; diff --git a/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts b/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts index 37a0952b1..96533029e 100644 --- a/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts +++ b/packages/cardano/src/wallet/lib/__tests__/get-inputs-value.test.ts @@ -1,7 +1,6 @@ /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable promise/avoid-new */ /* eslint-disable max-len */ /* eslint-disable sonarjs/no-identical-functions */ import '@testing-library/jest-dom'; diff --git a/packages/cardano/src/wallet/lib/cardano-wallet.ts b/packages/cardano/src/wallet/lib/cardano-wallet.ts index e17f23713..90aadb267 100644 --- a/packages/cardano/src/wallet/lib/cardano-wallet.ts +++ b/packages/cardano/src/wallet/lib/cardano-wallet.ts @@ -27,6 +27,7 @@ export interface WalletMetadata { name: string; lockValue?: HexBlob; lastActiveAccountIndex?: number; + walletAddresses?: Cardano.PaymentAddress[]; } export interface AccountMetadata { diff --git a/packages/cardano/src/wallet/lib/hardware-wallet.ts b/packages/cardano/src/wallet/lib/hardware-wallet.ts index ded662d2c..883d35fab 100644 --- a/packages/cardano/src/wallet/lib/hardware-wallet.ts +++ b/packages/cardano/src/wallet/lib/hardware-wallet.ts @@ -1,10 +1,12 @@ /* eslint-disable unicorn/no-null */ +import { Bip32PublicKeyHex } from '@cardano-sdk/crypto'; import * as KeyManagement from '@cardano-sdk/key-management'; -import { DeviceConnection, HardwareWallets } from '../types'; +import { HardwareWalletConnection, DeviceConnection, HardwareWallets, LedgerConnection } from '../types'; import * as HardwareLedger from '@cardano-sdk/hardware-ledger'; import * as HardwareTrezor from '@cardano-sdk/hardware-trezor'; import { WalletType } from '@cardano-sdk/web-extension'; -// Using nodejs CML version to satisfy the tests requirements, but this gets replaced by webpack to the browser version in the build +import { ledgerUSBVendorId } from '@ledgerhq/devices'; +import { TREZOR_USB_DESCRIPTORS } from '@trezor/transport'; const isTrezorHWSupported = (): boolean => process.env.USE_TREZOR_HW === 'true'; @@ -20,16 +22,150 @@ export const manifest: KeyManagement.TrezorConfig['manifest'] = { email: process.env.EMAIL_ADDRESS }; +const initializeTrezor = () => + HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ + manifest, + communicationType: DEFAULT_COMMUNICATION_TYPE + }); + const connectDevices: Record Promise> = { [WalletType.Ledger]: async () => await HardwareLedger.LedgerKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE), ...(AVAILABLE_WALLETS.includes(WalletType.Trezor) && { - [WalletType.Trezor]: async () => - await HardwareTrezor.TrezorKeyAgent.initializeTrezorTransport({ - manifest, - communicationType: DEFAULT_COMMUNICATION_TYPE - }) + [WalletType.Trezor]: async () => await initializeTrezor() }) }; export const connectDevice = async (model: HardwareWallets): Promise => await connectDevices[model](); + +type Descriptor = Partial; +type DescriptorEntries = [keyof T, T[keyof T]][]; +const isDeviceDescribedBy = (device: USBDevice, descriptors: Descriptor[]) => + descriptors.some((descriptor) => + (Object.entries(descriptor) as DescriptorEntries).every(([key, value]) => device[key] === value) + ); + +const ledgerNanoSWithNoAppOpenProductId = 4113; +const ledgerNanoSWithCardanoAppOpenProductId = 4117; +const ledgerNanoSPlusWithNoAppOpenProductId = 20_497; +const ledgerNanoSPlusWithCardanoAppOpenProductId = 20_501; +const ledgerNanoXWithNoAppOpenProductId = 16_401; +const ledgerNanoXWithCardanoAppOpenProductId = 16_405; +export const ledgerDescriptors = [ + ledgerNanoSWithNoAppOpenProductId, + ledgerNanoSWithCardanoAppOpenProductId, + ledgerNanoSPlusWithNoAppOpenProductId, + ledgerNanoSPlusWithCardanoAppOpenProductId, + ledgerNanoXWithNoAppOpenProductId, + ledgerNanoXWithCardanoAppOpenProductId +].map((productId) => ({ + vendorId: ledgerUSBVendorId, + productId +})); + +// eslint-disable-next-line unicorn/number-literal-case +const trezorModelTProductId = 0x53_c1; +const trezorDescriptors = TREZOR_USB_DESCRIPTORS.filter(({ productId }) => productId === trezorModelTProductId); +export const supportedHwUsbDescriptors = [...ledgerDescriptors, ...trezorDescriptors]; + +export const connectDeviceRevamped = async (usbDevice: USBDevice): Promise => { + if (isDeviceDescribedBy(usbDevice, ledgerDescriptors)) { + return { + type: WalletType.Ledger, + value: await HardwareLedger.LedgerKeyAgent.establishDeviceConnection(DEFAULT_COMMUNICATION_TYPE, usbDevice) + }; + } + if (isTrezorHWSupported() && isDeviceDescribedBy(usbDevice, trezorDescriptors)) { + await initializeTrezor(); + return { + type: WalletType.Trezor + }; + } + + throw new Error('Could not recognize the device'); +}; + +const invalidDeviceError = new Error('Invalid device type'); + +export const getHwExtendedAccountPublicKey = async ( + walletType: HardwareWallets, + accountIndex: number, + ledgerConnection?: LedgerConnection +): Promise => { + if (walletType === WalletType.Ledger) { + return HardwareLedger.LedgerKeyAgent.getXpub({ + communicationType: DEFAULT_COMMUNICATION_TYPE, + deviceConnection: ledgerConnection, + accountIndex + }); + } + if (isTrezorHWSupported() && walletType === WalletType.Trezor) { + return HardwareTrezor.TrezorKeyAgent.getXpub({ + communicationType: DEFAULT_COMMUNICATION_TYPE, + accountIndex + }); + } + throw invalidDeviceError; +}; + +type DeviceSpec = { + model: string; + firmwareVersion?: string; + cardanoAppVersion?: string; +}; + +const makeVersion = (major: number, minor: number, patch: number) => `${major}.${minor}.${patch}`; + +export const getDeviceSpec = async (connection: HardwareWalletConnection): Promise => { + if (connection.type === WalletType.Ledger) { + const { version } = await connection.value.getVersion(); + return { + model: connection.value.transport.deviceModel.id, + cardanoAppVersion: makeVersion(version.major, version.minor, version.patch) + }; + } + if (isTrezorHWSupported() && connection.type === WalletType.Trezor) { + // TODO: Remove these hardcoded specs once we have a logic that will prevent additional interaction with 3rd party Trezor Connect popup + const hardcodeTrezorSpec = true; + if (hardcodeTrezorSpec) { + return { + model: 'Trezor model T' + }; + } + + const features = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + return { + model: `${WalletType.Trezor} model ${features.model}`, + firmwareVersion: makeVersion(features.major_version, features.minor_version, features.patch_version) + }; + } + + throw invalidDeviceError; +}; + +type SoftwareVersion = { + major: number; + minor: number; + patch: number; +}; + +export const initConnectionAndGetSoftwareVersion = async (type: HardwareWallets): Promise => { + if (type === WalletType.Ledger) { + const connection = await HardwareLedger.LedgerKeyAgent.establishDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + const { version } = await connection.getVersion(); + return version; + } + if (isTrezorHWSupported() && type === WalletType.Trezor) { + // To allow checks once the app is refreshed. It won't affect the user flow + // TODO: Smarter Trezor initialization logic after onboarding revamp LW-9808 + await initializeTrezor(); + const features = await HardwareTrezor.TrezorKeyAgent.checkDeviceConnection(DEFAULT_COMMUNICATION_TYPE); + return { + major: features.major_version, + minor: features.minor_version, + patch: features.patch_version + }; + } + + throw invalidDeviceError; +}; diff --git a/packages/cardano/src/wallet/test/mocks/AssetsProviderStub.ts b/packages/cardano/src/wallet/test/mocks/AssetsProviderStub.ts index e3c3f44bb..49cdb3b9f 100644 --- a/packages/cardano/src/wallet/test/mocks/AssetsProviderStub.ts +++ b/packages/cardano/src/wallet/test/mocks/AssetsProviderStub.ts @@ -22,15 +22,13 @@ export const mockedAssets: Asset.AssetInfo[] = [ ]; export const assetsProviderStub = (assets: Asset.AssetInfo[] = mockedAssets): AssetProvider => ({ - getAsset: jest.fn().mockImplementation( - ({ assetId }) => - // eslint-disable-next-line promise/avoid-new - new Promise((resolve) => resolve(assets.find((asset) => asset.assetId === assetId) || assets[0])) - ), - getAssets: jest.fn().mockImplementation( - ({ assetIds }) => - // eslint-disable-next-line promise/avoid-new - new Promise((resolve) => resolve(assets.filter((asset) => assetIds.includes(asset.assetId)) || assets[0])) - ), + getAsset: jest + .fn() + .mockImplementation(async ({ assetId }) => assets.find((asset) => asset.assetId === assetId) || assets[0]), + getAssets: jest + .fn() + .mockImplementation( + async ({ assetIds }) => assets.filter((asset) => assetIds.includes(asset.assetId)) || assets[0] + ), healthCheck: jest.fn().mockResolvedValue({ ok: true }) }); diff --git a/packages/cardano/src/wallet/test/mocks/TxSubmitProviderFake.ts b/packages/cardano/src/wallet/test/mocks/TxSubmitProviderFake.ts index 0428b1fd7..23c95362e 100644 --- a/packages/cardano/src/wallet/test/mocks/TxSubmitProviderFake.ts +++ b/packages/cardano/src/wallet/test/mocks/TxSubmitProviderFake.ts @@ -8,7 +8,6 @@ export const TxSubmitProviderFake = { return { submittedTxs$: submittedTxs.asObservable(), submitTx: ({ signedTransaction }): Promise => - // eslint-disable-next-line promise/avoid-new new Promise((resolve) => { submittedTxs.next(signedTransaction); resolve(); diff --git a/packages/cardano/src/wallet/types.ts b/packages/cardano/src/wallet/types.ts index 21e2dd835..2942522c5 100644 --- a/packages/cardano/src/wallet/types.ts +++ b/packages/cardano/src/wallet/types.ts @@ -2,10 +2,21 @@ import { Cardano, Paginated } from '@cardano-sdk/core'; import type { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; import { WalletType } from '@cardano-sdk/web-extension'; -export type DeviceConnection = LedgerKeyAgent['deviceConnection'] | boolean; +export type LedgerConnection = LedgerKeyAgent['deviceConnection']; + +export type DeviceConnection = LedgerConnection | boolean; export type HardwareWallets = WalletType.Trezor | WalletType.Ledger; +export type HardwareWalletConnection = + | { + type: WalletType.Trezor; + } + | { + type: WalletType.Ledger; + value: LedgerConnection; + }; + export type StakePoolSearchResults = Paginated; export type DappInfo = { @@ -69,10 +80,3 @@ export enum WalletManagerProviderTypes { } export type ChainName = keyof typeof Cardano.ChainIds; - -export interface CreateHardwareWalletArgs { - deviceConnection: DeviceConnection; - name: string; - accountIndex: number; - activeChainId: Cardano.ChainId; -} diff --git a/packages/common/package.json b/packages/common/package.json index 8f7a9b4d5..80879fc98 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -27,7 +27,7 @@ ], "scripts": { "build": "run -T rollup -c rollup.config.js", - "cleanup": "yarn exec rm -rf dist node_modules .rollup.cache src/dist", + "cleanup": "yarn exec rm -rf dist node_modules", "lint": "cd ../.. && yarn common:lint", "prepack": "yarn build", "prepare": "ts-patch install -s", diff --git a/packages/common/src/analytics/types.ts b/packages/common/src/analytics/types.ts index ef09cee36..299e5a081 100644 --- a/packages/common/src/analytics/types.ts +++ b/packages/common/src/analytics/types.ts @@ -7,14 +7,12 @@ export enum PostHogAction { OnboardingMainViewPinExtensionClick = 'wallet | onboarding | lace main view | pin the wallet extension | click', OnboardingMainViewMultiAddressModalGotItClick = 'wallet | onboarding | lace main view | multi-address modal | got it | click', // Hardware wallet connect - OnboardingHWAnalyticsAgreeClick = 'onboarding | hardware wallet | analytics | agree | click', - OnboardingHWAnalyticsSkipClick = 'onboarding | hardware wallet | analytics | skip | click', - OnboardingHWClick = 'onboarding | hardware wallet | connect | click', - OnboardinHWLaceTermsOfUseNextClick = 'onboarding | hardware wallet | lace terms of use | next | click', - OnboardingHWConnectNextClick = 'onboarding | hardware wallet | connect hw | next | click', - OnboardingHWSelectAccountNextClick = 'onboarding | hardware wallet | select hw account | next | click', - OnboardingHWNameNextClick = 'onboarding | hardware wallet | name hw wallet | next | click', - OnboardingHWDoneGoToWallet = 'onboarding | hardware wallet | all done | go to my wallet | click', + OnboardingHWClick = 'onboarding | hardware wallet revamp | connect | click', + OnboardingHWConnectView = 'onboarding | hardware wallet revamp | connect your device | view', + OnboardingHWPopupConnectClick = 'onboarding | hardware wallet revamp | native browser pop-up with HWs | connect | click', + OnboardingHWConnectTryAgainClick = 'onboarding | hardware wallet revamp | connect your device | try again | click', + OnboardingHWSetupWalletAccountNoClick = "onboarding | hardware wallet revamp | let's set up your wallet | Account No | click", + OnboardingHWEnterWalletClick = "onboarding | hardware wallet revamp | let's set up your wallet | enter wallet | click", // Restore wallet OnboardingRestoreClick = 'onboarding | restore wallet revamp | restore | click', OnboardingRestoreEnterRecoveryPhraseNextClick = 'onboarding | restore wallet revamp | enter your recovery phrase | next | click', diff --git a/packages/common/src/ui/components/Loader/Loader.tsx b/packages/common/src/ui/components/Loader/Loader.tsx index 9aa1930ee..036b3e079 100644 --- a/packages/common/src/ui/components/Loader/Loader.tsx +++ b/packages/common/src/ui/components/Loader/Loader.tsx @@ -8,5 +8,5 @@ export interface LoaderProps { } export const Loader = ({ className }: LoaderProps): React.ReactElement => ( - + ); diff --git a/packages/common/src/ui/hooks/useFetchImage.ts b/packages/common/src/ui/hooks/useFetchImage.ts index 562fd8068..06c05ed87 100644 --- a/packages/common/src/ui/hooks/useFetchImage.ts +++ b/packages/common/src/ui/hooks/useFetchImage.ts @@ -31,7 +31,6 @@ type FetchAction = { const handleImageFetch = (image: string) => { const downloadingImage = new Image(); - // eslint-disable-next-line promise/avoid-new const imageResponse: Promise = new Promise((resolve) => { const onLoadEvent = (event: any) => { resolve({ diff --git a/packages/core/package.json b/packages/core/package.json index 5be490dda..5ba6dc705 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,7 @@ "scripts": { "build": "run -T rollup -c rollup.config.js", "build-storybook": "NODE_OPTIONS=--openssl-legacy-provider; build-storybook", - "cleanup": "yarn exec rm -rf dist node_modules .rollup.cache src/dist", + "cleanup": "yarn exec rm -rf dist node_modules", "lint": "cd ../.. && yarn core:lint", "prepack": "yarn build", "prestart": "yarn build", @@ -45,7 +45,7 @@ "@lace/icons": "0.1.0", "@lace/ui": "^0.1.0", "antd": "^4.24.10", - "axios": "0.21.4", + "axios": "0.28.0", "axios-cache-adapter": "2.7.3", "classnames": "^2.3.1", "debounce-promise": "^3.1.2", diff --git a/packages/core/src/ui/components/Activity/AssetActivityItem.module.scss b/packages/core/src/ui/components/Activity/AssetActivityItem.module.scss index d483d98a6..9fb039540 100644 --- a/packages/core/src/ui/components/Activity/AssetActivityItem.module.scss +++ b/packages/core/src/ui/components/Activity/AssetActivityItem.module.scss @@ -124,10 +124,14 @@ } } -.negativeBalance { +.pendingNegativeBalance { color: var(--data-orange); } .positiveBalance { color: var(--data-green); } + +.negativeBalance { + color: var(--text-color-primary); +} diff --git a/packages/core/src/ui/components/Activity/AssetActivityItem.tsx b/packages/core/src/ui/components/Activity/AssetActivityItem.tsx index 3cfc6cd2b..ba2e441fe 100644 --- a/packages/core/src/ui/components/Activity/AssetActivityItem.tsx +++ b/packages/core/src/ui/components/Activity/AssetActivityItem.tsx @@ -202,8 +202,9 @@ export const AssetActivityItem = ({
diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss index 5e90ec99b..fa9ada969 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss @@ -102,31 +102,16 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- font-weight: 500; line-height: 17px; } + + &.addressTag { + gap: size_unit(1); + } } .timestamp { flex: 0 0 35%; } - .amount { - display: flex; - flex-direction: column; - width: 100%; - align-items: flex-end; - - .ada { - color: var(--text-color-primary, #ffffff); - } - - .fiat { - color: var(--text-color-secondary, #878e9e); - } - - .addrName { - margin-bottom: size_unit(1); - } - } - .addressDetail { font-size: var(--bodySmall, 14px); font-weight: 400; diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx index 414ddf07b..97acb4e7b 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx @@ -56,7 +56,8 @@ const data: ComponentProps = { openExternalLink: (url) => window.open(url, '_blank', 'noopener,noreferrer'), handleOpenExternalHashLink: () => { console.log('handle on hash click', '639a43144dc2c0ead16f2fb753360f4b4f536502dbdb8aa5e424b00abb7534ff'); - } + }, + ownAddresses: [] }; const stakeVoteDelegationCertificate = [ diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx index 9fba039d6..c76fcd1f0 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx @@ -2,10 +2,13 @@ /* eslint-disable no-magic-numbers */ import React from 'react'; import cn from 'classnames'; -import { TransactionDetailAsset, TransactionMetadataProps, TxOutputInput, TxSummary } from './TransactionDetailAsset'; + import { Ellipsis, toast } from '@lace/common'; import { Box } from '@lace/ui'; -import { useTranslate } from '@src/ui/hooks'; +import { useTranslate } from '@ui/hooks'; +import { getAddressTagTranslations, renderAddressTag } from '@ui/utils'; + +import { TransactionDetailAsset, TransactionMetadataProps, TxOutputInput, TxSummary } from './TransactionDetailAsset'; import CopyToClipboard from 'react-copy-to-clipboard'; import { ActivityStatus } from '../Activity'; import styles from './TransactionDetails.module.scss'; @@ -76,6 +79,7 @@ export interface TransactionDetailsProps { txSummary?: TxSummary[]; coinSymbol: string; tooltipContent?: string; + ownAddresses: string[]; addressToNameMap: Map; isPopupView?: boolean; openExternalLink?: (url: string) => void; @@ -122,6 +126,7 @@ export const TransactionDetails = ({ txSummary = [], coinSymbol, pools, + ownAddresses, addressToNameMap, isPopupView, openExternalLink, @@ -353,22 +358,22 @@ export const TransactionDetails = ({ )} {(summary.addr as string[]).map((addr) => { - const addrName = addressToNameMap?.get(addr); const address = isPopupView ? ( - + ) : ( - {addr} + + {addr} + ); return ( -
- {addrName ? ( -
- {addrName} - {address} -
- ) : ( - address - )} +
+ {address} + {renderAddressTag(addr, getAddressTagTranslations(t), ownAddresses, addressToNameMap)}
); })} @@ -590,6 +595,8 @@ export const TransactionDetails = ({ coinSymbol={coinSymbol} withSeparatorLine sendAnalytics={sendAnalyticsInputs} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} /> )} {addrOutputs?.length > 0 && ( @@ -604,6 +611,8 @@ export const TransactionDetails = ({ }} coinSymbol={coinSymbol} sendAnalytics={sendAnalyticsOutputs} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} /> )} {metadata?.length > 0 && ( diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx index bb9369e65..0d930d140 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionInputOutput.tsx @@ -2,14 +2,18 @@ import React, { useState } from 'react'; import { Tooltip } from 'antd'; import cn from 'classnames'; -import { addEllipsis, Button } from '@lace/common'; import { InfoCircleOutlined, DownOutlined } from '@ant-design/icons'; +import { addEllipsis, Button } from '@lace/common'; + import { TxOutputInput } from './TransactionDetailAsset'; import { TranslationsFor } from '../../utils/types'; import { ReactComponent as BracketDown } from '../../assets/icons/bracket-down.component.svg'; import styles from './TransactionInputOutput.module.scss'; +import { Flex } from '@lace/ui'; +import { getAddressTagTranslations, renderAddressTag } from '@ui/utils'; +import { useTranslate } from '@ui/hooks'; const rotateOpen: React.CSSProperties = { transform: 'rotate(180deg)', @@ -30,6 +34,8 @@ export interface TransactionInputOutputProps { translations: TranslationsFor<'address' | 'sent'>; coinSymbol: string; withSeparatorLine?: boolean; + ownAddresses: string[]; + addressToNameMap: Map; sendAnalytics?: () => void; } @@ -42,9 +48,12 @@ export const TransactionInputOutput = ({ translations, coinSymbol, withSeparatorLine, + ownAddresses, + addressToNameMap, sendAnalytics }: TransactionInputOutputProps): React.ReactElement => { const [isVisible, setIsVisible] = useState(); + const { t } = useTranslate(); const animation = isVisible ? rotateOpen : rotateClose; const Icon = BracketDown ? : ; @@ -79,9 +88,12 @@ export const TransactionInputOutput = ({
{translations.address}
-
- {addEllipsis(inputAddress, 8, 8)} -
+ +
+ {addEllipsis(inputAddress, 8, 8)} +
+ {renderAddressTag(inputAddress, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
diff --git a/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx b/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx index 73522e5c2..26a3d9558 100644 --- a/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx +++ b/packages/core/src/ui/components/ActivityDetail/__tests__/TransactionDetails.test.tsx @@ -35,6 +35,7 @@ describe('Testing ActivityDetailsBrowser component', () => { ], amountTransformer: (amount) => `${amount} $`, coinSymbol: 'ADA', + ownAddresses: [], addressToNameMap: new Map() }; @@ -83,4 +84,22 @@ describe('Testing ActivityDetailsBrowser component', () => { const { queryByTestId: query } = render(); expect(query('tx-metadata')).not.toBeInTheDocument(); }); + + test('should show address tag for inputs', async () => { + // use empty addrOutputs (so we get only one toggle button for inputs) + const { findByTestId } = render(); + const inputsSectionToggle = await findByTestId('tx-addr-list_toggle'); + fireEvent.click(inputsSectionToggle); + + expect(await findByTestId('address-tag')).toBeVisible(); + }); + + test('should show address tag for outputs', async () => { + // use empty addrOutputs (so we get only one toggle button for outputs) + const { findByTestId } = render(); + const outputsSectionToggle = await findByTestId('tx-addr-list_toggle'); + fireEvent.click(outputsSectionToggle); + + expect(await findByTestId('address-tag')).toBeVisible(); + }); }); diff --git a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx index 34abb6ebd..69a065701 100644 --- a/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx +++ b/packages/core/src/ui/components/DappAddressSections/DappAddressSections.tsx @@ -8,8 +8,9 @@ import { Typography } from 'antd'; import styles from './DappAddressSections.module.scss'; import { useTranslate } from '@src/ui/hooks'; -import { TransactionAssets, SummaryExpander, DappTransactionSummary, Tooltip } from '@lace/ui'; +import { Flex, TransactionAssets, SummaryExpander, DappTransactionSummary, Tooltip } from '@lace/ui'; import classNames from 'classnames'; +import { getAddressTagTranslations, renderAddressTag } from '@ui/utils/render-address-tag'; interface GroupedAddressAssets { nfts: Array; @@ -23,6 +24,8 @@ export interface DappAddressSectionProps { isToAddressesEnabled: boolean; isFromAddressesEnabled: boolean; coinSymbol: string; + ownAddresses: string[]; + addressToNameMap?: Map; } const tryDecodeAsUtf8 = ( @@ -101,7 +104,9 @@ export const DappAddressSections = ({ groupedToAddresses, isToAddressesEnabled, isFromAddressesEnabled, - coinSymbol + coinSymbol, + ownAddresses, + addressToNameMap }: DappAddressSectionProps): React.ReactElement => { const { t } = useTranslate(); @@ -121,11 +126,14 @@ export const DappAddressSections = ({ {t('core.dappTransaction.address')} - - - {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} - - + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + + {renderAddressTag(address, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
{(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( <> @@ -179,11 +187,14 @@ export const DappAddressSections = ({ {t('core.dappTransaction.address')} - - - {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} - - + + + + {addEllipsis(address, charBeforeEllipsisName, charAfterEllipsisName)} + + + {renderAddressTag(address, getAddressTagTranslations(t), ownAddresses, addressToNameMap)} +
{(addressData.tokens.length > 0 || addressData.coins.length > 0) && ( <> diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx index f734549d4..691d4b8b8 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { DappTransaction } from './DappTransaction'; import { ComponentProps } from 'react'; import { Wallet } from '@lace/cardano'; +import { AssetInfoWithAmount, TokenTransferValue } from '@cardano-sdk/core'; const meta: Meta = { title: 'DappTransaction', @@ -15,14 +16,60 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const fromAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' +); + +const toAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' +); + +const toAddressBookAddress = Wallet.Cardano.PaymentAddress( + 'addr_test1qzqgfww9svrzelxrnlml0nmdq4yevwke7ck7ae27u5ptmq5dwuq25p4hr0yxhg4pce0d6t7v4c0msy3vr3xppygn9ktqe77950' +); + +const PXLAssetId = Wallet.Cardano.AssetId('1ec85dcee27f2d90ec1f9a1e4ce74a667dc9be8b184463223f9c960150584c'); +const PXLPolicyId = Wallet.Cardano.AssetId.getPolicyId(PXLAssetId); +const PXLAssetName = Wallet.Cardano.AssetId.getAssetName(PXLAssetId); +const PXLAssetInfo: AssetInfoWithAmount = { + // eslint-disable-next-line no-magic-numbers + amount: BigInt(100_000), + assetInfo: { + assetId: PXLAssetId, + name: PXLAssetName, + policyId: PXLPolicyId, + fingerprint: Wallet.Cardano.AssetFingerprint.fromParts(PXLPolicyId, PXLAssetName), + // eslint-disable-next-line no-magic-numbers + supply: BigInt(11_242_452_000), + quantity: BigInt(1) + } +}; + +const fromAddressTokens: TokenTransferValue = { + // eslint-disable-next-line no-magic-numbers + coins: BigInt(-100_000), + assets: new Map([[PXLAssetId, { ...PXLAssetInfo, amount: -PXLAssetInfo.amount }]]) +}; + +const toAddressTokens: TokenTransferValue = { + // eslint-disable-next-line no-magic-numbers + coins: BigInt(100_000), + assets: new Map([[PXLAssetId, PXLAssetInfo]]) +}; + const data: ComponentProps = { dappInfo: { name: 'Mint' }, coinSymbol: 'tAda', fiatCurrencyCode: 'usd', - fromAddress: new Map(), - toAddress: new Map(), + ownAddresses: [fromAddress], + addressToNameMap: new Map([[toAddressBookAddress, 'test']]), + fromAddress: new Map([[fromAddress, fromAddressTokens]]), + toAddress: new Map([ + [toAddress, toAddressTokens], + [toAddressBookAddress, toAddressTokens] + ]), // eslint-disable-next-line no-magic-numbers collateral: 150_000 as unknown as bigint, txInspectionDetails: { diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index aed6f5cfd..15ea1a1b0 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -30,6 +30,8 @@ export interface DappTransactionProps { /** tokens send to being sent to or from the user */ fromAddress: Map; toAddress: Map; + ownAddresses?: string[]; + addressToNameMap?: Map; collateral?: bigint; } @@ -57,6 +59,7 @@ const groupAddresses = (addresses: Map { const { t } = useTranslate(); @@ -205,6 +210,8 @@ export const DappTransaction = ({ groupedFromAddresses={groupedFromAddresses} groupedToAddresses={groupedToAddresses} coinSymbol={coinSymbol} + ownAddresses={ownAddresses} + addressToNameMap={addressToNameMap} />
diff --git a/packages/core/src/ui/components/SharedWallet/AddCoSigners/AddCoSigners.stories.tsx b/packages/core/src/ui/components/SharedWallet/AddCoSigners/AddCoSigners.stories.tsx index 243ab5b30..34b6481b8 100644 --- a/packages/core/src/ui/components/SharedWallet/AddCoSigners/AddCoSigners.stories.tsx +++ b/packages/core/src/ui/components/SharedWallet/AddCoSigners/AddCoSigners.stories.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-magic-numbers, promise/avoid-new */ import React from 'react'; import type { Meta } from '@storybook/react'; @@ -40,7 +39,7 @@ const addressBook = [ const handleResolution = 'addr_test1qzrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuql9tk0g' as Wallet.Cardano.PaymentAddress; -let timeout: NodeJS.Timeout; +let timeout: number; const validateAddress: ValidateAddress = async (address) => { if (!address) { @@ -51,10 +50,12 @@ const validateAddress: ValidateAddress = async (address) => { if (timeout) { clearTimeout(timeout); } - timeout = setTimeout(() => { + const twoSeconds = 2000; + timeout = window.setTimeout(() => { clearTimeout(timeout); - resolve(Math.random() < 0.5 ? { isValid: true, handleResolution } : { isValid: false }); - }, 2000); + const factor = 0.5; + resolve(Math.random() < factor ? { isValid: true, handleResolution } : { isValid: false }); + }, twoSeconds); }); } return { diff --git a/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx b/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx deleted file mode 100644 index 922fca549..000000000 --- a/packages/core/src/ui/components/WalletSetup/WalletSetupCreationStep.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable no-magic-numbers */ -import React from 'react'; -import cn from 'classnames'; -import { WalletSetupStepLayout, WalletTimelineSteps } from './WalletSetupStepLayout'; -import { TranslationsFor } from '@ui/utils/types'; -import styles from './WalletSetupFinalStep.module.scss'; -import { Loader } from '@lace/common'; - -type TranslationKeys = 'title' | 'description'; -export interface WalletSetupCreationStepProps { - translations: TranslationsFor; - isHardwareWallet?: boolean; -} -export const WalletSetupCreationStep = ({ - translations, - isHardwareWallet = false -}: WalletSetupCreationStepProps): React.ReactElement => ( - -
- -
-
-); diff --git a/packages/core/src/ui/components/WalletSetup/index.ts b/packages/core/src/ui/components/WalletSetup/index.ts index 4508c8e0c..ce8eef4bc 100644 --- a/packages/core/src/ui/components/WalletSetup/index.ts +++ b/packages/core/src/ui/components/WalletSetup/index.ts @@ -2,7 +2,6 @@ export * from './WalletSetupOptionsStep'; export * from './WalletSetupStepLayout'; export * from './WalletSetupRegisterStep'; export * from './WalletSetupMnemonicIntroStep'; -export * from './WalletSetupCreationStep'; export * from './WalletSetupModeStep'; export * from './WalletSetupFinalStep'; export * from './WalletSetupConnectHardwareWalletStep'; diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss new file mode 100644 index 000000000..120e3e063 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.module.scss @@ -0,0 +1,20 @@ +@import '../../styles/theme.scss'; +@import '../../../../../common/src/ui/styles/abstracts/typography'; + +.wrapper { + align-items: center; + display: flex; + flex-direction: column; + gap: size_unit(1); + height: 100%; + justify-content: center; +} + +.loader { + transform: translate(50%); +} + +.errorImage { + flex-grow: 1; + width: size_unit(16); +} diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx index d24207145..c1be02422 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupConnectHardwareWalletStepRevamp.tsx @@ -1,94 +1,50 @@ -import React, { useState } from 'react'; -import { WalletTimelineSteps } from '../WalletSetup/WalletSetupStepLayout'; -import styles from '../WalletSetup/WalletSetupConnectHardwareWalletStep.module.scss'; -import Icon from '@ant-design/icons'; -import { ReactComponent as LedgerLogo } from '../../assets/icons/ledger-wallet.component.svg'; -import { ReactComponent as TrezorLogo } from '../../assets/icons/trezor-wallet.component.svg'; -import { Typography } from 'antd'; +import { Banner, Loader } from '@lace/common'; import { TranslationsFor } from '@ui/utils/types'; -import classnames from 'classnames'; +import React from 'react'; +import ExclamationCircleIcon from '../../assets/icons/exclamation-circle.svg'; +import { WalletTimelineSteps } from '../WalletSetup'; +import styles from './WalletSetupConnectHardwareWalletStepRevamp.module.scss'; import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; -const { Text } = Typography; - -const logoMap = { - Ledger: LedgerLogo, - Trezor: TrezorLogo -}; - -export interface WalletSetupConnectHardwareWalletStepRevampProps { - wallets: string[]; +export interface WalletSetupConnectHardwareWalletStepProps { onBack: () => void; - onNext: () => void; - onConnect: (model: string) => Promise; - isNextEnable: boolean; - translations: TranslationsFor<'title' | 'subTitle' | 'supportedDevices' | 'connectDevice'>; - isHardwareWallet?: boolean; + translations: TranslationsFor<'title' | 'subTitle' | 'errorMessage' | 'errorCta'>; + state: 'loading' | 'error'; + onRetry?: () => void; } export const WalletSetupConnectHardwareWalletStepRevamp = ({ - wallets, onBack, - onNext, - onConnect, - isNextEnable, translations, - isHardwareWallet = false -}: WalletSetupConnectHardwareWalletStepRevampProps): React.ReactElement => { - const [walletModel, setWalletModel] = useState(); - const [isConnecting, setIsConnecting] = useState(false); - const isButtonActive = (model: string) => model === walletModel; - - const handleConnect = async (model: string) => { - setIsConnecting(true); - setWalletModel(model); - try { - await onConnect(model); - } finally { - setIsConnecting(false); - } - }; - - return ( - - - {translations.subTitle} - -
-
- {wallets.map((wallet: string) => ( - - ))} + state, + onRetry +}: WalletSetupConnectHardwareWalletStepProps): React.ReactElement => ( + +
+ {state === 'loading' && ( +
+
- - {translations.supportedDevices} - -
- - {translations.connectDevice} - -
- ); -}; + )} + {state === 'error' && ( + <> + hardware wallet connection error image + + + )} +
+ +); diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss new file mode 100644 index 000000000..5376ed944 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.module.scss @@ -0,0 +1,11 @@ +.walletCreationStep { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; +} + +.loader { + transform: translate(50%); +} diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx new file mode 100644 index 000000000..f8f387e54 --- /dev/null +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupHWCreationStep.tsx @@ -0,0 +1,25 @@ +import { Loader } from '@lace/common'; +import { TranslationsFor } from '@ui/utils/types'; +import React from 'react'; +import { WalletTimelineSteps } from '../WalletSetup'; +import styles from './WalletSetupHWCreationStep.module.scss'; +import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; + +type WalletSetupCreationStepProps = { + translations: TranslationsFor<'title' | 'description'>; +}; + +export const WalletSetupHWCreationStep = ({ translations }: WalletSetupCreationStepProps): React.ReactElement => ( + +
+
+ +
+
+
+); diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx index 4bc0e486b..3e6c142bd 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupNamePasswordStepRevamp.tsx @@ -11,7 +11,7 @@ import { } from '../WalletSetup/WalletSetupNamePasswordStep/utils'; import { WalletNameInput } from '../WalletSetup/WalletSetupNamePasswordStep/WalletNameInput'; import { WalletPasswordConfirmationInput } from '../WalletSetup/WalletSetupNamePasswordStep/WalletPasswordConfirmationInput'; -import { WalletSetupStepLayoutRevamp } from '../WalletSetupRevamp/WalletSetupStepLayoutRevamp'; +import { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; import { TranslationsFor } from '@ui/utils/types'; import { useTranslate } from '@ui/hooks'; import { passwordComplexity } from '@ui/utils/password-complexity'; diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx index 69a31ace8..7b5da5be0 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupSelectAccountsStepRevamp.tsx @@ -15,16 +15,16 @@ export interface WalletSetupSelectAccountsStepRevampProps { accounts: number; onBack: () => void; onSubmit: (accountIndex: number, name: string) => void; - isHardwareWallet?: boolean; - wallet?: string; isNextLoading?: boolean; + onSelectedAccountChange?: () => void; } export const WalletSetupSelectAccountsStepRevamp = ({ accounts, onBack, onSubmit, - isNextLoading + isNextLoading, + onSelectedAccountChange }: WalletSetupSelectAccountsStepRevampProps): React.ReactElement => { const [selectedAccount, setSelectedAccount] = useState('0'); const [walletName, setWalletName] = useState(INITIAL_WALLET_NAME); @@ -74,7 +74,10 @@ export const WalletSetupSelectAccountsStepRevamp = ({ setSelectedAccount(value)} + onValueChange={(value) => { + setSelectedAccount(value); + onSelectedAccountChange?.(); + }} showArrow withOutline selectedValue={selectedAccount} diff --git a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx index 14ae21593..0d295f4e5 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx +++ b/packages/core/src/ui/components/WalletSetupRevamp/WalletSetupStepLayoutRevamp.tsx @@ -3,7 +3,7 @@ import styles from './WalletSetupStepLayoutRevamp.module.scss'; import cn from 'classnames'; import { Button, Timeline } from '@lace/common'; import { Tooltip } from 'antd'; -import { urls } from '../../utils/constants'; +import { urls } from '@ui/utils/constants'; import { useTranslate } from '@ui/hooks'; import i18n from '@ui/lib/i18n'; import { WalletTimelineSteps } from '../WalletSetup'; diff --git a/packages/core/src/ui/components/WalletSetupRevamp/index.ts b/packages/core/src/ui/components/WalletSetupRevamp/index.ts index 3892e3136..4587a7601 100644 --- a/packages/core/src/ui/components/WalletSetupRevamp/index.ts +++ b/packages/core/src/ui/components/WalletSetupRevamp/index.ts @@ -1,8 +1,9 @@ -export * from './WalletSetupSelectAccountsStepRevamp'; -export * from './WalletSetupOptionsStepRevamp'; +export { WalletSetupSelectAccountsStepRevamp } from './WalletSetupSelectAccountsStepRevamp'; +export { WalletSetupOptionsStepRevamp } from './WalletSetupOptionsStepRevamp'; export { MnemonicVideoPopupContent } from './MnemonicVideoPopupContent'; export { WalletSetupMnemonicStepRevamp } from './WalletSetupMnemonicStepRevamp'; export { WalletSetupMnemonicVerificationStepRevamp } from './WalletSetupMnemonicStepRevamp'; export { WalletSetupStepLayoutRevamp } from './WalletSetupStepLayoutRevamp'; export { WalletSetupNamePasswordStepRevamp } from './WalletSetupNamePasswordStepRevamp'; export { WalletSetupConnectHardwareWalletStepRevamp } from './WalletSetupConnectHardwareWalletStepRevamp'; +export { WalletSetupHWCreationStep } from './WalletSetupHWCreationStep'; diff --git a/packages/core/src/ui/hooks/useTranslate.tsx b/packages/core/src/ui/hooks/useTranslate.tsx index 8e20494d3..773093b4a 100644 --- a/packages/core/src/ui/hooks/useTranslate.tsx +++ b/packages/core/src/ui/hooks/useTranslate.tsx @@ -6,7 +6,7 @@ import fallbackInstance from '../lib/i18n'; // If no i18n instance found in context then use the local one setI18n(fallbackInstance); -interface UseTranslate { +export interface UseTranslate { t: (key: string | string[], defaultValue?: string, options?: TOptions) => string; Trans: typeof Trans; i18n: typeof i18n; diff --git a/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts b/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts new file mode 100644 index 000000000..153b40e63 --- /dev/null +++ b/packages/core/src/ui/utils/__tests__/render-address-tag.test.ts @@ -0,0 +1,33 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { Wallet } from '@lace/cardano'; +import { AddressTagTranslations, renderAddressTag } from '@ui/utils'; + +const address = Wallet.Cardano.PaymentAddress( + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' +); + +const translations: AddressTagTranslations = { + own: 'own', + foreign: 'foreign' +}; + +describe('rendering correct tags for addresses', () => { + test('should tag own addresses', async () => { + const ownAddresses = [address]; + const { findByTestId } = render(renderAddressTag(address, translations, ownAddresses)); + expect(await findByTestId('address-tag')).toContainHTML(translations.own); + }); + + test('should tag foreign addresses', async () => { + const { findByTestId } = render(renderAddressTag(address, translations)); + expect(await findByTestId('address-tag')).toContainHTML(translations.foreign); + }); + + test('should tag address book addresses', async () => { + const addressName = 'test'; + const addressToNameMap = new Map([[address, addressName]]); + const { findByTestId } = render(renderAddressTag(address, translations, [], addressToNameMap)); + expect(await findByTestId('address-tag')).toContainHTML(`${translations.foreign}/${addressName}`); + }); +}); diff --git a/packages/core/src/ui/utils/index.ts b/packages/core/src/ui/utils/index.ts index 2983ba98d..1892c7749 100644 --- a/packages/core/src/ui/utils/index.ts +++ b/packages/core/src/ui/utils/index.ts @@ -1,3 +1,4 @@ export * from './sanitize-number'; export * from './handle'; export * from './address-form'; +export * from './render-address-tag'; diff --git a/packages/core/src/ui/utils/render-address-tag.tsx b/packages/core/src/ui/utils/render-address-tag.tsx new file mode 100644 index 000000000..b5cf677fe --- /dev/null +++ b/packages/core/src/ui/utils/render-address-tag.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { AddressTag, AddressTagVariants } from '@lace/ui'; +import { UseTranslate } from '@ui/hooks'; + +export type AddressTagTranslations = { own: string; foreign: string }; + +export const getAddressTagTranslations = (t: UseTranslate['t']): AddressTagTranslations => ({ + own: t('core.addressTags.own'), + foreign: t('core.addressTags.foreign') +}); + +export const renderAddressTag = ( + address: string, + translations: AddressTagTranslations, + ownAddresses: string[] = [], + addressToNameMap: Map = new Map() // address, name +): JSX.Element => { + const matchingAddressName = addressToNameMap.get(address); + return ownAddresses.includes(address) ? ( + {translations.own} + ) : ( + + {translations.foreign} + {matchingAddressName ? `/${matchingAddressName}` : ''} + + ); +}; diff --git a/packages/core/wallaby.js b/packages/core/wallaby.js new file mode 100644 index 000000000..6b7c500d8 --- /dev/null +++ b/packages/core/wallaby.js @@ -0,0 +1,7 @@ +module.exports = () => { + return { + testFramework: { + configFile: 'test/jest.config.js' + } + }; +}; diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index bd6542460..d1fb7a814 100755 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -8,7 +8,7 @@ "test": "yarn exec echo 'User test:local'", "test:local:chrome": "../../node_modules/.bin/wdio run wdio.conf.chrome.ts", "test:local:edge": "../../node_modules/.bin/wdio run wdio.conf.edge.ts", - "cleanup": "rm -rf node_modules logs reports screenshots", + "cleanup": "rm -rf logs node_modules reports screenshots", "lint": "run -T eslint -c .eslintrc.js .", "lint:fix": "run -T eslint -c .eslintrc.js . --fix", "pack-chrome-extension": "node ./src/utils/packExtension.ts", diff --git a/packages/e2e-tests/src/actor/webTester.ts b/packages/e2e-tests/src/actor/webTester.ts index 6a379e3b5..0da22a325 100755 --- a/packages/e2e-tests/src/actor/webTester.ts +++ b/packages/e2e-tests/src/actor/webTester.ts @@ -7,42 +7,6 @@ import { Logger } from '../support/logger'; export type LocatorStrategy = 'css selector' | 'xpath'; export default new (class WebTester { - async seeElement(selector: string, reverseOrder = false, timeoutMs = 3000) { - Logger.log(`Assert see element ${selector}, reverse = ${reverseOrder}`); - const shouldBeFound = reverseOrder ? 'should not be found' : 'should be found'; - await $(selector).waitForDisplayed({ - timeout: timeoutMs, - interval: 500, - reverse: reverseOrder, - timeoutMsg: `element: ${selector} ${shouldBeFound} after: ${timeoutMs}ms` - }); - } - - async seeWebElement(element: WebElement) { - await this.seeElement(element.toJSLocator()); - } - - async clickOnElement(selector: string, locatorStrategy?: LocatorStrategy): Promise { - Logger.log(`Click on ${selector} [strategy=${locatorStrategy ?? 'css selector'}]`); - const element = await $(selector); - await element.waitForDisplayed(); - await element - .waitForClickable({ timeout: 5000 }) - .then(async () => await $(element).click()) - .catch(() => { - throw new Error(`Element ${selector} not clickable`); - }); - } - - async clickElement(element: WebElement, retries?: number): Promise { - if (retries && retries > 0 && (await (await $(element.toJSLocator())).isDisplayed())) { - await browser.pause(500); - await this.clickElement(element, retries - 1); - } else { - await this.clickOnElement(element.toJSLocator(), element.locatorStrategy()); - } - } - async getTextValueFromElement(element: WebElement): Promise { return await this.getTextValue(element.toJSLocator()); } diff --git a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts index 1fab9e989..c67ada570 100644 --- a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts +++ b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts @@ -223,7 +223,7 @@ class DAppConnectorAssert { ); const dAppWalletLovelaceBalance = Math.trunc(Number(await ExampleDAppPage.walletBalance.getText()) / 10_000); - expect(dAppWalletLovelaceBalance).to.be.closeTo(actualWalletLovelaceBalance, 1); + expect(dAppWalletLovelaceBalance).to.be.closeTo(actualWalletLovelaceBalance, 2); expect(await ExampleDAppPage.walletChangeAddress.getText()).to.equal( getTestWallet(TestWalletName.TestAutomationWallet).address diff --git a/packages/e2e-tests/src/assert/drawerSendExtendedAssert.ts b/packages/e2e-tests/src/assert/drawerSendExtendedAssert.ts index 2e7b03fe4..48aeadf0b 100644 --- a/packages/e2e-tests/src/assert/drawerSendExtendedAssert.ts +++ b/packages/e2e-tests/src/assert/drawerSendExtendedAssert.ts @@ -206,8 +206,7 @@ class DrawerSendExtendedAssert { async assertDefaultInputsDoNotContainValues() { const coinConfigure = new CoinConfigure(); - const addressInput = new AddressInput(); - expect(await addressInput.input.getValue()).to.be.empty; + expect(await new AddressInput().input.getValue()).to.be.empty; expect(await coinConfigure.input.getValue()).to.equal('0.00'); } @@ -304,10 +303,10 @@ class DrawerSendExtendedAssert { expect(await TransactionNewPage.metadataInputField.getValue()).to.be.empty; } - async assertSeeIncorrectAddressError(shouldSee: boolean) { - await new TransactionBundle().bundleAddressInputError.waitForDisplayed({ reverse: !shouldSee }); + async assertSeeIncorrectAddressError(bundleIndex: number, shouldSee: boolean) { + await new TransactionBundle(bundleIndex).bundleAddressInputError.waitForDisplayed({ reverse: !shouldSee }); if (shouldSee) { - expect(await new TransactionBundle().bundleAddressInputError.getText()).to.equal( + expect(await new TransactionBundle(bundleIndex).bundleAddressInputError.getText()).to.equal( await t('general.errors.incorrectAddress') ); } @@ -368,8 +367,7 @@ class DrawerSendExtendedAssert { } async assertSeeIconForInvalidAdaHandle(shouldBeDisplayed: boolean) { - const addressInput = new AddressInput(); - await addressInput.invalidAdaHandleIcon.waitForDisplayed({ reverse: !shouldBeDisplayed }); + await new AddressInput().invalidAdaHandleIcon.waitForDisplayed({ reverse: !shouldBeDisplayed }); } async assertSeeAdaHandleError(shouldBeDisplayed: boolean) { @@ -381,13 +379,11 @@ class DrawerSendExtendedAssert { } async assertSeeSearchLoader(shouldBeDisplayed: boolean) { - const addressInput = new AddressInput(); - await addressInput.searchLoader.waitForDisplayed({ reverse: !shouldBeDisplayed, interval: 50 }); + await new AddressInput().searchLoader.waitForDisplayed({ reverse: !shouldBeDisplayed, interval: 50 }); } async assertAddressBookButtonEnabled(bundleIndex: number, shouldBeEnabled: boolean) { - const addressInput = new AddressInput(bundleIndex); - await addressInput.ctaButton.waitForEnabled({ reverse: !shouldBeEnabled }); + await new AddressInput(bundleIndex).ctaButton.waitForEnabled({ reverse: !shouldBeEnabled }); } async assertSeeReviewAddressBanner(handle: string) { diff --git a/packages/e2e-tests/src/assert/multidelegation/MultidelegationPageAssert.ts b/packages/e2e-tests/src/assert/multidelegation/MultidelegationPageAssert.ts index 3fe3f6ffb..e38af3f71 100644 --- a/packages/e2e-tests/src/assert/multidelegation/MultidelegationPageAssert.ts +++ b/packages/e2e-tests/src/assert/multidelegation/MultidelegationPageAssert.ts @@ -8,6 +8,10 @@ import { StakePoolListItem } from '../../elements/multidelegation/StakePoolListI import Tooltip from '../../elements/Tooltip'; import testContext from '../../utils/testContext'; import { StakePoolGridCard } from '../../elements/multidelegation/StakePoolGridCard'; +import { StakePoolListColumnName } from '../../types/staking'; +import { SortingOrder } from '../../types/sortingOrder'; +import { mapColumnNameStringToEnum, sortColumnContent } from '../../utils/stakePoolListContent'; +import { StakePoolListColumn } from '../../enums/StakePoolListColumn'; class MultidelegationPageAssert { assertSeeStakingOnPoolsCounter = async (poolsCount: number) => { @@ -188,36 +192,37 @@ class MultidelegationPageAssert { expect(await firstStakePool.ticker.getText()).to.equal(expectedTicker); }; - assertSeeTooltipForColumn = async (columnName: string) => { + assertSeeTooltipForColumn = async (column: StakePoolListColumn) => { + await MultidelegationPage.tooltip.waitForStable(); await MultidelegationPage.tooltip.waitForDisplayed(); let expectedTooltipText; - switch (columnName) { - case 'Ticker': + switch (column) { + case StakePoolListColumn.Ticker: expectedTooltipText = await t('browsePools.tooltips.ticker', 'staking'); break; - case 'Saturation': + case StakePoolListColumn.Saturation: expectedTooltipText = await t('browsePools.tooltips.saturation', 'staking'); break; - case 'ROS': + case StakePoolListColumn.ROS: expectedTooltipText = await t('browsePools.tooltips.ros', 'staking'); break; - case 'Cost': + case StakePoolListColumn.Cost: expectedTooltipText = await t('browsePools.tooltips.cost', 'staking'); break; - case 'Margin': + case StakePoolListColumn.Margin: expectedTooltipText = await t('browsePools.tooltips.margin', 'staking'); break; - case 'Blocks': + case StakePoolListColumn.Blocks: expectedTooltipText = await t('browsePools.tooltips.blocks', 'staking'); break; - case 'Pledge': + case StakePoolListColumn.Pledge: expectedTooltipText = await t('browsePools.tooltips.pledge', 'staking'); break; - case 'Live Stake': + case StakePoolListColumn.LiveStake: expectedTooltipText = await t('browsePools.tooltips.liveStake', 'staking'); break; default: - throw new Error(`Unsupported column name: ${columnName}`); + throw new Error(`Unsupported column name: ${column}`); } expect(await MultidelegationPage.tooltip.getText()).to.equal(expectedTooltipText); }; @@ -300,6 +305,39 @@ class MultidelegationPageAssert { const cardsInARow = Math.floor(rowWidth / cardWidth); expect(cardsInARow).to.equal(expectedCardsCount); }; + + assertSeeColumnSortingIndicator = async (column: StakePoolListColumnName, order: 'ascending' | 'descending') => { + await ( + await MultidelegationPage.getColumnSortingIndicator(mapColumnNameStringToEnum(column), order) + ).waitForDisplayed(); + }; + + assertSeeStakePoolsSorted = async ( + stakePoolsDisplayType: 'list rows' | 'cards', + sortingOption: StakePoolListColumnName, + order: SortingOrder, + poolLimit?: number + ) => { + await MultidelegationPage.waitForPoolsCounterToBeGreaterThanZero(); + poolLimit ??= await MultidelegationPage.getNumberOfPoolsFromCounter(); + if (stakePoolsDisplayType === 'cards') { + // TODO: add code to handle grid cards - LW-10284 + throw new Error('Please add validation for grid cards sorting'); + } else { + const columnContent = await MultidelegationPage.extractColumnContent( + mapColumnNameStringToEnum(sortingOption), + poolLimit + ); + const sortedColumnContent = await sortColumnContent( + columnContent, + mapColumnNameStringToEnum(sortingOption), + order + ); + + expect(columnContent).to.not.be.empty; + expect(columnContent).to.deep.equal(sortedColumnContent); + } + }; } export default new MultidelegationPageAssert(); diff --git a/packages/e2e-tests/src/assert/onboarding/ConnectYourDevicePageAssert.ts b/packages/e2e-tests/src/assert/onboarding/ConnectYourDevicePageAssert.ts new file mode 100644 index 000000000..5e5b761e7 --- /dev/null +++ b/packages/e2e-tests/src/assert/onboarding/ConnectYourDevicePageAssert.ts @@ -0,0 +1,42 @@ +import ConnectYourDevicePage from '../../elements/onboarding/ConnectYourDevicePage'; +import { t } from '../../utils/translationService'; +import { expect } from 'chai'; +import OnboardingCommonAssert from './onboardingCommonAssert'; + +class ConnectYourDevicePageAssert extends OnboardingCommonAssert { + async assertSeeConnectYourDevicePage() { + await this.assertSeeStepTitle(await t('core.walletSetupConnectHardwareWalletStepRevamp.title')); + // TODO: replace subtitle assertions when USE_TREZOR_HW=true + // await this.assertSeeStepSubtitle(await t('core.walletSetupConnectHardwareWalletStepRevamp.subTitle')); + await this.assertSeeStepSubtitle(await t('core.walletSetupConnectHardwareWalletStepRevamp.subTitleLedgerOnly')); + + await ConnectYourDevicePage.loader.waitForDisplayed(); + + await this.assertSeeBackButton(); + await this.assertSeeTryAgainButton(false); + + await this.assertSeeLegalLinks(); + await this.assertSeeHelpAndSupportButton(); + } + + async assertSeeError(expectedErrorMessage: string) { + await ConnectYourDevicePage.errorImage.waitForDisplayed(); + await ConnectYourDevicePage.banner.container.waitForDisplayed(); + expect(await ConnectYourDevicePage.banner.description.getText()).to.equal(expectedErrorMessage); + } + + async assertSeeTryAgainButton(shouldBeVisible: boolean) { + await ConnectYourDevicePage.tryAgainButton.waitForDisplayed({ reverse: !shouldBeVisible }); + if (shouldBeVisible) { + expect(await ConnectYourDevicePage.tryAgainButton.getText()).to.equal( + await t('core.walletSetupConnectHardwareWalletStepRevamp.errorCta') + ); + } + } + + async assertSeeTryAgainButtonEnabled(shouldBeEnabled: boolean) { + await ConnectYourDevicePage.tryAgainButton.waitForEnabled({ reverse: !shouldBeEnabled }); + } +} + +export default new ConnectYourDevicePageAssert(); diff --git a/packages/e2e-tests/src/assert/onboarding/onboardingConnectHWPageAssert.ts b/packages/e2e-tests/src/assert/onboarding/onboardingConnectHWPageAssert.ts deleted file mode 100644 index db1f3b31d..000000000 --- a/packages/e2e-tests/src/assert/onboarding/onboardingConnectHWPageAssert.ts +++ /dev/null @@ -1,47 +0,0 @@ -import OnboardingConnectHardwareWalletPage from '../../elements/onboarding/connectHardwareWalletPage'; -import { t } from '../../utils/translationService'; -import { expect } from 'chai'; -import OnboardingCommonAssert from './onboardingCommonAssert'; - -class OnboardingConnectHardwareWalletPageAssert extends OnboardingCommonAssert { - async assertSeeConnectHardwareWalletPageSubTitle() { - await OnboardingConnectHardwareWalletPage.subTitle.waitForDisplayed(); - expect(await OnboardingConnectHardwareWalletPage.subTitle.getText()).to.equal( - await t('core.walletSetupConnectHardwareWalletStep.subTitle') - ); - } - - async assertSeeSupportedDevicesText() { - await OnboardingConnectHardwareWalletPage.supportedDevices.waitForDisplayed(); - expect(await OnboardingConnectHardwareWalletPage.supportedDevices.getText()).to.equal( - await t('core.walletSetupConnectHardwareWalletStep.supportedDevices') - ); - } - - async assertSeeLedgerButtonDisplayed() { - await OnboardingConnectHardwareWalletPage.ledgerButton.waitForDisplayed(); - } - - async assertSeeConnectDeviceText() { - await OnboardingConnectHardwareWalletPage.connectDevice.waitForDisplayed(); - expect(await OnboardingConnectHardwareWalletPage.connectDevice.getText()).to.equal( - await t('core.walletSetupConnectHardwareWalletStep.connectDevice') - ); - } - - async assertSeeConnectHardwareWalletPage() { - await this.assertSeeStepTitle(await t('core.walletSetupConnectHardwareWalletStep.title')); - await this.assertSeeConnectHardwareWalletPageSubTitle(); - await this.assertSeeSupportedDevicesText(); - await this.assertSeeLedgerButtonDisplayed(); - await this.assertSeeConnectDeviceText(); - - await this.assertSeeBackButton(); - await this.assertSeeNextButton(); - - await this.assertSeeLegalLinks(); - await this.assertSeeHelpAndSupportButton(); - } -} - -export default new OnboardingConnectHardwareWalletPageAssert(); diff --git a/packages/e2e-tests/src/assert/onboarding/onboardingMainPageAssert.ts b/packages/e2e-tests/src/assert/onboarding/onboardingMainPageAssert.ts index 5c070fd6e..77eaccb6f 100644 --- a/packages/e2e-tests/src/assert/onboarding/onboardingMainPageAssert.ts +++ b/packages/e2e-tests/src/assert/onboarding/onboardingMainPageAssert.ts @@ -13,7 +13,7 @@ class OnboardingMainPageAssert extends OnboardingCommonAssert { async assertSeeAgreementText() { await OnboardingMainPage.agreementText.waitForDisplayed(); expect(await OnboardingMainPage.agreementText.getText()).to.equal( - 'By proceeding you agree to Lace’s Terms of Service and Privacy Policy' + 'By clicking the Create, Connect or Restore button above, you agree with Lace’s Terms of Service and Privacy Policy' ); } diff --git a/packages/e2e-tests/src/assert/stakingPageAssert.ts b/packages/e2e-tests/src/assert/stakingPageAssert.ts index d35d8e577..a296741be 100644 --- a/packages/e2e-tests/src/assert/stakingPageAssert.ts +++ b/packages/e2e-tests/src/assert/stakingPageAssert.ts @@ -1,13 +1,8 @@ import StakingPage from '../elements/staking/stakingPage'; import { TestnetPatterns } from '../support/patterns'; -import webTester from '../actor/webTester'; import StakingInfoComponent from '../elements/staking/stakingInfoComponent'; import { t } from '../utils/translationService'; -import { StakePoolListItem } from '../elements/staking/StakePoolListItem'; import StakingSuccessDrawer from '../elements/staking/StakingSuccessDrawer'; -import { Logger } from '../support/logger'; -import StakingExtendedPageObject from '../pageobject/stakingExtendedPageObject'; -import { sortColumnContent } from '../utils/stakePoolListContent'; import { expect } from 'chai'; import { StakePool } from '../data/expectedStakePoolsData'; import StakingPasswordDrawer from '../elements/staking/StakingPasswordDrawer'; @@ -114,18 +109,6 @@ class StakingPageAssert { }); }; - assertStakePoolItemsOrder = async (columnName: string, order: string) => { - const stakePoolListItem = new StakePoolListItem(); - await webTester.waitUntilSeeElement(stakePoolListItem.container(), 60_000); - const columnContent: string[] = await StakingExtendedPageObject.extractColumnContent(columnName); - Logger.log(`EXTRACTED DATA: ${columnContent}`); - const sortedColumnContent = await sortColumnContent(columnContent, columnName, order); - Logger.log(`SORTED DATA: ${sortedColumnContent}`); - - expect(columnContent).to.not.be.empty; - expect(columnContent).to.deep.equal(sortedColumnContent); - }; - assertSeeStakingPasswordDrawer = async () => { await StakingPasswordDrawer.title.waitForDisplayed(); expect(await StakingPasswordDrawer.title.getText()).to.equal( diff --git a/packages/e2e-tests/src/assert/tokensPageAssert.ts b/packages/e2e-tests/src/assert/tokensPageAssert.ts index c03d4466b..a43796bcb 100644 --- a/packages/e2e-tests/src/assert/tokensPageAssert.ts +++ b/packages/e2e-tests/src/assert/tokensPageAssert.ts @@ -15,6 +15,8 @@ type ExpectedTokenDetails = { }; class TokensPageAssert { + ADA_PRICE_CHECK_INTERVAL = 65_000; + assertSeeTitle = async () => { await TokensPage.title.waitForDisplayed({ timeout: 10_000 }); }; @@ -65,59 +67,45 @@ class TokensPageAssert { await TokensPage.tokenName(i).waitForDisplayed(); await TokensPage.tokenTicker(i).waitForDisplayed(); if (mode === 'extended') { - // TODO: verify price cells in extended mode + await TokensPage.tokenPriceAda(i).waitForDisplayed(); + await TokensPage.tokenPriceChange(i).waitForDisplayed(); } await TokensPage.tokenBalance(i).waitForDisplayed(); await TokensPage.tokenFiatBalance(i).waitForDisplayed(); } }; - assertSeeCardanoItem = async (mode: 'extended' | 'popup') => { - expect(await TokensPage.tokenName(0).getText()).to.equal(Asset.CARDANO.name); - expect(await TokensPage.tokenTicker(0).getText()).to.equal(Asset.CARDANO.ticker); - - if (mode === 'extended') { - // TODO: verify price cells in extended mode - } - - const tokenBalance = await TokensPage.getTokenBalanceAsFloatByIndex(0); - expect(tokenBalance).to.be.greaterThan(0); - - const tokenFiatBalance = (await TokensPage.tokenFiatBalance(0).getText()).replace(',', ''); - const tokenValueFiatFloat = Number.parseFloat(tokenFiatBalance.split(' ')[0]); - expect(tokenValueFiatFloat).to.be.greaterThan(0); + assertSeeNativeToken = async (tokenName: Asset, mode: 'extended' | 'popup') => { + await this.assertSeeTokenItemBasicData(tokenName); + await this.assertSeeTokenData(tokenName, true, mode); }; - assertSeeLaceCoinItem = async (mode: 'extended' | 'popup') => { - expect(await TokensPage.getTokenNames()).to.contain(Asset.LACE_COIN.name); - expect(await TokensPage.getTokenTickers()).to.contain(Asset.LACE_COIN.ticker); - - const tokensTableIndex = await TokensPage.getTokenRowIndex(Asset.LACE_COIN.name); - - if (mode === 'extended') { - // TODO: verify price cells in extended mode - } - - const tokenBalance = await TokensPage.getTokenBalanceAsFloatByIndex(tokensTableIndex); - expect(tokenBalance).to.be.greaterThan(0); + assertSeeNotNativeToken = async (tokenName: Asset, mode: 'extended' | 'popup') => { + await this.assertSeeTokenItemBasicData(tokenName); + await this.assertSeeTokenData(tokenName, false, mode); + }; - const tokenFiatBalance = await TokensPage.tokenFiatBalance(tokensTableIndex).getText(); - expect(tokenFiatBalance).to.equal('-'); + assertSeeTokenItemBasicData = async (tokenName: Asset) => { + const tokensTableIndex = await TokensPage.getTokenRowIndex(tokenName.name); + expect(await TokensPage.tokenName(tokensTableIndex).getText()).to.contain(tokenName.name); + expect(await TokensPage.tokenTicker(tokensTableIndex).getText()).to.contain(tokenName.ticker); + expect(await TokensPage.getTokenBalanceAsFloatByIndex(tokensTableIndex)).to.be.greaterThan(0); }; - assertSeeHoskyItem = async (mode: 'extended' | 'popup') => { - expect(await TokensPage.getTokenNames()).to.contain(Asset.HOSKY_TOKEN.name); - expect(await TokensPage.getTokenTickers()).to.contain(Asset.HOSKY_TOKEN.ticker); + private assertTokenValueMatchesPattern = async (tokenValue: string, pattern: RegExp, isNativeToken: boolean) => { + isNativeToken ? expect(tokenValue).to.match(pattern) : expect(tokenValue).to.equal('-'); + }; - const tokensTableIndex = await TokensPage.getTokenRowIndex(Asset.HOSKY_TOKEN.name); + assertSeeTokenData = async (tokenName: Asset, nativeToken: boolean, mode: 'extended' | 'popup') => { + const tokensTableIndex = await TokensPage.getTokenRowIndex(tokenName.name); + const tokenValueFiat = await TokensPage.tokenFiatBalance(tokensTableIndex).getText(); + await this.assertTokenValueMatchesPattern(tokenValueFiat, TestnetPatterns.TOKEN_VALUE_FIAT_REGEX, nativeToken); if (mode === 'extended') { - // TODO: verify price cells in extended mode + const tokenValuePriceAda = await TokensPage.tokenPriceAda(tokensTableIndex).getText(); + await this.assertTokenValueMatchesPattern(tokenValuePriceAda, TestnetPatterns.TOKEN_VALUE_ADA_REGEX, nativeToken); + const tokenValuePriceChange = await TokensPage.tokenPriceChange(tokensTableIndex).getText(); + await this.assertTokenValueMatchesPattern(tokenValuePriceChange, TestnetPatterns.TOKEN_PRICE_CHANGE, nativeToken); } - const tokenBalance = await TokensPage.getTokenBalanceAsFloatByIndex(tokensTableIndex); - expect(tokenBalance).to.be.greaterThan(0); - - const tokenFiatBalance = await TokensPage.tokenFiatBalance(tokensTableIndex).getText(); - expect(tokenFiatBalance).to.equal('-'); }; async assertSeeToken(shouldSee: boolean, tokenDetails: ExpectedTokenDetails, mode: 'extended' | 'popup') { @@ -129,7 +117,8 @@ class TokensPageAssert { const tokenBalance = await TokensPage.getTokenBalanceAsFloatByIndex(tokensTableIndex); expect(tokenBalance).to.equal(tokenDetails.value); if (mode === 'extended') { - // TODO: verify price cells in extended mode + await TokensPage.tokenPriceAda(tokensTableIndex).waitForDisplayed(); + await TokensPage.tokenPriceChange(tokensTableIndex).waitForDisplayed(); } } else { expect(await TokensPage.getTokenNames()).to.not.contain(tokenDetails.name); @@ -252,6 +241,32 @@ class TokensPageAssert { expect(tickerDisplayed).to.equal(expectedTicker); } + + async seePriceFetchExpiredErrorMessage(shouldBeVisible: boolean) { + await TokensPage.priceFetchErrorDescription.waitForDisplayed({ + reverse: !shouldBeVisible, + timeout: this.ADA_PRICE_CHECK_INTERVAL * 3 + }); + if (shouldBeVisible) { + const expiredErrorMessageToMatch = (await t('general.warnings.priceDataExpired')).split(':')[0]; + expect(await TokensPage.priceFetchErrorDescription.getText()) + .to.include(expiredErrorMessageToMatch) + .to.include(new Date().getFullYear()) + .to.include(new Date().getDate()) + .to.include(new Date().getMinutes()); + } + } + + async seePriceFetchFailedErrorMessage(shouldBeVisible: boolean) { + await TokensPage.priceFetchErrorDescription.waitForDisplayed({ + reverse: !shouldBeVisible, + timeout: this.ADA_PRICE_CHECK_INTERVAL * 3 + }); + if (shouldBeVisible) + expect(await TokensPage.priceFetchErrorDescription.getText()).to.equal( + await t('general.warnings.cannotFetchPrice') + ); + } } export default new TokensPageAssert(); diff --git a/packages/e2e-tests/src/assert/transaction/transactionBundleAssert.ts b/packages/e2e-tests/src/assert/transaction/transactionBundleAssert.ts index df7706ab0..7750423c1 100644 --- a/packages/e2e-tests/src/assert/transaction/transactionBundleAssert.ts +++ b/packages/e2e-tests/src/assert/transaction/transactionBundleAssert.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import webTester from '../../actor/webTester'; import { TransactionBundle } from '../../elements/newTransaction/transactionBundle'; import coinConfigureAssert from '../coinConfigureAssert'; import assetInputAssert from '../assetInputAssert'; @@ -7,19 +6,17 @@ import TransactionNewPage from '../../elements/newTransaction/transactionNewPage import { t } from '../../utils/translationService'; import { CoinConfigure } from '../../elements/newTransaction/coinConfigure'; import { AssetInput } from '../../elements/newTransaction/assetInput'; -import { AddressInput } from '../../elements/AddressInput'; class TransactionBundleAssert { assertSeeBundles = async (expectedNumberOfBundles: number) => { for (let i = 1; i <= expectedNumberOfBundles; i++) { const bundle = new TransactionBundle(i); if (expectedNumberOfBundles > 1) { - expect(await webTester.getTextValueFromElement(bundle.bundleTitle())).to.equal( - `${await t('core.outputSummaryList.output')} ${i}` - ); - await webTester.seeWebElement(bundle.bundleRemoveButton()); + await bundle.bundleTitle.waitForDisplayed(); + expect(await bundle.bundleTitle.getText()).to.equal(`${await t('core.outputSummaryList.output')} ${i}`); + await bundle.bundleRemoveButton.waitForDisplayed(); } - await new AddressInput(i).input.waitForDisplayed(); + await bundle.bundleAddressInput.input.waitForDisplayed(); await coinConfigureAssert.assertSeeCoinConfigure(); await assetInputAssert.assertSeeAssetInput(i); } @@ -27,15 +24,14 @@ class TransactionBundleAssert { async assertSeeTokenNameInBundleAndCoinConfigure(expectedName: string, bundleIndex: number) { await TransactionNewPage.cancelTransactionButton.waitForStable(); - const tokenName = await new TransactionBundle(bundleIndex) - .bundleAssetInput() + const tokenName = await new TransactionBundle(bundleIndex).bundleAssetInput .coinConfigure(bundleIndex, expectedName.replace('...', '')) .nameElement.getText(); expect(tokenName).to.contain(expectedName); } async assertSeeAssetNameAndValueInBundle(expectedName: string, expectedValue: number, bundleIndex: number) { - const asset = new TransactionBundle(bundleIndex).bundleAssetInput().coinConfigure(bundleIndex, expectedName); + const asset = new TransactionBundle(bundleIndex).bundleAssetInput.coinConfigure(bundleIndex, expectedName); const tokenName = await asset.nameElement.getText(); const tokenValue = await asset.getAmount(); diff --git a/packages/e2e-tests/src/assert/transactionDetailsAssert.ts b/packages/e2e-tests/src/assert/transactionDetailsAssert.ts index a97a63f46..a5eadfc94 100644 --- a/packages/e2e-tests/src/assert/transactionDetailsAssert.ts +++ b/packages/e2e-tests/src/assert/transactionDetailsAssert.ts @@ -21,6 +21,7 @@ export type PoolData = { export type TransactionData = { address: string; + addressTag?: string; ada: string; assets?: string[]; }; @@ -73,6 +74,11 @@ class TransactionsDetailsAssert { expect(expectedAddress.startsWith(actualAddressSplit[0])).to.be.true; expect(expectedAddress.endsWith(actualAddressSplit[1])).to.be.true; } + + if (expectedActivityDetails.transactionData[i].addressTag) { + const actualAddressTag = await TransactionDetailsPage.transactionDetailsToAddressTag(i).getText(); + expect(expectedActivityDetails.transactionData[i].addressTag).to.equal(actualAddressTag); + } } } @@ -109,13 +115,8 @@ class TransactionsDetailsAssert { for (let i = 0; i <= rowsNumber && i < 10; i++) { await TransactionsPage.clickOnTransactionRow(i); await TransactionDetailsPage.transactionDetailsDescription.waitForClickable({ timeout: 15_000 }); - await TransactionDetailsPage.transactionDetailsHash.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsStatus.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsTimestamp.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsInputsSection.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsOutputsSection.waitForDisplayed(); const txType = await TransactionDetailsPage.transactionDetailsDescription.getText(); - if (!txType.includes(stakeKeyRegistration)) { + if (!txType.includes(stakeKeyRegistration) && !txType.includes('Rewards')) { await TransactionDetailsPage.transactionDetailsFeeADA.waitForDisplayed(); await TransactionDetailsPage.transactionDetailsFeeFiat.waitForDisplayed(); } @@ -124,6 +125,13 @@ class TransactionsDetailsAssert { await TransactionDetailsPage.transactionDetailsStakepoolTicker.waitForDisplayed(); await TransactionDetailsPage.transactionDetailsStakePoolId.waitForDisplayed(); } + if (!txType.includes('Rewards')) { + await TransactionDetailsPage.transactionDetailsTimestamp.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsInputsSection.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsOutputsSection.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsStatus.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsHash.waitForDisplayed(); + } await TransactionDetailsPage.closeActivityDetails(mode); } @@ -135,19 +143,20 @@ class TransactionsDetailsAssert { for (let i = 0; i <= rowsNumber && i < 10; i++) { await TransactionsPage.clickOnTransactionRow(i); - await TransactionDetailsPage.transactionDetailsInputsDropdown.click(); - await TransactionDetailsPage.transactionDetailsOutputsDropdown.click(); - await TransactionDetailsPage.transactionDetailsInputAddress.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsInputAdaAmount.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsInputFiatAmount.waitForDisplayed(); - // TODO refactor steps below - // some transactions (ADA only) don't have this field - // await TransactionDetailsPage.transactionDetailsInputTokens.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsOutputAddress.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsOutputAdaAmount.waitForDisplayed(); - await TransactionDetailsPage.transactionDetailsOutputFiatAmount.waitForDisplayed(); - // await TransactionDetailsPage.transactionDetailsOutputTokens.waitForDisplayed(); - + if ((await TransactionDetailsPage.transactionDetailsDescription.getText()) !== 'Rewards') { + await TransactionDetailsPage.transactionDetailsInputsDropdown.click(); + await TransactionDetailsPage.transactionDetailsOutputsDropdown.click(); + await TransactionDetailsPage.transactionDetailsInputAddress.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsInputAdaAmount.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsInputFiatAmount.waitForDisplayed(); + // TODO refactor steps below + // some transactions (ADA only) don't have this field + // await TransactionDetailsPage.transactionDetailsInputTokens.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsOutputAddress.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsOutputAdaAmount.waitForDisplayed(); + await TransactionDetailsPage.transactionDetailsOutputFiatAmount.waitForDisplayed(); + // await TransactionDetailsPage.transactionDetailsOutputTokens.waitForDisplayed(); + } await TransactionDetailsPage.closeActivityDetails(mode); } } @@ -158,39 +167,41 @@ class TransactionsDetailsAssert { for (let i = 0; i <= rowsNumber && i < 10; i++) { await TransactionsPage.clickOnTransactionRow(i); - await TransactionDetailsPage.transactionDetailsInputsDropdown.click(); - await TransactionDetailsPage.transactionDetailsInputsDropdown.waitForStable(); - await TransactionDetailsPage.transactionDetailsOutputsDropdown.click(); - await TransactionDetailsPage.transactionDetailsOutputsDropdown.waitForStable(); - - const txDetailsInputADAValueString = await TransactionDetailsPage.transactionDetailsInputAdaAmount.getText(); - const txDetailsInputADAValue = Number(txDetailsInputADAValueString.split(' ', 1)); - const txType = await TransactionDetailsPage.transactionDetailsDescription.getText(); - - const txDetailsInputFiatValueString = await TransactionDetailsPage.transactionDetailsInputFiatAmount.getText(); - const txDetailsInputFiatValue = Number(txDetailsInputFiatValueString.slice(1).split(' ', 1)); - - const txDetailsOutputADAValueString = await TransactionDetailsPage.transactionDetailsOutputAdaAmount.getText(); - const txDetailsOutputADAValue = Number(txDetailsOutputADAValueString.split(' ', 1)); - - const txDetailsOutputFiatValueString = await TransactionDetailsPage.transactionDetailsOutputFiatAmount.getText(); - const txDetailsOutputFiatValue = Number(txDetailsOutputFiatValueString.slice(1).split(' ', 1)); - - if (!txType.includes(stakeKeyRegistration)) { - const txDetailsFeeADAValueString = await TransactionDetailsPage.transactionDetailsFeeADA.getText(); - const txDetailsFeeADAValue = Number(txDetailsFeeADAValueString.split(' ', 1)); - expect(txDetailsFeeADAValue).to.be.greaterThan(0); + if ((await TransactionDetailsPage.transactionDetailsDescription.getText()) !== 'Rewards') { + await TransactionDetailsPage.transactionDetailsInputsDropdown.click(); + await TransactionDetailsPage.transactionDetailsInputsDropdown.waitForStable(); + await TransactionDetailsPage.transactionDetailsOutputsDropdown.click(); + await TransactionDetailsPage.transactionDetailsOutputsDropdown.waitForStable(); + + const txDetailsInputADAValueString = await TransactionDetailsPage.transactionDetailsInputAdaAmount.getText(); + const txDetailsInputADAValue = Number(txDetailsInputADAValueString.split(' ', 1)); + const txType = await TransactionDetailsPage.transactionDetailsDescription.getText(); + + const txDetailsInputFiatValueString = await TransactionDetailsPage.transactionDetailsInputFiatAmount.getText(); + const txDetailsInputFiatValue = Number(txDetailsInputFiatValueString.slice(1).split(' ', 1)); + + const txDetailsOutputADAValueString = await TransactionDetailsPage.transactionDetailsOutputAdaAmount.getText(); + const txDetailsOutputADAValue = Number(txDetailsOutputADAValueString.split(' ', 1)); + + const txDetailsOutputFiatValueString = + await TransactionDetailsPage.transactionDetailsOutputFiatAmount.getText(); + const txDetailsOutputFiatValue = Number(txDetailsOutputFiatValueString.slice(1).split(' ', 1)); + + if (!txType.includes(stakeKeyRegistration)) { + const txDetailsFeeADAValueString = await TransactionDetailsPage.transactionDetailsFeeADA.getText(); + const txDetailsFeeADAValue = Number(txDetailsFeeADAValueString.split(' ', 1)); + expect(txDetailsFeeADAValue).to.be.greaterThan(0); + + const txDetailsFeeFiatValueString = await TransactionDetailsPage.transactionDetailsFeeFiat.getText(); + const txDetailsFeeFiatValue = Number(txDetailsFeeFiatValueString.slice(1).split(' ', 1)); + expect(txDetailsFeeFiatValue).to.be.greaterThan(0); + } - const txDetailsFeeFiatValueString = await TransactionDetailsPage.transactionDetailsFeeFiat.getText(); - const txDetailsFeeFiatValue = Number(txDetailsFeeFiatValueString.slice(1).split(' ', 1)); - expect(txDetailsFeeFiatValue).to.be.greaterThan(0); + expect(txDetailsInputADAValue).to.be.greaterThan(0); + expect(txDetailsInputFiatValue).to.be.greaterThan(0); + expect(txDetailsOutputADAValue).to.be.greaterThan(0); + expect(txDetailsOutputFiatValue).to.be.greaterThan(0); } - - expect(txDetailsInputADAValue).to.be.greaterThan(0); - expect(txDetailsInputFiatValue).to.be.greaterThan(0); - expect(txDetailsOutputADAValue).to.be.greaterThan(0); - expect(txDetailsOutputFiatValue).to.be.greaterThan(0); - await TransactionDetailsPage.closeActivityDetails(mode); } } @@ -204,7 +215,7 @@ class TransactionsDetailsAssert { await TransactionsPage.clickOnTransactionRow(i); await TransactionDetailsPage.transactionDetailsDescription.waitForClickable({ timeout: 15_000 }); if ( - !['Delegation', 'Stake Key De-Registration', 'Stake Key Registration', 'Self Transaction'].includes( + !['Delegation', 'Stake Key De-Registration', 'Stake Key Registration', 'Self Transaction', 'Rewards'].includes( transactionType ) ) { @@ -221,8 +232,8 @@ class TransactionsDetailsAssert { const rowsNumber = (await TransactionsPage.rows).length; for (let i = 0; i <= rowsNumber && i < 10; i++) { - // TODO Cover self transaction details with automation - if ((await TransactionsPage.transactionsTableItemType(i).getText()) !== 'Self Transaction') { + const skippedTransaction = ['Self Transaction', 'Rewards']; // should be covered in separate tests + if (!skippedTransaction.includes(await TransactionsPage.transactionsTableItemType(i).getText())) { await TransactionsPage.clickOnTransactionRow(i); await TransactionDetailsPage.transactionDetailsDescription.waitForClickable({ timeout: 15_000 }); const txType = (await TransactionDetailsPage.transactionDetailsDescription.getText()).split('\n')[0]; diff --git a/packages/e2e-tests/src/elements/multidelegation/MultidelegationPage.ts b/packages/e2e-tests/src/elements/multidelegation/MultidelegationPage.ts index d6311743d..7c738530d 100644 --- a/packages/e2e-tests/src/elements/multidelegation/MultidelegationPage.ts +++ b/packages/e2e-tests/src/elements/multidelegation/MultidelegationPage.ts @@ -8,11 +8,11 @@ import StakePoolDetails from '../staking/stakePoolDetails'; import testContext from '../../utils/testContext'; import { isPopupMode } from '../../utils/pageUtils'; import CommonDrawerElements from '../CommonDrawerElements'; -import { StakePoolListColumnType } from '../../types/staking'; import { StakePoolListItem } from './StakePoolListItem'; import { StakePoolGridCard } from './StakePoolGridCard'; import StakePoolDetailsDrawer from './StakePoolDetailsDrawer'; import MoreOptionsComponent from './MoreOptionsComponent'; +import { StakePoolListColumn } from '../../enums/StakePoolListColumn'; class MultidelegationPage { private ACTIVITY_TAB = '[data-testid="activity-tab"]'; @@ -41,6 +41,7 @@ class MultidelegationPage { private COLUMN_HEADER_BLOCKS = '[data-testid="stake-pool-list-header-blocks"]'; private COLUMN_HEADER_PLEDGE = '[data-testid="stake-pool-list-header-pledge"]'; private COLUMN_HEADER_LIVE_STAKE = '[data-testid="stake-pool-list-header-liveStake"]'; + private COLUMN_SORTING_INDICATOR_TEMPLATE = '[data-testid="stake-pool-sort-order-###"]'; private MANAGE_STAKING_BTN_NEXT = '[data-testid="preferences-next-button"]'; private CONFIRMATION_BTN_NEXT = '[data-testid="stake-pool-confirmation-btn"]'; private DELEGATED_POOL_ITEM = '[data-testid="delegated-pool-item"]'; @@ -73,6 +74,7 @@ class MultidelegationPage { private STAKE_POOL_CARD_SKELETON = '[data-testid="stake-pool-card-skeleton"]'; private SELCECTED_STAKE_POOLS_IN_GRID_VIEW = '[data-testid="selected-pools-list"] [data-testid="stake-pool-card"]'; private SELCECTED_STAKE_POOLS_IN_LIST_VIEW = '[data-testid="selected-pools-list"] [data-testid="stake-pool-item"]'; + private POOLS_COUNTER = '[data-testid="pools-counter"]'; get title() { return SectionTitle.sectionTitle; @@ -214,6 +216,10 @@ class MultidelegationPage { return $(this.MANAGE_BTN); } + get poolsCounter() { + return $(this.POOLS_COUNTER); + } + delegatedPoolLogo(index: number): ChainablePromiseElement { return $$(this.DELEGATED_POOL_ITEM)[index].$(this.DELEGATED_POOL_LOGO); } @@ -300,6 +306,43 @@ class MultidelegationPage { )) as WebdriverIO.Element; } + async getColumnSortingIndicator(columnName: StakePoolListColumn, order: 'ascending' | 'descending') { + const orderDirection = order === 'ascending' ? 'asc' : 'desc'; + const orderDirectionSelector = `${this.COLUMN_SORTING_INDICATOR_TEMPLATE.replace('###', orderDirection)}`; + let selector = ''; + + switch (columnName) { + case StakePoolListColumn.Ticker: + selector = `${this.COLUMN_HEADER_TICKER} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.Saturation: + selector = `${this.COLUMN_HEADER_SATURATION} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.ROS: + selector = `${this.COLUMN_HEADER_ROS} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.Cost: + selector = `${this.COLUMN_HEADER_COST} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.Margin: + selector = `${this.COLUMN_HEADER_MARGIN} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.Blocks: + selector = `${this.COLUMN_HEADER_BLOCKS} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.Pledge: + selector = `${this.COLUMN_HEADER_PLEDGE} ${orderDirectionSelector}`; + break; + case StakePoolListColumn.LiveStake: + selector = `${this.COLUMN_HEADER_LIVE_STAKE} ${orderDirectionSelector}`; + break; + default: + throw new Error(`Unsupported column name: ${columnName}`); + } + + return $(selector); + } + async clickAndGetTabStateAttribute(tab: 'Overview' | 'Browse pools') { let tabElement; switch (tab) { @@ -418,68 +461,69 @@ class MultidelegationPage { await poolItem.click(); } - async hoverOverColumnWithName(columnName: StakePoolListColumnType) { + async hoverOverColumn(column: StakePoolListColumn) { let header; - switch (columnName) { - case 'Ticker': + + switch (column) { + case StakePoolListColumn.Ticker: header = await this.columnHeaderTicker; break; - case 'Saturation': + case StakePoolListColumn.Saturation: header = await this.columnHeaderSaturation; break; - case 'ROS': + case StakePoolListColumn.ROS: header = await this.columnHeaderROS; break; - case 'Cost': + case StakePoolListColumn.Cost: header = await this.columnHeaderCost; break; - case 'Margin': + case StakePoolListColumn.Margin: header = await this.columnHeaderMargin; break; - case 'Blocks': + case StakePoolListColumn.Blocks: header = await this.columnHeaderBlocks; break; - case 'Pledge': + case StakePoolListColumn.Pledge: header = await this.columnHeaderPledge; break; - case 'Live Stake': + case StakePoolListColumn.LiveStake: header = await this.columnHeaderLiveStake; break; default: - throw new Error(`Unsupported column name: ${columnName}`); + throw new Error(`Unsupported column name: ${column}`); } // make hovering over ANTD component more stable await header?.$('span span span').moveTo(); } - async clickOnColumnWithName(columnName: StakePoolListColumnType) { - switch (columnName) { - case 'Ticker': + async clickOnColumn(column: StakePoolListColumn) { + switch (column) { + case StakePoolListColumn.Ticker: await this.columnHeaderTicker.click(); break; - case 'Saturation': + case StakePoolListColumn.Saturation: await this.columnHeaderSaturation.click(); break; - case 'ROS': + case StakePoolListColumn.ROS: await this.columnHeaderROS.click(); break; - case 'Cost': + case StakePoolListColumn.Cost: await this.columnHeaderCost.click(); break; - case 'Margin': + case StakePoolListColumn.Margin: await this.columnHeaderMargin.click(); break; - case 'Blocks': + case StakePoolListColumn.Blocks: await this.columnHeaderBlocks.click(); break; - case 'Pledge': + case StakePoolListColumn.Pledge: await this.columnHeaderPledge.click(); break; - case 'Live Stake': + case StakePoolListColumn.LiveStake: await this.columnHeaderLiveStake.click(); break; default: - throw new Error(`Unsupported column name: ${columnName}`); + throw new Error(`Unsupported column name: ${column}`); } } @@ -543,6 +587,69 @@ class MultidelegationPage { const selectedTickers = await this.getTickersOfSelectedPools(viewType); testContext.save('selectedTickers', selectedTickers); } + + async getNumberOfPoolsFromCounter(): Promise { + const poolsCounterText = await this.poolsCounter.getText(); + return Number(Number(poolsCounterText.replace(/.*\(/, '').replace(')', '').replace(',', ''))); + } + + async waitForPoolsCounterToBeGreaterThanZero(): Promise { + await this.poolsCounter.waitForDisplayed(); + await browser.waitUntil(async () => (await this.getNumberOfPoolsFromCounter()) > 0, { + timeoutMsg: 'No stake pools!' + }); + } + + async extractColumnContent(columnName: StakePoolListColumn, poolLimit = 100): Promise { + const columnContent: string[] = []; + + await this.listContainer.waitForStable(); + await browser.pause(500); + + for (let i = 0; i < poolLimit; i++) { + const displayedPoolsCounter = await this.displayedPools.length; + const listItem = new StakePoolListItem(i); + // Load more pools if all visible ones were processed + if (i % (displayedPoolsCounter - 1) === 0) { + await listItem.container.scrollIntoView(); + await this.stakePoolListRowSkeleton.waitForExist({ + reverse: true, + interval: 100, + timeout: 30_000 + }); + } + switch (columnName) { + case StakePoolListColumn.Ticker: + columnContent.push(await listItem.ticker.getText()); + break; + case StakePoolListColumn.Saturation: + columnContent.push(await listItem.saturation.getText()); + break; + case StakePoolListColumn.ROS: + columnContent.push(await listItem.ros.getText()); + break; + case StakePoolListColumn.Cost: + columnContent.push(await listItem.cost.getText()); + break; + case StakePoolListColumn.Margin: + columnContent.push(await listItem.margin.getText()); + break; + case StakePoolListColumn.Blocks: + columnContent.push(await listItem.blocks.getText()); + break; + case StakePoolListColumn.Pledge: + columnContent.push(await listItem.pledge.getText()); + break; + case StakePoolListColumn.LiveStake: + columnContent.push(await listItem.liveStake.getText()); + break; + default: + throw new Error(`Not supported column name: ${columnName}`); + } + } + + return columnContent; + } } export default new MultidelegationPage(); diff --git a/packages/e2e-tests/src/elements/multidelegation/StakePoolListItem.ts b/packages/e2e-tests/src/elements/multidelegation/StakePoolListItem.ts index 3543d5846..bfe82ddb4 100644 --- a/packages/e2e-tests/src/elements/multidelegation/StakePoolListItem.ts +++ b/packages/e2e-tests/src/elements/multidelegation/StakePoolListItem.ts @@ -3,7 +3,6 @@ import { ChainablePromiseElement } from 'webdriverio'; export class StakePoolListItem { private SELECTED_POOLS_LIST = '[data-testid="selected-pools-list"]'; - private AVAILABLE_POOLS_LIST = '[data-testid="stake-pool-list-scroll-wrapper"]'; private LIST_ITEM = '[data-testid="stake-pool-item"]'; private CHECKBOX = '[data-testid="stake-pool-list-checkbox"]'; private TICKER = '[data-testid="stake-pool-list-ticker"]'; @@ -18,9 +17,9 @@ export class StakePoolListItem { protected listItem; constructor(index = 0, isOnSelectedPoolsList = false) { - this.listItem = $(isOnSelectedPoolsList ? this.SELECTED_POOLS_LIST : this.AVAILABLE_POOLS_LIST).$$(this.LIST_ITEM)[ - index - ]; + this.listItem = $( + isOnSelectedPoolsList ? `${this.SELECTED_POOLS_LIST} ${this.LIST_ITEM}` : `[data-item-index="${index}"]` + ); } get container(): ChainablePromiseElement { diff --git a/packages/e2e-tests/src/elements/newTransaction/transactionBundle.ts b/packages/e2e-tests/src/elements/newTransaction/transactionBundle.ts index e29962602..6e7507a3e 100644 --- a/packages/e2e-tests/src/elements/newTransaction/transactionBundle.ts +++ b/packages/e2e-tests/src/elements/newTransaction/transactionBundle.ts @@ -1,50 +1,46 @@ /* eslint-disable no-undef */ -import { LocatorStrategy } from '../../actor/webTester'; -import { WebElement, WebElementFactory as Factory } from '../webElement'; import { AddressInput } from '../AddressInput'; import { AssetInput } from './assetInput'; import { ChainablePromiseElement } from 'webdriverio'; -export class TransactionBundle extends WebElement { +export class TransactionBundle { protected CONTAINER = '//div[@data-testid="asset-bundle-container"]'; private BUNDLE_TITLE = '//h5[@data-testid="asset-bundle-title"]'; private BUNDLE_REMOVE_BUTTON = '//button[@data-testid="asset-bundle-remove-button"]'; private ADDRESS_INPUT_ERROR = '[data-testid="address-input-error"]'; + readonly index: number = 1; - constructor(index?: number) { - super(); - this.CONTAINER = typeof index === 'undefined' ? this.CONTAINER : `(${this.CONTAINER})[${index}]`; + constructor(index = 1) { + this.index = index; + this.CONTAINER = `(${this.CONTAINER})[${index}]`; } - container(): WebElement { - return Factory.fromSelector(`${this.CONTAINER}`, 'xpath'); + get container(): ChainablePromiseElement { + return $(this.CONTAINER); } - bundleTitle(): WebElement { - return Factory.fromSelector(`${this.CONTAINER}${this.BUNDLE_TITLE}`, 'xpath'); + get bundleTitle(): ChainablePromiseElement { + return $(`${this.CONTAINER}${this.BUNDLE_TITLE}`); } - bundleAddressInput(): AddressInput { - return new AddressInput(); + get bundleAddressInput(): AddressInput { + return new AddressInput(this.index); } get bundleAddressInputError(): ChainablePromiseElement { return $(this.CONTAINER).$(this.ADDRESS_INPUT_ERROR); } - bundleAssetInput(): AssetInput { - return new AssetInput(); + get bundleAssetInput(): AssetInput { + return new AssetInput(this.index); } - bundleRemoveButton(): WebElement { - return Factory.fromSelector(`${this.CONTAINER}${this.BUNDLE_REMOVE_BUTTON}`, 'xpath'); + get bundleRemoveButton(): ChainablePromiseElement { + return $(`${this.CONTAINER}${this.BUNDLE_REMOVE_BUTTON}`); } - toJSLocator(): string { - return this.CONTAINER; - } - - locatorStrategy(): LocatorStrategy { - return 'xpath'; - } + clickRemoveBundleButton = async (): Promise => { + await this.bundleRemoveButton.waitForClickable(); + await this.bundleRemoveButton.click(); + }; } diff --git a/packages/e2e-tests/src/elements/newTransaction/transactionNewPage.ts b/packages/e2e-tests/src/elements/newTransaction/transactionNewPage.ts index 621358129..41ee89dc1 100644 --- a/packages/e2e-tests/src/elements/newTransaction/transactionNewPage.ts +++ b/packages/e2e-tests/src/elements/newTransaction/transactionNewPage.ts @@ -1,7 +1,6 @@ /* eslint-disable no-undef */ import { CoinConfigure } from './coinConfigure'; import { AddressInput } from '../AddressInput'; -import { TransactionBundle } from './transactionBundle'; import { Asset } from '../../data/Asset'; import { ChainablePromiseElement } from 'webdriverio'; import Banner from '../banner'; @@ -40,10 +39,6 @@ class TransactionNewPage extends CommonDrawerElements { return new CoinConfigure(bundleIndex, assetName); } - transactionBundle(index?: number): TransactionBundle { - return new TransactionBundle(index); - } - get title() { return this.drawerNavigationTitle; } diff --git a/packages/e2e-tests/src/elements/onboarding/ConnectYourDevicePage.ts b/packages/e2e-tests/src/elements/onboarding/ConnectYourDevicePage.ts new file mode 100644 index 000000000..6649de9d5 --- /dev/null +++ b/packages/e2e-tests/src/elements/onboarding/ConnectYourDevicePage.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-undef*/ +import CommonOnboardingElements from './commonOnboardingElements'; +import { ChainablePromiseElement } from 'webdriverio'; +import Banner from '../banner'; + +export class ConnectYourDevicePage extends CommonOnboardingElements { + private LOADER_IMAGE = '[data-testid="loader-image"]'; + private ERROR_IMAGE = '[data-testid="error-image"]'; + + get loader(): ChainablePromiseElement { + return $(this.LOADER_IMAGE); + } + + get errorImage(): ChainablePromiseElement { + return $(this.ERROR_IMAGE); + } + + get tryAgainButton(): ChainablePromiseElement { + return this.nextButton; + } + + get banner(): typeof Banner { + return Banner; + } +} + +export default new ConnectYourDevicePage(); diff --git a/packages/e2e-tests/src/elements/onboarding/connectHardwareWalletPage.ts b/packages/e2e-tests/src/elements/onboarding/connectHardwareWalletPage.ts deleted file mode 100644 index bf702eb08..000000000 --- a/packages/e2e-tests/src/elements/onboarding/connectHardwareWalletPage.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable no-undef*/ -import CommonOnboardingElements from './commonOnboardingElements'; -import { ChainablePromiseElement } from 'webdriverio'; - -export class OnboardingConnectHardwareWalletPage extends CommonOnboardingElements { - private SUBTITLE_TEXT = '[data-testid="connect-hardware-wallet-subtitle"]'; - private SUPPORTED_DEVICES_TEXT = '[data-testid="connect-hardware-wallet-supported-devices-text"]'; - private LEDGER_BUTTON = '[data-testid="connect-hardware-wallet-button-ledger"]'; - private TREZOR_BUTTON = '[data-testid="connect-hardware-wallet-button-trezor"]'; - - private CONNECT_DEVICE_TEXT = '[data-testid="connect-hardware-wallet-connect-device-text"]'; - - get subTitle(): ChainablePromiseElement { - return $(this.SUBTITLE_TEXT); - } - - get supportedDevices(): ChainablePromiseElement { - return $(this.SUPPORTED_DEVICES_TEXT); - } - - get ledgerButton(): ChainablePromiseElement { - return $(this.LEDGER_BUTTON); - } - - get trezorButton(): ChainablePromiseElement { - return $(this.TREZOR_BUTTON); - } - - get connectDevice(): ChainablePromiseElement { - return $(this.CONNECT_DEVICE_TEXT); - } -} - -export default new OnboardingConnectHardwareWalletPage(); diff --git a/packages/e2e-tests/src/elements/staking/StakePoolListItem.ts b/packages/e2e-tests/src/elements/staking/StakePoolListItem.ts deleted file mode 100644 index ef31ac490..000000000 --- a/packages/e2e-tests/src/elements/staking/StakePoolListItem.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable no-undef */ -import webTester, { LocatorStrategy } from '../../actor/webTester'; -import { WebElement, WebElementFactory as Factory } from './../webElement'; - -export class StakePoolListItem extends WebElement { - protected TABLE_ROW = '//div[@data-testid="stake-pool-table-item"]'; - private LOGO = '//img[@data-testid="stake-pool-list-logo"]'; - private NAME = '//h6[@data-testid="stake-pool-list-name"]'; - private TICKER = '//p[@data-testid="stake-pool-list-ticker"]'; - private ROS = '//p[@data-testid="stake-pool-list-ros"]'; - private COST = '//p[@data-testid="stake-pool-list-cost"]'; - private SATURATION = '//p[@data-testid="stake-pool-list-saturation"]'; - - constructor(index?: number) { - super(); - this.TABLE_ROW = - typeof index === 'undefined' || index.toString() === '' ? this.TABLE_ROW : `(${this.TABLE_ROW})[${index}]`; - } - - container(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}`, 'xpath'); - } - - tableRowWithName(poolName: string): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}[.//h6[contains(text(), '${poolName}')]]`, 'xpath'); - } - - async getRows(): Promise { - return $$(`${this.TABLE_ROW}`); - } - - logo(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.LOGO}`, 'xpath'); - } - - logoWithIndex(index: number): WebElement { - return Factory.fromSelector(`(${this.LOGO})[${index}]`, 'xpath'); - } - - name(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.NAME}`, 'xpath'); - } - - nameWithIndex(index: number): WebElement { - return Factory.fromSelector(`(${this.NAME})[${index}]`, 'xpath'); - } - - ticker(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.TICKER}`, 'xpath'); - } - - tickerWithIndex(index: number): WebElement { - return Factory.fromSelector(`(${this.TICKER})[${index}]`, 'xpath'); - } - - ros(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.ROS}`, 'xpath'); - } - - cost(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.COST}`, 'xpath'); - } - - saturation(): WebElement { - return Factory.fromSelector(`${this.TABLE_ROW}${this.SATURATION}`, 'xpath'); - } - - async getName(): Promise { - return await webTester.getTextValueFromElement(this.name()); - } - - async getTicker(): Promise { - return await webTester.getTextValueFromElement(this.ticker()); - } - - async getRos(): Promise { - return await webTester.getTextValueFromElement(this.ros()); - } - - async getCost(): Promise { - return await webTester.getTextValueFromElement(this.cost()); - } - - async getSaturation(): Promise { - return await webTester.getTextValueFromElement(this.saturation()); - } - - async getNameWithIndex(index: number): Promise { - return await webTester.getTextValueFromElement(this.nameWithIndex(index)); - } - - async getTickerWithIndex(index: number): Promise { - return await webTester.getTextValueFromElement(this.tickerWithIndex(index)); - } - - toJSLocator(): string { - return this.TABLE_ROW; - } - - locatorStrategy(): LocatorStrategy { - return 'xpath'; - } -} diff --git a/packages/e2e-tests/src/elements/staking/stakingPage.ts b/packages/e2e-tests/src/elements/staking/stakingPage.ts index 12f702cd7..1793d9bcb 100644 --- a/packages/e2e-tests/src/elements/staking/stakingPage.ts +++ b/packages/e2e-tests/src/elements/staking/stakingPage.ts @@ -2,17 +2,11 @@ import SectionTitle from '../sectionTitle'; class StakingPage { - private SEARCH_ICON = '[data-testid="search-icon"]'; private SEARCH_INPUT = '.ant-select-selection-search input'; - private SEARCH_INPUT_PLACEHOLDER_IN_POPUP = '.ant-select-selection-placeholder'; - private STAKE_POOL_LIST_HEADER_TEMPLATE = '[data-testid="stake-pool-list-header-###COLUMN_NAME###"]'; - private EMPTY_SEARCH_RESULTS_IMAGE = '[data-testid="stake-pool-table-empty-image"]'; - private EMPTY_SEARCH_RESULTS_MESSAGE = '[data-testid="stake-pool-table-empty-message"]'; private SEARCH_LOADER = '[data-testid="search-loader"]'; private STAKE_POOL_LIST_COST = '[data-testid="stake-pool-list-cost"]'; private STATS_TITLE = '[data-testid="stats-title"]'; private STATS_VALUE = '[data-testid="stats-value"]'; - private STAKE_POOL_TABLE_ROW = '[data-testid="stake-pool-table-item"]'; get title() { return SectionTitle.sectionTitle; @@ -22,10 +16,6 @@ class StakingPage { return SectionTitle.sectionCounter; } - get stakingPageSearchIcon() { - return $(this.SEARCH_ICON); - } - get statsTitle() { return $$(this.STATS_TITLE); } @@ -34,14 +24,6 @@ class StakingPage { return $(this.SEARCH_INPUT); } - get searchInputPlaceholderInPopup() { - return $(this.SEARCH_INPUT_PLACEHOLDER_IN_POPUP); - } - - get rows() { - return $$(this.STAKE_POOL_TABLE_ROW); - } - get statsValues() { return $$(this.STATS_VALUE); } @@ -62,27 +44,10 @@ class StakingPage { return $$(this.STAKE_POOL_LIST_COST); } - get statsValue() { - return $$(this.STATS_VALUE); - } - - get emptySearchResultsImage() { - return $(this.EMPTY_SEARCH_RESULTS_IMAGE); - } - - get emptySearchResultsMessage() { - return $(this.EMPTY_SEARCH_RESULTS_MESSAGE); - } - get searchLoader() { return $(this.SEARCH_LOADER); } - stakingPoolListColumnHeader(listHeader: string) { - const headerColumnSelector = this.STAKE_POOL_LIST_HEADER_TEMPLATE.replace('###COLUMN_NAME###', listHeader); - return $(headerColumnSelector); - } - stakingPoolWithName(poolName: string) { return $(`h6=${poolName}`); } diff --git a/packages/e2e-tests/src/elements/tokensPage.ts b/packages/e2e-tests/src/elements/tokensPage.ts index f992e9347..fe9e702c1 100644 --- a/packages/e2e-tests/src/elements/tokensPage.ts +++ b/packages/e2e-tests/src/elements/tokensPage.ts @@ -2,6 +2,7 @@ import SectionTitle from './sectionTitle'; import { ChainablePromiseElement } from 'webdriverio'; import { ChainablePromiseArray } from 'webdriverio/build/types'; +import TokensPageAssert from '../assert/tokensPageAssert'; class TokensPage { private BALANCE_LABEL = '[data-testid="portfolio-balance-label"]'; @@ -13,6 +14,8 @@ class TokensPage { private TOKEN_TICKER = '[data-testid="token-table-cell-ticker"]'; private TOKEN_BALANCE = '[data-testid="token-table-cell-balance"]'; private TOKEN_FIAT_BALANCE = '[data-testid="token-table-cell-fiat-balance"]'; + private TOKEN_PRICE = '[data-testid="token-table-cell-price"]'; + private TOKEN_VARIATION = '[data-testid="token-table-cell-price-variation"]'; private COINGECKO_CREDITS = '[data-testid="coingecko-credits"]'; private COINGECKO_LINK = '[data-testid="coingecko-link"]'; private RECEIVE_BUTTON_POPUP_MODE = 'main [data-testid="receive-button"]'; @@ -21,6 +24,7 @@ class TokensPage { private OPENED_EYE_ICON = '[data-testid="opened-eye-icon"]'; private VIEW_ALL_BUTTON = '[data-testid="view-all-button"]'; private TOKEN_ROW_SKELETON = '.ant-skeleton'; + private PRICE_FETCH_ERROR_DESCRIPTION = '[data-testid="banner-description"]'; get sendButtonPopupMode(): ChainablePromiseElement { return $(this.SEND_BUTTON_POPUP_MODE); @@ -78,6 +82,14 @@ class TokensPage { return $$(this.TOKENS_TABLE_ROW)[index].$(this.TOKEN_FIAT_BALANCE); } + tokenPriceAda(index: number): ChainablePromiseElement { + return $$(this.TOKENS_TABLE_ROW)[index].$(this.TOKEN_PRICE); + } + + tokenPriceChange(index: number): ChainablePromiseElement { + return $$(this.TOKENS_TABLE_ROW)[index].$(this.TOKEN_VARIATION); + } + tokensTableItemWithName(tokenName: string): ChainablePromiseElement { const selector = `${this.TOKENS_TABLE_ROW}[descendant::*[text()='${tokenName}']]`; return $(selector); @@ -103,6 +115,10 @@ class TokensPage { return $(this.TOKEN_ROW_SKELETON); } + get priceFetchErrorDescription(): ChainablePromiseElement { + return $(this.PRICE_FETCH_ERROR_DESCRIPTION); + } + async getRows(): Promise { return $$(this.TOKENS_TABLE_ROW); } @@ -143,6 +159,10 @@ class TokensPage { async getTokensCounterAsNumber(): Promise { return SectionTitle.getCounterAsNumber(); } + + async waitForPricesToBeFetched() { + await this.totalBalanceValue.waitForDisplayed({ timeout: TokensPageAssert.ADA_PRICE_CHECK_INTERVAL }); + } } export default new TokensPage(); diff --git a/packages/e2e-tests/src/elements/transactionDetails.ts b/packages/e2e-tests/src/elements/transactionDetails.ts index 9ed70b419..719b304d7 100644 --- a/packages/e2e-tests/src/elements/transactionDetails.ts +++ b/packages/e2e-tests/src/elements/transactionDetails.ts @@ -15,7 +15,8 @@ class ActivityDetailsPage extends CommonDrawerElements { private TRANSACTION_DETAILS_SENT_TOKEN = '[data-testid="tx-sent-detail-token"]'; private TRANSACTION_DETAILS_SENT_ADA = '[data-testid="tx-sent-detail-ada"]'; private TRANSACTION_DETAILS_SENT_FIAT = '[data-testid="tx-sent-detail-fiat"]'; - private TRANSACTION_DETAILS_TO_ADDRESS = '[data-testid="tx-to-detail"]'; + private TRANSACTION_DETAILS_TO_ADDRESS = '[data-testid="tx-to-address"]'; + private TRANSACTION_DETAILS_TO_ADDRESS_TAG = '[data-testid="address-tag"]'; private TRANSACTION_DETAILS_STATUS = '[data-testid="tx-status"]'; private TRANSACTION_DETAILS_TIMESTAMP = '[data-testid="tx-timestamp"]'; private TRANSACTION_DETAILS_FEE_ADA = '[data-testid="tx-amount-fee-amount"]'; @@ -86,6 +87,10 @@ class ActivityDetailsPage extends CommonDrawerElements { return $$(this.TRANSACTION_DETAILS_BUNDLE)[index].$(this.TRANSACTION_DETAILS_TO_ADDRESS); } + transactionDetailsToAddressTag(index = 0): ChainablePromiseElement { + return $$(this.TRANSACTION_DETAILS_BUNDLE)[index].$(this.TRANSACTION_DETAILS_TO_ADDRESS_TAG); + } + get transactionDetailsStatus(): ChainablePromiseElement { return $(this.TRANSACTION_DETAILS_STATUS); } diff --git a/packages/e2e-tests/src/enums/StakePoolListColumn.ts b/packages/e2e-tests/src/enums/StakePoolListColumn.ts new file mode 100644 index 000000000..1c90d58b6 --- /dev/null +++ b/packages/e2e-tests/src/enums/StakePoolListColumn.ts @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ +export enum StakePoolListColumn { + Ticker = 'Ticker', + Saturation = 'Saturation', + ROS = 'ROS', + Cost = 'Cost', + Margin = 'Margin', + Blocks = 'Blocks', + Pledge = 'Pledge', + LiveStake = 'Live Stake' +} diff --git a/packages/e2e-tests/src/features/AdaHandleExtended.feature b/packages/e2e-tests/src/features/AdaHandleExtended.feature index a18c4d15b..f92dede50 100644 --- a/packages/e2e-tests/src/features/AdaHandleExtended.feature +++ b/packages/e2e-tests/src/features/AdaHandleExtended.feature @@ -37,7 +37,7 @@ Feature: ADA handle - extended view When I fill address form with "AH 1 edited" name and "$test_handle_3" address Then Green tick icon is displayed next to ADA handle And I click "Done" button on "Edit address" drawer - And I see a toast with message: "browserView.addressBook.toast.editAddress" + And I see a toast with text: "Edited successfully" And I see address row with name "AH 1 edited" and address "$test_handle_3" on the list in extended mode @LW-7337 @@ -58,7 +58,7 @@ Feature: ADA handle - extended view When I fill address form with "AH 1 edited" name and "$test_handle_2" address Then Green tick icon is displayed next to ADA handle And I click "Done" button on "Edit address" drawer - And I see a toast with message: "addressBook.errors.givenAddressAlreadyExist" + And I see a toast with text: "Given address already exists" @LW-7140 @LW-7136 Scenario: Extended View - Ada handles displayed and sorted by handle length @@ -79,18 +79,23 @@ Feature: ADA handle - extended view And I see "Wallet Address" page in extended mode for wallet "WalletAdaHandle" When I click "Copy" button on "Receive" page for default wallet address Then I see a toast with text: "Address copied" + And I close a toast message And Clipboard contains address of wallet: "WalletAdaHandle" When I click "Copy" button on "Receive" page for handle: "$cde" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$cde" When I click "Copy" button on "Receive" page for handle: "$t_h_1" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$t_h_1" When I click "Copy" button on "Receive" page for handle: "$test_handle_1" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$test_handle_1" When I click "Copy" button on "Receive" page for handle: "$test_handle_3" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$test_handle_3" @LW-7427 @LW-7426 @@ -144,8 +149,7 @@ Feature: ADA handle - extended view And I see ADA handle NFT with custom image on the Select NFT page And the corresponding custom images are displayed - @LW-5025 @LW-5028 @LW-5030 @Pending - @issue=LW-9885 + @LW-5025 @LW-5028 @LW-5030 @Pending @issue=LW-9885 Scenario: Extended View - Send flow - Enter ADA handle and confirm validated When I click "Send" button on page header And I enter "$test_handle_3" in the bundle 1 recipient's address diff --git a/packages/e2e-tests/src/features/AdaHandlePopup.feature b/packages/e2e-tests/src/features/AdaHandlePopup.feature index 76f1c8316..21807257e 100644 --- a/packages/e2e-tests/src/features/AdaHandlePopup.feature +++ b/packages/e2e-tests/src/features/AdaHandlePopup.feature @@ -37,7 +37,7 @@ Feature: ADA handle - popup view When I fill address form with "AH 1 edited" name and "$test_handle_3" address Then Green tick icon is displayed next to ADA handle And I click "Done" button on "Edit address" drawer - And I see a toast with message: "browserView.addressBook.toast.editAddress" + And I see a toast with text: "Edited successfully" And I see address row with name "AH 1 edited" and address "$test_handle_3" on the list in popup mode @LW-7338 @@ -58,7 +58,7 @@ Feature: ADA handle - popup view When I fill address form with "AH 1 edited" name and "$test_handle_2" address Then Green tick icon is displayed next to ADA handle And I click "Done" button on "Edit address" drawer - And I see a toast with message: "addressBook.errors.givenAddressAlreadyExist" + And I see a toast with text: "Given address already exists" @LW-7135 @LW-7139 Scenario: Popup View - Ada handles displayed and sorted by handle length @@ -78,18 +78,23 @@ Feature: ADA handle - popup view And I see "Wallet Address" page in popup mode for wallet "WalletAdaHandle" When I click "Copy" button on "Receive" page for default wallet address Then I see a toast with text: "Address copied" + And I close a toast message And Clipboard contains address of wallet: "WalletAdaHandle" When I click "Copy" button on "Receive" page for handle: "$cde" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$cde" When I click "Copy" button on "Receive" page for handle: "$t_h_1" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$t_h_1" When I click "Copy" button on "Receive" page for handle: "$test_handle_1" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$test_handle_1" When I click "Copy" button on "Receive" page for handle: "$test_handle_3" Then I see a toast with text: "Handle copied" + And I close a toast message And Clipboard contains text: "$test_handle_3" @LW-7435 @LW-7436 diff --git a/packages/e2e-tests/src/features/AddressBookExtended.feature b/packages/e2e-tests/src/features/AddressBookExtended.feature index 90f0131ed..48501e142 100644 --- a/packages/e2e-tests/src/features/AddressBookExtended.feature +++ b/packages/e2e-tests/src/features/AddressBookExtended.feature @@ -36,6 +36,7 @@ Feature: Address book - extended view And I click "Save address" button on "Add new address" drawer Then I see a toast with text: "Address added" And I see address row with name "" and address "
" on the list in extended mode + Examples: | wallet_name | address | | Byron_manual | 37btjrVyb4KC6N6XtRHwEuLPQW2aa9JA89gbnm67PArSi8E7vGeqgA6W1pFBphc1hhrk1WKGPZpUbnvYRimVLRVnUH6M6d3dsVdxYoAC4m7oNj7Dzp | @@ -52,6 +53,7 @@ Feature: Address book - extended view When I fill address form with "" name and "
" address Then Contact "" name error and "" address error are displayed And "Save address" button is disabled on "Add new address" drawer + Examples: | wallet_name | address | name_error | address_error | | too_long_name_123456789 | addr_invalid | Max 20 Characters | Invalid Cardano address | @@ -69,6 +71,7 @@ Feature: Address book - extended view And I fill address form with "" name and "" address Then Contact "" name error and "" address error are displayed And "Save address" button is disabled on "Add new address" drawer + Examples: | wallet_name | wallet_name2 | address | address2 | name_error | address_error | | name_ok | empty | 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf | 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf | Name field is required | empty | @@ -100,11 +103,12 @@ Feature: Address book - extended view And I click "Add address" button on address book page When I fill address form with "" name and address from "" address And I click "Save address" button on "Add new address" drawer - Then I see a toast with message: "" + Then I see a toast with text: "" + Examples: - | wallet_name | wallet_address | toast_message | - | Byron | Byron | addressBook.errors.givenNameAlreadyExist | - | SomeWallet | Byron | addressBook.errors.givenAddressAlreadyExist | + | wallet_name | wallet_address | toast_message | + | Byron | Byron | Given name already exists | + | SomeWallet | Byron | Given address already exists | @LW-4469 Scenario: Extended-view - Address Book - Copy address button @@ -112,7 +116,7 @@ Feature: Address book - extended view And I have 3 addresses in my address book in extended mode When I click address on the list with name "Byron" And I click "Copy" button on address details page - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And address is saved to clipboard @LW-4470 @@ -124,6 +128,7 @@ Feature: Address book - extended view And I click "Done" button on "Edit address" drawer Then I see a toast with text: "Edited successfully" And I see address row with name "" and address "
" on the list in extended mode + Examples: | edited_address | wallet_name | address | | Shelley | Shelley_edited | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | @@ -145,17 +150,18 @@ Feature: Address book - extended view And I fill address form with "" name and "
" address Then Contact "" name error and "" address error are displayed And "Done" button is disabled on "Edit address" drawer + Examples: | wallet_name | address | name_error | address_error | | empty | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Name field is required | empty | | too_long_name_123456789 | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Max 20 Characters | empty | | " name preceded by space" | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Name has unnecessary white space | empty | -# | valid wallet name | empty | empty | Address field is required | # TODO: Uncomment when LW-7419 is fixed | valid wallet name | invalid_address | empty | Invalid Cardano address | | valid wallet name | " addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja" | empty | Address has unnecessary white space | | valid wallet name | "addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja " | empty | Address has unnecessary white space | -# | empty | empty | Name field is required | Address field is required | # TODO: Uncomment when LW-7419 is fixed | "name followed by space " | invalid_address | Name has unnecessary white space | Invalid Cardano address | +# | valid wallet name | empty | empty | Address field is required | # TODO: Uncomment when LW-7419 is fixed +# | empty | empty | Name field is required | Address field is required | # TODO: Uncomment when LW-7419 is fixed @LW-4567 Scenario Outline: Extended-view - Address Book - Edit address book entry - Uniqueness validation and toast display with text @@ -164,11 +170,12 @@ Feature: Address book - extended view And I click "Edit" button on address details page And I fill address form with "" name and address from "" address And I click "Done" button on "Edit address" drawer - Then I see a toast with message: "" + Then I see a toast with text: "" + Examples: - | wallet_name | wallet_address | toast_message | - | Byron | Byron | addressBook.errors.givenNameAlreadyExist | - | SomeWallet | Byron | addressBook.errors.givenAddressAlreadyExist | + | wallet_name | wallet_address | toast_message | + | Byron | Byron | Given name already exists | + | SomeWallet | Byron | Given address already exists | @LW-4535 Scenario: Extended-view - Address Book - Edit address and click exit button @@ -196,13 +203,13 @@ Feature: Address book - extended view Given I don't have any addresses added to my address book in extended mode When I click on a widget item with subtitle: "" Then I see a "" article with title "" + Examples: | type | subtitle | | Glossary | What is the Lace address book? | | Glossary | What is a saved address? | - @LW-4744 @Pending - @issue=LW-7697 + @LW-4744 @Pending @issue=LW-7697 Scenario: Extended-view - Address Book - Enter and Escape buttons support when editing address Given I have 3 addresses in my address book in extended mode When I click address on the list with name "Byron" @@ -214,7 +221,7 @@ Feature: Address book - extended view When I press keyboard Enter button And I fill address form with "Byron_edited" name and "37btjrVyb4KC6N6XtRHwEuLPQW2aa9JA89gbnm67PArSi8E7vGeqgA6W1pFBphc1hhrk1WKGPZpUbnvYRimVLRVnUH6M6d3dsVdxYoAC4m7oNj7Dzp" address When I press keyboard Enter button - Then I see a toast with message: "browserView.addressBook.toast.editAddress" + Then I see a toast with text: "Edited successfully" @LW-4745 Scenario: Extended-view - Address Book - Escape button support when closing drawer @@ -224,8 +231,7 @@ Feature: Address book - extended view When I press keyboard Escape button Then I do not see address detail page in extended mode with details of "Byron" address - @LW-4779 @Pending - @issue=LW-7419 + @LW-4779 @Pending @issue=LW-7419 Scenario: Extended-view - Address Book - Display error message after filling name and clicking outside with empty address Given I don't have any addresses added to my address book in extended mode And I click "Add address" button on address book page @@ -235,8 +241,7 @@ Feature: Address book - extended view And I click outside address form to lose focus Then Contact "empty" name error and "Address field is required" address error are displayed - @LW-4780 @Pending - @issue=LW-7419 + @LW-4780 @Pending @issue=LW-7419 Scenario: Extended-view - Address Book - Display error message when adding valid address and clicking outside with empty name field Given I don't have any addresses added to my address book in extended mode And I click "Add address" button on address book page @@ -246,8 +251,7 @@ Feature: Address book - extended view And I click outside address form to lose focus Then Contact "Name field is required" name error and "empty" address error are displayed - @LW-4781 @Pending - @issue=LW-7419 + @LW-4781 @Pending @issue=LW-7419 Scenario: Extended-view - Address Book - No error is displayed when leaving both fields empty Given I don't have any addresses added to my address book in extended mode And I click "Add address" button on address book page @@ -272,6 +276,7 @@ Feature: Address book - extended view And I close wallet synced toast When I add new address: "
" with name: "" in extended mode Then I verify that address: "
" with name: "" has been added in extended mode + Examples: | wallet_name | address | | example_name1 | addr_test1qzngq82mhkzqttqvdk8yl4twk4ea70ja2e7j92x9vqwatds4dm4z5j48w9mjpag2htut4g6pzfxm7x958m3wxjwc8t6q8k6txr | @@ -295,6 +300,7 @@ Feature: Address book - extended view And I switch network to: "Mainnet" in extended mode And I open address book in extended mode Then I see address row with name "" and address "
" on the list in extended mode + Examples: | wallet_name | address | | example_name2 | addr_test1qzcx0kfmglh9hg5wa7kxzt3c3e8psnm0pus38qth0wgmmljcexj60ge60d8h7nyz9ez0mzgxznr5kr6rfsemdqp74p0q9rw57j | diff --git a/packages/e2e-tests/src/features/AddressBookPopup.feature b/packages/e2e-tests/src/features/AddressBookPopup.feature index 594c8b0fd..ccb53a548 100644 --- a/packages/e2e-tests/src/features/AddressBookPopup.feature +++ b/packages/e2e-tests/src/features/AddressBookPopup.feature @@ -15,7 +15,7 @@ Feature: Address book - popup view And I have 3 addresses in my address book in popup mode When I click address on the list with name "Byron" And I click "Copy" button on address details page - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And address is saved to clipboard @LW-4475 @@ -27,6 +27,7 @@ Feature: Address book - popup view And I click "Done" button on "Edit address" drawer Then I see a toast with text: "Edited successfully" And I see address row with name "" and address "" on the list in popup mode + Examples: | edited_address | wallet_name | address | address_label | | Shelley | Shelley_edit | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | addr_tes...6ja | @@ -48,17 +49,18 @@ Feature: Address book - popup view And I fill address form with "" name and "
" address Then Contact "" name error and "" address error are displayed And "Done" button is disabled on "Edit address" drawer + Examples: | wallet_name | address | name_error | address_error | | empty | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Name field is required | empty | | too_long_name_123456789 | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Max 20 Characters | empty | | " name preceded by space" | addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja | Name has unnecessary white space | empty | -# | valid wallet name | empty | empty | Address field is required | # TODO: Uncomment when LW-7419 is fixed - | valid wallet name | invalid_address | empty | Invalid Cardano address | + | valid wallet name | invalid_address | empty | Invalid Cardano address | | valid wallet name | " addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja" | empty | Address has unnecessary white space | | valid wallet name | "addr_test1qq959a7g4spmkg4gz2yw02622c739p8crt6tzh04qzag992wcj4m99m95nmkgxhk8j0upqp2jzaxxdsj3jf9v4yhv3uqfwr6ja " | empty | Address has unnecessary white space | + | "name followed by space " | invalid_address | Name has unnecessary white space | Invalid Cardano address | +# | valid wallet name | empty | empty | Address field is required | # TODO: Uncomment when LW-7419 is fixed # | empty | empty | Name field is required | Address field is required | # TODO: Uncomment when LW-7419 is fixed - | "name followed by space " | invalid_address | Name has unnecessary white space | Invalid Cardano address | @LW-4568 Scenario Outline: Popup-view - Address Book - Edit address book entry - Uniqueness validation and toast display with text @@ -68,11 +70,12 @@ Feature: Address book - popup view And I fill address form with "" name and address from "" address And "Done" button is enabled on "Edit address" drawer Then I click "Done" button on "Edit address" drawer - And I see a toast with message: "" + And I see a toast with text: "" + Examples: - | wallet_name | wallet_address | toast_message | - | Byron | Byron | addressBook.errors.givenNameAlreadyExist | - | SomeWallet | Byron | addressBook.errors.givenAddressAlreadyExist | + | wallet_name | wallet_address | toast_message | + | Byron | Byron | Given name already exists | + | SomeWallet | Byron | Given address already exists | @LW-4534 Scenario: Popup-view - Address Book - Edit address and click back button @@ -111,6 +114,7 @@ Feature: Address book - popup view And I click "Save address" button on "Add new address" drawer Then I see a toast with text: "Address added" And I see address row with name "" and address "" on the list in popup mode + Examples: | wallet_name | wallet_address | | Byron_man | Byron | @@ -127,10 +131,11 @@ Feature: Address book - popup view And I fill address form with "" name and "
" address Then Contact "" name error and "" address error are displayed And "Save address" button is disabled on "Add new address" drawer + Examples: | wallet_name | address | name_error | address_error | - | too_long_name_123456789 | addr_invalid | Max 20 Characters | Invalid Cardano address | - | name_ok | addr_invalid | empty | Invalid Cardano address | + | too_long_name_123456789 | addr_invalid | Max 20 Characters | Invalid Cardano address | + | name_ok | addr_invalid | empty | Invalid Cardano address | | too_long_name_123456789 | 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf | Max 20 Characters | empty | | "name followed by space " | "2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf " | Name has unnecessary white space | Address has unnecessary white space | | " name preceded by space" | " 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf" | Name has unnecessary white space | Address has unnecessary white space | @@ -143,6 +148,7 @@ Feature: Address book - popup view And I fill address form with "" name and "" address Then Contact "" name error and "" address error are displayed And "Save address" button is disabled on "Add new address" drawer + Examples: | wallet_name | wallet_name2 | address | address2 | name_error | address_error | | name_ok | empty | 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf | 2cWKMJemoBainaQxNUjUnKDr6mGgSERDRrvKAJzWejubdymYZv1uKedpSYkkehHnSwMCf | Name field is required | empty | @@ -155,14 +161,14 @@ Feature: Address book - popup view When I click "Add address" button on address book page And I fill address form with "" name and address from "" address And I click "Save address" button on "Add new address" drawer - Then I see a toast with message: "" + Then I see a toast with text: "" + Examples: - | wallet_name | wallet_address | toast_message | - | Byron | Byron | addressBook.errors.givenNameAlreadyExist | - | SomeWallet | Byron | addressBook.errors.givenAddressAlreadyExist | + | wallet_name | wallet_address | toast_message | + | Byron | Byron | Given name already exists | + | SomeWallet | Byron | Given address already exists | - @LW-4784 @Pending - @issue=LW-7419 + @LW-4784 @Pending @issue=LW-7419 Scenario: Popup-view - Address Book - Display error message after filling name and clicking outside with empty address Given I don't have any addresses added to my address book in popup mode When I click "Add address" button on address book page @@ -172,8 +178,7 @@ Feature: Address book - popup view And I click outside address form to lose focus Then Contact "Name field is required" name error and "empty" address error are displayed - @LW-4783 @Pending - @issue=LW-7419 + @LW-4783 @Pending @issue=LW-7419 Scenario: Popup-view - Address Book - No error is displayed when leaving both fields empty Given I don't have any addresses added to my address book in popup mode When I click "Add address" button on address book page @@ -185,8 +190,7 @@ Feature: Address book - popup view Then Contact "empty" name error and "empty" address error are displayed And "Save address" button is disabled on "Add new address" drawer - @LW-4785 @Pending - @issue=LW-7419 + @LW-4785 @Pending @issue=LW-7419 Scenario: Popup-view - Address Book - Display error message after filling name and clicking outside with empty address Given I don't have any addresses added to my address book in popup mode When I click "Add address" button on address book page diff --git a/packages/e2e-tests/src/features/EmptyStatesExtended.feature b/packages/e2e-tests/src/features/EmptyStatesExtended.feature index 273e140f5..1697237c3 100644 --- a/packages/e2e-tests/src/features/EmptyStatesExtended.feature +++ b/packages/e2e-tests/src/features/EmptyStatesExtended.feature @@ -10,7 +10,7 @@ Feature: Empty states Then I see empty state banner for Tokens page in extended mode And I do not see CoinGecko credits When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-2516 @LW-7236 Scenario: Extended View - NFTs empty state @@ -18,21 +18,21 @@ Feature: Empty states Then I see empty state banner for NFTs page in extended mode And I do not see "Create folder" button on NFTs page in extended mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-4445 @Smoke Scenario: Extended View - Transactions empty state When I navigate to Transactions extended page Then I see empty state banner for Transactions page in extended mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-8447 Scenario: Extended View - Staking empty state And I navigate to Staking extended page Then I see empty state banner for Staking page in extended mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-3746 Scenario: Extended-view - verify that MAX button is hidden when user has no tokens available in the wallet diff --git a/packages/e2e-tests/src/features/EmptyStatesPopup.feature b/packages/e2e-tests/src/features/EmptyStatesPopup.feature index daa1864c5..b345407fd 100644 --- a/packages/e2e-tests/src/features/EmptyStatesPopup.feature +++ b/packages/e2e-tests/src/features/EmptyStatesPopup.feature @@ -9,7 +9,7 @@ Feature: Empty states Then I see empty state banner for Tokens page in popup mode And I do not see CoinGecko credits When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-2517 @LW-7239 Scenario: Popup View - NFTs empty state @@ -17,21 +17,21 @@ Feature: Empty states Then I see empty state banner for NFTs page in popup mode And I do not see "Create folder" button on NFTs page in popup mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-4448 Scenario: Popup View - Transactions empty state When I navigate to Transactions popup page Then I see empty state banner for Transactions page in popup mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-8470 Scenario: Popup View - Staking empty state And I navigate to Staking popup page Then I see empty state banner for Staking page in popup mode When I click "Copy" button on empty state banner - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-5522 Scenario: Popup View - Settings - Not enough Ada for Collateral diff --git a/packages/e2e-tests/src/features/MultiDelegationPageExtended.feature b/packages/e2e-tests/src/features/MultiDelegationPageExtended.part1.feature similarity index 89% rename from packages/e2e-tests/src/features/MultiDelegationPageExtended.feature rename to packages/e2e-tests/src/features/MultiDelegationPageExtended.part1.feature index 91e855d6e..335fc1244 100644 --- a/packages/e2e-tests/src/features/MultiDelegationPageExtended.feature +++ b/packages/e2e-tests/src/features/MultiDelegationPageExtended.part1.feature @@ -88,22 +88,27 @@ Feature: Staking Page - Extended View Then I see the Network Info component with the expected content @LW-8499 @Testnet @Mainnet - Scenario Outline: Extended View - Staking - Show tooltip for column in browse pools section + Scenario: Extended View - Staking - Show tooltip for columns in browse pools section When I navigate to Staking extended page And I open Browse pools tab And I switch to list view on "Browse pools" tab - When I hover over "" column name in stake pool list - Then tooltip for "" column is displayed - Examples: - | column_name | - | Ticker | - | Saturation | -# | ROS | #TODO: Uncomment when USE_ROS_STAKING_COLUMN=true - | Cost | - | Margin | - | Blocks | - | Pledge | - | Live Stake | + When I hover over "Ticker" column name in stake pool list + Then tooltip for "Ticker" column is displayed + When I hover over "Saturation" column name in stake pool list + Then tooltip for "Saturation" column is displayed + #TODO: Uncomment when USE_ROS_STAKING_COLUMN=true + #When I hover over "ROS" column name in stake pool list + #Then tooltip for "ROS" column is displayed + When I hover over "Cost" column name in stake pool list + Then tooltip for "Cost" column is displayed + When I hover over "Margin" column name in stake pool list + Then tooltip for "Margin" column is displayed + When I hover over "Blocks" column name in stake pool list + Then tooltip for "Blocks" column is displayed + When I hover over "Pledge" column name in stake pool list + Then tooltip for "Pledge" column is displayed + When I hover over "Live Stake" column name in stake pool list + Then tooltip for "Live Stake" column is displayed @LW-8637 @Testnet @Mainnet Scenario: Extended View - Staking password screen details @@ -186,12 +191,6 @@ Feature: Staking Page - Extended View | list | I refresh the page | | list | I open Overview tab | - @LW-10143 @Testnet @Mainnet - Scenario: Extended View - Staking - More options - Sorting options are displayed - When I am on Staking extended page - And I open Browse pools tab - Then "More options" component with stake pool sorting options is displayed - @LW-9996 @Testnet @Mainnet Scenario: Extended View - Grid - display stake pool cards based on browser width When I am on Staking extended page diff --git a/packages/e2e-tests/src/features/MultiDelegationPageExtended.part2.feature b/packages/e2e-tests/src/features/MultiDelegationPageExtended.part2.feature new file mode 100644 index 000000000..b1975ca61 --- /dev/null +++ b/packages/e2e-tests/src/features/MultiDelegationPageExtended.part2.feature @@ -0,0 +1,51 @@ +@Staking-NonDelegatedFunds-Extended +Feature: Staking Page - Extended View + + Background: + Given Lace is ready for test + + @LW-10143 @Testnet @Mainnet + Scenario: Extended View - Staking - More options - Sorting options are displayed + When I am on Staking extended page + And I open Browse pools tab + Then "More options" component with stake pool sorting options is displayed + + @LW-10139 @LW-10141 @LW-10142 @Testnet @Mainnet + Scenario: Extended View - Staking - List View - Stake pool list sorting by ticker (default) + When I am on Staking extended page + And I open Browse pools tab + And I switch to list view on "Browse pools" tab + Then stake pool list view is displayed + And ascending sorting indicator is displayed for "Ticker" column + And stake pool list rows are sorted by "Ticker" in ascending order + When I click on stake pools table "Ticker" column header + Then descending sorting indicator is displayed for "Ticker" column + And stake pool list rows are sorted by "Ticker" in descending order + When I click on stake pools table "Ticker" column header + Then ascending sorting indicator is displayed for "Ticker" column + And stake pool list rows are sorted by "Ticker" in ascending order + + @LW-10141 @LW-10142 @Testnet @Mainnet + Scenario Outline: Extended View - Staking - List View - sorting by column - + When I am on Staking extended page + And I open Browse pools tab + And I switch to list view on "Browse pools" tab + Then stake pool list view is displayed + When I click on stake pools table "" column header + Then sorting indicator is displayed for "" column + And stake pool list rows are sorted by "" in order + When I click on stake pools table "" column header + Then sorting indicator is displayed for "" column + And stake pool list rows are sorted by "" in order + When I click on stake pools table "" column header + Then sorting indicator is displayed for "" column + And stake pool list rows are sorted by "" in order + Examples: + | column | default_order | modified_order | + | Saturation | descending | ascending | +# | ROS | descending | ascending |# TODO: Uncomment when USE_ROS_STAKING_COLUMN=true + | Cost | ascending | descending | + | Margin | ascending | descending | + | Blocks | descending | ascending | + | Pledge | descending | ascending | + | Live Stake | descending | ascending | diff --git a/packages/e2e-tests/src/features/NavigationTopExtended.feature b/packages/e2e-tests/src/features/NavigationTopExtended.feature index 64480fa30..49a104135 100644 --- a/packages/e2e-tests/src/features/NavigationTopExtended.feature +++ b/packages/e2e-tests/src/features/NavigationTopExtended.feature @@ -38,7 +38,7 @@ Feature: Top Navigation - Extended view When I click the menu button Then the dropdown menu is visible When I click on the user details button - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-4598 @Testnet Scenario: Extended View - network id is visible for Testnet @@ -98,7 +98,7 @@ Feature: Top Navigation - Extended view When I click the menu button And I click on the network option When I click on "Preview" radio button - Then I see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I see a toast with text: "Switched network" Then Lace is loaded properly @LW-6074 @Testnet @Mainnet @@ -120,7 +120,7 @@ Feature: Top Navigation - Extended view Given I close wallet synced toast When I am in the offline network mode: true Then I see network id with status: offline - And I see a toast with message: "general.errors.networkError" + And I see a toast with text: "Network Error" When I click the menu button Then wallet sync status component is visible And sync status displays "Not synced to the blockchain" state diff --git a/packages/e2e-tests/src/features/NavigationTopPopup.feature b/packages/e2e-tests/src/features/NavigationTopPopup.feature index ec3240794..331761712 100644 --- a/packages/e2e-tests/src/features/NavigationTopPopup.feature +++ b/packages/e2e-tests/src/features/NavigationTopPopup.feature @@ -23,7 +23,7 @@ Feature: Top Navigation - Popup view When I click the menu button Then the dropdown menu is visible When I click on the user details button - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" @LW-4599 @Testnet Scenario: Popup View - network id is visible for Testnet @@ -73,7 +73,7 @@ Feature: Top Navigation - Popup view When I click the menu button And I click on the network option When I click on "Preview" radio button - Then I see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I see a toast with text: "Switched network" And Lace is loaded properly @LW-6079 @Testnet @Mainnet @@ -94,7 +94,7 @@ Feature: Top Navigation - Popup view Given I close wallet synced toast When I am in the offline network mode: true Then I see network id with status: offline - And I see a toast with message: "general.errors.networkError" + And I see a toast with text: "Network Error" When I click the menu button Then wallet sync status component is visible And sync status displays "Not synced to the blockchain" state diff --git a/packages/e2e-tests/src/features/OnboardingCreateWallet.feature b/packages/e2e-tests/src/features/OnboardingCreateWallet.feature index 137fac069..6327978ad 100755 --- a/packages/e2e-tests/src/features/OnboardingCreateWallet.feature +++ b/packages/e2e-tests/src/features/OnboardingCreateWallet.feature @@ -37,12 +37,12 @@ Feature: Onboarding - Create wallet When I enter wallet name: "wallet", password: "" and password confirmation: "" Then Password recommendation: "", complexity bar level: "" and password confirmation error: "" are displayed Examples: - | password | password_conf | passw_err | complex_bar_lvl | passw_conf_err | + | password | password_conf | passw_err | complex_bar_lvl | passw_conf_err | | a | | core.walletNameAndPasswordSetupStep.firstLevelPasswordStrengthFeedback | 1 | empty | | P@ss | | core.walletNameAndPasswordSetupStep.firstLevelPasswordStrengthFeedback | 1 | empty | | N_8J@bne | | core.walletNameAndPasswordSetupStep.secondLevelPasswordStrengthFeedback | 2 | empty | - | N_8J@bne87 | | empty | 3 | empty | - | N_8J@bne87A | N_8J@bne87 | empty | 4 | core.walletSetupRegisterStep.noMatchPassword | + | N_8J@bne87 | | empty | 3 | empty | + | N_8J@bne87A | N_8J@bne87 | empty | 4 | core.walletSetupRegisterStep.noMatchPassword | @LW-3013 Scenario: Create Wallet - Mnemonic writedown page - appears correctly after wallet setup page diff --git a/packages/e2e-tests/src/features/OnboardingHardwareWallet.feature b/packages/e2e-tests/src/features/OnboardingHardwareWallet.feature index 4ab90655e..0a5b85a7c 100755 --- a/packages/e2e-tests/src/features/OnboardingHardwareWallet.feature +++ b/packages/e2e-tests/src/features/OnboardingHardwareWallet.feature @@ -4,20 +4,12 @@ Feature: Onboarding - Hardware wallet @LW-3367 Scenario: Hardware Wallet - Connect button click When I click "Connect" button on wallet setup page - And I click "OK" button on "Limited support for DApp" modal - Then "Connect Hardware Wallet" page is displayed - - @LW-3368 - Scenario: Hardware wallet - Legal page - next button disabled - When I click "Connect" button on wallet setup page - And I click "OK" button on "Limited support for DApp" modal - And "Next" button is disabled during onboarding process + Then "Connect your device" page is displayed @LW-3374 - Scenario: Hardware wallet - Connect Hardware Wallet - back button click + Scenario: Hardware wallet - Connect your device - back button click Given I click "Connect" button on wallet setup page - And I click "OK" button on "Limited support for DApp" modal - And "Connect Hardware Wallet" page is displayed + And "Connect your device" page is displayed When I click "Back" button during wallet setup Then "Get started" page is displayed @@ -27,10 +19,17 @@ Feature: Onboarding - Hardware wallet When "Get started" page is displayed Then I see current onboarding page in mode And I click "Connect" button on wallet setup page - And I click "OK" button on "Limited support for DApp" modal - And "Connect Hardware Wallet" page is displayed + And "Connect your device" page is displayed Then I see current onboarding page in mode Examples: | mode | | dark | | light | + + @LW-10309 + Scenario: Hardware wallet - Connect your device - "No hardware wallet device was chosen." error + When I click "Connect" button on wallet setup page + # Step below triggers error by closing HID window + And I switch to window with Lace + Then "No hardware wallet device was chosen." error is displayed on "Connect your device" page + And "Try again" button is enabled on "Connect your device" page diff --git a/packages/e2e-tests/src/features/SendTransactionBundlesExtended.feature b/packages/e2e-tests/src/features/SendTransactionBundlesExtended.feature index 105fff29e..88ff1b545 100644 --- a/packages/e2e-tests/src/features/SendTransactionBundlesExtended.feature +++ b/packages/e2e-tests/src/features/SendTransactionBundlesExtended.feature @@ -167,15 +167,15 @@ Feature: Send - Extended Browser View (Advanced Tx) And I click "Send" button on page header And I enter a valid "shelley" address in the bundle 1 recipient's address And I enter a 51% of total "tADA" asset in bundle 1 + And I open cancel modal to trigger button validation Then I do not see insufficient balance error in bundle 1 for "tADA" asset And "Review transaction" button is enabled on "Send" page When I click "Add bundle" button on "Send" page And I enter a valid "shelley" address in the bundle 2 recipient's address And I enter a 51% of total "tADA" asset in bundle 2 + And I open cancel modal to trigger button validation Then I see insufficient balance error in bundle 2 for "tADA" asset And I do not see insufficient balance error in bundle 1 for "tADA" asset - # step below is a workaround to set focus on something else than input - And I click "Review transaction" button on "Send" page And "Review transaction" button is disabled on "Send" page @LW-1762 @@ -265,7 +265,7 @@ Feature: Send - Extended Browser View (Advanced Tx) @LW-3578 Scenario: Extended-view - Transaction error screen displayed for multiple bundles on transaction submit error - Given I enable network interception to fail request: "*/tx-submit/submit" with error 400 + Given I enable network interception to finish request: "*/tx-submit/submit" with error 400 And I click "Send" button on page header And I set 2 bundles with the same assets And I click "Review transaction" button on "Send" page diff --git a/packages/e2e-tests/src/features/SendTransactionBundlesExtendedMainnet.feature b/packages/e2e-tests/src/features/SendTransactionBundlesExtendedMainnet.feature index f99ad9fd4..6af08506f 100644 --- a/packages/e2e-tests/src/features/SendTransactionBundlesExtendedMainnet.feature +++ b/packages/e2e-tests/src/features/SendTransactionBundlesExtendedMainnet.feature @@ -147,11 +147,13 @@ Feature: Send - Extended Browser View (Advanced Tx) And I click "Send" button on page header And I enter a valid "shelley" address in the bundle 1 recipient's address And I enter a 51% of total "ADA" asset in bundle 1 + And I open cancel modal to trigger button validation Then I do not see insufficient balance error in bundle 1 for "ADA" asset And "Review transaction" button is enabled on "Send" page When I click "Add bundle" button on "Send" page And I enter a valid "shelley" address in the bundle 2 recipient's address And I enter a 51% of total "ADA" asset in bundle 2 + And I open cancel modal to trigger button validation Then I see insufficient balance error in bundle 2 for "ADA" asset And I see insufficient balance error in bundle 1 for "ADA" asset And "Review transaction" button is disabled on "Send" page diff --git a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part1.feature b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part1.feature index 6ca3d9e44..50132b38f 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part1.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part1.feature @@ -176,10 +176,11 @@ Feature: LW-484: Send & Receive - Extended Browser View (Simple Tx) And I click "Send" button on page header And I enter a valid "shelley" address in the bundle 1 recipient's address When I enter a value of: 99999999 to the "tADA" asset - And I click on transaction drawer background to lose focus + And I open cancel modal to trigger button validation Then "Insufficient balance" error is displayed on "Send" page And "Review transaction" button is disabled on "Send" page When I enter a value of: 2 to the "tADA" asset + And I open cancel modal to trigger button validation Then "Insufficient balance" error is not displayed on "Send" page And "Review transaction" button is enabled on "Send" page diff --git a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part2.feature b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part2.feature index 78f261130..eec03d9e0 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part2.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part2.feature @@ -128,7 +128,7 @@ Feature: LW-484: Send & Receive - Extended Browser View (Simple Tx) When I click "Send" button on page header When I enter a valid "shelley" address in the bundle 1 recipient's address Then I enter a value of: to the "tADA" asset in bundle 1 - And I click on transaction drawer background to lose focus + And I open cancel modal to trigger button validation Then "Insufficient balance" error displayed on "Send" page Then "Review transaction" button is on "Send" page Examples: diff --git a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part3.feature b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part3.feature index f3d9ffdf7..efd300400 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part3.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimpleExtended.part3.feature @@ -125,7 +125,7 @@ Feature: LW-484: Send & Receive - Extended Browser View (Simple Tx) @LW-2374 @Testnet Scenario: Extended-view - Transaction error screen displayed on transaction submit error - Given I enable network interception to fail request: "*/tx-submit/submit" with error 400 + Given I enable network interception to finish request: "*/tx-submit/submit" with error 400 And I click "Send" button on page header And I’ve entered accepted values for all fields of simple Tx And I click "Review transaction" button on "Send" page diff --git a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part1.feature b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part1.feature index 15bbe09cf..2b40f6cd7 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part1.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part1.feature @@ -169,10 +169,11 @@ Feature: LW-484: Send & Receive - Popup View (Simple Tx) And I click "Send" button on Tokens page in popup mode And I enter a valid "shelley" address in the bundle 1 recipient's address When I enter a value of: 99999999 to the "tADA" asset in bundle 1 - And I click on transaction drawer background to lose focus + And I open cancel modal to trigger button validation Then "Insufficient balance" error is displayed on "Send" page And "Review transaction" button is disabled on "Send" page When I enter a value of: 2 to the "tADA" asset in bundle 1 + And I open cancel modal to trigger button validation Then "Insufficient balance" error is not displayed on "Send" page Then "Review transaction" button is enabled on "Send" page diff --git a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part2.feature b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part2.feature index a5a6cf33f..a3f00b3b1 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part2.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part2.feature @@ -138,7 +138,7 @@ Feature: LW-484: Send & Receive - Popup View (Simple Tx) When I click "Send" button on Tokens page in popup mode When I enter a valid "shelley" address in the bundle 1 recipient's address Then I enter a value of: to the "tADA" asset in bundle 1 - And I click on transaction drawer background to lose focus + And I open cancel modal to trigger button validation Then "Insufficient balance" error displayed on "Send" page And "Review transaction" button is on "Send" page Examples: diff --git a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part3.feature b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part3.feature index 88910b51c..3d905a2b2 100644 --- a/packages/e2e-tests/src/features/SendTransactionSimplePopup.part3.feature +++ b/packages/e2e-tests/src/features/SendTransactionSimplePopup.part3.feature @@ -103,7 +103,7 @@ Feature: LW-484: Send & Receive - Popup View (Simple Tx) @LW-2408 @Testnet Scenario: Popup-view - Transaction error screen displayed on transaction submit error - Given I enable network interception to fail request: "*/tx-submit/submit" with error 400 + Given I enable network interception to finish request: "*/tx-submit/submit" with error 400 And I click "Send" button on Tokens page in popup mode And I’ve entered accepted values for all fields of simple Tx And I click "Review transaction" button on "Send" page diff --git a/packages/e2e-tests/src/features/SettingsPageExtended.feature b/packages/e2e-tests/src/features/SettingsPageExtended.feature index fc96c00e0..b3f0f35e8 100644 --- a/packages/e2e-tests/src/features/SettingsPageExtended.feature +++ b/packages/e2e-tests/src/features/SettingsPageExtended.feature @@ -65,7 +65,7 @@ Feature: General Settings - Extended Browser View And I click on "Your keys" setting And I click on Show public key button And I click "Copy" button on "Show public key" page - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And I see that content of "TestAutomationWallet" public key is in clipboard @LW-2674 @Mainnet @Testnet @@ -79,7 +79,7 @@ Feature: General Settings - Extended Browser View When I open settings from header menu And I click on "Network" setting When I click on "Preprod" radio button - Then I don't see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I don't see a toast with text: "Switched network" And I close the drawer by clicking close button When I navigate to Tokens extended page Then I see network id: "Preprod" @@ -90,7 +90,7 @@ Feature: General Settings - Extended Browser View When I open settings from header menu And I click on "Network" setting When I click on "Mainnet" radio button - Then I don't see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I don't see a toast with text: "Switched network" And I close the drawer by clicking close button When I navigate to Tokens extended page Then I do not see network id: "Mainnet" @@ -126,7 +126,7 @@ Feature: General Settings - Extended Browser View Scenario: Extended View - Settings - Toast displayed after switching network Given I am on Settings extended page When I switch network to: "Preview" without closing drawer - Then I see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I see a toast with text: "Switched network" @LW-2718 @Testnet Scenario: Extended View - Settings - Token/NFTs updated after network switching diff --git a/packages/e2e-tests/src/features/SettingsPagePopup.feature b/packages/e2e-tests/src/features/SettingsPagePopup.feature index a4e357eb1..1ee2ba223 100644 --- a/packages/e2e-tests/src/features/SettingsPagePopup.feature +++ b/packages/e2e-tests/src/features/SettingsPagePopup.feature @@ -74,7 +74,7 @@ Feature: General Settings - Popup View And I click on "Your keys" setting And I click on Show public key button And I click "Copy" button on "Show public key" page - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And I see that content of "TestAutomationWallet" public key is in clipboard @LW-2716 @Mainnet @Testnet @@ -88,7 +88,7 @@ Feature: General Settings - Popup View When I open settings from header menu And I click on "Network" setting When I click on "Preprod" radio button - Then I don't see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I don't see a toast with text: "Switched network" And I close the drawer by clicking back button When I navigate to Tokens popup page Then I see network id: "Preprod" @@ -99,7 +99,7 @@ Feature: General Settings - Popup View When I open settings from header menu And I click on "Network" setting When I click on "Mainnet" radio button - Then I don't see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I don't see a toast with text: "Switched network" And I close the drawer by clicking back button When I navigate to Tokens popup page Then I do not see network id: "Mainnet" @@ -153,7 +153,7 @@ Feature: General Settings - Popup View Scenario: Popup View - Settings - Toast displayed after switching network When I open settings from header menu When I switch network to: "Preview" without closing drawer - Then I see a toast with message: "browserView.settings.wallet.network.networkSwitched" + Then I see a toast with text: "Switched network" @LW-2719 @Testnet Scenario: Popup View - Settings - Token/NFTs updated after network switching diff --git a/packages/e2e-tests/src/features/StakingPageExtended.feature b/packages/e2e-tests/src/features/StakingPageExtended.feature deleted file mode 100644 index 4cd6d3f21..000000000 --- a/packages/e2e-tests/src/features/StakingPageExtended.feature +++ /dev/null @@ -1,41 +0,0 @@ -@Staking-NonDelegatedFunds-Extended @Pending -Feature: Staking Page - Extended Browser View - - Background: - Given Wallet is synced - - @LW-4024 @Pending @Testnet @Mainnet - @issue=ADP-2344 - Scenario: Extended View - Stake pool list default sorting by ROS - When I navigate to Staking extended page - And I reveal all stake pools - Then the results are in descending order according to "ros" column - - @LW-2706 @Pending @Testnet @Mainnet - @issue=ADP-2344 - Scenario Outline: Extended View - Sort lists ascending - column: - When I navigate to Staking extended page - And I click on the "" column header - And I reveal all stake pools - Then the results are in ascending order according to "" column - Examples: - | column | - | name | - | ros | - | cost | - | saturation | - - @LW-2706 @Pending @Testnet @Mainnet - @issue=ADP-2344 - Scenario Outline: Extended View - Sort lists descending - column - When I navigate to Staking extended page - And I click on the "" column header - And I click on the "" column header - And I reveal all stake pools - Then the results are in descending order according to "" column - Examples: - | column | - | name | - | ros | - | cost | - | saturation | diff --git a/packages/e2e-tests/src/features/TokensPageExtended.feature b/packages/e2e-tests/src/features/TokensPageExtended.feature index c59877f53..3b9be53e7 100644 --- a/packages/e2e-tests/src/features/TokensPageExtended.feature +++ b/packages/e2e-tests/src/features/TokensPageExtended.feature @@ -108,7 +108,7 @@ Feature: LW: Tokens tab - extended view And I see total wallet balance in USD And balance and FIAT balance for each token are visible - @Testnet @Mainnet @LW-7121 @LW-7123 + @LW-7121 @LW-7123 @Testnet @Mainnet Scenario Outline: Extended View - Hide my balance - keep state after the page When I click closed eye icon on Tokens page Then opened eye icon is displayed on Tokens page @@ -124,8 +124,7 @@ Feature: LW: Tokens tab - extended view # LW-7706 # | reopening | I reopen the page | - - @Testnet @Mainnet @LW-7125 + @LW-7125 @Testnet @Mainnet Scenario: Extended view - Hide my balance - keep state after switching to popup view When I click closed eye icon on Tokens page Then opened eye icon is displayed on Tokens page @@ -135,3 +134,47 @@ Feature: LW: Tokens tab - extended view Then opened eye icon is displayed on Tokens page And total wallet balance is masked with asterisks And balance and FIAT balance for each token are masked with asterisks + + @LW-6889 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Extended view - Token pricing - Price fetch expired error is displayed when coingecko request fails + Given ADA fiat price has been fetched + When I enable network interception to fail request: "https://coingecko.*" + And I shift back last fiat price fetch time in local storage by 500 seconds + Then "Price data expired" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + And "Price data expired" error is not displayed + + @LW-10283 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Extended view - Token pricing - Price fetch expired error is displayed when coingecko request returns 500 + Given ADA fiat price has been fetched + When I enable network interception to finish request: "https://coingecko.*" with error 500 + And I shift back last fiat price fetch time in local storage by 500 seconds + Then "Price data expired" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + And "Price data expired" error is not displayed + + @LW-6890 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Extended view - Token pricing - Fiat price unable to fetch error is displayed on failed request + Given ADA fiat price has been fetched + When I enable network interception to fail request: "https://coingecko.*" + And I delete fiat price timestamp from background storage + Then "Unable to fetch fiat values" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + Then "Unable to fetch fiat values" error is not displayed + + @LW-6681 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Extended view - Token pricing - Fiat price unable to fetch error is displayed when coingecko request returns 500 + Given ADA fiat price has been fetched + When I enable network interception to finish request: "https://coingecko.*" with error 500 + And I delete fiat price timestamp from background storage + Then "Unable to fetch fiat values" error is displayed + And I disable network interception + Then ADA fiat price has been fetched + Then "Unable to fetch fiat values" error is not displayed diff --git a/packages/e2e-tests/src/features/TokensPagePopup.feature b/packages/e2e-tests/src/features/TokensPagePopup.feature index fc0214081..6849bdd43 100644 --- a/packages/e2e-tests/src/features/TokensPagePopup.feature +++ b/packages/e2e-tests/src/features/TokensPagePopup.feature @@ -78,7 +78,7 @@ Feature: LW: Tokens tab - popup view And I see total wallet balance in USD And balance and FIAT balance for each token are visible - @Testnet @Mainnet @LW-7122 @LW-7124 + @LW-7122 @LW-7124 @Testnet @Mainnet Scenario Outline: Popup View - Hide my balance - keep state after the page When I click closed eye icon on Tokens page Then opened eye icon is displayed on Tokens page @@ -94,7 +94,7 @@ Feature: LW: Tokens tab - popup view # LW-7706 # | reopening | I reopen the page | - @Testnet @Mainnet @LW-7126 + @LW-7126 @Testnet @Mainnet Scenario: Popup View - Hide my balance - keep state after switching to extended view When I click closed eye icon on Tokens page Then opened eye icon is displayed on Tokens page @@ -104,3 +104,47 @@ Feature: LW: Tokens tab - popup view Then opened eye icon is displayed on Tokens page And total wallet balance is masked with asterisks And balance and FIAT balance for each token are masked with asterisks + + @LW-6684 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Popup view - Token pricing - Price fetch expired error is displayed when coingecko request fails + Given ADA fiat price has been fetched + When I enable network interception to fail request: "https://coingecko.*" + And I shift back last fiat price fetch time in local storage by 500 seconds + Then "Price data expired" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + And "Price data expired" error is not displayed + + @LW-10307 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Popup view - Token pricing - Price fetch expired error is displayed when coingecko request returns 500 + Given ADA fiat price has been fetched + When I enable network interception to finish request: "https://coingecko.*" with error 500 + And I shift back last fiat price fetch time in local storage by 500 seconds + Then "Price data expired" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + And "Price data expired" error is not displayed + + @LW-6682 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Popup view - Token pricing - Fiat price unable to fetch error is displayed on failed request + Given ADA fiat price has been fetched + When I enable network interception to fail request: "https://coingecko.*" + And I delete fiat price timestamp from background storage + Then "Unable to fetch fiat values" error is displayed + When I disable network interception + Then ADA fiat price has been fetched + Then "Unable to fetch fiat values" error is not displayed + + @LW-6683 @Testnet @Mainnet @Pending + @issue=LW-10296 + Scenario: Popup view - Token pricing - Fiat price unable to fetch error is displayed when coingecko request returns 500 + Given ADA fiat price has been fetched + When I enable network interception to finish request: "https://coingecko.*" with error 500 + And I delete fiat price timestamp from background storage + Then "Unable to fetch fiat values" error is displayed + And I disable network interception + Then ADA fiat price has been fetched + Then "Unable to fetch fiat values" error is not displayed diff --git a/packages/e2e-tests/src/features/e2e/SendNftExtendedE2E.feature b/packages/e2e-tests/src/features/e2e/SendNftExtendedE2E.feature index 5a05ab44f..3abefd644 100644 --- a/packages/e2e-tests/src/features/e2e/SendNftExtendedE2E.feature +++ b/packages/e2e-tests/src/features/e2e/SendNftExtendedE2E.feature @@ -6,7 +6,8 @@ Feature: Send NFT - Extended Browser View - E2E And I am on NFTs extended page And I use a single wallet with "Ibilecoin" NFT in extended mode - @LW-2502 @Smoke + @LW-2502 @Smoke @Pending + @issue=LW-10306 Scenario: Extended-view - Send NFT E2E And I'm sending the NFT with name: "Ibilecoin" in extended mode When I enter correct password and confirm the transaction diff --git a/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature b/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature index d54705a36..ba3fceb66 100644 --- a/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature +++ b/packages/e2e-tests/src/features/e2e/SendTransactionDappE2E.feature @@ -4,7 +4,8 @@ Feature: Send Transactions from Dapp - E2E Background: Given Wallet is synced - @LW-3761 @Testnet @Smoke + @LW-3761 @Testnet @Smoke @Pending + @issue=LW-10306 Scenario: Send ADA from DApp E2E And I save token: "Cardano" balance And I open and authorize test DApp with "Only once" setting diff --git a/packages/e2e-tests/src/features/e2e/SendTransactionSimpleExtendedE2E.feature b/packages/e2e-tests/src/features/e2e/SendTransactionSimpleExtendedE2E.feature index 3fdc19bfe..edcb948a4 100644 --- a/packages/e2e-tests/src/features/e2e/SendTransactionSimpleExtendedE2E.feature +++ b/packages/e2e-tests/src/features/e2e/SendTransactionSimpleExtendedE2E.feature @@ -34,7 +34,7 @@ Feature: Send Simple Transactions - Extended view - E2E Scenario: Extended-view - Self Transaction E2E And I click "Receive" button on page header And I click "Copy" button on "Receive" page for default wallet address - Then I see a toast with message: "core.infoWallet.addressCopied" + Then I see a toast with text: "Address copied" And I close the drawer by clicking close button And I click "Send" button on page header And I fill bundle 1 with "CopiedAddress" address with following assets: @@ -60,7 +60,7 @@ Feature: Send Simple Transactions - Extended view - E2E When I click "View transaction" button on submitted transaction page And I click on a transaction: 1 And I click on a transaction hash - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And I see 1 opened tab(s) When I wait for the transaction history to be loaded and all transactions to be confirmed And I click on a transaction hash and save hash information diff --git a/packages/e2e-tests/src/features/e2e/SendTransactionSimplePopupE2E.feature b/packages/e2e-tests/src/features/e2e/SendTransactionSimplePopupE2E.feature index 1b600cbe5..1a56b65de 100644 --- a/packages/e2e-tests/src/features/e2e/SendTransactionSimplePopupE2E.feature +++ b/packages/e2e-tests/src/features/e2e/SendTransactionSimplePopupE2E.feature @@ -34,7 +34,7 @@ Feature: Send Simple Transactions - Popup view - E2E Scenario: Popup-view - Self Transaction E2E And I click "Receive" button on Tokens page in popup mode And I click "Copy" button on "Receive" page for default wallet address - Then I see a toast with message: "core.infoWallet.addressCopied" + Then I see a toast with text: "Address copied" And I close the drawer by clicking close button And I click "Send" button on Tokens page in popup mode And I fill bundle 1 with "CopiedAddress" address with following assets: @@ -60,7 +60,7 @@ Feature: Send Simple Transactions - Popup view - E2E When I click "View transaction" button on submitted transaction page And I click on a transaction: 1 And I click on a transaction hash - Then I see a toast with message: "general.clipboard.copiedToClipboard" + Then I see a toast with text: "Copied to clipboard" And I see 1 opened tab(s) When I wait for the transaction history to be loaded and all transactions to be confirmed And I click on a transaction hash and save hash information diff --git a/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature b/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature index a0b538118..69d27b640 100644 --- a/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature +++ b/packages/e2e-tests/src/features/e2e/StakingInitialFundsE2E.feature @@ -1,7 +1,8 @@ @Staking-initial-E2E @E2E @Testnet Feature: Delegating funds to new pool E2E - @LW-2685 @Smoke + @LW-2685 @Smoke @Pending + @issue=LW-10306 Scenario: Extended view - Staking - Delegating funds to new pool (if not staked yet) E2E. Given I create new wallet and save wallet information And Wallet is synced diff --git a/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsExtendedE2E.feature b/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsExtendedE2E.feature index ba9e1a07e..8500b56a4 100644 --- a/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsExtendedE2E.feature +++ b/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsExtendedE2E.feature @@ -13,7 +13,7 @@ Feature: Staking Page - Switching pools - Extended Browser View - E2E And I wait for single search result And I click stake pool with name "OtherStakePool" Then I see drawer with "OtherStakePool" stake pool details and a button available for staking - And I save stake pool info + And I save stake pool details When I click "Stake on this pool" button on stake pool details drawer And I click "Fine by me" button on "Switching pool?" modal Then I see drawer with stakepool: "OtherStakePool" confirmation screen in extended mode @@ -37,7 +37,7 @@ Feature: Staking Page - Switching pools - Extended Browser View - E2E And I wait for single search result And I click stake pool with name "-" Then I see drawer with stake pool details without metadata and a button available for staking - And I save stake pool info + And I save stake pool details When I click "Stake on this pool" button on stake pool details drawer And I click "Fine by me" button on "Switching pool?" modal And I click "Next" button on staking confirmation drawer @@ -53,7 +53,7 @@ Feature: Staking Page - Switching pools - Extended Browser View - E2E @LW-4558 @Testnet Scenario: Extended View - Staking - Staking error screen displayed on transaction submit error - Given I enable network interception to fail request: "*/tx-submit/submit" with error 400 + Given I enable network interception to finish request: "*/tx-submit/submit" with error 400 When I navigate to Staking extended page Then I see currently staking stake pool in extended mode and choose new pool as "OtherStakePool" When I input "OtherStakePool" to the search bar diff --git a/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsPopupE2E.feature b/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsPopupE2E.feature index f61f7b43f..553979ca9 100644 --- a/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsPopupE2E.feature +++ b/packages/e2e-tests/src/features/e2e/StakingSwitchingPoolsPopupE2E.feature @@ -13,7 +13,7 @@ Feature: Staking Page - Switching pools - Popup View - E2E And I wait for single search result And I click stake pool with name "OtherStakePool" Then I see drawer with "OtherStakePool" stake pool details and a button available for staking - And I save stake pool info + And I save stake pool details When I click "Stake on this pool" button on stake pool details drawer And I click "Fine by me" button on "Switching pool?" modal Then I see drawer with stakepool: "OtherStakePool" confirmation screen in popup mode @@ -36,7 +36,7 @@ Feature: Staking Page - Switching pools - Popup View - E2E And I wait for single search result And I click stake pool with name "-" Then I see drawer with stake pool details without metadata and a button available for staking - When I save stake pool info + When I save stake pool details And I click "Stake on this pool" button on stake pool details drawer And I click "Fine by me" button on "Switching pool?" modal And I click "Next" button on staking confirmation drawer diff --git a/packages/e2e-tests/src/features/trezor/Trezor.feature b/packages/e2e-tests/src/features/trezor/Trezor.feature index b6d5c2751..66ad86a78 100644 --- a/packages/e2e-tests/src/features/trezor/Trezor.feature +++ b/packages/e2e-tests/src/features/trezor/Trezor.feature @@ -4,11 +4,12 @@ Feature: Trezor Onboarding Scenario: Onboarding Trezor wallet And I connect, unlock and enter correct pin on Trezor emulator Given I click "Connect" button on wallet setup page - And I click "OK" button on "Limited support for DApp" modal - And I am on "Lace terms of use" page and accept terms - And I am on "Help us improve your experience" page + # TODO: remove/replace outdated steps +# And I click "OK" button on "Limited support for DApp" modal +# And I am on "Lace terms of use" page and accept terms +# And I am on "Help us improve your experience" page When I click "Agree" button on Analytics page - And I click Trezor wallet icon +# And I click Trezor wallet icon And I click "Next" button during wallet setup And I select 1 account on Select Account page When I click "Next" button during wallet setup @@ -19,6 +20,6 @@ Feature: Trezor Onboarding And I click "Export" on Trezor Connect page And I confirm exporting public key on Trezor emulator And I switch to window with Lace - Then "All done" page is displayed +# Then "All done" page is displayed When I click "Go to my wallet" button on "All done" page Then I see LW homepage diff --git a/packages/e2e-tests/src/pageobject/newTransactionExtendedPageObject.ts b/packages/e2e-tests/src/pageobject/newTransactionExtendedPageObject.ts index b2fcc189b..8c5799a69 100644 --- a/packages/e2e-tests/src/pageobject/newTransactionExtendedPageObject.ts +++ b/packages/e2e-tests/src/pageobject/newTransactionExtendedPageObject.ts @@ -1,6 +1,4 @@ -import webTester from '../actor/webTester'; import TransactionNewPage from '../elements/newTransaction/transactionNewPage'; -import { TransactionBundle } from '../elements/newTransaction/transactionBundle'; import TokenSelectionPage from '../elements/newTransaction/tokenSelectionPage'; import { Asset } from '../data/Asset'; import extensionUtils from '../utils/utils'; @@ -9,10 +7,6 @@ import { AssetInput } from '../elements/newTransaction/assetInput'; import { AddressInput } from '../elements/AddressInput'; export default new (class NewTransactionExtendedPageObject { - clickRemoveBundleButton = async (outputIndex: number) => { - await webTester.clickElement(new TransactionBundle(outputIndex).bundleRemoveButton()); - }; - async setTwoAssetsForBundle(bundleIndex: number, assetValue1: number, assetValue2: number) { await new AddressInput(bundleIndex).fillAddress(byron.getAddress()); await new AssetInput(bundleIndex).clickAddAssetButton(); diff --git a/packages/e2e-tests/src/pageobject/stakingExtendedPageObject.ts b/packages/e2e-tests/src/pageobject/stakingExtendedPageObject.ts deleted file mode 100644 index 414ad5568..000000000 --- a/packages/e2e-tests/src/pageobject/stakingExtendedPageObject.ts +++ /dev/null @@ -1,60 +0,0 @@ -import webTester from '../actor/webTester'; -import StakingPage from '../elements/staking/stakingPage'; -import { StakePoolListItem } from '../elements/staking/StakePoolListItem'; -import testContext from '../utils/testContext'; -import StakePoolDetails from '../elements/staking/stakePoolDetails'; - -class StakingExtendedPageObject { - async clickStakePoolListHeader(listHeader: string) { - await StakingPage.stakingPoolListColumnHeader(listHeader).scrollIntoView(); - await StakingPage.stakingPoolListColumnHeader(listHeader).click(); - } - - async revealAllStakePools(): Promise { - const stakePoolListItem = new StakePoolListItem(); - await webTester.waitUntilSeeElement(stakePoolListItem.container(), 6000); - - const expectedTotalRows = Number((await StakingPage.counter.getText()).replace(/\D/g, '')); - let displayedRows = (await stakePoolListItem.getRows()).length; - - while (displayedRows < expectedTotalRows) { - await $(new StakePoolListItem(displayedRows).toJSLocator()).scrollIntoView(); - displayedRows = (await stakePoolListItem.getRows()).length; - } - } - - async extractColumnContent(columnName: string): Promise { - const rowsNumber = (await new StakePoolListItem().getRows()).length; - const columnContent: string[] = []; - for (let i = 1; i <= rowsNumber; i++) { - const listItem = new StakePoolListItem(i); - switch (columnName) { - case 'name': - columnContent.push((await listItem.getName()) as string); - break; - case 'ros': - columnContent.push((await listItem.getRos()) as string); - break; - case 'cost': - columnContent.push((await listItem.getCost()) as string); - break; - case 'saturation': - columnContent.push((await listItem.getSaturation()) as string); - break; - } - } - - return columnContent; - } - - saveStakePoolInfo = async () => { - const poolName = (await StakePoolDetails.poolName.getText()) as string; - testContext.save('poolName', poolName); - const poolTicker = (await StakePoolDetails.poolTicker.getText()) as string; - testContext.save('poolTicker', poolTicker); - const poolID = (await StakePoolDetails.poolId.getText()) as string; - testContext.save('poolID', poolID); - }; -} - -export default new StakingExtendedPageObject(); diff --git a/packages/e2e-tests/src/steps/AddressBookSteps.ts b/packages/e2e-tests/src/steps/AddressBookSteps.ts index 9a8985edb..aecd75ada 100644 --- a/packages/e2e-tests/src/steps/AddressBookSteps.ts +++ b/packages/e2e-tests/src/steps/AddressBookSteps.ts @@ -366,25 +366,6 @@ When( } ); -Then( - /^I see a toast with text: "(Edited successfully|Address added)"$/, - async (action: 'Edited successfully' | 'Address added') => { - let translationKey; - switch (action) { - case 'Edited successfully': - translationKey = 'browserView.addressBook.toast.editAddress'; - break; - case 'Address added': - translationKey = addressAddedToastTranslationKey; - break; - default: - throw new Error(`Unsupported action name: ${action}`); - } - - await ToastMessageAssert.assertSeeToastMessage(await t(translationKey), true); - } -); - Then( /^I add address with name: "([^"]*)" and address: "([^"]*)" to address book in (extended|popup) mode$/, async (name: string, address: string, mode: 'extended' | 'popup') => { diff --git a/packages/e2e-tests/src/steps/commonSteps.ts b/packages/e2e-tests/src/steps/commonSteps.ts index 09fdb002a..993f50c14 100755 --- a/packages/e2e-tests/src/steps/commonSteps.ts +++ b/packages/e2e-tests/src/steps/commonSteps.ts @@ -12,7 +12,11 @@ import localStorageManager from '../utils/localStorageManager'; import networkManager from '../utils/networkManager'; import { Logger } from '../support/logger'; import clipboard from 'clipboardy'; -import { cleanBrowserStorage } from '../utils/browserStorage'; +import { + shiftBackFiatPriceFetchedTimeInBrowserStorage, + cleanBrowserStorage, + deleteFiatPriceTimestampFromBackgroundStorage +} from '../utils/browserStorage'; import BackgroundStorageAssert from '../assert/backgroundStorageAssert'; import topNavigationAssert from '../assert/topNavigationAssert'; import testContext from '../utils/testContext'; @@ -26,6 +30,7 @@ import { } from '../utils/window'; import { Given } from '@wdio/cucumber-framework'; import tokensPageObject from '../pageobject/tokensPageObject'; +import ToastMessage from '../elements/toastMessage'; import menuMainAssert from '../assert/menuMainAssert'; import LocalStorageAssert from '../assert/localStorageAssert'; import ToastMessageAssert from '../assert/toastMessageAssert'; @@ -107,6 +112,10 @@ When( } ); +When(/^I close a toast message$/, async () => { + await ToastMessage.clickCloseButton(); +}); + // TODO: deprecated step, to be removed when remaining usages are replaced inside StakingPageDelegatedFundsExtended.feature Then(/(An|No) "([^"]*)" text is displayed/, async (expectedResult: string, expectedText: string) => { await $(`//*[contains(text(), "${(await t(expectedText)) ?? expectedText}")]`).waitForDisplayed({ @@ -125,10 +134,36 @@ Then( } ); -Then(/^I (see|don't see) a toast with message: "([^"]*)"$/, async (shouldSee: string, toastText: string) => { +Then(/^I (see|don't see) a toast with text: "([^"]*)"$/, async (shouldSee: string, toastText: string) => { await settingsExtendedPageObject.closeWalletSyncedToast(); - await ToastMessageAssert.assertSeeToastMessage(await t(toastText), shouldSee === 'see'); - if (toastText === 'general.clipboard.copiedToClipboard') Logger.log(`Clipboard contain: ${await clipboard.read()}`); + + const toastTextToTranslationKeyMap: { [key: string]: string } = { + 'Handle copied': 'core.infoWallet.handleCopied', + 'Address copied': 'core.infoWallet.addressCopied', + 'NFTs added to folder': 'browserView.nfts.folderDrawer.toast.update', + 'NFT removed': 'browserView.nfts.folderDrawer.toast.delete', + 'Folder created successfully': 'browserView.nfts.folderDrawer.toast.create', + 'Folder deleted successfully': 'browserView.nfts.deleteFolderSuccess', + 'Folder renamed successfully': 'browserView.nfts.renameFolderSuccess', + 'Edited successfully': 'browserView.addressBook.toast.editAddress', + 'Address added': 'browserView.addressBook.toast.addAddress', + 'Given address already exists': 'addressBook.errors.givenAddressAlreadyExist', + 'Given name already exists': 'addressBook.errors.givenNameAlreadyExist', + 'Switched network': 'browserView.settings.wallet.network.networkSwitched', + 'Network Error': 'general.errors.networkError', + 'Copied to clipboard': 'general.clipboard.copiedToClipboard' + }; + + const translationKey = toastTextToTranslationKeyMap[toastText]; + if (!translationKey) { + throw new Error(`Unsupported toast text: ${toastText}`); + } + + await ToastMessageAssert.assertSeeToastMessage(await t(translationKey), shouldSee === 'see'); + + if (translationKey === 'general.clipboard.copiedToClipboard') { + Logger.log(`Clipboard contain: ${await clipboard.read()}`); + } }); Then(/^I don't see any toast message$/, async () => { @@ -170,13 +205,6 @@ When(/^I am in the offline network mode: (true|false)$/, async (offline: 'true' await networkManager.changeNetworkCapabilitiesOfBrowser(offline === 'true'); }); -When( - /^I enable network interception to fail request: "([^"]*)" with error (\d*)$/, - async (urlPattern: string, errorCode: number) => { - await networkManager.failResponse(urlPattern, errorCode); - } -); - When(/^I click outside the drawer$/, async () => { await new CommonDrawerElements().areaOutsideDrawer.click(); }); @@ -333,3 +361,26 @@ When(/^I scroll (down|up) (\d*) pixels$/, async (direction: 'down' | 'up', pixel Given(/^I confirm multi-address discovery modal$/, async () => { await settingsExtendedPageObject.multiAddressModalConfirm(); }); + +When(/^I enable network interception to fail request: "([^"]*)"$/, async (urlPattern: string) => { + await networkManager.failRequest(urlPattern); +}); + +When( + /^I enable network interception to finish request: "([^"]*)" with error (\d*)$/, + async (urlPattern: string, errorCode: number) => { + await networkManager.finishWithResponseCode(urlPattern, errorCode); + } +); + +Given(/^I shift back last fiat price fetch time in local storage by (\d+) seconds$/, async (seconds: number) => { + await shiftBackFiatPriceFetchedTimeInBrowserStorage(seconds); +}); + +Then(/^I disable network interception$/, async () => { + await networkManager.closeOpenedCdpSessions(); +}); + +Given(/^I delete fiat price timestamp from background storage$/, async () => { + await deleteFiatPriceTimestampFromBackgroundStorage(); +}); diff --git a/packages/e2e-tests/src/steps/multidelegationSteps.ts b/packages/e2e-tests/src/steps/multidelegationSteps.ts index 8bbe8713c..8fa11fa1f 100644 --- a/packages/e2e-tests/src/steps/multidelegationSteps.ts +++ b/packages/e2e-tests/src/steps/multidelegationSteps.ts @@ -27,9 +27,10 @@ import StartStakingPage from '../elements/multidelegation/StartStakingPage'; import PortfolioBar from '../elements/multidelegation/PortfolioBar'; import PortfolioBarAssert from '../assert/multidelegation/PortfolioBarAssert'; import ChangingStakingPreferencesModalAssert from '../assert/multidelegation/ChangingStakingPreferencesModalAssert'; -import { StakePoolListColumnType, StakePoolSortingOptionType } from '../types/staking'; +import { StakePoolListColumnName, StakePoolSortingOptionType } from '../types/staking'; import SwitchingStakePoolModal from '../elements/staking/SwitchingStakePoolModal'; import MoreOptionsComponentAssert from '../assert/multidelegation/MoreOptionsComponentAssert'; +import { mapColumnNameStringToEnum } from '../utils/stakePoolListContent'; const validPassword = 'N_8J@bne87A'; @@ -235,15 +236,15 @@ Then(/^\(if applicable\) first stake pool search result has "([^"]*)" ticker$/, When( /^I hover over "(Ticker|Saturation|ROS|Cost|Margin|Blocks|Pledge|Live Stake)" column name in stake pool list$/, - async (columnName: StakePoolListColumnType) => { - await MultidelegationPage.hoverOverColumnWithName(columnName); + async (columnName: StakePoolListColumnName) => { + await MultidelegationPage.hoverOverColumn(mapColumnNameStringToEnum(columnName)); } ); Then( /^tooltip for "(Ticker|Saturation|ROS|Cost|Margin|Blocks|Pledge|Live Stake)" column is displayed$/, - async (columnName: StakePoolListColumnType) => { - await MultidelegationPageAssert.assertSeeTooltipForColumn(columnName); + async (columnName: StakePoolListColumnName) => { + await MultidelegationPageAssert.assertSeeTooltipForColumn(mapColumnNameStringToEnum(columnName)); } ); @@ -493,8 +494,8 @@ Then(/^I see (\d+) stake pool cards in a row$/, async (cardsCount: number) => { When( /^I click on stake pools table "(Ticker|Saturation|ROS|Cost|Margin|Blocks|Pledge|Live Stake)" column header$/, - async (headerName: StakePoolListColumnType) => { - await MultidelegationPage.clickOnColumnWithName(headerName); + async (headerName: StakePoolListColumnName) => { + await MultidelegationPage.clickOnColumn(mapColumnNameStringToEnum(headerName)); } ); @@ -504,9 +505,29 @@ When( await MultidelegationPage.moreOptionsComponent.selectSortingOption(sortingOption); } ); + Then( /^"More options" component with stake pool (sorting|filtering) options is displayed$/, async (tab: 'sorting' | 'filtering') => { await MoreOptionsComponentAssert.assertSeeMoreOptionsComponent(tab); } ); + +Then( + /^(ascending|descending) sorting indicator is displayed for "(Ticker|Saturation|ROS|Cost|Margin|Blocks|Pledge|Live Stake)" column$/, + async (order: 'ascending' | 'descending', sortingOption: StakePoolListColumnName) => { + await MultidelegationPageAssert.assertSeeColumnSortingIndicator(sortingOption, order); + } +); + +Then( + /^stake pool (list rows|cards) are sorted by "(Ticker|Saturation|ROS|Cost|Margin|Blocks|Pledge|Live Stake)" in (ascending|descending) order$/, + async ( + stakePoolsDisplayType: 'list rows' | 'cards', + sortingOption: StakePoolListColumnName, + order: 'ascending' | 'descending' + ) => { + const poolLimit = 100; // Limit verification to 100 stake pools due to time constraints + await MultidelegationPageAssert.assertSeeStakePoolsSorted(stakePoolsDisplayType, sortingOption, order, poolLimit); + } +); diff --git a/packages/e2e-tests/src/steps/nftFoldersSteps.ts b/packages/e2e-tests/src/steps/nftFoldersSteps.ts index 7ea097384..bf505b967 100644 --- a/packages/e2e-tests/src/steps/nftFoldersSteps.ts +++ b/packages/e2e-tests/src/steps/nftFoldersSteps.ts @@ -5,8 +5,6 @@ import NftCreateFolderPage from '../elements/NFTs/nftCreateFolderPage'; import YoullHaveToStartAgainModal from '../elements/NFTs/youllHaveToStartAgainModal'; import mainMenuPageObject from '../pageobject/mainMenuPageObject'; import NftSelectNftsPage from '../elements/NFTs/nftSelectNftsPage'; -import ToastMessageAssert from '../assert/toastMessageAssert'; -import { t } from '../utils/translationService'; import NftSelectNftsAssert from '../assert/nftSelectNftsAssert'; import IndexedDB from '../fixture/indexedDB'; import { NFTFolder } from '../data/NFTFolder'; @@ -237,36 +235,6 @@ Then( } ); -Then( - /^I see a toast with text: "Folder (created|renamed|deleted) successfully"$/, - async (action: 'created' | 'deleted' | 'renamed') => { - let translationKey; - switch (action) { - case 'created': - translationKey = 'browserView.nfts.folderDrawer.toast.create'; - break; - case 'deleted': - translationKey = 'browserView.nfts.deleteFolderSuccess'; - break; - case 'renamed': - translationKey = 'browserView.nfts.renameFolderSuccess'; - break; - default: - throw new Error(`Unsupported action name: ${action}`); - } - - await ToastMessageAssert.assertSeeToastMessage(await t(translationKey), true); - } -); - -Then(/^I see a toast with text: "NFTs added to folder"$/, async () => { - await ToastMessageAssert.assertSeeToastMessage(await t('browserView.nfts.folderDrawer.toast.update'), true); -}); - -Then(/^I see a toast with text: "NFT removed"$/, async () => { - await ToastMessageAssert.assertSeeToastMessage(await t('browserView.nfts.folderDrawer.toast.delete'), true); -}); - Then(/^I select (\d+) NFTs$/, async (numberOfNFTs: number) => { await NftSelectNftsPage.selectNFTs(numberOfNFTs); }); diff --git a/packages/e2e-tests/src/steps/onboardingSteps.ts b/packages/e2e-tests/src/steps/onboardingSteps.ts index 628672b68..6c7ebfd82 100644 --- a/packages/e2e-tests/src/steps/onboardingSteps.ts +++ b/packages/e2e-tests/src/steps/onboardingSteps.ts @@ -8,7 +8,6 @@ import Modal from '../elements/modal'; import ModalAssert from '../assert/modalAssert'; import OnboardingAnalyticsPage from '../elements/onboarding/analyticsPage'; import OnboardingCommonAssert from '../assert/onboarding/onboardingCommonAssert'; -import OnboardingConnectHWPageAssert from '../assert/onboarding/onboardingConnectHWPageAssert'; import OnboardingMainPage from '../elements/onboarding/mainPage'; import OnboardingMainPageAssert from '../assert/onboarding/onboardingMainPageAssert'; import OnboardingWalletSetupPage from '../elements/onboarding/walletSetupPage'; @@ -17,7 +16,6 @@ import TokensPageAssert from '../assert/tokensPageAssert'; import TopNavigationAssert from '../assert/topNavigationAssert'; import testContext from '../utils/testContext'; import CommonAssert from '../assert/commonAssert'; -import OnboardingConnectHardwareWalletPage from '../elements/onboarding/connectHardwareWalletPage'; import SelectAccountPage from '../elements/onboarding/selectAccountPage'; import { browser } from '@wdio/globals'; import type { RecoveryPhrase } from '../types/onboarding'; @@ -31,6 +29,7 @@ import { getWalletsFromRepository } from '../fixture/walletRepositoryInitializer import OnboardingWalletSetupPageAssert from '../assert/onboarding/onboardingWalletSetupPageAssert'; import OnboardingAnalyticsBannerAssert from '../assert/onboarding/onboardingAnalyticsBannerAssert'; import { shuffle } from '../utils/arrayUtils'; +import ConnectYourDevicePageAssert from '../assert/onboarding/ConnectYourDevicePageAssert'; const mnemonicWords: string[] = getTestWallet(TestWalletName.TestAutomationWallet).mnemonic ?? []; const invalidMnemonicWords: string[] = getTestWallet(TestWalletName.InvalidMnemonic).mnemonic ?? []; @@ -160,12 +159,19 @@ Then(/^I select (12|15|24) word passphrase length$/, async (length: RecoveryPhra await RecoveryPhrasePage.selectMnemonicLength(length); }); -Then(/^"Connect Hardware Wallet" page is displayed$/, async () => { - await OnboardingConnectHWPageAssert.assertSeeConnectHardwareWalletPage(); +Then(/^"Connect your device" page is displayed$/, async () => { + await ConnectYourDevicePageAssert.assertSeeConnectYourDevicePage(); }); -Then(/^I click Trezor wallet icon$/, async () => { - await OnboardingConnectHardwareWalletPage.trezorButton.click(); +Then(/^"No hardware wallet device was chosen." error is displayed on "Connect your device" page$/, async () => { + await ConnectYourDevicePageAssert.assertSeeError( + await t('core.walletSetupConnectHardwareWalletStepRevamp.errorMessage.devicePickerRejected') + ); +}); + +When(/^"Try again" button is enabled on "Connect your device" page$/, async () => { + await ConnectYourDevicePageAssert.assertSeeTryAgainButton(true); + await ConnectYourDevicePageAssert.assertSeeTryAgainButtonEnabled(true); }); Then(/^"Restoring a multi-address wallet\?" modal is displayed$/, async () => { diff --git a/packages/e2e-tests/src/steps/sendTransactionBundleSteps.ts b/packages/e2e-tests/src/steps/sendTransactionBundleSteps.ts index bbce36c69..945fe724f 100644 --- a/packages/e2e-tests/src/steps/sendTransactionBundleSteps.ts +++ b/packages/e2e-tests/src/steps/sendTransactionBundleSteps.ts @@ -11,13 +11,14 @@ import TransactionNewPage from '../elements/newTransaction/transactionNewPage'; import { AssetInput } from '../elements/newTransaction/assetInput'; import { AddressInput } from '../elements/AddressInput'; import TransactionSubmittedPage from '../elements/newTransaction/transactionSubmittedPage'; +import { TransactionBundle } from '../elements/newTransaction/transactionBundle'; Then(/^I see (\d) bundle rows$/, async (expectedNumberOfBundles: number) => { await transactionBundlesAssert.assertSeeBundles(expectedNumberOfBundles); }); When(/^I remove bundle (\d)$/, async (index: number) => { - await transactionExtendedPageObject.clickRemoveBundleButton(index); + await new TransactionBundle(index).clickRemoveBundleButton(); }); When(/^I set multiple outputs for advanced transaction$/, async () => { diff --git a/packages/e2e-tests/src/steps/sendTransactionSimpleSteps.ts b/packages/e2e-tests/src/steps/sendTransactionSimpleSteps.ts index 15a03e6da..92b73d90d 100644 --- a/packages/e2e-tests/src/steps/sendTransactionSimpleSteps.ts +++ b/packages/e2e-tests/src/steps/sendTransactionSimpleSteps.ts @@ -38,6 +38,7 @@ import { AddressInput } from '../elements/AddressInput'; import { AssetInput } from '../elements/newTransaction/assetInput'; import TokenSelectionPage from '../elements/newTransaction/tokenSelectionPage'; import TransactionPasswordPage from '../elements/newTransaction/transactionPasswordPage'; +import { Key } from 'webdriverio'; Given(/I have several contacts whose start with the same characters/, async () => { await indexedDB.clearAddressBook(); @@ -269,12 +270,15 @@ Then( async (valueToEnter: string, assetName: string, bundleIndex: number) => { assetName = assetName === 'tADA' && extensionUtils.isMainnet() ? 'ADA' : assetName; await TransactionNewPage.coinConfigure(bundleIndex, assetName).fillTokenValue(Number.parseFloat(valueToEnter)); - // workaround for test automation only to fire all events after finished typing - await TransactionNewPage.clickDrawerBackground(); - await TransactionNewPage.coinConfigure(bundleIndex, assetName).balanceFiatValueElement.click(); } ); +Then(/^I open cancel modal to trigger button validation$/, async () => { + // workaround for test automation only to fire all events after finished typing + await browser.keys(Key.Escape); + await Modal.cancelButton.click(); +}); + Then(/^I click on transaction drawer background to lose focus$/, async () => { await TransactionNewPage.clickDrawerBackground(); }); @@ -389,7 +393,11 @@ Then( transactionDescription: `${await t(type)}\n(1)`, hash: testContext.load('txHashValue'), transactionData: [ - { ada: `${adaValue} ${Asset.CARDANO.ticker}`, address: String(getTestWallet(walletName).address) } + { + ada: `${adaValue} ${Asset.CARDANO.ticker}`, + address: String(getTestWallet(walletName).address), + addressTag: 'foreign' + } ], status: 'Success' }; @@ -534,7 +542,7 @@ Then(/^Metadata input is empty$/, async () => { }); Then(/^"Incorrect address" error (is|is not) displayed under address input field$/, async (state: 'is' | 'is not') => { - await drawerSendExtendedAssert.assertSeeIncorrectAddressError(state === 'is'); + await drawerSendExtendedAssert.assertSeeIncorrectAddressError(1, state === 'is'); }); Then(/^"Review transaction" button is (enabled|disabled) on "Send" page$/, async (state: 'enabled' | 'disabled') => { diff --git a/packages/e2e-tests/src/steps/stakingSteps.ts b/packages/e2e-tests/src/steps/stakingSteps.ts index f89e4f049..ba2382abc 100644 --- a/packages/e2e-tests/src/steps/stakingSteps.ts +++ b/packages/e2e-tests/src/steps/stakingSteps.ts @@ -1,13 +1,10 @@ import { Then, When } from '@cucumber/cucumber'; import stakingPageAssert from '../assert/stakingPageAssert'; import stakePoolDetailsAssert from '../assert/stakePoolDetailsAssert'; -import stakingExtendedPageObject from '../pageobject/stakingExtendedPageObject'; import drawerCommonExtendedAssert from '../assert/drawerCommonExtendedAssert'; import { getStakePoolById, getStakePoolByName, StakePoolsData } from '../data/expectedStakePoolsData'; import testContext from '../utils/testContext'; import transactionDetailsAssert, { ExpectedActivityDetails } from '../assert/transactionDetailsAssert'; -import { StakePoolListItem } from '../elements/staking/StakePoolListItem'; -import webTester from '../actor/webTester'; import StakingExitModalAssert from '../assert/stakingExitModalAssert'; import extensionUtils from '../utils/utils'; import stakingConfirmationScreenAssert from '../assert/stakingConfirmationScreenAssert'; @@ -125,12 +122,6 @@ Then(/^the stakepool drawer is opened with "([^"]*)" stake pool information$/, a await drawerCommonExtendedAssert.assertSeeDrawerWithTitle(poolName); }); -When(/^I click on the "(.*)" column header$/, async (listHeader: string) => { - const stakePoolListItem = new StakePoolListItem(); - await webTester.waitUntilSeeElement(stakePoolListItem.container(), 60_000); - await stakingExtendedPageObject.clickStakePoolListHeader(listHeader); -}); - // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars Then(/^The Tx details are displayed for Staking (with|without) metadata$/, async (_ignored: 'with' | 'without') => { // no need to distinguish between pools with/without metadata @@ -144,22 +135,6 @@ Then(/^The Tx details are displayed for Staking (with|without) metadata$/, async await transactionDetailsAssert.assertSeeActivityDetails(expectedActivityDetails); }); -Then( - /^the results are in (ascending|descending) order according to "([^"]*)" column$/, - async (order: 'ascending' | 'descending', column: string) => { - await stakingPageAssert.assertStakePoolItemsOrder(column, order); - } -); - -When(/^I reveal all stake pools$/, async () => { - await webTester.waitUntilSeeElement(new StakePoolListItem().container(), 60_000); - await stakingExtendedPageObject.revealAllStakePools(); -}); - -When(/^I save stake pool info$/, async () => { - await stakingExtendedPageObject.saveStakePoolInfo(); -}); - Then(/^Staking password screen is displayed$/, async () => { await stakingPageAssert.assertSeeStakingPasswordDrawer(); }); diff --git a/packages/e2e-tests/src/steps/tokensPageSteps.ts b/packages/e2e-tests/src/steps/tokensPageSteps.ts index a6ec0b68d..59e48bb0a 100644 --- a/packages/e2e-tests/src/steps/tokensPageSteps.ts +++ b/packages/e2e-tests/src/steps/tokensPageSteps.ts @@ -1,4 +1,4 @@ -import { When, Then } from '@cucumber/cucumber'; +import { Then, When } from '@cucumber/cucumber'; import tokensPageAssert from '../assert/tokensPageAssert'; import tokensPageObject from '../pageobject/tokensPageObject'; import tokenDetailsAssert from '../assert/tokenDetailsAssert'; @@ -9,6 +9,7 @@ import { switchToLastWindow } from '../utils/window'; import extensionUtils from '../utils/utils'; import TokensPage from '../elements/tokensPage'; import type { NetworkType } from '../types/network'; +import { Given } from '@wdio/cucumber-framework'; When(/^I see Tokens counter with total number of tokens displayed$/, async () => { await tokensPageAssert.assertSeeTitleWithCounter(); @@ -34,8 +35,8 @@ Then( /^I see Cardano & LaceCoin tokens on the list with all the details in (extended|popup) mode$/, async (mode: 'extended' | 'popup') => { await tokensPageAssert.assertSeeTableItems(mode); - await tokensPageAssert.assertSeeCardanoItem(mode); - await tokensPageAssert.assertSeeLaceCoinItem(mode); + await tokensPageAssert.assertSeeNativeToken(Asset.CARDANO, mode); + await tokensPageAssert.assertSeeNotNativeToken(Asset.LACE_COIN, mode); } ); @@ -43,8 +44,8 @@ Then( /^I see Cardano & Hosky tokens on the list with all the details in (extended|popup) mode$/, async (mode: 'extended' | 'popup') => { await tokensPageAssert.assertSeeTableItems(mode); - await tokensPageAssert.assertSeeCardanoItem(mode); - await tokensPageAssert.assertSeeHoskyItem(mode); + await tokensPageAssert.assertSeeNativeToken(Asset.CARDANO, mode); + await tokensPageAssert.assertSeeNativeToken(Asset.HOSKY_TOKEN, mode); } ); @@ -217,3 +218,16 @@ Then(/^I see total wallet balance in ADA is "([^"]*)"$/, async (balanceInAda: nu Then(/^I see tMin token with the ADA balance of "([^"]*)"$/, async (balanceInAda: number) => { await tokensPageAssert.assertTMinBalance(balanceInAda); }); + +Then( + /^"(Price data expired|Unable to fetch fiat values)" error (is|is not) displayed$/, + async (errorType: 'Price data expired' | 'Unable to fetch fiat values', shouldBeDisplayed: 'is' | 'is not') => { + errorType === 'Price data expired' + ? await tokensPageAssert.seePriceFetchExpiredErrorMessage(shouldBeDisplayed === 'is') + : await tokensPageAssert.seePriceFetchFailedErrorMessage(shouldBeDisplayed === 'is'); + } +); + +Given(/^ADA fiat price has been fetched$/, async () => { + await TokensPage.waitForPricesToBeFetched(); +}); diff --git a/packages/e2e-tests/src/steps/waletAddressPageSteps.ts b/packages/e2e-tests/src/steps/waletAddressPageSteps.ts index 252bd22f2..91a01f613 100644 --- a/packages/e2e-tests/src/steps/waletAddressPageSteps.ts +++ b/packages/e2e-tests/src/steps/waletAddressPageSteps.ts @@ -2,9 +2,6 @@ import { Then, When } from '@wdio/cucumber-framework'; import { getTestWallet } from '../support/walletConfiguration'; import walletAddressPageAssert from '../assert/walletAddressPageAssert'; import walletAddressPage from '../elements/walletAddressPage'; -import ToastMessageAssert from '../assert/toastMessageAssert'; -import { t } from '../utils/translationService'; -import ToastMessage from '../elements/toastMessage'; import MenuHeader from '../elements/menuHeader'; When(/^I see handles listed on the "Receive" screen$/, async () => { @@ -48,24 +45,6 @@ Then(/^The first ADA handle displayed on the list is the shortest$/, async () => await walletAddressPageAssert.assertSeeTheShortestHandleFirst(); }); -Then(/^I see a toast with text: "(Handle|Address) copied"$/, async (action: string) => { - let translationKey; - switch (action) { - case 'Handle': - translationKey = 'core.infoWallet.handleCopied'; - break; - case 'Address': - translationKey = 'core.infoWallet.addressCopied'; - break; - default: - throw new Error(`Unsupported action name: ${action}`); - } - - await ToastMessageAssert.assertSeeToastMessage(await t(translationKey), true); - await ToastMessage.clickCloseButton(); - await ToastMessageAssert.assertSeeToastMessage(await t(translationKey), false); -}); - Then(/^I see ADA handle with custom image on the "Wallet Address" page$/, async () => { await walletAddressPageAssert.assertSeeAdaHandleCardWithCustomImage(); }); diff --git a/packages/e2e-tests/src/support/patterns.ts b/packages/e2e-tests/src/support/patterns.ts index 37797397c..b4dc198a6 100644 --- a/packages/e2e-tests/src/support/patterns.ts +++ b/packages/e2e-tests/src/support/patterns.ts @@ -16,6 +16,9 @@ const STAKE_POOL_LIST_COST_REGEX = /(\d*\.)?\d+\s?%(\s\+\s\d*ADA)?/; const TIMESTAMP_REGEX = /\d{2}:\d{2}:\d{2}/; const PLEDGE_REGEX = /^\d{1,3}(\.\d{0,2})?[BKM]?$/; const BLOCKS_REGEX = /^(-|\d*)$/; +const TOKEN_VALUE_FIAT_REGEX = /^([\d+,.])+\.\d+\s\D{2,3}$/; +const TOKEN_VALUE_ADA_REGEX = /^\d+\.\d+$/; +const TOKEN_PRICE_CHANGE = /^([+-])\d+\.\d{2}$/; export const TestnetPatterns = { TESTNET_ADDR_REGEX, @@ -35,5 +38,8 @@ export const TestnetPatterns = { STAKE_POOL_LIST_COST_REGEX, TIMESTAMP_REGEX, PLEDGE_REGEX, - BLOCKS_REGEX + BLOCKS_REGEX, + TOKEN_VALUE_FIAT_REGEX, + TOKEN_VALUE_ADA_REGEX, + TOKEN_PRICE_CHANGE }; diff --git a/packages/e2e-tests/src/types/sortingOrder.ts b/packages/e2e-tests/src/types/sortingOrder.ts new file mode 100644 index 000000000..22b91c4cd --- /dev/null +++ b/packages/e2e-tests/src/types/sortingOrder.ts @@ -0,0 +1 @@ +export type SortingOrder = 'ascending' | 'descending'; diff --git a/packages/e2e-tests/src/types/staking.ts b/packages/e2e-tests/src/types/staking.ts index be72dc088..d8edd320b 100644 --- a/packages/e2e-tests/src/types/staking.ts +++ b/packages/e2e-tests/src/types/staking.ts @@ -1,4 +1,4 @@ -export type StakePoolListColumnType = +export type StakePoolListColumnName = | 'Ticker' | 'Saturation' | 'ROS' diff --git a/packages/e2e-tests/src/utils/browserStorage.ts b/packages/e2e-tests/src/utils/browserStorage.ts index 78e01cced..429ce781b 100644 --- a/packages/e2e-tests/src/utils/browserStorage.ts +++ b/packages/e2e-tests/src/utils/browserStorage.ts @@ -71,3 +71,29 @@ export const clearBackgroundStorageKey: any = async (): Promise => { Logger.warn(`Clearing background storage key failed: ${error}`); } }; + +export const shiftBackFiatPriceFetchedTimeInBrowserStorage = async (seconds: number): Promise => { + const backgroundStorage = await getBackgroundStorage(); + backgroundStorage.fiatPrices.timestamp -= seconds * 1000; + try { + await browser.execute( + `await chrome.storage.local.set({ BACKGROUND_STORAGE: ${JSON.stringify(backgroundStorage)}})`, + [] + ); + } catch (error) { + throw new Error(`Setting browser storage failed: ${error}`); + } +}; + +export const deleteFiatPriceTimestampFromBackgroundStorage = async (): Promise => { + const backgroundStorage = await getBackgroundStorage(); + delete backgroundStorage.fiatPrices.timestamp; + try { + await browser.execute( + `await chrome.storage.local.set({ BACKGROUND_STORAGE: ${JSON.stringify(backgroundStorage)}})`, + [] + ); + } catch (error) { + throw new Error(`Setting browser storage failed: ${error}`); + } +}; diff --git a/packages/e2e-tests/src/utils/networkManager.ts b/packages/e2e-tests/src/utils/networkManager.ts index 18c96a2f3..42d2869be 100644 --- a/packages/e2e-tests/src/utils/networkManager.ts +++ b/packages/e2e-tests/src/utils/networkManager.ts @@ -62,14 +62,12 @@ export class NetworkManager { await browser.pause(2000); }; - failResponse = async (urlPattern: string, responseCode: number): Promise => { + finishWithResponseCode = async (urlPattern: string, responseCode: number): Promise => { await browser.call(async () => { const puppeteer = await browser.getPuppeteer(); const targets = puppeteer .targets() - .filter( - (target) => target.type() === 'page' || target.type() === 'service_worker' || target.type() === 'other' - ); + .filter((target) => ['page', 'service_worker', 'other'].includes(target.type())); targets.map(async (target) => { const client: CDPSession = (await target.createCDPSession()) as unknown as CDPSession; NetworkManager.cdpSessions.push(client); @@ -88,6 +86,29 @@ export class NetworkManager { }); }; + failRequest = async (urlPattern: string): Promise => { + await browser.call(async () => { + const puppeteer = await browser.getPuppeteer(); + const targets = puppeteer + .targets() + .filter((target) => ['page', 'service_worker', 'other'].includes(target.type())); + targets.map(async (target) => { + const client: CDPSession = (await target.createCDPSession()) as unknown as CDPSession; + NetworkManager.cdpSessions.push(client); + await client.send('Fetch.enable', { + patterns: [{ urlPattern }] + }); + client.on('Fetch.requestPaused', async ({ requestId, request }) => { + Logger.log(`found request: ${request.url}, failing request`); + await client.send('Fetch.failRequest', { + requestId, + errorReason: 'Failed' + }); + }); + }); + }); + }; + logFailedRequests = async (): Promise => { await browser.call(async () => { const puppeteer = await browser.getPuppeteer(); diff --git a/packages/e2e-tests/src/utils/stakePoolListContent.ts b/packages/e2e-tests/src/utils/stakePoolListContent.ts index 4151e756c..24738e188 100644 --- a/packages/e2e-tests/src/utils/stakePoolListContent.ts +++ b/packages/e2e-tests/src/utils/stakePoolListContent.ts @@ -1,104 +1,138 @@ -import { Asset } from '../data/Asset'; +import type { SortingOrder } from '../types/sortingOrder'; +import type { StakePoolListColumnName } from '../types/staking'; +import { StakePoolListColumn } from '../enums/StakePoolListColumn'; -interface Cost { - percentage: number; - ada: number; +interface AbbreviatedValue { + value: number; + suffix: '-' | 'K' | 'M'; } +const suffixOrderPriority = { + '-': 0, + K: 1, + M: 2 +}; + const emojiRegex = - // eslint-disable-next-line max-len /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g; -const parseCostStringToObject = (cost: string): Cost => { - let parsedItem: Cost = { percentage: 0, ada: 0 }; - if (cost.includes('%') && cost.includes('ADA')) { - const splitItem = cost.split('+'); - parsedItem = { - percentage: Number.parseFloat(splitItem[0].replace(/%/, '')), - ada: Number.parseFloat(splitItem[1].replace(/ADA/, '')) - }; - } +const sortTickerColumnContent = (columnContent: string[], order: SortingOrder): string[] => { + const itemsWithNoTicker = columnContent.filter((item) => item === '-'); + const itemsWithTicker = columnContent.filter((item) => item !== '-'); - if (cost.includes('%') && !cost.includes(Asset.CARDANO.ticker)) { - parsedItem = { - percentage: Number.parseFloat(cost.replace(/%/, '')), - ada: 0 - }; - } + const sortedItems = [...itemsWithTicker].sort((a, b) => { + const nameA = a.replace(emojiRegex, '').replace(' ', '').trim(); + const nameB = b.replace(emojiRegex, '').replace(' ', '').trim(); + return nameA.localeCompare(nameB); + }); - if (!cost.includes('%') && cost.includes(Asset.CARDANO.ticker)) { - parsedItem = { - percentage: 0, - ada: Number.parseFloat(cost.replace(/ADA/, '')) - }; + if (order === 'descending') { + sortedItems.reverse(); } - return parsedItem; + sortedItems.push(...itemsWithNoTicker); + + return sortedItems; }; -const parseCostObjectToString = (cost: Cost): string => { - let parsedItem = `${Number(cost.percentage).toFixed(2)}%`; +const sortBlocksColumnContent = (columnContent: string[], order: SortingOrder): string[] => { + const parsedColumnContent = columnContent.map((item) => Number(item.replace(',', ''))); + const sortedColumnContent = [...parsedColumnContent].sort((a, b) => a - b); - if (cost.ada > 0) { - parsedItem = `${parsedItem} + ${Number(cost.ada)}${Asset.CARDANO.ticker}`; + if (order === 'descending') { + sortedColumnContent.reverse(); } - return parsedItem; + return sortedColumnContent.map((item) => item.toLocaleString()); }; -export const sortNameColumn = (columnContent: string[], order: string): string[] => { - const itemsWithNoName = columnContent.filter((item) => item === '-'); - const itemsWithName = columnContent.filter((item) => item !== '-'); - - const sortedItems = [...itemsWithName].sort((a, b) => { - const nameA = a.replace(emojiRegex, '').replace(' ', '').trim(); - const nameB = b.replace(emojiRegex, '').replace(' ', '').trim(); - return nameA.localeCompare(nameB); - }); - if (order === 'descending') { - sortedItems.reverse(); +const parseValueFromColumnIntoAbbreviatedValueObject = (valueFromColumn: string): AbbreviatedValue => { + if (valueFromColumn.endsWith('K')) { + return { + value: Number(valueFromColumn.slice(0, -1)), + suffix: 'K' + }; + } + if (valueFromColumn.endsWith('M')) { + return { + value: Number(valueFromColumn.slice(0, -1)), + suffix: 'M' + }; } - sortedItems.push(...itemsWithNoName); + return { + value: Number(valueFromColumn), + suffix: '-' + }; +}; - return sortedItems; +const parseAbbreviatedValueObjectIntoString = (abbreviatedValueObject: AbbreviatedValue): string => + `${abbreviatedValueObject.value}${ + ['K', 'M'].includes(abbreviatedValueObject.suffix) ? abbreviatedValueObject.suffix : '' + }`; + +const compareAbbreviatedValues = (abbreviatedValue1: AbbreviatedValue, abbreviatedValue2: AbbreviatedValue): number => { + if (suffixOrderPriority[abbreviatedValue1.suffix] - suffixOrderPriority[abbreviatedValue2.suffix] === 0) { + return abbreviatedValue1.value - abbreviatedValue2.value; + } + return suffixOrderPriority[abbreviatedValue1.suffix] - suffixOrderPriority[abbreviatedValue2.suffix]; }; -export const sortCostColumn = (columnContent: string[], order: string): string[] => { - const parsedColumnContent = columnContent.map((item) => parseCostStringToObject(item)); - const costSorted = [...parsedColumnContent].sort((a, b) => a.ada - b.ada || a.percentage - b.percentage); +const sortColumnWithPercentageValues = (columnContent: string[], order: string): string[] => { + const parsedColumnContent = columnContent.map((item) => Number(item.replace(/%/, '')).toFixed(2)); + const sortedColumnContent = [...parsedColumnContent].sort((a, b) => Number(a) - Number(b)); + if (order === 'descending') { - costSorted.reverse(); + sortedColumnContent.reverse(); } - return costSorted.map((item) => parseCostObjectToString(item)); + + return sortedColumnContent.map((item) => String(`${item}%`)); }; -export const sortColumnWithPercentageValues = (columnContent: string[], order: string): string[] => { - const columnContentWithNumbers = columnContent.map((item) => Number.parseFloat(item.replace(/%/, ''))); - const sortedColumnContentWithNumbers = [...columnContentWithNumbers].sort((a, b) => a - b); +const sortColumnWithAbbreviatedNumbers = (columnContent: string[], order: string): string[] => { + const parsedColumnContent: AbbreviatedValue[] = columnContent.map((item) => + parseValueFromColumnIntoAbbreviatedValueObject(item) + ); + const sortedColumnContent = [...parsedColumnContent].sort((a, b) => compareAbbreviatedValues(a, b)); + if (order === 'descending') { - sortedColumnContentWithNumbers.reverse(); + sortedColumnContent.reverse(); } - return sortedColumnContentWithNumbers.map((item) => String(`${item}%`)); + + return sortedColumnContent.map((item) => parseAbbreviatedValueObjectIntoString(item)); }; export const sortColumnContent = async ( columnContent: string[], - columnName: string, - order: string + sortingOption: StakePoolListColumn, + order: SortingOrder ): Promise => { let sortedColumnContent: string[] = []; - if (columnName === 'name') { - sortedColumnContent = sortNameColumn(columnContent, order); - } - - if (['ros', 'saturation'].includes(columnName)) { - sortedColumnContent = sortColumnWithPercentageValues(columnContent, order); - } - - if (columnName === 'cost') { - sortedColumnContent = sortCostColumn(columnContent, order); + switch (sortingOption) { + case StakePoolListColumn.Ticker: + sortedColumnContent = sortTickerColumnContent(columnContent, order); + break; + case StakePoolListColumn.Saturation: + case StakePoolListColumn.ROS: + case StakePoolListColumn.Margin: + sortedColumnContent = sortColumnWithPercentageValues(columnContent, order); + break; + case StakePoolListColumn.Blocks: + sortedColumnContent = sortBlocksColumnContent(columnContent, order); + break; + case StakePoolListColumn.Cost: + case StakePoolListColumn.Pledge: + case StakePoolListColumn.LiveStake: + sortedColumnContent = sortColumnWithAbbreviatedNumbers(columnContent, order); + break; + default: + throw new Error(`Unsupported sorting option: ${sortingOption}`); } return sortedColumnContent; }; + +export const mapColumnNameStringToEnum = (columnName: StakePoolListColumnName): StakePoolListColumn => + columnName === 'Live Stake' + ? StakePoolListColumn.LiveStake + : StakePoolListColumn[columnName as keyof typeof StakePoolListColumn]; diff --git a/packages/icons/package.json b/packages/icons/package.json index 9275e7bd2..9ba11716d 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -21,6 +21,7 @@ "build:cleanup": "rm -rf ./tmp", "build:svgr": "svgr ./raw --out-dir ./tmp --typescript", "build:tsc": "tsc --project tsconfig.json", + "cleanup": "yarn exec rm -rf dist node_modules", "test": "echo \"@lace/icons: no test specified\"" }, "devDependencies": { diff --git a/packages/staking/package.json b/packages/staking/package.json index d1147173e..22e73457d 100644 --- a/packages/staking/package.json +++ b/packages/staking/package.json @@ -72,15 +72,18 @@ "devDependencies": { "@babel/core": "^7.21.0", "@cardano-sdk/core": "0.30.0", - "@cardano-sdk/input-selection": "0.12.26", - "@cardano-sdk/tx-construction": "0.18.2", + "@cardano-sdk/input-selection": "0.12.27", + "@cardano-sdk/tx-construction": "0.18.3", "@cardano-sdk/util": "0.15.0", + "@cardano-sdk/wallet": "0.37.0", + "@cardano-sdk/web-extension": "0.27.0", "@storybook/addon-actions": "^7.6.7", "@storybook/addon-essentials": "^7.6.7", "@storybook/addon-interactions": "^7.6.7", "@storybook/addon-links": "^7.6.7", "@storybook/blocks": "^7.6.7", "@storybook/jest": "^0.2.3", + "@storybook/preview-api": "^8.0.4", "@storybook/react": "^7.6.7", "@storybook/react-vite": "^7.6.7", "@storybook/test": "^7.6.7", @@ -122,8 +125,8 @@ "@cardano-sdk/input-selection": "0.12.26", "@cardano-sdk/tx-construction": "0.18.2", "@cardano-sdk/util": "0.15.0", - "@cardano-sdk/wallet": "0.35.2", - "@cardano-sdk/web-extension": "0.26.1", + "@cardano-sdk/wallet": "0.36.0", + "@cardano-sdk/web-extension": "0.26.2", "@lace/cardano": "^0.1.0", "@lace/common": "^0.1.0", "@lace/core": "0.1.0", diff --git a/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.stories.tsx b/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.stories.tsx index 05bdfe479..b87bdfd6a 100644 --- a/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.stories.tsx +++ b/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.stories.tsx @@ -1,23 +1,24 @@ import { Box, Cell, Flex, Grid, LocalThemeProvider, Section, ThemeColorScheme, Variants } from '@lace/ui'; import { action } from '@storybook/addon-actions'; -import { StakePoolSortOptions } from 'features/BrowsePools'; +import { useArgs } from '@storybook/preview-api'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { useCallback, useState } from 'react'; -import type { Meta } from '@storybook/react'; +import type { StakePoolSortOptions } from '../types'; +import type { Meta, StoryObj } from '@storybook/react'; import { PoolsFilter, QueryStakePoolsFilters } from '../../store'; +import { DEFAULT_SORT_OPTIONS } from '../constants'; import { BrowsePoolsPreferencesCard } from './BrowsePoolsPreferencesCard'; import { SortAndFilterTab } from './types'; -export default { +const meta: Meta = { + component: BrowsePoolsPreferencesCard, title: 'Cards/Stake Pool Sorting & Filter', -} as Meta; - +}; +export default meta; const Wrapper = ({ defaultTab }: { defaultTab: SortAndFilterTab }) => { const [activeTab, setActiveTab] = useState(defaultTab); - const [sort, setSort] = useState({ - field: 'saturation', - order: 'asc', - }); + const [sort, setSort] = useState(DEFAULT_SORT_OPTIONS); const [filter, setFilter] = useState({ [PoolsFilter.Saturation]: ['', ''], [PoolsFilter.ProfitMargin]: ['', ''], @@ -109,3 +110,57 @@ export const Overview = (): JSX.Element => ( ); + +export const Interactions: StoryObj = { + args: { + activeTab: SortAndFilterTab.sort, + filter: { + [PoolsFilter.Saturation]: ['', ''], + [PoolsFilter.ProfitMargin]: ['', ''], + [PoolsFilter.Performance]: ['', ''], + [PoolsFilter.Ros]: ['lastepoch'], + }, + sort: { + field: 'pledge', + order: 'desc', + }, + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + await waitFor(() => expect(canvas.getByTestId('radio-btn-test-id-ticker'))); + const tickerBtn = canvas.getByTestId('radio-btn-test-id-ticker'); + + await userEvent.click(tickerBtn); + const saturationBtn = canvas.getByTestId('radio-btn-test-id-saturation'); + await step('Change sort field', async () => { + await userEvent.click(saturationBtn); + }); + + const iconDesc = canvas.getByTestId('sort-desc'); + await waitFor(() => expect(saturationBtn).toBeChecked()); + await waitFor(() => expect(iconDesc).toBeInTheDocument()); + await step('Click on iconDesc', async () => { + await userEvent.click(iconDesc); + }); + + await waitFor(() => expect(iconDesc).not.toBeInTheDocument()); + const iconAsc = canvas.getByTestId('sort-asc'); + await waitFor(() => expect(iconAsc).toBeInTheDocument()); + }, + render: function Render(args) { + const [{ sort }, setArgs] = useArgs(); + + return ( + { + setArgs({ + ...args, + sort: newSort, + }); + }} + /> + ); + }, +}; diff --git a/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.tsx b/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.tsx index 80a459a83..db62eca50 100644 --- a/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.tsx +++ b/packages/staking/src/features/BrowsePools/BrowsePoolsPreferencesCard/BrowsePoolsPreferencesCard.tsx @@ -21,6 +21,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { USE_MULTI_DELEGATION_STAKING_FILTERS, USE_ROS_STAKING_COLUMN } from '../../../featureFlags'; import { PoolsFilter, QueryStakePoolsFilters } from '../../store'; +import { getDefaultSortOrderByField } from '../utils'; import * as styles from './BrowsePoolsPreferencesCard.css'; import { BrowsePoolsPreferencesCardLabel } from './BrowsePoolsPreferencesCardLabel'; import { FilterOption, SelectOption, SortAndFilterTab } from './types'; @@ -89,7 +90,7 @@ export const BrowsePoolsPreferencesCard = ({ onSortChange({ field: sortField, - order: 'asc', + order: getDefaultSortOrderByField(sortField), }); }, [analytics, onSortChange] @@ -135,8 +136,18 @@ export const BrowsePoolsPreferencesCard = ({ }; const sortingOptions: RadioButtonGroupOption[] = useMemo(() => { - const iconAlphabetical = direction === 'asc' ? SortAlphabeticalAscIcon : SortAlphabeticalDescIcon; - const iconNumerical = direction === 'asc' ? SortNumericalAscIcon : SortNumericalDescIcon; + const iconAlphabetical = + direction === 'asc' ? ( + + ) : ( + + ); + const iconNumerical = + direction === 'asc' ? ( + + ) : ( + + ); return [ { icon: iconAlphabetical, diff --git a/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListHeader.tsx b/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListHeader.tsx index 60e9d79df..a16120b8c 100644 --- a/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListHeader.tsx +++ b/packages/staking/src/features/BrowsePools/StakePoolsList/StakePoolsListHeader.tsx @@ -4,6 +4,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useOutsideHandles } from '../../outside-handles-provider'; import { analyticsActionsMap } from '../analytics'; +import { getDefaultSortOrderByField } from '../utils'; import { config } from './config'; export interface TableHeaders { @@ -66,7 +67,8 @@ export const StakePoolsListHeader = ({ setActiveSort, activeSort }: StakePoolsLi })); const onSortChange = (field: SortField) => { - const order = field === activeSort?.field && activeSort?.order === 'asc' ? 'desc' : 'asc'; + const inverseOrder = activeSort?.order === 'asc' ? 'desc' : 'asc'; + const order = field !== activeSort?.field ? getDefaultSortOrderByField(field) : inverseOrder; analytics.sendEventToPostHog(analyticsActionsMap[field]); setActiveSort({ field, order }); diff --git a/packages/staking/src/features/BrowsePools/__tests__/defaultSortOrderByField.test.ts b/packages/staking/src/features/BrowsePools/__tests__/defaultSortOrderByField.test.ts new file mode 100644 index 000000000..7541f64d3 --- /dev/null +++ b/packages/staking/src/features/BrowsePools/__tests__/defaultSortOrderByField.test.ts @@ -0,0 +1,19 @@ +/* eslint-disable unicorn/no-useless-undefined */ +import { getDefaultSortOrderByField } from '../utils'; + +describe('getDefaultSortOrderByField', () => { + it('returns asc', () => { + const tickerOrder = getDefaultSortOrderByField('ticker'); + const costOrder = getDefaultSortOrderByField('cost'); + const marginOrder = getDefaultSortOrderByField('margin'); + + expect(tickerOrder).toEqual('asc'); + expect(costOrder).toEqual('asc'); + expect(marginOrder).toEqual('asc'); + }); + + it('returns desc', () => { + const pledgeOrder = getDefaultSortOrderByField('pledge'); + expect(pledgeOrder).toEqual('desc'); + }); +}); diff --git a/packages/staking/src/features/BrowsePools/constants.ts b/packages/staking/src/features/BrowsePools/constants.ts index be5b99c4b..bae32b93e 100644 --- a/packages/staking/src/features/BrowsePools/constants.ts +++ b/packages/staking/src/features/BrowsePools/constants.ts @@ -1,10 +1,17 @@ -import { BrowsePoolsView, StakePoolSortOptions } from './types'; +import type { StakePoolSortOptions, StakingBrowserPreferences } from './types'; +import { BrowsePoolsView } from './types'; +import { getDefaultSortOrderByField } from './utils'; export const SEARCH_DEBOUNCE_IN_MS = 300; export const DEFAULT_SORT_OPTIONS: StakePoolSortOptions = { field: 'ticker', - order: 'asc', + order: getDefaultSortOrderByField('ticker'), }; export const DEFAULT_BROWSE_POOLS_VIEW: BrowsePoolsView = BrowsePoolsView.grid; + +export const DEFAULT_STAKING_BROWSER_PREFERENCES: StakingBrowserPreferences = { + poolsView: DEFAULT_BROWSE_POOLS_VIEW, + selectedPoolIds: [], +}; diff --git a/packages/staking/src/features/BrowsePools/index.ts b/packages/staking/src/features/BrowsePools/index.ts index d32c0715b..7681853e0 100644 --- a/packages/staking/src/features/BrowsePools/index.ts +++ b/packages/staking/src/features/BrowsePools/index.ts @@ -2,11 +2,11 @@ export { BrowsePools } from './BrowsePools'; export { BrowsePoolsPreferencesCard } from './BrowsePoolsPreferencesCard'; export { getPoolInfos } from './queries'; export { useBrowsePoolsPersistence } from './hooks'; -export { DEFAULT_SORT_OPTIONS } from './constants'; +export { DEFAULT_SORT_OPTIONS, DEFAULT_STAKING_BROWSER_PREFERENCES } from './constants'; // TODO: remove once multi delegation feature is GA'd -export { getSaturationLevel, isOversaturated } from './utils'; -export type { StakePoolSortOptions, TranslationsFor, SortField, SortOrder } from './types'; +export { getSaturationLevel, isOversaturated, getDefaultSortOrderByField } from './utils'; +export type { StakingBrowserPreferences, StakePoolSortOptions, TranslationsFor, SortField, SortOrder } from './types'; export { BrowsePoolsView } from './types'; export { StakePoolCardProgressBar } from './StakePoolCard'; export { StakePoolsListRowSkeleton, config as stakePoolTableConfig } from './StakePoolsList'; diff --git a/packages/staking/src/features/BrowsePools/types.ts b/packages/staking/src/features/BrowsePools/types.ts index 9c561e754..6e76f49b7 100644 --- a/packages/staking/src/features/BrowsePools/types.ts +++ b/packages/staking/src/features/BrowsePools/types.ts @@ -22,3 +22,8 @@ export type StakePoolSortOptions = { }; export type TranslationsFor = Record; + +export type StakingBrowserPreferences = { + poolsView: BrowsePoolsView; + selectedPoolIds: string[]; +}; diff --git a/packages/staking/src/features/BrowsePools/utils.ts b/packages/staking/src/features/BrowsePools/utils.ts index 790885c9d..1ed498b65 100644 --- a/packages/staking/src/features/BrowsePools/utils.ts +++ b/packages/staking/src/features/BrowsePools/utils.ts @@ -1,5 +1,5 @@ import inRange from 'lodash/inRange'; -import { SaturationLevels } from './types'; +import { SaturationLevels, SortField, SortOrder } from './types'; const mediumUpperBound = 90; const highUpperBound = 95; @@ -28,3 +28,6 @@ export const getSaturationLevel = (saturation: number): SaturationLevels => { } return SaturationLevels.Medium; }; + +export const getDefaultSortOrderByField = (field: SortField): SortOrder => + ['ticker', 'cost', 'margin'].includes(field) ? 'asc' : 'desc'; diff --git a/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx b/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx index 62ae3df8b..af7ddf04f 100644 --- a/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx +++ b/packages/staking/src/features/Drawer/confirmation/StakePoolConfirmationFooter.tsx @@ -37,14 +37,14 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation const [openPoolsManagementConfirmationModal, setOpenPoolsManagementConfirmationModal] = useState(null); - const isInMemory = walletType === WalletType.InMemory; + const isHardwareWallet = walletType === WalletType.Trezor || walletType === WalletType.Ledger; // TODO unify const signAndSubmitTransaction = useCallback(async () => { if (!delegationTxBuilder) throw new Error('Unable to submit transaction. The delegationTxBuilder not available'); const isMultidelegation = draftPortfolio && draftPortfolio.length > 1; - if (!isInMemory && isMultidelegation) { + if (isHardwareWallet && isMultidelegation) { const isSupported = await isMultidelegationSupportedByDevice(walletType); if (!isSupported) { throw new Error('MULTIDELEGATION_NOT_SUPPORTED'); @@ -52,11 +52,18 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation } const signedTx = await delegationTxBuilder.build().sign(); await inMemoryWallet.submitTx(signedTx); - }, [delegationTxBuilder, inMemoryWallet, isInMemory, isMultidelegationSupportedByDevice, walletType, draftPortfolio]); + }, [ + delegationTxBuilder, + inMemoryWallet, + isHardwareWallet, + isMultidelegationSupportedByDevice, + walletType, + draftPortfolio, + ]); const handleSubmission = useCallback(async () => { setOpenPoolsManagementConfirmationModal(null); - if (isInMemory) { + if (!isHardwareWallet) { portfolioMutators.executeCommand({ type: 'DrawerContinue' }); return; } @@ -76,8 +83,8 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation setIsConfirmingTx(false); } }, [ - currentPortfolio, - isInMemory, + currentPortfolio.length, + isHardwareWallet, portfolioMutators, setIsRestaking, signAndSubmitTransaction, @@ -105,14 +112,14 @@ export const StakePoolConfirmationFooter = ({ popupView }: StakePoolConfirmation }, [analytics, currentPortfolio, draftPortfolio, handleSubmission]); const confirmLabel = useMemo(() => { - if (!isInMemory) { + if (isHardwareWallet) { const staleLabels = popupView ? t('drawer.confirmation.button.continueInAdvancedView') : t('drawer.confirmation.button.confirmWithDevice', { hardwareWallet: walletType }); return isConfirmingTx ? t('drawer.confirmation.button.signing') : staleLabels; } return t('drawer.confirmation.button.confirm'); - }, [isConfirmingTx, isInMemory, walletType, popupView, t]); + }, [isConfirmingTx, isHardwareWallet, walletType, popupView, t]); return ( <> diff --git a/packages/staking/src/features/outside-handles-provider/types.ts b/packages/staking/src/features/outside-handles-provider/types.ts index 2282239c1..66d1c28ab 100644 --- a/packages/staking/src/features/outside-handles-provider/types.ts +++ b/packages/staking/src/features/outside-handles-provider/types.ts @@ -1,7 +1,7 @@ import { TxBuilder } from '@cardano-sdk/tx-construction'; import { Wallet } from '@lace/cardano'; import { AssetActivityListProps } from '@lace/core'; -import { BrowsePoolsView, StakePoolSortOptions } from 'features/BrowsePools/types'; +import { StakePoolSortOptions, StakingBrowserPreferences } from 'features/BrowsePools'; import type { IAnalyticsTracker } from '@lace/common'; type WalletBalance = { @@ -40,11 +40,6 @@ export enum StateStatus { ERROR = 'error', } -export interface StakingBrowserPreferences { - poolsView: BrowsePoolsView; - selectedPoolIds: string[]; -} - export interface IBlockchainProvider { stakePoolProvider: Wallet.StakePoolProvider; assetProvider: Wallet.AssetProvider; diff --git a/packages/staking/src/index.ts b/packages/staking/src/index.ts index 1c3003125..0759ea148 100644 --- a/packages/staking/src/index.ts +++ b/packages/staking/src/index.ts @@ -1,7 +1,7 @@ export { Staking, StakingPopup } from './features/staking'; -export { BrowsePoolsPreferencesCard } from './features/BrowsePools'; +export type { StakingBrowserPreferences } from 'features/BrowsePools'; +export { BrowsePoolsPreferencesCard, DEFAULT_STAKING_BROWSER_PREFERENCES } from './features/BrowsePools'; export { OutsideHandlesProvider } from './features/outside-handles-provider'; -export type { StakingBrowserPreferences } from './features/outside-handles-provider'; export { MAX_POOLS_COUNT } from './features/store'; // TODO: remove once multi delegaion feature is GA'd @@ -16,6 +16,8 @@ export { StakePoolsListRowSkeleton, getSaturationLevel, isOversaturated, + getDefaultSortOrderByField, + DEFAULT_SORT_OPTIONS, } from './features/BrowsePools'; export { mapStakePoolToDisplayData } from './features/store'; /* eslint-enable import/export */ diff --git a/packages/ui/package.json b/packages/ui/package.json index 986326574..762a42f14 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,7 +18,7 @@ "scripts": { "build": "run -T rollup -c rollup.config.js", "build-storybook": "NODE_OPTIONS=--openssl-legacy-provider; build-storybook", - "cleanup": "yarn exec rm -rf node_modules", + "cleanup": "yarn exec rm -rf dist node_modules", "format": "yarn prettier --write '**/*.tsx'", "lint": "eslint .", "storybook": "NODE_OPTIONS=--openssl-legacy-provider; start-storybook -p 6006", diff --git a/packages/ui/src/design-system/address-tags/address-tag.component.tsx b/packages/ui/src/design-system/address-tags/address-tag.component.tsx new file mode 100644 index 000000000..9f9e1cf89 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tag.component.tsx @@ -0,0 +1,31 @@ +import type { PropsWithChildren, HTMLAttributes } from 'react'; +import React from 'react'; + +import cs from 'classnames'; + +import * as cx from './address-tag.css'; + +import type { AddressTagVariants } from './types'; + +export type AddressBaseTageProps = PropsWithChildren< + HTMLAttributes & { + variant: AddressTagVariants; + testId?: string; + } +>; + +export const AddressTag = ({ + children, + className, + variant, + testId = 'address-tag', + ...restProps +}: Readonly): JSX.Element => ( +
+ {children} +
+); diff --git a/packages/ui/src/design-system/address-tags/address-tag.css.ts b/packages/ui/src/design-system/address-tags/address-tag.css.ts new file mode 100644 index 000000000..604787839 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tag.css.ts @@ -0,0 +1,33 @@ +import { recipe } from '@vanilla-extract/recipes'; + +import { vars } from '../../design-tokens'; + +import { AddressTagVariants } from './types'; + +export const addressTag = recipe({ + base: { + fontSize: vars.fontSizes.$12, + fontWeight: vars.fontWeights.$medium, + borderRadius: vars.radius.$medium, + padding: `0 ${vars.spacing.$8}`, + display: 'flex', + alignItems: 'center', + height: vars.spacing.$24, + }, + variants: { + scheme: { + [AddressTagVariants.Own]: { + color: vars.colors.$address_tag_own_color, + backgroundColor: vars.colors.$address_tag_own_bgColor, + }, + [AddressTagVariants.Handle]: { + color: vars.colors.$address_tag_handle_color, + backgroundColor: vars.colors.$address_tag_handle_bgColor, + }, + [AddressTagVariants.Foreign]: { + color: vars.colors.$address_tag_foreign_color, + backgroundColor: vars.colors.$address_tag_foreign_bgColor, + }, + }, + }, +}); diff --git a/packages/ui/src/design-system/address-tags/address-tags.stories.tsx b/packages/ui/src/design-system/address-tags/address-tags.stories.tsx new file mode 100644 index 000000000..227884074 --- /dev/null +++ b/packages/ui/src/design-system/address-tags/address-tags.stories.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import type { Meta } from '@storybook/react'; + +import { ThemeColorScheme, LocalThemeProvider } from '../../design-tokens'; +import { page, Section, Variants } from '../decorators'; +import { Cell, Grid } from '../grid'; + +import { AddressTag } from './address-tag.component'; +import { AddressTagVariants } from './types'; + +export default { + title: 'AddressTag', + decorators: [ + page({ + title: 'Address Tag', + subtitle: 'Simple component to flag addresses as own, handle or foreign.', + }), + ], +} as Meta; + +export const Overview = (): JSX.Element => { + const variantsData = [ + { + Component: AddressTag, + variant: AddressTagVariants.Own, + }, + { + Component: AddressTag, + variant: AddressTagVariants.Handle, + }, + { + Component: AddressTag, + variant: AddressTagVariants.Foreign, + }, + ]; + const renderTable = (showHeader = false): JSX.Element => ( + v.variant) : []} + > + + {variantsData.map(({ Component, variant }) => ( + + {variant} + + ))} + + + ); + + return ( + + +
+ <> + {renderTable(true)} + +
{renderTable()}
+
+ +
+
+
+ ); +}; diff --git a/packages/ui/src/design-system/address-tags/index.ts b/packages/ui/src/design-system/address-tags/index.ts new file mode 100644 index 000000000..4b40d22cb --- /dev/null +++ b/packages/ui/src/design-system/address-tags/index.ts @@ -0,0 +1,2 @@ +export { AddressTag } from './address-tag.component'; +export { AddressTagVariants } from './types'; diff --git a/packages/ui/src/design-system/address-tags/types.ts b/packages/ui/src/design-system/address-tags/types.ts new file mode 100644 index 000000000..d0721894e --- /dev/null +++ b/packages/ui/src/design-system/address-tags/types.ts @@ -0,0 +1,5 @@ +export enum AddressTagVariants { + Own = 'Own', + Handle = 'Handle', + Foreign = 'Foreign', +} diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index dcc11a288..a4704d4d5 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -53,3 +53,4 @@ export { SummaryExpander } from './summary-expander'; export * from './auto-suggest-box'; export * from './table'; export { InfoBar } from './info-bar'; +export * from './address-tags'; diff --git a/packages/ui/src/design-system/radio-button/radio-button.component.tsx b/packages/ui/src/design-system/radio-button/radio-button.component.tsx index b500ffd1e..41fe988bc 100644 --- a/packages/ui/src/design-system/radio-button/radio-button.component.tsx +++ b/packages/ui/src/design-system/radio-button/radio-button.component.tsx @@ -11,7 +11,7 @@ import * as cx from './radio-button.css'; export interface RadioButtonGroupOption { value: string; label: React.ReactNode; - icon?: React.ComponentType>; + icon?: JSX.Element; onIconClick?: () => void; tooltipText?: string; } @@ -41,7 +41,7 @@ export const RadioButtonGroup = ({ onValueChange={onValueChange} className={cx.radioGroupRoot} > - {options.map(({ value, label, icon: Icon, onIconClick }) => { + {options.map(({ value, label, icon, onIconClick }) => { const hasLabel = Boolean(label); return ( @@ -60,6 +60,7 @@ export const RadioButtonGroup = ({ id={`radio-btn-control-id-${value}`} value={value} className={cx.radioGroupIndicatorWrapper} + data-testid={`radio-btn-test-id-${value}`} > )} - {Icon !== undefined && value === selectedValue && ( + {icon !== undefined && value === selectedValue && ( )} diff --git a/packages/ui/src/design-tokens/colors.data.ts b/packages/ui/src/design-tokens/colors.data.ts index 4c9ef372b..12be7aea9 100644 --- a/packages/ui/src/design-tokens/colors.data.ts +++ b/packages/ui/src/design-tokens/colors.data.ts @@ -308,6 +308,13 @@ export const colors = { $info_bar_container_bgColor: '', $info_bar_message_color: '', $info_bar_icon_color: '', + + $address_tag_own_color: '', + $address_tag_own_bgColor: '', + $address_tag_handle_color: '', + $address_tag_handle_bgColor: '', + $address_tag_foreign_color: '', + $address_tag_foreign_bgColor: '', }; export type Colors = typeof colors; diff --git a/packages/ui/src/design-tokens/theme/dark-theme.css.ts b/packages/ui/src/design-tokens/theme/dark-theme.css.ts index 159d6e1b5..01e0b0551 100644 --- a/packages/ui/src/design-tokens/theme/dark-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/dark-theme.css.ts @@ -409,6 +409,16 @@ const colors: Colors = { $info_bar_container_bgColor: darkColorScheme.$primary_dark_grey_plus, $info_bar_icon_color: darkColorScheme.$secondary_data_pink, $info_bar_message_color: darkColorScheme.$primary_light_grey, + + $address_tag_own_color: darkColorScheme.$secondary_data_pink, + $address_tag_own_bgColor: rgba(darkColorScheme.$secondary_data_pink, 0.1), + $address_tag_handle_color: darkColorScheme.$primary_accent_purple, + $address_tag_handle_bgColor: rgba( + darkColorScheme.$primary_accent_purple, + 0.1, + ), + $address_tag_foreign_color: darkColorScheme.$primary_dark_grey, + $address_tag_foreign_bgColor: darkColorScheme.$primary_light_grey, }; const elevation: Elevation = { diff --git a/packages/ui/src/design-tokens/theme/light-theme.css.ts b/packages/ui/src/design-tokens/theme/light-theme.css.ts index 8ba4cc1f0..fd380d355 100644 --- a/packages/ui/src/design-tokens/theme/light-theme.css.ts +++ b/packages/ui/src/design-tokens/theme/light-theme.css.ts @@ -437,6 +437,16 @@ const colors: Colors = { $info_bar_container_bgColor: lightColorScheme.$secondary_cream, $info_bar_icon_color: lightColorScheme.$secondary_data_pink, $info_bar_message_color: lightColorScheme.$primary_black, + + $address_tag_own_color: lightColorScheme.$secondary_data_pink, + $address_tag_own_bgColor: rgba(lightColorScheme.$secondary_data_pink, 0.1), + $address_tag_handle_color: lightColorScheme.$primary_accent_purple, + $address_tag_handle_bgColor: rgba( + lightColorScheme.$primary_accent_purple, + 0.1, + ), + $address_tag_foreign_color: lightColorScheme.$primary_dark_grey, + $address_tag_foreign_bgColor: lightColorScheme.$primary_light_grey, }; export const elevation: Elevation = { diff --git a/yarn.lock b/yarn.lock index 870c07ed8..57f15a3b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7675,17 +7675,17 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/cardano-services-client@npm:0.18.0": - version: 0.18.0 - resolution: "@cardano-sdk/cardano-services-client@npm:0.18.0" +"@cardano-sdk/cardano-services-client@npm:0.19.0": + version: 0.19.0 + resolution: "@cardano-sdk/cardano-services-client@npm:0.19.0" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/util": ~0.15.0 - axios: ^0.27.2 + axios: ^0.28.0 class-validator: ^0.14.0 json-bigint: ~1.0.0 ts-log: ^2.2.4 - checksum: f722a14121c944ecd37360525ef664da41cad3076042acc43961e4313935a4a4acbda3e3d5019979f1e14a1a3df3889e5509a453b93b99b94c8cf79c7fb2cfe7 + checksum: 7a691c624394b374207dbd8b16a6d0e53b852c46e5291526e9085f12bb1fafc2cf539176be32c9f08c43e36399f0b7172eb7e1028fccb62f44caef1c3bcac81e languageName: node linkType: hard @@ -7756,46 +7756,47 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/hardware-ledger@npm:0.8.19, @cardano-sdk/hardware-ledger@npm:~0.8.19": - version: 0.8.19 - resolution: "@cardano-sdk/hardware-ledger@npm:0.8.19" +"@cardano-sdk/hardware-ledger@npm:0.9.1, @cardano-sdk/hardware-ledger@npm:~0.9.1": + version: 0.9.1 + resolution: "@cardano-sdk/hardware-ledger@npm:0.9.1" dependencies: "@cardano-foundation/ledgerjs-hw-app-cardano": ^6.0.0 "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/key-management": ~0.20.1 - "@cardano-sdk/tx-construction": ~0.18.2 + "@cardano-sdk/tx-construction": ~0.18.3 "@cardano-sdk/util": ~0.15.0 "@ledgerhq/hw-transport": ^6.28.1 "@ledgerhq/hw-transport-node-hid-noevents": ^6.27.12 - "@ledgerhq/hw-transport-webhid": ^6.27.12 + "@ledgerhq/hw-transport-webusb": ^6.27.12 + node-hid: ^2.1.2 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: ac2cb838d10de6cde097a5c1536fbc0f783fe2594bfa9ff00b00a0bf1d98c0dfb919a2439929e6b3969bf17fdd245c919fb3de26834027f29613d10886284cce + checksum: 1d2c4ad341b1029058f55689eec8ec4beddb8086c22ce0905331b62da8eed5822176d92e3ee59df93d24ceddd8e6941d64d44330fa5f919e423e402f22dd0ca8 languageName: node linkType: hard -"@cardano-sdk/hardware-trezor@npm:0.4.19, @cardano-sdk/hardware-trezor@npm:~0.4.19": - version: 0.4.19 - resolution: "@cardano-sdk/hardware-trezor@npm:0.4.19" +"@cardano-sdk/hardware-trezor@npm:0.4.20, @cardano-sdk/hardware-trezor@npm:~0.4.20": + version: 0.4.20 + resolution: "@cardano-sdk/hardware-trezor@npm:0.4.20" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/key-management": ~0.20.1 - "@cardano-sdk/tx-construction": ~0.18.2 + "@cardano-sdk/tx-construction": ~0.18.3 "@cardano-sdk/util": ~0.15.0 "@trezor/connect": 9.1.6 "@trezor/connect-web": 9.1.6 lodash: ^4.17.21 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: 51e32f95d39bb3b4d3f1af50178a00ac6142b43f840070b0d0e78dd372644586040ac4b60b60b362519a68e59729d34101aa9ef851b9f5946136a007cda71f67 + checksum: c8f23d141cb7db50a289b4acd61aa7baeb80f4fe331cdf65c192b17dcaf6d331caa1a397f1d965b2eddb2d7269cb7c91cfc7d2702e85e73ec1dcd3d54a7ab11f languageName: node linkType: hard -"@cardano-sdk/input-selection@npm:0.12.26, @cardano-sdk/input-selection@npm:~0.12.26": - version: 0.12.26 - resolution: "@cardano-sdk/input-selection@npm:0.12.26" +"@cardano-sdk/input-selection@npm:0.12.27, @cardano-sdk/input-selection@npm:~0.12.27": + version: 0.12.27 + resolution: "@cardano-sdk/input-selection@npm:0.12.27" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/key-management": ~0.20.1 @@ -7803,7 +7804,7 @@ __metadata: bignumber.js: ^9.1.1 lodash: ^4.17.21 ts-custom-error: ^3.2.0 - checksum: 0bc9f3bf84cb061196d61c38bc3590f284c9313f932a88c69207c1ab8919d08b47a76f59f1b16ce92c45a9ccf0c4cb3112b0f9cc5829166a5f803e99d9df8811 + checksum: 0d39ac9a32bcf17cf5b0485757dde49603bfa9af5adfccacba18f5e7a7a2d785686d834bd69a21365847d05ff72f4023770fcc07e6d6e140755961e7eea71980 languageName: node linkType: hard @@ -7828,22 +7829,22 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/tx-construction@npm:0.18.2, @cardano-sdk/tx-construction@npm:~0.18.2": - version: 0.18.2 - resolution: "@cardano-sdk/tx-construction@npm:0.18.2" +"@cardano-sdk/tx-construction@npm:0.18.3, @cardano-sdk/tx-construction@npm:~0.18.3": + version: 0.18.3 + resolution: "@cardano-sdk/tx-construction@npm:0.18.3" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 - "@cardano-sdk/input-selection": ~0.12.26 + "@cardano-sdk/input-selection": ~0.12.27 "@cardano-sdk/key-management": ~0.20.1 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.9 + "@cardano-sdk/util-rxjs": ~0.7.10 lodash: ^4.17.21 npm: ^9.3.0 rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.4 - checksum: 9b3e7d319421a2c8237afd289ef241a429e81a49e20b466c436919116536115da5fe62c4124fb1d452a4d90a3b4262468dc1150f402fcf21a74f01e3d7433bc7 + checksum: 47125f2a3887ead7652ce11fd4642d2bd66c39c6f587531e95b65c6054e62b77a66358b27ba4fb8e528f6aa7971b0adc0e0c2d64da892cd5c4167f36b7aa4e63 languageName: node linkType: hard @@ -7870,14 +7871,14 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/util-rxjs@npm:~0.7.9": - version: 0.7.9 - resolution: "@cardano-sdk/util-rxjs@npm:0.7.9" +"@cardano-sdk/util-rxjs@npm:~0.7.10": + version: 0.7.10 + resolution: "@cardano-sdk/util-rxjs@npm:0.7.10" dependencies: "@cardano-sdk/util": ~0.15.0 backoff-rxjs: ^7.0.0 rxjs: ^7.4.0 - checksum: 8474a90a5f7a3715e2ba333ceafbc3bffff715c11ce2b67121f29b486697064727d8dcf51a8dc954f0a35976ce31f0cc7c21beeedb7159aa5482d2ca2e7509fa + checksum: 9d36075e86b58dcd3c52beff3876ea08c25df889ad6b4484e4279dd724668310d67c01d61ebcb2e3c580ca62c72f720fe7807ea3ad22eb2b1740fb72287f1a77 languageName: node linkType: hard @@ -7895,20 +7896,20 @@ __metadata: languageName: node linkType: hard -"@cardano-sdk/wallet@npm:0.35.2, @cardano-sdk/wallet@npm:~0.35.2": - version: 0.35.2 - resolution: "@cardano-sdk/wallet@npm:0.35.2" +"@cardano-sdk/wallet@npm:0.37.0, @cardano-sdk/wallet@npm:~0.37.0": + version: 0.37.0 + resolution: "@cardano-sdk/wallet@npm:0.37.0" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/dapp-connector": ~0.12.14 - "@cardano-sdk/hardware-ledger": ~0.8.19 - "@cardano-sdk/hardware-trezor": ~0.4.19 - "@cardano-sdk/input-selection": ~0.12.26 + "@cardano-sdk/hardware-ledger": ~0.9.1 + "@cardano-sdk/hardware-trezor": ~0.4.20 + "@cardano-sdk/input-selection": ~0.12.27 "@cardano-sdk/key-management": ~0.20.1 - "@cardano-sdk/tx-construction": ~0.18.2 + "@cardano-sdk/tx-construction": ~0.18.3 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.9 + "@cardano-sdk/util-rxjs": ~0.7.10 backoff-rxjs: ^7.0.0 bignumber.js: ^9.1.1 delay: ^5.0.0 @@ -7918,24 +7919,24 @@ __metadata: rxjs: ^7.4.0 ts-custom-error: ^3.2.0 ts-log: ^2.2.3 - checksum: af592b22f6290c6640b71234a28781c251f6c4b7ca2f8fb6cb608bb4e9581fb386ba75b1698bb83a81f638202ab55a300f7a395e2138eb96cc96ba9490c2c69d + checksum: 1ae1b5d009053491fb520fe677a6281e7098b72b9582c6a61fde17664d9968420e70ff185b4b90a5ad6fa43e7015729e5dd7dfdb8c7e988c51a71e94f4c673b6 languageName: node linkType: hard -"@cardano-sdk/web-extension@npm:0.26.1": - version: 0.26.1 - resolution: "@cardano-sdk/web-extension@npm:0.26.1" +"@cardano-sdk/web-extension@npm:0.27.0": + version: 0.27.0 + resolution: "@cardano-sdk/web-extension@npm:0.27.0" dependencies: "@cardano-sdk/core": ~0.30.0 "@cardano-sdk/crypto": ~0.1.22 "@cardano-sdk/dapp-connector": ~0.12.14 - "@cardano-sdk/hardware-ledger": ~0.8.19 - "@cardano-sdk/hardware-trezor": ~0.4.19 + "@cardano-sdk/hardware-ledger": ~0.9.1 + "@cardano-sdk/hardware-trezor": ~0.4.20 "@cardano-sdk/key-management": ~0.20.1 - "@cardano-sdk/tx-construction": ~0.18.2 + "@cardano-sdk/tx-construction": ~0.18.3 "@cardano-sdk/util": ~0.15.0 - "@cardano-sdk/util-rxjs": ~0.7.9 - "@cardano-sdk/wallet": ~0.35.2 + "@cardano-sdk/util-rxjs": ~0.7.10 + "@cardano-sdk/wallet": ~0.37.0 backoff-rxjs: ^7.0.0 lodash: ^4.17.21 rxjs: ^7.4.0 @@ -7943,7 +7944,7 @@ __metadata: ts-log: ^2.2.3 uuid: ^8.3.2 webextension-polyfill: ^0.8.0 - checksum: 3362f8fedb1af6ad2b167763edc640eb879c87d25ac27ec3f12be6506c61951d02fea4ad3de9f9817528333d7e6cb11298309b81f450cd58eac35d7a302f5476 + checksum: 89d81e8c9c45c652bfe7ad5f6af6cb9e594471e0cebeac2733ba47b54ce66e08581e59eba542f25e16e13749c21785afdee40540f470395d5d4cfb4e3f21a2a4 languageName: node linkType: hard @@ -10577,14 +10578,14 @@ __metadata: resolution: "@lace/browser-extension-wallet@workspace:apps/browser-extension-wallet" dependencies: "@ant-design/icons": ^4.7.0 - "@cardano-sdk/cardano-services-client": 0.18.0 + "@cardano-sdk/cardano-services-client": 0.19.0 "@cardano-sdk/core": 0.30.0 "@cardano-sdk/dapp-connector": 0.12.14 - "@cardano-sdk/input-selection": 0.12.26 - "@cardano-sdk/tx-construction": 0.18.2 + "@cardano-sdk/input-selection": 0.12.27 + "@cardano-sdk/tx-construction": 0.18.3 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.37.0 + "@cardano-sdk/web-extension": 0.27.0 "@emurgo/cardano-message-signing-asmjs": 1.0.1 "@emurgo/cip14-js": ~3.0.1 "@koralabs/handles-public-api-interfaces": ^1.6.6 @@ -10604,7 +10605,7 @@ __metadata: "@vespaiach/axios-fetch-adapter": ^0.3.0 antd: ^4.24.10 are-you-es5: ^2.1.2 - axios: 0.21.4 + axios: 0.28.0 bignumber.js: 9.0.1 bip39: ^3.0.4 blake2b-no-wasm: 2.1.4 @@ -10647,19 +10648,21 @@ __metadata: version: 0.0.0-use.local resolution: "@lace/cardano@workspace:packages/cardano" dependencies: - "@cardano-sdk/cardano-services-client": 0.18.0 + "@cardano-sdk/cardano-services-client": 0.19.0 "@cardano-sdk/core": 0.30.0 "@cardano-sdk/crypto": 0.1.22 - "@cardano-sdk/hardware-ledger": 0.8.19 - "@cardano-sdk/hardware-trezor": 0.4.19 + "@cardano-sdk/hardware-ledger": 0.9.1 + "@cardano-sdk/hardware-trezor": 0.4.20 "@cardano-sdk/key-management": 0.20.1 "@cardano-sdk/util": 0.15.0 "@cardano-sdk/util-dev": 0.19.18 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.37.0 + "@cardano-sdk/web-extension": 0.27.0 "@emurgo/cardano-message-signing-browser": 1.0.1 "@lace/common": 0.1.0 + "@ledgerhq/devices": ^8.2.1 "@stablelib/chacha20poly1305": 1.0.1 + "@trezor/transport": ^1.1.18 bignumber.js: 9.0.1 buffer: 6.0.3 classnames: 2.3.1 @@ -10734,7 +10737,7 @@ __metadata: "@types/debounce-promise": ^3.1.6 "@types/uuid": ^8 antd: ^4.24.10 - axios: 0.21.4 + axios: 0.28.0 axios-cache-adapter: 2.7.3 classnames: ^2.3.1 debounce-promise: ^3.1.2 @@ -10818,9 +10821,11 @@ __metadata: "@ant-design/icons": ^4.7.0 "@babel/core": ^7.21.0 "@cardano-sdk/core": 0.30.0 - "@cardano-sdk/input-selection": 0.12.26 - "@cardano-sdk/tx-construction": 0.18.2 + "@cardano-sdk/input-selection": 0.12.27 + "@cardano-sdk/tx-construction": 0.18.3 "@cardano-sdk/util": 0.15.0 + "@cardano-sdk/wallet": 0.37.0 + "@cardano-sdk/web-extension": 0.27.0 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 "@lace/core": 0.1.0 @@ -10833,6 +10838,7 @@ __metadata: "@storybook/addon-links": ^7.6.7 "@storybook/blocks": ^7.6.7 "@storybook/jest": ^0.2.3 + "@storybook/preview-api": ^8.0.4 "@storybook/react": ^7.6.7 "@storybook/react-vite": ^7.6.7 "@storybook/test": ^7.6.7 @@ -10886,8 +10892,8 @@ __metadata: "@cardano-sdk/input-selection": 0.12.26 "@cardano-sdk/tx-construction": 0.18.2 "@cardano-sdk/util": 0.15.0 - "@cardano-sdk/wallet": 0.35.2 - "@cardano-sdk/web-extension": 0.26.1 + "@cardano-sdk/wallet": 0.36.0 + "@cardano-sdk/web-extension": 0.26.2 "@lace/cardano": ^0.1.0 "@lace/common": ^0.1.0 "@lace/core": 0.1.0 @@ -10988,6 +10994,18 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/devices@npm:^8.2.1": + version: 8.2.1 + resolution: "@ledgerhq/devices@npm:8.2.1" + dependencies: + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/logs": ^6.12.0 + rxjs: ^7.8.1 + semver: ^7.3.5 + checksum: 5c7fa3004a4ebd30b0dcb8563642db308478bbec115102e5404dd0affcc99f880d094137e88c1f2cc064f78d65a5e946d5ebd8db89141977e32860885ea23ebe + languageName: node + linkType: hard + "@ledgerhq/errors@npm:^6.12.3": version: 6.12.3 resolution: "@ledgerhq/errors@npm:6.12.3" @@ -10995,6 +11013,13 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/errors@npm:^6.16.2": + version: 6.16.2 + resolution: "@ledgerhq/errors@npm:6.16.2" + checksum: 2dd796c78b8428339c8906cfe2325e62c211f484576835198a9bf4efc8fed38b4ca5d342bfb08aef6c623720753ea3e5ce77e50367f2808ad5610e3ff54cec70 + languageName: node + linkType: hard + "@ledgerhq/hw-transport-node-hid-noevents@npm:^6.27.12": version: 6.27.12 resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.27.12" @@ -11008,15 +11033,15 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/hw-transport-webhid@npm:^6.27.12": - version: 6.27.12 - resolution: "@ledgerhq/hw-transport-webhid@npm:6.27.12" +"@ledgerhq/hw-transport-webusb@npm:^6.27.12": + version: 6.28.4 + resolution: "@ledgerhq/hw-transport-webusb@npm:6.28.4" dependencies: - "@ledgerhq/devices": ^8.0.0 - "@ledgerhq/errors": ^6.12.3 - "@ledgerhq/hw-transport": ^6.28.1 - "@ledgerhq/logs": ^6.10.1 - checksum: 5f5253417ba6f5eb4d979e031a24c8bef9a642bb6e317c49a6abec33e316061c13aac6de8a7b353a907a11bbb7dd14ef16716be4017f4a008a6cc9136a366cdd + "@ledgerhq/devices": ^8.2.1 + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/hw-transport": ^6.30.4 + "@ledgerhq/logs": ^6.12.0 + checksum: 41e3c71b11c9cc8363e42c11874d00f3b4673a3ea6dde738d8f483ea08a4bfe7c529e5db92e17c6e3fe9d585f22eafd90d8c803320fe2e79de09fb31e998240b languageName: node linkType: hard @@ -11031,6 +11056,18 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/hw-transport@npm:^6.30.4": + version: 6.30.4 + resolution: "@ledgerhq/hw-transport@npm:6.30.4" + dependencies: + "@ledgerhq/devices": ^8.2.1 + "@ledgerhq/errors": ^6.16.2 + "@ledgerhq/logs": ^6.12.0 + events: ^3.3.0 + checksum: f4878e0b1ea093c69d2905c94bc9567c1c6694af9cff034634dd9639bd318666f131f2394f67c95d3cfd96b64693e14e4735883136545a19d8df902e3b59bf5e + languageName: node + linkType: hard + "@ledgerhq/logs@npm:^6.10.1": version: 6.10.1 resolution: "@ledgerhq/logs@npm:6.10.1" @@ -11038,6 +11075,13 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/logs@npm:^6.12.0": + version: 6.12.0 + resolution: "@ledgerhq/logs@npm:6.12.0" + checksum: 53fb9ceaf26b2a9fd6e7639b19119f4fef2f814d465fdd910e69c9486dce78137a1790e24f019a03bfabc87e19b2e6683f4da93a7fd203a61117a709fdf6484c + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -14792,6 +14836,19 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:8.0.5": + version: 8.0.5 + resolution: "@storybook/channels@npm:8.0.5" + dependencies: + "@storybook/client-logger": 8.0.5 + "@storybook/core-events": 8.0.5 + "@storybook/global": ^5.0.0 + telejson: ^7.2.0 + tiny-invariant: ^1.3.1 + checksum: d17bee03ee26c6fe635da70b13d631f344803deebd238639508fd0399fcb1f650c4aa7a16ac2e57916b492ad632413e68e4e0745cd4d49708845a0604231251e + languageName: node + linkType: hard + "@storybook/cli@npm:7.4.3": version: 7.4.3 resolution: "@storybook/cli@npm:7.4.3" @@ -15044,6 +15101,15 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:8.0.5": + version: 8.0.5 + resolution: "@storybook/client-logger@npm:8.0.5" + dependencies: + "@storybook/global": ^5.0.0 + checksum: 1deae606ed148268806db2c1bdea7e3514480b5e3be2b2e99d5f7b09c2ae2a0c6f759540e708aea1a1a6084204a4bace3996fe4f8ba4e6de330a62ddf735cd96 + languageName: node + linkType: hard + "@storybook/codemod@npm:7.4.3": version: 7.4.3 resolution: "@storybook/codemod@npm:7.4.3" @@ -15610,6 +15676,15 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:8.0.5": + version: 8.0.5 + resolution: "@storybook/core-events@npm:8.0.5" + dependencies: + ts-dedent: ^2.0.0 + checksum: d21293b760f16ce63ccbf6829213de5b7cdd54cb495a23a0311ae2edd388336e0fc6752f92f6c79115435a156258516d0ff5cd658189ca18a2b2cdd3382684c1 + languageName: node + linkType: hard + "@storybook/core-server@npm:6.5.10": version: 6.5.10 resolution: "@storybook/core-server@npm:6.5.10" @@ -16557,6 +16632,28 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-api@npm:^8.0.4": + version: 8.0.5 + resolution: "@storybook/preview-api@npm:8.0.5" + dependencies: + "@storybook/channels": 8.0.5 + "@storybook/client-logger": 8.0.5 + "@storybook/core-events": 8.0.5 + "@storybook/csf": ^0.1.2 + "@storybook/global": ^5.0.0 + "@storybook/types": 8.0.5 + "@types/qs": ^6.9.5 + dequal: ^2.0.2 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + qs: ^6.10.0 + tiny-invariant: ^1.3.1 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: f765258498cca244f058338ea3cf5eaf180d5769c83437a6e3a43aab1743dcb1c990a128a96d62332e264b286a1be24054c1a9ab6962fd2ba2824374707ef1cf + languageName: node + linkType: hard + "@storybook/preview-web@npm:6.5.10": version: 6.5.10 resolution: "@storybook/preview-web@npm:6.5.10" @@ -17390,6 +17487,17 @@ __metadata: languageName: node linkType: hard +"@storybook/types@npm:8.0.5": + version: 8.0.5 + resolution: "@storybook/types@npm:8.0.5" + dependencies: + "@storybook/channels": 8.0.5 + "@types/express": ^4.7.0 + file-system-cache: 2.3.0 + checksum: 6df50d1b324b8da2698eb822a7b0997fde2b267b3c9318939f22d6685bd7ee8b2d765434682651d81561ae11f75650a57082d0572d616836470c6e790823023d + languageName: node + linkType: hard + "@storybook/ui@npm:6.5.10": version: 6.5.10 resolution: "@storybook/ui@npm:6.5.10" @@ -18735,7 +18843,7 @@ __metadata: languageName: node linkType: hard -"@trezor/transport@npm:1.1.18": +"@trezor/transport@npm:1.1.18, @trezor/transport@npm:^1.1.18": version: 1.1.18 resolution: "@trezor/transport@npm:1.1.18" dependencies: @@ -19205,16 +19313,6 @@ __metadata: languageName: node linkType: hard -"@types/eslint-scope@npm:^3.7.0": - version: 3.7.1 - resolution: "@types/eslint-scope@npm:3.7.1" - dependencies: - "@types/eslint": "*" - "@types/estree": "*" - checksum: 4271c9adad19ad8a1d23062d9020468a51c7f81594b12b8e68f7d460c09e14d57cae3e82b077c402766369c0c17e2de72da72c405fa465d18a46c0b14ce92530 - languageName: node - linkType: hard - "@types/eslint-scope@npm:^3.7.3": version: 3.7.4 resolution: "@types/eslint-scope@npm:3.7.4" @@ -19235,7 +19333,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:^0.0.50": +"@types/estree@npm:*": version: 0.0.50 resolution: "@types/estree@npm:0.0.50" checksum: 9a2b6a4a8c117f34d08fbda5e8f69b1dfb109f7d149b60b00fd7a9fb6ac545c078bc590aa4ec2f0a256d680cf72c88b3b28b60c326ee38a7bc8ee1ee95624922 @@ -23144,7 +23242,18 @@ __metadata: languageName: node linkType: hard -"axios@npm:0.21.4, axios@npm:^0.21.1": +"axios@npm:0.28.0": + version: 0.28.0 + resolution: "axios@npm:0.28.0" + dependencies: + follow-redirects: ^1.15.0 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: d3782377512e67510787bf325b664f16c8b595ccdbcf52fca58433fbd082e33c3ef58a19e7d016ce92666be5a1a5ea82028add0cb841077981df9617bd071615 + languageName: node + linkType: hard + +"axios@npm:^0.21.1": version: 0.21.4 resolution: "axios@npm:0.21.4" dependencies: @@ -23163,6 +23272,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^0.28.0": + version: 0.28.1 + resolution: "axios@npm:0.28.1" + dependencies: + follow-redirects: ^1.15.0 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: 5115a38d79064d07437c5a28f15841e3607634040e3120ec06a2c4367a7d07cf213b16496eab53b6f58ebc5fb377a440ba9ed4782529b14449a1e285734bfb54 + languageName: node + linkType: hard + "axios@npm:^1.6.1": version: 1.6.3 resolution: "axios@npm:1.6.3" @@ -28966,7 +29086,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.7.0, enhanced-resolve@npm:^5.8.3": +"enhanced-resolve@npm:^5.7.0": version: 5.8.3 resolution: "enhanced-resolve@npm:5.8.3" dependencies: @@ -52512,13 +52632,6 @@ __metadata: languageName: node linkType: hard -"webpack-sources@npm:^3.2.0": - version: 3.2.0 - resolution: "webpack-sources@npm:3.2.0" - checksum: 8f1d686bd6aab2eda330579a07e14803cb2e01415f5a603697402aea3c36e98c1d2731167c3e97e50170cf1b0214cf8ef945fc639b100d1e3b67c023feb35716 - languageName: node - linkType: hard - "webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" @@ -52639,118 +52752,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:>=4.43.0 <6.0.0": - version: 5.74.0 - resolution: "webpack@npm:5.74.0" - dependencies: - "@types/eslint-scope": ^3.7.3 - "@types/estree": ^0.0.51 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.7.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.10.0 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 - json-parse-even-better-errors: ^2.3.1 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.4.0 - webpack-sources: ^3.2.3 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 320c41369a75051b19e18c63f408b3dcc481852e992f83d311771c5ec0f05f2946385e8ebef62030cf3587f0a3d2f12779ffdb191569a966847289ba7313f946 - languageName: node - linkType: hard - -"webpack@npm:^5": - version: 5.58.2 - resolution: "webpack@npm:5.58.2" - dependencies: - "@types/eslint-scope": ^3.7.0 - "@types/estree": ^0.0.50 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.4.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.8.3 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.4 - json-parse-better-errors: ^1.0.2 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.2.0 - webpack-sources: ^3.2.0 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 775da3b72c181204ca3f5200c144dde874fec8e059ba6c08dc9596e33e8408fd55c9c21ab5996124fe4b65963a54817bd4dfbb79fb89f307c9bf6054d69eaef5 - languageName: node - linkType: hard - -"webpack@npm:^5.76.1": - version: 5.76.1 - resolution: "webpack@npm:5.76.1" - dependencies: - "@types/eslint-scope": ^3.7.3 - "@types/estree": ^0.0.51 - "@webassemblyjs/ast": 1.11.1 - "@webassemblyjs/wasm-edit": 1.11.1 - "@webassemblyjs/wasm-parser": 1.11.1 - acorn: ^8.7.1 - acorn-import-assertions: ^1.7.6 - browserslist: ^4.14.5 - chrome-trace-event: ^1.0.2 - enhanced-resolve: ^5.10.0 - es-module-lexer: ^0.9.0 - eslint-scope: 5.1.1 - events: ^3.2.0 - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.2.9 - json-parse-even-better-errors: ^2.3.1 - loader-runner: ^4.2.0 - mime-types: ^2.1.27 - neo-async: ^2.6.2 - schema-utils: ^3.1.0 - tapable: ^2.1.1 - terser-webpack-plugin: ^5.1.3 - watchpack: ^2.4.0 - webpack-sources: ^3.2.3 - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: b01fe0bc2dbca0e10d290ddb0bf81e807a031de48028176e2b21afd696b4d3f25ab9accdad888ef4a1f7c7f4d41f13d5bf2395b7653fdf3e5e3dafa54e56dab2 - languageName: node - linkType: hard - -"webpack@npm:^5.9.0": +"webpack@npm:>=4.43.0 <6.0.0, webpack@npm:^5, webpack@npm:^5.76.1, webpack@npm:^5.9.0": version: 5.84.1 resolution: "webpack@npm:5.84.1" dependencies: