From 644f5c2746c622f3ad6e1fddd4a67cf2bd25c5ec Mon Sep 17 00:00:00 2001 From: banklesss <105349292+banklesss@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:04:54 +0100 Subject: [PATCH] feature: domain resolver (#2866) Signed-off-by: banklesss <105349292+banklesss@users.noreply.github.com> Signed-off-by: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> Co-authored-by: Juliano Lazzarotto <30806844+stackchain@users.noreply.github.com> --- apps/wallet-mobile/.env | 2 + apps/wallet-mobile/.env.nightly | 2 + apps/wallet-mobile/.env.production | 2 + apps/wallet-mobile/.env.staging | 2 + .../.storybook/storybook.requires.js | 5 + apps/wallet-mobile/package.json | 4 +- .../src/TxHistory/ActionsBanner.tsx | 4 +- .../src/TxHistory/TxHistoryNavigator.tsx | 439 +++++----- .../src/WalletInit/WalletForm.tsx | 2 +- .../src/components/Icon/Check.tsx | 10 +- .../TextInput/TextInput.stories.tsx | 4 +- .../src/components/TextInput/TextInput.tsx | 26 +- .../Scan/common/useTriggerScanAction.tsx | 14 +- .../ButtonGroup/ButtonGroup.stories.tsx | 41 + .../Send/common/ButtonGroup/ButtonGroup.tsx | 62 ++ .../features/Send/common/SendContext.test.tsx | 28 +- .../src/features/Send/common/SendContext.tsx | 186 ++-- .../src/features/Send/common/constants.ts | 1 + .../src/features/Send/common/errors.ts | 2 + .../src/features/Send/common/mocks.ts | 5 +- .../src/features/Send/common/strings.ts | 83 +- .../features/Send/common/useSendAddress.tsx | 68 ++ .../features/Send/common/useSendReceiver.tsx | 72 ++ .../useCases/ConfirmTx/ConfirmTxScreen.tsx | 18 +- .../ConfirmTx/Summary/ReceiverInfo.tsx | 58 +- .../useCases/StartMultiTokenTx/InputMemo.tsx | 92 -- .../StartMultiTokenTx/InputMemo/InputMemo.tsx | 32 + .../InputMemo/ShowMemoErrorTooLong.tsx | 27 + .../InputMemo/ShowMemoInstructions.tsx | 27 + .../InputReceiver/InputReceiver.stories.tsx | 41 + .../InputReceiver/InputReceiver.tsx | 22 +- .../InputReceiver/ResolveAddress.tsx | 121 --- .../ShowResolvedAddressSelected.stories.tsx | 114 +++ .../ShowResolvedAddressSelected.tsx | 64 ++ .../NotifySupportedNameServers.stories.tsx | 41 + .../NotifySupportedNameServers.tsx | 90 ++ .../SelectNameServer.stories.tsx | 104 +++ .../SelectNameServer/SelectNameServer.tsx | 96 +++ .../useCases/StartMultiTokenTx/ShowErrors.tsx | 22 +- .../StartMultiTokenTxScreen.tsx | 101 ++- .../ChangePassword/ChangePasswordScreen.tsx | 2 +- .../ConfirmTx/ConfirmTxScreen.tsx | 18 +- .../ConfirmTx/Summary/ReceiverInfo.tsx | 40 +- .../ManageCollateralScreen.tsx | 14 +- .../wallet-mobile/src/i18n/locales/en-US.json | 805 +++++++++--------- apps/wallet-mobile/src/legacy/config.ts | 2 + apps/wallet-mobile/src/utils/debounceMaker.ts | 22 + .../yoroi-wallets/cardano/constants/common.ts | 2 +- .../src/yoroi-wallets/cardano/networks.ts | 2 +- .../src/yoroi-wallets/cardano/utils.ts | 9 +- .../src/yoroi-wallets/hooks/index.ts | 2 +- .../src/yoroi-wallets/types/yoroi.ts | 4 +- .../src/TxHistory/TxHistoryNavigator.json | 128 +-- .../messages/src/WalletInit/WalletForm.json | 24 +- .../src/features/Send/common/strings.json | 557 +++++++----- .../ConfirmTx/Summary/ReceiverInfo.json | 8 +- .../ChangePassword/ChangePasswordScreen.json | 24 +- .../ConfirmTx/Summary/ReceiverInfo.json | 8 +- metro.config.js | 6 +- package.json | 3 +- packages/common/src/api/fetchData.ts | 7 +- packages/resolver/.dependency-cruiser.js | 449 ++++++++++ packages/resolver/.gitignore | 70 ++ packages/resolver/README.md | 5 + packages/resolver/babel.config.js | 3 + packages/resolver/jest.setup.js | 5 + packages/resolver/package.json | 216 +++++ packages/resolver/scripts/flowgen.sh | 3 + packages/resolver/src/adapters/api.mocks.ts | 25 + packages/resolver/src/adapters/api.test.ts | 147 ++++ packages/resolver/src/adapters/api.ts | 131 +++ packages/resolver/src/adapters/cns.test.ts | 14 + packages/resolver/src/adapters/cns.ts | 11 + .../resolver/src/adapters/handle-api.mocks.ts | 33 + .../resolver/src/adapters/handle-api.test.ts | 154 ++++ packages/resolver/src/adapters/handle-api.ts | 106 +++ .../resolver/src/adapters/storage.mocks.ts | 40 + .../resolver/src/adapters/storage.test.ts | 65 ++ packages/resolver/src/adapters/storage.ts | 33 + .../src/adapters/unstoppable-api.mocks.ts | 24 + .../src/adapters/unstoppable-api.test.ts | 259 ++++++ .../resolver/src/adapters/unstoppable-api.ts | 113 +++ .../resolver/src/adapters/zod-errors.test.ts | 34 + packages/resolver/src/adapters/zod-errors.ts | 15 + .../resolver/src/fixtures/ErrorBoundary.tsx | 34 + .../src/fixtures/SuspenseBoundary.tsx | 16 + .../resolver/src/fixtures/manager-wrapper.tsx | 27 + .../resolver/src/fixtures/query-client.ts | 14 + packages/resolver/src/index.test.ts | 9 + packages/resolver/src/index.ts | 21 + .../resolver/src/translators/constants.ts | 7 + .../resolver/src/translators/manager.mocks.ts | 22 + .../resolver/src/translators/manager.test.ts | 23 + packages/resolver/src/translators/manager.ts | 14 + .../hooks/useResolverCryptoAddresses.test.tsx | 58 ++ .../hooks/useResolverCryptoAddresses.tsx | 36 + .../hooks/useResolverSetShowNotice.test.tsx | 43 + .../hooks/useResolverSetShowNotice.tsx | 21 + .../hooks/useResolverShowNotice.test.tsx | 73 ++ .../reactjs/hooks/useResolverShowNotice.tsx | 27 + .../provider/ResolverProvider.test.tsx | 57 ++ .../reactjs/provider/ResolverProvider.tsx | 31 + packages/resolver/src/utils/isDomain.test.ts | 13 + packages/resolver/src/utils/isDomain.ts | 4 + .../resolver/src/utils/isNameServer.test.ts | 15 + packages/resolver/src/utils/isNameServer.ts | 4 + .../src/utils/isResolvableDomain.test.ts | 27 + .../resolver/src/utils/isResolvableDomain.ts | 8 + .../utils/useMutationWithInvalidations.tsx | 34 + packages/resolver/tsconfig.build.json | 5 + packages/resolver/tsconfig.json | 26 + packages/swap/src/translators/reactjs.tsx | 0 packages/types/package.json | 3 + packages/types/src/index.ts | 39 + packages/types/src/resolver/api.ts | 23 + packages/types/src/resolver/errors.ts | 4 + packages/types/src/resolver/manager.ts | 9 + packages/types/src/resolver/name-server.ts | 5 + packages/types/src/resolver/receiver.ts | 8 + packages/types/src/resolver/storage.ts | 10 + scripts/install-pkgs.sh | 4 + yarn.lock | 228 +---- yoroi.code-workspace | 6 +- 123 files changed, 5179 insertions(+), 1605 deletions(-) create mode 100644 apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx create mode 100644 apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.tsx create mode 100644 apps/wallet-mobile/src/features/Send/common/constants.ts create mode 100644 apps/wallet-mobile/src/features/Send/common/errors.ts create mode 100644 apps/wallet-mobile/src/features/Send/common/useSendAddress.tsx create mode 100644 apps/wallet-mobile/src/features/Send/common/useSendReceiver.tsx delete mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/InputMemo.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoErrorTooLong.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoInstructions.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx delete mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ResolveAddress.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx create mode 100644 apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.tsx create mode 100644 apps/wallet-mobile/src/utils/debounceMaker.ts create mode 100644 packages/resolver/.dependency-cruiser.js create mode 100644 packages/resolver/.gitignore create mode 100644 packages/resolver/README.md create mode 100644 packages/resolver/babel.config.js create mode 100644 packages/resolver/jest.setup.js create mode 100644 packages/resolver/package.json create mode 100644 packages/resolver/scripts/flowgen.sh create mode 100644 packages/resolver/src/adapters/api.mocks.ts create mode 100644 packages/resolver/src/adapters/api.test.ts create mode 100644 packages/resolver/src/adapters/api.ts create mode 100644 packages/resolver/src/adapters/cns.test.ts create mode 100644 packages/resolver/src/adapters/cns.ts create mode 100644 packages/resolver/src/adapters/handle-api.mocks.ts create mode 100644 packages/resolver/src/adapters/handle-api.test.ts create mode 100644 packages/resolver/src/adapters/handle-api.ts create mode 100644 packages/resolver/src/adapters/storage.mocks.ts create mode 100644 packages/resolver/src/adapters/storage.test.ts create mode 100644 packages/resolver/src/adapters/storage.ts create mode 100644 packages/resolver/src/adapters/unstoppable-api.mocks.ts create mode 100644 packages/resolver/src/adapters/unstoppable-api.test.ts create mode 100644 packages/resolver/src/adapters/unstoppable-api.ts create mode 100644 packages/resolver/src/adapters/zod-errors.test.ts create mode 100644 packages/resolver/src/adapters/zod-errors.ts create mode 100644 packages/resolver/src/fixtures/ErrorBoundary.tsx create mode 100644 packages/resolver/src/fixtures/SuspenseBoundary.tsx create mode 100644 packages/resolver/src/fixtures/manager-wrapper.tsx create mode 100644 packages/resolver/src/fixtures/query-client.ts create mode 100644 packages/resolver/src/index.test.ts create mode 100644 packages/resolver/src/index.ts create mode 100644 packages/resolver/src/translators/constants.ts create mode 100644 packages/resolver/src/translators/manager.mocks.ts create mode 100644 packages/resolver/src/translators/manager.test.ts create mode 100644 packages/resolver/src/translators/manager.ts create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.test.tsx create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.tsx create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.test.tsx create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.tsx create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.test.tsx create mode 100644 packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.tsx create mode 100644 packages/resolver/src/translators/reactjs/provider/ResolverProvider.test.tsx create mode 100644 packages/resolver/src/translators/reactjs/provider/ResolverProvider.tsx create mode 100644 packages/resolver/src/utils/isDomain.test.ts create mode 100644 packages/resolver/src/utils/isDomain.ts create mode 100644 packages/resolver/src/utils/isNameServer.test.ts create mode 100644 packages/resolver/src/utils/isNameServer.ts create mode 100644 packages/resolver/src/utils/isResolvableDomain.test.ts create mode 100644 packages/resolver/src/utils/isResolvableDomain.ts create mode 100644 packages/resolver/src/utils/useMutationWithInvalidations.tsx create mode 100644 packages/resolver/tsconfig.build.json create mode 100644 packages/resolver/tsconfig.json delete mode 100644 packages/swap/src/translators/reactjs.tsx create mode 100644 packages/types/src/resolver/api.ts create mode 100644 packages/types/src/resolver/errors.ts create mode 100644 packages/types/src/resolver/manager.ts create mode 100644 packages/types/src/resolver/name-server.ts create mode 100644 packages/types/src/resolver/receiver.ts create mode 100644 packages/types/src/resolver/storage.ts diff --git a/apps/wallet-mobile/.env b/apps/wallet-mobile/.env index bf3b4c923e..1ac5746e88 100644 --- a/apps/wallet-mobile/.env +++ b/apps/wallet-mobile/.env @@ -17,3 +17,5 @@ BANXA_TEST_WALLET=addr1qyfuspldlchc5kechfe5jzdrr9jms2s9w0tq4hggy9zgkhxssydveuc8x FRONTEND_FEE_ADDRESS_MAINNET=addr1q9ry6jfdgm0lcrtfpgwrgxg7qfahv80jlghhrthy6w8hmyjuw9ngccy937pm7yw0jjnxasm7hzxjrf8rzkqcj26788lqws5fke FRONTEND_FEE_ADDRESS_PREPROD=addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km + +UNSTOPPABLE_API_KEY=czsajliz-wxgu6tujd1zqq7hey_pclfqhdjsqolsxjfsurgh diff --git a/apps/wallet-mobile/.env.nightly b/apps/wallet-mobile/.env.nightly index 24ff2b7c36..cb7d421413 100644 --- a/apps/wallet-mobile/.env.nightly +++ b/apps/wallet-mobile/.env.nightly @@ -8,3 +8,5 @@ BANXA_TEST_WALLET=addr1qyfuspldlchc5kechfe5jzdrr9jms2s9w0tq4hggy9zgkhxssydveuc8x FRONTEND_FEE_ADDRESS_MAINNET=addr1q9ry6jfdgm0lcrtfpgwrgxg7qfahv80jlghhrthy6w8hmyjuw9ngccy937pm7yw0jjnxasm7hzxjrf8rzkqcj26788lqws5fke FRONTEND_FEE_ADDRESS_PREPROD=addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km + +UNSTOPPABLE_API_KEY=czsajliz-wxgu6tujd1zqq7hey_pclfqhdjsqolsxjfsurgh diff --git a/apps/wallet-mobile/.env.production b/apps/wallet-mobile/.env.production index 44d32e4a7b..2300c957cc 100644 --- a/apps/wallet-mobile/.env.production +++ b/apps/wallet-mobile/.env.production @@ -5,3 +5,5 @@ SENTRY_DSN=https://7f7c6cb60a6f429facd34f491dfc5133@o1138840.ingest.sentry.io/67 FRONTEND_FEE_ADDRESS_MAINNET=addr1q9ry6jfdgm0lcrtfpgwrgxg7qfahv80jlghhrthy6w8hmyjuw9ngccy937pm7yw0jjnxasm7hzxjrf8rzkqcj26788lqws5fke FRONTEND_FEE_ADDRESS_PREPROD=addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km + +UNSTOPPABLE_API_KEY=czsajliz-wxgu6tujd1zqq7hey_pclfqhdjsqolsxjfsurgh diff --git a/apps/wallet-mobile/.env.staging b/apps/wallet-mobile/.env.staging index 052b636071..9c9cbb8f5d 100644 --- a/apps/wallet-mobile/.env.staging +++ b/apps/wallet-mobile/.env.staging @@ -17,3 +17,5 @@ BANXA_TEST_WALLET=addr1qyfuspldlchc5kechfe5jzdrr9jms2s9w0tq4hggy9zgkhxssydveuc8x FRONTEND_FEE_ADDRESS_MAINNET=addr1q9ry6jfdgm0lcrtfpgwrgxg7qfahv80jlghhrthy6w8hmyjuw9ngccy937pm7yw0jjnxasm7hzxjrf8rzkqcj26788lqws5fke FRONTEND_FEE_ADDRESS_PREPROD=addr_test1qrgpjmyy8zk9nuza24a0f4e7mgp9gd6h3uayp0rqnjnkl54v4dlyj0kwfs0x4e38a7047lymzp37tx0y42glslcdtzhqzp57km + +UNSTOPPABLE_API_KEY=czsajliz-wxgu6tujd1zqq7hey_pclfqhdjsqolsxjfsurgh diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index e656480dec..794a226259 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -120,6 +120,7 @@ const getStories = () => { "./src/features/Scan/useCases/ScanCodeScreen.stories.tsx": require("../src/features/Scan/useCases/ScanCodeScreen.stories.tsx"), "./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/OpenDeviceAppSettingsButton.stories.tsx"), "./src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx": require("../src/features/Scan/useCases/ShowCameraPermissionDeniedScreen/ShowCameraPermissionDeniedScreen.stories.tsx"), + "./src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx": require("../src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx"), "./src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.stories.tsx"), "./src/features/Send/useCases/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/FailedTx/FailedTxScreen.stories.tsx"), "./src/features/Send/useCases/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx": require("../src/features/Send/useCases/ConfirmTx/SubmittedTx/SubmittedTxScreen.stories.tsx"), @@ -128,6 +129,10 @@ const getStories = () => { "./src/features/Send/useCases/ListAmountsToSend/AddToken/Show/MaxAmountsPerTx.stories.tsx": require("../src/features/Send/useCases/ListAmountsToSend/AddToken/Show/MaxAmountsPerTx.stories.tsx"), "./src/features/Send/useCases/ListAmountsToSend/EditAmount/EditAmountScreen.stories.tsx": require("../src/features/Send/useCases/ListAmountsToSend/EditAmount/EditAmountScreen.stories.tsx"), "./src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.stories.tsx": require("../src/features/Send/useCases/ListAmountsToSend/ListAmountsToSendScreen.stories.tsx"), + "./src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx": require("../src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx"), + "./src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx": require("../src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx"), + "./src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx": require("../src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx"), + "./src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx": require("../src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx"), "./src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.stories.tsx": require("../src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.stories.tsx"), "./src/features/Settings/About/About.stories.tsx": require("../src/features/Settings/About/About.stories.tsx"), "./src/features/Settings/ApplicationSettings/ApplicationSettingsScreen.stories.tsx": require("../src/features/Settings/ApplicationSettings/ApplicationSettingsScreen.stories.tsx"), diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index 9891babc3b..63413ab01c 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -120,11 +120,11 @@ "@react-navigation/stack": "^6.3.16", "@sentry/react-native": "^5.8.0", "@shopify/flash-list": "^1.4.1", - "@unstoppabledomains/resolution": "6.0.3", "@yoroi/api": "1.3.0", "@yoroi/banxa": "1.3.0", "@yoroi/common": "1.3.0", "@yoroi/links": "1.3.0", + "@yoroi/resolver": "1.0.0", "@yoroi/staking": "1.0.0", "@yoroi/swap": "1.3.0", "add": "2.0.6", @@ -263,7 +263,7 @@ "prettier-plugin-packagejson": "^2.2.11", "prettylint": "^1.0.0", "react-addons-test-utils": "^15.6.2", - "react-devtools-core": "4.26.1", + "react-devtools-core": "^4.28", "react-dom": "16.8.3", "react-intl-translations-manager": "^5.0.3", "react-native-typescript-transformer": "^1.2.13", diff --git a/apps/wallet-mobile/src/TxHistory/ActionsBanner.tsx b/apps/wallet-mobile/src/TxHistory/ActionsBanner.tsx index c7d61f87a6..2f2ee53949 100644 --- a/apps/wallet-mobile/src/TxHistory/ActionsBanner.tsx +++ b/apps/wallet-mobile/src/TxHistory/ActionsBanner.tsx @@ -26,7 +26,7 @@ export const ActionsBanner = ({disabled = false}: {disabled: boolean}) => { const strings = useStrings() const navigateTo = useNavigateTo() const wallet = useSelectedWallet() - const {resetForm} = useSend() + const {reset: resetSendState} = useSend() const {orderData} = useSwap() const {resetSwapForm} = useSwapForm() const {track} = useMetrics() @@ -87,7 +87,7 @@ export const ActionsBanner = ({disabled = false}: {disabled: boolean}) => { const handleOnSend = () => { navigateTo.send() - resetForm() + resetSendState() } const handleOnSwap = () => { diff --git a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx index 725a8d6d1a..61abff4a1d 100644 --- a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx @@ -1,5 +1,6 @@ import {useNavigation} from '@react-navigation/native' import {createStackNavigator} from '@react-navigation/stack' +import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' import { milkTokenId, supportedProviders, @@ -8,7 +9,7 @@ import { SwapProvider, swapStorageMaker, } from '@yoroi/swap' -import {Swap} from '@yoroi/types' +import {Resolver, Swap} from '@yoroi/types' import React from 'react' import {defineMessages, useIntl} from 'react-intl' import {StyleSheet, Text, TouchableOpacity, TouchableOpacityProps, View, ViewProps} from 'react-native' @@ -39,6 +40,7 @@ import { } from '../features/Swap/useCases' import {SelectBuyTokenFromListScreen} from '../features/Swap/useCases/StartSwapScreen/CreateOrder/EditBuyAmount/SelectBuyTokenFromListScreen/SelectBuyTokenFromListScreen' import {SelectSellTokenFromListScreen} from '../features/Swap/useCases/StartSwapScreen/CreateOrder/EditSellAmount/SelectSellTokenFromListScreen/SelectSellTokenFromListScreen' +import {CONFIG} from '../legacy/config' import { BackButton, defaultStackNavigationOptions, @@ -84,6 +86,19 @@ export const TxHistoryNavigator = () => { return swapManagerMaker({swapStorage, swapApi, frontendFeeTiers, aggregator, aggregatorTokenId}) }, [wallet.networkId, wallet.primaryTokenInfo.id, stakingKey, frontendFees, aggregatorTokenId]) + // resolver + const resolverManager = React.useMemo(() => { + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: CONFIG.UNSTOPPABLE_API_KEY, + }, + }, + }) + const resolverStorage = resolverStorageMaker() + return resolverManagerMaker(resolverStorage, resolverApi) + }, []) + // claim const claimApi = React.useMemo(() => { return claimApiMaker({ @@ -98,218 +113,220 @@ export const TxHistoryNavigator = () => { return ( - - - - - - - {() => ( - - - - )} - - - , - headerStyle: { - elevation: 0, - shadowOpacity: 0, - backgroundColor: '#fff', - }, - }} - /> - - - - - - - - - - - - - - - - - - - {() => ( - - - - )} - - - - {() => ( - - - - )} - - - - {() => ( - - - - )} - - - + + + - {() => ( - - - - )} - - - - - - - - - , - }} - /> - - - - null}} - /> - - - - {strings.receiveInfoText} - - - + + + + {() => ( + + + + )} + + + , + headerStyle: { + elevation: 0, + shadowOpacity: 0, + backgroundColor: '#fff', + }, + }} + /> + + + + + + + + + + + + + + + + + + + {() => ( + + + + )} + + + + {() => ( + + + + )} + + + + {() => ( + + + + )} + + + + {() => ( + + + + )} + + + + + + + + + , + }} + /> + + + + null}} + /> + + + + {strings.receiveInfoText} + + + + ) diff --git a/apps/wallet-mobile/src/WalletInit/WalletForm.tsx b/apps/wallet-mobile/src/WalletInit/WalletForm.tsx index 03f678b128..f771d122af 100644 --- a/apps/wallet-mobile/src/WalletInit/WalletForm.tsx +++ b/apps/wallet-mobile/src/WalletInit/WalletForm.tsx @@ -81,7 +81,7 @@ export const WalletForm = ({onSubmit}: Props) => { onChangeText={setPassword} errorText={passwordErrorText} returnKeyType="next" - helperText={strings.passwordStrengthRequirement({ + helper={strings.passwordStrengthRequirement({ requiredPasswordLength: REQUIRED_PASSWORD_LENGTH, })} right={!passwordErrors.passwordIsWeak ? : undefined} diff --git a/apps/wallet-mobile/src/components/Icon/Check.tsx b/apps/wallet-mobile/src/components/Icon/Check.tsx index f2b2d7387e..b03a5f046d 100644 --- a/apps/wallet-mobile/src/components/Icon/Check.tsx +++ b/apps/wallet-mobile/src/components/Icon/Check.tsx @@ -2,21 +2,19 @@ import React from 'react' import {ImageStyle} from 'react-native' import Svg, {Path} from 'react-native-svg' -import {COLORS} from '../../theme' - type Props = { size?: number color?: string style?: ImageStyle } -export const Check = ({size = 40, color}: Props) => ( - +export const Check = ({size = 40, color = 'black'}: Props) => ( + ) diff --git a/apps/wallet-mobile/src/components/TextInput/TextInput.stories.tsx b/apps/wallet-mobile/src/components/TextInput/TextInput.stories.tsx index c52c9ba895..7b862596b6 100644 --- a/apps/wallet-mobile/src/components/TextInput/TextInput.stories.tsx +++ b/apps/wallet-mobile/src/components/TextInput/TextInput.stories.tsx @@ -83,7 +83,7 @@ storiesOf('TextInput', module) autoFocus label="with helper text" onChangeText={action('onChangeText')} - helperText="This is what helper text looks like" + helper="This is what helper text looks like" autoComplete="off" /> )) @@ -92,7 +92,7 @@ storiesOf('TextInput', module) autoFocus label="with helper text and error text" onChangeText={action('onChangeText')} - helperText="This is what helper text looks like" + helper="This is what helper text looks like" errorText="This is what an error looks likes" autoComplete="off" /> diff --git a/apps/wallet-mobile/src/components/TextInput/TextInput.tsx b/apps/wallet-mobile/src/components/TextInput/TextInput.tsx index a9bc3c4796..fb83082289 100644 --- a/apps/wallet-mobile/src/components/TextInput/TextInput.tsx +++ b/apps/wallet-mobile/src/components/TextInput/TextInput.tsx @@ -1,3 +1,4 @@ +import {isString} from '@yoroi/common' import React, {ForwardedRef} from 'react' import { StyleSheet, @@ -18,7 +19,7 @@ export type TextInputProps = RNTextInputProps & Omit, 'theme'> & { containerStyle?: ViewStyle renderComponentStyle?: ViewStyle - helperText?: string + helper?: string | React.ReactNode errorText?: string disabled?: boolean errorOnMount?: boolean @@ -52,7 +53,7 @@ export const TextInput = React.forwardRef((props: TextInputProps, ref: Forwarded containerStyle, renderComponentStyle, secureTextEntry, - helperText, + helper, errorText, errorOnMount, errorDelay, @@ -75,6 +76,20 @@ export const TextInput = React.forwardRef((props: TextInputProps, ref: Forwarded value, errorDelay, ) + const showError = errorTextEnabled && !isEmptyString(errorText) + const showHelperComponent = helper != null && !isString(helper) + + const helperToShow = showError ? ( + + {errorText} + + ) : showHelperComponent ? ( + helper + ) : ( + + {helper} + + ) return ( @@ -129,11 +144,7 @@ export const TextInput = React.forwardRef((props: TextInputProps, ref: Forwarded {...restProps} /> - {!noHelper && ( - - {errorTextEnabled && !isEmptyString(errorText) ? errorText : helperText} - - )} + {!noHelper && helperToShow} ) }) @@ -151,6 +162,7 @@ export const HelperText = ({ visible?: boolean }) => ( { @@ -43,9 +43,9 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeatur navigateTo.back() navigateTo.send() - if (insideFeature !== 'send') resetForm() + if (insideFeature !== 'send') resetSendState() - receiverChanged(scanAction.receiver) + receiverResolveChanged(scanAction.receiver) if (scanAction.params) { if ('amount' in scanAction.params) { @@ -66,15 +66,15 @@ export const useTriggerScanAction = ({insideFeature}: {insideFeature: ScanFeatur navigateTo.back() navigateTo.send() - if (insideFeature !== 'send') resetForm() + if (insideFeature !== 'send') resetSendState() - receiverChanged(scanAction.receiver) + receiverResolveChanged(scanAction.receiver) break } case 'claim': { navigateTo.back() - reset() + resetClaimState() scanActionClaimChanged(scanAction) const handleOnContinue = () => { diff --git a/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx b/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx new file mode 100644 index 0000000000..19120deb50 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.stories.tsx @@ -0,0 +1,41 @@ +import {action} from '@storybook/addon-actions' +import {storiesOf} from '@storybook/react-native' +import React from 'react' +import {StyleSheet, View} from 'react-native' + +import {ButtonGroup} from './ButtonGroup' + +const WithInitial = ({initial}: {initial: number}) => { + const handleActive = (index: number, label: string) => { + action(`onSelect ${index}:${label}`) + } + return ( + + labels={['label1', 'label2']} onSelect={handleActive} initial={initial} /> + + ) +} + +const NoInitial = () => { + const handleActive = (index: number, label: string) => { + action(`onSelect ${index}:${label}`) + } + return ( + + labels={Array.from({length: 10}, () => Math.random().toString())} onSelect={handleActive} /> + + ) +} + +storiesOf('Send ButtonGroup', module) + .addDecorator((story) => {story()}) + .add('label 1 inital', () => ) + .add('label 2 initial', () => ) + .add('many options', () => ) + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.tsx b/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 0000000000..054e9cfc9d --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import {StyleSheet, Text, TouchableOpacity, View, ViewProps} from 'react-native' + +import {Spacer} from '../../../../components/Spacer/Spacer' +import {COLORS} from '../../../../theme' + +type ButtonGroupProps = { + labels: T[] + onSelect: (index: number, label: T) => void + initial?: number +} + +export const ButtonGroup = ({ + initial, + labels, + onSelect, + style, + ...props +}: ButtonGroupProps & ViewProps) => { + const [selected, setSelected] = React.useState(initial) + + return ( + + {labels.map((label, index) => ( + <> + {index > 0 && } + + { + setSelected(index) + onSelect(index, label) + }} + style={[styles.button, index === selected && styles.selected]} + > + {label} + + + ))} + + ) +} + +const styles = StyleSheet.create({ + root: { + flexDirection: 'row', + }, + button: { + paddingHorizontal: 8, + paddingVertical: 8, + borderRadius: 8, + }, + selected: { + backgroundColor: COLORS.BORDER_GRAY, + }, + label: { + color: COLORS.BLACK, + fontFamily: 'Rubik-Medium', + fontWeight: '500', + lineHeight: 24, + fontSize: 16, + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/common/SendContext.test.tsx b/apps/wallet-mobile/src/features/Send/common/SendContext.test.tsx index 040bfc5750..6134ebe02e 100644 --- a/apps/wallet-mobile/src/features/Send/common/SendContext.test.tsx +++ b/apps/wallet-mobile/src/features/Send/common/SendContext.test.tsx @@ -1,5 +1,6 @@ import {act, renderHook} from '@testing-library/react-hooks' import {fireEvent, render} from '@testing-library/react-native' +import {Resolver} from '@yoroi/types' import * as React from 'react' import {TextInput} from 'react-native' @@ -11,7 +12,7 @@ import {initialState, SendProvider, useSelectedSecondaryAmountsCounter, useSend} const wrapper: React.FC = ({children}) => {children} -describe('SendContext :: ui', () => { +describe('SendContext', () => { test('resetForm', () => { const {getByTestId, getByText} = render( @@ -41,7 +42,7 @@ describe('SendContext :: hooks', () => { expect(result.current.memo).toBe('Test memo') act(() => { - result.current.resetForm() + result.current.reset() }) expect(result.current.targets).toEqual(initialState.targets) @@ -87,24 +88,19 @@ describe('SendContext :: hooks', () => { expect(result.current.yoroiUnsignedTx).toBeUndefined() }) - test('receiverChanged', () => { + test('receiverResolveChanged', () => { const {result} = renderHook(() => useSend(), {wrapper}) act(() => { - result.current.receiverChanged('receiver123') + result.current.receiverResolveChanged('address') }) - expect(result.current.targets[0].receiver).toBe('receiver123') - }) - - test('addressChanged', () => { - const {result} = renderHook(() => useSend(), {wrapper}) - - act(() => { - result.current.addressChanged('address123') + expect(result.current.targets[0].receiver).toEqual({ + resolve: 'address', + as: 'address', + selectedNameServer: undefined, + addressRecords: undefined, }) - - expect(result.current.targets[0].entry.address).toBe('address123') }) test('amountChanged', () => { @@ -151,13 +147,13 @@ describe('SendContext :: hooks', () => { }) const ResetFormTest = () => { - const {memoChanged, resetForm, memo} = useSend() + const {memoChanged, reset, memo} = useSend() return ( <> - + ) } diff --git a/apps/wallet-mobile/src/features/Send/common/SendContext.tsx b/apps/wallet-mobile/src/features/Send/common/SendContext.tsx index b6e5484793..213b6fd279 100644 --- a/apps/wallet-mobile/src/features/Send/common/SendContext.tsx +++ b/apps/wallet-mobile/src/features/Send/common/SendContext.tsx @@ -1,4 +1,6 @@ -import {Balance} from '@yoroi/types' +import {isNameServer, isResolvableDomain} from '@yoroi/resolver' +import {Balance, Resolver} from '@yoroi/types' +import {produce} from 'immer' import * as React from 'react' import {useSelectedWallet} from '../../../SelectedWallet/Context/SelectedWalletContext' @@ -18,16 +20,19 @@ export type SendState = { } type TargetActions = { + // Amount amountChanged: (quantity: Balance.Quantity) => void amountRemoved: (tokenId: string) => void - receiverChanged: (receiver: string) => void - addressChanged: (address: Address) => void + // Receiver + receiverResolveChanged: (resolve: Resolver.Receiver['resolve']) => void + nameServerSelectedChanged: (nameServer: Resolver.Receiver['selectedNameServer']) => void + addressRecordsFetched: (addressRecords: Resolver.Receiver['addressRecords']) => void } type SendActions = { yoroiUnsignedTxChanged: (yoroiUnsignedTx: YoroiUnsignedTx | undefined) => void tokenSelectedChanged: (tokenId: string) => void - resetForm: () => void + reset: () => void memoChanged: (memo: string) => void } @@ -51,10 +56,14 @@ export const SendProvider = ({children, ...props}: {initialState?: Partial({ - resetForm: () => dispatch({type: 'resetForm'}), + reset: () => dispatch({type: 'reset'}), - receiverChanged: (receiver: string) => dispatch({type: 'receiverChanged', receiver}), - addressChanged: (address: Address) => dispatch({type: 'addressChanged', address}), + receiverResolveChanged: (resolve: Resolver.Receiver['resolve']) => + dispatch({type: 'receiverResolveChanged', resolve}), + nameServerSelectedChanged: (nameServer: Resolver.Receiver['selectedNameServer']) => + dispatch({type: 'nameServerSelectedChanged', nameServer}), + addressRecordsFetched: (addressRecords: Resolver.Receiver['addressRecords']) => + dispatch({type: 'addressRecordsFetched', addressRecords}), memoChanged: (memo) => dispatch({type: 'memoChanged', memo}), @@ -71,7 +80,7 @@ export const SendProvider = ({children, ...props}: {initialState?: Partial { switch (action.type) { - case 'resetForm': + case 'reset': return {...initialState} case 'memoChanged': @@ -116,8 +125,16 @@ const sendReducer = (state: SendState, action: SendAction) => { export type TargetAction = | { - type: 'receiverChanged' - receiver: string + type: 'receiverResolveChanged' + resolve: Resolver.Receiver['resolve'] + } + | { + type: 'nameServerSelectedChanged' + nameServer: Resolver.Receiver['selectedNameServer'] + } + | { + type: 'addressRecordsFetched' + addressRecords: Resolver.Receiver['addressRecords'] } | { type: 'addressChanged' @@ -137,74 +154,96 @@ export type TargetAction = } const targetsReducer = (state: SendState, action: TargetAction) => { - switch (action.type) { - case 'receiverChanged': { - const {receiver} = action - const selectedTargetIndex = state.selectedTargetIndex - const updatedTargets = state.targets.map((target, index) => { - if (index === selectedTargetIndex) { - return {...target, receiver} - } - - return {...target} - }) - - return updatedTargets - } - - case 'addressChanged': { - const {address} = action - const selectedTargetIndex = state.selectedTargetIndex - const updatedTargets = state.targets.map((target, index) => { - if (index === selectedTargetIndex) { - return {...target, entry: {...target.entry, address}} - } - - return {...target} - }) - - return updatedTargets - } + return produce(state.targets, (draft) => { + switch (action.type) { + case 'receiverResolveChanged': { + const {resolve} = action + const selectedTargetIndex = state.selectedTargetIndex + + draft.forEach((target, index) => { + if (index === selectedTargetIndex) { + const isDomain: boolean = isResolvableDomain(resolve) + const as: Resolver.Receiver['as'] = isDomain ? 'domain' : 'address' + const address = isDomain ? '' : resolve + target.receiver = { + resolve, + as, + selectedNameServer: undefined, + addressRecords: undefined, + } + target.entry.address = address + } + }) + break + } - case 'amountChanged': { - const {quantity} = action - const selectedTargetIndex = state.selectedTargetIndex - const selectedTokenId = state.selectedTokenId - const updatedTargets = state.targets.map((target, index) => { - if (index === selectedTargetIndex) { - return {...target, entry: {...target.entry, amounts: {...target.entry.amounts, [selectedTokenId]: quantity}}} - } + case 'addressRecordsFetched': { + const {addressRecords} = action + const selectedTargetIndex = state.selectedTargetIndex + + draft.forEach((target, index) => { + if (index === selectedTargetIndex) { + if (addressRecords !== undefined) { + const keys = Object.keys(addressRecords).filter(isNameServer) + const nameServer = keys.length === 1 ? keys[0] : undefined + target.receiver.selectedNameServer = nameServer + if (nameServer !== undefined) { + target.entry.address = addressRecords[nameServer] ?? '' + } + } else { + target.receiver.selectedNameServer = undefined + } + target.receiver.addressRecords = addressRecords + } + }) + break + } - return {...target} - }) + case 'nameServerSelectedChanged': { + const {nameServer} = action + const selectedTargetIndex = state.selectedTargetIndex - return updatedTargets - } + draft.forEach((target, index) => { + if (index === selectedTargetIndex) { + target.receiver.selectedNameServer = nameServer - case 'amountRemoved': { - const {tokenId} = action - const selectedTargetIndex = state.selectedTargetIndex - const updatedTargets = state.targets.map((target, index) => { - if (index === selectedTargetIndex) { - const amounts = Object.keys(target.entry.amounts).reduce((acc, key) => { - if (key !== tokenId) { - acc[key] = target.entry.amounts[key] + if (nameServer !== undefined) { + target.entry.address = target.receiver.addressRecords?.[nameServer] ?? '' + } else { + const isDomain = target.receiver.as === 'domain' + if (isDomain) target.entry.address = '' } + } + }) + break + } - return acc - }, {}) - return {...target, entry: {...target.entry, amounts}} - } + case 'amountChanged': { + const {quantity} = action + const selectedTargetIndex = state.selectedTargetIndex + const selectedTokenId = state.selectedTokenId + + draft.forEach((target, index) => { + if (index === selectedTargetIndex) { + target.entry.amounts[selectedTokenId] = quantity + } + }) + break + } - return {...target} - }) + case 'amountRemoved': { + const {tokenId} = action + const selectedTargetIndex = state.selectedTargetIndex - return updatedTargets + draft.forEach((target, index) => { + if (index === selectedTargetIndex) { + delete target.entry.amounts[tokenId] + } + }) + break + } } - - default: - return state.targets - } + }) } export const useSend = () => React.useContext(SendContext) || missingProvider() @@ -222,7 +261,12 @@ export const initialState: SendState = { targets: [ { - receiver: '', + receiver: { + resolve: '', + as: 'address', + selectedNameServer: undefined, + addressRecords: undefined, + }, entry: { address: '', amounts: {}, diff --git a/apps/wallet-mobile/src/features/Send/common/constants.ts b/apps/wallet-mobile/src/features/Send/common/constants.ts new file mode 100644 index 0000000000..26052c2a1a --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/constants.ts @@ -0,0 +1 @@ +export const memoMaxLenght = 256 diff --git a/apps/wallet-mobile/src/features/Send/common/errors.ts b/apps/wallet-mobile/src/features/Send/common/errors.ts new file mode 100644 index 0000000000..f2afcd1285 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/errors.ts @@ -0,0 +1,2 @@ +export class AddressErrorWrongNetwork extends Error {} +export class AddressErrorInvalid extends Error {} diff --git a/apps/wallet-mobile/src/features/Send/common/mocks.ts b/apps/wallet-mobile/src/features/Send/common/mocks.ts index 3a9ec35bc8..d893d11bd1 100644 --- a/apps/wallet-mobile/src/features/Send/common/mocks.ts +++ b/apps/wallet-mobile/src/features/Send/common/mocks.ts @@ -13,7 +13,10 @@ export const mocks = { targets: [ { ...initialState.targets[0], - receiver: 'invalid_address', + receiver: { + ...initialState.targets[0].receiver, + receiver: 'invalid_address', + }, entry: { ...initialState.targets[0].entry, address: 'invalid_address', diff --git a/apps/wallet-mobile/src/features/Send/common/strings.ts b/apps/wallet-mobile/src/features/Send/common/strings.ts index b9c7ae7d82..2d6565561e 100644 --- a/apps/wallet-mobile/src/features/Send/common/strings.ts +++ b/apps/wallet-mobile/src/features/Send/common/strings.ts @@ -6,12 +6,11 @@ export const useStrings = () => { const intl = useIntl() return { - addressInputErrorInvalidAddress: intl.formatMessage(messages.addressInputErrorInvalidAddress), addressInputLabel: intl.formatMessage(messages.addressInputLabel), + addressReaderQrText: intl.formatMessage(messages.addressReaderQrText), all: intl.formatMessage(globalMessages.all), amount: intl.formatMessage(txLabels.amount), apply: intl.formatMessage(globalMessages.apply), - pools: intl.formatMessage(globalMessages.pools), asset: intl.formatMessage(messages.asset), assets: (qty: number) => intl.formatMessage(globalMessages.assets, {qty}), assetsLabel: intl.formatMessage(globalMessages.assetsLabel), @@ -29,10 +28,20 @@ export const useStrings = () => { domainUnsupportedError: intl.formatMessage(messages.domainUnsupportedError), errorBannerNetworkError: intl.formatMessage(messages.errorBannerNetworkError), errorBannerPendingOutgoingTransaction: intl.formatMessage(messages.errorBannerPendingOutgoingTransaction), + failedTxButton: intl.formatMessage(messages.failedTxButton), + failedTxText: intl.formatMessage(messages.failedTxText), + failedTxTitle: intl.formatMessage(messages.failedTxTitle), feeLabel: intl.formatMessage(messages.feeLabel), feeNotAvailable: intl.formatMessage(messages.feeNotAvailable), found: intl.formatMessage(messages.found), + helperAddressErrorInvalid: intl.formatMessage(messages.helperAddressErrorInvalid), + helperAddressErrorWrongNetwork: intl.formatMessage(messages.helperAddressErrorWrongNetwork), + helperMemoErrorTooLong: intl.formatMessage(messages.helperMemoErrorTooLong), + helperMemoInstructions: intl.formatMessage(messages.helperMemoInstructions), + helperResolverErrorUnsupportedDomain: intl.formatMessage(messages.helperResolverErrorUnsupportedDomain), + manyNameServersWarning: intl.formatMessage(messages.manyNameServersWarning), max: intl.formatMessage(globalMessages.max), + memoLabel: intl.formatMessage(messages.memoLabel), minPrimaryBalanceForTokens: intl.formatMessage(amountInputErrorMessages.minPrimaryBalanceForTokens), next: intl.formatMessage(globalMessages.next), nfts: (qty: number) => intl.formatMessage(globalMessages.nfts, {qty}), @@ -41,7 +50,10 @@ export const useStrings = () => { noBalance: intl.formatMessage(amountInputErrorMessages.insufficientBalance), ok: intl.formatMessage(globalMessages.ok), pleaseWait: intl.formatMessage(globalMessages.pleaseWait), - resolvesTo: intl.formatMessage(messages.resolvesTo), + pools: intl.formatMessage(globalMessages.pools), + receiver: intl.formatMessage(messages.receiver), + resolvedAddress: intl.formatMessage(messages.resolvedAddress), + resolverNoticeTitle: intl.formatMessage(messages.resolverNoticeTitle), searchTokens: intl.formatMessage(messages.searchTokens), selecteAssetTitle: intl.formatMessage(messages.selectAssetTitle), sendAllContinueButton: intl.formatMessage(confirmationMessages.commonButtons.continueButton), @@ -50,16 +62,13 @@ export const useStrings = () => { sendAllWarningAlert3: intl.formatMessage(messages.sendAllWarningAlert3), sendAllWarningText: intl.formatMessage(messages.sendAllWarningText), sendAllWarningTitle: intl.formatMessage(messages.sendAllWarningTitle), + submittedTxButton: intl.formatMessage(messages.submittedTxButton), + submittedTxText: intl.formatMessage(messages.submittedTxText), + submittedTxTitle: intl.formatMessage(messages.submittedTxTitle), tokens: (qty: number) => intl.formatMessage(globalMessages.tokens, {qty}), unknownAsset: intl.formatMessage(messages.unknownAsset), + walletAddress: intl.formatMessage(messages.walletAddress), youHave: intl.formatMessage(messages.youHave), - submittedTxTitle: intl.formatMessage(messages.submittedTxTitle), - submittedTxText: intl.formatMessage(messages.submittedTxText), - submittedTxButton: intl.formatMessage(messages.submittedTxButton), - failedTxTitle: intl.formatMessage(messages.failedTxTitle), - failedTxText: intl.formatMessage(messages.failedTxText), - failedTxButton: intl.formatMessage(messages.failedTxButton), - addressReaderQrText: intl.formatMessage(messages.addressReaderQrText), } } @@ -103,6 +112,14 @@ export const amountInputErrorMessages = defineMessages({ }) export const messages = defineMessages({ + walletAddress: { + id: 'components.send.sendscreen.walletAddress', + defaultMessage: '!!!Wallet Address', + }, + receiver: { + id: 'components.send.sendscreen.receiver', + defaultMessage: '!!!Receiver', + }, feeLabel: { id: 'components.send.sendscreen.feeLabel', defaultMessage: '!!!Fee', @@ -131,9 +148,13 @@ export const messages = defineMessages({ id: 'components.send.sendscreen.addressInputErrorInvalidAddress', defaultMessage: '!!!Please enter valid address', }, + addressInputErrorInvalidDomain: { + id: 'components.send.sendscreen.addressInputErrorInvalidDomain', + defaultMessage: '!!!Please enter valid domain', + }, addressInputLabel: { id: 'components.send.confirmscreen.receiver', - defaultMessage: '!!!Address', + defaultMessage: '!!!Receiver address, ADA Handle or domains', }, checkboxSendAllAssets: { id: 'components.send.sendscreen.checkboxSendAllAssets', @@ -158,10 +179,6 @@ export const messages = defineMessages({ defaultMessage: '!!!Domain is not supported', description: 'some desc', }, - resolvesTo: { - id: 'components.send.sendscreen.resolvesTo', - defaultMessage: '!!!Resolves to', - }, searchTokens: { id: 'components.send.sendscreen.searchTokens', defaultMessage: '!!!Search tokens', @@ -257,4 +274,40 @@ export const messages = defineMessages({ id: 'components.send.addressreaderqr.text', defaultMessage: '!!!Scan recipients QR code to add a wallet address', }, + resolvedAddress: { + id: 'components.send.sendscreen.resolvedAddress', + defaultMessage: '!!!Related Address', + }, + resolverNoticeTitle: { + id: 'components.send.sendscreen.resolverNoticeTitle', + defaultMessage: '!!!Yoroi Supports Name Resolution', + }, + manyNameServersWarning: { + id: 'send.warning.resolver.manyNameServers', + defaultMessage: '!!!Multiple name servers found. Please select one.', + }, + helperAddressErrorInvalid: { + id: 'send.helper.addressError.invalid', + defaultMessage: '!!!Please enter valid address', + }, + helperAddressErrorWrongNetwork: { + id: 'send.helper.addressError.wrongNetwork', + defaultMessage: '!!!Please enter valid domain', + }, + helperResolverErrorUnsupportedDomain: { + id: 'send.helper.resolverError.unsupportedDomain', + defaultMessage: '!!!Domain is not supported', + }, + memoLabel: { + id: 'components.send.memofield.label', + defaultMessage: '!!!Memo', + }, + helperMemoInstructions: { + id: 'components.send.memofield.message', + defaultMessage: '!!!(Optional) Memo is stored locally', + }, + helperMemoErrorTooLong: { + id: 'components.send.memofield.error', + defaultMessage: '!!!Memo is too long', + }, }) diff --git a/apps/wallet-mobile/src/features/Send/common/useSendAddress.tsx b/apps/wallet-mobile/src/features/Send/common/useSendAddress.tsx new file mode 100644 index 0000000000..86d2974f25 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/useSendAddress.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' +import {useQuery, UseQueryOptions} from 'react-query' + +import {useSelectedWallet} from '../../../SelectedWallet' +import {normalizeToAddress, toCardanoNetworkId} from '../../../yoroi-wallets/cardano/utils' +import {AddressErrorInvalid, AddressErrorWrongNetwork} from './errors' +import {useSend} from './SendContext' + +export const useSendAddress = () => { + const wallet = useSelectedWallet() + const chainId = toCardanoNetworkId(wallet.networkId) + + const {targets, selectedTargetIndex} = useSend() + const {address} = targets[selectedTargetIndex].entry + + const { + addressValidated, + isLoading: isValidatingAddress, + error: addressError, + refetch, + } = useValidateAddress( + {address, chainId}, + { + enabled: false, + }, + ) + + React.useEffect(() => { + if (address.length === 0) return + + refetch() + }, [address, refetch]) + + return { + addressValidated, + addressError, + isValidatingAddress, + } +} + +const useValidateAddress = ( + {address, chainId}: {address: string; chainId: number}, + options?: UseQueryOptions, +) => { + const query = useQuery({ + ...options, + staleTime: 0, + cacheTime: 0, + queryKey: ['useValidateAddress', address, chainId], + queryFn: () => validateAddress(address, chainId), + }) + + return { + ...query, + addressValidated: query.data, + } +} + +// NOTE: should be a wallet function from address manager +const validateAddress = async (address: string, chainId: number) => { + const chainAddress = await normalizeToAddress(address) + if (!chainAddress) throw new AddressErrorInvalid() + + const chainAddressChainId = await chainAddress.networkId() + if (chainAddressChainId !== chainId) throw new AddressErrorWrongNetwork() + + return true +} diff --git a/apps/wallet-mobile/src/features/Send/common/useSendReceiver.tsx b/apps/wallet-mobile/src/features/Send/common/useSendReceiver.tsx new file mode 100644 index 0000000000..f07f3e50d6 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/common/useSendReceiver.tsx @@ -0,0 +1,72 @@ +import {isDomain, isNameServer, isResolvableDomain, useResolverCryptoAddresses} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' +import {useQueryClient} from 'react-query' + +import {debounceMaker} from '../../../utils/debounceMaker' +import {useSend} from './SendContext' + +export const useSendReceiver = () => { + const queryClient = useQueryClient() + + const {targets, selectedTargetIndex, receiverResolveChanged, addressRecordsFetched} = useSend() + const receiver = targets[selectedTargetIndex].receiver + const isUnsupportedDomain = !isResolvableDomain(receiver.resolve) && isDomain(receiver.resolve) + + const { + error: receiverError, + cryptoAddresses, + refetch, + isLoading: isResolvingAddressess, + isSuccess, + } = useResolverCryptoAddresses( + {resolve: receiver.resolve}, + { + enabled: false, + }, + ) + const debouncedRefetch = React.useMemo(() => debounceMaker(refetch, 300), [refetch]) + + const cancelPendingRequests = React.useCallback( + () => queryClient.cancelQueries({queryKey: ['useResolverCryptoAddresses']}), + [queryClient], + ) + + React.useEffect(() => { + if (receiver.as === 'domain') cancelPendingRequests().then(() => debouncedRefetch.call()) + if (receiver.as === 'address') cancelPendingRequests() + return () => debouncedRefetch.clear() + }, [ + receiver.as, + refetch, + receiver.resolve, + debouncedRefetch, + queryClient, + cancelPendingRequests, + receiverResolveChanged, + ]) + + React.useEffect(() => { + if (isSuccess && cryptoAddresses !== undefined) { + const records = cryptoAddresses.reduce( + (addressRecords: Resolver.Receiver['addressRecords'], {address, nameServer}) => { + if (address !== null && nameServer !== null && isNameServer(nameServer) === true) + if (addressRecords !== undefined) { + addressRecords[nameServer] = address + } else { + return {[nameServer]: address} + } + return addressRecords + }, + undefined, + ) + addressRecordsFetched(records) + } + }, [addressRecordsFetched, cryptoAddresses, isSuccess]) + + return { + isResolvingAddressess, + isUnsupportedDomain, + receiverError, + } +} diff --git a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx index 0793033ee6..6d0ad1def9 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/ConfirmTxScreen.tsx @@ -86,7 +86,7 @@ export const ConfirmTxScreen = () => { {targets.map((target, index) => ( - + ))} @@ -103,15 +103,13 @@ export const ConfirmTxScreen = () => { {!wallet.isEasyConfirmationEnabled && !wallet.isHW && ( - <> - - + )} diff --git a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.tsx b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.tsx index a47f6f1819..28639c8196 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.tsx @@ -1,48 +1,46 @@ +import {nameServerName} from '@yoroi/resolver' import * as React from 'react' -import {defineMessages, useIntl} from 'react-intl' import {View} from 'react-native' +import {Spacer} from '../../../../../components/Spacer' import {Text} from '../../../../../components/Text' -import {txLabels} from '../../../../../i18n/global-messages' +import {YoroiTarget} from '../../../../../yoroi-wallets/types' +import {useStrings} from '../../../common/strings' type Props = { - receiver: string - address: string + target: YoroiTarget } -export const ReceiverInfo = ({receiver, address}: Props) => { +export const ReceiverInfo = ({target}: Props) => { const strings = useStrings() - - const isResolved = receiver !== address + const {receiver, entry} = target return ( - {strings.receiver} + {strings.receiver}: - {receiver} + - {isResolved && ( - - {strings.resolvesTo} + {target.receiver.as === 'domain' ? ( + <> + + {receiver.selectedNameServer ? nameServerName[receiver.selectedNameServer] : ''}: - {address} - - )} - - ) -} + -const messages = defineMessages({ - resolvesTo: { - id: 'components.send.sendscreen.resolvesTo', - defaultMessage: '!!!Resolves to', - }, -}) + {receiver.resolve} + -const useStrings = () => { - const intl = useIntl() + - return { - receiver: intl.formatMessage(txLabels.receiver), - resolvesTo: intl.formatMessage(messages.resolvesTo), - } + {strings.walletAddress}: + + + + {entry.address} + + ) : ( + {entry.address} + )} + + ) } diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo.tsx deleted file mode 100644 index 9c1f71bba0..0000000000 --- a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react' -import {defineMessages, useIntl} from 'react-intl' -import {StyleSheet, Text, View} from 'react-native' - -import {HelperText, TextInput} from '../../../../components' - -export const maxMemoLength = 256 - -type Props = { - memo: string - onChangeText: (memo: string) => void -} - -export const InputMemo = ({onChangeText, memo}: Props) => { - const strings = useStrings() - - const showError = memo.length > maxMemoLength - - return ( - - onChangeText(memo)} - label={strings.label} - autoComplete="off" - testID="memoFieldInput" - errorText={showError ? 'error' : undefined} // to show the error styling - renderComponentStyle={styles.input} - noHelper - multiline - focusable - /> - - - - - - - - ) -} - -const Message = ({showError}: {showError: boolean}) => { - const strings = useStrings() - return {showError ? strings.error : strings.message} -} - -const LengthCounter = ({memo, showError}: {memo: string; showError: boolean}) => { - return ( - - {`${memo.length}/${maxMemoLength}`} - - ) -} - -const useStrings = () => { - const intl = useIntl() - - return { - label: intl.formatMessage(messages.label), - message: intl.formatMessage(messages.message), - error: intl.formatMessage(messages.error), - } -} - -export const messages = defineMessages({ - label: { - id: 'components.send.memofield.label', - defaultMessage: '!!!Memo', - }, - message: { - id: 'components.send.memofield.message', - defaultMessage: '!!!(Optional) Memo is stored locally', - }, - error: { - id: 'components.send.memofield.error', - defaultMessage: '!!!Memo is too long', - }, -}) - -const styles = StyleSheet.create({ - container: { - paddingBottom: 25, - }, - helper: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - input: { - maxHeight: 80, - }, -}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/InputMemo.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/InputMemo.tsx new file mode 100644 index 0000000000..152037f4e3 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/InputMemo.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import {StyleSheet} from 'react-native' + +import {TextInput, TextInputProps} from '../../../../../components' +import {useStrings} from '../../../common/strings' +import {ShowMemoErrorTooLong} from './ShowMemoErrorTooLong' +import {ShowMemoInstructions} from './ShowMemoInstructions' + +export const InputMemo = ({isValid, value, ...props}: {isValid?: boolean} & TextInputProps) => { + const strings = useStrings() + + return ( + : } + {...props} + /> + ) +} + +const styles = StyleSheet.create({ + input: { + maxHeight: 80, + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoErrorTooLong.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoErrorTooLong.tsx new file mode 100644 index 0000000000..c8c7be3bb6 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoErrorTooLong.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import {StyleSheet, View} from 'react-native' + +import {HelperText} from '../../../../../components' +import {memoMaxLenght} from '../../../common/constants' +import {useStrings} from '../../../common/strings' + +export const ShowMemoErrorTooLong = ({memo = ''}: {memo?: string}) => { + const strings = useStrings() + + const lenghtInfo = `${memo.length}/${memoMaxLenght}` + + return ( + + {strings.helperMemoErrorTooLong} + + {lenghtInfo} + + ) +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoInstructions.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoInstructions.tsx new file mode 100644 index 0000000000..d75cd4d995 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputMemo/ShowMemoInstructions.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import {StyleSheet, View} from 'react-native' + +import {HelperText} from '../../../../../components' +import {memoMaxLenght} from '../../../common/constants' +import {useStrings} from '../../../common/strings' + +export const ShowMemoInstructions = ({memo = ''}: {memo?: string}) => { + const strings = useStrings() + + const lenghtInfo = `${memo.length}/${memoMaxLenght}` + + return ( + + {strings.helperMemoInstructions} + + {lenghtInfo} + + ) +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx new file mode 100644 index 0000000000..5342ebbc5d --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.stories.tsx @@ -0,0 +1,41 @@ +import {storiesOf} from '@storybook/react-native' +import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' + +import {QueryProvider} from '../../../../../../.storybook/decorators' +import {Boundary} from '../../../../../components' +import {SelectedWalletProvider} from '../../../../../SelectedWallet' +import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' +import {mocks as walletMocks} from '../../../../../yoroi-wallets/mocks/wallet' +import {SendProvider} from '../../../common/SendContext' +import {InputReceiver} from './InputReceiver' + +storiesOf('Send InputReceiver', module) + .addDecorator((story) => { + const wallet: YoroiWallet = walletMocks.wallet + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'apiKey', + }, + }, + }) + const resolverStorage = resolverStorageMaker() + const resolverManager = resolverManagerMaker(resolverStorage, resolverApi) + + return ( + + + + + {story()} + + + + + ) + }) + .add('loading', () => ) + .add('valid', () => ) + .add('invalid', () => ) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.tsx index 263eec69d9..ba859dd972 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/InputReceiver.tsx @@ -1,18 +1,30 @@ import React from 'react' -import {StyleSheet} from 'react-native' +import {ActivityIndicator, StyleSheet} from 'react-native' -import {TextInput, TextInputProps} from '../../../../../components' +import {Icon, TextInput, TextInputProps} from '../../../../../components' import {useNavigateTo} from '../../../common/navigation' import {ScannerButton} from '../../../common/ScannerButton' import {useStrings} from '../../../common/strings' +import {ShowResolvedAddressSelected} from './ShowResolvedAddressSelected' -export const InputReceiver = ({isLoading, ...props}: {isLoading: boolean} & TextInputProps) => { +export const InputReceiver = ({ + isLoading, + isValid, + ...props +}: {isLoading?: boolean; isValid?: boolean} & TextInputProps) => { const strings = useStrings() const navigateTo = useNavigateTo() + const rightAdornment = isLoading ? ( + + ) : isValid ? ( + + ) : ( + + ) return ( } + right={rightAdornment} label={strings.addressInputLabel} testID="receiverInput" style={styles.receiver} @@ -21,9 +33,9 @@ export const InputReceiver = ({isLoading, ...props}: {isLoading: boolean} & Text autoFocus errorOnMount showErrorOnBlur - noHelper multiline blurOnSubmit + helper={} {...props} /> ) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ResolveAddress.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ResolveAddress.tsx deleted file mode 100644 index 41abd8a568..0000000000 --- a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ResolveAddress.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import {Resolution} from '@unstoppabledomains/resolution' -import React from 'react' -import {Text, View, ViewProps} from 'react-native' -import {useQuery, UseQueryOptions} from 'react-query' - -import {HelperText} from '../../../../../components' -import {getNetworkConfigById} from '../../../../../yoroi-wallets/cardano/networks' -import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' -import {normalizeToAddress} from '../../../../../yoroi-wallets/cardano/utils' -import {NetworkId} from '../../../../../yoroi-wallets/types' -import {useStrings} from '../../../common/strings' -import {InputReceiver} from './InputReceiver' - -type ReceiverProps = ViewProps & { - receiver: string - address: string - errorMessage: string - isLoading: boolean - onChangeReceiver: (receiver: string) => void -} -export const ResolveAddress = ({ - isLoading, - address, - receiver, - errorMessage, - onChangeReceiver, - style, - ...props -}: ReceiverProps) => { - const strings = useStrings() - const isResolved = !isLoading && isDomain(receiver) && !receiver.includes(address) - const isError = errorMessage.length > 0 - - return ( - - - - - {isLoading ? {strings.pleaseWait} : {errorMessage}} - - - {isResolved && ( - - {`${strings.resolvesTo}: ${address}`} - - )} - - ) -} - -export const useReceiver = ( - {wallet, receiver}: {wallet: YoroiWallet; receiver: string}, - options?: UseQueryOptions, -) => { - const query = useQuery({ - queryKey: ['receiver', receiver], - queryFn: () => resolveAndCheckAddress(receiver, wallet.networkId), - ...options, - }) - - return { - ...query, - address: query.data ?? '', - } -} - -const resolveAndCheckAddress = async (receiver: string, networkId: NetworkId) => { - let address = receiver - if (isDomain(receiver)) { - address = await getUnstoppableDomainAddress(receiver) - } - - await isReceiverAddressValid(address, networkId) - return address -} - -export const getAddressErrorMessage = (error: Error & {code?: string}, strings: ReturnType) => { - switch (error?.code) { - case 'UnsupportedDomain': - return strings.domainUnsupportedError - case 'RecordNotFound': - return strings.domainRecordNotFoundError - case 'UnregisteredDomain': - return strings.domainNotRegisteredError - default: - return strings.addressInputErrorInvalidAddress - } -} - -const getUnstoppableDomainAddress = (receiver: string) => { - const resolution = new Resolution() - return resolution.addr(receiver, 'ADA') -} - -const isReceiverAddressValid = async (resolvedAddress: string, walletNetworkId: NetworkId): Promise => { - if (resolvedAddress.length === 0) return Promise.resolve() - - const address = await normalizeToAddress(resolvedAddress) - if (!address) return Promise.reject(new Error('Invalid address')) - - try { - const networkConfig: any = getNetworkConfigById(walletNetworkId) - const configNetworkId = Number(networkConfig.CHAIN_NETWORK_ID) - const addressNetworkId = await address.networkId() - if (addressNetworkId !== configNetworkId && !isNaN(configNetworkId)) { - return Promise.reject(new Error('Invalid address')) - } - } catch (e) { - return Promise.reject(new Error('Should not happen')) - } -} - -const isDomain = (receiver: string) => /.+\..+/.test(receiver) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx new file mode 100644 index 0000000000..3577a98472 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.stories.tsx @@ -0,0 +1,114 @@ +import {storiesOf} from '@storybook/react-native' +import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' + +import {QueryProvider} from '../../../../../../.storybook/decorators' +import {Boundary} from '../../../../../components' +import {SelectedWalletProvider} from '../../../../../SelectedWallet' +import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' +import {mocks as walletMocks} from '../../../../../yoroi-wallets/mocks/wallet' +import {initialState, SendProvider, SendState} from '../../../common/SendContext' +import {ShowResolvedAddressSelected} from './ShowResolvedAddressSelected' + +storiesOf('Send ShowResolvedAddressSelected', module) + .addDecorator((story) => { + const wallet: YoroiWallet = walletMocks.wallet + + return ( + + {story()} + + ) + }) + .add('handle', () => ) + .add('cns', () => ) + .add('unstoppable', () => ) + +const Wrapper = ({ns}: {ns: Partial}) => { + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'apiKey', + }, + }, + }) + const resolverStorage = resolverStorageMaker() + const resolverManager = resolverManagerMaker(resolverStorage, resolverApi) + + return ( + + + + + + + + ) +} + +const handle: SendState = { + ...initialState, + targets: [ + { + entry: {address: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', amounts: {'': '1000000'}}, + receiver: { + as: 'domain', + resolve: '$stackchain', + selectedNameServer: Resolver.NameServer.Handle, + addressRecords: { + handle: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', + cns: 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + unstoppable: + 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + }, + }, + }, + ], +} +const cns: SendState = { + ...initialState, + targets: [ + { + entry: { + address: + 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + amounts: {'': '1000000'}, + }, + receiver: { + as: 'domain', + resolve: '$stackchain', + selectedNameServer: Resolver.NameServer.Cns, + addressRecords: { + handle: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', + cns: 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + unstoppable: + 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + }, + }, + }, + ], +} +const unstoppable: SendState = { + ...initialState, + targets: [ + { + entry: { + address: + 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + amounts: {'': '1000000'}, + }, + receiver: { + as: 'domain', + resolve: '$stackchain', + selectedNameServer: Resolver.NameServer.Unstoppable, + addressRecords: { + handle: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', + cns: 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + unstoppable: + 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + }, + }, + }, + ], +} diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.tsx new file mode 100644 index 0000000000..fac5a1e683 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/InputReceiver/ShowResolvedAddressSelected.tsx @@ -0,0 +1,64 @@ +import {nameServerName} from '@yoroi/resolver' +import React from 'react' +import {StyleSheet, Text, View} from 'react-native' + +import {Spacer} from '../../../../../components' +import {useSend} from '../../../common/SendContext' +import {useStrings} from '../../../common/strings' + +export const ShowResolvedAddressSelected = () => { + const strings = useStrings() + const {targets, selectedTargetIndex} = useSend() + const {selectedNameServer} = targets[selectedTargetIndex].receiver + const {address} = targets[selectedTargetIndex].entry + + const hide = address.length === 0 || selectedNameServer == null + + if (hide) return null + + const serverName = nameServerName[selectedNameServer] + const shortenAddress = shortenString(address) + const resolvedAddressInfo = `${strings.resolvedAddress}: ${shortenAddress}` + + return ( + + + + + + {serverName} + + + + {resolvedAddressInfo} + + + + ) +} + +const shortenString = (text: string) => { + if (text.length > 16) { + return text.substring(0, 8) + '...' + text.substring(text.length - 8) + } + return text +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + serverName: { + fontFamily: 'Rubik', + fontSize: 12, + fontWeight: '400', + color: '#4A5065', + }, + address: { + fontFamily: 'Rubik', + fontSize: 12, + fontWeight: '400', + color: '#8A92A3', + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx new file mode 100644 index 0000000000..3bac671433 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.stories.tsx @@ -0,0 +1,41 @@ +import {storiesOf} from '@storybook/react-native' +import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' + +import {QueryProvider} from '../../../../../../.storybook/decorators' +import {Boundary} from '../../../../../components' +import {SelectedWalletProvider} from '../../../../../SelectedWallet' +import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' +import {mocks as walletMocks} from '../../../../../yoroi-wallets/mocks/wallet' +import {SendProvider} from '../../../common/SendContext' +import {NotifySupportedNameServers} from './NotifySupportedNameServers' + +storiesOf('Send NotifySupportedNameServers', module).add('initial', () => ) + +const Initial = () => { + const wallet: YoroiWallet = walletMocks.wallet + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'apiKey', + }, + }, + }) + const resolverStorage = resolverStorageMaker() + const resolverManager = resolverManagerMaker(resolverStorage, resolverApi) + + return ( + + + + + + + + + + + + ) +} diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.tsx new file mode 100644 index 0000000000..4f5c177274 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/NotifySupportedNameServers/NotifySupportedNameServers.tsx @@ -0,0 +1,90 @@ +import {nameServerName, useResolverSetShowNotice, useResolverShowNotice} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import React from 'react' +import {StyleSheet, Text, View} from 'react-native' +import LinearGradient from 'react-native-linear-gradient' + +import {Icon, Spacer} from '../../../../../components' +import {PressableIcon} from '../../../../../components/PressableIcon/PressableIcon' +import {useStrings} from '../../../common/strings' + +export const NotifySupportedNameServers = () => { + const strings = useStrings() + const {showNotice, refetch} = useResolverShowNotice() + + const {setShowNotice} = useResolverSetShowNotice({ + onSuccess: () => refetch(), + }) + const handleOnClose = React.useCallback(() => { + setShowNotice(false) + }, [setShowNotice]) + + if (!showNotice) return null + + return ( + + + + {strings.resolverNoticeTitle} + + + + + + + + + + + + + + + + ) +} + +const NameServer = ({text}: {text: string}) => { + return ( + + + + · + + + + {text} + + ) +} + +const styles = StyleSheet.create({ + gradient: { + borderRadius: 8, + padding: 12, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + nameServerRoot: { + flexDirection: 'row', + alignItems: 'center', + lineHeight: 22, + color: '#000', + }, + nameServerText: { + fontWeight: '400', + fontFamily: 'Rubik-Regular', + fontSize: 14, + lineHeight: 22, + color: '#000', + }, + title: { + fontFamily: 'Rubik-Medium', + fontSize: 16, + fontWeight: '500', + color: '#000', + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx new file mode 100644 index 0000000000..d39cb60330 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.stories.tsx @@ -0,0 +1,104 @@ +import {storiesOf} from '@storybook/react-native' +import {resolverApiMaker, resolverManagerMaker, ResolverProvider, resolverStorageMaker} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' + +import {QueryProvider} from '../../../../../../.storybook/decorators' +import {Boundary} from '../../../../../components' +import {SelectedWalletProvider} from '../../../../../SelectedWallet' +import {YoroiWallet} from '../../../../../yoroi-wallets/cardano/types' +import {mocks as walletMocks} from '../../../../../yoroi-wallets/mocks/wallet' +import {initialState, SendProvider, SendState} from '../../../common/SendContext' +import {SelectNameServer} from './SelectNameServer' + +storiesOf('Send SelectNameServer', module) + .addDecorator((story) => { + const wallet: YoroiWallet = walletMocks.wallet + + return ( + + {story()} + + ) + }) + .add('unselected NS', () => ) + .add('selected NS', () => ) + +const UnselectedNS = () => { + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'apiKey', + }, + }, + }) + const resolverStorage = resolverStorageMaker() + const resolverManager = resolverManagerMaker(resolverStorage, resolverApi) + + return ( + + + + + + + + ) +} + +const SelectedNS = () => { + const resolverApi = resolverApiMaker({ + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'apiKey', + }, + }, + }) + const resolverStorage = resolverStorageMaker() + const resolverManager = resolverManagerMaker(resolverStorage, resolverApi) + + return ( + + + + + + + + ) +} + +const mockSelectedNameServer: SendState = { + ...initialState, + targets: [ + { + entry: {address: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', amounts: {'': '1000000'}}, + receiver: { + as: 'domain', + resolve: '$stackchain', + selectedNameServer: Resolver.NameServer.Cns, + addressRecords: { + handle: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', + cns: 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + }, + }, + }, + ], +} +const mockUnselectedNameServer: SendState = { + ...initialState, + targets: [ + { + entry: {address: '', amounts: {'': '1000000'}}, + receiver: { + as: 'domain', + resolve: '$stackchain', + selectedNameServer: undefined, + addressRecords: { + handle: 'addr1vxggvx6uq9mtf6e0tyda2mahg84w8azngpvkwr5808ey6qsy2ww7d', + cns: 'addr1qywgh46dqu7lq6mp5c6tzldpmzj6uwx335ydrpq8k7rru4q6yhkfqn5pc9f3z76e4cr64e5mf98aaeht6zwf8xl2nc9qr66sqg', + }, + }, + }, + ], +} diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.tsx new file mode 100644 index 0000000000..151ca16351 --- /dev/null +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/SelectNameServer/SelectNameServer.tsx @@ -0,0 +1,96 @@ +import {isNameServer, nameServerName} from '@yoroi/resolver' +import {Resolver} from '@yoroi/types' +import * as React from 'react' +import {Animated, StyleSheet, Text, View} from 'react-native' + +import {Icon} from '../../../../../components/Icon' +import {Spacer} from '../../../../../components/Spacer/Spacer' +import {ButtonGroup} from '../../../common/ButtonGroup/ButtonGroup' +import {useSend} from '../../../common/SendContext' +import {useStrings} from '../../../common/strings' + +export const SelectNameServer = () => { + const {targets, selectedTargetIndex, nameServerSelectedChanged} = useSend() + const receiver = targets[selectedTargetIndex].receiver + const {addressRecords} = receiver + const addressRecordsEntries = toAddressRecordsEntries(addressRecords) + const labels = addressRecordsEntries.map(([nameServer]) => nameServerName[nameServer]) + + const shouldShow = addressRecordsEntries.length > 1 + + const [animatedValue] = React.useState(new Animated.Value(0)) + const [waitAnimation, setWaitAnimation] = React.useState(false) + React.useEffect(() => { + animatedValue.stopAnimation() + if (shouldShow) { + setWaitAnimation(true) + Animated.timing(animatedValue, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }).start() + } else { + Animated.timing(animatedValue, { + toValue: 0, + duration: 1000, + useNativeDriver: true, + }).start(() => setWaitAnimation(false)) + } + }, [animatedValue, shouldShow]) + + const handleOnSelectNameServer = (index: number) => { + const [nameServer] = addressRecordsEntries[index] + nameServerSelectedChanged(nameServer) + } + + return ( + + {(waitAnimation || shouldShow) && ( + <> + + + + + + + + + )} + + ) +} + +export const ShowManyAddressWarning = () => { + const strings = useStrings() + + return ( + + + + + + {strings.manyNameServersWarning} + + ) +} + +const toAddressRecordsEntries = (addressRecords: Resolver.Receiver['addressRecords']) => + Object.entries(addressRecords ?? {}).reduce((acc, [key, value]) => { + if (isNameServer(key)) { + acc.push([key, value]) + } + return acc + }, [] as [Resolver.NameServer, string][]) + +const styles = StyleSheet.create({ + notice: { + backgroundColor: '#FDF7E2', + padding: 12, + }, + text: { + fontFamily: 'Rubik-Medium', + fontSize: 14, + fontWeight: '500', + lineHeight: 22, + }, +}) diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/ShowErrors.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/ShowErrors.tsx index dc3502a5f9..39758cbb32 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/ShowErrors.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/ShowErrors.tsx @@ -1,6 +1,6 @@ import React from 'react' -import {Banner, ClickableBanner} from '../../../../components' +import {Banner, ClickableBanner, Spacer} from '../../../../components' import {useSelectedWallet} from '../../../../SelectedWallet' import {useHasPendingTx, useSync} from '../../../../yoroi-wallets/hooks' import {useStrings} from '../../common/strings' @@ -13,10 +13,22 @@ export const ShowErrors = () => { const {isLoading, error, sync} = useSync(wallet) if (error != null && !isLoading) { - return sync()} text={strings.errorBannerNetworkError} /> + return ( + <> + sync()} text={strings.errorBannerNetworkError} /> + + + + ) } else if (hasPendingTx) { - return - } else { - return null + return ( + <> + + + + + ) } + + return null } diff --git a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.tsx b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.tsx index f948135939..9342482e1f 100644 --- a/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.tsx +++ b/apps/wallet-mobile/src/features/Send/useCases/StartMultiTokenTx/StartMultiTokenTxScreen.tsx @@ -8,11 +8,17 @@ import {useSelectedWallet} from '../../../../SelectedWallet' import {COLORS} from '../../../../theme' import {useHasPendingTx, useIsOnline} from '../../../../yoroi-wallets/hooks' import {Amounts} from '../../../../yoroi-wallets/utils' +import {memoMaxLenght} from '../../common/constants' +import {AddressErrorWrongNetwork} from '../../common/errors' import {useNavigateTo} from '../../common/navigation' import {useSend} from '../../common/SendContext' import {useStrings} from '../../common/strings' -import {InputMemo, maxMemoLength} from './InputMemo' -import {getAddressErrorMessage, ResolveAddress, useReceiver} from './InputReceiver/ResolveAddress' +import {useSendAddress} from '../../common/useSendAddress' +import {useSendReceiver} from '../../common/useSendReceiver' +import {InputMemo} from './InputMemo/InputMemo' +import {InputReceiver} from './InputReceiver/InputReceiver' +import {NotifySupportedNameServers} from './NotifySupportedNameServers/NotifySupportedNameServers' +import {SelectNameServer} from './SelectNameServer/SelectNameServer' import {ShowErrors} from './ShowErrors' export const StartMultiTokenTxScreen = () => { @@ -28,32 +34,36 @@ export const StartMultiTokenTxScreen = () => { const hasPendingTx = useHasPendingTx(wallet) const isOnline = useIsOnline(wallet) - const {targets, selectedTargetIndex, receiverChanged, memo, memoChanged, addressChanged} = useSend() - const {address, amounts} = targets[selectedTargetIndex].entry - const shouldOpenAddToken = Amounts.toArray(amounts).length === 0 + const {targets, selectedTargetIndex, memo, memoChanged, receiverResolveChanged} = useSend() + const {amounts} = targets[selectedTargetIndex].entry const receiver = targets[selectedTargetIndex].receiver - const {error, isLoading} = useReceiver( - {wallet, receiver}, - { - onSettled(address, error) { - if (error) { - addressChanged('') - } else { - addressChanged(address ?? '') - } - }, - }, - ) - const addressErrorMessage = error != null ? getAddressErrorMessage(error, strings) : '' - const isValid = isOnline && !hasPendingTx && _.isEmpty(error) && memo.length <= maxMemoLength && address.length > 0 + const shouldOpenAddToken = Amounts.toArray(amounts).length === 0 + + const {isResolvingAddressess, receiverError, isUnsupportedDomain} = useSendReceiver() + const {isValidatingAddress, addressError, addressValidated} = useSendAddress() - const onNext = () => { + const isLoading = isResolvingAddressess || isValidatingAddress + const {hasReceiverError, receiverErrorMessage} = useReceiverError({ + isUnsupportedDomain, + isLoading, + receiverError, + addressError, + }) + const isValidAddress = addressValidated && !hasReceiverError + + const hasMemoError = memo.length > memoMaxLenght + + const canGoNext = isOnline && !hasPendingTx && isValidAddress && !hasMemoError + + const handleOnNext = () => { if (shouldOpenAddToken) { navigateTo.addToken() } else { navigateTo.selectedTokens() } } + const handleOnChangeReceiver = (text: string) => receiverResolveChanged(text) + const handleOnChangeMemo = (text: string) => memoChanged(text) return ( @@ -61,26 +71,29 @@ export const StartMultiTokenTxScreen = () => { - + - + + - + @@ -92,11 +105,41 @@ export const StartMultiTokenTxScreen = () => { const Actions = ({style, ...props}: ViewProps) => +const useReceiverError = ({ + isUnsupportedDomain, + receiverError, + addressError, + isLoading, +}: { + isUnsupportedDomain: boolean + isLoading: boolean + receiverError: Error | null + addressError: Error | null +}) => { + const strings = useStrings() + + // NOTE: order matters + if (isLoading) return {hasReceiverError: false, receiverErrorMessage: ''} + if (isUnsupportedDomain) + return {hasReceiverError: true, receiverErrorMessage: strings.helperResolverErrorUnsupportedDomain} + if (receiverError != null) + return {hasReceiverError: true, receiverErrorMessage: strings.helperResolverErrorUnsupportedDomain} + if (addressError instanceof AddressErrorWrongNetwork) + return {hasReceiverError: true, receiverErrorMessage: strings.helperAddressErrorWrongNetwork} + if (addressError != null) return {hasReceiverError: true, receiverErrorMessage: strings.helperAddressErrorInvalid} + + return { + hasReceiverError: false, + receiverErrorMessage: '', + } +} + const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: COLORS.WHITE, paddingHorizontal: 16, + paddingTop: 16, }, flex: { flex: 1, diff --git a/apps/wallet-mobile/src/features/Settings/ChangePassword/ChangePasswordScreen.tsx b/apps/wallet-mobile/src/features/Settings/ChangePassword/ChangePasswordScreen.tsx index d9c0d7d5c1..10a7a54a45 100644 --- a/apps/wallet-mobile/src/features/Settings/ChangePassword/ChangePasswordScreen.tsx +++ b/apps/wallet-mobile/src/features/Settings/ChangePassword/ChangePasswordScreen.tsx @@ -63,7 +63,7 @@ export const ChangePasswordScreen = () => { value={newPassword} onChangeText={setNewPassword} errorText={newPasswordErrors.passwordIsWeak ? strings.passwordStrengthRequirement : undefined} - helperText={strings.passwordStrengthRequirement} + helper={strings.passwordStrengthRequirement} returnKeyType="next" onSubmitEditing={() => newPasswordConfirmationRef.current?.focus()} right={!newPasswordErrors.passwordIsWeak ? : undefined} diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.tsx b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.tsx index 08ff6809c5..d3f8b6308f 100644 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.tsx +++ b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/ConfirmTxScreen.tsx @@ -67,7 +67,7 @@ export const ConfirmTxScreen = () => { {targets.map((target, index) => ( - + ))} @@ -84,15 +84,13 @@ export const ConfirmTxScreen = () => { {!wallet.isEasyConfirmationEnabled && !wallet.isHW && ( - <> - - + )} diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.tsx b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.tsx index a47f6f1819..4b518e7916 100644 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.tsx +++ b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.tsx @@ -1,48 +1,24 @@ import * as React from 'react' -import {defineMessages, useIntl} from 'react-intl' import {View} from 'react-native' +import {Spacer} from '../../../../../components/Spacer' import {Text} from '../../../../../components/Text' -import {txLabels} from '../../../../../i18n/global-messages' +import {YoroiTarget} from '../../../../../yoroi-wallets/types' +import {useStrings} from '../../../../Send/common/strings' type Props = { - receiver: string - address: string + target: YoroiTarget } -export const ReceiverInfo = ({receiver, address}: Props) => { +export const ReceiverInfo = ({target}: Props) => { const strings = useStrings() - const isResolved = receiver !== address - return ( - {strings.receiver} - - {receiver} + {strings.receiver}: - {isResolved && ( - - {strings.resolvesTo} + - {address} - - )} + {target.entry.address} ) } - -const messages = defineMessages({ - resolvesTo: { - id: 'components.send.sendscreen.resolvesTo', - defaultMessage: '!!!Resolves to', - }, -}) - -const useStrings = () => { - const intl = useIntl() - - return { - receiver: intl.formatMessage(txLabels.receiver), - resolvesTo: intl.formatMessage(messages.resolvesTo), - } -} diff --git a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx index 46b0b7747f..90f15a7a42 100644 --- a/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx +++ b/apps/wallet-mobile/src/features/Settings/ManageCollateral/ManageCollateralScreen.tsx @@ -41,7 +41,13 @@ export const ManageCollateralScreen = () => { const balances = useBalances(wallet) const lockedAmount = useLockedAmount({wallet}) - const {resetForm, addressChanged, amountChanged, tokenSelectedChanged, yoroiUnsignedTxChanged} = useSend() + const { + reset: resetSendState, + receiverResolveChanged, + amountChanged, + tokenSelectedChanged, + yoroiUnsignedTxChanged, + } = useSend() const {refetch: createUnsignedTx, isFetching: isLoadingTx} = useSendTx( { wallet, @@ -73,9 +79,9 @@ export const ManageCollateralScreen = () => { tokenId: wallet.primaryTokenInfo.id, } - // dispatch only for confirmation screen - resetForm() - addressChanged(address) + // populate for confirmation screen + resetSendState() + receiverResolveChanged(address) tokenSelectedChanged(amount.tokenId) amountChanged(amount.quantity) diff --git a/apps/wallet-mobile/src/i18n/locales/en-US.json b/apps/wallet-mobile/src/i18n/locales/en-US.json index cdd25dccde..8fd1a8f11c 100644 --- a/apps/wallet-mobile/src/i18n/locales/en-US.json +++ b/apps/wallet-mobile/src/i18n/locales/en-US.json @@ -1,28 +1,4 @@ { - "api.error.title": "Oops! API error", - "api.error.badRequest": "Server could not understand the request", - "api.error.unauthorized": "You are not authorized to access this resource", - "api.error.forbidden": "You are not allowed to access this resource", - "api.error.notFound": "The requested resource could not be found", - "api.error.conflict": "The request could not be completed due to a conflict", - "api.error.gone": "The requested resource is no longer available", - "api.error.tooEarly": "The requested resource is not yet available, try again later", - "api.error.tooManyRequests": "Too many requests, wait a bit and try again", - "api.error.serverSide": "The server encountered an unexpected condition", - "api.error.unknown": "An error that is unknown for the app happened, please try again", - "api.error.network": "Something went wrong with the network", - "api.error.invalidState": "Invalid state", - "api.error.responseMalformed": "The server response is malformed we can't interpret it", - "menu": "Menu", - "menu.allWallets": "All Wallets", - "menu.catalystVoting": "Catalyst Voting", - "menu.settings": "Settings", - "menu.appSettings": "App Settings", - "menu.releases": "Releases", - "menu.supportTitle": "Any questions?", - "menu.supportLink": "Ask our support team", - "menu.knowledgeBase": "Knowledge base", - "menu.governanceCentre": "Governance centre", "analytics.accept": "Accept", "analytics.anonymous": "Anonymous analytics data", "analytics.description": "Share user insights to help us fine tune Yoroi to better serve your needs.", @@ -31,39 +7,81 @@ "analytics.noip": "We are not recording IP addresses", "analytics.nosell": "We do not sell data", "analytics.optout": "You can always opt-out via Settings", + "analytics.privacyNotice": "Privacy Notice", + "analytics.privacyPolicy": "Privacy Policy", "analytics.private": "We cannot access private keys", + "analytics.selectLanguage": "Select Language", "analytics.skip": "Skip", "analytics.toggle": "Allow Yoroi analytics", - "analytics.selectLanguage": "Select Language", - "analytics.tosIAgreeWith": "I agree with", "analytics.tosAgreement": "Terms Of Service Agreement", - "claim.askConfirmation.title": "Confirm claim", - "claim.showSuccess.title": "Claim success", - "claim.domain": "Domain", - "claim.code": "Code", - "claim.addressSharingWarning": "You will be sharing with the domain listed here your address", - "claim.accepted.title": "Claim accepted 👍", + "analytics.tosAnd": "and", + "analytics.tosIAgreeWith": "I agree with", + "api.error.badRequest": "Server could not understand the request", + "api.error.conflict": "The request could not be completed due to a conflict", + "api.error.forbidden": "You are not allowed to access this resource", + "api.error.gone": "The requested resource is no longer available", + "api.error.invalidState": "Invalid state", + "api.error.network": "Something went wrong with the network", + "api.error.notFound": "The requested resource could not be found", + "api.error.responseMalformed": "The server response is malformed we can't interpret it", + "api.error.serverSide": "The server encountered an unexpected condition", + "api.error.title": "Oops! API error", + "api.error.tooEarly": "The requested resource is not yet available, try again later", + "api.error.tooManyRequests": "Too many requests, wait a bit and try again", + "api.error.unauthorized": "You are not authorized to access this resource", + "api.error.unknown": "An error that is unknown for the app happened, please try again", "claim.accepted.message": "Claim has been accepted, you will receive your asset(s) soon, please scan the code again to check the status", - "claim.processing.title": "Processing claim ⏳️", - "claim.processing.message": "Claim is being processed, you will receive your asset(s) soon, please scan the code again to check the status", - "claim.done.title": "Claim completed ✔️", - "claim.done.message": "Claim was completed, you should have received your asset(s), you can verify the transaction on the chain explorer", - "claim.apiError.title": "The claim failed", - "claim.apiError.invalidRequest": "The server rejecte the request, check the address provided by the wallet is on the wrong network for this claim, and if the code is still avaible", - "claim.apiError.notFound": "The claim could not be found", + "claim.accepted.title": "Claim accepted 👍", + "claim.addressSharingWarning": "You will be sharing with the domain listed here your address", "claim.apiError.alreadyClaimed": "This claim has already been done", "claim.apiError.expired": "This claim has ended, it is no longer availble", - "claim.apiError.tooEarly": "This claim hasn't started yet, please try again later", + "claim.apiError.invalidRequest": "The server rejecte the request, check the address provided by the wallet is on the wrong network for this claim, and if the code is still avaible", + "claim.apiError.notFound": "The claim could not be found", "claim.apiError.rateLimited": "Too many claims happening, wait a bit and try again", + "claim.apiError.title": "The claim failed", + "claim.apiError.tooEarly": "This claim hasn't started yet, please try again later", + "claim.askConfirmation.title": "Confirm claim", + "claim.code": "Code", + "claim.domain": "Domain", + "claim.done.message": "Claim was completed, you should have received your asset(s), you can verify the transaction on the chain explorer", + "claim.done.title": "Claim completed ✔️", + "claim.processing.message": "Claim is being processed, you will receive your asset(s) soon, please scan the code again to check the status", + "claim.processing.title": "Processing claim ⏳️", + "claim.showSuccess.title": "Claim success", + "components.catalyst.banner.name": "Catalyst Voting Registration", + "components.catalyst.catalystbackupcheckmodal.consequencesCheckbox": "I understand that if I did not save my Catalyst PIN and QR code (or secret code) I will not be able to register and vote for Catalyst proposals.", + "components.catalyst.catalystbackupcheckmodal.pinCheckbox": "I have written down my Catalyst PIN which I obtained in previous steps.", + "components.catalyst.catalystbackupcheckmodal.qrCodeCheckbox": "I have taken a screenshot of my QR code and saved my Catalyst secret code as a fallback.", + "components.catalyst.insufficientBalance": "Participating requires at least {requiredBalance}, but you only have {currentBalance}. Unwithdrawn rewards are not included in this amount", + "components.catalyst.step1.stakingKeyNotRegistered": "Catalyst voting rewards are sent to delegation accounts and your wallet does not seem to have a registered delegation certificate. If you want to receive voting rewards, you need to delegate your funds first.", + "components.catalyst.step1.subTitle": "Before you begin, make sure to download the Catalyst Voting App.", + "components.catalyst.step1.tip": "Tip: Make sure you know how to take a screenshot with your device, so that you can backup your catalyst QR code.", + "components.catalyst.step2.description": "Please write down this PIN as you will need it every time you want to access the Catalyst Voting app", + "components.catalyst.step2.subTitle": "Write Down PIN", + "components.catalyst.step3.description": "Please enter the PIN as you will need it every time you want to access the Catalyst Voting app", + "components.catalyst.step3.subTitle": "Enter PIN", + "components.catalyst.step4.bioAuthInstructions": "Please authenticate so that Yoroi can generate the required certificate for voting", + "components.catalyst.step4.description": "Enter your spending password to be able to generate the required certificate for voting", + "components.catalyst.step4.subTitle": "Enter Spending Password", + "components.catalyst.step5.bioAuthDescription": "Please confirm your voting registration. You will be asked to authenticate once again to sign and submit the certificate generated in the previous step.", + "components.catalyst.step5.description": "Enter your spending password to confirm your voting registration and submit the certificate generated in previous step to the blockchain.", + "components.catalyst.step5.subTitle": "Confirm Registration", + "components.catalyst.step6.description": "Please take a screenshot of this QR code.", + "components.catalyst.step6.description2": "We strongly recommend you to save your Catalyst secret code in plain text too, so that you can re-create your QR code if necessary.", + "components.catalyst.step6.description3": "Then, send the QR code to an external device, as you will need to scan it with your phone using the Catalyst mobile app.", + "components.catalyst.step6.note": "Keep it — you won’t be able to access this code after tapping on Complete.", + "components.catalyst.step6.secretCode": "Secret Code", + "components.catalyst.step6.subTitle": "Backup Catalyst Code", + "components.catalyst.title": "Register to vote", "components.common.errormodal.hideError": "Hide error message", "components.common.errormodal.showError": "Show error message", "components.common.fingerprintscreenbase.welcomeMessage": "Welcome Back", + "components.common.languagepicker.acknowledgement": "**The selected language translation is fully provided by the community**. EMURGO is grateful to all those who have contributed", "components.common.languagepicker.brazilian": "Português brasileiro", "components.common.languagepicker.chinese": "简体中文", "components.common.languagepicker.continueButton": "Choose language", "components.common.languagepicker.contributors": "_", "components.common.languagepicker.czech": "Čeština", - "components.common.languagepicker.slovak": "Slovenčina", "components.common.languagepicker.dutch": "Nederlands", "components.common.languagepicker.english": "English", "components.common.languagepicker.french": "Français", @@ -73,11 +91,12 @@ "components.common.languagepicker.italian": "Italiano", "components.common.languagepicker.japanese": "日本語", "components.common.languagepicker.korean": "한국어", - "components.common.languagepicker.acknowledgement": "**The selected language translation is fully provided by the community**. EMURGO is grateful to all those who have contributed", "components.common.languagepicker.russian": "Русский", + "components.common.languagepicker.slovak": "Slovenčina", "components.common.languagepicker.spanish": "Español", "components.common.navigation.dashboardButton": "Dashboard", "components.common.navigation.delegateButton": "Delegate", + "components.common.navigation.nftGallery": "NFT Gallery", "components.common.navigation.transactionsButton": "Transactions", "components.delegation.delegationnavigationbuttons.stakingCenterButton": "Go to Staking Center", "components.delegation.withdrawaldialog.deregisterButton": "Deregister", @@ -96,10 +115,10 @@ "components.delegationsummary.delegatedStakepoolInfo.warning": "If you just delegated to a new stake pool, it may take a couple of minutes for the network to process your request.", "components.delegationsummary.epochProgress.endsIn": "Ends in", "components.delegationsummary.epochProgress.title": "Epoch progress", - "components.delegationsummary.failedwalletupgrademodal.title": "Heads up!", "components.delegationsummary.failedwalletupgrademodal.explanation1": "Some users experienced problems while upgrading their wallets in the Shelley testnet.", "components.delegationsummary.failedwalletupgrademodal.explanation2": "If you observed an unexpected zero balance after having upgraded your wallet, we recommend you to restore your wallet once again. We apologize for any inconvenience this may have caused.", "components.delegationsummary.failedwalletupgrademodal.okButton": "OK", + "components.delegationsummary.failedwalletupgrademodal.title": "Heads up!", "components.delegationsummary.notDelegatedInfo.firstLine": "You have not delegated your ADA yet.", "components.delegationsummary.notDelegatedInfo.secondLine": "Go to the Staking Center to choose which stake pool you want to delegate to. Note that you may delegate only to one stake pool.", "components.delegationsummary.upcomingReward.followingLabel": "Following reward", @@ -112,13 +131,59 @@ "components.delegationsummary.warningbanner.message": "The last ITN rewards were distributed on epoch 190. Rewards can be claimed on mainnet once Shelley is released on mainnet.", "components.delegationsummary.warningbanner.message2": "Your ITN wallet rewards and balance may not be correctly displayed, but this information is still securely stored in the ITN blockchain.", "components.delegationsummary.warningbanner.title": "Note:", + "components.firstrun.custompinscreen.pinInputConfirmationSubTitle": "Repeat a new PIN to quickly access your wallet", + "components.governance.abstaining": "Abstaining", + "components.governance.actionAbstainDescription": "You are choosing not to cast a vote on all proposals now and in the future.", + "components.governance.actionAbstainTitle": "Abstain", + "components.governance.actionDelegateToADRepDescription": "You are designating someone else to cast vote on your behalf for all proposals now and in the future.", + "components.governance.actionDelegateToADRepTitle": "Delegate to a DRep", + "components.governance.actionNoConfidenceDescription": "You are expressing a lack of trust for all proposals now and in the future.", + "components.governance.actionNoConfidenceTitle": "No confidence", + "components.governance.actionYouHaveSelected": "You have selected {action} as your governance status. You can change it at any time by clicking in the card below.", + "components.governance.actionYouHaveSelectedTxPending": "You have selected {action} as your governance status.", + "components.governance.changeDRep": "Change DRep", + "components.governance.confirm": "Confirm", + "components.governance.confirmTxTitle": "Confirm transaction", + "components.governance.delegateVotingToDRep": "Delegate voting to\n{drepID}", + "components.governance.delegatingToADRep": "Delegating to a DRep", + "components.governance.drepID": "Drep ID", + "components.governance.drepKey": "DRep Key", + "components.governance.enterADrepIDAndPassword": "Enter a Drep ID and password to sign this transaction", + "components.governance.enterDRepID": "Choose your Drep", + "components.governance.enterDrepIDInfo": "Identify your preferred DRep and enter their ID below to delegate your vote", + "components.governance.enterPassword": "Enter password to sign this transaction", + "components.governance.findDRepHere": "Find a DRep here", + "components.governance.goToGovernance": "Go to Governance", + "components.governance.goToStaking": "Go to staking", + "components.governance.goToWallet": "Go to wallet", + "components.governance.governanceCentreTitle": "Governance centre", + "components.governance.hardwareWalletSupportComingSoon": "Hardware wallet support coming soon", + "components.governance.learnMoreAboutGovernance": "Learn more About Governance", + "components.governance.operations": "Operations", + "components.governance.participationBenefits": "Participating in the Cardano Governance gives you the opportunity to participate in the voting as well as withdraw your staking rewards", + "components.governance.readyToCollectRewards": "You are now ready to collect your rewards", + "components.governance.registerStakingKey": "Register staking key", + "components.governance.reviewActions": "Review the selections carefully to assign yourself a Governance Status", + "components.governance.selectAbstain": "Select abstain", + "components.governance.selectNoConfidence": "Select no confidence", + "components.governance.thankYouForParticipating": "Thank you for participating in the Cardano Governance", + "components.governance.thisTransactionCanTakeAWhile": "This transaction can take a while!", + "components.governance.total": "Total", + "components.governance.transactionDetails": "Transaction details", + "components.governance.transactionFailed": "Transaction failed", + "components.governance.transactionFailedDescription": "Your transaction has not been processed properly due to technical issues", + "components.governance.tryAgain": "Try again", + "components.governance.txFees": "Transaction fee", + "components.governance.withdrawWarningButton": "Participate on governance", + "components.governance.withdrawWarningDescription": "To withdraw your rewards, you need to participate in the Cardano Governance. Your rewards will continue to accumulate, but you are only able to withdraw it once you join the Governance process.", + "components.governance.withdrawWarningTitle": "Withdraw warning", + "components.governance.workingOnHardwareWalletSupport": "We are currently working on integrating hardware wallet support for Governance", "components.initialization.acepttermsofservicescreen.aggreeClause": "I agree with the Terms of Service", "components.initialization.acepttermsofservicescreen.continueButton": "Accept", "components.initialization.acepttermsofservicescreen.savingConsentModalTitle": "Initializing", "components.initialization.acepttermsofservicescreen.title": "Terms of Service Agreement", "components.initialization.custompinscreen.pinConfirmationTitle": "Repeat PIN", "components.initialization.custompinscreen.pinInputSubtitle": "Choose a new PIN to quickly access your wallet", - "components.firstrun.custompinscreen.pinInputConfirmationSubTitle": "Repeat a new PIN to quickly access your wallet", "components.initialization.custompinscreen.pinInputTitle": "Enter PIN", "components.initialization.custompinscreen.title": "Set PIN", "components.initialization.languagepicker.title": "Select language", @@ -134,129 +199,6 @@ "components.login.appstartscreen.loginButton": "Login", "components.login.custompinlogin.title": "Enter PIN", "components.ma.assetSelector.placeHolder": "Select an asset", - "nft.detail.title": "NFT Details", - "nft.detail.overview": "Overview", - "nft.detail.metadata": "Metadata", - "nft.detail.nftName": "NFT Name", - "nft.detail.createdAt": "Created", - "nft.detail.description": "Description", - "nft.detail.author": "Author", - "nft.detail.fingerprint": "Fingerprint", - "nft.detail.policyId": "Policy id", - "nft.detail.detailsLinks": "Details on", - "nft.detail.copyMetadata": "Copy metadata", - "nft.gallery.noNftsFound": "No NFTs found", - "nft.gallery.noNftsInWallet": "No NFTs added to your wallet yet", - "nft.gallery.nftCount": "NFT count", - "nft.gallery.errorTitle": "Oops!", - "nft.gallery.errorDescription": "Something went wrong.", - "nft.gallery.reloadApp": "Try to restart the app.", - "nft.navigation.title": "NFT Gallery", - "nft.navigation.search": "Search NFT", - "scan.title": "Scan the QR code", - "scan.cameraPermissionDenied.help": "It looks the app doesn't have access to your device camera, if you are no longer asked to grant camera permission, please go to your device settings, search for Yoroi and allow access to the camera", - "scan.cameraPermissionDenied.title": "The app needs access to your camera", - "scan.errorUnknown.help": "An unknown error occurred while scanning the code, please try again", - "scan.errorUnknown.title": "Unknown error", - "scan.errorUnknownContent.help": "The content of the code was not recognized by the app", - "scan.errorUnknownContent.title": "Content is unknown'", - "scan.linksErrorExtraParamsDenied.help": "The link contains extra parameter(s) which is denied", - "scan.linksErrorExtraParamsDenied.title": "Extra parameter denied", - "scan.linksErrorForbiddenParamsProvided.help": "The link contains forbidden parameter(s)", - "scan.linksErrorForbiddenParamsProvided.title": "Forbidden parameter(s) provided", - "scan.linksErrorRequiredParamsMissing.help": "The link does not contain required parameter(s)", - "scan.linksErrorRequiredParamsMissing.title": "Missing required parameter(s)", - "scan.linksErrorParamsValidationFailed.help": "The link contains parameter(s) that failed the validation'", - "scan.linksErrorParamsValidationFailed.title": "Parameter validation failed", - "scan.linksErrorUnsupportedAuthority.help": "The link authority is not yet supported by the app'", - "scan.linksErrorUnsupportedAuthority.title": "Unsupported authority", - "scan.linksErrorUnsupportedVersion.help": "The link authority version is not yet supported by the app'", - "scan.linksErrorUnsupportedVersion.title": "Unsupported version", - "scan.linksErrorSchemeNotImplemented.help": "The link scheme of the code is not supported by the app", - "scan.linksErrorSchemeNotImplemented.title": "Scheme not implemented", - "swap.swapScreen.swapTitle": "Swap", - "swap.swapScreen.batcherFee": "Batcher Fee", - "swap.swapScreen.dex": "dex", - "swap.swapScreen.tokenSwapTab": "Asset swap", - "swap.swapScreen.ordersSwapTab": "Orders", - "swap.swapScreen.openOrders": "Open orders", - "swap.swapScreen.completedOrders": "Completed orders", - "swap.swapScreen.marketButton": "Market", - "swap.swapScreen.limitButton": "Limit", - "swap.swapScreen.swapFrom": "Swap from", - "swap.swapScreen.swapTo": "Swap to", - "swap.swapScreen.currentBalance": "Current balance", - "swap.swapScreen.notEnoughBalance": "Not enough balance", - "swap.swapScreen.notEnoughFeeBalance": "Not enough balance, please consider the fees", - "swap.swapScreen.notEnoughSupply": "Not enough supply in the pool", - "swap.swapScreen.noPool": "This pair is not available in any liquidity pool", - "swap.swapScreen.balance": "Balance", - "swap.swapScreen.selectToken": "Select token", - "swap.swapScreen.marketPrice": "Market price", - "swap.swapScreen.limitPrice": "Limit price", - "swap.swapScreen.goToOrders": "Go to orders", - "swap.swapScreen.slippageTolerance": "Slippage tolerance", - "swap.swapScreen.slippageToleranceError": "Slippage must be a number between 0 and 75 and have up to 1 decimal", - "swap.swapScreen.slippageToleranceInfo": "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", - "swap.swapScreen.selectPool": "Select DEX", - "swap.swapScreen.seeOnExplorer": "see on explorer", - "swap.swapScreen.eachVerifiedToken": "Each verified tokens gets", - "swap.swapScreen.verifiedBadge": "verified badge", - "swap.swapScreen.verifiedBy": "Verified by {pool}", - "swap.swapScreen.transactionSigned": "Transaction submitted", - "swap.swapScreen.transactionDisplay": "In a few minutes, your transactions will be displayed both in the list of transaction and Open swap orders", - "swap.swapScreen.poolVerificationInfo": "Cardano projects that list their own tokens can apply for an additional {pool} verification. This verification is a manual validation that {pool} team performs with the help of Cardano Foundation.", - "swap.swapScreen.poolFee": "DEX Fee", - "swap.swapScreen.assetsIn": "This asset is in my portfolio", - "swap.swapScreen.noAssetsFound": "No assets found for this pair", - "swap.swapScreen.noAssetsFoundFor": "No assets found for \"{search}\"", - "swap.swapScreen.slippageInfo": "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.", - "swap.swapScreen.autoPool": "(auto)", - "swap.swapScreen.changePool": "change dex", - "swap.swapScreen.swapMinAda": "Min-ADA is the minimum ADA amount required to be contained when holding or sending Cardano native assets.", - "swap.swapScreen.swapMinAdaTitle": "Min ADA", - "swap.swapScreen.swapFees": "Swap fees include the following:\n • DEX Fee\n • Frontend Fee", - "swap.swapScreen.swapFeesTitle": "Fees", - "swap.swapScreen.swapLiqProvFee": "Liq. prov. fee", - "swap.swapScreen.swapLiquidityFee": "Liquidity provider fee", - "swap.swapScreen.swapLiquidityFeeInfo": "Liquidity provider fee is a fixed {fee}% operational fee from the whole transaction volume, that is taken to support DEX “liquidity” allowing traders to buy and sell assets on the decentralized Cardano network.", - "swap.swapScreen.swapMinReceived": "Minimum amount of assets you can get because of the slippage tolerance.", - "swap.swapScreen.swapMinReceivedTitle": "Min Received", - "swap.swapScreen.enterSlippage": "Enter a value from 0% to 75%. You can also enter up to 1 decimal", - "swap.swapScreen.poolVerification": "{pool} verification", - "swap.swapScreen.volume": "Volume, 24h", - "swap.swapScreen.tvl": "TVL", - "swap.listOrders.completed": "completed orders", - "swap.listOrders.open": "open orders", - "swap.listOrders.sheet.title": "Confirm order cancellation", - "swap.listOrders.card.buttonText": "Cancel order", - "swap.listOrders.sheet.contentTitle": "Are you sure you want to cancel this order?", - "swap.listOrders.sheet.link": "Learn more about swap orders in Yoroi", - "swap.listOrders.sheet.assetPrice": "Asset price", - "swap.listOrders.sheet.assetAmount": "Asset amount", - "swap.listOrders.sheet.totalReturned": "Total returned", - "swap.listOrders.sheet.cancellationFee": "Cancellation Fee", - "swap.listOrders.sheet.confirm": "Confirm", - "swap.listOrders.sheet.back": "Back", - "swap.listOrders.total": "Total", - "swap.listOrders.liquidityPool": "Liquidity Pool", - "swap.listOrders.timeCreated": "Time created", - "swap.listOrders.timeCompleted": "Time completed", - "swap.listOrders.txId": "Transaction ID", - "swap.listOrders.emptyOpenOrders": "No orders available yet", - "swap.listOrders.emptyOpenOrdersSub": "Start doing the swap operations to your open orders here", - "swap.listOrders.emptyCompletedOrders": "No orders completed yet", - "swap.swapScreen.limitPriceWarningTitle": "Limit price", - "swap.swapScreen.limitPriceWarningDescription": "Are you sure you want to proceed this order with the limit price that is 10% or more higher than the market price?", - "swap.swapScreen.limitPriceWarningYourPrice": "Your limit price", - "swap.swapScreen.limitPriceWarningMarketPrice": "Market price", - "swap.swapScreen.limitPriceWarningBack": "Back", - "swap.swapScreen.limitPriceWarningConfirm": "Swap", - "swap.slippage.slippageWarningTitle": "Slippage Warning", - "swap.slippage.slippageWarningText": "Are you sure you want to proceed this order with the current slippage tolerance? It could result in receiving no assets.", - "swap.slippage.yourSlippage": "Your slippage tolerance", - "swap.slippage.changeAmount": "Increase the amount to proceed or change slippage tolerance to 0%", - "components.common.navigation.nftGallery": "NFT Gallery", "components.receive.addressmodal.BIP32path": "Derivation path", "components.receive.addressmodal.copiedLabel": "Copied", "components.receive.addressmodal.copyLabel": "Copy address", @@ -275,62 +217,63 @@ "components.receive.receivescreen.unusedAddresses": "Unused addresses", "components.receive.receivescreen.usedAddresses": "Used addresses", "components.receive.receivescreen.verifyAddress": "Verify address", + "components.send.addressreaderqr.text": "Scan recipients QR code to add a wallet address", + "components.send.addressreaderqr.title": "Scan QR code address", "components.send.addToken": "Add asset", - "components.send.selectasset.title": "Select Asset", + "components.send.amountfield.label": "Amount", + "components.send.assetselectorscreen.found": "found", + "components.send.assetselectorscreen.noAssets": "No assets found", + "components.send.assetselectorscreen.noAssetsAddedYet": "No {fungible} added yet", "components.send.assetselectorscreen.searchlabel": "Search by name or subject", "components.send.assetselectorscreen.sendallassets": "SELECT ALL ASSETS", "components.send.assetselectorscreen.unknownAsset": "Unknown Asset", - "components.send.assetselectorscreen.noAssets": "No assets found", - "components.send.assetselectorscreen.found": "found", "components.send.assetselectorscreen.youHave": "You have", - "components.send.assetselectorscreen.noAssetsAddedYet": "No {fungible} added yet", - "components.send.addressreaderqr.title": "Scan QR code address", - "components.send.addressreaderqr.text": "Scan recipients QR code to add a wallet address", - "components.send.amountfield.label": "Amount", - "components.send.editamountscreen.title": "Asset amount", - "components.send.memofield.label": "Memo", - "components.send.memofield.message": "(Optional) Memo is stored locally", - "components.send.memofield.error": "Memo is too long", - "components.send.biometricauthscreen.DECRYPTION_FAILED": "Biometrics login failed. Please use an alternate login method.", - "components.send.biometricauthscreen.NOT_RECOGNIZED": "Biometrics were not recognized. Try again", - "components.send.biometricauthscreen.SENSOR_LOCKOUT": "Too many failed attempts. The sensor is now disabled", - "components.send.biometricauthscreen.SENSOR_LOCKOUT_PERMANENT": "Your biometrics sensor has been permanently locked. Use an alternate login method.", - "components.send.biometricauthscreen.UNKNOWN_ERROR": "Something went wrong, try again later. Check your app settings.", "components.send.biometricauthscreen.authorizeOperation": "Authorize operation", "components.send.biometricauthscreen.cancelButton": "Cancel", + "components.send.biometricauthscreen.DECRYPTION_FAILED": "Biometrics login failed. Please use an alternate login method.", "components.send.biometricauthscreen.headings1": "Authorize with your", "components.send.biometricauthscreen.headings2": "biometrics", + "components.send.biometricauthscreen.NOT_RECOGNIZED": "Biometrics were not recognized. Try again", + "components.send.biometricauthscreen.SENSOR_LOCKOUT_PERMANENT": "Your biometrics sensor has been permanently locked. Use an alternate login method.", + "components.send.biometricauthscreen.SENSOR_LOCKOUT": "Too many failed attempts. The sensor is now disabled", + "components.send.biometricauthscreen.UNKNOWN_ERROR": "Something went wrong, try again later. Check your app settings.", "components.send.biometricauthscreen.useFallbackButton": "Use other login method", "components.send.confirmscreen.amount": "Amount", + "components.send.confirmscreen.assignCollateral": "Assign collateral", "components.send.confirmscreen.balanceAfterTx": "Balance after transaction", "components.send.confirmscreen.beforeConfirm": "Before tapping on confirm, please follow these instructions:", - "components.send.confirmscreen.confirmButton": "Confirm", - "components.send.confirmscreen.assignCollateral": "Assign collateral", "components.send.confirmscreen.collateralNotFound": "Collateral not found", - "components.send.confirmscreen.noActiveCollateral": "You don't have an active collateral utxo", + "components.send.confirmscreen.confirmButton": "Confirm", "components.send.confirmscreen.fees": "Fees", + "components.send.confirmscreen.noActiveCollateral": "You don't have an active collateral utxo", "components.send.confirmscreen.password": "Spending password", - "components.send.confirmscreen.receiver": "Receiver", + "components.send.confirmscreen.receiver": "Receiver address, ADA Handle or domains", "components.send.confirmscreen.sendingModalTitle": "Submitting transaction", "components.send.confirmscreen.title": "Send", + "components.send.editamountscreen.title": "Asset amount", "components.send.listamountstosendscreen.title": "Assets added", + "components.send.memofield.error": "Memo is too long", + "components.send.memofield.label": "Memo", + "components.send.memofield.message": "(Optional) Memo is stored locally", + "components.send.selectasset.title": "Select Asset", "components.send.sendscreen.addressInputErrorInvalidAddress": "Please enter a valid address", + "components.send.sendscreen.addressInputErrorInvalidDomain": "Please enter a valid domain", "components.send.sendscreen.addressInputLabel": "Address", "components.send.sendscreen.amountInput.error.assetOverflow": "Maximum value of a token inside a UTXO exceeded (overflow).", + "components.send.sendscreen.amountInput.error.insufficientBalance": "Not enough funds to make this transaction", "components.send.sendscreen.amountInput.error.INVALID_AMOUNT": "Please enter a valid amount", "components.send.sendscreen.amountInput.error.LT_MIN_UTXO": "Cannot send less than {minUtxo} {ticker}", "components.send.sendscreen.amountInput.error.NEGATIVE": "Amount must be positive", "components.send.sendscreen.amountInput.error.TOO_LARGE": "Amount too large", "components.send.sendscreen.amountInput.error.TOO_LOW": "Amount is too low", "components.send.sendscreen.amountInput.error.TOO_MANY_DECIMAL_PLACES": "Please enter a valid amount", - "components.send.sendscreen.amountInput.error.insufficientBalance": "Not enough funds to make this transaction", "components.send.sendscreen.availableFundsBannerIsFetching": "Checking balance...", "components.send.sendscreen.availableFundsBannerNotAvailable": "-", "components.send.sendscreen.balanceAfterLabel": "Balance after", "components.send.sendscreen.balanceAfterNotAvailable": "-", "components.send.sendscreen.checkboxLabel": "Send full balance", - "components.send.sendscreen.checkboxSendAllAssets": "Send all assets (including all tokens)", "components.send.sendscreen.checkboxSendAll": "Send all {assetId}", + "components.send.sendscreen.checkboxSendAllAssets": "Send all assets (including all tokens)", "components.send.sendscreen.continueButton": "Continue", "components.send.sendscreen.domainNotRegisteredError": "Domain is not registered", "components.send.sendscreen.domainRecordNotFoundError": "No Cardano record found for this domain", @@ -338,52 +281,55 @@ "components.send.sendscreen.errorBannerMaxTokenLimit": "is the maximum number allowed to send in one transaction", "components.send.sendscreen.errorBannerNetworkError": "We encountered a problem while fetching your current balance. Click to retry.", "components.send.sendscreen.errorBannerPendingOutgoingTransaction": "You cannot send a new transaction while an existing one is still pending", + "components.send.sendscreen.failedTxButton": "Try again", + "components.send.sendscreen.failedTxText": "Your transaction has not been processed properly due to technical issues", + "components.send.sendscreen.failedTxTitle": "Transaction failed", "components.send.sendscreen.feeLabel": "Fee", "components.send.sendscreen.feeNotAvailable": "-", - "components.send.sendscreen.resolvesTo": "Resolves to", + "components.send.sendscreen.receiver": "Receiver", + "components.send.sendscreen.resolvedAddress": "Resolved address", + "components.send.sendscreen.resolverNoticeTitle": "Yoroi Supports", "components.send.sendscreen.searchTokens": "Search assets", - "components.send.sendscreen.sendAllWarningText": "You have selected the send all option. Please confirm that you understand how this feature works.", - "components.send.sendscreen.sendAllWarningTitle": "Do you really want to send all?", "components.send.sendscreen.sendAllWarningAlert1": "All your {assetNameOrId} balance will be transferred in this transaction.", "components.send.sendscreen.sendAllWarningAlert2": "All your tokens, including NFTs and any other native assets in your wallet, will also be transferred in this transaction.", "components.send.sendscreen.sendAllWarningAlert3": "After you confirm the transaction in the next screen, your wallet will be emptied.", + "components.send.sendscreen.sendAllWarningText": "You have selected the send all option. Please confirm that you understand how this feature works.", + "components.send.sendscreen.sendAllWarningTitle": "Do you really want to send all?", + "components.send.sendscreen.submittedTxButton": "Go to transactions", + "components.send.sendscreen.submittedTxText": "Check this transaction in the list of wallet transactions", + "components.send.sendscreen.submittedTxTitle": "Transaction submitted", "components.send.sendscreen.title": "Send", - "components.settings.applicationsettingsscreen.label.general": "General", - "components.settings.applicationsettingsscreen.label.securityReporting": "Security & Reporting", - "components.settings.applicationsettingsscreen.crashReportingInfo": "Changes to this option will be reflected after restarting the application", - "components.settings.applicationsettingsscreen.selectLanguage": "Language", - "components.settings.applicationsettingsscreen.selectFiatCurrency": "Fiat Currency", + "components.send.sendscreen.walletAddress": "Wallet Address", "components.settings.applicationsettingsscreen.about": "About", - "components.settings.applicationsettingsscreen.privacyMode": "Hide balance", - "components.settings.applicationsettingsscreen.privacyModeInfo": "This function will be applied to all wallets in your app", - "components.settings.applicationsettingsscreen.biometricsSignInInfo": "Changes to this option will be reflected after restarting the application", - "components.settings.applicationsettingsscreen.crashReporting": "Send crash report to Emurgo", - "components.settings.applicationsettingsscreen.appSettingsTitle": "App settings", - "components.settings.applicationsettingsscreen.walletType": "Wallet type", "components.settings.applicationsettingsscreen.analytics": "Analytics", "components.settings.applicationsettingsscreen.analyticsTitle": "User Insights", - "components.settings.applicationsettingsscreen.privacyPolicy": "Privacy Policy", - "components.settings.applicationsettingsscreen.screenSharing": "Enable screensharing", - "components.settings.applicationsettingsscreen.screenSharingInfo": "Changes to this option will enable you to make screenshots as well share your screen via third party apps", - "components.send.sendscreen.submittedTxTitle": "Transaction submitted", - "components.send.sendscreen.submittedTxText": "Check this transaction in the list of wallet transactions", - "components.send.sendscreen.submittedTxButton": "Go to transactions", - "components.send.sendscreen.failedTxTitle": "Transaction failed", - "components.send.sendscreen.failedTxText": "Your transaction has not been processed properly due to technical issues", - "components.send.sendscreen.failedTxButton": "Try again", + "components.settings.applicationsettingsscreen.appSettingsTitle": "App settings", "components.settings.applicationsettingsscreen.biometricsSignIn": "Sign in with your biometrics", + "components.settings.applicationsettingsscreen.biometricsSignInInfo": "Changes to this option will be reflected after restarting the application", "components.settings.applicationsettingsscreen.changePin": "Change PIN", "components.settings.applicationsettingsscreen.commit": "Commit:", + "components.settings.applicationsettingsscreen.crashReporting": "Send crash report to Emurgo", + "components.settings.applicationsettingsscreen.crashReportingInfo": "Changes to this option will be reflected after restarting the application", "components.settings.applicationsettingsscreen.crashReportingText": "Send crash reports to EMURGO. Changes to this option will be reflected after restarting the application.", "components.settings.applicationsettingsscreen.currentLanguage": "English", + "components.settings.applicationsettingsscreen.label.general": "General", + "components.settings.applicationsettingsscreen.label.securityReporting": "Security & Reporting", "components.settings.applicationsettingsscreen.language": "Your language", "components.settings.applicationsettingsscreen.network": "Network:", + "components.settings.applicationsettingsscreen.privacyMode": "Hide balance", + "components.settings.applicationsettingsscreen.privacyModeInfo": "This function will be applied to all wallets in your app", + "components.settings.applicationsettingsscreen.privacyPolicy": "Privacy Policy", + "components.settings.applicationsettingsscreen.screenSharing": "Enable screensharing", + "components.settings.applicationsettingsscreen.screenSharingInfo": "Changes to this option will enable you to make screenshots as well share your screen via third party apps", "components.settings.applicationsettingsscreen.security": "Security", + "components.settings.applicationsettingsscreen.selectFiatCurrency": "Fiat Currency", + "components.settings.applicationsettingsscreen.selectLanguage": "Language", "components.settings.applicationsettingsscreen.support": "Support", "components.settings.applicationsettingsscreen.tabTitle": "Application", "components.settings.applicationsettingsscreen.termsOfUse": "Terms of Use", "components.settings.applicationsettingsscreen.title": "Settings", "components.settings.applicationsettingsscreen.version": "Current version:", + "components.settings.applicationsettingsscreen.walletType": "Wallet type", "components.settings.biometricslinkscreen.enableFingerprintsMessage": "Enable use of fingerprints in device settings first!", "components.settings.biometricslinkscreen.heading": "Use your device biometrics", "components.settings.biometricslinkscreen.linkButton": "Link", @@ -396,6 +342,7 @@ "components.settings.changecustompinscreen.PinRegistrationForm.PinInput.subtitle": "Choose new PIN for quick access to wallet.", "components.settings.changecustompinscreen.PinRegistrationForm.PinInput.title": "Enter PIN", "components.settings.changecustompinscreen.title": "Change PIN", + "components.settings.changelanguagescreen.title": "Language", "components.settings.changepasswordscreen.continueButton": "Change password", "components.settings.changepasswordscreen.newPasswordInputLabel": "New password", "components.settings.changepasswordscreen.oldPasswordInputLabel": "Current password", @@ -405,6 +352,22 @@ "components.settings.changewalletname.changeButton": "Change name", "components.settings.changewalletname.title": "Change wallet name", "components.settings.changewalletname.walletNameInputLabel": "Wallet name", + "components.settings.collateral.collateralSpent": "Your collateral is gone, please generate new collateral", + "components.settings.collateral.generateCollateral": "Generate collateral", + "components.settings.collateral.lockedAsCollateral": "Locked as collateral", + "components.settings.collateral.notEnoughFundsAlertMessage": "We could not find enough funds in this wallet to create collateral.", + "components.settings.collateral.notEnoughFundsAlertOK": "OK", + "components.settings.collateral.notEnoughFundsAlertTitle": "Not enough funds", + "components.settings.collateral.removeCollateral": "If you want to return the amount locked as collateral to your balance press the remove icon", + "components.settings.disableeasyconfirmationscreen.disableButton": "Disable", + "components.settings.disableeasyconfirmationscreen.disableHeading": "By disabling this option, you will be able to spend your assets only with your master password.", + "components.settings.disableeasyconfirmationscreen.title": "Disable easy confirmation", + "components.settings.enableeasyconfirmationscreen.enableButton": "Enable", + "components.settings.enableeasyconfirmationscreen.enableHeading": "This option will allow you to send transactions from your wallet by simply confirming with fingerprint or facial recognition (with a standard system fallback option). This makes your wallet less secure. This is a compromise between UX and security!", + "components.settings.enableeasyconfirmationscreen.enableMasterPassword": "Master password", + "components.settings.enableeasyconfirmationscreen.enableWarning": "Please remember your master password, as you may need it in case your biometrics data are removed from the device.", + "components.settings.enableeasyconfirmationscreen.title": "Enable easy confirmation", + "components.settings.privacypolicyscreen.title": "Privacy Policy", "components.settings.removewalletscreen.descriptionParagraph1": "If you wish to permanently delete the wallet, make sure you have written down your 15-word recovery phrase.", "components.settings.removewalletscreen.descriptionParagraph2": "To confirm this operation, type the wallet name below.", "components.settings.removewalletscreen.hasWrittenDownMnemonic": "I have written down the recovery phrase of this wallet and understand that I cannot recover the wallet without it.", @@ -421,19 +384,12 @@ "components.settings.settingsscreen.reportUrl": "https://yoroi-wallet.com/support/", "components.settings.settingsscreen.title": "Support", "components.settings.termsofservicescreen.title": "Terms of Service Agreement", - "components.settings.privacypolicyscreen.title": "Privacy Policy", - "components.settings.disableeasyconfirmationscreen.disableButton": "Disable", - "components.settings.disableeasyconfirmationscreen.disableHeading": "By disabling this option, you will be able to spend your assets only with your master password.", - "components.settings.disableeasyconfirmationscreen.title": "Disable easy confirmation", - "components.settings.enableeasyconfirmationscreen.enableButton": "Enable", - "components.settings.enableeasyconfirmationscreen.enableHeading": "This option will allow you to send transactions from your wallet by simply confirming with fingerprint or facial recognition (with a standard system fallback option). This makes your wallet less secure. This is a compromise between UX and security!", - "components.settings.enableeasyconfirmationscreen.enableMasterPassword": "Master password", - "components.settings.enableeasyconfirmationscreen.enableWarning": "Please remember your master password, as you may need it in case your biometrics data are removed from the device.", - "components.settings.enableeasyconfirmationscreen.title": "Enable easy confirmation", - "components.settings.walletsettingscreen.unknownWalletType": "Unknown Wallet Type", + "components.settings.walletsettingscreen.about": "About", + "components.settings.walletsettingscreen.actions": "Actions", "components.settings.walletsettingscreen.byronWallet": "Byron-era wallet", "components.settings.walletsettingscreen.changePassword": "Change spending password", "components.settings.walletsettingscreen.easyConfirmation": "Easy transaction confirmation", + "components.settings.walletsettingscreen.general": "General", "components.settings.walletsettingscreen.logout": "Logout", "components.settings.walletsettingscreen.removeWallet": "Remove wallet", "components.settings.walletsettingscreen.resyncWallet": "Resync wallet", @@ -442,25 +398,16 @@ "components.settings.walletsettingscreen.switchWallet": "Switch wallet", "components.settings.walletsettingscreen.tabTitle": "Wallet", "components.settings.walletsettingscreen.title": "Settings", + "components.settings.walletsettingscreen.unknownWalletType": "Unknown Wallet Type", "components.settings.walletsettingscreen.walletName": "Wallet name", "components.settings.walletsettingscreen.walletType": "Wallet type:", - "components.settings.walletsettingscreen.about": "About", - "components.settings.walletsettingscreen.general": "General", - "components.settings.walletsettingscreen.actions": "Actions", - "components.settings.changelanguagescreen.title": "Language", - "components.settings.collateral.lockedAsCollateral": "Locked as collateral", - "components.settings.collateral.removeCollateral": "If you want to return the amount locked as collateral to your balance press the remove icon", - "components.settings.collateral.collateralSpent": "Your collateral is gone, please generate new collateral", - "components.settings.collateral.generateCollateral": "Generate collateral", - "components.settings.collateral.notEnoughFundsAlertTitle": "Not enough funds", - "components.settings.collateral.notEnoughFundsAlertMessage": "We could not find enough funds in this wallet to create collateral.", - "components.settings.collateral.notEnoughFundsAlertOK": "OK", "components.stakingcenter.confirmDelegation.delegateButtonLabel": "Delegate", "components.stakingcenter.confirmDelegation.ofFees": "of fees", "components.stakingcenter.confirmDelegation.rewardsExplanation": "Current approximation of rewards that you will receive per epoch:", "components.stakingcenter.confirmDelegation.title": "Confirm delegation", "components.stakingcenter.delegationbyid.stakePoolId": "Stake pool id", "components.stakingcenter.delegationbyid.title": "Delegation by Id", + "components.stakingcenter.delegationTxBuildError": "Error while building delegation transaction", "components.stakingcenter.noPoolDataDialog.message": "The data from the stake pool(s) you selected is invalid. Please try again", "components.stakingcenter.noPoolDataDialog.title": "Invalid pool data", "components.stakingcenter.pooldetailscreen.title": "Nightly TESTING POOL", @@ -471,22 +418,21 @@ "components.stakingcenter.poolwarningmodal.title": "Attention", "components.stakingcenter.poolwarningmodal.unknown": "Causes some unknown issue (look online for more info)", "components.stakingcenter.title": "Staking Center", - "components.stakingcenter.delegationTxBuildError": "Error while building delegation transaction", "components.transfer.transfersummarymodal.unregisterExplanation": "This transaction will unregister one or more staking keys, giving you back your {refundAmount} from your deposit.", "components.txhistory.balancebanner.pairedbalance.error": "Error obtaining {currency} pairing", - "components.txhistory.flawedwalletmodal.title": "Warning", "components.txhistory.flawedwalletmodal.explanation1": "It looks like you have accidentally created or restored a wallet that is only included in special versions for development. As a security measure, we have disabled this wallet.", "components.txhistory.flawedwalletmodal.explanation2": "You still can create a new wallet or restore one without restrictions. If you were affected in some way by this issue, please contact EMURGO.", "components.txhistory.flawedwalletmodal.okButton": "I understand", + "components.txhistory.flawedwalletmodal.title": "Warning", "components.txhistory.txdetails.addressPrefixChange": "/change", "components.txhistory.txdetails.addressPrefixNotMine": "not mine", "components.txhistory.txdetails.addressPrefixReceive": "/{idx}", "components.txhistory.txdetails.confirmations": "{cnt} {cnt, plural, one {CONFIRMATION} other {CONFIRMATIONS}}", "components.txhistory.txdetails.fee": "Fee:", "components.txhistory.txdetails.fromAddresses": "From Addresses", + "components.txhistory.txdetails.memo": "Memo", "components.txhistory.txdetails.omittedCount": "+ {cnt} omitted {cnt, plural, one {address} other {addresses}}", "components.txhistory.txdetails.toAddresses": "To Addresses", - "components.txhistory.txdetails.memo": "Memo", "components.txhistory.txdetails.transactionId": "Transaction ID", "components.txhistory.txdetails.txAssuranceLevel": "Transaction assurance level", "components.txhistory.txdetails.txTypeMulti": "Multi-party transaction", @@ -494,11 +440,11 @@ "components.txhistory.txdetails.txTypeSelf": "Intrawallet transaction", "components.txhistory.txdetails.txTypeSent": "Sent funds", "components.txhistory.txhistory.noTransactions": "There are no transactions in your wallet yet", - "components.txhistory.txhistory.warningbanner.title": "Note:", - "components.txhistory.txhistory.warningbanner.message": "The Shelley protocol upgrade adds a new Shelley wallet type which supports delegation. To delegate your ADA you will need to upgrade to a Shelley wallet.", - "components.txhistory.txhistory.warningbanner.buttonText": "Upgrade", - "components.txhistory.txhistory.syncErrorBannerTextWithRefresh": "We are experiencing synchronization issues. Pull to refresh", "components.txhistory.txhistory.syncErrorBannerTextWithoutRefresh": "We are experiencing synchronization issues.", + "components.txhistory.txhistory.syncErrorBannerTextWithRefresh": "We are experiencing synchronization issues. Pull to refresh", + "components.txhistory.txhistory.warningbanner.buttonText": "Upgrade", + "components.txhistory.txhistory.warningbanner.message": "The Shelley protocol upgrade adds a new Shelley wallet type which supports delegation. To delegate your ADA you will need to upgrade to a Shelley wallet.", + "components.txhistory.txhistory.warningbanner.title": "Note:", "components.txhistory.txhistorylistitem.assuranceLevelFailed": "Failed", "components.txhistory.txhistorylistitem.assuranceLevelHeader": "Assurance level:", "components.txhistory.txhistorylistitem.assuranceLevelHigh": "Success", @@ -514,8 +460,8 @@ "components.txhistory.txnavigationbuttons.sendButton": "Send", "components.uikit.offlinebanner.offline": "You are offline. Please check settings on your device.", "components.walletinit.connectnanox.checknanoxscreen.introline": "Before continuing, please make sure that:", - "components.walletinit.connectnanox.checknanoxscreen.title": "Connect to Ledger Device", "components.walletinit.connectnanox.checknanoxscreen.learnMore": "Learn more about using Yoroi with Ledger", + "components.walletinit.connectnanox.checknanoxscreen.title": "Connect to Ledger Device", "components.walletinit.connectnanox.connectnanoxscreen.caption": "Scanning bluetooth devices...", "components.walletinit.connectnanox.connectnanoxscreen.error": "An error occurred while trying to connect with your hardware wallet:", "components.walletinit.connectnanox.connectnanoxscreen.exportKey": "Action needed: Please, export public key from your Ledger device.", @@ -524,8 +470,8 @@ "components.walletinit.connectnanox.savenanoxscreen.ledgerWalletNameSuggestion": "My Ledger Wallet", "components.walletinit.connectnanox.savenanoxscreen.save": "Save", "components.walletinit.connectnanox.savenanoxscreen.title": "Save wallet", - "components.walletinit.createwallet.createwalletscreen.title": "Create a new wallet", "components.walletinit.createwallet.createwalletscreen.passwordLengthRequirement": "Minimum {requiredPasswordLength} characters", + "components.walletinit.createwallet.createwalletscreen.title": "Create a new wallet", "components.walletinit.createwallet.mnemonicbackupimportancemodal.confirmationButton": "I understand", "components.walletinit.createwallet.mnemonicbackupimportancemodal.keysStorageCheckbox": "I understand that my secret keys are held securely on this device only, not on the company`s servers", "components.walletinit.createwallet.mnemonicbackupimportancemodal.newDeviceRecoveryCheckbox": "I understand that if this application is moved to another device or deleted, my funds can be only recovered with the backup phrase that I have written down and saved in a secure place.", @@ -561,117 +507,47 @@ "components.walletinit.restorewallet.upgradeconfirmmodal.finalBalanceLabel": "Final balance", "components.walletinit.restorewallet.upgradeconfirmmodal.fromLabel": "From", "components.walletinit.restorewallet.upgradeconfirmmodal.noUpgradeLabel": "All done!", - "components.walletinit.restorewallet.upgradeconfirmmodal.toLabel": "To", - "components.walletinit.restorewallet.upgradeconfirmmodal.txIdLabel": "Transaction ID", - "components.walletinit.restorewallet.walletcredentialsscreen.title": "Wallet credentials", - "components.walletinit.restorewallet.walletcredentialsscreen.walletCheckError": "Could not verify wallet funds", - "components.walletinit.savereadonlywalletscreen.defaultWalletName": "My read-only wallet", - "components.walletinit.savereadonlywalletscreen.derivationPath": "Derivation path:", - "components.walletinit.savereadonlywalletscreen.key": "Key:", - "components.walletinit.savereadonlywalletscreen.title": "Verify read-only wallet", - "components.walletinit.walletdescription.slogan": "Your gateway to the financial world", - "components.walletinit.walletform.continueButton": "Continue", - "components.walletinit.walletform.newPasswordInput": "Spending password", - "components.walletinit.walletform.repeatPasswordInputError": "Passwords do not match", - "components.walletinit.walletform.repeatPasswordInputLabel": "Repeat spending password", - "components.walletinit.walletform.walletNameInputLabel": "Wallet name", - "components.walletinit.walletfreshinitscreen.addWalletButton": "Add wallet", - "components.walletinit.walletfreshinitscreen.addWalletOnShelleyButton": "Add wallet (Jormungandr ITN)", - "components.walletinit.walletinitscreen.createWalletButton": "Create wallet", - "components.walletinit.walletinitscreen.createWalletWithLedgerButton": "Connect to Ledger Nano", - "components.walletinit.walletinitscreen.importReadOnlyWalletExplanation": "The Yoroi extension allows you to export any of your wallets' public keys in a QR code. Choose this option to import a wallet from a QR code in read-only mode.", - "components.walletinit.walletinitscreen.importReadOnlyWalletLabel": "Read-only wallet", - "components.walletinit.walletinitscreen.restoreNormalWalletLabel": "15-word Wallet", - "components.walletinit.walletinitscreen.restoreNWordWalletExplanation": "If you have a recovery phrase consisting of {mnemonicLength} words, choose this option to restore your wallet.", - "components.walletinit.walletinitscreen.restoreWalletButton": "Restore wallet", - "components.walletinit.walletinitscreen.restore24WordWalletLabel": "24-word Wallet", - "components.walletinit.walletinitscreen.restoreShelleyWalletButton": "Restore wallet (Shelley Testnet)", - "components.walletinit.walletinitscreen.title": "Add wallet", - "components.walletinit.verifyrestoredwallet.title": "Verify restored wallet", - "components.walletinit.verifyrestoredwallet.checksumLabel": "Your Wallet Account checksum:", - "components.walletinit.verifyrestoredwallet.instructionLabel": "Be careful about wallet restoration:", - "components.walletinit.verifyrestoredwallet.instructionLabel-1": "Make sure your wallet account checksum and icon match what you remember.", - "components.walletinit.verifyrestoredwallet.instructionLabel-2": "Make sure the address(es) match what you remember", - "components.walletinit.verifyrestoredwallet.instructionLabel-3": "If you’ve entered wrong mnemonics you will just open another empty wallet with wrong account checksum and wrong addresses.", - "components.walletinit.verifyrestoredwallet.walletAddressLabel": "Wallet Address(es):", - "components.walletinit.verifyrestoredwallet.buttonText": "CONTINUE", - "components.walletselection.walletselectionscreen.addWalletButton": "Add wallet", - "components.walletselection.walletselectionscreen.addWalletOnShelleyButton": "Add wallet (Jormungandr ITN)", - "components.walletselection.walletselectionscreen.header": "My wallets", - "components.walletselection.walletselectionscreen.stakeDashboardButton": "Stake Dashboard", - "components.walletselection.walletselectionscreen.loadingWallet": "Loading wallet", - "components.walletselection.walletselectionscreen.supportTicketLink": "Ask our support team", - "components.catalyst.banner.name": "Catalyst Voting Registration", - "components.catalyst.catalystbackupcheckmodal.consequencesCheckbox": "I understand that if I did not save my Catalyst PIN and QR code (or secret code) I will not be able to register and vote for Catalyst proposals.", - "components.catalyst.catalystbackupcheckmodal.qrCodeCheckbox": "I have taken a screenshot of my QR code and saved my Catalyst secret code as a fallback.", - "components.catalyst.catalystbackupcheckmodal.pinCheckbox": "I have written down my Catalyst PIN which I obtained in previous steps.", - "components.catalyst.insufficientBalance": "Participating requires at least {requiredBalance}, but you only have {currentBalance}. Unwithdrawn rewards are not included in this amount", - "components.catalyst.step1.subTitle": "Before you begin, make sure to download the Catalyst Voting App.", - "components.catalyst.step1.stakingKeyNotRegistered": "Catalyst voting rewards are sent to delegation accounts and your wallet does not seem to have a registered delegation certificate. If you want to receive voting rewards, you need to delegate your funds first.", - "components.catalyst.step1.tip": "Tip: Make sure you know how to take a screenshot with your device, so that you can backup your catalyst QR code.", - "components.catalyst.step2.subTitle": "Write Down PIN", - "components.catalyst.step2.description": "Please write down this PIN as you will need it every time you want to access the Catalyst Voting app", - "components.catalyst.step3.subTitle": "Enter PIN", - "components.catalyst.step3.description": "Please enter the PIN as you will need it every time you want to access the Catalyst Voting app", - "components.catalyst.step4.bioAuthInstructions": "Please authenticate so that Yoroi can generate the required certificate for voting", - "components.catalyst.step4.description": "Enter your spending password to be able to generate the required certificate for voting", - "components.catalyst.step4.subTitle": "Enter Spending Password", - "components.catalyst.step5.subTitle": "Confirm Registration", - "components.catalyst.step5.bioAuthDescription": "Please confirm your voting registration. You will be asked to authenticate once again to sign and submit the certificate generated in the previous step.", - "components.catalyst.step5.description": "Enter your spending password to confirm your voting registration and submit the certificate generated in previous step to the blockchain.", - "components.catalyst.step6.subTitle": "Backup Catalyst Code", - "components.catalyst.step6.description": "Please take a screenshot of this QR code.", - "components.catalyst.step6.description2": "We strongly recommend you to save your Catalyst secret code in plain text too, so that you can re-create your QR code if necessary.", - "components.catalyst.step6.description3": "Then, send the QR code to an external device, as you will need to scan it with your phone using the Catalyst mobile app.", - "components.catalyst.step6.note": "Keep it — you won’t be able to access this code after tapping on Complete.", - "components.catalyst.step6.secretCode": "Secret Code", - "components.catalyst.title": "Register to vote", - "components.governance.governanceCentreTitle": "Governance centre", - "components.governance.confirmTxTitle": "Confirm transaction", - "components.governance.learnMoreAboutGovernance": "Learn more About Governance", - "components.governance.actionDelegateToADRepTitle": "Delegate to a DRep", - "components.governance.actionDelegateToADRepDescription": "You are designating someone else to cast vote on your behalf for all proposals now and in the future.", - "components.governance.actionAbstainTitle": "Abstain", - "components.governance.actionAbstainDescription": "You are choosing not to cast a vote on all proposals now and in the future.", - "components.governance.actionNoConfidenceTitle": "No confidence", - "components.governance.actionNoConfidenceDescription": "You are expressing a lack of trust for all proposals now and in the future.", - "components.governance.drepKey": "DRep Key", - "components.governance.delegatingToADRep": "Delegating to a DRep", - "components.governance.abstaining": "Abstaining", - "components.governance.delegateVotingToDRep": "Delegate voting to\n{drepID}", - "components.governance.selectAbstain": "Select abstain", - "components.governance.selectNoConfidence": "Select no confidence", - "components.governance.operations": "Operations", - "components.governance.enterADrepIDAndPassword": "Enter a Drep ID and password to sign this transaction", - "components.governance.drepID": "Drep ID", - "components.governance.thankYouForParticipating": "Thank you for participating in the Cardano Governance", - "components.governance.thisTransactionCanTakeAWhile": "This transaction can take a while!", - "components.governance.participationBenefits": "Participating in the Cardano Governance gives you the opportunity to participate in the voting as well as withdraw your staking rewards", - "components.governance.goToGovernance": "Go to Governance", - "components.governance.findDRepHere": "Find a DRep here", - "components.governance.reviewActions": "Review the selections carefully to assign yourself a Governance Status", - "components.governance.actionYouHaveSelectedTxPending": "You have selected {action} as your governance status.", - "components.governance.actionYouHaveSelected": "You have selected {action} as your governance status. You can change it at any time by clicking in the card below.", - "components.governance.changeDRep": "Change DRep", - "components.governance.confirm": "Confirm", - "components.governance.transactionDetails": "Transaction details", - "components.governance.total": "Total", - "components.governance.transactionFailed": "Transaction failed", - "components.governance.transactionFailedDescription": "Your transaction has not been processed properly due to technical issues", - "components.governance.tryAgain": "Try again", - "components.governance.withdrawWarningTitle": "Withdraw warning", - "components.governance.withdrawWarningDescription": "To withdraw your rewards, you need to participate in the Cardano Governance. Your rewards will continue to accumulate, but you are only able to withdraw it once you join the Governance process.", - "components.governance.withdrawWarningButton": "Participate on governance", - "components.governance.enterDRepID": "Choose your Drep", - "components.governance.enterPassword": "Enter password to sign this transaction", - "components.governance.hardwareWalletSupportComingSoon": "Hardware wallet support coming soon", - "components.governance.workingOnHardwareWalletSupport": "We are currently working on integrating hardware wallet support for Governance", - "components.governance.goToWallet": "Go to wallet", - "components.governance.txFees": "Transaction fee", - "components.governance.registerStakingKey": "Register staking key", - "components.governance.enterDrepIDInfo": "Identify your preferred DRep and enter their ID below to delegate your vote", - "components.governance.goToStaking": "Go to staking", - "components.governance.readyToCollectRewards": "You are now ready to collect your rewards", + "components.walletinit.restorewallet.upgradeconfirmmodal.toLabel": "To", + "components.walletinit.restorewallet.upgradeconfirmmodal.txIdLabel": "Transaction ID", + "components.walletinit.restorewallet.walletcredentialsscreen.title": "Wallet credentials", + "components.walletinit.restorewallet.walletcredentialsscreen.walletCheckError": "Could not verify wallet funds", + "components.walletinit.savereadonlywalletscreen.defaultWalletName": "My read-only wallet", + "components.walletinit.savereadonlywalletscreen.derivationPath": "Derivation path:", + "components.walletinit.savereadonlywalletscreen.key": "Key:", + "components.walletinit.savereadonlywalletscreen.title": "Verify read-only wallet", + "components.walletinit.verifyrestoredwallet.buttonText": "CONTINUE", + "components.walletinit.verifyrestoredwallet.checksumLabel": "Your Wallet Account checksum:", + "components.walletinit.verifyrestoredwallet.instructionLabel-1": "Make sure your wallet account checksum and icon match what you remember.", + "components.walletinit.verifyrestoredwallet.instructionLabel-2": "Make sure the address(es) match what you remember", + "components.walletinit.verifyrestoredwallet.instructionLabel-3": "If you’ve entered wrong mnemonics you will just open another empty wallet with wrong account checksum and wrong addresses.", + "components.walletinit.verifyrestoredwallet.instructionLabel": "Be careful about wallet restoration:", + "components.walletinit.verifyrestoredwallet.title": "Verify restored wallet", + "components.walletinit.verifyrestoredwallet.walletAddressLabel": "Wallet Address(es):", + "components.walletinit.walletdescription.slogan": "Your gateway to the financial world", + "components.walletinit.walletform.continueButton": "Continue", + "components.walletinit.walletform.newPasswordInput": "Spending password", + "components.walletinit.walletform.repeatPasswordInputError": "Passwords do not match", + "components.walletinit.walletform.repeatPasswordInputLabel": "Repeat spending password", + "components.walletinit.walletform.walletNameInputLabel": "Wallet name", + "components.walletinit.walletfreshinitscreen.addWalletButton": "Add wallet", + "components.walletinit.walletfreshinitscreen.addWalletOnShelleyButton": "Add wallet (Jormungandr ITN)", + "components.walletinit.walletinitscreen.createWalletButton": "Create wallet", + "components.walletinit.walletinitscreen.createWalletWithLedgerButton": "Connect to Ledger Nano", + "components.walletinit.walletinitscreen.importReadOnlyWalletExplanation": "The Yoroi extension allows you to export any of your wallets' public keys in a QR code. Choose this option to import a wallet from a QR code in read-only mode.", + "components.walletinit.walletinitscreen.importReadOnlyWalletLabel": "Read-only wallet", + "components.walletinit.walletinitscreen.restore24WordWalletLabel": "24-word Wallet", + "components.walletinit.walletinitscreen.restoreNormalWalletLabel": "15-word Wallet", + "components.walletinit.walletinitscreen.restoreNWordWalletExplanation": "If you have a recovery phrase consisting of {mnemonicLength} words, choose this option to restore your wallet.", + "components.walletinit.walletinitscreen.restoreShelleyWalletButton": "Restore wallet (Shelley Testnet)", + "components.walletinit.walletinitscreen.restoreWalletButton": "Restore wallet", + "components.walletinit.walletinitscreen.title": "Add wallet", + "components.walletselection.walletselectionscreen.addWalletButton": "Add wallet", + "components.walletselection.walletselectionscreen.addWalletOnShelleyButton": "Add wallet (Jormungandr ITN)", + "components.walletselection.walletselectionscreen.header": "My wallets", + "components.walletselection.walletselectionscreen.loadingWallet": "Loading wallet", + "components.walletselection.walletselectionscreen.stakeDashboardButton": "Stake Dashboard", + "components.walletselection.walletselectionscreen.supportTicketLink": "Ask our support team", + "components.yoroiLogo": "Light wallet for Cardano assets", "crypto.errors.rewardAddressEmpty": "Reward address is empty.", "crypto.keystore.approveTransaction": "Authorize with your biometrics", "crypto.keystore.cancelButton": "Cancel", @@ -682,11 +558,11 @@ "global.actions.dialogs.biometricsIsTurnedOff.message": "It seems that you turned off biometrics. Please turn it on", "global.actions.dialogs.biometricsIsTurnedOff.title": "Biometrics was turned off", "global.actions.dialogs.commonbuttons.backButton": "Back", + "global.actions.dialogs.commonbuttons.cancelButton": "Cancel", + "global.actions.dialogs.commonbuttons.completeButton": "Complete", "global.actions.dialogs.commonbuttons.confirmButton": "Confirm", "global.actions.dialogs.commonbuttons.continueButton": "Continue", - "global.actions.dialogs.commonbuttons.cancelButton": "Cancel", "global.actions.dialogs.commonbuttons.iUnderstandButton": "I understand", - "global.actions.dialogs.commonbuttons.completeButton": "Complete", "global.actions.dialogs.disableEasyConfirmationFirst.message": "Please disable easy confirmation function in all your wallets first", "global.actions.dialogs.disableEasyConfirmationFirst.title": "Action failed", "global.actions.dialogs.enableFingerprintsFirst.message": "You need to enable biometrics in your device first in order to be able link it with this app", @@ -699,14 +575,16 @@ "global.actions.dialogs.generalError.title": "Unexpected error", "global.actions.dialogs.generalLocalizableError.message": "Requested operation failed: {message}", "global.actions.dialogs.generalLocalizableError.title": "Operation failed", - "global.actions.dialogs.generalTxError.title": "Transaction Error", "global.actions.dialogs.generalTxError.message": "An error occurred while trying to send the transaction.", + "global.actions.dialogs.generalTxError.title": "Transaction Error", "global.actions.dialogs.hwConnectionError.message": "An error occurred while trying to connect with your hardware wallet. Please, make sure you are following the steps correctly. Restarting your hardware wallet may also fix the problem. Error: {message}", "global.actions.dialogs.hwConnectionError.title": "Connection error", "global.actions.dialogs.incorrectPassword.message": "Password you provided is incorrect.", "global.actions.dialogs.incorrectPassword.title": "Wrong password", "global.actions.dialogs.incorrectPin.message": "The PIN you entered is incorrect.", "global.actions.dialogs.incorrectPin.title": "Invalid PIN", + "global.actions.dialogs.insufficientBalance.message": "Not enough funds to make this transaction", + "global.actions.dialogs.insufficientBalance.title": "Transaction error", "global.actions.dialogs.invalidQRCode.message": "The QR code you scanned does not seem to contain a valid public key. Please try again with a new one.", "global.actions.dialogs.invalidQRCode.title": "Invalid QR code", "global.actions.dialogs.itnNotSupported.message": "Wallets created during the Incentivized Testnet (ITN) are no longer operative.\nIf you would like to claim your rewards, we will update Yoroi Mobile as well as Yoroi Desktop in the next couple of weeks.", @@ -715,8 +593,6 @@ "global.actions.dialogs.logout.noButton": "No", "global.actions.dialogs.logout.title": "Logout", "global.actions.dialogs.logout.yesButton": "Yes", - "global.actions.dialogs.insufficientBalance.title": "Transaction error", - "global.actions.dialogs.insufficientBalance.message": "Not enough funds to make this transaction", "global.actions.dialogs.networkError.message": "Error connecting to the server. Please check your internet connection", "global.actions.dialogs.networkError.title": "Network error", "global.actions.dialogs.notSupportedError.message": "This feature is not yet supported. It will be enabled in a future release.", @@ -733,28 +609,22 @@ "global.actions.dialogs.wrongPinError.title": "Invalid PIN", "global.all": "All", "global.apply": "Apply", - "global.assets.assetsLabel": "Assets", "global.assets.assetLabel": "Asset", + "global.assets.assetsLabel": "Assets", "global.assets": "{qty, plural, one {asset} other {asset(s)}}", - "global.pools": "{qty, plural, one {DEX} other {DEX(es)}}", - "global.price": "Price", "global.available": "available", "global.availableFunds": "Available funds", "global.buy": "Buy", - "global.buyTitle": "Exchange ADA", "global.buyInfo": "Yoroi uses Banxa to provide direct Fiat-ADA exchange. By clicking “Proceed,” you also acknowledge that you will be redirected to our partner’s website, where you may be asked to accept their terms and conditions.", - "global.proceed": "proceed", + "global.buyTitle": "Exchange ADA", "global.cancel": "Cancel", - "global.collateral": "Collateral", - "global.close": "close", "global.clear": "clear", - "global.signTransaction": "Sign transaction", - "global.sign": "sign", - "global.spendingPassword": "Spending Password", - "global.enterSpendingPassword": "Enter spending password to sign this transaction", - "global.confirmationTransaction": "Confirm transaction", + "global.close": "close", + "global.collateral": "Collateral", "global.comingSoon": "Coming soon", - "global.currency": "Currency", + "global.commit": "Commit", + "global.confirmationTransaction": "Confirm transaction", + "global.continue": "Continue", "global.currency.ADA": "Cardano", "global.currency.BRL": "Brazilian Real", "global.currency.BTC": "Bitcoin", @@ -764,22 +634,27 @@ "global.currency.JPY": "Japanese Yen", "global.currency.KRW": "South Korean Won", "global.currency.USD": "US Dollar", - "global.error": "Error", + "global.currency": "Currency", + "global.currentVersion": "Current Version", + "global.deprecated": "Deprecated", + "global.enterSpendingPassword": "Enter spending password to sign this transaction", "global.error.insufficientBalance": "Insufficent balance", "global.error.serviceUnavailable": "Service unavailable", "global.error.serviceUnavailableInfo": "The server is temporarily busy due to maintenance downtime or capacity problems", "global.error.walletNameAlreadyTaken": "You already have a wallet with this name", - "global.error.walletNameTooLong": "Wallet name cannot exceed 40 letters", "global.error.walletNameMustBeFilled": "Must be filled", - "global.info": "Info", + "global.error.walletNameTooLong": "Wallet name cannot exceed 40 letters", + "global.error": "Error", "global.info.minPrimaryBalanceForTokens": "Bear in mind that you need to keep some min ADA for holding your tokens and NFTs", + "global.info": "Info", "global.learnMore": "Learn more", "global.ledgerMessages.appInstalled": "Cardano ADA app is installed on your Ledger device.", "global.ledgerMessages.appOpened": "Cardano ADA app must remain open on your Ledger device.", - "global.ledgerMessages.bluetoothEnabled": "Bluetooth is enabled on your smartphone.", "global.ledgerMessages.bluetoothDisabledError": "Bluetooth is disabled in your smartphone, or the requested permission was denied.", + "global.ledgerMessages.bluetoothEnabled": "Bluetooth is enabled on your smartphone.", "global.ledgerMessages.connectionError": "An error occurred while trying to connect with your hardware wallet. Please, make sure you are following the steps correctly. Restarting your hardware wallet may also fix the problem.", "global.ledgerMessages.connectUsb": "Connect your Ledger device through your smartphone's USB port using your OTG adapter.", + "global.ledgerMessages.continueOnLedger": "Continue on Ledger", "global.ledgerMessages.deprecatedAdaAppError": "The Cardano ADA app installed in your Ledger device is not up-to-date. Required version: {version}", "global.ledgerMessages.enableLocation": "Enable location services.", "global.ledgerMessages.enableTransport": "Enable bluetooth.", @@ -792,28 +667,36 @@ "global.ledgerMessages.openApp": "Open Cardano ADA app on the Ledger device.", "global.ledgerMessages.rejectedByUserError": "Operation rejected by user.", "global.ledgerMessages.usbAlwaysConnected": "Your Ledger device remains connected through USB until the process is completed.", - "global.ledgerMessages.continueOnLedger": "Continue on Ledger", "global.lockedDeposit": "Locked deposit", "global.lockedDepositHint": "This amount cannot be transferred or delegated while you hold assets like tokens or NFTs", "global.max": "Max", "global.network.syncErrorBannerTextWithoutRefresh": "We are experiencing synchronization issues.", "global.network.syncErrorBannerTextWithRefresh": "We are experiencing synchronization issues. Pull to refresh", + "global.network": "Network", + "global.next": "Next", "global.nfts": "{qty, plural, one {NFT} other {NFTs}}", "global.notSupported": "Feature not supported", - "global.deprecated": "Deprecated", "global.ok": "OK", "global.openAppSettings": "Open app settings", "global.openInExplorer": "Open in explorer", "global.pleaseConfirm": "Please confirm", "global.pleaseWait": "please wait ...", + "global.pools": "{qty, plural, one {DEX} other {DEX(es)}}", + "global.price": "Price", + "global.proceed": "proceed", "global.receive": "Receive", + "global.search": "Search", "global.send": "Send", - "global.swap": "Swap", - "global.staking": "Staking", + "global.sign": "sign", + "global.signTransaction": "Sign transaction", + "global.spendingPassword": "Spending Password", "global.staking.epochLabel": "Epoch", - "global.staking.stakePoolName": "Stake pool name", "global.staking.stakePoolHash": "Stake pool hash", + "global.staking.stakePoolName": "Stake pool name", + "global.staking": "Staking", + "global.swap": "Swap", "global.termsOfUse": "Terms of use", + "global.title": "Language", "global.tokens": "{qty, plural, one {Token} other {Tokens}}", "global.total": "Total", "global.totalAda": "Total ADA", @@ -825,29 +708,155 @@ "global.txLabels.fees": "Fees", "global.txLabels.password": "Spending password", "global.txLabels.receiver": "Receiver", + "global.txLabels.signingTx": "Signing transaction", "global.txLabels.stakeDeregistration": "Staking key deregistration", "global.txLabels.submittingTx": "Submitting transaction", - "global.txLabels.signingTx": "Signing transaction", "global.txLabels.transactions": "Transactions", "global.txLabels.withdrawals": "Withdrawals", - "global.next": "Next", - "utils.format.today": "Today", - "utils.format.unknownAssetName": "[Unknown asset name]", - "utils.format.yesterday": "Yesterday", - "global.network": "Network", - "global.commit": "Commit", - "global.currentVersion": "Current Version", - "global.title": "Language", - "global.search": "Search", - "global.continue": "Continue", - "components.yoroiLogo": "Light wallet for Cardano assets", - "termsOfService.agreementUpdateTitle": "Terms of Service Agreement Update", + "menu.allWallets": "All Wallets", + "menu.appSettings": "App Settings", + "menu.catalystVoting": "Catalyst Voting", + "menu.governanceCentre": "Governance centre", + "menu.knowledgeBase": "Knowledge base", + "menu.releases": "Releases", + "menu.settings": "Settings", + "menu.supportLink": "Ask our support team", + "menu.supportTitle": "Any questions?", + "menu": "Menu", + "nft.detail.author": "Author", + "nft.detail.copyMetadata": "Copy metadata", + "nft.detail.createdAt": "Created", + "nft.detail.description": "Description", + "nft.detail.detailsLinks": "Details on", + "nft.detail.fingerprint": "Fingerprint", + "nft.detail.metadata": "Metadata", + "nft.detail.nftName": "NFT Name", + "nft.detail.overview": "Overview", + "nft.detail.policyId": "Policy id", + "nft.detail.title": "NFT Details", + "nft.gallery.errorDescription": "Something went wrong.", + "nft.gallery.errorTitle": "Oops!", + "nft.gallery.nftCount": "NFT count", + "nft.gallery.noNftsFound": "No NFTs found", + "nft.gallery.noNftsInWallet": "No NFTs added to your wallet yet", + "nft.gallery.reloadApp": "Try to restart the app.", + "nft.navigation.search": "Search NFT", + "nft.navigation.title": "NFT Gallery", + "scan.cameraPermissionDenied.help": "It looks the app doesn't have access to your device camera, if you are no longer asked to grant camera permission, please go to your device settings, search for Yoroi and allow access to the camera", + "scan.cameraPermissionDenied.title": "The app needs access to your camera", + "scan.errorUnknown.help": "An unknown error occurred while scanning the code, please try again", + "scan.errorUnknown.title": "Unknown error", + "scan.errorUnknownContent.help": "The content of the code was not recognized by the app", + "scan.errorUnknownContent.title": "Content is unknown'", + "scan.linksErrorExtraParamsDenied.help": "The link contains extra parameter(s) which is denied", + "scan.linksErrorExtraParamsDenied.title": "Extra parameter denied", + "scan.linksErrorForbiddenParamsProvided.help": "The link contains forbidden parameter(s)", + "scan.linksErrorForbiddenParamsProvided.title": "Forbidden parameter(s) provided", + "scan.linksErrorParamsValidationFailed.help": "The link contains parameter(s) that failed the validation'", + "scan.linksErrorParamsValidationFailed.title": "Parameter validation failed", + "scan.linksErrorRequiredParamsMissing.help": "The link does not contain required parameter(s)", + "scan.linksErrorRequiredParamsMissing.title": "Missing required parameter(s)", + "scan.linksErrorSchemeNotImplemented.help": "The link scheme of the code is not supported by the app", + "scan.linksErrorSchemeNotImplemented.title": "Scheme not implemented", + "scan.linksErrorUnsupportedAuthority.help": "The link authority is not yet supported by the app'", + "scan.linksErrorUnsupportedAuthority.title": "Unsupported authority", + "scan.linksErrorUnsupportedVersion.help": "The link authority version is not yet supported by the app'", + "scan.linksErrorUnsupportedVersion.title": "Unsupported version", + "scan.title": "Scan the QR code", + "send.helper.addressError.invalid": "Please enter a valid address", + "send.helper.addressError.wrongNetwork": "Please enter an address for the current wallet network", + "send.helper.resolver.resolvedAddress": "Resolved address", + "send.helper.resolverError.unsupportedDomain": "Please enter a supported domain", + "send.warning.resolver.manyNameServers": "There are more than one address for this domain. Please choose the desired name server.", + "swap.listOrders.card.buttonText": "Cancel order", + "swap.listOrders.completed": "completed orders", + "swap.listOrders.emptyCompletedOrders": "No orders completed yet", + "swap.listOrders.emptyOpenOrders": "No orders available yet", + "swap.listOrders.emptyOpenOrdersSub": "Start doing the swap operations to your open orders here", + "swap.listOrders.liquidityPool": "Liquidity Pool", + "swap.listOrders.open": "open orders", + "swap.listOrders.sheet.assetAmount": "Asset amount", + "swap.listOrders.sheet.assetPrice": "Asset price", + "swap.listOrders.sheet.back": "Back", + "swap.listOrders.sheet.cancellationFee": "Cancellation Fee", + "swap.listOrders.sheet.confirm": "Confirm", + "swap.listOrders.sheet.contentTitle": "Are you sure you want to cancel this order?", + "swap.listOrders.sheet.link": "Learn more about swap orders in Yoroi", + "swap.listOrders.sheet.title": "Confirm order cancellation", + "swap.listOrders.sheet.totalReturned": "Total returned", + "swap.listOrders.timeCompleted": "Time completed", + "swap.listOrders.timeCreated": "Time created", + "swap.listOrders.total": "Total", + "swap.listOrders.txId": "Transaction ID", + "swap.slippage.changeAmount": "Increase the amount to proceed or change slippage tolerance to 0%", + "swap.slippage.slippageWarningText": "Are you sure you want to proceed this order with the current slippage tolerance? It could result in receiving no assets.", + "swap.slippage.slippageWarningTitle": "Slippage Warning", + "swap.slippage.yourSlippage": "Your slippage tolerance", + "swap.swapScreen.assetsIn": "This asset is in my portfolio", + "swap.swapScreen.autoPool": "(auto)", + "swap.swapScreen.balance": "Balance", + "swap.swapScreen.batcherFee": "Batcher Fee", + "swap.swapScreen.changePool": "change dex", + "swap.swapScreen.completedOrders": "Completed orders", + "swap.swapScreen.currentBalance": "Current balance", + "swap.swapScreen.dex": "dex", + "swap.swapScreen.eachVerifiedToken": "Each verified tokens gets", + "swap.swapScreen.enterSlippage": "Enter a value from 0% to 75%. You can also enter up to 1 decimal", + "swap.swapScreen.goToOrders": "Go to orders", + "swap.swapScreen.limitButton": "Limit", + "swap.swapScreen.limitPrice": "Limit price", + "swap.swapScreen.limitPriceWarningBack": "Back", + "swap.swapScreen.limitPriceWarningConfirm": "Swap", + "swap.swapScreen.limitPriceWarningDescription": "Are you sure you want to proceed this order with the limit price that is 10% or more higher than the market price?", + "swap.swapScreen.limitPriceWarningMarketPrice": "Market price", + "swap.swapScreen.limitPriceWarningTitle": "Limit price", + "swap.swapScreen.limitPriceWarningYourPrice": "Your limit price", + "swap.swapScreen.marketButton": "Market", + "swap.swapScreen.marketPrice": "Market price", + "swap.swapScreen.noAssetsFound": "No assets found for this pair", + "swap.swapScreen.noAssetsFoundFor": "No assets found for \"{search}\"", + "swap.swapScreen.noPool": "This pair is not available in any liquidity pool", + "swap.swapScreen.notEnoughBalance": "Not enough balance", + "swap.swapScreen.notEnoughFeeBalance": "Not enough balance, please consider the fees", + "swap.swapScreen.notEnoughSupply": "Not enough supply in the pool", + "swap.swapScreen.openOrders": "Open orders", + "swap.swapScreen.ordersSwapTab": "Orders", + "swap.swapScreen.poolFee": "DEX Fee", + "swap.swapScreen.poolVerification": "{pool} verification", + "swap.swapScreen.poolVerificationInfo": "Cardano projects that list their own tokens can apply for an additional {pool} verification. This verification is a manual validation that {pool} team performs with the help of Cardano Foundation.", + "swap.swapScreen.seeOnExplorer": "see on explorer", + "swap.swapScreen.selectPool": "Select DEX", + "swap.swapScreen.selectToken": "Select token", + "swap.swapScreen.slippageInfo": "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.", + "swap.swapScreen.slippageTolerance": "Slippage tolerance", + "swap.swapScreen.slippageToleranceError": "Slippage must be a number between 0 and 75 and have up to 1 decimal", + "swap.swapScreen.slippageToleranceInfo": "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", + "swap.swapScreen.swapFees": "Swap fees include the following:\n • DEX Fee\n • Frontend Fee", + "swap.swapScreen.swapFeesTitle": "Fees", + "swap.swapScreen.swapFrom": "Swap from", + "swap.swapScreen.swapLiqProvFee": "Liq. prov. fee", + "swap.swapScreen.swapLiquidityFee": "Liquidity provider fee", + "swap.swapScreen.swapLiquidityFeeInfo": "Liquidity provider fee is a fixed {fee}% operational fee from the whole transaction volume, that is taken to support DEX “liquidity” allowing traders to buy and sell assets on the decentralized Cardano network.", + "swap.swapScreen.swapMinAda": "Min-ADA is the minimum ADA amount required to be contained when holding or sending Cardano native assets.", + "swap.swapScreen.swapMinAdaTitle": "Min ADA", + "swap.swapScreen.swapMinReceived": "Minimum amount of assets you can get because of the slippage tolerance.", + "swap.swapScreen.swapMinReceivedTitle": "Min Received", + "swap.swapScreen.swapTitle": "Swap", + "swap.swapScreen.swapTo": "Swap to", + "swap.swapScreen.tokenSwapTab": "Asset swap", + "swap.swapScreen.transactionDisplay": "In a few minutes, your transactions will be displayed both in the list of transaction and Open swap orders", + "swap.swapScreen.transactionSigned": "Transaction submitted", + "swap.swapScreen.tvl": "TVL", + "swap.swapScreen.verifiedBadge": "verified badge", + "swap.swapScreen.verifiedBy": "Verified by {pool}", + "swap.swapScreen.volume": "Volume, 24h", "termsOfService.agreementUpdateDescription": "We have updated our Terms of Service Agreement to enhance your experience. Please review and accept them to keep enjoying Yoroi.", - "termsOfService.tosIAgreeWith": "I agree with", + "termsOfService.agreementUpdateTitle": "Terms of Service Agreement Update", + "termsOfService.privacyPolicyTitle": "Privacy Policy", "termsOfService.tosAgreement": "Terms Of Service Agreement", + "termsOfService.tosIAgreeWith": "I agree with", "toggleAnalyticsSettings.analyticsTitle": "User Insights", - "termsOfService.privacyPolicyTitle": "Privacy Policy", - "analytics.privacyPolicy": "Privacy Policy", - "analytics.privacyNotice": "Privacy Notice", - "analytics.tosAnd": "and" + "utils.format.today": "Today", + "utils.format.unknownAssetName": "[Unknown asset name]", + "utils.format.yesterday": "Yesterday" } diff --git a/apps/wallet-mobile/src/legacy/config.ts b/apps/wallet-mobile/src/legacy/config.ts index d4324da841..ae99ac27e0 100644 --- a/apps/wallet-mobile/src/legacy/config.ts +++ b/apps/wallet-mobile/src/legacy/config.ts @@ -15,6 +15,7 @@ const FORCE_CRASH_REPORTS = isNightly() const AGREEMENT_DATE = 1691967600000 +const UNSTOPPABLE_API_KEY = env.getString('UNSTOPPABLE_API_KEY') const GOVERNANCE_ENABLED_SINCE_BLOCK = { SANCHONET: 0, MAINNET: Infinity, // TODO: Add block number once known @@ -28,5 +29,6 @@ export const CONFIG = { COMMIT: _COMMIT, FORCE_CRASH_REPORTS, AGREEMENT_DATE, + UNSTOPPABLE_API_KEY, GOVERNANCE_ENABLED_SINCE_BLOCK, } diff --git a/apps/wallet-mobile/src/utils/debounceMaker.ts b/apps/wallet-mobile/src/utils/debounceMaker.ts new file mode 100644 index 0000000000..436573610f --- /dev/null +++ b/apps/wallet-mobile/src/utils/debounceMaker.ts @@ -0,0 +1,22 @@ +export const debounceMaker = unknown>(callback: T, delay: number) => { + let timeoutId: NodeJS.Timeout | null = null + + const clear = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId) + } + } + + const call = (...args: Parameters) => { + clear() + + timeoutId = setTimeout(() => { + callback(...args) + }, delay) + } + + return { + clear, + call, + } as const +} diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/constants/common.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/constants/common.ts index 95a3a02bd3..44df21d3a9 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/constants/common.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/constants/common.ts @@ -54,7 +54,7 @@ export const LINEAR_FEE = { } as const export const MINIMUM_UTXO_VAL = '1000000' -export const HISTORY_REFRESH_TIME = 25000 +export const HISTORY_REFRESH_TIME = 35000 export const COINS_PER_UTXO_WORD = '34482' diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/networks.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/networks.ts index fc76c336dd..6c5a818045 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/networks.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/networks.ts @@ -200,7 +200,7 @@ export const NETWORKS = { JORMUNGANDR, SANCHONET: SANCHONET_CONFIG.NETWORK_CONFIG, } -type NetworkConfig = +export type NetworkConfig = | typeof NETWORKS.BYRON_MAINNET | typeof NETWORKS.HASKELL_SHELLEY | typeof NETWORKS.HASKELL_SHELLEY_TESTNET diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/utils.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/utils.ts index cb1781ac2a..5502cd9209 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/utils.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/utils.ts @@ -23,8 +23,6 @@ import { WALLET_CONFIG as HASKELL_SHELLEY, WALLET_CONFIG_24 as HASKELL_SHELLEY_24, } from './constants/mainnet/constants' -import {NETWORK_ID as sanchonetNetworkId} from './constants/sanchonet/constants' -import {NETWORK_ID as testnetId} from './constants/testnet/constants' import {withMinAmounts} from './getMinAmounts' import {MultiToken} from './MultiToken' import {CardanoHaskellShelleyNetwork, PRIMARY_ASSET_CONSTANTS} from './networks' @@ -41,19 +39,16 @@ export const normalizeToAddress = async (addr: string) => { } } catch (_e) {} - // eslint-disable-line no-empty // 2) If already base16, simply return try { return await CardanoMobile.Address.fromBytes(Buffer.from(addr, 'hex')) } catch (_e) {} - // eslint-disable-line no-empty // 3) Try converting from bech32 try { return await CardanoMobile.Address.fromBech32(addr) } catch (_e) {} - // eslint-disable-line no-empty return undefined } @@ -273,10 +268,8 @@ export const CATALYST = { export const toCardanoNetworkId = (networkId: number) => { if (networkId === mainnetId) return 1 - if (networkId === testnetId) return 0 - if (networkId === sanchonetNetworkId) return 0 - throw new Error('invalid network id') + return 0 } export const toSendTokenList = (amounts: Balance.Amounts, primaryToken: Token): Array => { diff --git a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts index 0f17d7c3c1..352d30fb52 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/hooks/index.ts @@ -733,7 +733,7 @@ export const useIsOnline = ( () => true, () => false, ), - refetchInterval: 5000, + refetchInterval: 15000, suspense: true, useErrorBoundary: false, onSuccess: (isOnline) => { diff --git a/apps/wallet-mobile/src/yoroi-wallets/types/yoroi.ts b/apps/wallet-mobile/src/yoroi-wallets/types/yoroi.ts index d7a440fa2b..f74a494fd7 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/types/yoroi.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/types/yoroi.ts @@ -1,5 +1,5 @@ import {Datum} from '@emurgo/yoroi-lib' -import {App, Balance} from '@yoroi/types' +import {App, Balance, Resolver} from '@yoroi/types' import {CardanoTypes, YoroiWallet} from '../cardano/types' import {HWDeviceInfo} from '../hw' @@ -53,7 +53,7 @@ export type YoroiMetadata = { } export type YoroiTarget = { - receiver: string + receiver: Resolver.Receiver entry: YoroiEntry } diff --git a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json index 149a605e77..31f5caf6c9 100644 --- a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Receive", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 319, + "line": 336, "column": 16, - "index": 11577 + "index": 12573 }, "end": { - "line": 322, + "line": 339, "column": 3, - "index": 11666 + "index": 12662 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Swap", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 323, + "line": 340, "column": 13, - "index": 11681 + "index": 12677 }, "end": { - "line": 326, + "line": 343, "column": 3, - "index": 11754 + "index": 12750 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Swap from", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 327, + "line": 344, "column": 17, - "index": 11773 + "index": 12769 }, "end": { - "line": 330, + "line": 347, "column": 3, - "index": 11850 + "index": 12846 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Swap to", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 331, + "line": 348, "column": 15, - "index": 11867 + "index": 12863 }, "end": { - "line": 334, + "line": 351, "column": 3, - "index": 11940 + "index": 12936 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Slippage Tolerance", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 335, + "line": 352, "column": 21, - "index": 11963 + "index": 12959 }, "end": { - "line": 338, + "line": 355, "column": 3, - "index": 12058 + "index": 13054 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Select pool", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 339, + "line": 356, "column": 14, - "index": 12074 + "index": 13070 }, "end": { - "line": 342, + "line": 359, "column": 3, - "index": 12155 + "index": 13151 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Send", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 343, + "line": 360, "column": 13, - "index": 12170 + "index": 13166 }, "end": { - "line": 346, + "line": 363, "column": 3, - "index": 12250 + "index": 13246 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Scan QR code address", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 347, + "line": 364, "column": 18, - "index": 12270 + "index": 13266 }, "end": { - "line": 350, + "line": 367, "column": 3, - "index": 12371 + "index": 13367 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Select asset", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 351, + "line": 368, "column": 20, - "index": 12393 + "index": 13389 }, "end": { - "line": 354, + "line": 371, "column": 3, - "index": 12482 + "index": 13478 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Selected tokens", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 355, + "line": 372, "column": 26, - "index": 12510 + "index": 13506 }, "end": { - "line": 358, + "line": 375, "column": 3, - "index": 12614 + "index": 13610 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Edit amount", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 359, + "line": 376, "column": 19, - "index": 12635 + "index": 13631 }, "end": { - "line": 362, + "line": 379, "column": 3, - "index": 12728 + "index": 13724 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Confirm", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 363, + "line": 380, "column": 16, - "index": 12746 + "index": 13742 }, "end": { - "line": 366, + "line": 383, "column": 3, - "index": 12832 + "index": 13828 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 367, + "line": 384, "column": 19, - "index": 12853 + "index": 13849 }, "end": { - "line": 373, + "line": 390, "column": 3, - "index": 13091 + "index": 14087 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!Confirm transaction", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 374, + "line": 391, "column": 27, - "index": 13120 + "index": 14116 }, "end": { - "line": 377, + "line": 394, "column": 3, - "index": 13213 + "index": 14209 } }, { @@ -214,14 +214,14 @@ "defaultMessage": "!!!Please scan a QR code", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 378, + "line": 395, "column": 13, - "index": 13228 + "index": 14224 }, "end": { - "line": 381, + "line": 398, "column": 3, - "index": 13303 + "index": 14299 } }, { @@ -229,14 +229,14 @@ "defaultMessage": "!!!Success", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 382, + "line": 399, "column": 25, - "index": 13330 + "index": 14326 }, "end": { - "line": 385, + "line": 402, "column": 3, - "index": 13404 + "index": 14400 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/WalletInit/WalletForm.json b/apps/wallet-mobile/translations/messages/src/WalletInit/WalletForm.json index 8e715c6a95..ea6be1579c 100644 --- a/apps/wallet-mobile/translations/messages/src/WalletInit/WalletForm.json +++ b/apps/wallet-mobile/translations/messages/src/WalletInit/WalletForm.json @@ -6,12 +6,12 @@ "start": { "line": 133, "column": 24, - "index": 4974 + "index": 4970 }, "end": { "line": 136, "column": 3, - "index": 5082 + "index": 5078 } }, { @@ -21,12 +21,12 @@ "start": { "line": 137, "column": 20, - "index": 5104 + "index": 5100 }, "end": { "line": 140, "column": 3, - "index": 5214 + "index": 5210 } }, { @@ -36,12 +36,12 @@ "start": { "line": 141, "column": 18, - "index": 5234 + "index": 5230 }, "end": { "line": 144, "column": 3, - "index": 5333 + "index": 5329 } }, { @@ -51,12 +51,12 @@ "start": { "line": 145, "column": 31, - "index": 5366 + "index": 5362 }, "end": { "line": 148, "column": 3, - "index": 5507 + "index": 5503 } }, { @@ -66,12 +66,12 @@ "start": { "line": 149, "column": 28, - "index": 5537 + "index": 5533 }, "end": { "line": 152, "column": 3, - "index": 5653 + "index": 5649 } }, { @@ -81,12 +81,12 @@ "start": { "line": 153, "column": 28, - "index": 5683 + "index": 5679 }, "end": { "line": 156, "column": 3, - "index": 5806 + "index": 5802 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Send/common/strings.json b/apps/wallet-mobile/translations/messages/src/features/Send/common/strings.json index 2e398debbc..783053fceb 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Send/common/strings.json +++ b/apps/wallet-mobile/translations/messages/src/features/Send/common/strings.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Please enter valid amount", "file": "src/features/Send/common/strings.ts", "start": { - "line": 67, + "line": 76, "column": 18, - "index": 4187 + "index": 4880 }, "end": { - "line": 70, + "line": 79, "column": 3, - "index": 4315 + "index": 5008 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Please enter valid amount", "file": "src/features/Send/common/strings.ts", "start": { - "line": 71, + "line": 80, "column": 27, - "index": 4344 + "index": 5037 }, "end": { - "line": 74, + "line": 83, "column": 3, - "index": 4481 + "index": 5174 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Amount too large", "file": "src/features/Send/common/strings.ts", "start": { - "line": 75, + "line": 84, "column": 13, - "index": 4496 + "index": 5189 }, "end": { - "line": 78, + "line": 87, "column": 3, - "index": 4610 + "index": 5303 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Amount is too low", "file": "src/features/Send/common/strings.ts", "start": { - "line": 79, + "line": 88, "column": 11, - "index": 4623 + "index": 5316 }, "end": { - "line": 82, + "line": 91, "column": 3, - "index": 4736 + "index": 5429 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Cannot send less than {minUtxo} {ticker}", "file": "src/features/Send/common/strings.ts", "start": { - "line": 83, + "line": 92, "column": 15, - "index": 4753 + "index": 5446 }, "end": { - "line": 86, + "line": 95, "column": 3, - "index": 4893 + "index": 5586 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Amount must be positive", "file": "src/features/Send/common/strings.ts", "start": { - "line": 87, + "line": 96, "column": 12, - "index": 4907 + "index": 5600 }, "end": { - "line": 90, + "line": 99, "column": 3, - "index": 5027 + "index": 5720 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Not enough money to make this transaction", "file": "src/features/Send/common/strings.ts", "start": { - "line": 91, + "line": 100, "column": 23, - "index": 5052 + "index": 5745 }, "end": { - "line": 94, + "line": 103, "column": 3, - "index": 5201 + "index": 5894 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!!Maximum value of a token inside a UTXO exceeded (overflow).", "file": "src/features/Send/common/strings.ts", "start": { - "line": 95, + "line": 104, "column": 17, - "index": 5220 + "index": 5913 }, "end": { - "line": 98, + "line": 107, "column": 3, - "index": 5382 + "index": 6075 } }, { @@ -124,14 +124,44 @@ "defaultMessage": "!!!Keep some balance for tokens", "file": "src/features/Send/common/strings.ts", "start": { - "line": 99, + "line": 108, "column": 30, - "index": 5414 + "index": 6107 + }, + "end": { + "line": 111, + "column": 3, + "index": 6217 + } + }, + { + "id": "components.send.sendscreen.walletAddress", + "defaultMessage": "!!!Wallet Address", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 115, + "column": 17, + "index": 6281 + }, + "end": { + "line": 118, + "column": 3, + "index": 6379 + } + }, + { + "id": "components.send.sendscreen.receiver", + "defaultMessage": "!!!Receiver", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 119, + "column": 12, + "index": 6393 }, "end": { - "line": 102, + "line": 122, "column": 3, - "index": 5524 + "index": 6480 } }, { @@ -139,14 +169,14 @@ "defaultMessage": "!!!Fee", "file": "src/features/Send/common/strings.ts", "start": { - "line": 106, + "line": 123, "column": 12, - "index": 5583 + "index": 6494 }, "end": { - "line": 109, + "line": 126, "column": 3, - "index": 5665 + "index": 6576 } }, { @@ -154,14 +184,14 @@ "defaultMessage": "!!!-", "file": "src/features/Send/common/strings.ts", "start": { - "line": 110, + "line": 127, "column": 19, - "index": 5686 + "index": 6597 }, "end": { - "line": 113, + "line": 130, "column": 3, - "index": 5773 + "index": 6684 } }, { @@ -169,14 +199,14 @@ "defaultMessage": "!!!Balance after", "file": "src/features/Send/common/strings.ts", "start": { - "line": 114, + "line": 131, "column": 21, - "index": 5796 + "index": 6707 }, "end": { - "line": 117, + "line": 134, "column": 3, - "index": 5883 + "index": 6794 } }, { @@ -184,14 +214,14 @@ "defaultMessage": "!!!-", "file": "src/features/Send/common/strings.ts", "start": { - "line": 118, + "line": 135, "column": 28, - "index": 5913 + "index": 6824 }, "end": { - "line": 121, + "line": 138, "column": 3, - "index": 6009 + "index": 6920 } }, { @@ -199,14 +229,14 @@ "defaultMessage": "!!!Checking balance...", "file": "src/features/Send/common/strings.ts", "start": { - "line": 122, + "line": 139, "column": 34, - "index": 6045 + "index": 6956 }, "end": { - "line": 125, + "line": 142, "column": 3, - "index": 6165 + "index": 7076 } }, { @@ -214,14 +244,14 @@ "defaultMessage": "!!!-", "file": "src/features/Send/common/strings.ts", "start": { - "line": 126, + "line": 143, "column": 36, - "index": 6203 + "index": 7114 }, "end": { - "line": 129, + "line": 146, "column": 3, - "index": 6307 + "index": 7218 } }, { @@ -229,29 +259,44 @@ "defaultMessage": "!!!Please enter valid address", "file": "src/features/Send/common/strings.ts", "start": { - "line": 130, + "line": 147, "column": 35, - "index": 6344 + "index": 7255 + }, + "end": { + "line": 150, + "column": 3, + "index": 7383 + } + }, + { + "id": "components.send.sendscreen.addressInputErrorInvalidDomain", + "defaultMessage": "!!!Please enter valid domain", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 151, + "column": 34, + "index": 7419 }, "end": { - "line": 133, + "line": 154, "column": 3, - "index": 6472 + "index": 7545 } }, { "id": "components.send.confirmscreen.receiver", - "defaultMessage": "!!!Address", + "defaultMessage": "!!!Receiver address, ADA Handle or domains", "file": "src/features/Send/common/strings.ts", "start": { - "line": 134, + "line": 155, "column": 21, - "index": 6495 + "index": 7568 }, "end": { - "line": 137, + "line": 158, "column": 3, - "index": 6584 + "index": 7689 } }, { @@ -259,14 +304,14 @@ "defaultMessage": "!!!Send all assets (including all tokens)", "file": "src/features/Send/common/strings.ts", "start": { - "line": 138, + "line": 159, "column": 25, - "index": 6611 + "index": 7716 }, "end": { - "line": 141, + "line": 162, "column": 3, - "index": 6741 + "index": 7846 } }, { @@ -274,14 +319,14 @@ "defaultMessage": "!!!Send all {assetId}", "file": "src/features/Send/common/strings.ts", "start": { - "line": 142, + "line": 163, "column": 19, - "index": 6762 + "index": 7867 }, "end": { - "line": 145, + "line": 166, "column": 3, - "index": 6866 + "index": 7971 } }, { @@ -290,14 +335,14 @@ "defaultMessage": "!!!Domain is not registered", "file": "src/features/Send/common/strings.ts", "start": { - "line": 146, + "line": 167, "column": 28, - "index": 6896 + "index": 8001 }, "end": { - "line": 150, + "line": 171, "column": 3, - "index": 7045 + "index": 8150 } }, { @@ -306,14 +351,14 @@ "defaultMessage": "!!!No Cardano record found for this domain", "file": "src/features/Send/common/strings.ts", "start": { - "line": 151, + "line": 172, "column": 29, - "index": 7076 + "index": 8181 }, "end": { - "line": 155, + "line": 176, "column": 3, - "index": 7241 + "index": 8346 } }, { @@ -322,29 +367,14 @@ "defaultMessage": "!!!Domain is not supported", "file": "src/features/Send/common/strings.ts", "start": { - "line": 156, + "line": 177, "column": 26, - "index": 7269 - }, - "end": { - "line": 160, - "column": 3, - "index": 7415 - } - }, - { - "id": "components.send.sendscreen.resolvesTo", - "defaultMessage": "!!!Resolves to", - "file": "src/features/Send/common/strings.ts", - "start": { - "line": 161, - "column": 14, - "index": 7431 + "index": 8374 }, "end": { - "line": 164, + "line": 181, "column": 3, - "index": 7523 + "index": 8520 } }, { @@ -352,14 +382,14 @@ "defaultMessage": "!!!Search tokens", "file": "src/features/Send/common/strings.ts", "start": { - "line": 165, + "line": 182, "column": 16, - "index": 7541 + "index": 8538 }, "end": { - "line": 168, + "line": 185, "column": 3, - "index": 7637 + "index": 8634 } }, { @@ -367,14 +397,14 @@ "defaultMessage": "!!!Select asset", "file": "src/features/Send/common/strings.ts", "start": { - "line": 169, + "line": 186, "column": 20, - "index": 7659 + "index": 8656 }, "end": { - "line": 172, + "line": 189, "column": 3, - "index": 7748 + "index": 8745 } }, { @@ -382,14 +412,14 @@ "defaultMessage": "!!!Unknown asset", "file": "src/features/Send/common/strings.ts", "start": { - "line": 173, + "line": 190, "column": 16, - "index": 7766 + "index": 8763 }, "end": { - "line": 176, + "line": 193, "column": 3, - "index": 7871 + "index": 8868 } }, { @@ -397,14 +427,14 @@ "defaultMessage": "!!!No assets found", "file": "src/features/Send/common/strings.ts", "start": { - "line": 177, + "line": 194, "column": 12, - "index": 7885 + "index": 8882 }, "end": { - "line": 180, + "line": 197, "column": 3, - "index": 7988 + "index": 8985 } }, { @@ -412,14 +442,14 @@ "defaultMessage": "!!!found", "file": "src/features/Send/common/strings.ts", "start": { - "line": 181, + "line": 198, "column": 9, - "index": 7999 + "index": 8996 }, "end": { - "line": 184, + "line": 201, "column": 3, - "index": 8089 + "index": 9086 } }, { @@ -427,14 +457,14 @@ "defaultMessage": "!!!You have", "file": "src/features/Send/common/strings.ts", "start": { - "line": 185, + "line": 202, "column": 11, - "index": 8102 + "index": 9099 }, "end": { - "line": 188, + "line": 205, "column": 3, - "index": 8197 + "index": 9194 } }, { @@ -442,14 +472,14 @@ "defaultMessage": "!!!No {fungible} added yet", "file": "src/features/Send/common/strings.ts", "start": { - "line": 189, + "line": 206, "column": 20, - "index": 8219 + "index": 9216 }, "end": { - "line": 192, + "line": 209, "column": 3, - "index": 8338 + "index": 9335 } }, { @@ -457,14 +487,14 @@ "defaultMessage": "!!!Do you really want to send all?", "file": "src/features/Send/common/strings.ts", "start": { - "line": 193, + "line": 210, "column": 23, - "index": 8363 + "index": 9360 }, "end": { - "line": 196, + "line": 213, "column": 3, - "index": 8484 + "index": 9481 } }, { @@ -472,14 +502,14 @@ "defaultMessage": "!!!You have selected the send all option. Please confirm that you understand how this feature works.", "file": "src/features/Send/common/strings.ts", "start": { - "line": 197, + "line": 214, "column": 22, - "index": 8508 + "index": 9505 }, "end": { - "line": 201, + "line": 218, "column": 3, - "index": 8700 + "index": 9697 } }, { @@ -487,14 +517,14 @@ "defaultMessage": "!!!All you {assetNameOrId} balance will be transferred in this transaction.", "file": "src/features/Send/common/strings.ts", "start": { - "line": 202, + "line": 219, "column": 24, - "index": 8726 + "index": 9723 }, "end": { - "line": 205, + "line": 222, "column": 3, - "index": 8889 + "index": 9886 } }, { @@ -502,14 +532,14 @@ "defaultMessage": "!!!All your tokens, including NFTs and any other native assets in your wallet, will also be transferred in this transaction.", "file": "src/features/Send/common/strings.ts", "start": { - "line": 206, + "line": 223, "column": 24, - "index": 8915 + "index": 9912 }, "end": { - "line": 211, + "line": 228, "column": 3, - "index": 9144 + "index": 10141 } }, { @@ -517,14 +547,14 @@ "defaultMessage": "!!!After you confirm the transaction in the next screen, your wallet will be emptied.", "file": "src/features/Send/common/strings.ts", "start": { - "line": 212, + "line": 229, "column": 24, - "index": 9170 + "index": 10167 }, "end": { - "line": 215, + "line": 232, "column": 3, - "index": 9343 + "index": 10340 } }, { @@ -532,14 +562,14 @@ "defaultMessage": "!!!Continue", "file": "src/features/Send/common/strings.ts", "start": { - "line": 216, + "line": 233, "column": 18, - "index": 9363 + "index": 10360 }, "end": { - "line": 219, + "line": 236, "column": 3, - "index": 9456 + "index": 10453 } }, { @@ -547,14 +577,14 @@ "defaultMessage": "!!!We are experiencing issues with fetching your current balance. Click to retry.", "file": "src/features/Send/common/strings.ts", "start": { - "line": 220, + "line": 237, "column": 27, - "index": 9485 + "index": 10482 }, "end": { - "line": 223, + "line": 240, "column": 3, - "index": 9657 + "index": 10654 } }, { @@ -562,14 +592,14 @@ "defaultMessage": "!!!You cannot send a new transaction while an existing one is still pending", "file": "src/features/Send/common/strings.ts", "start": { - "line": 224, + "line": 241, "column": 41, - "index": 9700 + "index": 10697 }, "end": { - "line": 227, + "line": 244, "column": 3, - "index": 9880 + "index": 10877 } }, { @@ -577,14 +607,14 @@ "defaultMessage": "!!!Transaction submitted", "file": "src/features/Send/common/strings.ts", "start": { - "line": 228, + "line": 245, "column": 20, - "index": 9902 + "index": 10899 }, "end": { - "line": 231, + "line": 248, "column": 3, - "index": 10010 + "index": 11007 } }, { @@ -592,14 +622,14 @@ "defaultMessage": "!!!Check this transaction in the list of wallet transactions", "file": "src/features/Send/common/strings.ts", "start": { - "line": 232, + "line": 249, "column": 19, - "index": 10031 + "index": 11028 }, "end": { - "line": 235, + "line": 252, "column": 3, - "index": 10174 + "index": 11171 } }, { @@ -607,14 +637,14 @@ "defaultMessage": "!!!Go to transactions", "file": "src/features/Send/common/strings.ts", "start": { - "line": 236, + "line": 253, "column": 21, - "index": 10197 + "index": 11194 }, "end": { - "line": 239, + "line": 256, "column": 3, - "index": 10303 + "index": 11300 } }, { @@ -622,14 +652,14 @@ "defaultMessage": "!!!Transaction failed", "file": "src/features/Send/common/strings.ts", "start": { - "line": 240, + "line": 257, "column": 17, - "index": 10322 + "index": 11319 }, "end": { - "line": 243, + "line": 260, "column": 3, - "index": 10424 + "index": 11421 } }, { @@ -637,14 +667,14 @@ "defaultMessage": "!!!Your transaction has not been processed properly due to technical issues", "file": "src/features/Send/common/strings.ts", "start": { - "line": 244, + "line": 261, "column": 16, - "index": 10442 + "index": 11439 }, "end": { - "line": 247, + "line": 264, "column": 3, - "index": 10597 + "index": 11594 } }, { @@ -652,14 +682,14 @@ "defaultMessage": "!!!Try again", "file": "src/features/Send/common/strings.ts", "start": { - "line": 248, + "line": 265, "column": 18, - "index": 10617 + "index": 11614 }, "end": { - "line": 251, + "line": 268, "column": 3, - "index": 10711 + "index": 11708 } }, { @@ -667,14 +697,14 @@ "defaultMessage": "!!!Asset", "file": "src/features/Send/common/strings.ts", "start": { - "line": 252, + "line": 269, "column": 9, - "index": 10722 + "index": 11719 }, "end": { - "line": 255, + "line": 272, "column": 3, - "index": 10795 + "index": 11792 } }, { @@ -682,14 +712,149 @@ "defaultMessage": "!!!Scan recipients QR code to add a wallet address", "file": "src/features/Send/common/strings.ts", "start": { - "line": 256, + "line": 273, + "column": 23, + "index": 11817 + }, + "end": { + "line": 276, + "column": 3, + "index": 11944 + } + }, + { + "id": "components.send.sendscreen.resolvedAddress", + "defaultMessage": "!!!Related Address", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 277, + "column": 19, + "index": 11965 + }, + "end": { + "line": 280, + "column": 3, + "index": 12066 + } + }, + { + "id": "components.send.sendscreen.resolverNoticeTitle", + "defaultMessage": "!!!Yoroi Supports Name Resolution", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 281, "column": 23, - "index": 10820 + "index": 12091 + }, + "end": { + "line": 284, + "column": 3, + "index": 12211 + } + }, + { + "id": "send.warning.resolver.manyNameServers", + "defaultMessage": "!!!Multiple name servers found. Please select one.", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 285, + "column": 26, + "index": 12239 + }, + "end": { + "line": 288, + "column": 3, + "index": 12367 + } + }, + { + "id": "send.helper.addressError.invalid", + "defaultMessage": "!!!Please enter valid address", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 289, + "column": 29, + "index": 12398 + }, + "end": { + "line": 292, + "column": 3, + "index": 12500 + } + }, + { + "id": "send.helper.addressError.wrongNetwork", + "defaultMessage": "!!!Please enter valid domain", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 293, + "column": 34, + "index": 12536 + }, + "end": { + "line": 296, + "column": 3, + "index": 12642 + } + }, + { + "id": "send.helper.resolverError.unsupportedDomain", + "defaultMessage": "!!!Domain is not supported", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 297, + "column": 40, + "index": 12684 + }, + "end": { + "line": 300, + "column": 3, + "index": 12794 + } + }, + { + "id": "components.send.memofield.label", + "defaultMessage": "!!!Memo", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 301, + "column": 13, + "index": 12809 + }, + "end": { + "line": 304, + "column": 3, + "index": 12888 + } + }, + { + "id": "components.send.memofield.message", + "defaultMessage": "!!!(Optional) Memo is stored locally", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 305, + "column": 26, + "index": 12916 + }, + "end": { + "line": 308, + "column": 3, + "index": 13026 + } + }, + { + "id": "components.send.memofield.error", + "defaultMessage": "!!!Memo is too long", + "file": "src/features/Send/common/strings.ts", + "start": { + "line": 309, + "column": 26, + "index": 13054 }, "end": { - "line": 259, + "line": 312, "column": 3, - "index": 10947 + "index": 13145 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.json b/apps/wallet-mobile/translations/messages/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.json index 9c10e00d00..a7b4a45e5e 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.json +++ b/apps/wallet-mobile/translations/messages/src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Resolves to", "file": "src/features/Send/useCases/ConfirmTx/Summary/ReceiverInfo.tsx", "start": { - "line": 35, + "line": 55, "column": 14, - "index": 772 + "index": 1453 }, "end": { - "line": 38, + "line": 58, "column": 3, - "index": 864 + "index": 1545 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Settings/ChangePassword/ChangePasswordScreen.json b/apps/wallet-mobile/translations/messages/src/features/Settings/ChangePassword/ChangePasswordScreen.json index aa01c74a46..66710ee09c 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Settings/ChangePassword/ChangePasswordScreen.json +++ b/apps/wallet-mobile/translations/messages/src/features/Settings/ChangePassword/ChangePasswordScreen.json @@ -6,12 +6,12 @@ "start": { "line": 111, "column": 25, - "index": 4319 + "index": 4315 }, "end": { "line": 114, "column": 3, - "index": 4441 + "index": 4437 } }, { @@ -21,12 +21,12 @@ "start": { "line": 115, "column": 25, - "index": 4468 + "index": 4464 }, "end": { "line": 118, "column": 3, - "index": 4586 + "index": 4582 } }, { @@ -36,12 +36,12 @@ "start": { "line": 119, "column": 31, - "index": 4619 + "index": 4615 }, "end": { "line": 122, "column": 3, - "index": 4784 + "index": 4780 } }, { @@ -51,12 +51,12 @@ "start": { "line": 123, "column": 28, - "index": 4814 + "index": 4810 }, "end": { "line": 126, "column": 3, - "index": 4942 + "index": 4938 } }, { @@ -66,12 +66,12 @@ "start": { "line": 127, "column": 36, - "index": 4980 + "index": 4976 }, "end": { "line": 130, "column": 3, - "index": 5119 + "index": 5115 } }, { @@ -81,12 +81,12 @@ "start": { "line": 131, "column": 18, - "index": 5139 + "index": 5135 }, "end": { "line": 134, "column": 3, - "index": 5253 + "index": 5249 } } ] \ No newline at end of file diff --git a/apps/wallet-mobile/translations/messages/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.json b/apps/wallet-mobile/translations/messages/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.json index f4e0b49697..3773ba2a2a 100644 --- a/apps/wallet-mobile/translations/messages/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.json +++ b/apps/wallet-mobile/translations/messages/src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Resolves to", "file": "src/features/Settings/ManageCollateral/ConfirmTx/Summary/ReceiverInfo.tsx", "start": { - "line": 35, + "line": 55, "column": 14, - "index": 772 + "index": 1485 }, "end": { - "line": 38, + "line": 58, "column": 3, - "index": 864 + "index": 1577 } } ] \ No newline at end of file diff --git a/metro.config.js b/metro.config.js index 3f61ef73a9..596cfdaeaa 100644 --- a/metro.config.js +++ b/metro.config.js @@ -5,11 +5,11 @@ module.exports = { watchFolders: [ path.resolve(__dirname, "node_modules"), path.resolve(__dirname, "packages/types"), - path.resolve(__dirname, "packages/banxa"), - path.resolve(__dirname, "packages/links"), path.resolve(__dirname, "packages/common"), - path.resolve(__dirname, "packages/links"), path.resolve(__dirname, "packages/api"), + path.resolve(__dirname, "packages/banxa"), + path.resolve(__dirname, "packages/links"), + path.resolve(__dirname, "packages/resolver"), path.resolve(__dirname, "packages/openswap"), path.resolve(__dirname, "packages/swap"), path.resolve(__dirname, "packages/staking"), diff --git a/package.json b/package.json index 1386c04e9f..74b7792a81 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,9 @@ "workspaces": { "packages": [ "packages/types", - "packages/openswap", "packages/common", - "packages/links", "packages/api", + "packages/openswap", "packages/*", "apps/*", "e2e/*" diff --git a/packages/common/src/api/fetchData.ts b/packages/common/src/api/fetchData.ts index 614b63bb29..a225d29cc1 100644 --- a/packages/common/src/api/fetchData.ts +++ b/packages/common/src/api/fetchData.ts @@ -14,9 +14,10 @@ type OtherRequestConfig = { headers?: Record } -type RequestConfig = GetRequestConfig | OtherRequestConfig +export type RequestConfig = GetRequestConfig | OtherRequestConfig export type FetchData = ( config: RequestConfig, + fetcherConfig?: AxiosRequestConfig, ) => Promise> /** @@ -65,11 +66,13 @@ export type FetchData = ( */ export const fetchData: FetchData = ( config: RequestConfig, + fetcherConfig?: AxiosRequestConfig, ): Promise> => { const method = config.method ?? 'get' const isNotGet = method !== 'get' - const axiosConfig: AxiosRequestConfig = { + const axiosConfig: AxiosRequestConfig = { + ...fetcherConfig, url: config.url, method: method, headers: config.headers ?? {'Content-Type': 'application/json'}, diff --git a/packages/resolver/.dependency-cruiser.js b/packages/resolver/.dependency-cruiser.js new file mode 100644 index 0000000000..fff414129a --- /dev/null +++ b/packages/resolver/.dependency-cruiser.js @@ -0,0 +1,449 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + /* rules from the 'recommended' preset: */ + { + name: 'fix-circular', + severity: 'error', + comment: + 'This dependency is part of a circular relationship. You might want to revise ' + + 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', + from: {}, + to: { + circular: true + } + }, + { + name: 'no-orphans', + comment: + "This is an orphan module - it's likely not used (anymore?). Either use it or " + + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " + + "add an exception for it in your dependency-cruiser configuration. By default " + + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + + "files (.d.ts), tsconfig.json and some of the babel and webpack configs.", + severity: 'warn', + from: { + orphan: true, + pathNot: [ + '(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files + '\\.d\\.ts$', // TypeScript declaration files + '(^|/)tsconfig\\.json$', // TypeScript config + '(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$' // other configs + ] + }, + to: {}, + }, + { + name: 'no-deprecated-core', + comment: + 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + + "bound to exist - node doesn't deprecate lightly.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'core' + ], + path: [ + '^(v8\/tools\/codemap)$', + '^(v8\/tools\/consarray)$', + '^(v8\/tools\/csvparser)$', + '^(v8\/tools\/logreader)$', + '^(v8\/tools\/profile_view)$', + '^(v8\/tools\/profile)$', + '^(v8\/tools\/SourceMap)$', + '^(v8\/tools\/splaytree)$', + '^(v8\/tools\/tickprocessor-driver)$', + '^(v8\/tools\/tickprocessor)$', + '^(node-inspect\/lib\/_inspect)$', + '^(node-inspect\/lib\/internal\/inspect_client)$', + '^(node-inspect\/lib\/internal\/inspect_repl)$', + '^(async_hooks)$', + '^(punycode)$', + '^(domain)$', + '^(constants)$', + '^(sys)$', + '^(_linklist)$', + '^(_stream_wrap)$' + ], + } + }, + { + name: 'not-to-deprecated', + comment: + 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + + 'version of that module, or find an alternative. Deprecated modules are a security risk.', + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'deprecated' + ] + } + }, + { + name: 'no-non-package-json', + severity: 'error', + comment: + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " + + "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " + + "in your package.json.", + from: {}, + to: { + dependencyTypes: [ + 'npm-no-pkg', + 'npm-unknown' + ] + } + }, + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + + 'module: add it to your package.json. In all other cases you likely already know what to do.', + severity: 'error', + from: {}, + to: { + couldNotResolve: true + } + }, + { + name: 'no-duplicate-dep-types', + comment: + "Likely this module depends on an external ('npm') package that occurs more than once " + + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + + "maintenance problems later on.", + severity: 'warn', + from: {}, + to: { + moreThanOneDependencyType: true, + // as it's pretty common to have a type import be a type only import + // _and_ (e.g.) a devDependency - don't consider type-only dependency + // types for this rule + dependencyTypesNot: ["type-only"] + } + }, + + /* rules you might want to tweak for your specific situation: */ + { + name: 'not-to-spec', + comment: + 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + + "If there's something in a spec that's of use to other modules, it doesn't have that single " + + 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', + severity: 'error', + from: {}, + to: { + path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$' + } + }, + { + name: 'not-to-dev-dep', + severity: 'error', + comment: + "This module depends on an npm package from the 'devDependencies' section of your " + + 'package.json. It looks like something that ships to production, though. To prevent problems ' + + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + + 'section of your package.json. If this module is development only - add it to the ' + + 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', + from: { + path: '^(src)', + pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$' + }, + to: { + dependencyTypes: [ + 'npm-dev' + ] + } + }, + { + name: 'optional-deps-used', + severity: 'info', + comment: + "This module depends on an npm package that is declared as an optional dependency " + + "in your package.json. As this makes sense in limited situations only, it's flagged here. " + + "If you're using an optional dependency here by design - add an exception to your" + + "dependency-cruiser configuration.", + from: {}, + to: { + dependencyTypes: [ + 'npm-optional' + ] + } + }, + { + name: 'peer-deps-used', + comment: + "This module depends on an npm package that is declared as a peer dependency " + + "in your package.json. This makes sense if your package is e.g. a plugin, but in " + + "other cases - maybe not so much. If the use of a peer dependency is intentional " + + "add an exception to your dependency-cruiser configuration.", + severity: 'warn', + from: {}, + to: { + dependencyTypes: [ + 'npm-peer' + ] + } + } + ], + options: { + + /* conditions specifying which files not to follow further when encountered: + - path: a regular expression to match + - dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/master/doc/rules-reference.md#dependencytypes-and-dependencytypesnot + for a complete list + */ + doNotFollow: { + path: 'node_modules' + }, + + /* conditions specifying which dependencies to exclude + - path: a regular expression to match + - dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies. + leave out if you want to exclude neither (recommended!) + */ + // exclude : { + // path: '', + // dynamic: true + // }, + + /* pattern specifying which files to include (regular expression) + dependency-cruiser will skip everything not matching this pattern + */ + // includeOnly : '', + + /* dependency-cruiser will include modules matching against the focus + regular expression in its output, as well as their neighbours (direct + dependencies and dependents) + */ + // focus : '', + + /* list of module systems to cruise */ + // moduleSystems: ['amd', 'cjs', 'es6', 'tsd'], + + /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/' + to open it on your online repo or `vscode://file/${process.cwd()}/` to + open it in visual studio code), + */ + // prefix: '', + + /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation + true: also detect dependencies that only exist before typescript-to-javascript compilation + "specify": for each dependency identify whether it only exists before compilation or also after + */ + tsPreCompilationDeps: true, + + /* + list of extensions to scan that aren't javascript or compile-to-javascript. + Empty by default. Only put extensions in here that you want to take into + account that are _not_ parsable. + */ + // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"], + + /* if true combines the package.jsons found from the module up to the base + folder the cruise is initiated from. Useful for how (some) mono-repos + manage dependencies & dependency definitions. + */ + // combinedDependencies: false, + + /* if true leave symlinks untouched, otherwise use the realpath */ + // preserveSymlinks: false, + + /* TypeScript project file ('tsconfig.json') to use for + (1) compilation and + (2) resolution (e.g. with the paths property) + + The (optional) fileName attribute specifies which file to take (relative to + dependency-cruiser's current working directory). When not provided + defaults to './tsconfig.json'. + */ + tsConfig: { + fileName: 'tsconfig.json' + }, + + /* Webpack configuration to use to get resolve options from. + + The (optional) fileName attribute specifies which file to take (relative + to dependency-cruiser's current working directory. When not provided defaults + to './webpack.conf.js'. + + The (optional) `env` and `args` attributes contain the parameters to be passed if + your webpack config is a function and takes them (see webpack documentation + for details) + */ + // webpackConfig: { + // fileName: './webpack.config.js', + // env: {}, + // args: {}, + // }, + + /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use + for compilation (and whatever other naughty things babel plugins do to + source code). This feature is well tested and usable, but might change + behavior a bit over time (e.g. more precise results for used module + systems) without dependency-cruiser getting a major version bump. + */ + // babelConfig: { + // fileName: './.babelrc' + // }, + + /* List of strings you have in use in addition to cjs/ es6 requires + & imports to declare module dependencies. Use this e.g. if you've + re-declared require, use a require-wrapper or use window.require as + a hack. + */ + // exoticRequireStrings: [], + /* options to pass on to enhanced-resolve, the package dependency-cruiser + uses to resolve module references to disk. You can set most of these + options in a webpack.conf.js - this section is here for those + projects that don't have a separate webpack config file. + + Note: settings in webpack.conf.js override the ones specified here. + */ + enhancedResolveOptions: { + /* List of strings to consider as 'exports' fields in package.json. Use + ['exports'] when you use packages that use such a field and your environment + supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack). + + If you have an `exportsFields` attribute in your webpack config, that one + will have precedence over the one specified here. + */ + exportsFields: ["exports"], + /* List of conditions to check for in the exports field. e.g. use ['imports'] + if you're only interested in exposed es6 modules, ['require'] for commonjs, + or all conditions at once `(['import', 'require', 'node', 'default']`) + if anything goes for you. Only works when the 'exportsFields' array is + non-empty. + + If you have a 'conditionNames' attribute in your webpack config, that one will + have precedence over the one specified here. + */ + conditionNames: ["import", "require", "node", "default"], + /* + The extensions, by default are the same as the ones dependency-cruiser + can access (run `npx depcruise --info` to see which ones that are in + _your_ environment. If that list is larger than what you need (e.g. + it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use + TypeScript you can pass just the extensions you actually use (e.g. + [".js", ".jsx"]). This can speed up the most expensive step in + dependency cruising (module resolution) quite a bit. + */ + // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"], + /* + If your TypeScript project makes use of types specified in 'types' + fields in package.jsons of external dependencies, specify "types" + in addition to "main" in here, so enhanced-resolve (the resolver + dependency-cruiser uses) knows to also look there. You can also do + this if you're not sure, but still use TypeScript. In a future version + of dependency-cruiser this will likely become the default. + */ + mainFields: ["main", "types"], + }, + reporterOptions: { + dot: { + /* pattern of modules that can be consolidated in the detailed + graphical dependency graph. The default pattern in this configuration + collapses everything in node_modules to one folder deep so you see + the external modules, but not the innards your app depends upon. + */ + collapsePattern: 'node_modules/[^/]+', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + don't worry - dependency-cruiser will fall back to the default one. + */ + // theme: { + // graph: { + // /* use splines: "ortho" for straight lines. Be aware though + // graphviz might take a long time calculating ortho(gonal) + // routings. + // */ + // splines: "true" + // }, + // modules: [ + // { + // criteria: { matchesFocus: true }, + // attributes: { + // fillcolor: "lime", + // penwidth: 2, + // }, + // }, + // { + // criteria: { matchesFocus: false }, + // attributes: { + // fillcolor: "lightgrey", + // }, + // }, + // { + // criteria: { matchesReaches: true }, + // attributes: { + // fillcolor: "lime", + // penwidth: 2, + // }, + // }, + // { + // criteria: { matchesReaches: false }, + // attributes: { + // fillcolor: "lightgrey", + // }, + // }, + // { + // criteria: { source: "^src/model" }, + // attributes: { fillcolor: "#ccccff" } + // }, + // { + // criteria: { source: "^src/view" }, + // attributes: { fillcolor: "#ccffcc" } + // }, + // ], + // dependencies: [ + // { + // criteria: { "rules[0].severity": "error" }, + // attributes: { fontcolor: "red", color: "red" } + // }, + // { + // criteria: { "rules[0].severity": "warn" }, + // attributes: { fontcolor: "orange", color: "orange" } + // }, + // { + // criteria: { "rules[0].severity": "info" }, + // attributes: { fontcolor: "blue", color: "blue" } + // }, + // { + // criteria: { resolved: "^src/model" }, + // attributes: { color: "#0000ff77" } + // }, + // { + // criteria: { resolved: "^src/view" }, + // attributes: { color: "#00770077" } + // } + // ] + // } + }, + archi: { + /* pattern of modules that can be consolidated in the high level + graphical dependency graph. If you use the high level graphical + dependency graph reporter (`archi`) you probably want to tweak + this collapsePattern to your situation. + */ + collapsePattern: '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/[^/]+', + + /* Options to tweak the appearance of your graph.See + https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions + for details and some examples. If you don't specify a theme + for 'archi' dependency-cruiser will use the one specified in the + dot section (see above), if any, and otherwise use the default one. + */ + // theme: { + // }, + }, + "text": { + "highlightFocused": true + }, + } + } +}; +// generated: dependency-cruiser@12.10.0 on 2023-03-08T01:53:10.874Z \ No newline at end of file diff --git a/packages/resolver/.gitignore b/packages/resolver/.gitignore new file mode 100644 index 0000000000..75356714f9 --- /dev/null +++ b/packages/resolver/.gitignore @@ -0,0 +1,70 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Expo +.expo/ + +# Turborepo +.turbo/ + +# generated by bob +lib/ diff --git a/packages/resolver/README.md b/packages/resolver/README.md new file mode 100644 index 0000000000..711956b34e --- /dev/null +++ b/packages/resolver/README.md @@ -0,0 +1,5 @@ +# Yoroi CNS Module + +## Installation + +## Usage diff --git a/packages/resolver/babel.config.js b/packages/resolver/babel.config.js new file mode 100644 index 0000000000..cf1f9fbbc1 --- /dev/null +++ b/packages/resolver/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +} diff --git a/packages/resolver/jest.setup.js b/packages/resolver/jest.setup.js new file mode 100644 index 0000000000..10deefd07a --- /dev/null +++ b/packages/resolver/jest.setup.js @@ -0,0 +1,5 @@ +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock'), +) + +jest.setTimeout(30000) \ No newline at end of file diff --git a/packages/resolver/package.json b/packages/resolver/package.json new file mode 100644 index 0000000000..c5db5476bc --- /dev/null +++ b/packages/resolver/package.json @@ -0,0 +1,216 @@ +{ + "name": "@yoroi/resolver", + "version": "1.0.0", + "description": "Yoroi domain resolver", + "keywords": [ + "yoroi", + "cardano", + "resolver", + "browser", + "react", + "react-native" + ], + "homepage": "https://github.com/Emurgo/yoroi/packages/resolver#readme", + "bugs": { + "url": "https://github.com/Emurgo/yoroi/issues" + }, + "repository": { + "type": "github", + "url": "https://github.com/Emurgo/yoroi.git", + "directory": "packages/resolver" + }, + "license": "Apache-2.0", + "author": "EMURGO Fintech (https://github.com/Emurgo/yoroi)", + "contributors": [ + { + "name": "Juliano Lazzarotto", + "email": "30806844+stackchain@users.noreply.github.com" + }, + { + "name": "Javier Bueno", + "email": "105349292+banklesss@users.noreply.github.com" + } + ], + "main": "lib/commonjs/index", + "module": "lib/module/index", + "source": "src/index", + "browser": "lib/module/index", + "types": "lib/typescript/index.d.ts", + "files": [ + "src", + "lib", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "build": "yarn tsc && yarn lint && yarn clean && bob build", + "build:release": "yarn build && yarn flow", + "clean": "del-cli lib", + "dgraph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg", + "flow": ". ./scripts/flowgen.sh", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "prepack": "yarn build:release", + "prepublish:beta": "yarn build:release", + "publish:beta": "npm publish --scope yoroi --tag beta --access beta", + "prepublish:prod": "yarn build:release", + "publish:prod": "npm publish --scope yoroi --access public", + "release": "release-it", + "test": "jest", + "test:watch": "jest --watch", + "tsc": "tsc --noEmit -p tsconfig.json" + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "prettier": { + "bracketSpacing": false, + "quoteProps": "consistent", + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false + }, + "eslintConfig": { + "extends": [ + "@react-native-community", + "prettier" + ], + "rules": { + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "bracketSpacing": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false, + "semi": false + } + ] + }, + "root": true + }, + "eslintIgnore": [ + "node_modules/", + "lib/", + "jest.setup.js", + "coverage/" + ], + "jest": { + "collectCoverage": true, + "collectCoverageFrom": [ + "src/**/*.{js,jsx,ts,tsx}", + "!src/**/*.mocks.{js,jsx,ts,tsx}", + "!src/**/*.d.ts" + ], + "coverageReporters": [ + "text-summary", + "html" + ], + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/lib/" + ], + "preset": "react-native", + "setupFiles": [ + "/jest.setup.js" + ] + }, + "dependencies": { + "@yoroi/common": "1.3.0", + "axios": "^1.5.0", + "zod": "^3.22.2" + }, + "devDependencies": { + "@commitlint/config-conventional": "^17.0.2", + "@react-native-async-storage/async-storage": "^1.19.3", + "@react-native-community/eslint-config": "^3.0.2", + "@release-it/conventional-changelog": "^5.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react-native": "^12.3.0", + "@types/jest": "^28.1.2", + "@yoroi/types": "1.3.0", + "commitlint": "^17.0.2", + "del-cli": "^5.0.0", + "dependency-cruiser": "^13.1.1", + "eslint": "^8.4.1", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-ft-flow": "^3.0.0", + "eslint-plugin-prettier": "^4.0.0", + "flowgen": "^1.21.0", + "jest": "^28.1.1", + "metro-react-native-babel-preset": "0.73.9", + "prettier": "^2.0.5", + "react": "18.2.0", + "react-native": "~0.71.0", + "react-native-builder-bob": "^0.20.4", + "react-query": "^3.39.3", + "react-test-renderer": "^18.2.0", + "release-it": "^15.0.0", + "typescript": "4.8.4" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": ">= 1.19.3 <= 1.20.0", + "react": ">= 16.8.0 <= 19.0.0", + "react-query": "^3.39.3" + }, + "packageManager": "yarn@1.22.21", + "engines": { + "node": ">= 16.19.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + [ + "typescript", + { + "project": "tsconfig.build.json", + "tsc": "./node_modules/.bin/tsc" + } + ] + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": false + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular" + } + } + } +} diff --git a/packages/resolver/scripts/flowgen.sh b/packages/resolver/scripts/flowgen.sh new file mode 100644 index 0000000000..2638ef8d27 --- /dev/null +++ b/packages/resolver/scripts/flowgen.sh @@ -0,0 +1,3 @@ +for i in $(find lib -type f -name "*.d.ts"); + do sh -c "npx flowgen $i -o ${i%.*.*}.js.flow"; +done; diff --git a/packages/resolver/src/adapters/api.mocks.ts b/packages/resolver/src/adapters/api.mocks.ts new file mode 100644 index 0000000000..106b6d18f4 --- /dev/null +++ b/packages/resolver/src/adapters/api.mocks.ts @@ -0,0 +1,25 @@ +const getCardanoAddresses = { + success: () => + Promise.resolve([ + {address: 'unstoppableAddress', error: null, nameServer: 'unstoppable'}, + ]), + + error: () => + Promise.resolve([{address: null, error: null, nameServer: null}]), +} as const + +const getCryptoAddressesResponse = { + success: [ + {address: 'unstoppableAddress', error: null, nameServer: 'unstoppable'}, + ], + error: [{address: null, error: 'any error', nameServer: null}], +} + +export const resolverApiMocks = { + getCardanoAddresses, + getCryptoAddressesResponse, +} as const + +export const mockResolverApi = { + getCardanoAddresses: getCardanoAddresses.success, +} as const diff --git a/packages/resolver/src/adapters/api.test.ts b/packages/resolver/src/adapters/api.test.ts new file mode 100644 index 0000000000..993e86ff44 --- /dev/null +++ b/packages/resolver/src/adapters/api.test.ts @@ -0,0 +1,147 @@ +import {Resolver} from '@yoroi/types' + +import {resolverApiMaker} from './api' + +const mockApiConfig = { + apiConfig: { + [Resolver.NameServer.Unstoppable]: { + apiKey: 'mock-api-key', + }, + }, +} +const mockError = new Error('Test Error') + +describe('resolverApiMaker', () => { + const domain = 'example.domain' + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('getCardanoAddresses', () => { + describe('strategy "all"', () => { + it('all resolved', async () => { + const deps = { + unstoppableApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue( + jest.fn().mockResolvedValue('unstoppableAddress'), + ), + }, + handleApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockResolvedValue('handleAddress')), + }, + cnsApi: { + getCryptoAddress: jest.fn().mockResolvedValue('cnsAddress'), + }, + } + + const api = resolverApiMaker(mockApiConfig, deps) + const results = await api.getCardanoAddresses({resolve: domain}) + + expect(results).toEqual([ + {address: 'handleAddress', error: null, nameServer: 'handle'}, + { + address: 'unstoppableAddress', + error: null, + nameServer: 'unstoppable', + }, + {address: 'cnsAddress', error: null, nameServer: 'cns'}, + ]) + }) + + it('some resolved', async () => { + const deps = { + unstoppableApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockRejectedValue(mockError)), + }, + handleApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockResolvedValue('handleAddress')), + }, + cnsApi: { + getCryptoAddress: jest.fn().mockRejectedValue(mockError), + }, + } + + const api = resolverApiMaker(mockApiConfig, deps) + const results = await api.getCardanoAddresses({resolve: domain}) + + expect(results).toEqual([ + {address: 'handleAddress', error: null, nameServer: 'handle'}, + {address: null, error: mockError.message, nameServer: 'unstoppable'}, + {address: null, error: mockError.message, nameServer: 'cns'}, + ]) + }) + }) + + describe('strategy "first"', () => { + it('any resolved', async () => { + const deps = { + unstoppableApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockRejectedValue(mockError)), + }, + handleApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockResolvedValue('handleAddress')), + }, + cnsApi: { + getCryptoAddress: jest.fn().mockRejectedValue(mockError), + }, + } + + const api = resolverApiMaker(mockApiConfig, deps) + const results = await api.getCardanoAddresses({ + resolve: domain, + strategy: 'first', + }) + + expect(results).toEqual([ + {address: 'handleAddress', error: null, nameServer: 'handle'}, + ]) + }) + + it('none resolved', async () => { + const deps = { + unstoppableApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockRejectedValue(mockError)), + }, + handleApi: { + getCryptoAddress: jest + .fn() + .mockReturnValue(jest.fn().mockRejectedValue(mockError)), + }, + cnsApi: { + getCryptoAddress: jest.fn().mockRejectedValue(mockError), + }, + } + + const api = resolverApiMaker(mockApiConfig, deps) + const results = await api.getCardanoAddresses({ + resolve: domain, + strategy: 'first', + }) + + expect(results).toEqual([ + {address: null, error: 'Not resolved', nameServer: null}, + ]) + }) + }) + + it('instantiate - coverage only', async () => { + const api = resolverApiMaker(mockApiConfig) + expect(api).toBeDefined() + }) + }) +}) diff --git a/packages/resolver/src/adapters/api.ts b/packages/resolver/src/adapters/api.ts new file mode 100644 index 0000000000..ca19851c02 --- /dev/null +++ b/packages/resolver/src/adapters/api.ts @@ -0,0 +1,131 @@ +import {Resolver} from '@yoroi/types' +import {AxiosRequestConfig} from 'axios' + +import {handleApiGetCryptoAddress} from './handle-api' +import {unstoppableApiGetCryptoAddress} from './unstoppable-api' +import {getCnsCryptoAddress} from './cns' + +type ApiConfig = { + [Resolver.NameServer.Unstoppable]: { + apiKey: string + } +} + +const initialDeps = { + unstoppableApi: { + getCryptoAddress: unstoppableApiGetCryptoAddress, + }, + handleApi: { + getCryptoAddress: handleApiGetCryptoAddress, + }, + cnsApi: { + getCryptoAddress: getCnsCryptoAddress, + }, +} as const + +export const resolverApiMaker = ( + { + apiConfig, + }: { + apiConfig: Readonly + }, + { + unstoppableApi, + handleApi, + cnsApi, + }: { + unstoppableApi: { + getCryptoAddress: typeof unstoppableApiGetCryptoAddress + } + handleApi: { + getCryptoAddress: typeof handleApiGetCryptoAddress + } + cnsApi: { + getCryptoAddress: typeof getCnsCryptoAddress + } + } = initialDeps, +): Resolver.Api => { + const getHandleCryptoAddress = handleApi.getCryptoAddress() + const getUnstoppableCryptoAddress = unstoppableApi.getCryptoAddress( + apiConfig[Resolver.NameServer.Unstoppable], + ) + // @ts-expect-error TODO: bugfix on TS 5.4 (readonly array of readonly array) + const operationsGetCryptoAddress: GetCryptoAddressOperations = [ + [Resolver.NameServer.Handle, getHandleCryptoAddress], + [Resolver.NameServer.Unstoppable, getUnstoppableCryptoAddress], + [Resolver.NameServer.Cns, cnsApi.getCryptoAddress], + ] as const + + // facade to the different name servers + const getCardanoAddresses = async ( + { + resolve, + strategy = 'all', + }: { + resolve: Resolver.Receiver['resolve'] + strategy: Resolver.Strategy + }, + fetcherConfig?: AxiosRequestConfig, + ): Promise => { + if (strategy === 'all') + return resolveAll(operationsGetCryptoAddress, resolve, fetcherConfig) + + return resolveFirst(operationsGetCryptoAddress, resolve, fetcherConfig) + } + + return { + getCardanoAddresses, + } as const +} + +const safelyExecuteOperation = async ( + operationFn: GetCryptoAddress, + nameServer: Resolver.NameServer, + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, +): Promise => { + try { + const address = await operationFn(resolve, fetcherConfig) + return {error: null, address, nameServer} + } catch (error) { + return {error: (error as Error).message, address: null, nameServer} + } +} + +const resolveAll = async ( + operations: GetCryptoAddressOperations, + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, +): Promise => { + const promises = operations.map(([nameServer, operationFn]) => + safelyExecuteOperation(operationFn, nameServer, resolve, fetcherConfig), + ) + const result = await Promise.all(promises) + return result +} + +const resolveFirst = async ( + operations: GetCryptoAddressOperations, + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, +): Promise => { + const promises = operations.map(async ([nameServer, operationFn]) => { + const address = await operationFn(resolve, fetcherConfig) + return {error: null, address, nameServer} + }) + try { + const result = await Promise.any(promises) + return [result] + } catch (error) { + return [{address: null, error: 'Not resolved', nameServer: null}] + } +} + +type GetCryptoAddress = ( + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, +) => Promise + +type GetCryptoAddressOperations = ReadonlyArray< + [Resolver.NameServer, GetCryptoAddress] +> diff --git a/packages/resolver/src/adapters/cns.test.ts b/packages/resolver/src/adapters/cns.test.ts new file mode 100644 index 0000000000..d1838b02f9 --- /dev/null +++ b/packages/resolver/src/adapters/cns.test.ts @@ -0,0 +1,14 @@ +import {Resolver} from '@yoroi/types' + +import {getCnsCryptoAddress} from './cns' // Adjust the path accordingly + +describe('getCnsCryptoAddress', () => { + it.each` + domain | error + ${'test'} | ${Resolver.Errors.InvalidDomain} + ${'test.tada'} | ${Resolver.Errors.UnsupportedTld} + ${'test.ada'} | ${Resolver.Errors.UnsupportedTld} + `('should reject with error $error', async ({domain, error}) => { + await expect(getCnsCryptoAddress(domain)).rejects.toThrowError(error) + }) +}) diff --git a/packages/resolver/src/adapters/cns.ts b/packages/resolver/src/adapters/cns.ts new file mode 100644 index 0000000000..c31f0aa1d5 --- /dev/null +++ b/packages/resolver/src/adapters/cns.ts @@ -0,0 +1,11 @@ +import {Resolver} from '@yoroi/types' + +export const getCnsCryptoAddress = async (receiver: string) => { + if (!receiver.includes('.')) throw new Resolver.Errors.InvalidDomain() + if (!isCnsDomain(receiver)) + return Promise.reject(new Resolver.Errors.UnsupportedTld()) + return Promise.reject(new Resolver.Errors.UnsupportedTld()) +} + +export const cnsSupportedTld = '.ada' +export const isCnsDomain = (value: string) => value.endsWith(cnsSupportedTld) diff --git a/packages/resolver/src/adapters/handle-api.mocks.ts b/packages/resolver/src/adapters/handle-api.mocks.ts new file mode 100644 index 0000000000..8d5fa33e50 --- /dev/null +++ b/packages/resolver/src/adapters/handle-api.mocks.ts @@ -0,0 +1,33 @@ +import {HandleApiGetCryptoAddressResponse} from './handle-api' + +export const getCrypoAddress: HandleApiGetCryptoAddressResponse = { + hex: '737461636b636861696e', + name: 'stackchain', + image: 'ipfs://QmdEu1i3WxjFjQeJNm7Nmkqg9EU9RThHhAnKd1jsjw7LdC', + standard_image: 'ipfs://QmdEu1i3WxjFjQeJNm7Nmkqg9EU9RThHhAnKd1jsjw7LdC', + holder: 'stake1u8ggzxkv7vrnzr23t40yhvd3a9d37uu3f8y42m3tzve8yasraq5q3', + holder_type: 'wallet', + length: 10, + og_number: 0, + rarity: 'basic', + utxo: '5d3df979188b000e2829ec99fc58c9e5cee79dfe6fa4be13697bdd819ff3de77#5', + characters: 'letters', + numeric_modifiers: '', + default_in_wallet: 'stackchain', + pfp_image: '', + bg_image: '', + resolved_addresses: { + ada: 'addr1q9tgylj36qjp94rr3yjr5vtku3sltaqpefhergrnvluhtm7ssydveuc8xyx4zh27fwcmr62mraeezjwf24hzkyejwfmq0x9kll', + }, + created_slot_number: 68419174, + updated_slot_number: 103477127, + has_datum: false, + svg_version: '', + image_hash: '', + standard_image_hash: '', + version: 0, +} as const + +export const handleApiMockResponses = { + getCrypoAddress, +} as const diff --git a/packages/resolver/src/adapters/handle-api.test.ts b/packages/resolver/src/adapters/handle-api.test.ts new file mode 100644 index 0000000000..abefe8a6fb --- /dev/null +++ b/packages/resolver/src/adapters/handle-api.test.ts @@ -0,0 +1,154 @@ +import {Api, Left, Resolver, Right} from '@yoroi/types' + +import { + HandleApiGetCryptoAddressResponse, + handleApiConfig, + handleApiGetCryptoAddress, +} from './handle-api' +import {handleApiMockResponses} from './handle-api.mocks' + +describe('getCryptoAddress', () => { + const mockAddress = + handleApiMockResponses.getCrypoAddress.resolved_addresses.ada + const mockApiResponse = handleApiMockResponses.getCrypoAddress + + it('should return the address', async () => { + const domain = `$${mockApiResponse.name}` + const sanitizedDomain = `${mockApiResponse.name}` + const expectedUrl = `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}` + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: mockApiResponse, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = handleApiGetCryptoAddress({request: mockFetchData}) + + const result = await getCryptoAddress(domain) + + expect(mockFetchData).toHaveBeenCalledWith( + { + url: expectedUrl, + }, + undefined, + ) + expect(result).toBe(mockAddress) + }) + + it('should throw invalid domain if the domain provided is not a valid ada handle (doesnt start with $)', async () => { + const sanitizedDomain = `${mockApiResponse.name}` + const expectedUrl = `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}` + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: mockApiResponse, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = handleApiGetCryptoAddress({request: mockFetchData}) + + await expect(() => getCryptoAddress(sanitizedDomain)).rejects.toThrow( + Resolver.Errors.InvalidDomain, + ) + expect(mockFetchData).not.toHaveBeenCalledWith({ + url: expectedUrl, + }) + }) + + it('should throw invalid response if the response doesnt contain the address for ada', async () => { + const domain = `$${mockApiResponse.name}` + const sanitizedDomain = `${mockApiResponse.name}` + const expectedUrl = `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}` + const invalidApiResponse = {...mockApiResponse, resolved_addresses: {}} + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: invalidApiResponse as any, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = handleApiGetCryptoAddress({request: mockFetchData}) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.InvalidResponse, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + url: expectedUrl, + }, + undefined, + ) + }) + + it('should throw not found if the ada handle doesnt have an owner yet', async () => { + const domain = `$${mockApiResponse.name}` + const sanitizedDomain = `${mockApiResponse.name}` + const expectedUrl = `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}` + const errorApiResponse: Api.ResponseError = { + status: 404, + message: 'Not found', + } + + const mockFetchDataResponse: Left = { + tag: 'left', + error: errorApiResponse, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = handleApiGetCryptoAddress({request: mockFetchData}) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.NotFound, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + url: expectedUrl, + }, + undefined, + ) + }) + + it('should rethrow the api error if hasnt a map to a handle error', async () => { + const domain = `$${mockApiResponse.name}` + const sanitizedDomain = `${mockApiResponse.name}` + const expectedUrl = `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}` + const errorApiResponse: Api.ResponseError = { + status: 425, + message: 'Too Early', + } + + const mockFetchDataResponse: Left = { + tag: 'left', + error: errorApiResponse, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = handleApiGetCryptoAddress({request: mockFetchData}) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Api.Errors.TooEarly, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + url: expectedUrl, + }, + undefined, + ) + }) + + it('should build without dependencies (coverage only)', () => { + const getCryptoAddress = handleApiGetCryptoAddress() + expect(getCryptoAddress).toBeDefined() + }) +}) diff --git a/packages/resolver/src/adapters/handle-api.ts b/packages/resolver/src/adapters/handle-api.ts new file mode 100644 index 0000000000..af4cdf8e46 --- /dev/null +++ b/packages/resolver/src/adapters/handle-api.ts @@ -0,0 +1,106 @@ +import {Api, Resolver} from '@yoroi/types' +import {fetchData, FetchData, handleApiError, isLeft} from '@yoroi/common' +import {z} from 'zod' +import {AxiosRequestConfig} from 'axios' + +import {handleZodErrors} from './zod-errors' + +const initialDeps = {request: fetchData} as const + +export const handleApiGetCryptoAddress = ({ + request, +}: {request: FetchData} = initialDeps) => { + return async ( + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, + ): Promise => { + if (!isAdaHandleDomain(resolve)) throw new Resolver.Errors.InvalidDomain() + + const sanitizedDomain = resolve.replace(/^\$/, '') + const config = { + url: `${handleApiConfig.mainnet.getCryptoAddress}${sanitizedDomain}`, + } as const + + try { + const response = await request( + config, + fetcherConfig, + ) + + if (isLeft(response)) { + handleApiError(response.error) + } else { + const parsedResponse = HandleApiResponseSchema.parse( + response.value.data, + ) + return parsedResponse.resolved_addresses.ada + } + } catch (error: unknown) { + return handleHandleApiError(error) + } + } +} + +// https://github.com/koralabs/handles-public-api/blob/fd9d9f2cf3143e317a780b81b22869a755ab6af8/src/models/view/handle.view.model.ts +export type HandleApiGetCryptoAddressResponse = { + hex: string + name: string + image: string + standard_image: string + holder: string + holder_type: string + length: number + og_number: number + // export enum Rarity { + // basic = 'basic', // - 8-15 characters + // common = 'common', // - 4-7 characters + // rare = 'rare', // - 3 characters + // ultra_rare = 'ultra_rare', // - 2 characters + // legendary = 'legendary' // - 1 character + // } + rarity: string // translated to string only + utxo: string + characters: string + numeric_modifiers: string + default_in_wallet: string + pfp_image?: string + pfp_asset?: string + bg_image?: string + bg_asset?: string + resolved_addresses: { + ada: string + eth?: string | undefined + btc?: string | undefined + } + created_slot_number: number + updated_slot_number: number + has_datum: boolean + svg_version: string + image_hash: string + standard_image_hash: string + version: number +} + +const HandleApiResponseSchema = z.object({ + resolved_addresses: z.object({ + ada: z.string(), + }), +}) +export const isAdaHandleDomain = (value: string) => + value.startsWith('$') && value.length > 1 + +export const handleApiConfig = { + mainnet: { + getCryptoAddress: 'https://api.handle.me/handles/', + }, +} as const + +export const handleHandleApiError = (error: unknown): never => { + const zodErrorMessage = handleZodErrors(error) + if (zodErrorMessage) + throw new Resolver.Errors.InvalidResponse(zodErrorMessage) + + if (error instanceof Api.Errors.NotFound) throw new Resolver.Errors.NotFound() + + throw error +} diff --git a/packages/resolver/src/adapters/storage.mocks.ts b/packages/resolver/src/adapters/storage.mocks.ts new file mode 100644 index 0000000000..efcfe1234e --- /dev/null +++ b/packages/resolver/src/adapters/storage.mocks.ts @@ -0,0 +1,40 @@ +import {Resolver} from '@yoroi/types' + +const makeStorageMakerSuccess = (): Readonly => { + const showNotice: Resolver.Storage['showNotice'] = { + read: () => Promise.resolve(true), + remove: () => Promise.resolve(), + save: () => Promise.resolve(), + key: 'mock-resolver-show-notice', + } + + const clear: Resolver.Storage['clear'] = () => Promise.resolve() + + return { + showNotice, + clear, + } as const +} + +const makeStorageMakerError = (): Readonly => { + const showNotice: Resolver.Storage['showNotice'] = { + read: unknownError, + remove: unknownError, + save: unknownError, + key: 'mock-resolver-show-notice', + } + + const clear: Resolver.Storage['clear'] = unknownError + + return { + showNotice, + clear, + } as const +} + +export const mockStorageMaker = { + success: makeStorageMakerSuccess(), + error: makeStorageMakerError(), +} as const + +const unknownError = () => Promise.reject(new Error('Unknown error')) diff --git a/packages/resolver/src/adapters/storage.test.ts b/packages/resolver/src/adapters/storage.test.ts new file mode 100644 index 0000000000..24c04751c0 --- /dev/null +++ b/packages/resolver/src/adapters/storage.test.ts @@ -0,0 +1,65 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import {Resolver} from '@yoroi/types' + +import {resolverStorageMaker, resolverStorageNoticedKey} from './storage' + +jest.mock('@react-native-async-storage/async-storage') + +const mockedAsyncStorage = AsyncStorage as jest.Mocked + +describe('resolverStorageMaker', () => { + let resolverStorage: Resolver.Storage + + beforeEach(() => { + jest.clearAllMocks() + resolverStorage = resolverStorageMaker() + }) + + it('showNotice.save', async () => { + const showNotice = true + await resolverStorage.showNotice.save(showNotice) + expect(mockedAsyncStorage.setItem).toHaveBeenCalledWith( + resolverStorageNoticedKey, + JSON.stringify(showNotice), + ) + }) + + it('showNotice.read', async () => { + const showNotice = true + mockedAsyncStorage.getItem.mockResolvedValue(JSON.stringify(showNotice)) + const result = await resolverStorage.showNotice.read() + expect(result).toEqual(showNotice) + expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith( + resolverStorageNoticedKey, + ) + }) + + it('showNotice.read should fallback to true when empty / invalid', async () => { + mockedAsyncStorage.getItem.mockResolvedValue( + JSON.stringify('not a boolean'), + ) + const result = await resolverStorage.showNotice.read() + expect(result).toEqual(true) + expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith( + resolverStorageNoticedKey, + ) + mockedAsyncStorage.getItem.mockResolvedValue('[1, 2, ]') + const result2 = await resolverStorage.showNotice.read() + expect(result2).toEqual(true) + expect(mockedAsyncStorage.getItem).toHaveBeenCalledWith( + resolverStorageNoticedKey, + ) + }) + + it('showNotice.remove', async () => { + await resolverStorage.showNotice.remove() + expect(mockedAsyncStorage.removeItem).toHaveBeenCalledWith( + resolverStorageNoticedKey, + ) + }) + + it('clear', async () => { + await resolverStorage.clear() + expect(mockedAsyncStorage.removeItem).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/resolver/src/adapters/storage.ts b/packages/resolver/src/adapters/storage.ts new file mode 100644 index 0000000000..2ca4adcdab --- /dev/null +++ b/packages/resolver/src/adapters/storage.ts @@ -0,0 +1,33 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' +import {parseBoolean} from '@yoroi/common' +import {Resolver, BaseStorage} from '@yoroi/types' + +const initialDeps = {storage: AsyncStorage} as const + +export function resolverStorageMaker( + deps: {storage: BaseStorage | typeof AsyncStorage} = initialDeps, +): Readonly { + const {storage} = deps + + const showNotice: Readonly = { + save: (newShowNotice) => + storage.setItem(resolverStorageNoticedKey, JSON.stringify(newShowNotice)), + read: () => + storage + .getItem(resolverStorageNoticedKey) + .then((value) => parseBoolean(value) ?? true), + remove: () => storage.removeItem(resolverStorageNoticedKey), + key: resolverStorageNoticedKey, + } as const + + const clear = async () => { + await Promise.all([storage.removeItem(resolverStorageNoticedKey)]) + } + + return { + showNotice, + clear, + } as const +} + +export const resolverStorageNoticedKey = 'resolver-show-notice' diff --git a/packages/resolver/src/adapters/unstoppable-api.mocks.ts b/packages/resolver/src/adapters/unstoppable-api.mocks.ts new file mode 100644 index 0000000000..17a090b186 --- /dev/null +++ b/packages/resolver/src/adapters/unstoppable-api.mocks.ts @@ -0,0 +1,24 @@ +export const getCrypoAddress = { + meta: { + domain: 'stackchain.blockchain', + tokenId: + '5628062919663387899230227759020083743619439687633903328854109212936643864855', + namehash: + '0x0c715ee7fb0ff8a168b56d262d106d7a551f7b6f4ca2d690ef2bbd304920e917', + blockchain: 'MATIC', + networkId: 137, + owner: '0x10a242e7900920d8e1a8747abadad38ea2255662', + resolver: '0xa9a6a3626993d487d2dbda3173cf58ca1a9d9e9f', + registry: '0xa9a6a3626993d487d2dbda3173cf58ca1a9d9e9f', + reverse: false, + type: 'Uns', + }, + records: { + 'crypto.ADA.address': + 'addr1qxvaxrqpvquxzg2pqc093u7zp4mp0n3qdsqlg9a8ecv46e6qgkmm877p949fe53xpjv4lpfn48ew5qawrd0nzc3qd7cqcdlzjp', + }, +} as const + +export const handleApiMockResponses = { + getCrypoAddress, +} as const diff --git a/packages/resolver/src/adapters/unstoppable-api.test.ts b/packages/resolver/src/adapters/unstoppable-api.test.ts new file mode 100644 index 0000000000..37ea53d11e --- /dev/null +++ b/packages/resolver/src/adapters/unstoppable-api.test.ts @@ -0,0 +1,259 @@ +import {Api, Left, Resolver, Right} from '@yoroi/types' + +import { + UnstoppableApiGetCryptoAddressResponse, + unstoppableApiConfig, + unstoppableApiGetCryptoAddress, +} from './unstoppable-api' +import {handleApiMockResponses} from './unstoppable-api.mocks' + +describe('getCryptoAddress', () => { + const mockAddress = + handleApiMockResponses.getCrypoAddress.records['crypto.ADA.address'] + const mockApiResponse = handleApiMockResponses.getCrypoAddress + const mockOptions = { + apiKey: 'mock-api-key', + } + + it('should return the address', async () => { + const domain = mockApiResponse.meta.domain + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: mockApiResponse, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + const result = await getCryptoAddress(domain) + + expect(mockFetchData).toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mockOptions.apiKey}`, + }, + url: expectedUrl, + }, + undefined, + ) + expect(result).toBe(mockAddress) + }) + + it('should throw invalid domain if the domain provided is not a valid unstoppable domain (doesnt contain .)', async () => { + const domain = 'not-a-valid-domain' + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: mockApiResponse, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.InvalidDomain, + ) + expect(mockFetchData).not.toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Bearer': mockOptions.apiKey, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should throw unsupported tlds', async () => { + const domain = 'ud.what' + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: mockApiResponse, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.UnsupportedTld, + ) + expect(mockFetchData).not.toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Bearer': mockOptions.apiKey, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should throw invalid response if the response doesnt contain the address for ada', async () => { + const domain = mockApiResponse.meta.domain + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + const invalidApiResponse = { + ...mockApiResponse, + records: {'crypto.ADA.address': 123}, + } + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: invalidApiResponse as any, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.InvalidResponse, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mockOptions.apiKey}`, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should throw not found if the response doesnt contain the address for ada', async () => { + const domain = mockApiResponse.meta.domain + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + const invalidApiResponse = { + ...mockApiResponse, + records: {}, + } + + const mockFetchDataResponse: Right< + Api.ResponseSuccess + > = { + tag: 'right', + value: { + data: invalidApiResponse as any, + status: 200, + }, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.NotFound, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mockOptions.apiKey}`, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should throw not found if the ada handle doesnt have an owner yet', async () => { + const domain = mockApiResponse.meta.domain + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + const errorApiResponse: Api.ResponseError = { + status: 404, + message: 'Not found', + } + + const mockFetchDataResponse: Left = { + tag: 'left', + error: errorApiResponse, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Resolver.Errors.NotFound, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mockOptions.apiKey}`, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should rethrow the api error if hasnt a map to a handle error', async () => { + const domain = mockApiResponse.meta.domain + const expectedUrl = `${unstoppableApiConfig.mainnet.getCryptoAddress}${domain}` + const errorApiResponse: Api.ResponseError = { + status: 425, + message: 'Too Early', + } + + const mockFetchDataResponse: Left = { + tag: 'left', + error: errorApiResponse, + } + const mockFetchData = jest.fn().mockReturnValue(mockFetchDataResponse) + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions, { + request: mockFetchData, + }) + + await expect(() => getCryptoAddress(domain)).rejects.toThrow( + Api.Errors.TooEarly, + ) + expect(mockFetchData).toHaveBeenCalledWith( + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${mockOptions.apiKey}`, + }, + url: expectedUrl, + }, + undefined, + ) + }) + + it('should build without dependencies (coverage only)', () => { + const getCryptoAddress = unstoppableApiGetCryptoAddress(mockOptions) + expect(getCryptoAddress).toBeDefined() + }) +}) diff --git a/packages/resolver/src/adapters/unstoppable-api.ts b/packages/resolver/src/adapters/unstoppable-api.ts new file mode 100644 index 0000000000..d744af27dc --- /dev/null +++ b/packages/resolver/src/adapters/unstoppable-api.ts @@ -0,0 +1,113 @@ +import {Api, Resolver} from '@yoroi/types' +import {fetchData, FetchData, handleApiError, isLeft} from '@yoroi/common' +import {z} from 'zod' +import {AxiosRequestConfig} from 'axios' + +import {handleZodErrors} from './zod-errors' + +const initialDeps = {request: fetchData} as const + +export const unstoppableApiGetCryptoAddress = ( + {apiKey}: {apiKey: string}, + {request}: {request: FetchData} = initialDeps, +) => { + return async ( + resolve: Resolver.Receiver['resolve'], + fetcherConfig?: AxiosRequestConfig, + ): Promise => { + if (!resolve.includes('.')) throw new Resolver.Errors.InvalidDomain() + + if (!isUnstoppableHandleDomain(resolve)) + throw new Resolver.Errors.UnsupportedTld() + + const config = { + url: `${unstoppableApiConfig.mainnet.getCryptoAddress}${resolve}`, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + }, + } as const + + try { + const response = await request( + config, + fetcherConfig, + ) + + if (isLeft(response)) { + handleApiError(response.error) + } else { + const parsedResponse = UnstoppableApiResponseSchema.parse( + response.value.data, + ) + + const result = parsedResponse.records['crypto.ADA.address'] + if (!result) throw new Resolver.Errors.NotFound() + return result + } + } catch (error: unknown) { + return handleUnstoppableApiError(error) + } + } +} + +// https://docs.unstoppabledomains.com/openapi/resolution/#operation/DomainsController.getDomain +export type UnstoppableApiGetCryptoAddressResponse = { + meta: { + domain: string + tokenId: string + namehash: string + blockchain: string + networkId: number + owner: string + resolver: string + registry: string + reverse: boolean + type: string + } + records: { + 'crypto.ADA.address'?: string + } +} + +const UnstoppableApiResponseSchema = z.object({ + records: z.object({ + 'crypto.ADA.address': z.string().optional(), + }), +}) + +export const unstoppableSupportedTlds = [ + '.x', + '.crypto', + '.nft', + '.wallet', + '.polygon', + '.dao', + '.888', + '.zil', + '.go', + '.blockchain', + '.bitcoin', + '.eth', + '.com', + '.unstoppable', +] as const +export const isUnstoppableHandleDomain = (value: string) => { + return unstoppableSupportedTlds.some((tld) => value.endsWith(tld)) +} + +export const unstoppableApiConfig = { + mainnet: { + getCryptoAddress: 'https://api.unstoppabledomains.com/resolve/domains/', + }, +} as const + +export const handleUnstoppableApiError = (error: unknown): never => { + const zodErrorMessage = handleZodErrors(error) + if (zodErrorMessage) + throw new Resolver.Errors.InvalidResponse(zodErrorMessage) + + if (error instanceof Api.Errors.NotFound) throw new Resolver.Errors.NotFound() + + throw error +} diff --git a/packages/resolver/src/adapters/zod-errors.test.ts b/packages/resolver/src/adapters/zod-errors.test.ts new file mode 100644 index 0000000000..c91cb99e40 --- /dev/null +++ b/packages/resolver/src/adapters/zod-errors.test.ts @@ -0,0 +1,34 @@ +import {ZodError, ZodIssue} from 'zod' +import {handleZodErrors} from './zod-errors' + +describe('handleZodErrors', () => { + it('formats ZodError correctly', () => { + const mockIssues: ZodIssue[] = [ + { + // @ts-ignore + code: 'some_error_code', + path: ['field1'], + message: 'Invalid field1', + }, + { + // @ts-ignore + code: 'another_error_code', + path: ['nested', 'field2'], + message: 'Invalid field2', + }, + ] + + const mockZodError = new ZodError(mockIssues) + + const result = handleZodErrors(mockZodError) + expect(result).toBe( + 'Invalid data: field1: Invalid field1, nested.field2: Invalid field2', + ) + }) + + it('returns null for non-Zod errors', () => { + const nonZodError = new Error('Regular error') + const result = handleZodErrors(nonZodError) + expect(result).toBeNull() + }) +}) diff --git a/packages/resolver/src/adapters/zod-errors.ts b/packages/resolver/src/adapters/zod-errors.ts new file mode 100644 index 0000000000..94d7327319 --- /dev/null +++ b/packages/resolver/src/adapters/zod-errors.ts @@ -0,0 +1,15 @@ +import {ZodError} from 'zod' + +export function handleZodErrors(error: any) { + if (error instanceof ZodError) { + const errorDetails = error.issues.map((e) => ({ + field: e.path.join('.'), + message: e.message, + })) + const errorMessage = `Invalid data: ${errorDetails + .map((e) => `${e.field}: ${e.message}`) + .join(', ')}` + return errorMessage + } + return null +} diff --git a/packages/resolver/src/fixtures/ErrorBoundary.tsx b/packages/resolver/src/fixtures/ErrorBoundary.tsx new file mode 100644 index 0000000000..2cc55d1de9 --- /dev/null +++ b/packages/resolver/src/fixtures/ErrorBoundary.tsx @@ -0,0 +1,34 @@ +import React, {Component, ReactNode} from 'react' +import {View, Text} from 'react-native' + +interface Props { + children: ReactNode +} + +interface State { + hasError: boolean + error?: Error +} + +export class ErrorBoundary extends Component { + state: State = { + hasError: false, + } + + static getDerivedStateFromError(error: Error): State { + return {hasError: true, error} + } + + render() { + if (this.state.hasError) { + return ( + + hasError + {JSON.stringify(this.state.error)} + + ) + } + + return this.props.children + } +} diff --git a/packages/resolver/src/fixtures/SuspenseBoundary.tsx b/packages/resolver/src/fixtures/SuspenseBoundary.tsx new file mode 100644 index 0000000000..f9b3ee6fc6 --- /dev/null +++ b/packages/resolver/src/fixtures/SuspenseBoundary.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import {Text, View} from 'react-native' + +export const SuspenseBoundary = ({children}: {children: React.ReactNode}) => { + return ( + + suspending + + } + > + {children} + + ) +} diff --git a/packages/resolver/src/fixtures/manager-wrapper.tsx b/packages/resolver/src/fixtures/manager-wrapper.tsx new file mode 100644 index 0000000000..4e1ed0e143 --- /dev/null +++ b/packages/resolver/src/fixtures/manager-wrapper.tsx @@ -0,0 +1,27 @@ +import {Resolver} from '@yoroi/types' +import * as React from 'react' +import {QueryClient, QueryClientProvider} from 'react-query' + +import {SuspenseBoundary} from './SuspenseBoundary' +import {ErrorBoundary} from './ErrorBoundary' +import {ResolverProvider} from '../translators/reactjs/provider/ResolverProvider' + +type Props = { + queryClient: QueryClient + resolverManager: Resolver.Manager +} + +export const wrapperManagerFixture = + ({queryClient, resolverManager}: Props) => + ({children}: {children: React.ReactNode}) => + ( + + + + + {children} + + + + + ) diff --git a/packages/resolver/src/fixtures/query-client.ts b/packages/resolver/src/fixtures/query-client.ts new file mode 100644 index 0000000000..d3cb0695fb --- /dev/null +++ b/packages/resolver/src/fixtures/query-client.ts @@ -0,0 +1,14 @@ +import {QueryClient} from 'react-query' + +export const queryClientFixture = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + mutations: { + retry: false, + }, + }, + }) diff --git a/packages/resolver/src/index.test.ts b/packages/resolver/src/index.test.ts new file mode 100644 index 0000000000..a685517432 --- /dev/null +++ b/packages/resolver/src/index.test.ts @@ -0,0 +1,9 @@ +import {mocksResolver} from './index' + +describe('mocksResolver', () => { + it('should have the correct structure', () => { + expect(mocksResolver).toHaveProperty('storage') + expect(mocksResolver).toHaveProperty('api') + expect(mocksResolver).toHaveProperty('manager') + }) +}) diff --git a/packages/resolver/src/index.ts b/packages/resolver/src/index.ts new file mode 100644 index 0000000000..1d100da034 --- /dev/null +++ b/packages/resolver/src/index.ts @@ -0,0 +1,21 @@ +import {mockStorageMaker} from './adapters/storage.mocks' +import {mockResolverApi} from './adapters/api.mocks' +import {resolverManagerMocks} from './translators/manager.mocks' + +export * from './translators/manager' +export * from './translators/constants' +export * from './adapters/storage' +export * from './adapters/api' +export * from './translators/reactjs/hooks/useResolverCryptoAddresses' +export * from './translators/reactjs/hooks/useResolverSetShowNotice' +export * from './translators/reactjs/hooks/useResolverShowNotice' +export * from './translators/reactjs/provider/ResolverProvider' +export * from './utils/isResolvableDomain' +export * from './utils/isDomain' +export * from './utils/isNameServer' + +export const mocksResolver = { + storage: mockStorageMaker, + api: mockResolverApi, + manager: resolverManagerMocks.success, +} as const diff --git a/packages/resolver/src/translators/constants.ts b/packages/resolver/src/translators/constants.ts new file mode 100644 index 0000000000..4ad1385e4d --- /dev/null +++ b/packages/resolver/src/translators/constants.ts @@ -0,0 +1,7 @@ +import {Resolver} from '@yoroi/types' + +export const nameServerName = { + [Resolver.NameServer.Cns]: 'CNS', + [Resolver.NameServer.Unstoppable]: 'Unstoppable Domains', + [Resolver.NameServer.Handle]: 'ADA Handle', +} as const diff --git a/packages/resolver/src/translators/manager.mocks.ts b/packages/resolver/src/translators/manager.mocks.ts new file mode 100644 index 0000000000..b261d754f1 --- /dev/null +++ b/packages/resolver/src/translators/manager.mocks.ts @@ -0,0 +1,22 @@ +import {Resolver} from '@yoroi/types' + +import {resolverApiMocks} from '../adapters/api.mocks' +import {mockStorageMaker} from '../adapters/storage.mocks' + +const resolverManagerSuccess: Resolver.Manager = { + crypto: {getCardanoAddresses: resolverApiMocks.getCardanoAddresses.success}, + showNotice: mockStorageMaker.success.showNotice, +} as const + +const resolverManagerError: Resolver.Manager = { + crypto: {getCardanoAddresses: resolverApiMocks.getCardanoAddresses.error}, + showNotice: mockStorageMaker.error.showNotice, +} as const + +export const resolverManagerMocks = { + success: resolverManagerSuccess, + error: resolverManagerError, + + getCardanoAddresses: resolverApiMocks.getCardanoAddresses, + getCryptoAddressesResponse: resolverApiMocks.getCryptoAddressesResponse, +} as const diff --git a/packages/resolver/src/translators/manager.test.ts b/packages/resolver/src/translators/manager.test.ts new file mode 100644 index 0000000000..a2fcbbed5a --- /dev/null +++ b/packages/resolver/src/translators/manager.test.ts @@ -0,0 +1,23 @@ +/* istanbul ignore file */ +import {mockResolverApi} from '../adapters/api.mocks' +import {mockStorageMaker} from '../adapters/storage.mocks' +import {resolverManagerMaker} from './manager' + +describe('resolverManagerMaker', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('creates a module with getCryptoAddress function', () => { + const module = resolverManagerMaker( + mockStorageMaker.success, + mockResolverApi, + ) + + expect(module).toHaveProperty('crypto.getCardanoAddresses') + expect(module).toHaveProperty('showNotice.read') + expect(module).toHaveProperty('showNotice.remove') + expect(module).toHaveProperty('showNotice.save') + expect(module).toHaveProperty('showNotice.key') + }) +}) diff --git a/packages/resolver/src/translators/manager.ts b/packages/resolver/src/translators/manager.ts new file mode 100644 index 0000000000..324a34c0ed --- /dev/null +++ b/packages/resolver/src/translators/manager.ts @@ -0,0 +1,14 @@ +import {Resolver} from '@yoroi/types' + +export const resolverManagerMaker = ( + resolverStorage: Resolver.Storage, + resolverApi: Resolver.Api, +): Resolver.Manager => { + const {showNotice} = resolverStorage + const {getCardanoAddresses} = resolverApi + + return { + crypto: {getCardanoAddresses}, + showNotice, + } as const +} diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.test.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.test.tsx new file mode 100644 index 0000000000..5ebecd94af --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react' +import {QueryClient} from 'react-query' +import {Text, View} from 'react-native' +import {render, waitFor} from '@testing-library/react-native' + +import {queryClientFixture} from '../../../fixtures/query-client' +import {wrapperManagerFixture} from '../../../fixtures/manager-wrapper' +import {useResolverCryptoAddresses} from './useResolverCryptoAddresses' +import {resolverManagerMocks} from '../../manager.mocks' + +describe('useResolverCryptoAddresses', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + const mockResolverManager = {...resolverManagerMocks.success} + const domain = '$test' + + it('success', async () => { + const TestResolverAddresss = () => { + const addresses = useResolverCryptoAddresses({resolve: domain}) + return ( + + {JSON.stringify(addresses.data)} + + ) + } + + mockResolverManager.crypto.getCardanoAddresses = jest + .fn() + .mockResolvedValue( + resolverManagerMocks.getCryptoAddressesResponse.success, + ) + const wrapper = wrapperManagerFixture({ + queryClient, + resolverManager: mockResolverManager, + }) + const {getByTestId} = render(, {wrapper}) + + await waitFor(() => { + expect(getByTestId('addresses')).toBeDefined() + }) + + expect(getByTestId('addresses').props.children).toEqual( + JSON.stringify(resolverManagerMocks.getCryptoAddressesResponse.success), + ) + expect( + mockResolverManager.crypto.getCardanoAddresses, + ).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.tsx new file mode 100644 index 0000000000..feb2c07c65 --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverCryptoAddresses.tsx @@ -0,0 +1,36 @@ +import {UseQueryOptions, useQuery} from 'react-query' +import {Resolver} from '@yoroi/types' + +import {useResolver} from '../provider/ResolverProvider' + +// half of average block time +const tenSeconds = 10 * 1000 +export const useResolverCryptoAddresses = ( + { + resolve, + strategy = 'all', + }: {resolve: Resolver.Receiver['resolve']; strategy?: Resolver.Strategy}, + options?: UseQueryOptions< + Resolver.AddressesResponse, + Error, + Resolver.AddressesResponse, + ['useResolverCryptoAddresses', string] + >, +) => { + const {crypto} = useResolver() + + const query = useQuery({ + useErrorBoundary: true, + queryKey: ['useResolverCryptoAddresses', resolve], + staleTime: 0, + cacheTime: tenSeconds, + ...options, + queryFn: ({signal}) => + crypto.getCardanoAddresses({resolve, strategy}, {signal}), + }) + + return { + ...query, + cryptoAddresses: query.data ?? [], + } +} diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.test.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.test.tsx new file mode 100644 index 0000000000..6961657ee9 --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.test.tsx @@ -0,0 +1,43 @@ +import {QueryClient} from 'react-query' +import {renderHook, act} from '@testing-library/react-hooks' + +import {queryClientFixture} from '../../../fixtures/query-client' +import {wrapperManagerFixture} from '../../../fixtures/manager-wrapper' +import {resolverManagerMocks} from '../../manager.mocks' +import {useResolverSetShowNotice} from './useResolverSetShowNotice' + +describe('useResolverSetShowNotice', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + const mockResolverManager = {...resolverManagerMocks.success} + + it('success', async () => { + mockResolverManager.showNotice.save = jest.fn().mockResolvedValue(undefined) + const wrapper = wrapperManagerFixture({ + queryClient, + resolverManager: mockResolverManager, + }) + + const {result, waitFor: waitForHook} = renderHook( + () => useResolverSetShowNotice(), + {wrapper}, + ) + + await act(async () => result.current.setShowNotice(true)) + + await waitForHook(() => expect(result.current.isLoading).toBe(false)) + + expect(mockResolverManager.showNotice.save).toHaveBeenCalledTimes(1) + expect(mockResolverManager.showNotice.save).toHaveBeenCalledWith(true) + expect(result.current.isError).toBe(false) + }) +}) diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.tsx new file mode 100644 index 0000000000..90a721edb9 --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverSetShowNotice.tsx @@ -0,0 +1,21 @@ +import {UseMutationOptions} from 'react-query' + +import {useResolver} from '../provider/ResolverProvider' +import {useMutationWithInvalidations} from '../../../utils/useMutationWithInvalidations' + +export const useResolverSetShowNotice = ( + options?: UseMutationOptions, +) => { + const {showNotice} = useResolver() + + const mutation = useMutationWithInvalidations({ + ...options, + mutationKey: ['useResolverSetShowNotice'], + mutationFn: showNotice.save, + invalidateQueries: [['useResolverShowNotice']], + }) + return { + ...mutation, + setShowNotice: mutation.mutate, + } +} diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.test.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.test.tsx new file mode 100644 index 0000000000..ad40eea148 --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.test.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import {QueryClient} from 'react-query' +import {Text, View} from 'react-native' +import {render, waitFor} from '@testing-library/react-native' + +import {queryClientFixture} from '../../../fixtures/query-client' +import {wrapperManagerFixture} from '../../../fixtures/manager-wrapper' +import {resolverManagerMocks} from '../../manager.mocks' +import {useResolverShowNotice} from './useResolverShowNotice' + +describe('useResolverCryptoAddresses', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + const mockResolverManager = {...resolverManagerMocks.success} + + it('success', async () => { + const TestResolver = () => { + const showNotice = useResolverShowNotice() + return ( + + {JSON.stringify(showNotice.data)} + + ) + } + + mockResolverManager.showNotice.read = jest.fn().mockResolvedValue(false) + const wrapper = wrapperManagerFixture({ + queryClient, + resolverManager: mockResolverManager, + }) + const {getByTestId} = render(, {wrapper}) + + await waitFor(() => { + expect(getByTestId('showNotice')).toBeDefined() + }) + + expect(getByTestId('showNotice').props.children).toEqual( + JSON.stringify(false), + ) + expect(mockResolverManager.showNotice.read).toHaveBeenCalledTimes(1) + }) + + it('error', async () => { + const TestResolver = () => { + const showNotice = useResolverShowNotice() + return ( + + {JSON.stringify(showNotice)} + + ) + } + mockResolverManager.showNotice.read = jest.fn().mockRejectedValue('error') + + const wrapper = wrapperManagerFixture({ + queryClient, + resolverManager: mockResolverManager, + }) + const {getByTestId} = render(, {wrapper}) + + await waitFor(() => { + expect(getByTestId('hasError')).toBeDefined() + }) + }) +}) diff --git a/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.tsx b/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.tsx new file mode 100644 index 0000000000..d423da62ba --- /dev/null +++ b/packages/resolver/src/translators/reactjs/hooks/useResolverShowNotice.tsx @@ -0,0 +1,27 @@ +import {UseQueryOptions, useQuery} from 'react-query' + +import {useResolver} from '../provider/ResolverProvider' + +export const useResolverShowNotice = ( + options: UseQueryOptions< + boolean, + Error, + boolean, + ['useResolverShowNotice'] + > = {}, +) => { + const {showNotice} = useResolver() + + const query = useQuery({ + useErrorBoundary: true, + suspense: true, + ...options, + queryKey: ['useResolverShowNotice'], + queryFn: showNotice.read, + }) + + return { + ...query, + showNotice: query.data, + } +} diff --git a/packages/resolver/src/translators/reactjs/provider/ResolverProvider.test.tsx b/packages/resolver/src/translators/reactjs/provider/ResolverProvider.test.tsx new file mode 100644 index 0000000000..078f7b093c --- /dev/null +++ b/packages/resolver/src/translators/reactjs/provider/ResolverProvider.test.tsx @@ -0,0 +1,57 @@ +import {Resolver} from '@yoroi/types' +import {QueryClient} from 'react-query' +import {renderHook, act} from '@testing-library/react-hooks' + +import {queryClientFixture} from '../../../fixtures/query-client' +import {useResolver} from './ResolverProvider' +import {wrapperManagerFixture} from '../../../fixtures/manager-wrapper' + +const resolverManagerMock: Resolver.Manager = { + crypto: { + getCardanoAddresses: jest.fn(), + }, + showNotice: { + read: jest.fn(), + remove: jest.fn(), + save: jest.fn(), + key: 'show-notice-key', + }, +} + +describe('ResolverProvider', () => { + let queryClient: QueryClient + + beforeEach(() => { + jest.clearAllMocks() + queryClient = queryClientFixture() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('works', () => { + const wrapper = wrapperManagerFixture({ + queryClient, + resolverManager: resolverManagerMock, + }) + const {result} = renderHook(() => useResolver(), { + wrapper, + }) + + act(() => { + result.current.showNotice.read() + result.current.showNotice.save(true) + result.current.showNotice.remove() + result.current.crypto.getCardanoAddresses({resolve: 'domain'}) + }) + + expect(resolverManagerMock.showNotice.read).toHaveBeenCalled() + expect(resolverManagerMock.showNotice.save).toHaveBeenCalledWith(true) + expect(resolverManagerMock.showNotice.remove).toHaveBeenCalled() + expect(resolverManagerMock.crypto.getCardanoAddresses).toHaveBeenCalledWith( + {resolve: 'domain'}, + ) + expect(result.current.showNotice.key).toBe('show-notice-key') + }) +}) diff --git a/packages/resolver/src/translators/reactjs/provider/ResolverProvider.tsx b/packages/resolver/src/translators/reactjs/provider/ResolverProvider.tsx new file mode 100644 index 0000000000..91893b1e29 --- /dev/null +++ b/packages/resolver/src/translators/reactjs/provider/ResolverProvider.tsx @@ -0,0 +1,31 @@ +import {Resolver} from '@yoroi/types' +import * as React from 'react' + +import {resolverManagerMocks} from '../../manager.mocks' + +type ResolverProviderContext = React.PropsWithChildren + +const initialResolverProvider: ResolverProviderContext = { + ...resolverManagerMocks.error, +} + +const ResolverContext = React.createContext( + initialResolverProvider, +) +export const ResolverProvider = ({ + children, + resolverManager, +}: { + children: React.ReactNode + resolverManager: Resolver.Manager +}) => { + const context = React.useMemo(() => ({...resolverManager}), [resolverManager]) + + return ( + + {children} + + ) +} + +export const useResolver = () => React.useContext(ResolverContext) diff --git a/packages/resolver/src/utils/isDomain.test.ts b/packages/resolver/src/utils/isDomain.test.ts new file mode 100644 index 0000000000..4ca40727e3 --- /dev/null +++ b/packages/resolver/src/utils/isDomain.test.ts @@ -0,0 +1,13 @@ +import {isDomain} from './isDomain' + +describe('isDomain', () => { + it.each` + resolve | expected + ${'$handle'} | ${true} + ${'domain.ada'} | ${true} + ${'not$handle'} | ${false} + ${'whatever-domains'} | ${false} + `('should return $expected for $resolve', ({resolve, expected}) => { + expect(isDomain(resolve)).toBe(expected) + }) +}) diff --git a/packages/resolver/src/utils/isDomain.ts b/packages/resolver/src/utils/isDomain.ts new file mode 100644 index 0000000000..7393b9077f --- /dev/null +++ b/packages/resolver/src/utils/isDomain.ts @@ -0,0 +1,4 @@ +import {isAdaHandleDomain} from '../adapters/handle-api' + +export const isDomain = (resolve: string) => + isAdaHandleDomain(resolve) || resolve.includes('.') diff --git a/packages/resolver/src/utils/isNameServer.test.ts b/packages/resolver/src/utils/isNameServer.test.ts new file mode 100644 index 0000000000..6ec741027d --- /dev/null +++ b/packages/resolver/src/utils/isNameServer.test.ts @@ -0,0 +1,15 @@ +import {Resolver} from '@yoroi/types' + +import {isNameServer} from './isNameServer' + +describe('isNameServer', () => { + it.each` + nameServer | expected + ${Resolver.NameServer.Cns} | ${true} + ${Resolver.NameServer.Handle} | ${true} + ${Resolver.NameServer.Unstoppable} | ${true} + ${'whatever-domains'} | ${false} + `('should return $expected for $domain', ({nameServer, expected}) => { + expect(isNameServer(nameServer)).toBe(expected) + }) +}) diff --git a/packages/resolver/src/utils/isNameServer.ts b/packages/resolver/src/utils/isNameServer.ts new file mode 100644 index 0000000000..38e7c7feaa --- /dev/null +++ b/packages/resolver/src/utils/isNameServer.ts @@ -0,0 +1,4 @@ +import {Resolver} from '@yoroi/types' + +export const isNameServer = (key: string): key is Resolver.NameServer => + Object.values(Resolver.NameServer).includes(key as Resolver.NameServer) diff --git a/packages/resolver/src/utils/isResolvableDomain.test.ts b/packages/resolver/src/utils/isResolvableDomain.test.ts new file mode 100644 index 0000000000..d7e467d080 --- /dev/null +++ b/packages/resolver/src/utils/isResolvableDomain.test.ts @@ -0,0 +1,27 @@ +import {isResolvableDomain} from './isResolvableDomain' + +describe('isResolvableDomain', () => { + it.each` + resolve | expected + ${'ud.com'} | ${true} + ${'ud.eth'} | ${true} + ${'ud.crypto'} | ${true} + ${'ud.zil'} | ${true} + ${'ud.bitcoin'} | ${true} + ${'ud.blockchain'} | ${true} + ${'ud.go'} | ${true} + ${'ud.888'} | ${true} + ${'ud.dao'} | ${true} + ${'ud.polygon'} | ${true} + ${'ud.wallet'} | ${true} + ${'ud.nft'} | ${true} + ${'ud.x'} | ${true} + ${'ud.unstoppable'} | ${true} + ${'$adahandle'} | ${true} + ${'cns.ada'} | ${true} + ${'other.uk'} | ${false} + ${'$'} | ${false} + `('should return $expected for $resolve', ({resolve, expected}) => { + expect(isResolvableDomain(resolve)).toBe(expected) + }) +}) diff --git a/packages/resolver/src/utils/isResolvableDomain.ts b/packages/resolver/src/utils/isResolvableDomain.ts new file mode 100644 index 0000000000..d3883e5b26 --- /dev/null +++ b/packages/resolver/src/utils/isResolvableDomain.ts @@ -0,0 +1,8 @@ +import {isUnstoppableHandleDomain} from '../adapters/unstoppable-api' +import {isAdaHandleDomain} from '../adapters/handle-api' +import {isCnsDomain} from '../adapters/cns' + +export const isResolvableDomain = (resolve: string) => + isAdaHandleDomain(resolve) || + isUnstoppableHandleDomain(resolve) || + isCnsDomain(resolve) diff --git a/packages/resolver/src/utils/useMutationWithInvalidations.tsx b/packages/resolver/src/utils/useMutationWithInvalidations.tsx new file mode 100644 index 0000000000..e547ff3567 --- /dev/null +++ b/packages/resolver/src/utils/useMutationWithInvalidations.tsx @@ -0,0 +1,34 @@ +import { + QueryKey, + UseMutationOptions, + useMutation, + useQueryClient, +} from 'react-query' + +// TODO: import later from @yoroi/common utils/hooks +/* istanbul ignore next */ +export const useMutationWithInvalidations = < + TData = unknown, + TError = unknown, + TVariables = void, + TContext = unknown, +>({ + invalidateQueries, + ...options +}: UseMutationOptions & { + invalidateQueries?: Array +} = {}) => { + const queryClient = useQueryClient() + + return useMutation({ + ...options, + onMutate: (variables) => { + invalidateQueries?.forEach((key) => queryClient.cancelQueries(key)) + return options?.onMutate?.(variables) + }, + onSuccess: (data, variables, context) => { + invalidateQueries?.forEach((key) => queryClient.invalidateQueries(key)) + return options?.onSuccess?.(data, variables, context) + }, + }) +} diff --git a/packages/resolver/tsconfig.build.json b/packages/resolver/tsconfig.build.json new file mode 100644 index 0000000000..1382480b0c --- /dev/null +++ b/packages/resolver/tsconfig.build.json @@ -0,0 +1,5 @@ + +{ + "extends": "./tsconfig", + "exclude": ["example", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/resolver/tsconfig.json b/packages/resolver/tsconfig.json new file mode 100644 index 0000000000..69b73f85f3 --- /dev/null +++ b/packages/resolver/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "declaration": true, + "baseUrl": "./src", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext" + }, + "exclude": ["node_modules"] +} diff --git a/packages/swap/src/translators/reactjs.tsx b/packages/swap/src/translators/reactjs.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/types/package.json b/packages/types/package.json index efc7989d24..cb4a66c61c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -94,6 +94,9 @@ "babel.config.js", "jest.setup.js" ], + "dependencies": { + "axios": "^1.5.0" + }, "devDependencies": { "@commitlint/config-conventional": "^17.0.2", "@release-it/conventional-changelog": "^5.0.0", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5d3a75753a..b4aae49a76 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -24,6 +24,15 @@ import {NumberLocale} from './intl/numbers' import {SwapAggregator} from './swap/aggregator' import {AppApi} from './app/api' import {AppFrontendFeesResponse, AppFrontendFeeTier} from './app/frontend-fees' +import { + ResolverAddressResponse, + ResolverAddressesResponse, + ResolverApi, + ResolverStrategy, +} from './resolver/api' +import {ResolverManager} from './resolver/manager' +import {ResolverReceiver} from './resolver/receiver' +import {ResolverStorage} from './resolver/storage' import {LinksLink, LinksModule, LinksUriConfig} from './links/link' import { LinksErrorExtraParamsDenied, @@ -50,6 +59,13 @@ import { ApiErrorInvalidState, ApiErrorResponseMalformed, } from './api/errors' +import {ResolverNameServer} from './resolver/name-server' +import { + ResolverErrorInvalidDomain, + ResolverErrorInvalidResponse, + ResolverErrorNotFound, + ResolverErrorUnsupportedTld, +} from './resolver/errors' export namespace App { export interface Storage extends AppStorage {} @@ -151,5 +167,28 @@ export namespace Numbers { export type Locale = NumberLocale } +export namespace Resolver { + export interface Api extends ResolverApi {} + export type Manager = ResolverManager + + export type NameServer = ResolverNameServer + export const NameServer = ResolverNameServer + export type Receiver = ResolverReceiver + + export type AddressResponse = ResolverAddressResponse + export type AddressesResponse = ResolverAddressesResponse + + export type Strategy = ResolverStrategy + + export type Storage = ResolverStorage + + export namespace Errors { + export class InvalidResponse extends ResolverErrorInvalidResponse {} + export class InvalidDomain extends ResolverErrorInvalidDomain {} + export class NotFound extends ResolverErrorNotFound {} + export class UnsupportedTld extends ResolverErrorUnsupportedTld {} + } +} + export * from './helpers/types' export * from './helpers/storage' diff --git a/packages/types/src/resolver/api.ts b/packages/types/src/resolver/api.ts new file mode 100644 index 0000000000..bd569fc868 --- /dev/null +++ b/packages/types/src/resolver/api.ts @@ -0,0 +1,23 @@ +import {AxiosRequestConfig} from 'axios' + +import {ResolverReceiver} from './receiver' + +export interface ResolverApi { + getCardanoAddresses( + args: { + resolve: ResolverReceiver['resolve'] + strategy?: ResolverStrategy + }, + fetcherOptions?: AxiosRequestConfig, + ): Promise +} + +export type ResolverStrategy = 'all' | 'first' + +export type ResolverAddressResponse = Readonly<{ + address: string | null + error: string | null + nameServer: string | null +}> + +export type ResolverAddressesResponse = ReadonlyArray diff --git a/packages/types/src/resolver/errors.ts b/packages/types/src/resolver/errors.ts new file mode 100644 index 0000000000..511dbe618a --- /dev/null +++ b/packages/types/src/resolver/errors.ts @@ -0,0 +1,4 @@ +export class ResolverErrorInvalidResponse extends Error {} +export class ResolverErrorInvalidDomain extends Error {} +export class ResolverErrorNotFound extends Error {} +export class ResolverErrorUnsupportedTld extends Error {} diff --git a/packages/types/src/resolver/manager.ts b/packages/types/src/resolver/manager.ts new file mode 100644 index 0000000000..99001fbf43 --- /dev/null +++ b/packages/types/src/resolver/manager.ts @@ -0,0 +1,9 @@ +import {ResolverApi} from './api' +import {ResolverStorage} from './storage' + +export type ResolverManager = Readonly<{ + crypto: { + getCardanoAddresses: ResolverApi['getCardanoAddresses'] + } + showNotice: ResolverStorage['showNotice'] +}> diff --git a/packages/types/src/resolver/name-server.ts b/packages/types/src/resolver/name-server.ts new file mode 100644 index 0000000000..154cfd537b --- /dev/null +++ b/packages/types/src/resolver/name-server.ts @@ -0,0 +1,5 @@ +export enum ResolverNameServer { + Cns = 'cns', + Unstoppable = 'unstoppable', + Handle = 'handle', +} diff --git a/packages/types/src/resolver/receiver.ts b/packages/types/src/resolver/receiver.ts new file mode 100644 index 0000000000..e3a04c4fb2 --- /dev/null +++ b/packages/types/src/resolver/receiver.ts @@ -0,0 +1,8 @@ +import {ResolverNameServer} from './name-server' + +export type ResolverReceiver = { + resolve: string + as: 'domain' | 'address' + selectedNameServer: ResolverNameServer | undefined + addressRecords: {[key in ResolverNameServer]?: string} | undefined +} diff --git a/packages/types/src/resolver/storage.ts b/packages/types/src/resolver/storage.ts new file mode 100644 index 0000000000..1a7711b3b2 --- /dev/null +++ b/packages/types/src/resolver/storage.ts @@ -0,0 +1,10 @@ +export type ResolverStorage = Readonly<{ + showNotice: { + read(): Promise + remove(): Promise + save(noticed: boolean): Promise + key: string + } + + clear(): Promise +}> diff --git a/scripts/install-pkgs.sh b/scripts/install-pkgs.sh index c0a0439f82..5921650890 100644 --- a/scripts/install-pkgs.sh +++ b/scripts/install-pkgs.sh @@ -24,6 +24,10 @@ yarn workspace @yoroi/swap add -D @yoroi/types@"$1" yarn workspace @yoroi/swap add @yoroi/api@"$1" yarn workspace @yoroi/swap add @yoroi/openswap@"$1" +# resolver +yarn workspace @yoroi/resolver add -D @yoroi/types@"$1" +yarn workspace @yoroi/resolver add @yoroi/common@"$1" + # wallet-mobile yarn workspace @yoroi/wallet-mobile add -D @yoroi/types@"$1" yarn workspace @yoroi/wallet-mobile add @yoroi/banxa@"$1" diff --git a/yarn.lock b/yarn.lock index f9beca7df3..50d6218d30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2291,183 +2291,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== -"@ethersproject/abi@^5.0.1": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" - integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/hash" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/abstract-provider@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" - integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/networks" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/transactions" "^5.7.0" - "@ethersproject/web" "^5.7.0" - -"@ethersproject/abstract-signer@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" - integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== - dependencies: - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - -"@ethersproject/address@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" - integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - -"@ethersproject/base64@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" - integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== - dependencies: - "@ethersproject/bytes" "^5.7.0" - -"@ethersproject/bignumber@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" - integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - bn.js "^5.2.1" - -"@ethersproject/bytes@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" - integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/constants@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" - integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== - dependencies: - "@ethersproject/bignumber" "^5.7.0" - -"@ethersproject/hash@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" - integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== - dependencies: - "@ethersproject/abstract-signer" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - -"@ethersproject/keccak256@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" - integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - js-sha3 "0.8.0" - -"@ethersproject/logger@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" - integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== - -"@ethersproject/networks@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" - integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/properties@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" - integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== - dependencies: - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/rlp@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" - integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/signing-key@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" - integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - bn.js "^5.2.1" - elliptic "6.5.4" - hash.js "1.1.7" - -"@ethersproject/strings@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" - integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== - dependencies: - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - -"@ethersproject/transactions@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" - integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== - dependencies: - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/signing-key" "^5.7.0" - -"@ethersproject/web@^5.7.0": - version "5.7.1" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" - integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== - dependencies: - "@ethersproject/base64" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/logger" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/strings" "^5.7.0" - "@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@expo/bunyan/-/bunyan-4.0.0.tgz#be0c1de943c7987a9fbd309ea0b1acd605890c7b" @@ -6443,19 +6266,6 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@unstoppabledomains/resolution@6.0.3": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@unstoppabledomains/resolution/-/resolution-6.0.3.tgz#a70888840c86a5918cb3134e7ac745e054c92aa5" - integrity sha512-J57SOzDqTDbrTfTzMSybM010LNvyNgEqKtdNcC5CAFg1IeNjC9woX5j1o/IbglXd9CPp1NL7n2Q1mVn9py211g== - dependencies: - "@ethersproject/abi" "^5.0.1" - bn.js "^4.4.0" - cross-fetch "^3.1.4" - elliptic "^6.5.4" - ethereum-ens-network-map "^1.0.2" - js-sha256 "^0.9.0" - js-sha3 "^0.8.0" - "@urql/core@2.3.6": version "2.3.6" resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552" @@ -7967,12 +7777,12 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1, bn.js@^5.2.1: +bn.js@^5.0.0, bn.js@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -9726,7 +9536,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@^3.0.6, cross-fetch@^3.1.4, cross-fetch@^3.1.5: +cross-fetch@^3.0.6, cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== @@ -10556,7 +10366,7 @@ electron-to-chromium@^1.4.431: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.457.tgz#3fdc7b4f97d628ac6b51e8b4b385befb362fe343" integrity sha512-/g3UyNDmDd6ebeWapmAoiyy+Sy2HyJ+/X8KyvNeHfKRFfHaA2W8oF5fxD5F3tjBDcjpwo0iek6YNgxNXDBoEtA== -elliptic@6.5.4, elliptic@^6.5.3, elliptic@^6.5.4: +elliptic@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -11346,11 +11156,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -ethereum-ens-network-map@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ethereum-ens-network-map/-/ethereum-ens-network-map-1.0.2.tgz#4e27bad18dae7bd95d84edbcac2c9e739fc959b9" - integrity sha512-5qwJ5n3YhjSpE6O/WEBXCAb2nagUgyagJ6C0lGUBWC4LjKp/rRzD+pwtDJ6KCiITFEAoX4eIrWOjRy0Sylq5Hg== - event-pubsub@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e" @@ -12999,7 +12804,7 @@ hash-wasm@^4.9.0: resolved "https://registry.yarnpkg.com/hash-wasm/-/hash-wasm-4.9.0.tgz#7e9dcc9f7d6bd0cc802f2a58f24edce999744206" integrity sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w== -hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: +hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -15414,12 +15219,7 @@ js-queue@2.0.2: dependencies: easy-stack "^1.0.1" -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - -js-sha3@0.8.0, js-sha3@^0.8.0: +js-sha3@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== @@ -19291,14 +19091,6 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== -react-devtools-core@4.26.1: - version "4.26.1" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.26.1.tgz#2893fea58089be64c5356d5bd0eebda8d1bbf317" - integrity sha512-r1csa5n9nABVpSdAadwTG7K+SfgRJPc/Hdx89BkV5IlA1mEGgGi3ir630ST5D/xYlJQaY3VE75YGADgpNW7HIw== - dependencies: - shell-quote "^1.6.1" - ws "^7" - react-devtools-core@^4.26.1: version "4.28.0" resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.0.tgz#3fa18709b24414adddadac33b6b9cea96db60f2f" @@ -19307,6 +19099,14 @@ react-devtools-core@^4.26.1: shell-quote "^1.6.1" ws "^7" +react-devtools-core@^4.28: + version "4.28.5" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" + integrity sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA== + dependencies: + shell-quote "^1.6.1" + ws "^7" + react-dom@16.8.3: version "16.8.3" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32" diff --git a/yoroi.code-workspace b/yoroi.code-workspace index cd6ffbca17..2754955695 100644 --- a/yoroi.code-workspace +++ b/yoroi.code-workspace @@ -24,6 +24,10 @@ "name": "@yoroi/api", "path": "./packages/api" }, + { + "name": "@yoroi/resolver", + "path": "./packages/resolver" + }, { "name": "@yoroi/common", "path": "./packages/common" @@ -53,6 +57,6 @@ "java.compile.nullAnalysis.mode": "automatic", "typescript.preferGoToSourceDefinition": true, "typescript.referencesCodeLens.enabled": true, - "typescript.tsserver.maxTsServerMemory": 12072 + "typescript.tsserver.maxTsServerMemory": 12072, } }