diff --git a/packages/e2e-tests/package-lock.json b/packages/e2e-tests/package-lock.json index ec61f16915..c6f22ae6d9 100644 --- a/packages/e2e-tests/package-lock.json +++ b/packages/e2e-tests/package-lock.json @@ -41,7 +41,7 @@ "@emurgo/cardano-serialization-lib-nodejs": "^12.0.0-alpha.26", "bignumber.js": "^9.1.2", "chai": "^4.3.10", - "chromedriver": "128.0.3", + "chromedriver": "129.0.0", "cross-env": "^7.0.3", "json-server": "^0.17.4", "mocha": "^10.2.0", @@ -450,9 +450,9 @@ } }, "node_modules/chromedriver": { - "version": "128.0.3", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-128.0.3.tgz", - "integrity": "sha512-Xn/bknOpGlY9tKinwS/hVWeNblSeZvbbJbF8XZ73X1jeWfAFPRXx3fMLdNNz8DqruDbx3cKEJ5wR3mnst6G3iw==", + "version": "129.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-129.0.0.tgz", + "integrity": "sha512-B1ccqD6hDjNrw94FeqdynIotn1ZV/TnFrkRz2Rync2kzSnq6D6IrSkN1w5Pnuvnc98QhN2xujxDXxkqEqy/PWg==", "dev": true, "hasInstallScript": true, "dependencies": { diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 8c33a02848..55f38e6737 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -47,7 +47,7 @@ "@emurgo/cardano-serialization-lib-nodejs": "^12.0.0-alpha.26", "bignumber.js": "^9.1.2", "chai": "^4.3.10", - "chromedriver": "128.0.3", + "chromedriver": "129.0.0", "cross-env": "^7.0.3", "json-server": "^0.17.4", "mocha": "^10.2.0", diff --git a/packages/yoroi-extension/app/Routes.js b/packages/yoroi-extension/app/Routes.js index e2f5af2e70..18b96f082c 100644 --- a/packages/yoroi-extension/app/Routes.js +++ b/packages/yoroi-extension/app/Routes.js @@ -28,6 +28,7 @@ import NFTsWrapper from './containers/wallet/NFTsWrapper'; import Wallet from './containers/wallet/Wallet'; import RestoreWalletPage, { RestoreWalletPagePromise } from './containers/wallet/restore/RestoreWalletPage'; +import PagePreparation from './components/page-preparation/PagePreparation'; // New UI pages // $FlowIgnore: suppressing this error import { createCurrrentWalletInfo } from './UI/features/governace/common/helpers'; @@ -376,6 +377,7 @@ const SwapSubpages = (stores, actions) => ( } /> } /> + } /> ); diff --git a/packages/yoroi-extension/app/UI/components/Collapsible/Collapsible.tsx b/packages/yoroi-extension/app/UI/components/Collapsible/Collapsible.tsx index 5c3ff3cb03..f587de6765 100644 --- a/packages/yoroi-extension/app/UI/components/Collapsible/Collapsible.tsx +++ b/packages/yoroi-extension/app/UI/components/Collapsible/Collapsible.tsx @@ -1,4 +1,3 @@ -import { IconButton } from '@mui/material'; import MuiAccordion from '@mui/material/Accordion'; import MuiAccordionDetails from '@mui/material/AccordionDetails'; import MuiAccordionSummary from '@mui/material/AccordionSummary'; @@ -6,8 +5,9 @@ import Typography from '@mui/material/Typography'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Icon } from '../icons/index'; +import { IconButtonWrapper } from '../wrappers/IconButtonWrapper'; -const Accordion: any = styled(MuiAccordion)(() => ({ +const Accordion: any = styled(MuiAccordion)(({ theme }: any) => ({ '&:not(:last-child)': { borderBottom: 0, }, @@ -17,7 +17,7 @@ const Accordion: any = styled(MuiAccordion)(() => ({ '& .MuiAccordionSummary-root': { margin: '0px', padding: '0px', - backgroundColor: 'transparent', + backgroundColor: theme.palette.ds.bg_color_max, minHeight: '24px', height: '24px', }, @@ -29,9 +29,10 @@ const AccordionSummary = styled(MuiAccordionSummary)(() => ({ }, })); -const AccordionDetails = styled(MuiAccordionDetails)(() => ({ +const AccordionDetails = styled(MuiAccordionDetails)(({ theme }: any) => ({ padding: 0, paddingTop: '16px', + backgroundColor: theme.palette.ds.bg_color_max, })); type Props = { @@ -52,9 +53,9 @@ export const Collapsible = ({ title, content }: Props) => { aria-controls="panel1d-content" id="panel1d-header" expandIcon={ - + - + } > diff --git a/packages/yoroi-extension/app/UI/components/Input/PasswordInput.tsx b/packages/yoroi-extension/app/UI/components/Input/PasswordInput.tsx index 257a46e3bc..1bae2f01ae 100644 --- a/packages/yoroi-extension/app/UI/components/Input/PasswordInput.tsx +++ b/packages/yoroi-extension/app/UI/components/Input/PasswordInput.tsx @@ -3,11 +3,11 @@ import React from 'react'; import FormControl from '@mui/material/FormControl'; import FormHelperText from '@mui/material/FormHelperText'; -import IconButton from '@mui/material/IconButton'; import InputAdornment from '@mui/material/InputAdornment'; import InputLabel from '@mui/material/InputLabel'; import OutlinedInput from '@mui/material/OutlinedInput'; import { Icon } from '../icons/index'; +import { IconButtonWrapper } from '../wrappers/IconButtonWrapper'; type StyledInputProps = { id: string; @@ -43,14 +43,14 @@ export const PasswordInput = ({ id, label, onChange, value, error, disabled, hel disabled={disabled} endAdornment={ - {!showPassword ? : } - + } label={label} diff --git a/packages/yoroi-extension/app/UI/components/TransactionSubmitted/TransactionSubmitted.tsx b/packages/yoroi-extension/app/UI/components/TransactionSubmitted/TransactionSubmitted.tsx index a1ba401ee8..cdeb66461c 100644 --- a/packages/yoroi-extension/app/UI/components/TransactionSubmitted/TransactionSubmitted.tsx +++ b/packages/yoroi-extension/app/UI/components/TransactionSubmitted/TransactionSubmitted.tsx @@ -29,12 +29,12 @@ export const TransactionSubmitted = ({ {title ? title : } {subtitle && ( - + {subtitle} )} - + {content ? content : } + + ); + } +} diff --git a/packages/yoroi-extension/app/components/settings/categories/general-setting/ThemeSettingsBlock.js b/packages/yoroi-extension/app/components/settings/categories/general-setting/ThemeSettingsBlock.js index e10112d8c3..0051dc6b8f 100644 --- a/packages/yoroi-extension/app/components/settings/categories/general-setting/ThemeSettingsBlock.js +++ b/packages/yoroi-extension/app/components/settings/categories/general-setting/ThemeSettingsBlock.js @@ -165,7 +165,7 @@ export default class ThemeSettingsBlock extends Component { */} {currentTheme === THEMES.YOROI_BASE && environment.isDev() && ( - + )} {currentTheme !== THEMES.YOROI_BASE && ( @@ -183,19 +183,10 @@ export default class ThemeSettingsBlock extends Component { > {intl.formatMessage(messages.selectColorTheme)} - + - + @@ -219,21 +210,11 @@ export default class ThemeSettingsBlock extends Component { - } - label="Modern" - /> + } label="Modern" /> - + - } - label="classic" - /> + } label="classic" /> diff --git a/packages/yoroi-extension/app/components/settings/themeToggler/index.js b/packages/yoroi-extension/app/components/settings/themeToggler/index.js index a7c0de361a..65a4d4c3cb 100644 --- a/packages/yoroi-extension/app/components/settings/themeToggler/index.js +++ b/packages/yoroi-extension/app/components/settings/themeToggler/index.js @@ -4,8 +4,21 @@ import { Box, FormControlLabel, Radio, RadioGroup, useTheme } from '@mui/materia import type { Node } from 'react'; import { useThemeMode } from '../../../styles/context/mode'; import LocalStorageApi from '../../../api/localStorage'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import { defineMessages } from 'react-intl'; -const ThemeToggler = (): Node => { +const messages = defineMessages({ + lightTheme: { + id: 'settings.general.theme.light', + defaultMessage: '!!!Light Theme', + }, + darkTheme: { + id: 'settings.general.theme.dark', + defaultMessage: '!!!Dark Theme', + }, +}); + +const ThemeToggler = ({ intl }: {| intl: $npm$ReactIntl$IntlFormat |}): Node => { const { toggleColorMode } = useThemeMode(); const localStorageApi = new LocalStorageApi(); const { name } = useTheme(); @@ -35,13 +48,13 @@ const ThemeToggler = (): Node => { size="small" /> } - label={'Light Theme'} + label={intl.formatMessage(messages.lightTheme)} id="switchToNewVersionButton" /> } - label={'Dark Theme'} + label={intl.formatMessage(messages.darkTheme)} id="switchToOldVersionButton" sx={{ marginRight: '20px', diff --git a/packages/yoroi-extension/app/components/swap/PriceImpact.js b/packages/yoroi-extension/app/components/swap/PriceImpact.js index 3810504993..53374fb5e0 100644 --- a/packages/yoroi-extension/app/components/swap/PriceImpact.js +++ b/packages/yoroi-extension/app/components/swap/PriceImpact.js @@ -1,5 +1,5 @@ // @flow -import { Box, Button, Typography, useTheme } from '@mui/material'; +import { Box, Button, Typography, useTheme, styled } from '@mui/material'; import { useSwap } from '@yoroi/swap'; import type { Node } from 'react'; import { ReactComponent as ErrorTriangleIcon } from '../../assets/images/revamp/error.triangle.svg'; @@ -15,7 +15,7 @@ function colorsBySeverity(isSevere: boolean) { const theme = useTheme(); return isSevere ? { fg: theme.palette.ds.text_error, bg: theme.palette.ds.sys_magenta_100 } - : { fg: theme.palette.ds.sys_yellow_500, bg: theme.palette.ds.sys_yellow_100 }; + : { fg: theme.palette.ds.sys_orange_500, bg: theme.palette.ds.sys_yellow_100 }; } export function PriceImpactColored({ @@ -50,11 +50,27 @@ export function PriceImpactIcon({ isSevere, small }: {| isSevere: boolean, small marginLeft: '-3px', }} > - {isSevere ? : } + {isSevere ? ( + + + + ) : ( + + + + )} ); } +const IconWrapper = styled(Box)(({ theme, isSevere }) => ({ + '& svg': { + '& path': { + fill: isSevere ? theme.palette.ds.sys_magenta_500 : theme.palette.ds.sys_orange_500, + }, + }, +})); + function PriceImpactWarningText({ isSevere }: {| isSevere: boolean |}): Node { const { palette } = useTheme(); return isSevere ? ( diff --git a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js index 0498a70061..0dc60487f8 100644 --- a/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js +++ b/packages/yoroi-extension/app/components/swap/SelectAssetDialog.js @@ -1,9 +1,10 @@ // @flow -import { Box, Typography } from '@mui/material'; +import { Box, Typography, useTheme } from '@mui/material'; import { useEffect, useState } from 'react'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; import adaTokenImage from '../../assets/images/ada.inline.svg'; import defaultTokenImage from '../../assets/images/revamp/asset-default.inline.svg'; +import defaultTokenDarkImage from '../../assets/images/revamp/asset-default-dark.inline.svg'; import { ReactComponent as ArrowBottomIcon } from '../../assets/images/revamp/icons/arrow-bottom.inline.svg'; import { ReactComponent as ArrowTopIcon } from '../../assets/images/revamp/icons/arrow-top.inline.svg'; import { ReactComponent as SearchIcon } from '../../assets/images/revamp/icons/search.inline.svg'; @@ -15,6 +16,7 @@ import Dialog from '../widgets/Dialog'; import { InfoTooltip } from '../widgets/InfoTooltip'; import { PriceImpactColored, PriceImpactIcon } from './PriceImpact'; import type { AssetAmount, PriceImpact } from './types'; +import LoadingSpinner from '../widgets/LoadingSpinner'; const fromTemplateColumns = '1fr minmax(auto, 136px)'; const toTemplateColumns = '1fr minmax(auto, 152px) minmax(auto, 136px)'; @@ -23,6 +25,7 @@ const toColumns = []; type Props = {| assets: Array, + assetsStillLoading: boolean, type: 'from' | 'to', onAssetSelected: any => void, onClose: void => void, @@ -32,6 +35,7 @@ type Props = {| export default function SelectAssetDialog({ assets = [], + assetsStillLoading, type, onAssetSelected, onClose, @@ -107,14 +111,14 @@ export default function SelectAssetDialog({ /> - + {filteredAssets.length} assets {searchTerm ? 'found' : 'available'} } > - {filteredAssets.length !== 0 && ( + {(filteredAssets.length !== 0 && !assetsStillLoading) && ( )} - {filteredAssets.length === 0 && ( + {(filteredAssets.length === 0 || assetsStillLoading) && ( - + {assetsStillLoading ? ( + + ) : ( + + )} {type === 'from' ? `No tokens found for “${searchTerm}”` : 'No asset was found to swap'} @@ -176,6 +184,8 @@ export const AssetAndAmountRow = ({ priceImpactState?: ?PriceImpact, |}): React$Node => { const [remoteTokenLogo, setRemoteTokenLogo] = useState(null); + const theme = useTheme(); + const defaultImage = theme.name === 'dark-theme' ? defaultTokenDarkImage : defaultTokenImage; const isFrom = type === 'from'; @@ -199,7 +209,7 @@ export const AssetAndAmountRow = ({ } }, [id]); - const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage : remoteTokenLogo ?? defaultTokenImage; + const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage : remoteTokenLogo ?? defaultImage; const amount = displayAmount ?? assetAmount; @@ -245,7 +255,7 @@ export const AssetAndAmountRow = ({ src={imgSrc} alt={name} onError={e => { - e.target.src = defaultTokenImage; + e.target.src = defaultImage; }} /> diff --git a/packages/yoroi-extension/app/components/swap/SlippageDialog.js b/packages/yoroi-extension/app/components/swap/SlippageDialog.js index 505045b671..e14eef8155 100644 --- a/packages/yoroi-extension/app/components/swap/SlippageDialog.js +++ b/packages/yoroi-extension/app/components/swap/SlippageDialog.js @@ -1,8 +1,8 @@ // @flow -import { useState } from 'react'; import { Box, Button, Typography } from '@mui/material'; -import Dialog from '../widgets/Dialog'; +import { useState } from 'react'; import Tabs from '../common/tabs/Tabs'; +import Dialog from '../widgets/Dialog'; const defaultSlippages = ['0', '0.1', '0.5', '1', '2', '3', '5', '10']; @@ -56,7 +56,7 @@ export default function SlippageDialog({ onSetNewSlippage, onClose, slippageValu - + Slippage tolerance is set as a percentage of the total swap value. Your transactions will not be executed if the price moves by more than this amount. diff --git a/packages/yoroi-extension/app/components/swap/SwapInput.js b/packages/yoroi-extension/app/components/swap/SwapInput.js index ea426f692e..c56c641182 100644 --- a/packages/yoroi-extension/app/components/swap/SwapInput.js +++ b/packages/yoroi-extension/app/components/swap/SwapInput.js @@ -1,10 +1,11 @@ // @flow -import { Box, Typography } from '@mui/material'; +import { Box, Typography, useTheme } from '@mui/material'; import type { Node } from 'react'; import { useEffect, useState } from 'react'; import adaTokenImage from '../../assets/images/ada.inline.svg'; import { ReactComponent as ChevronDownIcon } from '../../assets/images/revamp/icons/chevron-down.inline.svg'; import defaultTokenImage from '../../assets/images/revamp/token-default.inline.svg'; +import defaultTokenDarkImage from '../../assets/images/revamp/asset-default-dark.inline.svg'; import type { AssetAmount } from './types'; import type { RemoteTokenInfo } from '../../api/ada/lib/state-fetch/types'; import type { State } from '../../containers/swap/context/swap-form/types'; @@ -38,6 +39,8 @@ export default function SwapInput({ }: Props): Node { const [remoteTokenLogo, setRemoteTokenLogo] = useState(null); const { id, amount: quantity = undefined, ticker } = tokenInfo || {}; + const { name } = useTheme(); + console.log('name', name); const handleChange = e => { if (!disabled) { @@ -62,7 +65,8 @@ export default function SwapInput({ } }, [id]); - const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage : remoteTokenLogo ?? defaultTokenImage; + const defaultImage = name === 'dark-theme' ? defaultTokenDarkImage : defaultTokenImage; + const imgSrc = ticker === defaultTokenInfo.ticker ? adaTokenImage : remoteTokenLogo ?? defaultImage; return ( @@ -72,7 +76,7 @@ export default function SwapInput({ sx={{ borderStyle: 'solid', borderWidth: (tokenInfo.id?.length > 0 && error) || focusState.value ? '2px' : '1px', - borderColor: error ? 'magenta.500' : isFocusedColor, + borderColor: error ? 'ds.sys_magenta_500' : isFocusedColor, borderRadius: '8px', p: '16px', pr: '8px', @@ -138,7 +142,7 @@ export default function SwapInput({ src={imgSrc} alt="" onError={e => { - e.target.src = defaultTokenImage; + e.target.src = defaultImage; }} /> @@ -181,7 +185,7 @@ export default function SwapInput({ {error && ( - + {error} )} diff --git a/packages/yoroi-extension/app/components/topbar/WalletListDialog.js b/packages/yoroi-extension/app/components/topbar/WalletListDialog.js index 580034cd94..ad49c99685 100644 --- a/packages/yoroi-extension/app/components/topbar/WalletListDialog.js +++ b/packages/yoroi-extension/app/components/topbar/WalletListDialog.js @@ -19,7 +19,7 @@ import DialogCloseButton from '../widgets/DialogCloseButton'; import styles from './WalletListDialog.scss'; import WalletCard from './WalletCard'; import globalMessages from '../../i18n/global-messages'; -import AmountDisplay, { FiatDisplay } from '../common/AmountDisplay'; +import AmountDisplay from '../common/AmountDisplay'; import type { WalletType } from '../../../chrome/extension/background/types'; import type { WalletChecksum } from '@emurgo/cip4-js'; import { Typography, styled } from '@mui/material'; @@ -37,10 +37,6 @@ const messages = defineMessages({ id: 'wallet.topbar.dialog.totalBalance', defaultMessage: '!!!Total Balance', }, - cardano: { - id: 'wallet.topbar.dialog.cardano', - defaultMessage: '!!!Cardano, ADA', - }, }); export type WalletInfo = {| @@ -242,11 +238,6 @@ export default class WalletListDialog extends Component { sx={{ overflow: 'auto', overflowY: 'auto', height: '400px' }} id="changeWalletDialog-walletList-box" > - {cardanoWalletsIdx.length > 0 && ( -
-

{intl.formatMessage(messages.cardano)}

-
- )} this.onDragEnd('cardano', result)}> {provided => ( @@ -293,27 +284,16 @@ export default class WalletListDialog extends Component { renderWalletsTotal(): ?Node { const { unitOfAccountSetting, cardanoWallets, shouldHideBalance, getCurrentPrice } = this.props; - if (unitOfAccountSetting.enabled) { - const adaFiat = this.sumWallets(cardanoWallets).fiat; - if (adaFiat != null) { - const totalFiat = adaFiat; - const { currency } = unitOfAccountSetting; - return ; - } - } - // either unit of account is not enabled, or fails to convert to fiat - const amount = this.sumWallets(cardanoWallets).sum; - const totalAmountId = `changeWalletDialog:total`; return ( ); } diff --git a/packages/yoroi-extension/app/components/uri/URIDisplayDialog.js b/packages/yoroi-extension/app/components/uri/URIDisplayDialog.js index 83d09b0c04..5d94bbf2a4 100644 --- a/packages/yoroi-extension/app/components/uri/URIDisplayDialog.js +++ b/packages/yoroi-extension/app/components/uri/URIDisplayDialog.js @@ -7,7 +7,7 @@ import { observer } from 'mobx-react'; import { intlShape, defineMessages } from 'react-intl'; import { buildURI } from '../../utils/URIHandling'; import { ReactComponent as InfoIcon } from '../../assets/images/revamp/icons/info.inline.svg'; -import { Box, Typography } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import classnames from 'classnames'; import Dialog from '../widgets/Dialog'; import DialogBackButton from '../widgets/DialogBackButton'; @@ -18,6 +18,13 @@ import BigNumber from 'bignumber.js'; import styles from './URIDisplayDialog.scss'; import globalMessages from '../../i18n/global-messages'; +const WarningBox = styled(Box)(({ theme }) => ({ + padding: '16px', + paddingTop: '12px', + borderRadius: '8px', + background: theme.palette.ds.bg_gradient_1, +})); + const messages = defineMessages({ uriDisplayDialogTitle: { id: 'uri.display.dialog.title', @@ -29,8 +36,7 @@ const messages = defineMessages({ }, usabilityWarning: { id: 'uri.display.dialog.usabilityWarning', - defaultMessage: - '!!!This link can only be opened by users with Yoroi installed on their browser', + defaultMessage: '!!!This link can only be opened by users with Yoroi installed on their browser', }, }); @@ -65,41 +71,35 @@ export default class URIDisplayDialog extends Component { closeButton={} onClose={onClose} backButton={} - id='uriDisplayDialog' + id="uriDisplayDialog" > - + - + {intl.formatMessage(globalMessages.important)} - + {intl.formatMessage(messages.usabilityWarning)} - +
- +
onCopyAddressTooltip(uriNotificationId)} notification={notification} placementTooltip="bottom-start" sx={{ + color: 'ds.text_gray_medium', alignItems: 'flex-start', '& > .CopyableAddress_copyIconBig': { p: '6px', diff --git a/packages/yoroi-extension/app/components/uri/URIGenerateDialog.js b/packages/yoroi-extension/app/components/uri/URIGenerateDialog.js index ccd1930b80..684a66cdac 100644 --- a/packages/yoroi-extension/app/components/uri/URIGenerateDialog.js +++ b/packages/yoroi-extension/app/components/uri/URIGenerateDialog.js @@ -12,11 +12,7 @@ import NumericInputRP from '../common/NumericInputRP'; import globalMessages from '../../i18n/global-messages'; import type { TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import TextField from '../common/TextField'; -import { - formattedAmountToNaturalUnits, - formattedAmountToBigNumber, - truncateToken, -} from '../../utils/formatters'; +import { formattedAmountToNaturalUnits, formattedAmountToBigNumber, truncateToken } from '../../utils/formatters'; import config from '../../config'; import { getTokenName } from '../../stores/stateless/tokenHelpers'; import BigNumber from 'bignumber.js'; @@ -50,10 +46,7 @@ type Props = {| +classicTheme: boolean, +walletAddress: string, +amount: ?BigNumber, - +validateAmount: ( - amountInNaturalUnits: BigNumber, - tokenRow: $ReadOnly - ) => Promise<[boolean, void | string]>, + +validateAmount: (amountInNaturalUnits: BigNumber, tokenRow: $ReadOnly) => Promise<[boolean, void | string]>, +tokenInfo: $ReadOnly, |}; @@ -90,10 +83,7 @@ export default class URIGenerateDialog extends Component { return [false, this.context.intl.formatMessage(globalMessages.fieldIsRequired)]; } const formattedAmount = new BigNumber( - formattedAmountToNaturalUnits( - amountValue, - this.props.tokenInfo.Metadata.numberOfDecimals - ) + formattedAmountToNaturalUnits(amountValue, this.props.tokenInfo.Metadata.numberOfDecimals) ); return await this.props.validateAmount(formattedAmount, this.props.tokenInfo); }, @@ -158,11 +148,7 @@ export default class URIGenerateDialog extends Component { ({ '& svg': { '& path': { - fill: theme.palette.ds.el_gray_low, + fill: theme.palette.ds.text_gray_medium, }, }, })); @@ -201,7 +201,7 @@ export default class WalletReceiveRevamp extends Component { linkType={address.type === CoreAddressTypes.CARDANO_REWARD ? 'stakeAddress' : 'address'} > - + {truncateAddressShort(address.address, 16)} diff --git a/packages/yoroi-extension/app/components/wallet/assets/NFTDetails.js b/packages/yoroi-extension/app/components/wallet/assets/NFTDetails.js index b83a481d35..7a73eccb15 100644 --- a/packages/yoroi-extension/app/components/wallet/assets/NFTDetails.js +++ b/packages/yoroi-extension/app/components/wallet/assets/NFTDetails.js @@ -190,10 +190,11 @@ function NFTDetails({ nftInfo, network, intl, nextNftId, prevNftId, tab }: Props objectFit: 'unset', }, backgroundColor: 'ds.bg_color_max', + height: '100%', }} onClick={() => nftImage !== null && setOpenAndTrack()} > - + diff --git a/packages/yoroi-extension/app/components/wallet/assets/NFTsList.js b/packages/yoroi-extension/app/components/wallet/assets/NFTsList.js index 88039cd147..329b9ff1f4 100644 --- a/packages/yoroi-extension/app/components/wallet/assets/NFTsList.js +++ b/packages/yoroi-extension/app/components/wallet/assets/NFTsList.js @@ -206,13 +206,11 @@ export function NftImage({ name, width, height, - contentHeight, }: {| imageUrl: ?string, name: string, width: string, height: string, - contentHeight?: string, |}): Node { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -235,7 +233,7 @@ export function NftImage({ if (error || url === null) return ( - + ); @@ -266,7 +264,7 @@ function NftCardImage({ ipfsUrl, name }: {| ipfsUrl: string | null, name: string return ( - + ({ + background: theme.palette.ds.bg_gradient_1, +})); + const messages = defineMessages({ walletAddressLabel: { id: 'wallet.receive.page.walletAddressLabel', @@ -90,14 +94,7 @@ export default class StandardHeaderRevamp extends Component { - theme.palette.gradients['blue-green-bg'], - }} - > + { > - + @@ -149,7 +146,7 @@ export default class StandardHeaderRevamp extends Component { - + diff --git a/packages/yoroi-extension/app/components/wallet/send/MaxAssetsError.js b/packages/yoroi-extension/app/components/wallet/send/MaxAssetsError.js index 7aac44ab87..276242e03d 100644 --- a/packages/yoroi-extension/app/components/wallet/send/MaxAssetsError.js +++ b/packages/yoroi-extension/app/components/wallet/send/MaxAssetsError.js @@ -2,16 +2,16 @@ import { Component } from 'react'; import type { Node } from 'react'; import { Typography } from '@mui/material'; -import { Box } from '@mui/system'; +import { Box, Stack } from '@mui/system'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import globalMessages from '../../../i18n/global-messages'; +import { ReactComponent as AttentionIcon } from '../../../assets/images/attention-modern.inline.svg'; const messages = defineMessages({ maxNumberAllowed: { id: 'wallet.send.form.dialog.maxNumberAllowed', - defaultMessage: - '!!!{number} Assets is maximum number allowed to be send in one transaction', + defaultMessage: '!!!{number} Assets is maximum number allowed to be send in one transaction', }, }); @@ -28,15 +28,15 @@ export default class MaxAssetsError extends Component { const { intl } = this.context; return ( - - - {intl.formatMessage(globalMessages.errorLabel)} - + + + + + {intl.formatMessage(globalMessages.errorLabel)} + + - + ); diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js index dabf96c57b..4dc7005b84 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormRevamp.js @@ -629,7 +629,7 @@ export default class WalletSendFormRevamp extends Component { @@ -663,7 +663,7 @@ export default class WalletSendFormRevamp extends Component { {memo ? memo.length : 0}/{MAX_MEMO_SIZE} @@ -696,7 +696,7 @@ export default class WalletSendFormRevamp extends Component { variant="caption1" sx={{ position: 'absolute', - color: 'magenta.500', + color: 'ds.sys_magenta_500', left: '50%', top: '-14px', transform: 'translateX(-50%)', @@ -715,12 +715,12 @@ export default class WalletSendFormRevamp extends Component { ? { borderWidth: '2px', borderStyle: 'solid', - borderColor: 'magenta.500', + borderColor: 'ds.sys_magenta_500', } : { borderWidth: '1px', borderStyle: 'solid', - borderColor: 'grey.400', + borderColor: 'ds.el_gray_min', }), }} > @@ -732,7 +732,7 @@ export default class WalletSendFormRevamp extends Component { left: '6px', backgroundColor: 'ds.bg_color_max', paddingX: '4px', - color: shouldSendAll && 'grayscale.200', + color: 'ds.text_gray_medium', fontWeight: 400, fontSize: '12px', lineHeight: '16px', @@ -794,6 +794,9 @@ export default class WalletSendFormRevamp extends Component { sx={{ '&.MuiButton-sizeSmall': { lineHeight: '17px', + padding: '8px', + backgroundColor: 'ds.gray_100', + color: 'ds.text_gray_low', }, }} disabled={maxSendableAmount.isExecuting} @@ -823,11 +826,13 @@ export default class WalletSendFormRevamp extends Component { {this.renderUnitOfAccountAmount(amountFieldProps.value)} @@ -840,7 +845,7 @@ export default class WalletSendFormRevamp extends Component { position: 'absolute', bottom: '-25px', left: '17px', - color: 'magenta.500', + color: 'ds.text_error', fontSize: '12px', }} id="wallet:send:addAssetsStep-amountError-text" @@ -912,7 +917,7 @@ export default class WalletSendFormRevamp extends Component { getCurrentPrice={this.props.getCurrentPrice} isClassicTheme={this.props.isClassicTheme} ledgerSendError={this.props.ledgerSendError} - trezorSendError={this.props.ledgerSendError} + trezorSendError={this.props.trezorSendError} ledgerSend={this.props.ledgerSend} trezorSend={this.props.trezorSend} selectedExplorer={this.props.selectedExplorer} diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/AddNFTDialog.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/AddNFTDialog.js index 7064e2a649..8a5c775f0b 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/AddNFTDialog.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/AddNFTDialog.js @@ -4,10 +4,7 @@ import type { Node } from 'react'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import type { FormattedNFTDisplay } from '../../../../utils/wallet'; import type { TokenLookupKey } from '../../../../api/common/lib/MultiToken'; -import type { - TokenRow, - NetworkRow, -} from '../../../../api/ada/lib/storage/database/primitives/tables'; +import type { TokenRow, NetworkRow } from '../../../../api/ada/lib/storage/database/primitives/tables'; import { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; @@ -98,9 +95,7 @@ export default class AddNFTDialog extends Component { componentDidMount(): void { const { spendableBalance, getTokenInfo, plannedTxInfoMap } = this.props; const nftsList = getNFTs(spendableBalance, getTokenInfo); - const selectedTokens = plannedTxInfoMap - .filter(({ token }) => token.IsNFT) - .map(({ token }) => ({ token, included: true })); + const selectedTokens = plannedTxInfoMap.filter(({ token }) => token.IsNFT).map(({ token }) => ({ token, included: true })); this.setState({ fullNftsList: nftsList, currentNftsList: nftsList, selectedTokens }); } @@ -111,9 +106,7 @@ export default class AddNFTDialog extends Component { selectedTokens: [], }; - search: (e: SyntheticEvent) => void = ( - event: SyntheticEvent - ) => { + search: (e: SyntheticEvent) => void = (event: SyntheticEvent) => { const keyword = event.currentTarget.value; this.setState(prev => ({ currentNftsList: prev.fullNftsList })); if (!keyword) return; @@ -127,23 +120,18 @@ export default class AddNFTDialog extends Component { if (this.isTokenIncluded(token)) { this.onRemoveToken(token); } else { - const selectedTokens = [...this.state.selectedTokens].filter( - ({ token: t }) => t.Identifier !== token.Identifier - ); + const selectedTokens = [...this.state.selectedTokens].filter(({ token: t }) => t.Identifier !== token.Identifier); this.setState({ selectedTokens: [...selectedTokens, { token, included: true }] }); } }; onRemoveToken: ($ReadOnly) => void = token => { - const filteredTokens = [...this.state.selectedTokens].filter( - ({ token: t }) => t.Identifier !== token.Identifier - ); + const filteredTokens = [...this.state.selectedTokens].filter(({ token: t }) => t.Identifier !== token.Identifier); this.setState({ selectedTokens: [...filteredTokens, { token, included: false }] }); }; isTokenIncluded: ($ReadOnly) => boolean = token => { - return !!this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier) - ?.included; + return !!this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier)?.included; }; onAddAll: void => void = () => { @@ -200,10 +188,7 @@ export default class AddNFTDialog extends Component { scrollableContentClass={styles.nftsGrid} actions={[ { - disabled: - hasSelectedTokensIncluded.length === 0 || - !shouldAddMore || - currentNftsList.length === 0, + disabled: hasSelectedTokensIncluded.length === 0 || !shouldAddMore || currentNftsList.length === 0, onClick: this.onAddAll, primary: true, label: intl.formatMessage(globalMessages.confirm), @@ -212,9 +197,7 @@ export default class AddNFTDialog extends Component { >
- + {' '} {' '} @@ -245,10 +228,7 @@ export default class AddNFTDialog extends Component {
- {intl.formatMessage( - fullNftsList.length === 0 ? messages.noNFTsYet : messages.noNFTsFound - )} - + {intl.formatMessage(fullNftsList.length === 0 ? messages.noNFTsYet : messages.noNFTsFound)}
) : ( @@ -278,15 +258,11 @@ export default class AddNFTDialog extends Component { justifyContent: 'flex-start', }} > - - + ({ + '& svg': { + '& path': { + fill: theme.palette.ds.el_gray_medium, + }, + }, +})); + type Props = {| +onClose: void => void, +spendableBalance: ?MultiToken, @@ -139,18 +143,14 @@ export default class AddTokenDialog extends Component { onSelect: ($ReadOnly) => void = token => { // Remove if it already in the list - const selectedTokens = this.state.selectedTokens.filter( - ({ token: t }) => t.Identifier !== token.Identifier - ); + const selectedTokens = this.state.selectedTokens.filter(({ token: t }) => t.Identifier !== token.Identifier); this.setState({ selectedTokens: [...selectedTokens, { token, included: true, amount: null }] }); }; onRemoveToken: ($ReadOnly) => void = token => { const tokenEntry = this.getSelectedToken(token); if (!tokenEntry) return; - const selectedTokens = [...this.state.selectedTokens].filter( - ({ token: t }) => t.Identifier !== token.Identifier - ); + const selectedTokens = [...this.state.selectedTokens].filter(({ token: t }) => t.Identifier !== token.Identifier); this.setState({ selectedTokens: [...selectedTokens, { token, included: false, amount: null }], @@ -158,14 +158,11 @@ export default class AddTokenDialog extends Component { }; isTokenIncluded: ($ReadOnly) => boolean = token => { - return !!this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier) - ?.included; + return !!this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier)?.included; }; updateAmount: ($ReadOnly, BigNumber | null) => void = (token, amount) => { - const filteredTokens = this.state.selectedTokens.filter( - ({ token: t }) => t.Identifier !== token.Identifier - ); + const filteredTokens = this.state.selectedTokens.filter(({ token: t }) => t.Identifier !== token.Identifier); this.setState({ selectedTokens: [...filteredTokens, { token, amount, included: true }] }); }; @@ -182,9 +179,7 @@ export default class AddTokenDialog extends Component { amount: BigNumber | null, included: boolean, |} | null = token => { - return ( - this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier) ?? null - ); + return this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier) ?? null; }; onAddAll: void => void = () => { @@ -232,21 +227,15 @@ export default class AddTokenDialog extends Component { }; getMaxAmount: ($ReadOnly) => BigNumber = tokenInfo => { - const token = this.state.fullTokensList.find( - entry => entry.info.Identifier === tokenInfo.Identifier - ); + const token = this.state.fullTokensList.find(entry => entry.info.Identifier === tokenInfo.Identifier); if (!token) throw new Error('Token not found.'); - const amount = new BigNumber( - formattedAmountToNaturalUnits(token.amount ?? '0', token.info.Metadata.numberOfDecimals) - ); + const amount = new BigNumber(formattedAmountToNaturalUnits(token.amount ?? '0', token.info.Metadata.numberOfDecimals)); return amount; }; isValidAmount: ($ReadOnly) => boolean = token => { - const tokenEntry = this.state.selectedTokens.find( - ({ token: t }) => t.Identifier === token.Identifier - ); + const tokenEntry = this.state.selectedTokens.find(({ token: t }) => t.Identifier === token.Identifier); if (tokenEntry && tokenEntry.included) { // Should not show any error if no amount entered // Will disable the `ADD` button only @@ -262,28 +251,19 @@ export default class AddTokenDialog extends Component { isValidAmounts: void => boolean = () => { for (const tokenEntry of this.state.selectedTokens) { if (!tokenEntry.included) continue; - if ( - !this.isValidAmount(tokenEntry.token) || - !tokenEntry.amount || - Number(tokenEntry.amount) === 0 - ) - return false; + if (!this.isValidAmount(tokenEntry.token) || !tokenEntry.amount || Number(tokenEntry.amount) === 0) return false; } return true; }; - search: (e: SyntheticEvent) => void = ( - event: SyntheticEvent - ) => { + search: (e: SyntheticEvent) => void = (event: SyntheticEvent) => { const keyword = event.currentTarget.value; this.setState(prev => ({ currentTokensList: prev.fullTokensList })); if (!keyword) return; const regExp = new RegExp(keyword, 'gi'); const tokensListCopy = [...this.state.fullTokensList]; - const filteredTokensList = tokensListCopy.filter( - a => a.label.match(regExp) || a.id.match(regExp) - ); + const filteredTokensList = tokensListCopy.filter(a => a.label.match(regExp) || a.id.match(regExp)); this.setState({ currentTokensList: filteredTokensList }); }; @@ -328,25 +308,16 @@ export default class AddTokenDialog extends Component { const { intl } = this.context; const { onClose, calculateMinAda, shouldAddMoreTokens } = this.props; const { currentTokensList, fullTokensList, selectedTokens } = this.state; - const shouldAddMore = shouldAddMoreTokens( - selectedTokens.map(({ token, included }) => ({ token, included })) - ); + const shouldAddMore = shouldAddMoreTokens(selectedTokens.map(({ token, included }) => ({ token, included }))); const hasSelectedTokensIncluded = selectedTokens.filter(t => t.included); return ( { transform: 'translateY(-50%)', }} > - {' '} - {' '} + + +
{
{isCardanoHaskell(this.props.selectedNetwork) && ( - ({ token, included })) - )} - /> + ({ token, included })))} /> )} {!shouldAddMore && ( @@ -402,20 +370,13 @@ export default class AddTokenDialog extends Component { {currentTokensList.length === 0 ? (
-

- {intl.formatMessage( - fullTokensList.length === 0 ? messages.noTokensYet : messages.noTokensFound - )} -

+ + {intl.formatMessage(fullTokensList.length === 0 ? messages.noTokensYet : messages.noTokensFound)} +
) : ( - +
  • ); }); @@ -94,16 +104,20 @@ export default class AssetsDropdown extends Component {
    {tokens.length > 0 && (
    - {isTokensOpen && ( @@ -118,16 +132,22 @@ export default class AssetsDropdown extends Component { {nfts.length > 0 && (
    - diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/IncludedTokens.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/IncludedTokens.js index 3681729ecd..da86c63965 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/IncludedTokens.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/IncludedTokens.js @@ -9,10 +9,21 @@ import globalMessages from '../../../../i18n/global-messages'; import { ReactComponent as RemoveIcon } from '../../../../assets/images/forms/close-small.inline.svg'; import type { TokenRow } from '../../../../api/ada/lib/storage/database/primitives/tables'; import NFTImage from './NFTImage'; -import { Box, Typography } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import { splitAmount } from '../../../../utils/formatters'; import BigNumber from 'bignumber.js'; +const IconWrapper = styled(Box)(({ theme }) => ({ + '& svg': { + '& path': { + fill: theme.palette.ds.el_gray_medium, + }, + '& rect': { + fill: theme.palette.ds.bg_color_max, + }, + }, +})); + type Props = {| +shouldSendAll: boolean, +onRemoveTokens: (Array<$ReadOnly>) => void, @@ -27,9 +38,7 @@ export default class IncludedTokens extends Component { renderItems(items: FormattedNFTDisplay[] | FormattedTokenDisplay[]): Node { return items.map(item => { const numberOfDecimals = item.info?.Metadata.numberOfDecimals || 0; - const displayAmount = item.amount - ? splitAmount(new BigNumber(item.amount), numberOfDecimals).join('') - : '0'; + const displayAmount = item.amount ? splitAmount(new BigNumber(item.amount), numberOfDecimals).join('') : '0'; return ( { }, }} > - + - {item.name} @@ -105,9 +110,10 @@ export default class IncludedTokens extends Component { }} > - { - + {displayAmount} @@ -134,12 +140,19 @@ export default class IncludedTokens extends Component { > {!this.props.shouldSendAll && ( this.props.onRemoveTokens([item.info])} > - + + + )} @@ -155,12 +168,7 @@ export default class IncludedTokens extends Component { {tokens.length > 0 && ( 0 ? '24px' : '0px'}> - + {intl.formatMessage(globalMessages.tokens)} {this.renderItems(tokens)} @@ -169,19 +177,13 @@ export default class IncludedTokens extends Component { {nfts.length > 0 && ( - + {intl.formatMessage(globalMessages.nfts)} {this.renderItems(nfts)} )} - ); } } diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.js index 3c775a7780..9fd8386054 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.js @@ -17,7 +17,18 @@ import type { FormattedTokenDisplay } from '../../../../utils/wallet'; import type { TokenRow } from '../../../../api/ada/lib/storage/database/primitives/tables'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import classnames from 'classnames'; -import { Box, Typography } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; + +const IconWrapper = styled(Box)(({ theme }) => ({ + '& svg': { + '& path': { + fill: theme.palette.ds.gray_max, + }, + '& rect': { + fill: theme.palette.ds.bg_color_max, + }, + }, +})); type Props = {| +token: FormattedTokenDisplay, @@ -39,12 +50,11 @@ const messages = defineMessages({ defaultMessage: '!!!Not enough balance', }, }); -export default class SingleTokenRow extends Component { +export default class SingleTokenRow extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired, }; - constructor(props: Props) { super(props); // eslint-disable-next-line react/state-in-constructor @@ -59,9 +69,7 @@ export default class SingleTokenRow extends Component { onAmountUpdate(value: string | null): void { const formattedAmount = - value !== null && value !== '' - ? new BigNumber(formattedAmountToNaturalUnits(value, this.getNumDecimals())) - : null; + value !== null && value !== '' ? new BigNumber(formattedAmountToNaturalUnits(value, this.getNumDecimals())) : null; if (formattedAmount && formattedAmount.isNegative()) return; this.props.updateAmount(this.props.token.info, formattedAmount); } @@ -77,83 +85,64 @@ export default class SingleTokenRow extends Component { amount = amount.shiftedBy(-numberOfDecimals).toString(); } - const displayAmount = token.amount - ? splitAmount(new BigNumber(token.amount), numberOfDecimals).join('') - : '0'; + const displayAmount = token.amount ? splitAmount(new BigNumber(token.amount), numberOfDecimals).join('') : '0'; return (
    - {!this.props.isTokenIncluded(token.info) ? ( - - ) : ( - -
    -
    - -
    - - {token.label} - -
    -
    - - {truncateAddressShort(token.id, 14)} - -
    -
    - { - this.setState({ isInputFocused: true }) - }} - onBlur={() => { - this.setState({ isInputFocused: false }) - }} - /> -
    - -
    - {!isValid && intl.formatMessage(messages.notEnoughMoneyToSendError)} -
    -
    - )} +
    + + {truncateAddressShort(token.id, 14)} + + + {this.props.isTokenIncluded(token.info) ? ( + <> + + { + this.setState({ isInputFocused: true }); + }} + onBlur={() => { + this.setState({ isInputFocused: false }); + }} + autoFocus + /> + + +
    {!isValid && intl.formatMessage(messages.notEnoughMoneyToSendError)}
    + + ) : ( + + {displayAmount} + + )} +
    ); } diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.scss b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.scss index fa3f274eb4..c1db028784 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.scss +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/SingleTokenRow.scss @@ -44,12 +44,12 @@ align-items: center; border-radius: 8px; position: relative; - width: calc(100% - 6px); + width: 565px; height: 56px; margin-top: 20px; & > div { - padding: 10px 24px; + padding: 8px 22px; } .error { @@ -79,11 +79,13 @@ margin: 0 0; padding:0; margin-bottom: 6px; + padding-right:10px; input { text-align: right; font-size: 16px; line-height: 24px; width: 100%; + margin-top:-16px; } input::placeholder { @@ -95,8 +97,8 @@ .close { position: absolute; - height: 32px; - width: 32px; + height: 22px; + width: 22px; background-color: #f0f3f5; border-radius: 50%; display: flex; diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js index 5a4538aed8..2841901c8b 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.js @@ -31,8 +31,8 @@ import config from '../../../../config'; import WarningBox from '../../../widgets/WarningBox'; import AssetsDropdown from './AssetsDropdown'; import LoadingSpinner from '../../../widgets/LoadingSpinner'; -import ErrorBlock from '../../../widgets/ErrorBlock'; import { SEND_FORM_STEP } from '../../../../types/WalletSendTypes'; +import { ReactComponent as AttentionIcon } from '../../../../assets/images/attention-modern.inline.svg'; const SBox = styled(Box)(({ theme }) => ({ background: theme.palette.ds.bg_gradient_3, @@ -217,21 +217,33 @@ export default class WalletSendPreviewStep extends Component { const { unitOfAccountSetting } = this.props; return unitOfAccountSetting.enabled ? ( - <> -
    - {formatValue(entry)} -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} -
    -
    - {this.convertedToUnitOfAccount(entry, unitOfAccountSetting.currency)} -  {unitOfAccountSetting.currency} -
    - + + + + {formatValue(entry)} + + +  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} + + + + + {this.convertedToUnitOfAccount(entry, unitOfAccountSetting.currency)} + + +  {unitOfAccountSetting.currency} + + + ) : ( -
    - {formatValue(entry)} -  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} -
    + + + {formatValue(entry)} + + +  {truncateToken(getTokenName(this.props.getTokenInfo(entry)))} + + ); }; renderSingleFee: TokenEntry => Node = entry => { @@ -363,22 +375,62 @@ export default class WalletSendPreviewStep extends Component { return globalMessages.confirm; } + renderErrorBanner: (string, Node) => Node = (errorTitle, descriptionNode) => { + return ( + + + + + {errorTitle} + + + + {descriptionNode} + + + ); + }; + renderError(): Node { const { walletType } = this.props; + const { intl } = this.context; if (walletType === 'mnemonic') { const { txError } = this.state; if (txError !== null) { - return
    {txError}
    ; + return this.renderErrorBanner( + 'Transaction error', +
    + The transaction cannot be done due to technical reasons. Try again or + + Ask our support team + +
    + ); } return null; } if (walletType === 'trezor') { const { trezorSendError } = this.props; - return ; + if (trezorSendError !== null) { + return this.renderErrorBanner('Transaction error', intl.formatMessage(trezorSendError)); + } + return null; } if (walletType === 'ledger') { const { ledgerSendError } = this.props; - return ; + if (ledgerSendError !== null) { + return this.renderErrorBanner('Transaction error', intl.formatMessage(ledgerSendError)); + } + return null; } throw new Error('unexpected wallet type'); } @@ -416,7 +468,7 @@ export default class WalletSendPreviewStep extends Component { {receiverHandle ? (
    - + {intl.formatMessage(messages.receiverHandleLabel)} @@ -428,7 +480,7 @@ export default class WalletSendPreviewStep extends Component { color: 'grayscale.900', overflowWrap: 'break-word', }} - id='wallet:send:confrimTransactionStep-receiverHandleInfo-text' + id="wallet:send:confrimTransactionStep-receiverHandleInfo-text" > {receiverHandle.nameServer}: {receiverHandle.handle} @@ -437,7 +489,7 @@ export default class WalletSendPreviewStep extends Component { ) : null}
    - + {intl.formatMessage(messages.receiverLabel)} @@ -446,7 +498,7 @@ export default class WalletSendPreviewStep extends Component { component="div" variant="body1" sx={{ - color: 'grayscale.900', + color: 'ds.text_gray_medium', overflowWrap: 'break-word', }} id="wallet:send:confrimTransactionStep-receiverAddress-text" @@ -457,36 +509,44 @@ export default class WalletSendPreviewStep extends Component {
    -
    {intl.formatMessage(globalMessages.walletSendConfirmationTotalLabel)}
    + + {intl.formatMessage(globalMessages.walletSendConfirmationTotalLabel)} +
    - - - {this.renderTotalAmount(this.props.totalAmount.getDefaultEntry())} - + + {/* */} + {this.renderTotalAmount(this.props.totalAmount.getDefaultEntry())} + {/* */} {amount.nonDefaultEntries().length > 0 && ( -
    + {intl.formatMessage(messages.nAssets, { number: amount.nonDefaultEntries().length, })} -
    + )}
    -
    {intl.formatMessage(globalMessages.transactionFee)}
    -
    + + {intl.formatMessage(globalMessages.transactionFee)} + + {this.renderBundle({ amount: this.props.transactionFee, render: this.renderSingleFee, })} -
    +
    -
    {this._amountLabel()}
    -
    {this.renderDefaultTokenAmount(amount.getDefaultEntry())}
    + + {this._amountLabel()} + + + {this.renderDefaultTokenAmount(amount.getDefaultEntry())} +
    diff --git a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.scss b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.scss index ca99146c3e..d0e626f3ad 100644 --- a/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.scss +++ b/packages/yoroi-extension/app/components/wallet/send/WalletSendFormSteps/WalletSendPreviewStep.scss @@ -14,14 +14,9 @@ .txError { display: flex; flex-flow: column; - align-items: center; justify-content: center; - padding: 16px 0 16px 0; + padding: 12px 16px; border-radius: 8px; - background-color: rgba(255, 19, 81, 0.06); - box-shadow: var(--yoroi-warning-box-bg-shadow); - font-weight: 500; - color: var(--yoroi-palette-error-200); font-size: 16px; font-weight: 500; margin-bottom: 20px; diff --git a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryDialog.js b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryDialog.js index 15c734b5bf..df32c9a3d2 100644 --- a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryDialog.js +++ b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryDialog.js @@ -10,7 +10,7 @@ import Dialog from '../../../widgets/Dialog'; import { injectIntl, defineMessages } from 'react-intl'; import type { $npm$ReactIntl$IntlShape } from 'react-intl'; import { RewardHistoryItem } from './RewardHistoryTab'; -import InvalidURIImg from '../../../../assets/images/uri/invalid-uri.inline.svg'; +import { ReactComponent as InvalidURIImg } from '../../../../assets/images/uri/invalid-uri.inline.svg'; import ErrorBlock from '../../../widgets/ErrorBlock'; import LoadingSpinner from '../../../widgets/LoadingSpinner'; import VerticallyCenteredLayout from '../../../layout/VerticallyCenteredLayout'; diff --git a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryGraph.js b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryGraph.js index 92e606c7e1..715a15164f 100644 --- a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryGraph.js +++ b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryGraph.js @@ -7,7 +7,7 @@ import globalMessages from '../../../../i18n/global-messages'; import type { $npm$ReactIntl$IntlShape } from 'react-intl'; import { getAvatarFromPoolId } from '../utils'; import RewardGraphClean from './RewardGraphClean'; -import InvalidURIImg from '../../../../assets/images/uri/invalid-uri.inline.svg'; +import { ReactComponent as InvalidURIImg } from '../../../../assets/images/uri/invalid-uri.inline.svg'; import ErrorBlock from '../../../widgets/ErrorBlock'; import VerticallyCenteredLayout from '../../../layout/VerticallyCenteredLayout'; import LoadingSpinner from '../../../widgets/LoadingSpinner'; diff --git a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryTab.js b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryTab.js index 63ff190098..c100d25d29 100644 --- a/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryTab.js +++ b/packages/yoroi-extension/app/components/wallet/staking/dashboard-revamp/RewardHistoryTab.js @@ -8,7 +8,7 @@ import globalMessages from '../../../../i18n/global-messages'; import type { $npm$ReactIntl$IntlShape } from 'react-intl'; import { getAvatarFromPoolId, groupByPoolName } from '../utils'; import type { GraphRewardData } from './RewardHistoryDialog'; -import InvalidURIImg from '../../../../assets/images/uri/invalid-uri.inline.svg'; +import { ReactComponent as InvalidURIImg } from '../../../../assets/images/uri/invalid-uri.inline.svg'; import ErrorBlock from '../../../widgets/ErrorBlock'; import VerticallyCenteredLayout from '../../../layout/VerticallyCenteredLayout'; import LoadingSpinner from '../../../widgets/LoadingSpinner'; diff --git a/packages/yoroi-extension/app/components/widgets/DialogBackButton.js b/packages/yoroi-extension/app/components/widgets/DialogBackButton.js index 459c113609..7ce9d0e04a 100644 --- a/packages/yoroi-extension/app/components/widgets/DialogBackButton.js +++ b/packages/yoroi-extension/app/components/widgets/DialogBackButton.js @@ -2,19 +2,27 @@ import { Component } from 'react'; import type { Node } from 'react'; import { observer } from 'mobx-react'; -import { ReactComponent as BackArrow } from '../../assets/images/back-arrow-ic.inline.svg'; -import { IconButton } from '@mui/material'; +import { ReactComponent as BackArrow } from '../../assets/images/back-arrow-ic.inline.svg'; +import { IconButton, styled } from '@mui/material'; type Props = {| +onBack: void => PossiblyAsync, |}; +const IconWrapper = styled(IconButton)(({ theme }) => ({ + '& svg': { + '& path': { + fill: theme.palette.ds.el_gray_medium, + }, + }, +})); + @observer export default class DialogBackButton extends Component { render(): Node { const { onBack } = this.props; return ( - { svg: { width: '20px', height: '16px', - path: { - fill: 'hsl(220deg 2% 28%)', - }, }, }} > - + ); } } diff --git a/packages/yoroi-extension/app/components/widgets/QrCodeWrapper.js b/packages/yoroi-extension/app/components/widgets/QrCodeWrapper.js index c3b76c99be..43840bb1b2 100644 --- a/packages/yoroi-extension/app/components/widgets/QrCodeWrapper.js +++ b/packages/yoroi-extension/app/components/widgets/QrCodeWrapper.js @@ -1,7 +1,6 @@ // @flow import type { Node } from 'react'; import QRCode from 'qrcode.react'; -import { readCssVar } from '../../styles/utils'; import { useTheme } from '@mui/material'; type Props = {| @@ -13,12 +12,12 @@ type Props = {| +fgColor?: string, |}; -const QrCodeWrapper = ({ value, size, id = 'qr-code', includeMargin = false, addBg = true, fgColor }: Props): Node => { +const QrCodeWrapper = ({ value, size, id = 'qr-code', includeMargin = false, addBg = true }: Props): Node => { const theme = useTheme(); console.log('theme', theme); // Get QRCode color value from active theme's CSS variable const qrCodeBackgroundColor = addBg ? theme.palette.ds.el_gray_max : '#ffffff'; - const qrCodeForegroundColor = fgColor ?? readCssVar('--yoroi-qr-code-foreground'); + const qrCodeForegroundColor = theme.palette.ds.gray_min; return (
    , ]; diff --git a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss index 123afcffa9..9f846562d5 100644 --- a/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss +++ b/packages/yoroi-extension/app/connector/components/connect/ConnectedWallet.scss @@ -13,6 +13,14 @@ width: 40px; } + & .icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + } + & .wrapper { display: flex; align-items: center; diff --git a/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js b/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js index be51882700..70075151b8 100644 --- a/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js +++ b/packages/yoroi-extension/app/connector/components/signin/CardanoSignTxPage.js @@ -428,14 +428,18 @@ class SignTxPage extends Component {
    {this.renderPayload(signData.payload)}
    - - - + + + {walletType === 'mnemonic' && ( + + + + )} ); utxosContent = null; diff --git a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js index c8c0e8deab..7858d22b7e 100644 --- a/packages/yoroi-extension/app/connector/stores/ConnectorStore.js +++ b/packages/yoroi-extension/app/connector/stores/ConnectorStore.js @@ -54,10 +54,10 @@ import type LocalizableError from '../../i18n/LocalizableError'; import { convertToLocalizableError as convertToLocalizableLedgerError } from '../../domain/LedgerLocalizedError'; import { convertToLocalizableError as convertToLocalizableTrezorError } from '../../domain/TrezorLocalizedError'; import { - ledgerSignDataUnsupportedError, transactionHashMismatchError, trezorSignDataUnsupportedError, unsupportedTransactionError, + unknownAddressError, } from '../../domain/HardwareWalletLocalizedError'; import { wrapWithFrame } from '../../stores/lib/TrezorWrapper'; import { ampli } from '../../../ampli/index'; @@ -73,10 +73,12 @@ import { connectWindowRetrieveData, removeWalletFromWhiteList, getConnectedSites, + getProtocolParameters, } from '../../api/thunk'; import type { WalletState } from '../../../chrome/extension/background/types'; import { addressBech32ToHex } from '../../api/ada/lib/cardanoCrypto/utils'; -import AdaApi from '../../api/ada'; +import AdaApi, { findPath } from '../../api/ada'; +import { MessageAddressFieldType } from '@cardano-foundation/ledgerjs-hw-app-cardano'; // Need to run only once - Connecting wallets let initedConnecting = false; @@ -294,12 +296,62 @@ export default class ConnectorStore extends Store { }); } } else if (signingMessage.sign.type === 'data') { - userSignConfirm({ - tx: null, - uid: signingMessage.sign.uid, - tabId: signingMessage.tabId, - password, - }); + const { payload } = signingMessage.sign; + + if (wallet.type === 'mnemonic') { + userSignConfirm({ + tx: null, + uid: signingMessage.sign.uid, + tabId: signingMessage.tabId, + password, + }); + } else if (wallet.type === 'ledger') { + const signingPath = findPath(wallet, signingMessage.sign.address); + if (signingPath == null) { + runInAction(() => { + this.hwWalletError = unknownAddressError; + this.isHwWalletErrorRecoverable = false; + }); + return; + } + + const expectedSerial = wallet.hardwareWalletDeviceId || ''; + + const ledgerConnect = new LedgerConnect({ + locale: this.stores.profile.currentLocale, + }); + + let ledgerSignResult; + try { + ledgerSignResult = await ledgerConnect.signMessage({ + serial: expectedSerial, + params: { + messageHex: payload, + signingPath, + hashPayload: false, + preferHexDisplay: false, + // see https://vacuumlabs.github.io/ledgerjs-cardano-shelley/7.1.3/enums/MessageAddressFieldType.html#ADDRESS + addressFieldType: MessageAddressFieldType.KEY_HASH, + }, + }); + userSignConfirm({ + tx: null, + uid: signingMessage.sign.uid, + tabId: signingMessage.tabId, + password: '', + signedMessageData: ledgerSignResult, + }); + } catch (error) { + bringWindowToForeground(); + runInAction(() => { + this.hwWalletError = new convertToLocalizableLedgerError(error); + this.isHwWalletErrorRecoverable = true; + }); + return; + } + } else { + throw new Error('Not expected to reach here. Unexpectedly wallet type'); + } } else { throw new Error(`unkown sign data type ${signingMessage.sign.type}`); } @@ -660,7 +712,7 @@ export default class ConnectorStore extends Store { const govActionIds = votingProcedures.get_governance_action_ids_by_voter( voter ); - for (let j = 0; i < govActionIds.len(); j++) { + for (let j = 0; j < govActionIds.len(); j++) { const govActionId = govActionIds.get(j); if (!govActionId) { throw new Error('unexpectedly missing governance action id'); @@ -737,6 +789,8 @@ export default class ConnectorStore extends Store { throw new Error('wallet has no used address'); } + const protocolParameters = await getProtocolParameters(connectedWallet); + const { unsignedTx, collateralOutputAddressSet } = await adaApi._createReorgTx( getNetworkById(connectedWallet.networkId), { @@ -751,6 +805,7 @@ export default class ConnectorStore extends Store { addressedUtxos, connectedWallet.submittedTransactions, usedAddress, + protocolParameters, ); // record the unsigned tx, so that after the user's approval, we can sign // it without re-generating @@ -1128,6 +1183,7 @@ export default class ConnectorStore extends Store { params: ledgerSignTxPayload, }); } catch (error) { + bringWindowToForeground(); runInAction(() => { this.hwWalletError = new convertToLocalizableLedgerError(error); this.isHwWalletErrorRecoverable = true; @@ -1172,12 +1228,9 @@ export default class ConnectorStore extends Store { if (connectedWallet == null) { return; } - if (connectedWallet.type !== 'mnemonic') { - const hwWalletError = connectedWallet.type === 'ledger' - ? ledgerSignDataUnsupportedError - : trezorSignDataUnsupportedError; + if (connectedWallet.type === 'trezor') { runInAction(() => { - this.hwWalletError = hwWalletError; + this.hwWalletError = trezorSignDataUnsupportedError; this.isHwWalletErrorRecoverable = false; }); } @@ -1201,3 +1254,12 @@ function deserializeAnchor(anchor: ?RustModule.WalletV4.Anchor): Anchor | null { +function bringWindowToForeground(): void { + declare var chrome; + chrome.windows.update( + -2, // current window + { + focused: true, + } + ); +} diff --git a/packages/yoroi-extension/app/containers/SidebarContainer.js b/packages/yoroi-extension/app/containers/SidebarContainer.js index 5e8d2c2aa5..d7745dad25 100644 --- a/packages/yoroi-extension/app/containers/SidebarContainer.js +++ b/packages/yoroi-extension/app/containers/SidebarContainer.js @@ -90,8 +90,7 @@ class SidebarContainer extends Component { { actions.router.goToRoute.trigger({ - route: ROUTES.WALLETS.TRANSACTIONS, - publicDeriverId: stores.wallets.selected?.publicDeriverId, + route: ROUTES.WALLETS.ROOT, }); }} onCategoryClicked={category => { diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js b/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js index 30ae5b75f8..a1d504fd58 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/ConfirmSwapTransaction.js @@ -1,5 +1,5 @@ //@flow -import { Box, Typography } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import { makeLimitOrder, makePossibleMarketOrder, useSwap, useSwapCreateOrder } from '@yoroi/swap'; import { useEffect } from 'react'; import { IncorrectWalletPasswordError } from '../../../api/common/errors'; @@ -23,6 +23,10 @@ import type { RemoteTokenInfo } from '../../../api/ada/lib/state-fetch/types'; import type { PriceImpact } from '../../../components/swap/types'; import type { State } from '../context/swap-form/types'; +const GradientBox = styled(Box)(({ theme }: any) => ({ + backgroundImage: theme.palette.ds.bg_gradient_3, +})); + type Props = {| slippageValue: string, walletAddress: ?string, @@ -33,6 +37,7 @@ type Props = {| defaultTokenInfo: RemoteTokenInfo, getTokenInfo: string => Promise, getFormattedPairingValue: (amount: string) => string, + onError: () => void, |}; const priceStrings = { @@ -58,6 +63,7 @@ export default function ConfirmSwapTransaction({ defaultTokenInfo, getTokenInfo, getFormattedPairingValue, + onError, }: Props): React$Node { const { orderData } = useSwap(); const { @@ -76,12 +82,12 @@ export default function ConfirmSwapTransaction({ onSuccess: data => { onRemoteOrderDataResolved(data).catch(e => { console.error('Failed to handle remote order resolution', e); - alert('Failed to prepare order transaction'); + onError(); }); }, onError: error => { console.error('useSwapCreateOrder fail', error); - alert('Failed to receive remote data for the order'); + onError(); }, }); useEffect(() => { @@ -101,7 +107,7 @@ export default function ConfirmSwapTransaction({ }, []); return ( - + Confirm swap transaction @@ -110,7 +116,7 @@ export default function ConfirmSwapTransaction({ - + Swap from @@ -126,7 +132,7 @@ export default function ConfirmSwapTransaction({ - + Swap to @@ -174,11 +180,11 @@ export default function ConfirmSwapTransaction({ )} - + - Total + Total - + {formattedNonPtAmount ?? formattedPtAmount} @@ -186,18 +192,18 @@ export default function ConfirmSwapTransaction({ {formattedNonPtAmount && ( - + {formattedPtAmount} )} - + {getFormattedPairingValue(ptAmount)} - + {userPasswordState != null && ( ( - + {col1} {withInfo ? ( diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js index d520c31996..6e81c8aecf 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/SwapPage.js @@ -146,7 +146,7 @@ function SwapPage(props: StoresAndActionsProps & Intl): Node { .catch(e => { console.error('Failed to load stored slippage', e); }); - setSelectedWalletAddress(addressHexToBech32(wallet.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0].address)) + setSelectedWalletAddress(addressHexToBech32(wallet.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0].address)); props.stores.substores.ada.stateFetchStore.fetcher .getSwapFeeTiers({ network }) .then(feeTiers => { @@ -357,6 +357,9 @@ function SwapPage(props: StoresAndActionsProps & Intl): Node { defaultTokenInfo={defaultTokenInfo} getTokenInfo={getTokenInfo} getFormattedPairingValue={getFormattedPairingValue} + onError={() => { + props.actions.router.goToRoute.trigger({ route: ROUTES.SWAP.ERROR }); + }} /> )} {orderStep === 2 && ( diff --git a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js index 0efba0d0b7..8b7590f1ea 100644 --- a/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js +++ b/packages/yoroi-extension/app/containers/swap/asset-swap/edit-buy-amount/SelectBuyTokenFromList.js @@ -1,21 +1,27 @@ //@flow -import { useMemo, type Node } from 'react'; -import { useSwap, useSwapTokensOnlyVerified } from '@yoroi/swap'; +import { type Node } from 'react'; +import { useSwap } from '@yoroi/swap'; import SelectAssetDialog from '../../../../components/swap/SelectAssetDialog'; import { useSwapForm } from '../../context/swap-form'; import type { RemoteTokenInfo } from '../../../../api/ada/lib/state-fetch/types'; import SwapStore from '../../../../stores/ada/SwapStore'; -import { comparatorByGetter } from '../../../../coreUtils'; +import { useBuyVerifiedSwapTokens } from '../hooks'; type Props = {| store: SwapStore, onClose(): void, - onTokenInfoChanged: * => void, + onTokenInfoChanged: (*) => void, defaultTokenInfo: RemoteTokenInfo, - getTokenInfoBatch: Array => { [string]: Promise }, + getTokenInfoBatch: (Array) => { [string]: Promise }, |}; -export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChanged, defaultTokenInfo, getTokenInfoBatch }: Props): Node { +export default function SelectBuyTokenFromList({ + store, + onClose, + onTokenInfoChanged, + defaultTokenInfo, + getTokenInfoBatch, +}: Props): Node { const { sellQuantity: { isTouched: isSellTouched }, buyQuantity: { isTouched: isBuyTouched }, @@ -25,26 +31,13 @@ export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChan switchTokens, } = useSwapForm(); - const { onlyVerifiedTokens } = useSwapTokensOnlyVerified(); - const walletAssets = store.assets; - - const walletVerifiedAssets = useMemo(() => { - const isSellingPt = sellTokenInfo.id === ''; - const pt = walletAssets.find(a => a.id === ''); - const nonPtAssets = onlyVerifiedTokens.map(ovt => { - if (ovt.id === '') return null; - const vft = walletAssets.find(a => a.fingerprint === ovt.fingerprint); - return { ...ovt, ...(vft ?? {}) }; - }).filter(Boolean).sort(comparatorByGetter(a => a.name?.toLowerCase())); - return [...(isSellingPt ? [] : [pt]), ...nonPtAssets]; - }, [onlyVerifiedTokens, walletAssets, sellTokenInfo]); + const { walletVerifiedAssets, isLoading } = useBuyVerifiedSwapTokens(store.assets, sellTokenInfo); const { orderData, resetQuantities } = useSwap(); const handleAssetSelected = token => { const { id, decimals } = token; - const shouldUpdateToken = - id !== orderData.amounts.buy.tokenId || !isBuyTouched || decimals !== buyTokenInfo.decimals; + const shouldUpdateToken = id !== orderData.amounts.buy.tokenId || !isBuyTouched || decimals !== buyTokenInfo.decimals; const shouldSwitchTokens = id === orderData.amounts.sell.tokenId && isSellTouched; // useCase - switch tokens when selecting the same already selected token on the other side if (shouldSwitchTokens) { @@ -63,6 +56,7 @@ export default function SelectBuyTokenFromList({ store, onClose, onTokenInfoChan return ( - Min ADA + Min ADA {withInfo && ( )} @@ -52,7 +52,7 @@ export default function SwapPoolFullInfo({ defaultTokenInfo, withInfo, showMinAd )} - Fees + Fees {withInfo && ( - Minimum assets received + Minimum assets received {withInfo && } @@ -78,7 +78,7 @@ export default function SwapPoolFullInfo({ defaultTokenInfo, withInfo, showMinAd - Liquidity provider fee + Liquidity provider fee {withInfo && ( { - return assets.map(a => { - const vft = onlyVerifiedTokens.find(ovt => ovt.fingerprint === a.fingerprint); - return a.id === '' || vft ? { ...a, ...vft } : undefined; - }).filter(Boolean).sort(comparatorByGetter(a => a.name?.toLowerCase())); - }, [onlyVerifiedTokens, assets]); - + const { walletVerifiedAssets, isLoading } = useSellVerifiedSwapTokens(store.assets) + const { orderData, resetQuantities } = useSwap(); const { buyQuantity: { isTouched: isBuyTouched }, @@ -58,6 +51,7 @@ export default function SelectSellTokenFromList({ store, onClose, onTokenInfoCha return ( , +): {| walletVerifiedAssets: Array, isLoading: boolean |} { + + const { onlyVerifiedTokens, isLoading } = useSwapTokensOnlyVerified({ + useErrorBoundary: false, + }); + + // maybe check `error` field from query and make it available for UI to do something + + const swapFromVerifiedAssets = useMemo(() => { + return assets + .map(a => { + const vft = onlyVerifiedTokens?.find(ovt => ovt.fingerprint === a.fingerprint); + return a.id === '' || vft ? { ...a, ...(vft ?? {}) } : undefined; + }) + .filter(Boolean) + .sort(comparatorByGetter(a => a.name?.toLowerCase())); + }, [onlyVerifiedTokens, assets]); + + return { walletVerifiedAssets: swapFromVerifiedAssets, isLoading }; +} + +export function useBuyVerifiedSwapTokens( + assets: Array, + sellTokenInfo: { id: string, ... }, +): {| walletVerifiedAssets: Array, isLoading: boolean |} { + + const { onlyVerifiedTokens, isLoading } = useSwapTokensOnlyVerified({ + useErrorBoundary: false, + }); + + // maybe check `error` field from query and make it available for UI to do something + + const swapToVerifiedAssets = useMemo(() => { + const isSellingPt = sellTokenInfo.id === ''; + const pt = assets.find(a => a.id === ''); + const nonPtAssets = (onlyVerifiedTokens ?? []) + .map(ovt => { + if (ovt.id === '') return null; + const vft = assets.find(a => a.fingerprint === ovt.fingerprint); + return { ...ovt, ...(vft ?? {}) }; + }) + .filter(Boolean) + .sort(comparatorByGetter(a => a.name?.toLowerCase())); + return [...(isSellingPt ? [] : [pt]), ...nonPtAssets]; + }, [onlyVerifiedTokens, assets, sellTokenInfo]); + + return { walletVerifiedAssets: swapToVerifiedAssets, isLoading }; +} diff --git a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js index fa28429735..5ec5533543 100644 --- a/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js +++ b/packages/yoroi-extension/app/containers/swap/context/swap-form/SwapFormProvider.js @@ -1,15 +1,15 @@ //@flow +import { useSwap } from '@yoroi/swap'; import type { Node } from 'react'; import { useCallback, useEffect, useReducer, useState } from 'react'; -import type { SwapFormAction, SwapFormState } from './types'; -import { StateWrap, SwapFormActionTypeValues } from './types'; +import { PRICE_PRECISION } from '../../../../components/swap/common'; import type { AssetAmount } from '../../../../components/swap/types'; -import { useSwap } from '@yoroi/swap'; -import Context from './context'; -import { Quantities } from '../../../../utils/quantities'; import SwapStore from '../../../../stores/ada/SwapStore'; +import { Quantities } from '../../../../utils/quantities'; +import Context from './context'; import { defaultSwapFormState } from './DefaultSwapFormState'; -import { PRICE_PRECISION } from '../../../../components/swap/common'; +import type { SwapFormAction, SwapFormState } from './types'; +import { StateWrap, SwapFormActionTypeValues } from './types'; // const PRECISION = 14; type Props = {| @@ -107,10 +107,8 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { } = swapFormState; const actions = { - sellTouched: (token?: AssetAmount) => - dispatch({ type: SwapFormActionTypeValues.SellTouched, token }), - buyTouched: (token?: AssetAmount) => - dispatch({ type: SwapFormActionTypeValues.BuyTouched, token }), + sellTouched: (token?: AssetAmount) => dispatch({ type: SwapFormActionTypeValues.SellTouched, token }), + buyTouched: (token?: AssetAmount) => dispatch({ type: SwapFormActionTypeValues.BuyTouched, token }), switchTouched: () => dispatch({ type: SwapFormActionTypeValues.SwitchTouched }), switchTokens: () => { switchTokens(); @@ -127,18 +125,13 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { resetState(); dispatch({ type: SwapFormActionTypeValues.ResetSwapForm }); }, - canSwapChanged: (canSwap: boolean) => - dispatch({ type: SwapFormActionTypeValues.CanSwapChanged, canSwap }), - buyInputValueChanged: (value: string) => - dispatch({ type: SwapFormActionTypeValues.BuyInputValueChanged, value }), - sellInputValueChanged: (value: string) => - dispatch({ type: SwapFormActionTypeValues.SellInputValueChanged, value }), + canSwapChanged: (canSwap: boolean) => dispatch({ type: SwapFormActionTypeValues.CanSwapChanged, canSwap }), + buyInputValueChanged: (value: string) => dispatch({ type: SwapFormActionTypeValues.BuyInputValueChanged, value }), + sellInputValueChanged: (value: string) => dispatch({ type: SwapFormActionTypeValues.SellInputValueChanged, value }), limitPriceInputValueChanged: (value: string) => dispatch({ type: SwapFormActionTypeValues.LimitPriceInputValueChanged, value }), - buyAmountErrorChanged: (error: string | null) => - dispatch({ type: SwapFormActionTypeValues.BuyAmountErrorChanged, error }), - sellAmountErrorChanged: (error: string | null) => - dispatch({ type: SwapFormActionTypeValues.SellAmountErrorChanged, error }), + buyAmountErrorChanged: (error: string | null) => dispatch({ type: SwapFormActionTypeValues.BuyAmountErrorChanged, error }), + sellAmountErrorChanged: (error: string | null) => dispatch({ type: SwapFormActionTypeValues.SellAmountErrorChanged, error }), }; /** @@ -173,7 +166,8 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { */ useEffect(() => { if (sellTokenId != null && buyTokenId != null && sellTokenId !== buyTokenId) { - pools.list.byPair({ tokenA: sellTokenId, tokenB: buyTokenId }) + pools.list + .byPair({ tokenA: sellTokenId, tokenB: buyTokenId }) .then(poolsArray => poolPairsChanged(poolsArray)) .catch(err => console.error(`Failed to fetch pools for pair: ${sellTokenId}/${buyTokenId}`, err)); } @@ -184,10 +178,9 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { if (swapFormState.buyQuantity.error != null) actions.buyAmountErrorChanged(null); }, [actions, swapFormState.buyQuantity.error, swapFormState.sellQuantity.error]); - const baseSwapFieldChangeHandler = ( - tokenInfo: any, - handler: ({| input: string, quantity: string |}) => void - ) => (text: string = '') => { + const baseSwapFieldChangeHandler = (tokenInfo: any, handler: ({| input: string, quantity: string |}) => void) => ( + text: string = '' + ) => { if (tokenInfo.tokenId === '') { // empty input return; @@ -207,13 +200,21 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { const sellAvailableAmount = swapFormState.sellTokenInfo.amount ?? '0'; if (quantity !== '' && sellAvailableAmount !== '') { const decimals = swapFormState.sellTokenInfo.decimals ?? 0; - const [, availableQuantity] = Quantities.parseFromText( - sellAvailableAmount, - decimals, - numberLocale, - ); + const calculation = orderData.selectedPoolCalculation; + + const [, availableQuantity] = Quantities.parseFromText(sellAvailableAmount, decimals, numberLocale); + const diff = Quantities.diff(availableQuantity, quantity); + if (Quantities.isGreaterThan(quantity, availableQuantity)) { actions.sellAmountErrorChanged('Not enough balance'); + return; + } + if (calculation?.cost) { + const totalFee = Quantities.sum([calculation.cost.batcherFee.quantity, calculation.cost.deposit.quantity]); + + if (Number(diff) < Number(totalFee)) { + actions.sellAmountErrorChanged('Not enough balance, please consider the fees'); + } } } }; @@ -232,20 +233,22 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { actions.limitPriceInputValueChanged(input); }; - const onChangeSellQuantity = useCallback( - baseSwapFieldChangeHandler(swapFormState.sellTokenInfo, sellUpdateHandler), - [sellQuantityChanged, actions, clearErrors] - ); + const onChangeSellQuantity = useCallback(baseSwapFieldChangeHandler(swapFormState.sellTokenInfo, sellUpdateHandler), [ + sellQuantityChanged, + actions, + clearErrors, + ]); - const onChangeBuyQuantity = useCallback( - baseSwapFieldChangeHandler(swapFormState.buyTokenInfo, buyUpdateHandler), - [buyQuantityChanged, actions, clearErrors] - ); + const onChangeBuyQuantity = useCallback(baseSwapFieldChangeHandler(swapFormState.buyTokenInfo, buyUpdateHandler), [ + buyQuantityChanged, + actions, + clearErrors, + ]); const onChangeLimitPrice = useCallback( baseSwapFieldChangeHandler( { tokenId: 'priceDenomination', decimals: priceDenomination, precision: PRICE_PRECISION }, - limitUpdateHandler, + limitUpdateHandler ), [limitPriceChanged, actions, clearErrors, priceDenomination] ); @@ -303,9 +306,5 @@ export default function SwapFormProvider({ swapStore, children }: Props): Node { onChangeLimitPrice, }; - return ( - - {children} - - ); + return {children}; } diff --git a/packages/yoroi-extension/app/containers/swap/orders/hooks.js b/packages/yoroi-extension/app/containers/swap/orders/hooks.js index 550229e7d8..f8f4560081 100644 --- a/packages/yoroi-extension/app/containers/swap/orders/hooks.js +++ b/packages/yoroi-extension/app/containers/swap/orders/hooks.js @@ -79,6 +79,7 @@ export function useRichOrders( const { data: tokensMap } = useQuery({ suspense: true, queryKey: ['useSwapTokensOnlyVerified'], + useErrorBoundary: false, queryFn: () => tokens.list.onlyVerified() .then(tokensArray => tokensArray.reduce((map, t) => ({ ...map, [t.id]: t }), {})) .catch(e => { @@ -92,6 +93,7 @@ export function useRichOrders( */ const { data: openOrdersData, isLoading: openOrdersLoading } = useQuery({ queryKey: ['useSwapOrdersByStatusOpen', stakingKey], + useErrorBoundary: false, queryFn: () => order.list.byStatusOpen().catch(e => { console.error('Failed to load open orders!', e); throw e; @@ -103,6 +105,7 @@ export function useRichOrders( */ const { data: completedOrdersData, isLoading: completedOrdersLoading } = useQuery({ queryKey: ['useSwapOrdersByStatusCompleted', stakingKey], + useErrorBoundary: false, queryFn: () => order.list.byStatusCompleted().catch(e => { console.error('Failed to load completed orders!', e); throw e; diff --git a/packages/yoroi-extension/app/containers/wallet/Wallet.js b/packages/yoroi-extension/app/containers/wallet/Wallet.js index 8015921d98..f546c11519 100644 --- a/packages/yoroi-extension/app/containers/wallet/Wallet.js +++ b/packages/yoroi-extension/app/containers/wallet/Wallet.js @@ -25,6 +25,7 @@ import WalletSyncingOverlay from '../../components/wallet/syncingOverlay/WalletS import WalletLoadingAnimation from '../../components/wallet/WalletLoadingAnimation'; import { RevampAnnouncementDialog } from './dialogs/RevampAnnouncementDialog'; import { PoolTransitionDialog } from './dialogs/pool-transition/PoolTransitionDialog'; +import { Redirect } from 'react-router'; type Props = {| ...StoresAndActionsProps, @@ -47,14 +48,6 @@ class Wallet extends Component { }; componentDidMount() { - // reroute to the default path for the wallet - const newRoute = this.checkRoute(); - if (newRoute != null) { - this.props.actions.router.redirect.trigger({ - route: newRoute, - }); - } - if (!this.props.stores.profile.isRevampAnnounced) this.props.actions.dialogs.open.trigger({ dialog: RevampAnnouncementDialog }); } @@ -69,8 +62,8 @@ class Wallet extends Component { // void -> this route is fine for this wallet type // string -> what you should be redirected to - const publicDeriver = this.props.stores.wallets.selected; - if (publicDeriver == null) return; + const wallet = this.props.stores.wallets.selected; + if (wallet == null) return; const spendableBalance = this.props.stores.transactions.balance; const walletHasAssets = !!spendableBalance?.nonDefaultEntries().length; @@ -83,8 +76,8 @@ class Wallet extends Component { // ex: a cardano-only page for an Ergo wallet // or no category is selected yet (wallet selected for the first time) const visibilityContext = { - selected: publicDeriver.publicDeriverId, - networkId: publicDeriver.networkId, + selected: wallet.publicDeriverId, + networkId: wallet.networkId, walletHasAssets }; if ( @@ -106,8 +99,9 @@ class Wallet extends Component { render(): Node { const { actions, stores } = this.props; // abort rendering if the page isn't valid for this wallet - if (this.checkRoute() != null) { - return null; + const newRoute = this.checkRoute(); + if (newRoute != null) { + return ; } const { intl } = this.context; const selectedWallet = stores.wallets.selected; diff --git a/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js b/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js index a8e839b3f9..5b45d1ca2b 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js @@ -101,14 +101,8 @@ function WalletDelegationBanner({ - - + + {intl.formatMessage(emptyDashboardMessages.title, { ticker })} @@ -210,7 +204,6 @@ const WrapperBanner = styled(Box)({ padding: '24px', borderRadius: '8px', overflowY: 'hidden', - border: '2px solid red', }); const AvatarWrapper = styled(Box)({ diff --git a/packages/yoroi-extension/app/containers/wallet/WalletEmptyBanner.js b/packages/yoroi-extension/app/containers/wallet/WalletEmptyBanner.js index ce0ef26170..f1d40e6ea5 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletEmptyBanner.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletEmptyBanner.js @@ -37,18 +37,19 @@ function WalletEmptyBanner({ onBuySellClick, intl }: Props & Intl): Node { borderRadius: '8px', overflowY: 'hidden', position: 'relative', - padding: '24px', + padding: '16px', + height: '156px', }} - id='wallet|staking-emptyWalletBanner-box' + id="wallet|staking-emptyWalletBanner-box" > - + - + {intl.formatMessage(messages.welcomeMessage)} - + {intl.formatMessage(messages.welcomeMessageSubtitle)} @@ -59,7 +60,7 @@ function WalletEmptyBanner({ onBuySellClick, intl }: Props & Intl): Node { size="medium" sx={{ '&.MuiButton-sizeMedium': { - padding: '9px 25px', + padding: '9px 20px', height: 'unset', }, }} diff --git a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js index 9b17ad6203..b91530bf7e 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletSendPage.js @@ -16,7 +16,7 @@ import WalletSendConfirmationDialogContainer from './dialogs/WalletSendConfirmat import WalletSendConfirmationDialog from '../../components/wallet/send/WalletSendConfirmationDialog'; import MemoNoExternalStorageDialog from '../../components/wallet/memos/MemoNoExternalStorageDialog'; import { HaskellShelleyTxSignRequest } from '../../api/ada/transactions/shelley/HaskellShelleyTxSignRequest'; -import { getMinimumValue, validateAmount } from '../../utils/validations'; +import { validateAmount } from '../../utils/validations'; import { addressToDisplayString } from '../../api/ada/lib/storage/bridge/utils'; import type { TokenRow } from '../../api/ada/lib/storage/database/primitives/tables'; import { genLookupOrFail } from '../../stores/stateless/tokenHelpers'; @@ -94,6 +94,9 @@ class WalletSendPage extends Component { this.showSupportedAddressDomainBanner = this.props.stores.substores.ada.addresses.getSupportedAddressDomainBannerState(); }); + const { loadProtocolParametersRequest } = this.props.stores.protocolParameters; + loadProtocolParametersRequest.reset(); + loadProtocolParametersRequest.execute(); ampli.sendInitiated(); } @@ -134,9 +137,17 @@ class WalletSendPage extends Component { const { selected } = this.props.stores.wallets; if (!selected) throw new Error(`Active wallet required for ${nameof(WalletSendPage)}.`); - const { transactionBuilderStore } = this.props.stores; + const { + uiDialogs, + profile, + transactionBuilderStore, + protocolParameters, + } = this.props.stores; + + if (!protocolParameters.loadProtocolParametersRequest.wasExecuted) { + return null; + } - const { uiDialogs, profile } = this.props.stores; const { actions } = this.props; const { hasAnyPending } = this.props.stores.transactions; const { txBuilderActions } = this.props.actions; @@ -244,10 +255,7 @@ class WalletSendPage extends Component { validateAmount( amount, transactionBuilderStore.selectedToken ?? defaultToken, - getMinimumValue( - network, - transactionBuilderStore.selectedToken?.IsDefault ?? true - ), + transactionBuilderStore.minAda.getDefault(), this.context.intl ) } diff --git a/packages/yoroi-extension/app/domain/HardwareWalletLocalizedError.js b/packages/yoroi-extension/app/domain/HardwareWalletLocalizedError.js index 61d2607d0d..70230e6137 100644 --- a/packages/yoroi-extension/app/domain/HardwareWalletLocalizedError.js +++ b/packages/yoroi-extension/app/domain/HardwareWalletLocalizedError.js @@ -12,14 +12,14 @@ const errors: * = defineMessages({ id: 'wallet.hw.tx.unsupported.error', defaultMessage: '!!!Signing this transaction with hardware wallet is not supported.', }, - ledgerSignDataUnsupportedError: { - id: 'wallet.hw.ledger.data.sign.unsupported.error', - defaultMessage: '!!!The Ledger Cardano app does not support data signing at this memoment', - }, trezorSignDataUnsupportedError: { id: 'wallet.hw.trezor.data.sign.unsupported.error', defaultMessage: '!!!Trezor does not support data signing at this memoment', }, + unknownAddressError: { + id: 'wallet.hw.data.sign.unkown.address', + defaultMessage: '!!!The requested signing address is not found in this wallet', + }, }); export const transactionHashMismatchError: LocalizableError = new LocalizableError( @@ -30,10 +30,10 @@ export const unsupportedTransactionError: LocalizableError = new LocalizableErro errors.unsupportedTransactionError ); -export const ledgerSignDataUnsupportedError: LocalizableError = new LocalizableError( - errors.ledgerSignDataUnsupportedError -); - export const trezorSignDataUnsupportedError: LocalizableError = new LocalizableError( errors.trezorSignDataUnsupportedError ); + +export const unknownAddressError: LocalizableError = new LocalizableError( + errors.unknownAddressError +); diff --git a/packages/yoroi-extension/app/domain/LedgerLocalizedError.js b/packages/yoroi-extension/app/domain/LedgerLocalizedError.js index 2e2ac997b0..3df2297340 100644 --- a/packages/yoroi-extension/app/domain/LedgerLocalizedError.js +++ b/packages/yoroi-extension/app/domain/LedgerLocalizedError.js @@ -43,6 +43,14 @@ export const ledgerErrors: * = defineMessages({ id: 'wallet.hw.ledger.catalyst.cip36.unsupported', defaultMessage: '!!!Catalyst registration requires Ledger app version 6.', }, + deviceVersionNoDataSigning: { + id: 'wallet.hw.ledger.error.deviceVersionNoDataSigning', + defaultMessage: '!!!CIP-8 message signing not supported by your Ledger app version', + }, + deviceStatusError: { + id: 'wallet.hw.ledger.error.deviceStatus', + defaultMessage: '!!!Invalid or oversized data for Ledger.', + }, }); export function convertToLocalizableError(error: Error): LocalizableError { @@ -74,6 +82,19 @@ export function convertToLocalizableError(error: Error): LocalizableError { }); } } + if (/^DeviceVersionUnsupported/.test(error.message)) { + return new LocalizableError(ledgerErrors.deviceVersionNoDataSigning); + } + if (/Invalid data supplied to Ledger/.test(error.message)) { + return new LocalizableError( + ledgerErrors.deviceStatusError + ); + } + if (/Action rejected by user/.test(error.message)) { + return new LocalizableError( + ledgerErrors.cancelOnDeviceError101 + ); + } // Ledger device related error happened, convert then to LocalizableError switch (error.message) { case 'TransportError: Failed to sign with Ledger device: U2F TIMEOUT': @@ -81,10 +102,6 @@ export function convertToLocalizableError(error: Error): LocalizableError { // Showing - Failed to connect. Please check your ledger device and retry. localizableError = new LocalizableError(globalMessages.ledgerError101); break; - case 'DeviceStatusError: Action rejected by user': - // Showing - Operation cancelled on Ledger device. - localizableError = new LocalizableError(ledgerErrors.cancelOnDeviceError101); - break; case 'NotAllowedError: The operation either timed out or was not allowed. See: https://w3c.github.io/webauthn/#sec-assertion-privacy.': case 'AbortError: The operation was aborted. ': case 'Forcefully cancelled by user': diff --git a/packages/yoroi-extension/app/domain/TrezorLocalizedError.js b/packages/yoroi-extension/app/domain/TrezorLocalizedError.js index 23af88506b..24253f8ae0 100644 --- a/packages/yoroi-extension/app/domain/TrezorLocalizedError.js +++ b/packages/yoroi-extension/app/domain/TrezorLocalizedError.js @@ -4,9 +4,7 @@ import LocalizableError, { UnexpectedError } from '../i18n/LocalizableError'; import globalMessages from '../i18n/global-messages'; import { defineMessages } from 'react-intl'; -import { - Logger, -} from '../utils/logging'; +import { Logger } from '../utils/logging'; const messages = defineMessages({ signTxError101: { @@ -19,7 +17,8 @@ const messages = defineMessages({ }, noWitnessError: { id: 'wallet.send.trezor.error.noWitness', - defaultMessage: '!!!Could not sign the transaction. Please ensure the passphrase you entered is the passhprase used to create this wallet.', + defaultMessage: + '!!!Could not sign the transaction. Please ensure the passphrase you entered is the passhprase used to create this wallet.', }, }); @@ -34,38 +33,39 @@ export function convertToLocalizableError(error: Error): LocalizableError { if (error.message.includes('no witness for')) { // from `buildSignedTransaction()`, the only realistic cause being passphrase mismatch localizableError = new LocalizableError(messages.noWitnessError); + } else if (/Cancelled/.test(error.message)) { + localizableError = new LocalizableError(messages.signTxError101); } else { // Trezor device related error happend, convert then to LocalizableError switch (error.message) { - case 'Iframe timeout': - localizableError = new LocalizableError(globalMessages.trezorError101); - break; - case 'Trezor signing error: Permissions not granted': - localizableError = new LocalizableError(globalMessages.hwError101); - break; - case 'Trezor signing error: Popup closed (code=Method_Interrupted)': - localizableError = new LocalizableError(globalMessages.trezorError103); - break; - case 'Trezor signing error: Cancelled (code=Failure_ActionCancelled)': - case 'Trezor signing error: Failed to execute \'transferIn\' on \'USBDevice\': A transfer error has occurred. (code=19)': - localizableError = new LocalizableError(messages.signTxError101); - break; - case 'Feature AuxiliaryData not supported by device firmware': - localizableError = new LocalizableError(messages.firmwareCatalystSupportError); - break; - default: - /** we are not able to figure out why Error is thrown - * make it, Something unexpected happened */ - Logger.error(`TrezorLocalizedError::${nameof(convertToLocalizableError)}::error: ${error.message}`); - localizableError = new UnexpectedError(); - break; + case 'Iframe timeout': + localizableError = new LocalizableError(globalMessages.trezorError101); + break; + case 'Trezor signing error: Permissions not granted': + localizableError = new LocalizableError(globalMessages.hwError101); + break; + case 'Popup closed': + localizableError = new LocalizableError(globalMessages.trezorError103); + break; + case "Trezor signing error: Failed to execute 'transferIn' on 'USBDevice': A transfer error has occurred. (code=19)": + localizableError = new LocalizableError(messages.signTxError101); + break; + case 'Feature AuxiliaryData not supported by device firmware': + localizableError = new LocalizableError(messages.firmwareCatalystSupportError); + break; + default: + /** we are not able to figure out why Error is thrown + * make it, Something unexpected happened */ + Logger.error(`TrezorLocalizedError::${nameof(convertToLocalizableError)}::error: ${error.message}`); + localizableError = new UnexpectedError(); + break; } } } if (!localizableError) { /** we are not able to figure out why Error is thrown - * make it, Something unexpected happened */ + * make it, Something unexpected happened */ localizableError = new UnexpectedError(); } diff --git a/packages/yoroi-extension/app/i18n/locales/en-US.json b/packages/yoroi-extension/app/i18n/locales/en-US.json index dbd9f29c7d..73b9001f92 100644 --- a/packages/yoroi-extension/app/i18n/locales/en-US.json +++ b/packages/yoroi-extension/app/i18n/locales/en-US.json @@ -32,6 +32,7 @@ "api.errors.invalidWitnessError": "The signature is invalid.", "api.errors.noInputsError": "Your recovered wallet is empty. Please check your recovery phrase and restore again.", "api.errors.noOutputsError": "The transaction requires at least 1 output, but none was provided.", + "api.errors.oversizedTransaction": "Maximum transaction size exceeded.", "api.errors.poolMissingApiError": "Pool could not be found. Please check the pool ID and ensure the pool was not deregistered.", "api.errors.rewardAddressEmpty": "Reward address is not visible until users get any reward.", "api.errors.rollbackApiError": "Rollback was detected.", @@ -202,6 +203,9 @@ "global.yoroi": "Yoroi", "global.yoroi.intro": "Light wallet for Cardano assets", "global.yoroiNightly": "Yoroi Nightly", + "global.labels.unexpectedError": "Something unexpected happened", + "global.labels.contactSupport": "If this keep happening, contact our support team.", + "global.labels.pleaseGoBack": "Please go back and try again.", "incorrectTime.line1": "WARNING: time on your computer does not match the server. This can cause unexpected results", "incorrectTime.line2": "Time difference:", "incorrectTime.line3": "Synchronize time on your device to resolve this issue", @@ -289,6 +293,8 @@ "settings.general.languageSelect.labelInfo": "The selected language translation is fully provided by the community", "settings.general.translation.acknowledgment": "Thank you to the following for their contribution: ", "settings.general.translation.contributors": "_", + "settings.general.theme.light": "Light Theme", + "settings.general.theme.dark": "Dark Theme", "settings.menu.analytics.link.label": "Analytics", "settings.menu.assetDeposit.link.label": "Locked assets deposit", "settings.menu.blockchain.link.label": "Blockchain", @@ -588,9 +594,10 @@ "wallet.delegation.transaction.success.title": "Successfully delegated", "wallet.deprecation.byronLine1": "The Shelley protocol upgrade adds a new Shelley wallet type which supports delegation.", "wallet.deprecation.byronLine2": "To delegate your {ticker} you will need to upgrade to a Shelley wallet.", - "wallet.emptyWalletMessage": "Your wallet is empty", - "wallet.emptyWalletMessageSubtitle": "Top up your wallet safely using our trusted partners", + "wallet.emptyWalletMessage": "Get your first ADA in Yoroi 🔥", + "wallet.emptyWalletMessageSubtitle": "Buy ADA directly within your wallet and unlock the power of Cardano.", "wallet.hw.common.error.101": "Necessary permissions were not granted by the user. Please retry.", + "wallet.hw.data.sign.unkown.address": "The requested signing address is not found in this wallet", "wallet.hw.incorrectDevice": "Incorrect device detected. Expected device {expectedDeviceId}, but got device {responseDeviceId}. Please plug in the correct device", "wallet.hw.incorrectVersion": "Incorrect device version detected. We support version {supportedVersions} but you have version {responseVersion}.", "wallet.hw.ledger.app.not.running": "The Cardano App is not running on your Ledger", @@ -601,6 +608,8 @@ "wallet.hw.ledger.common.error.103": "Ledger device is locked, please unlock it and retry.", "wallet.hw.ledger.common.error.104": "Ledger device timeout, please retry.", "wallet.hw.ledger.common.error.105": "Network error. Please check your internet connection.", + "wallet.hw.ledger.error.deviceStatus": "Invalid or oversized data for Ledger.", + "wallet.hw.ledger.error.deviceVersionNoDataSigning": "CIP-8 message signing not supported by your Ledger app version", "wallet.hw.ledger.data.sign.unsupported.error": "The Ledger Cardano app does not support data signing at this memoment", "wallet.hw.trezor.data.sign.unsupported.error": "Trezor does not support data signing at this memoment", "wallet.hw.tx.hash.error": "The transaction hash computed by Yoroi extension and that by the device mismatch.", @@ -887,7 +896,6 @@ "wallet.syncingOverlay.explanation": "Please wait while we process wallet data. This may take some time.", "wallet.syncingOverlay.return": "Return to my wallets", "wallet.syncingOverlay.title": "Wallet Syncing", - "wallet.topbar.dialog.cardano": "Cardano, ADA", "wallet.topbar.dialog.tokenTypes": "Token types", "wallet.topbar.dialog.totalBalance": "Total Balance", "wallet.transaction.address.from": "From address", diff --git a/packages/yoroi-extension/app/i18n/locales/hu-HU.json b/packages/yoroi-extension/app/i18n/locales/hu-HU.json index 9fdff9f90f..da375039ae 100644 --- a/packages/yoroi-extension/app/i18n/locales/hu-HU.json +++ b/packages/yoroi-extension/app/i18n/locales/hu-HU.json @@ -32,6 +32,7 @@ "api.errors.invalidWitnessError": "Az aláírás érvénytelen.", "api.errors.noInputsError": "Your recovered wallet is empty. Please check your recovery phrase and restore again.", "api.errors.noOutputsError": "The transaction requires at least 1 output, but none was provided.", + "api.errors.oversizedTransaction": "Maximum transaction size exceeded.", "api.errors.poolMissingApiError": "Pool could not be found. Please check the pool ID and ensure the pool was not deregistered.", "api.errors.rewardAddressEmpty": "Reward address is not visible until users get any reward.", "api.errors.rollbackApiError": "Rollback was detected.", @@ -202,6 +203,9 @@ "global.yoroi": "Yoroi", "global.yoroi.intro": "Light wallet for Cardano assets", "global.yoroiNightly": "Yoroi Nightly", + "global.labels.unexpectedError": "Something unexpected happened", + "global.labels.contactSupport": "If this keep happening, contact our support team.", + "global.labels.pleaseGoBack": "Please go back and try again.", "incorrectTime.line1": "WARNING: time on your computer does not match the server. This can cause unexpected results", "incorrectTime.line2": "Time difference:", "incorrectTime.line3": "Synchronize time on your device to resolve this issue", @@ -289,6 +293,8 @@ "settings.general.languageSelect.labelInfo": "The selected language translation is fully provided by the community", "settings.general.translation.acknowledgment": "Thank you to the following for their contribution: ", "settings.general.translation.contributors": "adacoin.hu / GULAS Stake Pool", + "settings.general.theme.light": "Light Theme", + "settings.general.theme.dark": "Dark Theme", "settings.menu.analytics.link.label": "Analytics", "settings.menu.assetDeposit.link.label": "Locked assets deposit", "settings.menu.blockchain.link.label": "Blockchain", @@ -588,9 +594,10 @@ "wallet.delegation.transaction.success.title": "Successfully delegated", "wallet.deprecation.byronLine1": "The Shelley protocol upgrade adds a new Shelley wallet type which supports delegation.", "wallet.deprecation.byronLine2": "To delegate your {ticker} you will need to upgrade to a Shelley wallet.", - "wallet.emptyWalletMessage": "Your wallet is empty", - "wallet.emptyWalletMessageSubtitle": "Top up your wallet safely using our trusted partners", + "wallet.emptyWalletMessage": "Get your first ADA in Yoroi 🔥", + "wallet.emptyWalletMessageSubtitle": "Buy ADA directly within your wallet and unlock the power of Cardano.", "wallet.hw.common.error.101": "Necessary permissions were not granted by the user. Please retry.", + "wallet.hw.data.sign.unkown.address": "The requested signing address is not found in this wallet", "wallet.hw.incorrectDevice": "Incorrect device detected. Expected device {expectedDeviceId}, but got device {responseDeviceId}. Please plug in the correct device", "wallet.hw.incorrectVersion": "Incorrect device version detected. We support version {supportedVersions} but you have version {responseVersion}.", "wallet.hw.ledger.app.not.running": "The Cardano App is not running on your Ledger", @@ -601,6 +608,8 @@ "wallet.hw.ledger.common.error.103": "Ledger device is locked, please unlock it and retry.", "wallet.hw.ledger.common.error.104": "Ledger device timeout, please retry.", "wallet.hw.ledger.common.error.105": "Network error. Please check your internet connection.", + "wallet.hw.ledger.error.deviceStatus": "Invalid or oversized data for Ledger.", + "wallet.hw.ledger.error.deviceVersionNoDataSigning": "CIP-8 message signing not supported by your Ledger app version", "wallet.hw.ledger.data.sign.unsupported.error": "The Ledger Cardano app does not support data signing at this memoment", "wallet.hw.trezor.data.sign.unsupported.error": "Trezor does not support data signing at this memoment", "wallet.hw.tx.hash.error": "The transaction hash computed by Yoroi extension and that by the device mismatch.", @@ -887,7 +896,6 @@ "wallet.syncingOverlay.explanation": "Please wait while we process wallet data. This may take some time.", "wallet.syncingOverlay.return": "Return to my wallets", "wallet.syncingOverlay.title": "Wallet Syncing", - "wallet.topbar.dialog.cardano": "Cardano, ADA", "wallet.topbar.dialog.tokenTypes": "Token types", "wallet.topbar.dialog.totalBalance": "Total Balance", "wallet.transaction.address.from": "From address", diff --git a/packages/yoroi-extension/app/i18n/locales/vi-VN.json b/packages/yoroi-extension/app/i18n/locales/vi-VN.json index 990483bd18..eafe8c3d65 100644 --- a/packages/yoroi-extension/app/i18n/locales/vi-VN.json +++ b/packages/yoroi-extension/app/i18n/locales/vi-VN.json @@ -32,6 +32,7 @@ "api.errors.invalidWitnessError": "Chữ ký không hợp lệ.", "api.errors.noInputsError": "Ví đã khôi phục của bạn trống. Vui lòng kiểm tra cụm từ khôi phục của bạn và khôi phục lại.", "api.errors.noOutputsError": "Giao dịch yêu cầu ít nhất 1 đầu ra, nhưng không có đầu ra nào được cung cấp.", + "api.errors.oversizedTransaction": "Maximum transaction size exceeded.", "api.errors.poolMissingApiError": "Pool không thể tìm thấy. Làm ơn kiểm tra lại Pool ID và đảm bảo rằng pool không bị hủy đăng ký.", "api.errors.rewardAddressEmpty": "Địa chỉ trả thưởng chưa hiển thị cho đến khi người dùng có phần thưởng.", "api.errors.rollbackApiError": "Rollback đã được phát hiện.", @@ -202,6 +203,9 @@ "global.yoroi": "Yoroi", "global.yoroi.intro": "Light wallet for Cardano assets", "global.yoroiNightly": "Yoroi Nightly", + "global.labels.unexpectedError": "Something unexpected happened", + "global.labels.contactSupport": "If this keep happening, contact our support team.", + "global.labels.pleaseGoBack": "Please go back and try again.", "incorrectTime.line1": "CẢNH BÁO: thời gian trên máy tính của bạn không khớp với máy chủ. Điều này có thể gây ra kết quả không mong muốn", "incorrectTime.line2": "Thời gian khác biệt:", "incorrectTime.line3": "Đồng bộ hóa thời gian trên thiết bị của bạn để giải quyết vấn đề này", @@ -289,6 +293,8 @@ "settings.general.languageSelect.labelInfo": "Bản dịch ngôn ngữ đã chọn được cung cấp đầy đủ bởi cộng đồng", "settings.general.translation.acknowledgment": "Xin cảm ơn những điều sau đây vì sự đóng góp của họ: ", "settings.general.translation.contributors": "_", + "settings.general.theme.light": "Light Theme", + "settings.general.theme.dark": "Dark Theme", "settings.menu.analytics.link.label": "Analytics", "settings.menu.assetDeposit.link.label": "Khóa tài sản đặt cọc", "settings.menu.blockchain.link.label": "Blockchain", @@ -588,9 +594,10 @@ "wallet.delegation.transaction.success.title": "Ủy quyền thành công", "wallet.deprecation.byronLine1": "Bản nâng cấp giao thức Shelley thêm một loại ví Shelley mới hỗ trợ ủy quyền.", "wallet.deprecation.byronLine2": "Để ủy quyền {ticker} của bạn, bạn sẽ cần nâng cấp lên ví Shelley.", - "wallet.emptyWalletMessage": "Your wallet is empty", - "wallet.emptyWalletMessageSubtitle": "Top up your wallet safely using our trusted partners", + "wallet.emptyWalletMessage": "Get your first ADA in Yoroi 🔥", + "wallet.emptyWalletMessageSubtitle": "Buy ADA directly within your wallet and unlock the power of Cardano.", "wallet.hw.common.error.101": "Người dùng không cấp các quyền cần thiết. Làm ơn hãy thử lại.", + "wallet.hw.data.sign.unkown.address": "The requested signing address is not found in this wallet", "wallet.hw.incorrectDevice": "Đã phát hiện thiết bị không chính xác. Cần có thiết bị {expectedDeviceId}, nhưng có thiết bị {responseDeviceId}. Vui lòng cắm đúng thiết bị", "wallet.hw.incorrectVersion": "Đã phát hiện phiên bản thiết bị không chính xác. Chúng tôi hỗ trợ phiên bản {supportedVersions} nhưng bạn có phiên bản {responseVersion}.", "wallet.hw.ledger.app.not.running": "Ứng dụng Cardano không chạy trên Sổ cái của bạn", @@ -601,6 +608,8 @@ "wallet.hw.ledger.common.error.103": "Thiết bị sổ cái bị khóa, vui lòng mở khóa và thử lại.", "wallet.hw.ledger.common.error.104": "Hết thời gian chờ của thiết bị sổ cái, vui lòng thử lại.", "wallet.hw.ledger.common.error.105": "Lỗi mạng. Xin vui lòng kiểm tra kết nối Internet của bạn.", + "wallet.hw.ledger.error.deviceStatus": "Invalid or oversized data for Ledger.", + "wallet.hw.ledger.error.deviceVersionNoDataSigning": "CIP-8 message signing not supported by your Ledger app version", "wallet.hw.ledger.data.sign.unsupported.error": "Ví Trezor không hỗ trợ ký dữ liệu tại thời điểm này", "wallet.hw.trezor.data.sign.unsupported.error": "Ví Trezor không hỗ trợ ký dữ liệu tại thời điểm này", "wallet.hw.tx.hash.error": "Giá trị hàm băm giao dịch được tính toán bởi tiện ích mở rộng Yoroi và từ thiết bị không khớp.", @@ -887,7 +896,6 @@ "wallet.syncingOverlay.explanation": "Vui lòng đợi trong khi chúng tôi xử lý dữ liệu ví. Điều này có thể mất một thời gian.", "wallet.syncingOverlay.return": "Quay lại ví của tôi", "wallet.syncingOverlay.title": "Đồng bộ ví", - "wallet.topbar.dialog.cardano": "Cardano, ADA", "wallet.topbar.dialog.tokenTypes": "Các kiểu token", "wallet.topbar.dialog.totalBalance": "Tổng số dư", "wallet.transaction.address.from": "Từ địa chỉ", diff --git a/packages/yoroi-extension/app/routes-config.js b/packages/yoroi-extension/app/routes-config.js index 2121dd72fe..df36933045 100644 --- a/packages/yoroi-extension/app/routes-config.js +++ b/packages/yoroi-extension/app/routes-config.js @@ -74,6 +74,7 @@ export const ROUTES = { }, SWAP: { ROOT: '/swap', + ERROR: '/swap/page-error', ORDERS: '/swap/orders', }, EXCHANGE_END: '/exchange-end', diff --git a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js index 2487d980f4..f29a173354 100644 --- a/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js +++ b/packages/yoroi-extension/app/stores/ada/AdaDelegationTransactionStore.js @@ -10,6 +10,7 @@ import { ROUTES } from '../../routes-config'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; import type { WalletState } from '../../../chrome/extension/background/types'; +import { getProtocolParameters } from '../../api/thunk'; export default class AdaDelegationTransactionStore extends Store { @observable createWithdrawalTx: LocalizedRequest> = new LocalizedRequest< @@ -76,6 +77,8 @@ export default class AdaDelegationTransactionStore extends Store { return await this.api.ada.createWithdrawalTx({ wallet: request.wallet, @@ -119,6 +125,7 @@ export default class AdaDelegationTransactionStore extends Store RustModule.WalletV4.BigNum.from_str(config.LinearFee.coefficient), RustModule.WalletV4.BigNum.from_str(config.LinearFee.constant), ), - coinsPerUtxoWord: RustModule.WalletV4.BigNum.from_str(config.CoinsPerUtxoWord), + coinsPerUtxoByte: RustModule.WalletV4.BigNum.from_str(config.CoinsPerUtxoByte), poolDeposit: RustModule.WalletV4.BigNum.from_str(config.PoolDeposit), networkId: selectedNetwork.NetworkId, }, diff --git a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js index 6cee5d3028..76b8c7891f 100644 --- a/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js +++ b/packages/yoroi-extension/app/stores/ada/LedgerConnectStore.js @@ -28,7 +28,7 @@ import { getCardanoHaskellBaseConfig } from '../../api/ada/lib/storage/database/ import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; import type { GetExtendedPublicKeyResponse, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; -import { createHardwareWallet } from '../../api/thunk'; +import { createHardwareWallet, getProtocolParameters } from '../../api/thunk'; import type { CreateHardwareWalletRequest } from '../../api/thunk'; import type { WalletState } from '../../../chrome/extension/background/types'; @@ -211,6 +211,7 @@ export default class LedgerConnectStore const fullConfig = getCardanoHaskellBaseConfig( selectedNetwork ); + const protocolParameters = await getProtocolParameters({ networkId: selectedNetwork.NetworkId }); try { const currentTime = this.stores.serverConnectionStore.serverTime ?? new Date(); await this.stores.substores.ada.yoroiTransfer.transferRequest.execute({ @@ -223,6 +224,7 @@ export default class LedgerConnectStore absSlotNumber: new BigNumber(TimeUtils.timeToAbsoluteSlot(fullConfig, currentTime)), network: selectedNetwork, defaultToken: this.stores.tokenInfoStore.getDefaultTokenInfo(selectedNetwork.NetworkId), + protocolParameters, }).promise; } catch (_e) { // usually this means no internet connection or not enough ADA to upgrade diff --git a/packages/yoroi-extension/app/stores/ada/SwapStore.js b/packages/yoroi-extension/app/stores/ada/SwapStore.js index f06149c4b4..187d324fc6 100644 --- a/packages/yoroi-extension/app/stores/ada/SwapStore.js +++ b/packages/yoroi-extension/app/stores/ada/SwapStore.js @@ -29,7 +29,7 @@ import type { RemoteUnspentOutput } from '../../api/ada/lib/state-fetch/types'; import type { CardanoConnectorSignRequest } from '../../connector/types'; import type { AddressDetails } from '../../api/ada'; import type{ WalletState } from '../../../chrome/extension/background/types'; -import { broadcastTransaction } from '../../api/thunk'; +import { broadcastTransaction, getProtocolParameters } from '../../api/thunk'; import { getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks'; import { CoreAddressTypes } from '../../api/ada/lib/storage/database/primitives/enums'; @@ -102,6 +102,7 @@ export default class SwapStore extends Store { const submittedTxs = wallet.submittedTransactions; const reorgTargetAmount = '2000000'; const firstExternalAddress: AddressDetails = wallet.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0]; + const protocolParameters = await getProtocolParameters(wallet); const { unsignedTx, collateralOutputAddressSet } = await this.api.ada._createReorgTx( getNetworkById(wallet.networkId), wallet.balance.getDefaults(), @@ -113,6 +114,7 @@ export default class SwapStore extends Store { addressedUtxos, submittedTxs, firstExternalAddress.address, + protocolParameters, ); const unsignedTxHex = unsignedTx.unsignedTx.build_tx().to_hex(); const hash = transactionHexToHash(unsignedTxHex); @@ -156,7 +158,7 @@ export default class SwapStore extends Store { feFees: {| tokenId: string, quantity: string |}, ptFees: {| deposit: string, batcher: string |}, poolProvider: string, - |}) => Promise = ({ + |}) => Promise = async ({ wallet, contractAddress, datum, @@ -196,10 +198,12 @@ export default class SwapStore extends Store { amount: createSwapFeFeeAmount({ wallet, feFees }), }); } + const protocolParameters = await getProtocolParameters(wallet); return this.api.ada.createSimpleTx({ publicDeriver: wallet, entries, metadata, + protocolParameters, }); }; diff --git a/packages/yoroi-extension/app/stores/ada/VotingStore.js b/packages/yoroi-extension/app/stores/ada/VotingStore.js index b48a57b9f2..f53cc14358 100644 --- a/packages/yoroi-extension/app/stores/ada/VotingStore.js +++ b/packages/yoroi-extension/app/stores/ada/VotingStore.js @@ -28,7 +28,7 @@ import { loadCatalystRoundInfo, saveCatalystRoundInfo, } from '../../api/localSt import { CoreAddressTypes } from '../../api/ada/lib/storage/database/primitives/enums'; import { derivePublicByAddressing } from '../../api/ada/lib/cardanoCrypto/deriveByAddressing'; import type { WalletState } from '../../../chrome/extension/background/types'; -import { getPrivateStakingKey } from '../../api/thunk'; +import { getPrivateStakingKey, getProtocolParameters } from '../../api/thunk'; export const ProgressStep = Object.freeze({ GENERATE: 0, @@ -231,6 +231,8 @@ export default class VotingStore extends Store { const firstAddress = publicDeriver.externalAddressesByType[CoreAddressTypes.CARDANO_BASE][0]; + const protocolParameters = await getProtocolParameters(publicDeriver); + let votingRegTxPromise; if (publicDeriver.type !== 'mnemonic') { @@ -261,6 +263,7 @@ export default class VotingStore extends Store { paymentAddress: firstAddress.address, nonce: currentAbsoluteSlot, }, + protocolParameters, }).promise; } else if (publicDeriver.type === 'ledger') { votingRegTxPromise = this.createVotingRegTx.execute({ @@ -274,6 +277,7 @@ export default class VotingStore extends Store { paymentAddress: firstAddress.address, nonce: currentAbsoluteSlot, }, + protocolParameters, }).promise; } else { throw new Error(`${nameof(this._createTransaction)} unexpected hardware wallet type`); @@ -302,6 +306,7 @@ export default class VotingStore extends Store { wallet: publicDeriver, absSlotNumber, normalWallet: { metadata: trxMeta }, + protocolParameters, }).promise; } diff --git a/packages/yoroi-extension/app/stores/index.js b/packages/yoroi-extension/app/stores/index.js index bd56308f05..7dad1c5bcc 100644 --- a/packages/yoroi-extension/app/stores/index.js +++ b/packages/yoroi-extension/app/stores/index.js @@ -26,6 +26,8 @@ import TokenInfoStore from './toplevel/TokenInfoStore'; import ExplorerStore from './toplevel/ExplorerStore'; import ServerConnectionStore from './toplevel/ServerConnectionStore'; import ConnectorStore from './toplevel/DappConnectorStore' +import ProtocolParametersStore from './toplevel/ProtocolParametersStore'; + /** Map of var name to class. Allows dynamic lookup of class so we can init all stores one loop */ const storeClasses = Object.freeze({ stateFetchStore: StateFetchStore, @@ -49,6 +51,7 @@ const storeClasses = Object.freeze({ yoroiTransfer: YoroiTransferStore, explorers: ExplorerStore, connector: ConnectorStore, + protocolParameters: ProtocolParametersStore, // note: purposely exclude substores and router }); @@ -79,6 +82,7 @@ export type StoresMap = {| |}, // $FlowFixMe[value-as-type] router: RouterStore, + protocolParameters: ProtocolParametersStore, |}; /** Constant that represents the stores across the lifetime of the application */ @@ -111,6 +115,7 @@ const stores: StoresMap = (observable({ substores: null, router: null, connector: null, + protocolParameters: null, }): any); function initializeSubstore( diff --git a/packages/yoroi-extension/app/stores/stateless/topbarCategories.js b/packages/yoroi-extension/app/stores/stateless/topbarCategories.js index e9dd5c920b..33623e584e 100644 --- a/packages/yoroi-extension/app/stores/stateless/topbarCategories.js +++ b/packages/yoroi-extension/app/stores/stateless/topbarCategories.js @@ -128,7 +128,6 @@ export const CARDANO_DELEGATION: TopbarCategory = registerCategory({ const { networkId } = request; return ( (environment.isTest() || - networkId === networks.CardanoTestnet.NetworkId || networkId === networks.CardanoPreprodTestnet.NetworkId || networkId === networks.CardanoPreviewTestnet.NetworkId || networkId === networks.CardanoSanchoTestnet.NetworkId) diff --git a/packages/yoroi-extension/app/stores/toplevel/ProtocolParametersStore.js b/packages/yoroi-extension/app/stores/toplevel/ProtocolParametersStore.js new file mode 100644 index 0000000000..383b509e2f --- /dev/null +++ b/packages/yoroi-extension/app/stores/toplevel/ProtocolParametersStore.js @@ -0,0 +1,35 @@ +// @flow +// Some legacy code expects the protocol parameters to be accessible synchronously. So this store caches +// the protocol parameters to make it accessible synchronously. + +import Store from '../base/Store'; +import type { ProtocolParameters } from '@emurgo/yoroi-lib/dist/protocol-parameters/models'; +import { getProtocolParameters } from '../../api/thunk'; +import { networks } from '../../api/ada/lib/storage/database/prepackaged/networks'; +import LocalizedRequest from '../lib/LocalizedRequest'; +import { observable } from 'mobx'; + +export default class ProtocolParametersStore< + StoresMapType: { ... }, // no dependency on other stores +> extends Store { + @observable loadProtocolParametersRequest: LocalizedRequest<() => Promise> = + new LocalizedRequest(() => this.loadProtocolParameters()); + + cache: Map = new Map(); + + async loadProtocolParameters(): Promise { + for (const key of Object.keys(networks)) { + const networkId = networks[key].NetworkId; + const protocolParameters = await getProtocolParameters({ networkId }); + this.cache.set(networkId, protocolParameters); + } + } + + getProtocolParameters(networkId: number): ProtocolParameters { + const protocolParameters = this.cache.get(networkId); + if (protocolParameters == null) { + throw new Error(`unexpectedly missing protocol parameters for network id ${networkId}`); + } + return protocolParameters; + } +} diff --git a/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js b/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js index 7a87a30e93..fe8b8e85b3 100644 --- a/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/TransactionBuilderStore.js @@ -7,22 +7,17 @@ import LocalizedRequest from '../lib/LocalizedRequest'; import type { ISignRequest } from '../../api/common/lib/transactions/ISignRequest'; import type { IGetAllUtxosResponse } from '../../api/ada/lib/storage/models/PublicDeriver/interfaces'; -import { - isCardanoHaskell, getCardanoHaskellBaseConfig, getNetworkById, -} from '../../api/ada/lib/storage/database/prepackaged/networks'; +import { isCardanoHaskell, getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks'; import type { TransactionMetadata } from '../../api/ada/lib/storage/bridge/metadataUtils'; -import { - MultiToken, -} from '../../api/common/lib/MultiToken'; +import { MultiToken } from '../../api/common/lib/MultiToken'; import type { TokenRow, } from '../../api/ada/lib/storage/database/primitives/tables'; import { getDefaultEntryToken } from './TokenInfoStore'; -import { - cardanoMinAdaRequiredFromAssets_coinsPerWord, -} from '../../api/ada/transactions/utils'; import type { ActionsMap } from '../../actions/index'; import type { StoresMap } from '../index'; import { maxSendableADA } from '../../api/ada/transactions/shelley/transactions'; import type { WalletState } from '../../../chrome/extension/background/types'; +import { cardanoMinAdaRequiredFromAssets } from '../../api/ada/transactions/utils'; +import { getProtocolParameters } from '../../api/thunk'; export type SetupSelfTxRequest = {| publicDeriver: WalletState, @@ -117,12 +112,11 @@ export default class TransactionBuilderStore extends Store |}>) => string = (tokens) => { const publicDeriver = this.stores.wallets.selected; - if (!publicDeriver) throw new Error(`${nameof(this.minAda)} requires wallet to be selected`); + if (!publicDeriver) throw new Error(`${nameof(this.calculateMinAda)} requires wallet to be selected`); const network = getNetworkById(publicDeriver.networkId); const defaultToken = this.stores.tokenInfoStore.getDefaultTokenInfo(network.NetworkId) if (!isCardanoHaskell(network)) return '0'; const filteredTokens = tokens.filter(({ token }) => !token.IsDefault); if (filteredTokens.length === 0) return String(1_000_000); - const fullConfig = getCardanoHaskellBaseConfig(network); - const squashedConfig = fullConfig.reduce((acc, next) => Object.assign(acc, next), {}); const fakeAmount = new BigNumber('1000000'); const fakeMultitoken = new MultiToken( [{ @@ -291,9 +283,12 @@ export default class TransactionBuilderStore extends Store this.api.ada.createUnsignedTx({ publicDeriver, receiver, @@ -338,6 +335,7 @@ export default class TransactionBuilderStore extends Store { throw new Error('unexpected nullish headRequest.promise'); } headRequest.promise.then(result => { - const { txs } = this.getTxHistoryState(publicDeriverId); - runInAction(() => { - txs.splice(0, 0, ...result); - }); - return this._afterLoadingNewTxs(result, publicDeriver); + return this.updateNewTransactions(result, publicDeriver); }).catch(error => { console.error('error when loading transaction list head', error) }); @@ -550,9 +546,13 @@ export default class TransactionsStore extends Store { publicDeriver: WalletState, ): Promise { const { txs } = this.getTxHistoryState(publicDeriver.publicDeriverId); + // newTxs is not supposed to have duplicate txs to existing ones but there were unknown cases + // reported + const existingTxHashes = new Set(txs.map(tx => tx.txid)); + const unseenTxs = [...new Set(newTxs)].filter(tx => !existingTxHashes.has(tx.txid)); runInAction(() => { - txs.splice(0, 0, ...newTxs); + txs.splice(0, 0, ...unseenTxs); }); - await this._afterLoadingNewTxs(txs, publicDeriver); + await this._afterLoadingNewTxs(unseenTxs, publicDeriver); } } diff --git a/packages/yoroi-extension/app/stores/toplevel/WalletStore.js b/packages/yoroi-extension/app/stores/toplevel/WalletStore.js index 747ca142c9..b420f8e86d 100644 --- a/packages/yoroi-extension/app/stores/toplevel/WalletStore.js +++ b/packages/yoroi-extension/app/stores/toplevel/WalletStore.js @@ -156,7 +156,7 @@ export default class WalletStore extends Store { } @computed get selected(): null | WalletState { - if (typeof this.selectedIndex === 'number') { + if (this.selectedIndex != null) { return this.wallets[this.selectedIndex]; } return null; @@ -415,6 +415,9 @@ export default class WalletStore extends Store { @action onRenameSelectedWallet: (string) => void = (newName) => { this.selectedWalletName = newName; + if (this.selectedIndex != null) { + this.wallets[this.selectedIndex].name = newName; + } } } diff --git a/packages/yoroi-extension/app/styles/context/layout.js b/packages/yoroi-extension/app/styles/context/layout.js index 8dc955f4ec..26f2cf1994 100644 --- a/packages/yoroi-extension/app/styles/context/layout.js +++ b/packages/yoroi-extension/app/styles/context/layout.js @@ -26,6 +26,7 @@ const LayoutProvider = (props: Object): Node => { selectedLayout: localLayout, currentTheme: layout, isRevampLayout: localLayout === 'REVAMP', + // disabling legacy UI renderLayoutComponent: (layoutMap: LayoutComponentMap = {}) => { const selectedComponent = layoutMap[localLayout]; return selectedComponent; diff --git a/packages/yoroi-extension/app/styles/globalStyles.js b/packages/yoroi-extension/app/styles/globalStyles.js index 66d2fbee6c..e4c3e9c8bf 100644 --- a/packages/yoroi-extension/app/styles/globalStyles.js +++ b/packages/yoroi-extension/app/styles/globalStyles.js @@ -2,6 +2,7 @@ // @flow import type { Node } from 'react'; import { GlobalStyles } from '@mui/material'; +import type { DSColorPalette } from './themes/types'; const getColorPath = (themePalette: any, color: string) => { const path = []; @@ -67,7 +68,14 @@ export const formatPalette = (palette: any, theme: any): FormatedPalette => { return formatedPalette; }; -export function getMainYoroiPalette(theme: Object): { [string]: string | number } { +type ColorPaletteForStyles = {| + name: 'modern' | 'classic' | 'revamp-light', + palette: {| + ds: DSColorPalette, + |}, +|}; + +export function getMainYoroiPalette(theme: ColorPaletteForStyles): { [string]: string | number } { // if (theme.name === 'light-theme' || theme.name === 'dark-theme') return {}; return { @@ -82,57 +90,57 @@ export function getMainYoroiPalette(theme: Object): { [string]: string | number to keep consistency and allow users to override few options from BASE if they want to */ /* === BASE === */ - '--yoroi-palette-common-white': theme.palette.common.white, - '--yoroi-palette-common-black': theme.palette.common.black, - - '--yoroi-palette-primary-50': theme.palette.primary['50'], - '--yoroi-palette-primary-100': theme.palette.primary['100'], - '--yoroi-palette-primary-200': theme.palette.primary['200'], - '--yoroi-palette-primary-300': theme.palette.primary['300'], - '--yoroi-palette-primary-contrastText': theme.palette.primary.contrastText, - - '--yoroi-palette-secondary-50': theme.palette.secondary['50'], - '--yoroi-palette-secondary-100': theme.palette.secondary['100'], - '--yoroi-palette-secondary-200': theme.palette.secondary['200'], - '--yoroi-palette-secondary-300': theme.palette.secondary['300'], - '--yoroi-palette-secondary-contrastText': theme.palette.secondary.contrastText, - - '--yoroi-palette-error-50': theme.palette?.error['50'], - '--yoroi-palette-error-100': theme.palette?.error['100'], - '--yoroi-palette-error-200': theme.palette?.error['200'], - - '--yoroi-palette-cyan-50': theme.palette.cyan['50'], - '--yoroi-palette-cyan-100': theme.palette.cyan['100'], - - '--yoroi-palette-gray-50': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['50'], - '--yoroi-palette-gray-100': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['100'], - '--yoroi-palette-gray-200': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['200'], - '--yoroi-palette-gray-300': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['300'], - '--yoroi-palette-gray-400': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['400'], - '--yoroi-palette-gray-500': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['500'], - '--yoroi-palette-gray-600': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['600'], - '--yoroi-palette-gray-700': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['700'], - '--yoroi-palette-gray-800': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['800'], - '--yoroi-palette-gray-900': (theme.palette.grey ? theme.palette.grey : theme.palette.grayscale)['900'], - '--yoroi-palette-gray-max': theme.palette.common.black, - '--yoroi-palette-gray-min': theme.palette.common.white, - '--yoroi-palette-background-overlay': theme.palette.background.overlay, - - '--yoroi-palette-tx-status-pending-background': theme.palette.txStatus?.pending.background, - '--yoroi-palette-tx-status-pending-text': theme.palette.txStatus?.pending.text, - '--yoroi-palette-tx-status-pending-stripes': theme.palette.txStatus?.pending.stripes, - '--yoroi-palette-tx-status-high-background': theme.palette.txStatus?.high.background, - '--yoroi-palette-tx-status-high-text': theme.palette.txStatus?.high.text, - '--yoroi-palette-tx-status-failed-background': theme.palette.txStatus?.failed.background, - '--yoroi-palette-tx-status-failed-text': theme.palette.txStatus?.failed.text, - '--yoroi-palette-tx-status-medium-background': theme.palette.txStatus?.medium.background, - '--yoroi-palette-tx-status-medium-text': theme.palette.txStatus?.medium.text, - '--yoroi-palette-tx-status-low-background': theme.palette.txStatus?.low.background, - '--yoroi-palette-tx-status-low-text': theme.palette.txStatus?.low.text, - - '--yoroi-palette-background-banner-warning': theme.palette.background.banner.warning, - '--yoroi-palette-background-walletAdd-title': theme.palette.background.walletAdd.title, - '--yoroi-palette-background-walletAdd-subtitle': theme.palette.background.walletAdd.subtitle, + '--yoroi-palette-common-white': theme.palette.ds.white_static, + '--yoroi-palette-common-black': theme.palette.ds.black_static, + + '--yoroi-palette-primary-50': theme.palette.ds.primary_100, + '--yoroi-palette-primary-100': theme.palette.ds.primary_100, + '--yoroi-palette-primary-200': theme.palette.ds.primary_200, + '--yoroi-palette-primary-300': theme.palette.ds.primary_300, + '--yoroi-palette-primary-contrastText': theme.palette.ds.text_gray_medium, + + '--yoroi-palette-secondary-50': theme.palette.ds.primary_100, + '--yoroi-palette-secondary-100': theme.palette.ds.primary_100, + '--yoroi-palette-secondary-200': theme.palette.ds.primary_200, + '--yoroi-palette-secondary-300': theme.palette.ds.primary_300, + '--yoroi-palette-secondary-contrastText': theme.palette.ds.text_gray_medium, + + '--yoroi-palette-error-50': theme.palette.ds.text_error, + '--yoroi-palette-error-100': theme.palette.ds.sys_magenta_100, + '--yoroi-palette-error-200': theme.palette.ds.sys_magenta_300, + + '--yoroi-palette-cyan-50': theme.palette.ds.sys_cyan_100, + '--yoroi-palette-cyan-100': theme.palette.ds.sys_cyan_500, + + '--yoroi-palette-gray-50': theme.palette.ds.gray_50, + '--yoroi-palette-gray-100': theme.palette.ds.gray_100, + '--yoroi-palette-gray-200': theme.palette.ds.gray_200, + '--yoroi-palette-gray-300': theme.palette.ds.gray_300, + '--yoroi-palette-gray-400': theme.palette.ds.gray_400, + '--yoroi-palette-gray-500': theme.palette.ds.gray_500, + '--yoroi-palette-gray-600': theme.palette.ds.gray_600, + '--yoroi-palette-gray-700': theme.palette.ds.gray_700, + '--yoroi-palette-gray-800': theme.palette.ds.gray_800, + '--yoroi-palette-gray-900': theme.palette.ds.gray_900, + '--yoroi-palette-gray-max': theme.palette.ds.black_static, + '--yoroi-palette-gray-min': theme.palette.ds.white_static, + '--yoroi-palette-background-overlay': theme.palette.ds.bg_gradient_1, + + '--yoroi-palette-tx-status-pending-background': theme.palette.ds.web_overlay, + '--yoroi-palette-tx-status-pending-text': theme.palette.ds.text_gray_medium, + '--yoroi-palette-tx-status-pending-stripes': theme.palette.ds.gray_200, + '--yoroi-palette-tx-status-high-background': theme.palette.ds.el_gray_max, + '--yoroi-palette-tx-status-high-text': theme.palette.ds.text_gray_medium, + '--yoroi-palette-tx-status-failed-background': theme.palette.ds.sys_magenta_300, + '--yoroi-palette-tx-status-failed-text': theme.palette.ds.text_error, + '--yoroi-palette-tx-status-medium-background': theme.palette.ds.gray_300, + '--yoroi-palette-tx-status-medium-text': theme.palette.ds.text_gray_medium, + '--yoroi-palette-tx-status-low-background': theme.palette.ds.sys_magenta_100, + '--yoroi-palette-tx-status-low-text': theme.palette.ds.text_gray_low, + + '--yoroi-palette-background-banner-warning': theme.palette.ds.sys_orange_500, + '--yoroi-palette-background-walletAdd-title': theme.palette.ds.text_gray_medium, + '--yoroi-palette-background-walletAdd-subtitle': theme.palette.ds.text_gray_low, /* === BUTTON === */ // button primary variant @@ -206,9 +214,9 @@ export function getMainYoroiPalette(theme: Object): { [string]: string | number '--yoroi-comp-dialog-min-width-md': '540px', '--yoroi-comp-dialog-min-width-lg': '600px', - '--yoroi-sidebar-text': theme.palette.background.sidebar.text, - '--yoroi-sidebar-background': `linear-gradient(22.58deg, ${theme.palette.background.sidebar.start} 0%, ${theme.palette.background.sidebar.end} 100%)`, - '--yoroi-sidebar-end': theme.palette.background.sidebar.end, + '--yoroi-sidebar-text': theme.palette.ds.text_gray_medium, + '--yoroi-sidebar-background': `linear-gradient(22.58deg, ${theme.palette.ds.el_primary_medium} 0%, ${theme.palette.ds.el_primary_min} 100%)`, + '--yoroi-sidebar-end': theme.palette.ds.el_primary_min, '--yoroi-notification-message-background': 'rgba(21, 209, 170, 0.8)', diff --git a/packages/yoroi-extension/app/styles/overrides/Link.js b/packages/yoroi-extension/app/styles/overrides/Link.js new file mode 100644 index 0000000000..ed9561334b --- /dev/null +++ b/packages/yoroi-extension/app/styles/overrides/Link.js @@ -0,0 +1,11 @@ +// @flow + +const Link = { + styleOverrides: { + root: { + color: 'ds.text_primary_medium', + textDecoration: 'none' + }, + }, +}; +export { Link }; diff --git a/packages/yoroi-extension/app/styles/overrides/index.js b/packages/yoroi-extension/app/styles/overrides/index.js index a996d34d02..b876b0e4b9 100644 --- a/packages/yoroi-extension/app/styles/overrides/index.js +++ b/packages/yoroi-extension/app/styles/overrides/index.js @@ -1,15 +1,17 @@ // @flow export * from './Button'; export * from './Checkbox'; -export * from './TextField'; -export * from './OutlinedInput'; +export * from './Chip'; export * from './FormControl'; export * from './FormHelperText'; -export * from './Select'; -export * from './MenuItem'; +export * from './InputLabel'; +export * from './Link'; export * from './Menu'; -export * from './Tabs'; +export * from './MenuItem'; +export * from './OutlinedInput'; +export * from './Select'; export * from './TabPanel'; -export * from './Chip'; +export * from './Tabs'; +export * from './TextField'; export * from './Tooltip'; -export * from './InputLabel'; + diff --git a/packages/yoroi-extension/app/styles/themes/base-palettes/dark-palette.js b/packages/yoroi-extension/app/styles/themes/base-palettes/dark-palette.js index 586dfe96cb..e4d690cc07 100644 --- a/packages/yoroi-extension/app/styles/themes/base-palettes/dark-palette.js +++ b/packages/yoroi-extension/app/styles/themes/base-palettes/dark-palette.js @@ -8,7 +8,7 @@ export const darkPalette = { primary_400: '#2E4BB0', primary_300: '#304489', primary_200: '#242D4F', - primary_100: '#171B28', + primary_100: '#1F253B', secondary_900: '#E4F7F3', secondary_800: '#C6F7ED', @@ -39,8 +39,8 @@ export const darkPalette = { sys_magenta_700: '#FFC0D0', sys_magenta_600: '#FB9CB5', sys_magenta_500: '#FF7196', - sys_magenta_300: '#572835', - sys_magenta_100: '#2F171D', + sys_magenta_300: '#64303E', + sys_magenta_100: '#3B252A', sys_cyan_500: '#59B1F4', sys_cyan_100: '#112333', @@ -51,9 +51,10 @@ export const darkPalette = { sys_orange_500: '#FAB357', sys_orange_100: '#291802', - bg_gradient_1: 'linear-gradient(195.39deg, #1AE3BB 26%, #4B6DDE 10%,#4B6DDE 16%)', - bg_gradient_2: 'linear-gradient(205.51deg, #0B997D 48%, #08C29D 8%)', - bg_gradient_3: 'linear-gradient(23deg, #2E4BB0 100%, #2B3E7D 100%)', + bg_gradient_1: + 'linear-gradient(195deg, rgba(26, 227, 187, 0.26) 0.57%, rgba(75, 109, 222, 0.10) 41.65%, rgba(75, 109, 222, 0.16) 100%)', + bg_gradient_2: 'linear-gradient(206deg, rgba(11, 153, 125, 0.48) -10.43%, rgba(8, 194, 157, 0.08) 100%)', + bg_gradient_3: 'linear-gradient(23deg, #2E4BB0 15.04%, #2B3E7D 84.96%)', special_web_overlay: 'rgba(31, 35, 46, 0.80)', special_web_bg_sidebar: 'rgba(0, 0, 0, 0.16)', }; diff --git a/packages/yoroi-extension/app/styles/themes/base-palettes/light-palette.js b/packages/yoroi-extension/app/styles/themes/base-palettes/light-palette.js index 840d74f308..7c2ec6cc89 100644 --- a/packages/yoroi-extension/app/styles/themes/base-palettes/light-palette.js +++ b/packages/yoroi-extension/app/styles/themes/base-palettes/light-palette.js @@ -51,8 +51,8 @@ export const lightPalette = { sys_orange_500: '#ED8600', sys_orange_100: '#FFF2E2', - bg_gradient_1: 'linear-gradient(180deg, #93F5E1 100%, #E4E8F7 100%)', - bg_gradient_2: 'linear-gradient(180deg, #93F5E1 100%, #C6F7ED 100%)', + bg_gradient_1: 'linear-gradient(312deg, #C6F7ED 0%, #E4E8F7 70.58%)', + bg_gradient_2: 'linear-gradient(206deg, rgba(11, 153, 125, 0.48) -10.43%, rgba(8, 194, 157, 0.08) 100%)', bg_gradient_3: 'linear-gradient(180deg, #244ABF 100%, #4760FF 100%)', special_web_overlay: 'rgba(31, 35, 46, 0.80)', special_web_bg_sidebar: '#1F232ECC', diff --git a/packages/yoroi-extension/app/styles/themes/dark-theme-mui.js b/packages/yoroi-extension/app/styles/themes/dark-theme-mui.js index c3000d01d4..54e3936987 100644 --- a/packages/yoroi-extension/app/styles/themes/dark-theme-mui.js +++ b/packages/yoroi-extension/app/styles/themes/dark-theme-mui.js @@ -1,24 +1,25 @@ // @flow import { createTheme } from '@mui/material/styles'; -import { commonTheme } from './common-theme'; import { deepmerge } from '@mui/utils'; -import { darkThemeBase } from './dark-theme-base'; import { - DarkButton, Checkbox, - TextField, - OutlinedInput, - FormHelperText, + Chip, + DarkButton, FormControl, + FormHelperText, + InputLabel, + Link, Menu, MenuItem, - Tabs, - TabPanel, - Chip, - Tooltip, - InputLabel, + OutlinedInput, Select, + TabPanel, + Tabs, + TextField, + Tooltip } from '../overrides'; +import { commonTheme } from './common-theme'; +import { darkThemeBase } from './dark-theme-base'; const darkThemeComponents = { components: { @@ -36,6 +37,7 @@ const darkThemeComponents = { MuiTabPanel: TabPanel, MuiChip: Chip, MuiTooltip: Tooltip, + MuiLink: Link, }, }; diff --git a/packages/yoroi-extension/app/styles/themes/light-theme-mui.js b/packages/yoroi-extension/app/styles/themes/light-theme-mui.js index 16dca89166..1e7e905f8e 100644 --- a/packages/yoroi-extension/app/styles/themes/light-theme-mui.js +++ b/packages/yoroi-extension/app/styles/themes/light-theme-mui.js @@ -1,24 +1,25 @@ // @flow import { createTheme } from '@mui/material/styles'; -import { commonTheme } from './common-theme'; import { deepmerge } from '@mui/utils'; -import { lightThemeBase } from './light-theme-base'; import { - LightButton, Checkbox, - TextField, - OutlinedInput, - FormHelperText, + Chip, FormControl, + FormHelperText, + InputLabel, + LightButton, + Link, Menu, MenuItem, - Tabs, - TabPanel, - Chip, - Tooltip, - InputLabel, + OutlinedInput, Select, + TabPanel, + Tabs, + TextField, + Tooltip } from '../overrides'; +import { commonTheme } from './common-theme'; +import { lightThemeBase } from './light-theme-base'; const lightThemeComponents = { components: { @@ -36,6 +37,7 @@ const lightThemeComponents = { MuiTabPanel: TabPanel, MuiChip: Chip, MuiTooltip: Tooltip, + MuiLink: Link, }, }; diff --git a/packages/yoroi-extension/app/styles/themes/themed-palettes/dark.js b/packages/yoroi-extension/app/styles/themes/themed-palettes/dark.js index 3196fc2533..20dda15ce0 100644 --- a/packages/yoroi-extension/app/styles/themes/themed-palettes/dark.js +++ b/packages/yoroi-extension/app/styles/themes/themed-palettes/dark.js @@ -1,10 +1,11 @@ // @flow import { darkPalette } from '../base-palettes/dark-palette'; import { tokens } from '../tokens/tokens'; +import type { DSColorPalette } from '../types'; const { opacity } = tokens; -export const dark = { +export const dark: DSColorPalette = { ...darkPalette, text_primary_max: darkPalette.primary_700, // hover, text, button, links, text in tabs, chips diff --git a/packages/yoroi-extension/app/styles/themes/themed-palettes/light.js b/packages/yoroi-extension/app/styles/themes/themed-palettes/light.js index 6636c97b39..1451dcd948 100644 --- a/packages/yoroi-extension/app/styles/themes/themed-palettes/light.js +++ b/packages/yoroi-extension/app/styles/themes/themed-palettes/light.js @@ -1,10 +1,11 @@ // @flow import { lightPalette } from '../base-palettes/light-palette'; import { tokens } from '../tokens/tokens'; +import type { DSColorPalette } from '../types'; const { opacity } = tokens; -export const light = { +export const light: DSColorPalette = { ...lightPalette, text_primary_max: lightPalette.primary_600, // hover, text, button, links, text in tabs, chips diff --git a/packages/yoroi-extension/app/styles/themes/types.js b/packages/yoroi-extension/app/styles/themes/types.js new file mode 100644 index 0000000000..438bebb6cb --- /dev/null +++ b/packages/yoroi-extension/app/styles/themes/types.js @@ -0,0 +1,78 @@ +// @flow +export type DSColorPalette = {| + primary_900: string, + primary_800: string, + primary_700: string, + primary_600: string, + primary_500: string, + primary_400: string, + primary_300: string, + primary_200: string, + primary_100: string, + secondary_900: string, + secondary_800: string, + secondary_700: string, + secondary_600: string, + secondary_500: string, + secondary_400: string, + secondary_300: string, + secondary_200: string, + secondary_100: string, + gray_max: string, + gray_900: string, + gray_800: string, + gray_700: string, + gray_600: string, + gray_500: string, + gray_400: string, + gray_300: string, + gray_200: string, + gray_100: string, + gray_50: string, + gray_min: string, + black_static: string, + white_static: string, + sys_magenta_700: string, + sys_magenta_600: string, + sys_magenta_500: string, + sys_magenta_300: string, + sys_magenta_100: string, + sys_cyan_500: string, + sys_cyan_100: string, + sys_yellow_500: string, + sys_yellow_100: string, + sys_orange_500: string, + sys_orange_100: string, + bg_gradient_1: string, + bg_gradient_2: string, + bg_gradient_3: string, + special_web_overlay: string, + special_web_bg_sidebar: string, + text_primary_max: string, + text_primary_medium: string, + text_primary_min: string, + text_gray_max: string, + text_gray_medium: string, + text_gray_low: string, + text_gray_min: string, + text_error: string, + text_warning: string, + text_success: string, + text_info: string, + bg_color_max: string, + bg_color_min: string, + el_primary_max: string, + el_primary_medium: string, + el_primary_min: string, + el_gray_max: string, + el_gray_medium: string, + el_gray_low: string, + el_gray_min: string, + el_secondary: string, + web_overlay: string, + web_sidebar_item_active: string, + web_sidebar_item_inactive: string, + web_sidebar_item_active_bg: string, + mobile_overlay: string, + mobile_bg_blur: string, +|}; diff --git a/packages/yoroi-extension/app/utils/hwConnectHandler.js b/packages/yoroi-extension/app/utils/hwConnectHandler.js index 48254b3df5..f954b2397f 100644 --- a/packages/yoroi-extension/app/utils/hwConnectHandler.js +++ b/packages/yoroi-extension/app/utils/hwConnectHandler.js @@ -11,6 +11,8 @@ import type { ShowAddressRequest, GetVersionResponse, GetSerialResponse, + MessageData, + SignedMessageData, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import type { MessageType } from '../../ledger/types/cmn'; @@ -94,6 +96,20 @@ export class LedgerConnect { ); } + signMessage: {| + serial: ?string, + params: MessageData, + useOpenTab?: boolean, + |} => Promise = (request) => { + return this._requestLedger( + OPERATION_NAME.SIGN_MESSAGE, + request.params, + request.serial, + false, + request.useOpenTab === true, + ); + } + async _requestLedger( action: string, params: any, diff --git a/packages/yoroi-extension/app/utils/validations.js b/packages/yoroi-extension/app/utils/validations.js index 078864bd30..da60ccc4a1 100644 --- a/packages/yoroi-extension/app/utils/validations.js +++ b/packages/yoroi-extension/app/utils/validations.js @@ -3,8 +3,7 @@ import BigNumber from 'bignumber.js'; import { MAX_MEMO_SIZE } from '../config/externalStorageConfig'; import type { $npm$ReactIntl$IntlFormat, } from 'react-intl'; import { defineMessages, } from 'react-intl'; -import type { NetworkRow, TokenRow } from '../api/ada/lib/storage/database/primitives/tables'; -import { getCardanoHaskellBaseConfig, isCardanoHaskell } from '../api/ada/lib/storage/database/prepackaged/networks'; +import type { TokenRow } from '../api/ada/lib/storage/database/primitives/tables'; import { getTokenName } from '../stores/stateless/tokenHelpers'; import { truncateToken } from './formatters'; @@ -91,19 +90,3 @@ export async function validateAmount( } return [true, undefined]; } - -export function getMinimumValue( - network: $ReadOnly, - isToken: boolean, -): BigNumber { - if (isToken) { - // when sending a token, Yoroi will handle making sure the minimum value is in the UTXO - return new BigNumber(0); - } - if (isCardanoHaskell(network)) { - const config = getCardanoHaskellBaseConfig(network) - .reduce((acc, next) => Object.assign(acc, next), {}); - return new BigNumber(config.MinimumUtxoVal); - } - return new BigNumber(0); -} diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/content/rpc.js b/packages/yoroi-extension/chrome/extension/background/handlers/content/rpc.js index a97be9b726..0994b074f0 100644 --- a/packages/yoroi-extension/chrome/extension/background/handlers/content/rpc.js +++ b/packages/yoroi-extension/chrome/extension/background/handlers/content/rpc.js @@ -65,6 +65,7 @@ import { } from './connect'; import type { CardanoTxRequest } from '../../../../../app/api/ada'; import type { NFTMetadata } from '../../../../../app/api/ada/lib/storage/database/primitives/tables'; +import { getProtocolParameters } from '../yoroi/protocolParameters'; declare var chrome; @@ -436,6 +437,7 @@ const Handlers = Object.freeze({ null, message.params[0], foreignUtxoFetcher, + await getProtocolParameters(wallet.getParent().getNetworkInfo().NetworkId), ); return { ok: resp }; }), @@ -601,6 +603,7 @@ const Handlers = Object.freeze({ requiredAmount, addressedUtxos, submittedTxs, + await getProtocolParameters(wallet.getParent().getNetworkInfo().NetworkId), ); } catch (error) { if (error instanceof NotEnoughMoneyToSendError) { diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/utils.js b/packages/yoroi-extension/chrome/extension/background/handlers/utils.js index ee32267e7e..f7c3f3850a 100644 --- a/packages/yoroi-extension/chrome/extension/background/handlers/utils.js +++ b/packages/yoroi-extension/chrome/extension/background/handlers/utils.js @@ -30,12 +30,11 @@ import { Bip44Wallet } from '../../../../app/api/ada/lib/storage/models/Bip44Wal import { isTestnet, isCardanoHaskell, - getCardanoHaskellBaseConfig, } from '../../../../app/api/ada/lib/storage/database/prepackaged/networks'; import BigNumber from 'bignumber.js'; import { asAddressedUtxo, - cardanoMinAdaRequiredFromRemoteFormat_coinsPerWord, + cardanoValueFromRemoteFormat, } from '../../../../app/api/ada/transactions/utils'; import { MultiToken } from '../../../../app/api/common/lib/MultiToken'; import { RustModule } from '../../../../app/api/ada/lib/cardanoCrypto/rustLoader'; @@ -44,6 +43,7 @@ import { getDb } from '../state/databaseManager'; // eslint-disable-next-line import/no-cycle import { refreshingWalletIdSet } from '../state/refreshScheduler'; import { loadWalletsFromStorage } from '../../../../app/api/ada/lib/storage/models/load'; +import { getProtocolParameters } from './yoroi/protocolParameters'; export async function getWalletsState(publicDeriverId: ?number): Promise> { const db = await getDb(); @@ -83,17 +83,23 @@ async function getWalletState(publicDeriver: PublicDeriver<>): Promise u.assets.length > 0); - const config = getCardanoHaskellBaseConfig(network).reduce( - (acc, next) => Object.assign(acc, next), - {} - ); + const protocolParameters = await getProtocolParameters(network.NetworkId); const deposits: Array = addressedUtxos.map(u => { try { - return cardanoMinAdaRequiredFromRemoteFormat_coinsPerWord( + const output = RustModule.WalletV4.TransactionOutput.new( + // using a dummy common base address here. This is the longest address + // to ensure safety but and not optimum. + RustModule.WalletV4.Address.from_hex('0'.repeat(114)), // $FlowIgnore[prop-missing] property `addressing` is missing in `RemoteUnspentOutput` [1] but exists in `CardanoAddressedUtxo` [2] - u, - new BigNumber(config.CoinsPerUtxoWord), + cardanoValueFromRemoteFormat(u), ); + // todo: set data hash here if necessary + return new BigNumber(RustModule.WalletV4.min_ada_for_output( + output, + RustModule.WalletV4.DataCost.new_coins_per_byte( + RustModule.WalletV4.BigNum.from_str(protocolParameters.coinsPerUtxoByte) + ), + ).to_str()); } catch (e) { // eslint-disable-next-line no-console console.error( diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/connector.js b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/connector.js index c76b142d8c..04003276b3 100644 --- a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/connector.js +++ b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/connector.js @@ -35,7 +35,7 @@ import { RustModule } from '../../../../../app/api/ada/lib/cardanoCrypto/rustLoa import { mergeWitnessSets } from '../../../../../app/api/ada/transactions/utils'; import { hexToBytes } from '../../../../../app/coreUtils'; import { Logger } from '../../../../../app/utils/logging'; -import { walletSignData } from '../../../../../app/api/ada'; +import { walletSignData, encodeHardwareWalletSignResult } from '../../../../../app/api/ada'; type RpcUid = number; @@ -140,6 +140,11 @@ type ConfirmedSignData = {| password: string, // hardware wallet: witnessSetHex?: ?string, + signedMessageData?: {| + signatureHex: string; + signingPublicKeyHex: string; + addressFieldHex: string; + |}, |}; export const UserSignConfirm: HandlerType< @@ -232,28 +237,44 @@ export const UserSignConfirm: HandlerType< rpcResponse(request.tabId, request.uid, { err: 'unexpected error' }); return; } + const { address, payload } = responseData.continuationData; + let dataSig; - try { - dataSig = await walletSignData( - wallet, - request.password, - address, - payload, - ); - } catch (error) { - Logger.error(`error when signing data ${error}`); - rpcResponse( - request.tabId, - request.uid, - { - err: { - code: DataSignErrorCodes.DATA_SIGN_PROOF_GENERATION, - info: error.message, + + if (request.password !== '') { + try { + dataSig = await walletSignData( + wallet, + request.password, + address, + payload, + ); + } catch (error) { + Logger.error(`error when signing data ${error}`); + rpcResponse( + request.tabId, + request.uid, + { + err: { + code: DataSignErrorCodes.DATA_SIGN_PROOF_GENERATION, + info: error.message, + } } - } + ); + return; + } + } else { + const { signedMessageData } = request; + if (signedMessageData == null) { + throw new Error('unexpected user signed confirmation: missing hardware wallet signing response'); + } + dataSig = await encodeHardwareWalletSignResult( + signedMessageData.addressFieldHex, + signedMessageData.signatureHex, + payload, + signedMessageData.signingPublicKeyHex, ); - return; } rpcResponse(request.tabId, request.uid, { ok: dataSig }); } diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/index.js b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/index.js index 4446a209af..f935d3711a 100644 --- a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/index.js +++ b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/index.js @@ -41,6 +41,7 @@ import { RemoveWalletFromWhiteList, GetConnectedSites, } from './connector'; +import { GetProtocolParameters } from './protocolParameters'; import { subscribe } from '../../subscriptionManager'; const handlerMap = Object.freeze({ @@ -83,6 +84,8 @@ const handlerMap = Object.freeze({ [ConnectWindowRetrieveData.typeTag]: ConnectWindowRetrieveData.handle, [RemoveWalletFromWhiteList.typeTag]: RemoveWalletFromWhiteList.handle, [GetConnectedSites.typeTag]: GetConnectedSites.handle, + + [GetProtocolParameters.typeTag]: GetProtocolParameters.handle, }); type Handler = ( diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/protocolParameters.js b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/protocolParameters.js new file mode 100644 index 0000000000..93e4cdb8c0 --- /dev/null +++ b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/protocolParameters.js @@ -0,0 +1,228 @@ +// @flow +import type { HandlerType } from './type'; +import type { + ProtocolParameters as _ProtocolParameters +} from '@emurgo/yoroi-lib/dist/protocol-parameters/models'; +import { ProtocolParametersApi } from '@emurgo/yoroi-lib/dist/protocol-parameters/emurgo-api'; +import { + getNetworkById, + getCardanoHaskellBaseConfigCombined, + networks, +} from '../../../../../app/api/ada/lib/storage/database/prepackaged/networks'; +import type { + CardanoHaskellConfig, + NetworkRow, +} from '../../../../../app/api/ada/lib/storage/database/primitives/tables'; +import { + type StorageField, + createStorageField, +} from '../../../../../app/api/localStorage'; + +// tmp measure before lib update +type ProtocolParameters = {| + ...$Exact<_ProtocolParameters>, + epoch: number, +|}; + +// cache protocol parameters of the current epoch +// also may cache for the future epoch should the backend provides them +type ProtocolParameterCache = Array; + +function validate(protocolParameters: Object): boolean { + const decNumRegex = /^\d+$/; + return ( + typeof protocolParameters?.linearFee?.constant === 'string' && + decNumRegex.test(protocolParameters?.linearFee?.constant) && + typeof protocolParameters?.linearFee?.coefficient === 'string' && + decNumRegex.test(protocolParameters?.linearFee?.coefficient) && + typeof protocolParameters?.coinsPerUtxoByte === 'string' && + decNumRegex.test(protocolParameters?.coinsPerUtxoByte) && + typeof protocolParameters?.poolDeposit === 'string' && + decNumRegex.test(protocolParameters?.poolDeposit) && + typeof protocolParameters?.keyDeposit === 'string' && + decNumRegex.test(protocolParameters?.keyDeposit) && + typeof protocolParameters.epoch === 'number' + ); +} + +const EPOCH_TIMESTAMP = Object.freeze({ + CardanoMainnet: { + epoch: 504, + startTimestamp: 1723931091000, // Aug 18, 2024 5:44:51 AM + }, + CardanoPreprodTestnet: { + epoch: 162, + startTimestamp: 1724025600000, // Aug 19, 2024 8:00:00 AM + }, + CardanoPreviewTestnet: { + epoch: 665, + startTimestamp: 1724112000000, // Aug 20, 2024 8:00:00 AM + }, + CardanoSanchoTestnet: { + epoch: 431, + startTimestamp: 1724027420000, // 08/19/2024, 08:30:20 + }, +}); +// type assertion to ensure we have every network in `EpochTimestam` +(networks: {| [$Keys]: any |}); + +class ProcolParameterApi { + #networkId: number; + + constructor(networkId: number) { + this.#networkId = networkId; + } + + getNetworkKey(): string { + for (const key of Object.keys(networks)) { + if (networks[key].NetworkId === this.#networkId) { + return key; + } + } + throw new Error('nonexistent network id'); + } + + getNetwork(): $ReadOnly { + const network = getNetworkById(this.#networkId); + if (!network) { + throw new Error('unexpectedly missing network'); + } + return network; + } + + getConfig(): CardanoHaskellConfig { + return getCardanoHaskellBaseConfigCombined(getNetworkById(this.#networkId)); + } + + getEpochData(): {| + currentEpoch: number, + |} { + const { SlotsPerEpoch, SlotDuration } = this.getConfig(); + if (SlotsPerEpoch == null || SlotDuration == null) { + throw new Error('unexpectedly missing epoch length data'); + } + const { epoch, startTimestamp } = EPOCH_TIMESTAMP[this.getNetworkKey()]; + + const now = Date.now(); + const delta = now - startTimestamp; + const currentEpoch = epoch + Math.floor(delta / (SlotsPerEpoch * SlotDuration * 1000)); + + return { currentEpoch }; + } + + getDefaultProtocolParameters(): ProtocolParameters { + const { currentEpoch } = this.getEpochData(); + const config = this.getConfig(); + if (!config.LinearFee || !config.CoinsPerUtxoByte || !config.PoolDeposit || !config.KeyDeposit) { + throw new Error('unexpectedly missing config parameters'); + } + + return { + linearFee: { + constant: config.LinearFee.constant, + coefficient: config.LinearFee.coefficient, + }, + coinsPerUtxoByte: config.CoinsPerUtxoByte, + poolDeposit: config.PoolDeposit, + keyDeposit: config.KeyDeposit, + epoch: currentEpoch, + }; + } + + getCacheStorage(): StorageField { + return createStorageField( + 'PROTOCOL_PARAMETERS_' + this.#networkId, + JSON.stringify, + JSON.parse, + null + ); + } + + async getCachedProtocolParameters( + epoch: number, + allowFallbackToPreviousEpoch: boolean + ): Promise { + const storage = this.getCacheStorage(); + const cache = await storage.get(); + if (!cache) { + return null; + } + return cache.find((protocolParameters) => { + if (allowFallbackToPreviousEpoch) { + return protocolParameters.epoch <= epoch; + } + return protocolParameters.epoch === epoch; + }); + } + + async cacheProtocolParameters(protocolParameters: ProtocolParameters): Promise { + const storage = this.getCacheStorage(); + let cache = await storage.get(); + if (!cache) { + cache = []; + } + cache = cache.filter(( { epoch } ) => epoch > protocolParameters.epoch); + cache.push(protocolParameters); + await storage.set(cache); + } + + async fetchProtocolParametersFromNetwork(): Promise { + const { BackendServiceZero } = this.getNetwork().Backend; + const api = new ProtocolParametersApi(BackendServiceZero); + const protocolParameters: Object = await api.getParameters(); + + if (!validate(protocolParameters)) { + return null; + } + await this.cacheProtocolParameters(protocolParameters); + return protocolParameters; + } + + async getProtocolParameters(): Promise { + const { currentEpoch } = this.getEpochData(); + const cached = await this.getCachedProtocolParameters(currentEpoch, false); + if (cached) { + return cached; + } + try { + const fetched = await this.fetchProtocolParametersFromNetwork(); + if (fetched) { + return fetched; + } + } catch (error) { + console.error( + 'failed to fetch protocol parameters for network %s epoch $S:', + this.#networkId, + currentEpoch, + error + ); + } + const cachedPrevious = await this.getCachedProtocolParameters(currentEpoch, true); + if (cachedPrevious) { + return cachedPrevious; + } + return this.getDefaultProtocolParameters(); + } +} + +export const GetProtocolParameters: HandlerType< + { networkId: number, ... }, + ProtocolParameters +> = Object.freeze({ + typeTag: 'get-protocol-parameters', + + handle: async (request) => { + const api = new ProcolParameterApi(request.networkId); + return await api.getProtocolParameters(); + }, +}); + +export const getProtocolParameters: (number) => Promise = + (networkId) => GetProtocolParameters.handle({ networkId }); + +export async function updateProtocolParametersCacheFromNetwork(networkId: number, epoch: ?number) { + const api = new ProcolParameterApi(networkId); + if (epoch == null || await api.getCachedProtocolParameters(epoch, false) == null) { + await api.fetchProtocolParametersFromNetwork(); + } +} diff --git a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/wallet.js b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/wallet.js index a941942566..71724c3768 100644 --- a/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/wallet.js +++ b/packages/yoroi-extension/chrome/extension/background/handlers/yoroi/wallet.js @@ -28,6 +28,7 @@ import { import type { ReferenceTransaction, BaseGetTransactionsRequest } from '../../../../../app/api/common'; import WalletTransaction from '../../../../../app/domain/WalletTransaction'; import type { AdaGetTransactionsRequest } from '../../../../../app/api/ada'; +import { updateProtocolParametersCacheFromNetwork } from './protocolParameters'; type CreateWalletRequest = {| networkId: number, @@ -86,6 +87,8 @@ export const CreateHardwareWallet: HandlerType< typeTag: 'create-hardware-wallet', handle: async (request) => { + await RustModule.load(); + const db = await getDb(); const stateFetcher = await getCardanoStateFetcher(new LocalStorageApi()); @@ -186,6 +189,9 @@ export const ResyncWallet: HandlerType< handle: async (request) => { const publicDeriver = await getPublicDeriverById(request.publicDeriverId); await syncWallet(publicDeriver, 'UI resync'); + await updateProtocolParametersCacheFromNetwork( + publicDeriver.getParent().getNetworkInfo().NetworkId + ); }, }); diff --git a/packages/yoroi-extension/chrome/extension/background/state/refreshScheduler.js b/packages/yoroi-extension/chrome/extension/background/state/refreshScheduler.js index a6fcb5fcb6..16598d0faa 100644 --- a/packages/yoroi-extension/chrome/extension/background/state/refreshScheduler.js +++ b/packages/yoroi-extension/chrome/extension/background/state/refreshScheduler.js @@ -14,6 +14,12 @@ import type { WalletState } from '../types'; import WalletTransaction from '../../../../app/domain/WalletTransaction'; // eslint-disable-next-line import/no-cycle import { getWalletsState } from '../handlers/utils'; +import TimeUtils from '../../../../app/api/ada/lib/storage/bridge/timeUtils'; +import { updateProtocolParametersCacheFromNetwork } from '../handlers/yoroi/protocolParameters'; +import { + getCardanoHaskellBaseConfig, + getNetworkById, +} from '../../../../app/api/ada/lib/storage/database/prepackaged/networks'; registerCallback(params => { if (params.type === 'subscriptionChange') { @@ -149,7 +155,20 @@ async function _syncWallet(publicDeriver: PublicDeriver<>, logInfo: string): Pro persistSubmittedTransactions(submittedTransactions); } console.log('Syncing wallet %s finished.', publicDeriverId); - emitUpdate(publicDeriverId, false, (await getWalletsState(publicDeriverId))[0], newTxs); + emitUpdate( + publicDeriverId, + false, + (await getWalletsState(publicDeriverId))[0], + newTxs + ); + + const networkId = publicDeriver.getParent().getNetworkInfo().NetworkId; + const baseConfig = getCardanoHaskellBaseConfig(getNetworkById(networkId)); + const updatedLastSyncInfo = await publicDeriver.getLastSyncInfo(); + if (updatedLastSyncInfo?.SlotNum != null) { + const { epoch } = TimeUtils.toRelativeSlotNumber(baseConfig, updatedLastSyncInfo.SlotNum); + await updateProtocolParametersCacheFromNetwork(networkId, epoch); + } } catch (error) { console.error('Syncing wallet %s failed:', publicDeriverId, error); } finally { diff --git a/packages/yoroi-extension/chrome/extension/connector/api.js b/packages/yoroi-extension/chrome/extension/connector/api.js index e8a33c0c15..6de8b93074 100644 --- a/packages/yoroi-extension/chrome/extension/connector/api.js +++ b/packages/yoroi-extension/chrome/extension/connector/api.js @@ -69,6 +69,7 @@ import { ChainDerivations, DREP_KEY_INDEX, STAKING_KEY_INDEX } from '../../../ap import { pubKeyHashToRewardAddress, transactionHexToHash } from '../../../app/api/ada/lib/cardanoCrypto/utils'; import { sendTx } from '../../../app/api/ada/lib/state-fetch/remoteFetcher'; import type { WalletState } from '../background/types'; +import type { ProtocolParameters } from '@emurgo/yoroi-lib/dist/protocol-parameters/models'; function paginateResults(results: T[], paginate: ?Paginate): T[] { if (paginate != null) { @@ -859,6 +860,7 @@ export async function connectorCreateCardanoTx( password: ?string, cardanoTxRequest: CardanoTxRequest, foreignUtxoFetcher: ForeignUtxoFetcher, + protocolParameters: ProtocolParameters, ): Promise { const withUtxos = asGetAllUtxos(publicDeriver); if (withUtxos == null) { @@ -889,6 +891,7 @@ export async function connectorCreateCardanoTx( cardanoTxRequest, submittedTxs, utxos, + protocolParameters, }, foreignUtxoFetcher, ); diff --git a/packages/yoroi-extension/ledger/components/connect/operation/OperationBlock.js b/packages/yoroi-extension/ledger/components/connect/operation/OperationBlock.js index 5f56bd5af7..40a78f8ff1 100644 --- a/packages/yoroi-extension/ledger/components/connect/operation/OperationBlock.js +++ b/packages/yoroi-extension/ledger/components/connect/operation/OperationBlock.js @@ -21,6 +21,8 @@ import ConnectLedgerMultiKeysHintBlock from './connect/ConnectLedgerMultiKeysHin import SendTxHintBlock from './send/SendTxHintBlock'; import VerifyAddressHintBlock from './verify/VerifyAddressHintBlock'; import DeriveAddressHintBlock from './derive/DeriveAddressHintBlock'; +import SignMessageBlock from './send/SignMessageBlock'; + import type { DeriveAddressRequest, SignTransactionRequest, @@ -135,6 +137,15 @@ export default class OperationBlock extends React.Component { /> ); break; + case OPERATION_NAME.SIGN_MESSAGE: + content = ( + + ); + break; default: console.error(`[YLC] Unexpected operation: ${currentOperationName}`); return (null); diff --git a/packages/yoroi-extension/ledger/components/connect/operation/send/SignMessageBlock.js b/packages/yoroi-extension/ledger/components/connect/operation/send/SignMessageBlock.js new file mode 100644 index 0000000000..9b5b211a42 --- /dev/null +++ b/packages/yoroi-extension/ledger/components/connect/operation/send/SignMessageBlock.js @@ -0,0 +1,364 @@ +// @flow // +import React from 'react'; +import type { Node } from 'react'; +import { observer } from 'mobx-react'; +import { intlShape, defineMessages } from 'react-intl'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import semverGte from 'semver/functions/gte'; + +import type { DeviceCodeType } from '../../../../types/enum'; +import HintBlock from '../../../widgets/hint/HintBlock'; +import HintGap from '../../../widgets/hint/HintGap'; +import type { SignTransactionRequest, Certificate } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { + AddressType, + CertificateType, + TxAuxiliaryDataType, + CredentialParamsType, +} from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { + pathToString, +} from '../../../../utils/cmn'; +import { bech32 } from 'bech32'; +import { getAddressHintBlock } from '../../../widgets/hint/AddressHintBlock'; + +import styles from './SendTxHintBlock.scss'; + +const message = defineMessages({ + sStartNewTx: { + id: 'hint.sendTx.startNewTx', + defaultMessage: '!!!Check your Ledger screen, then press right button.' + }, + sConfirmValue: { + id: 'hint.sendTx.confirmValue', + defaultMessage: '!!!Confirm the ADA amount by pressing both buttons.' + }, + sConfirmAddress: { + id: 'hint.sendTx.confirmAddress', + defaultMessage: "!!!Confirm the receiver's address by pressing both buttons." + }, + sConfirmFee: { + id: 'hint.sendTx.confirmFee', + defaultMessage: '!!!Confirm Transaction Fee by pressing both buttons.' + }, + sConfirmTx: { + id: 'hint.sendTx.confirmTx', + defaultMessage: '!!!Confirm Transaction Fee by pressing both buttons.' + }, + sTtl: { + id: 'hint.sendTx.ttl', + defaultMessage: '!!!Confirm the time-to-live by pressing both buttons.' + }, + sRegistration: { + id: 'hint.certificate.registration', + defaultMessage: '!!!Confirm the staking key registration by pressing both buttons.' + }, + sRegistrationComplete: { + id: 'hint.certificate.registrationComplete', + defaultMessage: '!!!Confirm the staking key registration by pressing the right button.' + }, + sDeregistration: { + id: 'hint.certificate.deregistration', + defaultMessage: '!!!Confirm the deregistration by pressing both buttons.' + }, + sDeregistrationComplete: { + id: 'hint.certificate.deregistrationComplete', + defaultMessage: '!!!Confirm the deregistration by pressing the right button.' + }, + sDelegation: { + id: 'hint.certificate.delegation', + defaultMessage: '!!!Make sure the pool shown on your Ledger is the same as the one shown below, then press both buttons.' + }, + sDelegationComplete: { + id: 'hint.certificate.delegationComplete', + defaultMessage: '!!!Confirm the delegation by pressing the right button.' + }, + sPath: { + id: 'hint.verifyAddress.path', + defaultMessage: '!!!Make sure the address path shown on your Ledger is the same as the one shown below, then press both buttons.' + }, + sWithdrawal: { + id: 'hint.withdrawal', + defaultMessage: '!!!Confirm the withdrawal, then press both buttons.' + }, + sMetadata: { + id: 'hint.metadata', + defaultMessage: '!!!Confirm the metadata hash, then press both buttons.' + }, + xStartNewTx: { + id: 'hint.nanoX.sendTx.startNewTx', + defaultMessage: '!!!Check your Ledger screen, then press both buttons.' + }, + xConfirmValue: { + id: 'hint.sendTx.confirmValue', + defaultMessage: '!!!Confirm the ADA amount by pressing both buttons.' + }, + xConfirmAddress: { + id: 'hint.nanoX.sendTx.confirmAddress', + defaultMessage: "!!!Confirm the receiver's address by pressing the right button to scroll through the entire address. Then press both buttons." + }, + xConfirmFee: { + id: 'hint.sendTx.confirmFee', + defaultMessage: '!!!Confirm Transaction Fee by pressing both buttons.' + }, + xConfirmTx: { + id: 'hint.nanoX.sendTx.confirmTx', + defaultMessage: '!!!Confirm the transaction by pressing the both buttons.' + }, + xTtl: { + id: 'hint.sendTx.ttl', + defaultMessage: '!!!Confirm the time-to-live by pressing both buttons.' + }, + xRegistration: { + id: 'hint.certificate.registration', + defaultMessage: '!!!Confirm the staking key registration by pressing both buttons.' + }, + xRegistrationComplete: { + id: 'hint.certificate.registrationComplete', + defaultMessage: '!!!Confirm the staking key registration by pressing the right button.' + }, + xDeregistration: { + id: 'hint.certificate.deregistration', + defaultMessage: '!!!Confirm the deregistration by pressing both buttons.' + }, + xDeregistrationComplete: { + id: 'hint.certificate.deregistrationComplete', + defaultMessage: '!!!Confirm the deregistration by pressing the right button.' + }, + xDelegation: { + id: 'hint.certificate.delegation', + defaultMessage: '!!!Make sure the pool shown on your Ledger is the same as the one shown below, then press both buttons.' + }, + xDelegationComplete: { + id: 'hint.certificate.delegationComplete', + defaultMessage: '!!!Confirm the delegation by pressing the right button.' + }, + xPath: { + id: 'hint.verifyAddress.path', + defaultMessage: '!!!Make sure the address path shown on your Ledger is the same as the one shown below, then press both buttons.' + }, + xWithdrawal: { + id: 'hint.withdrawal', + defaultMessage: '!!!Confirm the withdrawal, then press both buttons.' + }, + xMetadata: { + id: 'hint.metadata', + defaultMessage: '!!!Confirm the metadata hash, then press both buttons.' + }, + catalystStep1: { + id: 'hint.catalystStep1', + defaultMessage: '!!!Confirm the network ID by pressing both buttons.', + }, + catalystStep3: { + id: 'hint.catalystStep3', + defaultMessage: '!!!Confirm to register Catalyst voting key by pressing the right button.', + }, + catalystStep4: { + id: 'hint.catalystStep4', + defaultMessage: '!!!Confirm the voting public key by pressing both buttons.', + }, + catalystStep5: { + id: 'hint.catalystStep5', + defaultMessage: '!!!Confirm the staking key path by pressing both buttons.', + }, + catalystStep6: { + id: 'hint.catalystStep6', + defaultMessage: '!!!Confirm the reward address by pressing both buttons.', + }, + catalystStep7: { + id: 'hint.catalystStep7', + defaultMessage: '!!!Confirm the nonce by pressing both buttons.', + }, + catalystStep8: { + id: 'hint.catalystStep8', + defaultMessage: '!!!Confirm voting key registration by pressing the right button.', + }, + catalystStep9: { + id: 'hint.catalystStep9', + defaultMessage: '!!!Confirm the auxilliary data hash by pressing both buttons.', + }, + confirmAssetFingerprint: { + id: 'hint.sendTx.confirmAssetFingerprint', + defaultMessage: '!!!Confirm the asset fingerprint by pressing both buttons', + }, + confirmAssetAmount: { + id: 'hint.sendTx.confirmAssetAmount', + defaultMessage: '!!!Confirm the token amount by pressing both buttons', + }, +}); + +type Props = {| + deviceCode: DeviceCodeType, + wasDeviceLocked: boolean, + deviceVersion: ?string, +|}; + +@observer +export default class SendTxHintBlock extends React.Component { + static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { + intl: intlShape.isRequired + }; + + renderCertificate: {| + cert: Certificate, + getAndIncrementStep: void => number, + deviceVersion: ?string, + |} => Array = (request) => { + const stakingKey = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-staking-key.png`); + + if (request.cert.type === CertificateType.STAKE_REGISTRATION) { + const { params } = request.cert; + if (params.stakeCredential.type !== CredentialParamsType.KEY_PATH) { + throw new Error('unsupported stake credential type'); + } + // $FLowIgnore + const { keyPath } = params.stakeCredential; + const imgRegister = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-registration.png`); + const imgRegisterConfirm = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-registration-confirm.png`); + const firstStep = request.getAndIncrementStep(); + const secondStep = request.getAndIncrementStep(); + const thirdStep = request.getAndIncrementStep(); + return [ + (), + (), + (), + (), + (), + (), + ]; + } + if (request.cert.type === CertificateType.STAKE_DELEGATION) { + const { params } = request.cert; + if (params.stakeCredential.type !== CredentialParamsType.KEY_PATH) { + throw new Error('unsupported stake credential type'); + } + // $FLowIgnore + const { keyPath } = params.stakeCredential; + const imgDelegatePool = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-delegation-pool.png`); + const imgDelegateConfirm = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-delegation-confirm.png`); + const firstStep = request.getAndIncrementStep(); + const secondStep = request.getAndIncrementStep(); + const thirdStep = request.getAndIncrementStep(); + + let poolId; + if (request.deviceVersion === undefined) { + throw new Error('unexpect null deviceVersion'); + } + // Starting from version 2.4.1, the Ledger Cardano app show the pool ID + // in bech32, complying with CIP0005 + if (semverGte(request.deviceVersion, '2.4.1')) { + poolId = bech32.encode( + 'pool', + bech32.toWords(Buffer.from(params.poolKeyHashHex, 'hex')) + ); + } else { + poolId = params.poolKeyHashHex; + } + + return [ + (), + (), + (), + (), + (), + (), + ]; + } + if (request.cert.type === CertificateType.STAKE_DEREGISTRATION) { + const { params } = request.cert; + if (params.stakeCredential.type !== CredentialParamsType.KEY_PATH) { + throw new Error('unsupported stake credential type'); + } + // $FLowIgnore + const { keyPath } = params.stakeCredential; + const imgDeregister = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-deregister-key.png`); + const imgDeregisterConfirm = require(`../../../../assets/img/nano-${this.props.deviceCode}/hint-deregister-confirm.png`); + const firstStep = request.getAndIncrementStep(); + const secondStep = request.getAndIncrementStep(); + const thirdStep = request.getAndIncrementStep(); + return [ + (), + (), + (), + (), + (), + (), + ]; + } + if (request.cert.type === CertificateType.STAKE_POOL_REGISTRATION) { + const { params } = request.cert; + // TODO + } + // unhandled certificate type + return []; + } + + render(): Node { + const { + deviceCode, + wasDeviceLocked, + deviceVersion, + } = this.props; + const stepStartNumber: number = wasDeviceLocked ? 2 : 0; // 2 = count of common step + let stepNumber = stepStartNumber; + let content = 'Please confirm on your Ledger.'; + + + return ( +
    +
    + {content} +
    +
    + ); + } +} diff --git a/packages/yoroi-extension/ledger/stores/ConnectStore.js b/packages/yoroi-extension/ledger/stores/ConnectStore.js index 00de555609..0dec40242a 100644 --- a/packages/yoroi-extension/ledger/stores/ConnectStore.js +++ b/packages/yoroi-extension/ledger/stores/ConnectStore.js @@ -16,6 +16,7 @@ import type { DeriveAddressRequest, GetExtendedPublicKeyRequest, GetExtendedPublicKeysRequest, + MessageData, } from '@cardano-foundation/ledgerjs-hw-app-cardano'; import type { MessageType, @@ -239,6 +240,12 @@ export default class ConnectStore { params, }); break; + case OPERATION_NAME.SIGN_MESSAGE: + this.signMessage({ + actn, + params, + }); + break; default: throw new Error(`[YLC] Unexpected action called: ${actn}`); } @@ -418,6 +425,28 @@ export default class ConnectStore { } }; + signMessage: {| + actn: OperationNameType, + params: MessageData, + |} => Promise = async (request) => { + let transport; + try { + transport = await makeTransport(this.transportId); + const { version } = await this._detectLedgerDevice(transport); + + const adaApp = new AdaApp(transport); + const resp = await adaApp.signMessage( + request.params + ); + + this._replyMessageWrap(request.actn, true, resp); + } catch (err) { + this._replyError(request.actn, err); + } finally { + transport && transport.close(); + } + }; + // #==============================================# // Yoroi extension main tab <==> this tab communications // #==============================================# @@ -479,6 +508,7 @@ export default class ConnectStore { case OPERATION_NAME.SIGN_TX: case OPERATION_NAME.SHOW_ADDRESS: case OPERATION_NAME.DERIVE_ADDRESS: + case OPERATION_NAME.SIGN_MESSAGE: // Only one operation in one session if (!this.userInteractableRequest) { this.userInteractableRequest = { diff --git a/packages/yoroi-extension/ledger/types/enum.js b/packages/yoroi-extension/ledger/types/enum.js index 1e0d06a9ed..d7823db40c 100644 --- a/packages/yoroi-extension/ledger/types/enum.js +++ b/packages/yoroi-extension/ledger/types/enum.js @@ -18,6 +18,7 @@ export const OPERATION_NAME = Object.freeze({ SIGN_TX: 'ledger-sign-transaction', SHOW_ADDRESS: 'ledger-show-address', DERIVE_ADDRESS: 'ledger-derive-address', + SIGN_MESSAGE: 'sign-message', CLOSE_WINDOW: 'close-window', }); export type OperationNameType = $Values; diff --git a/packages/yoroi-extension/package-lock.json b/packages/yoroi-extension/package-lock.json index 987e2595c7..e9f9d11776 100644 --- a/packages/yoroi-extension/package-lock.json +++ b/packages/yoroi-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "yoroi", - "version": "5.3.131", + "version": "5.3.200", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "yoroi", - "version": "5.3.131", + "version": "5.3.200", "license": "MIT", "dependencies": { "@amplitude/analytics-browser": "^2.1.3", diff --git a/packages/yoroi-extension/package.json b/packages/yoroi-extension/package.json index f8872efe08..b6d37e3748 100644 --- a/packages/yoroi-extension/package.json +++ b/packages/yoroi-extension/package.json @@ -1,6 +1,6 @@ { "name": "yoroi", - "version": "5.3.131", + "version": "5.3.200", "description": "Cardano ADA wallet", "scripts": { "dev-mv2": "rimraf dev/ && NODE_OPTIONS=--openssl-legacy-provider babel-node scripts-mv2/build --type=debug --env 'mainnet'",