Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased (develop)

- added: WalletConnect support for Bitcoin (bip122), so proof-of-ownership signature requests from on-ramp partners work with existing BTC wallets

## 4.51.0 (staging)

- added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks.
Expand Down
7 changes: 3 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,7 @@ export default [
'src/components/scenes/SwapSuccessScene.tsx',

'src/components/scenes/WalletRestoreScene.tsx',
'src/components/scenes/WcConnectionsScene.tsx',
'src/components/scenes/WcConnectScene.tsx',

'src/components/scenes/WcDisconnectScene.tsx',
'src/components/scenes/WebViewScene.tsx',
'src/components/services/AccountCallbackManager.tsx',
Expand All @@ -330,7 +329,7 @@ export default [

'src/components/services/SortedWalletList.ts',
'src/components/services/StatusBarManager.tsx',
'src/components/services/WalletConnectService.tsx',

'src/components/services/WalletLifecycle.ts',
'src/components/services/WipeLogsService.tsx',

Expand Down Expand Up @@ -433,7 +432,7 @@ export default [
'src/hooks/useTokenDisplayData.ts',
'src/hooks/useTransactionList.ts',
'src/hooks/useUnmount.ts',
'src/hooks/useWalletConnect.tsx',

'src/hooks/useWalletsSubscriber.ts',
'src/hooks/useWhyDidYouUpdate.ts',
'src/locales/intl.ts',
Expand Down
187 changes: 187 additions & 0 deletions src/components/modals/WcSignMessageModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import type { EdgeCurrencyWallet } from 'edge-core-js'
import * as React from 'react'
import { Image, ScrollView, View } from 'react-native'
import type { AirshipBridge } from 'react-native-airship'

import WalletConnectLogo from '../../assets/images/walletconnect-logo.png'
import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings'
import { useHandler } from '../../hooks/useHandler'
import { useWalletConnect } from '../../hooks/useWalletConnect'
import { lstrings } from '../../locales/strings'
import { getCurrencyIconUris } from '../../util/CdnUris'
import { getWalletName } from '../../util/CurrencyWalletHelpers'
import { ModalButtons } from '../buttons/ModalButtons'
import { EdgeCard } from '../cards/EdgeCard'
import { FlashNotification } from '../navigation/FlashNotification'
import { EdgeRow } from '../rows/EdgeRow'
import { Airship, showError } from '../services/AirshipInstance'
import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
import { Alert } from '../themed/Alert'
import { ModalFooter, ModalTitle } from '../themed/ModalParts'
import { EdgeModal } from './EdgeModal'

interface Props {
bridge: AirshipBridge<void>
dAppIcon: string
dAppName: string
message: string
/** The address the session advertised, which is the one the dapp verifies
* the signature against. */
publicAddress: string
requestId: number
topic: string
wallet: EdgeCurrencyWallet
}

/**
* Approval prompt for a WalletConnect `signMessage` request, used by chains
* whose signing proves address ownership rather than moving funds (bip122
* proof of ownership for on-ramp partners). Signing is free and spends
* nothing, so it confirms with buttons rather than the slider the
* smart-contract modal uses for value transfers.
*/
export const WcSignMessageModal: React.FC<Props> = props => {
const {
bridge,
dAppIcon,
dAppName,
message,
publicAddress,
requestId,
topic,
wallet
} = props

const theme = useTheme()
const styles = getStyles(theme)
const walletConnect = useWalletConnect()

const [isSigning, setIsSigning] = React.useState(false)

const walletName = getWalletName(wallet)
const walletImageUri = getCurrencyIconUris(
wallet.currencyInfo.pluginId,
null
).symbolImage

const handleApprove = useHandler(async (): Promise<void> => {
setIsSigning(true)
try {
// `signMessage` signs the literal UTF-8 message, which is what the dapp
// verifies. `signBytes` would base64-re-encode first and sign the wrong
// data. BIP137 encodes the signing address' script type in the header
// byte, which SegWit verifiers require and which collapses to the legacy
// encoding for non-SegWit addresses.
// eslint-disable-next-line @typescript-eslint/no-deprecated
const signature = await wallet.signMessage(message, {
otherParams: { publicAddress, signatureFormat: 'bip137' }
})
await walletConnect.approveRequest(topic, requestId, {
address: publicAddress,
signature
})
Airship.show(bridge => (
<FlashNotification
bridge={bridge}
message={lstrings.wc_sign_message_confirmed}
onPress={() => {}}
/>
)).catch((err: unknown) => {
showError(err)
})
bridge.resolve()
} catch (error: unknown) {
await walletConnect.rejectRequest(topic, requestId)
showError(error)
bridge.resolve()
}
})

const handleReject = useHandler((): void => {
walletConnect.rejectRequest(topic, requestId).catch((err: unknown) => {
showError(err)
})
bridge.resolve()
})

return (
<EdgeModal
bridge={bridge}
// Dismissing while the signature is in flight would reject a request the
// approve path is about to answer, so the modal only closes when idle.
onCancel={isSigning ? undefined : handleReject}
title={
<View style={styles.title}>
<Image style={styles.logo} source={WalletConnectLogo} />
<ModalTitle>{lstrings.wc_sign_message_title}</ModalTitle>
</View>
}
>
<ScrollView
contentContainerStyle={styles.scrollPadding}
scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX}
>
<Alert
numberOfLines={0}
title={lstrings.wc_smartcontract_warning_title}
message={lstrings.wc_sign_message_warning_text}
type="warning"
/>
<EdgeCard icon={dAppIcon}>
<EdgeRow title={lstrings.wc_sign_message_dapp} body={dAppName} />
</EdgeCard>
<EdgeCard icon={walletImageUri}>
<EdgeRow title={lstrings.wc_sign_message_wallet} body={walletName} />
</EdgeCard>
<EdgeCard>
<EdgeRow
maximumHeight="large"
title={lstrings.wc_sign_message_address}
body={publicAddress}
/>
</EdgeCard>
{/* The message is what the user is authorizing, so it is never
truncated: an ellipsized tail would be signed unseen. */}
<EdgeCard>
<EdgeRow
maximumHeight="large"
title={lstrings.wc_sign_message_message}
body={message}
/>
Comment thread
cursor[bot] marked this conversation as resolved.
</EdgeCard>
Comment thread
cursor[bot] marked this conversation as resolved.
<ModalButtons
primary={{
label: lstrings.wc_sign_message_approve_button,
onPress: handleApprove,
spinner: isSigning,
testID: 'wcSignMessageSignButton'
}}
secondary={{
label: lstrings.wc_sign_message_reject_button,
onPress: handleReject,
disabled: isSigning,
testID: 'wcSignMessageRejectButton'
}}
/>
</ScrollView>
</EdgeModal>
)
}

const getStyles = cacheStyles((theme: Theme) => ({
title: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: theme.rem(1),
paddingTop: theme.rem(1)
},
logo: {
height: theme.rem(2),
width: theme.rem(2),
resizeMode: 'contain',
padding: theme.rem(0.5)
},
scrollPadding: {
paddingBottom: theme.rem(ModalFooter.bottomRem)
}
}))
3 changes: 2 additions & 1 deletion src/components/scenes/CreateWalletEditNameScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ const CreateWalletEditNameComponent: React.FC<Props> = props => {
const specialInfo = getSpecialCurrencyInfo(pluginId)
const namespace = specialInfo.walletConnectV2ChainId?.namespace
if (namespace === 'eip155') return lstrings.split_description_evm
if (namespace == null) return lstrings.split_description_utxo
if (namespace == null || namespace === 'bip122')
return lstrings.split_description_utxo
return lstrings.split_description
}, [splitSourceWalletId, currencyWallets])

Expand Down
15 changes: 9 additions & 6 deletions src/components/scenes/WcConnectScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { MAX_ADDRESS_CHARACTERS } from '../../constants/WalletAndCurrencyConstan
import { useAsyncEffect } from '../../hooks/useAsyncEffect'
import { useHandler } from '../../hooks/useHandler'
import { useUnmount } from '../../hooks/useUnmount'
import { useWalletConnect } from '../../hooks/useWalletConnect'
import {
getWalletConnectAddress,
useWalletConnect
} from '../../hooks/useWalletConnect'
import { useWalletName } from '../../hooks/useWalletName'
import { lstrings } from '../../locales/strings'
import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes'
Expand Down Expand Up @@ -70,14 +73,14 @@ export const WcConnectScene = withWallet((props: Props) => {

useAsyncEffect(
async () => {
const r = await wallet.getReceiveAddress({ tokenId: null })
setWalletAddress(r.publicAddress)
const address = await getWalletConnectAddress(wallet)
if (address != null) setWalletAddress(address)
},
[wallet],
'WcConnectScene'
)

const handleConnect = async () => {
const handleConnect = async (): Promise<void> => {
try {
await walletConnect.approveSession(proposal, wallet.id)
connected.current = true
Expand All @@ -87,7 +90,7 @@ export const WcConnectScene = withWallet((props: Props) => {
message={lstrings.wc_confirm_return_to_browser}
onPress={() => {}}
/>
)).catch(e => {
)).catch((e: unknown) => {
showError(e)
})
navigation.navigate('wcConnections', {})
Expand Down Expand Up @@ -121,7 +124,7 @@ export const WcConnectScene = withWallet((props: Props) => {
}
})

const renderWalletSelect = () => {
const renderWalletSelect = (): React.ReactElement => {
const walletNameStr = truncateString(walletName, MAX_ADDRESS_CHARACTERS)
const walletImage = (
<CryptoIcon pluginId={wallet.currencyInfo.pluginId} tokenId={null} />
Expand Down
21 changes: 16 additions & 5 deletions src/components/scenes/WcConnectionsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface WcConnectionsParams {
uri?: string
}

export const WcConnectionsScene = (props: Props) => {
export const WcConnectionsScene: React.FC<Props> = props => {
const { navigation, route } = props
const { uri } = route.params ?? {}
const theme = useTheme()
Expand All @@ -57,7 +57,7 @@ export const WcConnectionsScene = (props: Props) => {

useMount(() => {
if (uri != null)
onScanSuccess(uri).catch(err => {
onScanSuccess(uri).catch((err: unknown) => {
showError(err)
})
})
Expand All @@ -72,7 +72,7 @@ export const WcConnectionsScene = (props: Props) => {
'WcConnectionsScene'
)

const onScanSuccess = async (qrResult: string) => {
const onScanSuccess = async (qrResult: string): Promise<void> => {
setConnecting(true)
try {
let proposal = sessionProposal.get(qrResult)
Expand Down Expand Up @@ -119,11 +119,13 @@ export const WcConnectionsScene = (props: Props) => {
setConnecting(false)
}

const handleActiveConnectionPress = (wcConnectionInfo: WcConnectionInfo) => {
const handleActiveConnectionPress = (
wcConnectionInfo: WcConnectionInfo
): void => {
navigation.navigate('wcDisconnect', { wcConnectionInfo })
}

const handleNewConnectionPress = async () => {
const handleNewConnectionPress = async (): Promise<void> => {
if (checkAndShowLightBackupModal(account, navigation as NavigationBase)) {
await Promise.resolve()
} else {
Expand Down Expand Up @@ -323,5 +325,14 @@ const getProposalNamespaceCompatibleEdgeTokenIds = (
throw new Error(NO_WALLETS_DAPP_REQUIREMENTS)
}

// A dapp that lists its chains as optional never trips the check above, so an
// unsupported chain reaches here as an empty match set. Handing that to the
// wallet picker as `allowedAssets` would filter every wallet out and leave
// only the create-wallet rows, which reads as "Edge wants me to make a new
// wallet" instead of "Edge cannot serve this dapp".
if (edgeTokenIdMap.size === 0) {
throw new Error(NO_WALLETS_DAPP_REQUIREMENTS)
}

return [...edgeTokenIdMap.values()]
}
Loading
Loading