Skip to content

Commit

Permalink
refactor(wallet-mobile): tx review cbor signing generalization (#3794)
Browse files Browse the repository at this point in the history
Signed-off-by: banklesss <[email protected]>
  • Loading branch information
banklesss authored Jan 28, 2025
1 parent 1e13d28 commit 4a7eb49
Show file tree
Hide file tree
Showing 22 changed files with 429 additions and 288 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {YoroiSignedTx, YoroiUnsignedTx} from '../../yoroi-wallets/types/yoroi'
type Props = {
onSuccess?: (signedTx: YoroiSignedTx) => void
unsignedTx: YoroiUnsignedTx
onError?: () => void
onError?: (error: unknown) => void
}

export const ConfirmTxWithOsModal = ({onSuccess, unsignedTx, onError}: Props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {useStrings} from './strings'
type Props = {
onSuccess?: (signedTx: YoroiSignedTx) => void
unsignedTx: YoroiUnsignedTx
onError?: () => void
onError?: (error: unknown) => void
}

export const ConfirmTxWithSpendingPasswordModal = ({onSuccess, unsignedTx, onError}: Props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type OnConfirmOptions = {transportType: TransportType; deviceInfo: HW.DeviceInfo

type Props = {
onConfirm: (options: OnConfirmOptions) => Promise<void>
onClose: () => void
onCancel: () => void
onClose?: () => void
onCancel?: () => void
}

export const useConfirmHWConnectionModal = () => {
Expand Down Expand Up @@ -104,23 +104,23 @@ const ConfirmHWConnectionModal = ({onConfirm}: Pick<Props, 'onConfirm'>) => {
}

const useStyles = () => {
const {color} = useTheme()
const {color, atoms} = useTheme()

const colors = {
spinner: color.gray_max,
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
...atoms.flex_1,
...atoms.align_center,
...atoms.justify_center,
gap: 35,
},
text: {
fontSize: 18,
color: color.gray_max,
textAlign: 'center',
...atoms.text_center,
},
})

Expand Down
51 changes: 0 additions & 51 deletions apps/wallet-mobile/src/features/Discover/common/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import {DappConnectorManager, useDappConnector} from '@yoroi/dapp-connector'
import * as React from 'react'
import {WebView, WebViewMessageEvent} from 'react-native-webview'

import {useModal} from '../../../components/Modal/ModalContext'
import {logger} from '../../../kernel/logger/logger'
import {YoroiWallet} from '../../../yoroi-wallets/cardano/types'
import {ConfirmRawTxWithOs} from '../../Swap/common/ConfirmRawTx/ConfirmRawTxWithOs'
import {ConfirmRawTxWithPassword} from '../../Swap/common/ConfirmRawTx/ConfirmRawTxWithPassword'
import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet'
import {useStrings} from './useStrings'
import {walletConfig} from './wallet-config'

export const useConnectWalletToWebView = (wallet: YoroiWallet, webViewRef: React.RefObject<WebView | null>) => {
Expand Down Expand Up @@ -56,49 +51,3 @@ const getInitScript = (sessionId: string, dappConnector: DappConnectorManager) =
sessionId,
})
}

type PromptRootKeyOptions = {
onConfirm: (rootKey: string) => Promise<void>
onClose: () => void
title?: string
summary?: string
}

export const usePromptRootKey = () => {
const {openModal, closeModal} = useModal()
const {meta} = useSelectedWallet()
const strings = useStrings()
const modalHeight = 350

return React.useCallback(
({onConfirm, onClose, title, summary}: PromptRootKeyOptions) => {
const handleOnConfirm = async (rootKey: string) => {
const result = await onConfirm(rootKey)
closeModal()
return result
}

if (meta.isHW) {
throw new Error('Not implemented yet')
}

if (meta.isEasyConfirmationEnabled) {
openModal({
title: title ?? strings.confirmTx,
content: <ConfirmRawTxWithOs onConfirm={handleOnConfirm} />,
height: modalHeight,
onClose,
})
return
}

openModal({
title: title ?? strings.confirmTx,
content: <ConfirmRawTxWithPassword summary={summary} onConfirm={handleOnConfirm} />,
height: modalHeight,
onClose,
})
},
[meta.isHW, meta.isEasyConfirmationEnabled, openModal, strings.confirmTx, closeModal],
)
}
140 changes: 41 additions & 99 deletions apps/wallet-mobile/src/features/Discover/useDappConnectorManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import {useAsyncStorage} from '@yoroi/common'
import {DappConnector} from '@yoroi/dapp-connector'
import * as React from 'react'
import {InteractionManager} from 'react-native'
import {useMutation} from 'react-query'

import {logger} from '../../kernel/logger/logger'
import {useWalletNavigation} from '../../kernel/navigation'
import {isEmptyString} from '../../kernel/utils'
import {cip30LedgerExtensionMaker} from '../../yoroi-wallets/cardano/cip30/cip30-ledger'
import {BaseLedgerError} from '../../yoroi-wallets/hw/hw'
import {usePromptRootKey} from '../ReviewTx/common/hooks/usePromptRootKey'
import {CreatedByInfoItem} from '../ReviewTx/useCases/ReviewTxScreen/ReviewTx/Overview/OverviewTab'
import {useSelectedWallet} from '../WalletManager/common/hooks/useSelectedWallet'
import {useBrowser} from './common/BrowserProvider'
import {useOpenConfirmConnectionModal} from './common/ConfirmConnectionModal'
import {useConfirmHWConnectionModal} from './common/ConfirmHWConnectionModal'
import {userRejectedError} from './common/errors'
import {createDappConnector} from './common/helpers'
import {usePromptRootKey} from './common/hooks'
import {useOpenUnverifiedDappModal} from './common/UnverifiedDappModal'
import {useNavigateTo} from './common/useNavigateTo'
import {useStrings} from './common/useStrings'
Expand All @@ -36,9 +36,6 @@ export const useDappConnectorManager = () => {
const signData = useSignData()
const signDataWithHW = useSignDataWithHW()

const promptRootKey = useConnectorPromptRootKey()
const {sign: signTxWithHW} = useSignTxWithHW()

const handleSignTx = React.useCallback(
({cbor, manager}: {cbor: string; manager: DappConnector}) => {
return new Promise<string>((resolve, reject) => {
Expand All @@ -49,23 +46,37 @@ export const useDappConnectorManager = () => {
navigateToTxReview({
cbor,
createdBy: matchingDapp != null && <CreatedByInfoItem logo={matchingDapp.logo} url={matchingDapp.uri} />,
onConfirm: async () => {
if (!shouldResolve) return
onSuccess: (args) => {
shouldResolve = false
const rootKey = await promptRootKey()
resolve(rootKey)
if (isEmptyString(args?.rootKey) || args?.rootKey == null) {
reject(new Error('useDappConnectorManager::handleSignTx: invalid state'))
return
}

resolve(args?.rootKey)
navigateTo.browseDapp()
},
onCancel: () => {
if (!shouldResolve) return
shouldResolve = false
reject(userRejectedError())
},
onClose: () => {
if (shouldResolve) {
shouldResolve = false
reject(userRejectedError())
}
},
onError: (error) => {
shouldResolve = false
logger.error('useDappConnectorManager::handleSignTx', {error})
reject(error)
},
})
})
})
},
[activeTabOrigin, navigateToTxReview, promptRootKey, navigateTo],
[activeTabOrigin, navigateToTxReview, navigateTo],
)

const handleSignTxWithHW = React.useCallback(
Expand All @@ -77,32 +88,37 @@ export const useDappConnectorManager = () => {
activeTabOrigin != null ? dapps.find((dapp) => dapp.origins.includes(activeTabOrigin)) : null
navigateToTxReview({
cbor,
partial,
createdBy: matchingDapp != null && <CreatedByInfoItem logo={matchingDapp.logo} url={matchingDapp.uri} />,
onConfirm: () => {
if (!shouldResolve) return
onSuccess: (args) => {
shouldResolve = false
signTxWithHW(
{cbor, partial},
{
onSuccess: (signature) => resolve(signature),
onError: (error) => {
logger.error('ReviewTransaction::handleOnConfirm', {error})
reject(error)
},
},
)
if (!args?.tx) {
reject(new Error('useDappConnectorManager::handleSignTxWithHW: invalid state'))
return
}
resolve(args?.tx)
navigateTo.browseDapp()
},
onError: (error) => {
shouldResolve = false
logger.error('useDappConnectorManager::handleSignTxWithHW', {error})
reject(error)
},
onCancel: () => {
if (!shouldResolve) return
shouldResolve = false
reject(userRejectedError())
},
onClose: () => {
if (!shouldResolve) return
shouldResolve = false
reject(userRejectedError())
},
})
})
})
},
[activeTabOrigin, navigateToTxReview, navigateTo, signTxWithHW],
[activeTabOrigin, navigateToTxReview, navigateTo],
)

return React.useMemo(
Expand All @@ -122,7 +138,7 @@ export const useDappConnectorManager = () => {
}

const useSignData = () => {
const promptRootKey = usePromptRootKey()
const {promptRootKey} = usePromptRootKey()
const strings = useStrings()

return React.useCallback(
Expand All @@ -135,7 +151,7 @@ const useSignData = () => {
promptRootKey({
title,
summary,
onConfirm: (rootKey) => {
onSuccess: (rootKey) => {
resolve(rootKey)
shouldResolveOnClose = false
return Promise.resolve()
Expand Down Expand Up @@ -241,77 +257,3 @@ const useConfirmConnection = () => {
[openConfirmConnectionModal, openUnverifiedDappModal, closeModal],
)
}

const useConnectorPromptRootKey = () => {
const promptRootKey = usePromptRootKey()

return React.useCallback(() => {
return new Promise<string>((resolve, reject) => {
let shouldResolveOnClose = true

try {
promptRootKey({
onConfirm: (rootKey) => {
resolve(rootKey)
shouldResolveOnClose = false
return Promise.resolve()
},
onClose: () => {
if (shouldResolveOnClose) reject(userRejectedError())
},
})
} catch (error) {
reject(error)
}
})
}, [promptRootKey])
}

export const useSignTxWithHW = () => {
const {confirmHWConnection, closeModal} = useConfirmHWConnectionModal()
const {wallet, meta} = useSelectedWallet()

const mutationFn = React.useCallback(
(options: {cbor: string; partial?: boolean}) => {
return new Promise<Transaction>((resolve, reject) => {
let isClosed = false
confirmHWConnection({
onConfirm: async ({transportType, deviceInfo}) => {
try {
const cip30 = cip30LedgerExtensionMaker(wallet, meta)
const tx = await cip30.signTx(options.cbor, options.partial ?? false, deviceInfo, transportType === 'USB')
resolve(tx)
isClosed = true
closeModal()
} catch (error) {
if (error instanceof BaseLedgerError) {
throw error
}
reject(error)
isClosed = true
closeModal()
}
},
onCancel: () => {
reject(userRejectedError())
isClosed = true
closeModal()
},
onClose: () => {
if (isClosed) return
reject(userRejectedError())
},
})
})
},
[confirmHWConnection, wallet, meta, closeModal],
)

const mutation = useMutation<Transaction, Error, {cbor: string; partial?: boolean}>({
mutationFn,
useErrorBoundary: false,
mutationKey: ['useSignTxWithHW'],
})

return {...mutation, sign: mutation.mutate}
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ const useGenerateVotingKeys = (

const catalystKeyHex = Buffer.from(catalystKey).toString('hex')

const password = Buffer.from(pin.split('').map(Number))
const password = new Uint8Array(Buffer.from(pin.split('').map(Number)))
const votingKeyEncrypted = await encryptWithPassword(password, catalystKey)

return {
Expand Down
Loading

0 comments on commit 4a7eb49

Please sign in to comment.