From 5d72fe9d6d13c7c0a60c77a2864d2f4cc441005f Mon Sep 17 00:00:00 2001 From: Michal S Date: Wed, 22 May 2024 18:45:38 +0100 Subject: [PATCH 1/2] feat(dapp-connector): Add CIP-30 support (#3225) --- .../.storybook/storybook.requires.js | 1 + apps/wallet-mobile/ios/Podfile.lock | 4 +- apps/wallet-mobile/package.json | 2 +- .../src/components/Icon/Connection.tsx | 18 + .../src/components/Icon/Icon.stories.tsx | 4 + .../src/components/Icon/YoroiApp.tsx | 34 ++ .../src/components/Icon/index.ts | 4 + .../src/components/Modal/ModalContext.tsx | 26 +- .../features/Discover/DiscoverNavigator.tsx | 61 ++- .../common/ConfirmConnectionModal.stories.tsx | 39 ++ .../common/ConfirmConnectionModal.tsx | 123 ++++++ .../src/features/Discover/common/helpers.ts | 52 ++- .../Discover/common/{hooks.ts => hooks.tsx} | 37 +- .../Discover/common/useDAppsConnected.tsx | 3 +- .../features/Discover/common/useStrings.tsx | 35 ++ .../useCases/BrowseDapp/BrowserTabBar.tsx | 17 +- .../useCases/BrowseDapp/WebViewItem.tsx | 5 +- .../SearchDappInBrowserScreen.tsx | 5 +- .../DAppListItem/DAppListItem.tsx | 35 +- .../SelectDappFromListScreen.tsx | 217 ++++++----- .../wallet-mobile/src/i18n/locales/en-US.json | 8 +- .../src/yoroi-wallets/cardano/api/api.ts | 2 +- .../cardano/byron/ByronWallet.ts | 16 +- .../src/yoroi-wallets/cardano/cip30.test.ts | 91 +++++ .../src/yoroi-wallets/cardano/cip30.ts | 363 ++++++++++++++++++ .../cardano/common/signatureUtils.ts | 22 +- .../cardano/shelley/ShelleyWallet.ts | 14 +- .../src/yoroi-wallets/cardano/types.ts | 14 +- .../src/yoroi-wallets/cardano/utils.ts | 2 +- .../cardano/utxoManager/types.ts | 1 + .../cardano/utxoManager/utxos.test.ts | 4 +- .../cardano/utxoManager/utxos.ts | 29 +- .../src/yoroi-wallets/cardano/wrappedCsl.ts | 62 +++ .../src/yoroi-wallets/mocks/wallet.ts | 8 +- .../messages/src/Dashboard/Dashboard.json | 4 +- .../features/Discover/common/useStrings.json | 201 +++++++--- .../SetupWallet/legacy/WalletForm.json | 92 +++++ packages/dapp-connector/src/connector.js | 20 +- .../dapp-connector/src/dapp-connector.test.ts | 218 ++++++++++- packages/dapp-connector/src/dapp-connector.ts | 16 +- packages/dapp-connector/src/index.ts | 5 +- packages/dapp-connector/src/resolver.ts | 153 +++++++- yarn.lock | 39 +- 43 files changed, 1771 insertions(+), 335 deletions(-) create mode 100644 apps/wallet-mobile/src/components/Icon/Connection.tsx create mode 100644 apps/wallet-mobile/src/components/Icon/YoroiApp.tsx create mode 100644 apps/wallet-mobile/src/features/Discover/common/ConfirmConnectionModal.stories.tsx create mode 100644 apps/wallet-mobile/src/features/Discover/common/ConfirmConnectionModal.tsx rename apps/wallet-mobile/src/features/Discover/common/{hooks.ts => hooks.tsx} (59%) create mode 100644 apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.test.ts create mode 100644 apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.ts create mode 100644 apps/wallet-mobile/src/yoroi-wallets/cardano/wrappedCsl.ts create mode 100644 apps/wallet-mobile/translations/messages/src/features/SetupWallet/legacy/WalletForm.json diff --git a/apps/wallet-mobile/.storybook/storybook.requires.js b/apps/wallet-mobile/.storybook/storybook.requires.js index 78213ed93d..c214949c1d 100644 --- a/apps/wallet-mobile/.storybook/storybook.requires.js +++ b/apps/wallet-mobile/.storybook/storybook.requires.js @@ -112,6 +112,7 @@ const getStories = () => { "./src/features/Claim/illustrations/Ilustrations.stories.tsx": require("../src/features/Claim/illustrations/Ilustrations.stories.tsx"), "./src/features/Claim/useCases/AskConfirmation.stories.tsx": require("../src/features/Claim/useCases/AskConfirmation.stories.tsx"), "./src/features/Claim/useCases/ShowSuccessScreen.stories.tsx": require("../src/features/Claim/useCases/ShowSuccessScreen.stories.tsx"), + "./src/features/Discover/common/ConfirmConnectionModal.stories.tsx": require("../src/features/Discover/common/ConfirmConnectionModal.stories.tsx"), "./src/features/Discover/common/LabelCategoryDApp.stories.tsx": require("../src/features/Discover/common/LabelCategoryDApp.stories.tsx"), "./src/features/Discover/common/LabelConnected.stories.tsx": require("../src/features/Discover/common/LabelConnected.stories.tsx"), "./src/features/Discover/useCases/BrowseDapp/BrowseDappScreen.stories.tsx": require("../src/features/Discover/useCases/BrowseDapp/BrowseDappScreen.stories.tsx"), diff --git a/apps/wallet-mobile/ios/Podfile.lock b/apps/wallet-mobile/ios/Podfile.lock index 9adeb1bc5c..8efec1c935 100644 --- a/apps/wallet-mobile/ios/Podfile.lock +++ b/apps/wallet-mobile/ios/Podfile.lock @@ -821,7 +821,7 @@ SPEC CHECKSUMS: amplitude-react-native: 1ea3d5e1f80ccc357dd178c55c29e51c89f1cd11 boost: 57d2868c099736d80fcd648bf211b4431e51a558 BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 - DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXBarCodeScanner: 8e23fae8d267dbef9f04817833a494200f1fce35 EXCamera: 0fbfa338a3776af2722d626a3437abe33f708aad @@ -838,7 +838,7 @@ SPEC CHECKSUMS: FBLazyVector: 12ea01e587c9594e7b144e1bfc86ac4d9ac28fde FBReactNativeSpec: faca7d16c37626ca5780a87adef703817722fe61 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libaom: 144606b1da4b5915a1054383c3a4459ccdb3c661 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index 407c76ce46..877a68d992 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -99,7 +99,7 @@ "@emurgo/csl-mobile-bridge": "6.0.0-alpha.9", "@emurgo/react-native-blockies-svg": "^0.0.2", "@emurgo/react-native-hid": "^5.15.6", - "@emurgo/yoroi-lib": "0.15.4-alpha.6", + "@emurgo/yoroi-lib": "0.15.4-alpha.7", "@formatjs/intl-datetimeformat": "^6.7.0", "@formatjs/intl-getcanonicallocales": "^2.1.0", "@formatjs/intl-locale": "^3.2.1", diff --git a/apps/wallet-mobile/src/components/Icon/Connection.tsx b/apps/wallet-mobile/src/components/Icon/Connection.tsx new file mode 100644 index 0000000000..389cdee819 --- /dev/null +++ b/apps/wallet-mobile/src/components/Icon/Connection.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +type Props = { + size?: number + color?: string +} + +export const Connection = ({size = 36, color = '#000000'}: Props) => { + return ( + + + + ) +} diff --git a/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx b/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx index 5092c39e1d..90023ecf54 100644 --- a/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx +++ b/apps/wallet-mobile/src/components/Icon/Icon.stories.tsx @@ -272,6 +272,10 @@ storiesOf('Icon', module).add('Gallery', () => { } title="Square" /> } title="Google" /> + + } title="Connection" /> + + } title="YoroiApp" /> ) diff --git a/apps/wallet-mobile/src/components/Icon/YoroiApp.tsx b/apps/wallet-mobile/src/components/Icon/YoroiApp.tsx new file mode 100644 index 0000000000..4bcda88191 --- /dev/null +++ b/apps/wallet-mobile/src/components/Icon/YoroiApp.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import Svg, {Defs, LinearGradient, Path, Rect, Stop} from 'react-native-svg' + +type Props = { + size?: number +} + +export const YoroiApp = ({size = 36}: Props) => { + return ( + + + + + + + + + + + + + + ) +} diff --git a/apps/wallet-mobile/src/components/Icon/index.ts b/apps/wallet-mobile/src/components/Icon/index.ts index b04759e0c7..25a590bc3b 100644 --- a/apps/wallet-mobile/src/components/Icon/index.ts +++ b/apps/wallet-mobile/src/components/Icon/index.ts @@ -20,6 +20,7 @@ import {Clock} from './Clock' import {Close} from './Close' import {Coins} from './Coins' import {Collateral} from './Collateral' +import {Connection} from './Connection' import {Copy} from './Copy' import {CopySuccess} from './CopySuccess' import {Cross} from './Cross' @@ -120,6 +121,7 @@ import {Wallets} from './Wallets' import {WalletStack} from './WalletStack' import {Warning} from './Warning' import {WingRiders} from './WingRiders' +import {YoroiApp} from './YoroiApp' import {YoroiNightly} from './YoroiNightly' import {YoroiWallet} from './YoroiWallet' @@ -248,4 +250,6 @@ export const Icon = { Reload, Square, Google, + Connection, + YoroiApp, } diff --git a/apps/wallet-mobile/src/components/Modal/ModalContext.tsx b/apps/wallet-mobile/src/components/Modal/ModalContext.tsx index 7dd04387ab..7cf9316741 100644 --- a/apps/wallet-mobile/src/components/Modal/ModalContext.tsx +++ b/apps/wallet-mobile/src/components/Modal/ModalContext.tsx @@ -1,4 +1,4 @@ -import {useNavigation} from '@react-navigation/native' +import {NavigationProp, useNavigation} from '@react-navigation/native' import React from 'react' import {Keyboard} from 'react-native' @@ -10,7 +10,12 @@ type ModalState = { isLoading: boolean } type ModalActions = { - openModal: (title: ModalState['title'], content: ModalState['content'], height?: ModalState['height']) => void + openModal: ( + title: ModalState['title'], + content: ModalState['content'], + height?: ModalState['height'], + onClose?: () => void, + ) => void closeModal: () => void startLoading: () => void stopLoading: () => void @@ -35,18 +40,25 @@ export const ModalProvider = ({ }) => { const [state, dispatch] = React.useReducer(modalReducer, {...defaultState, ...initialState}) const navigation = useNavigation() + const onCloseRef = React.useRef<() => void>() const actions = React.useRef({ closeModal: () => { - const lastRouteName = navigation.getState().routes.slice(-1)[0].name - if (lastRouteName === 'modal') { + if (getLastRouteName(navigation) === 'modal') { dispatch({type: 'close'}) navigation.goBack() + onCloseRef.current?.() } }, - openModal: (title: ModalState['title'], content: ModalState['content'], height?: ModalState['height']) => { + openModal: ( + title: ModalState['title'], + content: ModalState['content'], + height?: ModalState['height'], + onClose?: () => void, + ) => { Keyboard.dismiss() dispatch({type: 'open', title, content, height}) navigation.navigate('modal') + onCloseRef.current = onClose }, startLoading: () => dispatch({type: 'startLoading'}), stopLoading: () => dispatch({type: 'stopLoading'}), @@ -96,3 +108,7 @@ const defaultState: ModalState = Object.freeze({ isOpen: false, isLoading: false, }) + +const getLastRouteName = (navigation: NavigationProp) => { + return navigation.getState().routes.slice(-1)[0].name +} diff --git a/apps/wallet-mobile/src/features/Discover/DiscoverNavigator.tsx b/apps/wallet-mobile/src/features/Discover/DiscoverNavigator.tsx index c07d40ef68..a47550e8cb 100644 --- a/apps/wallet-mobile/src/features/Discover/DiscoverNavigator.tsx +++ b/apps/wallet-mobile/src/features/Discover/DiscoverNavigator.tsx @@ -1,6 +1,6 @@ import {createStackNavigator} from '@react-navigation/stack' import {useAsyncStorage} from '@yoroi/common' -import {DappConnectorProvider} from '@yoroi/dapp-connector' +import {DappConnector, DappConnectorProvider} from '@yoroi/dapp-connector' import {useTheme} from '@yoroi/theme' import * as React from 'react' @@ -9,7 +9,9 @@ import {defaultStackNavigationOptions, DiscoverRoutes} from '../../navigation' import {useSelectedWallet} from '../WalletManager/context/SelectedWalletContext' import {BrowserNavigator} from './BrowserNavigator' import {BrowserProvider} from './common/BrowserProvider' +import {useOpenConfirmConnectionModal} from './common/ConfirmConnectionModal' import {createDappConnector} from './common/helpers' +import {useConfirmRawTx} from './common/hooks' import {useStrings} from './common/useStrings' import {ListSkeleton} from './useCases/SelectDappFromList/ListSkeleton' import {SelectDappFromListScreen} from './useCases/SelectDappFromList/SelectDappFromListScreen' @@ -20,9 +22,7 @@ export const DiscoverNavigator = () => { const {atoms, color} = useTheme() const strings = useStrings() - const appStorage = useAsyncStorage() - const wallet = useSelectedWallet() - const manager = React.useMemo(() => createDappConnector(appStorage, wallet), [appStorage, wallet]) + const manager = useDappConnectorManager() return ( @@ -50,3 +50,56 @@ export const DiscoverNavigator = () => { ) } + +const useDappConnectorManager = () => { + const appStorage = useAsyncStorage() + const wallet = useSelectedWallet() + const {openConfirmConnectionModal} = useOpenConfirmConnectionModal() + const confirmRawTx = useConfirmRawTx(wallet) + + const confirmConnection = React.useCallback( + async (origin: string, manager: DappConnector) => { + const recommendedDApps = await manager.getDAppList() + const selectedDapp = recommendedDApps.dapps.find((dapp) => dapp.origins.includes(origin)) + return new Promise((resolve) => { + openConfirmConnectionModal({ + name: selectedDapp?.name ?? origin, + website: origin, + logo: selectedDapp?.logo ?? '', + onConfirm: () => resolve(true), + onClose: () => resolve(false), + }) + }) + }, + [openConfirmConnectionModal], + ) + + const signTx = React.useCallback(() => { + return new Promise((resolve, reject) => { + confirmRawTx({ + onConfirm: (rootKey) => { + resolve(rootKey) + return Promise.resolve() + }, + onClose: () => reject(new Error('User rejected')), + }) + }) + }, [confirmRawTx]) + + const signData = React.useCallback(() => { + return new Promise((resolve, reject) => { + confirmRawTx({ + onConfirm: (rootKey) => { + resolve(rootKey) + return Promise.resolve() + }, + onClose: () => reject(new Error('User rejected')), + }) + }) + }, [confirmRawTx]) + + return React.useMemo( + () => createDappConnector({appStorage, wallet, confirmConnection, signTx, signData}), + [appStorage, wallet, confirmConnection, signTx, signData], + ) +} diff --git a/apps/wallet-mobile/src/features/Discover/common/ConfirmConnectionModal.stories.tsx b/apps/wallet-mobile/src/features/Discover/common/ConfirmConnectionModal.stories.tsx new file mode 100644 index 0000000000..840d9ce35e --- /dev/null +++ b/apps/wallet-mobile/src/features/Discover/common/ConfirmConnectionModal.stories.tsx @@ -0,0 +1,39 @@ +import {action} from '@storybook/addon-actions' +import {storiesOf} from '@storybook/react-native' +import * as React from 'react' +import {View} from 'react-native' + +import {Button} from '../../../components' +import {ConfirmConnectionModal, useOpenConfirmConnectionModal} from './ConfirmConnectionModal' + +storiesOf('Discover ConfirmConnectionModal', module) + .addDecorator((story) => {story()}) + .add('initial', () => ) + .add('With Button', () => ) + +const Initial = () => { + return ( + + ) +} + +const WithButton = () => { + const {openConfirmConnectionModal} = useOpenConfirmConnectionModal() + + const handleOnPress = () => { + openConfirmConnectionModal({ + onConfirm: action('onConfirm'), + website: 'example.com', + name: 'Example DApp', + logo: 'https://daehx1qv45z7c.cloudfront.net/cardano-spot.png', + onClose: action('onClose'), + }) + } + + return