Skip to content

Commit

Permalink
feat: on-chain WC signing
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Oct 18, 2023
1 parent 2453d54 commit c9b4086
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/components/tx-flow/flows/SignMessage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Box, Typography } from '@mui/material'
import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard'

const APP_LOGO_FALLBACK_IMAGE = '/images/apps/apps-icon.svg'
const APP_NAME_FALLBACK = 'Sign message off-chain'
const APP_NAME_FALLBACK = 'Sign message'

export const AppTitle = ({ name, logoUri }: { name?: string | null; logoUri?: string | null }) => {
const appName = name || APP_NAME_FALLBACK
Expand Down
9 changes: 9 additions & 0 deletions src/services/safe-wallet-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ type SafeInfo = {
chainId: number
}

type SafeSettings = {
offChainSigning?: boolean
}

export type AppInfo = {
name: string
description: string
Expand All @@ -19,6 +23,7 @@ export type WalletSDK = {
) => Promise<{ safeTxHash: string; txHash?: string }>
getBySafeTxHash: (safeTxHash: string) => Promise<{ txHash?: string }>
switchChain: (chainId: string, appInfo: AppInfo) => Promise<null>
setSafeSettings: (safeSettings: SafeSettings) => SafeSettings
proxy: (method: string, params: unknown[]) => Promise<unknown>
}

Expand Down Expand Up @@ -181,6 +186,10 @@ export class SafeWalletProvider {
return this.sdk.proxy(method, params)
}

case 'safe_setSettings': {
return this.sdk.setSafeSettings(params[0] as SafeSettings)
}

default: {
return await this.sdk.proxy(method, params)
}
Expand Down
57 changes: 53 additions & 4 deletions src/services/safe-wallet-provider/useSafeWalletProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import * as router from 'next/router'

import * as web3 from '@/hooks/wallets/web3'
import * as notifications from './notifications'
import { renderHook } from '@/tests/test-utils'
import { act, renderHook } from '@/tests/test-utils'
import { TxModalContext } from '@/components/tx-flow'
import useSafeWalletProvider, { _useTxFlowApi } from './useSafeWalletProvider'
import { SafeWalletProvider } from '.'
import { StoreHydrator } from '@/store'
import { StoreHydrator, makeStore } from '@/store'
import * as messages from '@/utils/safe-messages'
import { createStoreHydrator } from '@/store/storeHydrator'

const appInfo = {
name: 'test',
Expand Down Expand Up @@ -61,8 +63,9 @@ describe('useSafeWalletProvider', () => {
expect(result.current?.proxy).toBeDefined()
})

it('should open signing window for messages', () => {
it('should open signing window for off-chain messages', () => {
jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter)
jest.spyOn(messages, 'isOffchainEIP1271Supported').mockReturnValue(true)
const showNotificationSpy = jest.spyOn(notifications, 'showNotification')

const mockSetTxFlow = jest.fn()
Expand Down Expand Up @@ -92,7 +95,53 @@ describe('useSafeWalletProvider', () => {
expect(resp).toBeInstanceOf(Promise)
})

it('should open signing window for typed messages', () => {
it('should open a signing window for on-chain messages', async () => {
jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter)
jest.spyOn(messages, 'isOffchainEIP1271Supported').mockReturnValue(true)
const showNotificationSpy = jest.spyOn(notifications, 'showNotification')

const mockSetTxFlow = jest.fn()

const StoreHydrator = createStoreHydrator(() =>
makeStore({ settings: { signing: { useOnChainSigning: false } } }),
)

const { result } = renderHook(() => _useTxFlowApi('1', '0x1234567890000000000000000000000000000000'), {
// TODO: Improve render/renderHook to allow custom wrappers within the "defaults"
wrapper: ({ children }) => (
<StoreHydrator>
<TxModalContext.Provider value={{ setTxFlow: mockSetTxFlow } as any}>{children}</TxModalContext.Provider>
</StoreHydrator>
),
})

act(() => {
// Set Safe settings to on-chain signing
const resp1 = result.current?.setSafeSettings({ offChainSigning: false })

expect(resp1).toStrictEqual({ offChainSigning: false })
})

const resp2 = result?.current?.signMessage('message', appInfo)

expect(showNotificationSpy).toHaveBeenCalledWith('Signature request', {
body: 'test wants you to sign a message. Open the Safe{Wallet} to continue.',
})

// SignMessageOnChainFlow props
expect(mockSetTxFlow.mock.calls[0][0].props).toStrictEqual({
props: {
appId: undefined,
requestId: expect.any(String),
message: 'message',
method: 'signMessage',
},
})

expect(resp2).toBeInstanceOf(Promise)
})

it('should open signing window for off-chain typed messages', () => {
jest.spyOn(router, 'useRouter').mockReturnValue({} as unknown as router.NextRouter)
const showNotificationSpy = jest.spyOn(notifications, 'showNotification')

Expand Down
64 changes: 55 additions & 9 deletions src/services/safe-wallet-provider/useSafeWalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useEffect, useMemo, useRef } from 'react'
import { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { BigNumber } from 'ethers'
import { useRouter } from 'next/router'

Expand All @@ -11,29 +11,41 @@ import SignMessageFlow from '@/components/tx-flow/flows/SignMessage'
import { safeMsgSubscribe, SafeMsgEvent } from '@/services/safe-messages/safeMsgEvents'
import SafeAppsTxFlow from '@/components/tx-flow/flows/SafeAppsTx'
import { TxEvent, txSubscribe } from '@/services/tx/txEvents'
import type { EIP712TypedData } from '@safe-global/safe-apps-sdk'
import { Methods } from '@safe-global/safe-apps-sdk'
import type { EIP712TypedData, SafeSettings } from '@safe-global/safe-apps-sdk'
import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
import { getTransactionDetails } from '@safe-global/safe-gateway-typescript-sdk'
import { getAddress } from 'ethers/lib/utils'
import { AppRoutes } from '@/config/routes'
import useChains from '@/hooks/useChains'
import useChains, { useCurrentChain } from '@/hooks/useChains'
import { NotificationMessages, showNotification } from './notifications'
import { SafeAppsTag } from '@/config/constants'
import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps'
import SignMessageOnChainFlow from '@/components/tx-flow/flows/SignMessageOnChain'
import { useAppSelector } from '@/store'
import { selectOnChainSigning } from '@/store/settingsSlice'
import { isOffchainEIP1271Supported } from '@/utils/safe-messages'

const useWalletConnectApp = () => {
const [matchingApps] = useRemoteSafeApps(SafeAppsTag.WALLET_CONNECT)
return matchingApps?.[0]
}

export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK | undefined => {
const { safe } = useSafeInfo()
const currentChain = useCurrentChain()
const { setTxFlow } = useContext(TxModalContext)
const web3ReadOnly = useWeb3ReadOnly()
const router = useRouter()
const { configs } = useChains()
const pendingTxs = useRef<Record<string, string>>({})
const wcApp = useWalletConnectApp()

const onChainSigning = useAppSelector(selectOnChainSigning)
const [settings, setSettings] = useState<SafeSettings>({
offChainSigning: true,
})

useEffect(() => {
const unsubscribe = txSubscribe(TxEvent.PROCESSING, async ({ txId, txHash }) => {
if (!txId) return
Expand All @@ -45,10 +57,20 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK |
return useMemo<WalletSDK | undefined>(() => {
if (!chainId || !safeAddress) return

const signMessage = (message: string | EIP712TypedData, appInfo: AppInfo): Promise<{ signature: string }> => {
const signMessage = (
message: string | EIP712TypedData,
appInfo: AppInfo,
method: Methods.signMessage | Methods.signTypedMessage,
): Promise<{ signature: string }> => {
const id = Math.random().toString(36).slice(2)
setTxFlow(<SignMessageFlow logoUri={appInfo.iconUrl} name={appInfo.name} message={message} requestId={id} />)

const shouldSignOffChain =
isOffchainEIP1271Supported(safe, currentChain) && !onChainSigning && settings.offChainSigning

if (shouldSignOffChain) {
setTxFlow(<SignMessageFlow logoUri={appInfo.iconUrl} name={appInfo.name} message={message} requestId={id} />)
} else {
setTxFlow(<SignMessageOnChainFlow props={{ appId: wcApp?.id, requestId: id, message, method }} />)
}
const { title, options } = NotificationMessages.SIGNATURE_REQUEST(appInfo)
showNotification(title, options)

Expand Down Expand Up @@ -80,11 +102,11 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK |

return {
async signMessage(message, appInfo) {
return await signMessage(message, appInfo)
return await signMessage(message, appInfo, Methods.signMessage)
},

async signTypedMessage(typedData, appInfo) {
return await signMessage(typedData as EIP712TypedData, appInfo)
return await signMessage(typedData as EIP712TypedData, appInfo, Methods.signTypedMessage)
},

async send(params: { txs: any[]; params: { safeTxGas: number } }, appInfo) {
Expand Down Expand Up @@ -172,12 +194,36 @@ export const _useTxFlowApi = (chainId: string, safeAddress: string): WalletSDK |
return null
},

setSafeSettings(newSettings) {
const res = {
...settings,
...newSettings,
}

setSettings(newSettings)

return res
},

async proxy(method, params) {
const data = await web3ReadOnly?.send(method, params)
return data.result
},
}
}, [chainId, safeAddress, setTxFlow, wcApp?.url, configs, router, web3ReadOnly])
}, [
chainId,
safeAddress,
safe,
currentChain,
onChainSigning,
settings,
setTxFlow,
wcApp?.id,
wcApp?.url,
configs,
router,
web3ReadOnly,
])
}

const useSafeWalletProvider = (): SafeWalletProvider | undefined => {
Expand Down
2 changes: 1 addition & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const _hydrationReducer: typeof rootReducer = (state, action) => {
return rootReducer(state, action)
}

const makeStore = (initialState?: Record<string, any>) => {
export const makeStore = (initialState?: Record<string, any>) => {
return configureStore({
reducer: _hydrationReducer,
middleware: (getDefaultMiddleware) => {
Expand Down
15 changes: 15 additions & 0 deletions src/utils/__tests__/safe-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,20 @@ describe('safe-messages', () => {
),
).toBeFalsy()
})

it('true for no safeAppsSdk version', () => {
expect(
isOffchainEIP1271Supported(
{
chainId: '5',
version: '1.3.0',
fallbackHandler: { value: hexZeroPad('0x2222', 20) },
} as any,
{
features: [FEATURES.EIP1271],
} as any,
),
).toBeTruthy()
})
})
})
4 changes: 2 additions & 2 deletions src/utils/safe-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const generateSafeMessageHash = (safe: SafeInfo, message: SafeMessage['me
export const isOffchainEIP1271Supported = (
{ version, fallbackHandler }: SafeInfo,
chain: ChainInfo | undefined,
sdkVersion: string,
sdkVersion?: string,
): boolean => {
if (!version) {
return false
Expand All @@ -85,7 +85,7 @@ export const isOffchainEIP1271Supported = (
}

// If the Safe apps sdk does not support off-chain signing yet
if (!gte(sdkVersion, EIP1271_OFFCHAIN_SUPPORTED_SAFE_APPS_SDK_VERSION)) {
if (sdkVersion && !gte(sdkVersion, EIP1271_OFFCHAIN_SUPPORTED_SAFE_APPS_SDK_VERSION)) {
return false
}

Expand Down

0 comments on commit c9b4086

Please sign in to comment.