Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
29 changes: 29 additions & 0 deletions packages/chain-adapters/src/error/ErrorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'

import { ChainAdapterError, ErrorHandler } from './ErrorHandler'

const metadata = { translation: 'chainAdapters.errors.signMessage' }

describe('ErrorHandler', () => {
it('includes the contents of a thrown non-Error object in the message', async () => {
const failureEvent = {
message_type: 'FAILURE',
message_enum: 3,
message: { code: 1, message: 'Malformed packet' },
from_wallet: true,
}

const err = await ErrorHandler(failureEvent, metadata).catch(e => e)

expect(err).toBeInstanceOf(ChainAdapterError)
expect(err.message).toBe(`Unknown Error: ${JSON.stringify(failureEvent)}`)
expect(err.metadata).toEqual(metadata)
})

it('falls back to String for a thrown object that cannot be serialized', async () => {
const err = await ErrorHandler({ amount: 1n }, metadata).catch(e => e)

expect(err).toBeInstanceOf(ChainAdapterError)
expect(err.message).toBe('Unknown Error: [object Object]')
})
})
10 changes: 9 additions & 1 deletion packages/chain-adapters/src/error/ErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const isResponseError = (err: unknown): err is unchained.ResponseError => {
)
}

const stringifyError = (error: unknown): string => {
try {
return JSON.stringify(error) ?? String(error)
} catch {
return String(error)
}
}

export class ChainAdapterError extends Error {
metadata: ErrorMetadata

Expand All @@ -25,7 +33,7 @@ export class ChainAdapterError extends Error {
} else if (error instanceof Error || error instanceof ChainAdapterError) {
super(error.message)
} else {
super(`Unknown Error: ${error}`)
super(`Unknown Error: ${stringifyError(error)}`)
}

this.name = this.constructor.name
Expand Down
6 changes: 5 additions & 1 deletion packages/chain-adapters/src/evm/EvmBaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,11 @@ export abstract class EvmBaseAdapter<T extends EvmChainId> implements IChainAdap
await this.assertSwitchChain(wallet)
await verifyLedgerAppOpen(this.chainId, wallet)

const signedMessage = await wallet.ethSignMessage(messageToSign)
const message = isHex(messageToSign.message)
? messageToSign.message
: toHex(messageToSign.message)

const signedMessage = await wallet.ethSignMessage({ ...messageToSign, message })

if (!signedMessage) throw new Error('error signing message')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,39 @@ describe('EthereumChainAdapter', () => {

await expect(adapter.signMessage(message)).rejects.toThrow(/error signing message/)
})

it('should hex encode a utf8 message before passing it to the wallet', async () => {
const adapter = new ethereum.ChainAdapter(makeChainAdapterArgs())
const wallet = await getWallet()
const addressNList = toAddressNList(adapter.getBip44Params({ accountNumber: 0 }))

const ethSignMessage = vi.fn().mockResolvedValue({ address: '0x', signature: '0xsig' })
wallet.ethSignMessage = ethSignMessage

await adapter.signMessage({
wallet,
messageToSign: { message: 'Hello world 111', addressNList },
})

expect(ethSignMessage).toHaveBeenCalledWith({
message: '0x48656c6c6f20776f726c6420313131',
addressNList,
})
})

it('should pass a hex message to the wallet unchanged', async () => {
const adapter = new ethereum.ChainAdapter(makeChainAdapterArgs())
const wallet = await getWallet()
const addressNList = toAddressNList(adapter.getBip44Params({ accountNumber: 0 }))
const message = '0x48656c6c6f20776f726c6420313131'

const ethSignMessage = vi.fn().mockResolvedValue({ address: '0x', signature: '0xsig' })
wallet.ethSignMessage = ethSignMessage

await adapter.signMessage({ wallet, messageToSign: { message, addressNList } })

expect(ethSignMessage).toHaveBeenCalledWith({ message, addressNList })
})
})

describe('broadcastTransaction', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/hdwallet-integration/src/wallets/trezor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class MockTransport extends trezor.TrezorTransport {
{
path: [2147483692, 2147483708, 2147483648, 0, 0],
message: '0x48656c6c6f20576f726c64', // "Hello World"
hex: true,
},
{
success: true,
Expand Down
4 changes: 4 additions & 0 deletions packages/hdwallet-trezor/src/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,12 @@ export async function ethSignMessage(
const res = await transport.call('ethereumSignMessage', {
path: msg.addressNList,
message: msg.message,
hex: true,
})
handleError(transport, res, 'Could not sign ETH message with Trezor')
if (!res.payload.signature) {
throw new Error(`Could not sign ETH message with Trezor: '${JSON.stringify(res.payload)}'`)
}
return {
address: res.payload.address,
signature: '0x' + res.payload.signature,
Expand Down
39 changes: 26 additions & 13 deletions src/plugins/walletConnectToDapps/WalletConnectModalManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,21 +207,34 @@ export const WalletConnectModalManager: FC<WalletConnectModalManagerProps> = ({
const selectedAccountId = customTransactionData?.accountId || accountId
const selectedAccountMetadata = portfolioAccountMetadata[selectedAccountId ?? '']

const approvalResponse = await approveSessionAuthRequest({
wallet,
web3wallet,
sessionAuthRequest,
customTransactionData,
accountId,
accountMetadata: selectedAccountMetadata,
})
try {
const approvalResponse = await approveSessionAuthRequest({
wallet,
web3wallet,
sessionAuthRequest,
customTransactionData,
accountId,
accountMetadata: selectedAccountMetadata,
})

if (!approvalResponse?.session) return
if (!approvalResponse?.session) return

dispatch({
type: WalletConnectActionType.ADD_SESSION,
payload: approvalResponse.session,
})
dispatch({
type: WalletConnectActionType.ADD_SESSION,
payload: approvalResponse.session,
})
} catch (e) {
console.error('[WC Auth] request failed:', e)
await web3wallet
.rejectSessionAuthenticate({
id: sessionAuthRequest.id,
reason: {
code: getSdkError('USER_REJECTED').code,
message: e instanceof Error && e.message ? e.message : 'Unknown error',
},
})
.catch(error => console.error('[WC Auth] reject failed:', error))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

handleClose()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SessionProposalOverview } from '@/plugins/walletConnectToDapps/componen
import { PeerMeta } from '@/plugins/walletConnectToDapps/components/PeerMeta'
import { MessageContent } from '@/plugins/walletConnectToDapps/components/WalletConnectSigningModal/content/MessageContent'
import type { CustomTransactionData } from '@/plugins/walletConnectToDapps/types'
import { getSessionAuthIss } from '@/plugins/walletConnectToDapps/utils/SessionAuthRequestHandlerUtil'
import type { WalletConnectSessionAuthModalProps } from '@/plugins/walletConnectToDapps/WalletConnectModalManager'
import {
selectAccountIdsByAccountNumberAndChainId,
Expand Down Expand Up @@ -71,13 +72,10 @@ export const SessionAuthenticateConfirmation: FC<WalletConnectSessionAuthModalPr
if (!sessionAuthPayload || !accountId || !state.web3wallet || !chainId)
return translate('plugins.walletConnectToDapps.modal.sessionAuth.invalidRequest')

// Build DID:PKH identifier for SIWE message (accountId is already in chainId:address format)
const iss = `did:pkh:${accountId}`

try {
const message = state.web3wallet.formatAuthMessage({
request: sessionAuthPayload,
iss,
iss: getSessionAuthIss(accountId),
})
return message
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { formatJsonRpcResult } from '@json-rpc-tools/utils'
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import type { WalletKitTypes } from '@reown/walletkit'
import { isKeepKey } from '@shapeshiftoss/hdwallet-keepkey'
import { isTrezor } from '@shapeshiftoss/hdwallet-trezor'
import type { PairingJsonRpcTypes, SignClientTypes } from '@walletconnect/types'
import { getSdkError } from '@walletconnect/utils'
import { useCallback, useEffect, useMemo } from 'react'

import { useWallet } from '@/hooks/useWallet/useWallet'
import { useWalletConnectEventsHandler } from '@/plugins/walletConnectToDapps/eventsManager/useWalletConnectEventsHandler'
import type {
SupportedSessionRequest,
Expand Down Expand Up @@ -37,6 +41,16 @@ export const useWalletConnectEventsManager = (
const { handleSessionProposal, handleSessionAuthRequest, handleSessionRequest } =
useWalletConnectEventsHandler(dispatch, state.web3wallet)

const {
state: { wallet },
} = useWallet()

// KeepKey and Trezor One can't sign the >1024 byte session_authenticate message, with no listener the dApp's fallback proposal is used
const supportsSessionAuthenticate = useMemo(
() => !(wallet && (isKeepKey(wallet) || isTrezor(wallet))),
[wallet],
)

const signClientEvents = useMemo(() => state.web3wallet?.engine.signClient.events, [state])
const pairingEvents = useMemo(() => state.core?.pairing.events, [state])

Expand All @@ -53,7 +67,16 @@ export const useWalletConnectEventsManager = (
})
}

if (!isSupportedSessionRequest(request)) return
// The sign client holds back later requests until the current one is responded to
if (!isSupportedSessionRequest(request)) {
console.warn(`Unsupported WalletConnect request: ${request.params.request.method}`)
state.web3wallet?.respondSessionRequest({
topic: request.topic,
response: formatJsonRpcError(request.id, getSdkError('UNSUPPORTED_METHODS')),
})
return
}

handleSessionRequest(request)
},
[handleSessionRequest, state.web3wallet],
Expand Down Expand Up @@ -105,7 +128,9 @@ export const useWalletConnectEventsManager = (
signClientEvents?.on('session_update', sessionUpdateListener)
signClientEvents?.on('session_delete', sessionDeleteListener)

state.web3wallet?.on('session_authenticate', handleSessionAuthRequest)
if (supportsSessionAuthenticate) {
state.web3wallet?.on('session_authenticate', handleSessionAuthRequest)
}

// Pairing events
pairingEvents?.on('pairing_ping', pairingPingListener)
Expand Down Expand Up @@ -140,5 +165,6 @@ export const useWalletConnectEventsManager = (
sessionRequestListener,
sessionUpdateListener,
pairingPingListener,
supportsSessionAuthenticate,
])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest'

import { getSessionAuthIss } from './SessionAuthRequestHandlerUtil'

describe('getSessionAuthIss', () => {
it('checksums the address of a lowercase account id', () => {
expect(getSessionAuthIss('eip155:8453:0xa44c286ba83bb771cd0107b2c1df678435bd1535')).toBe(
'did:pkh:eip155:8453:0xA44C286BA83Bb771cd0107B2c1Df678435Bd1535',
)
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { IWalletKit, WalletKitTypes } from '@reown/walletkit'
import type { AccountId } from '@shapeshiftoss/caip'
import { fromAccountId } from '@shapeshiftoss/caip'
import { toAddressNList } from '@shapeshiftoss/chain-adapters'
import type { HDWallet } from '@shapeshiftoss/hdwallet-core'
import type { AccountMetadata } from '@shapeshiftoss/types'
import { getAddress } from 'viem'

import { assertGetEvmChainAdapter } from '@/lib/utils/evm'
import type { CustomTransactionData } from '@/plugins/walletConnectToDapps/types'
Expand All @@ -11,10 +14,16 @@ type ApproveSessionAuthRequestArgs = {
web3wallet: IWalletKit
sessionAuthRequest: WalletKitTypes.EventArguments['session_authenticate']
customTransactionData?: CustomTransactionData
accountId?: string
accountId?: AccountId
accountMetadata?: AccountMetadata
}

// DID:PKH identifier of the signer (iss in SIWE), EIP-4361 requires the address to be EIP-55 checksummed
export const getSessionAuthIss = (accountId: AccountId): string => {
const { chainId, account } = fromAccountId(accountId)
return `did:pkh:${chainId}:${getAddress(account)}`
}

export const approveSessionAuthRequest = async ({
wallet,
web3wallet,
Expand All @@ -33,10 +42,7 @@ export const approveSessionAuthRequest = async ({

const chainAdapter = assertGetEvmChainAdapter(selectedChainId)

// Build the DID:PKH identifier from the account ID (iss = issuer in SIWE)
// Format: did:pkh:<account_id> where account_id is already chainId:address format
// See: https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc#session_authenticate
const iss = `did:pkh:${selectedAccountId}`
const iss = getSessionAuthIss(selectedAccountId)

const message = web3wallet.formatAuthMessage({
request: authPayload,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'

import { createApprovalNamespaces } from './createApprovalNamespaces'

describe('createApprovalNamespaces', () => {
it('lists the approved chains in account order', () => {
const address = '0x6bf198c2b5c8e48af4e876bc2173175b89b1da0c'
const baseAccountId = `eip155:8453:${address}`
const ethAccountId = `eip155:1:${address}`

const optionalNamespaces = {
eip155: {
chains: ['eip155:8453'],
methods: ['personal_sign'],
events: ['chainChanged', 'accountsChanged'],
},
}

const namespaces = createApprovalNamespaces(
{},
optionalNamespaces,
[baseAccountId, ethAccountId],
['eip155:8453', 'eip155:1'],
)

expect(namespaces.eip155.accounts).toEqual([baseAccountId, ethAccountId])
expect(namespaces.eip155.chains).toEqual(['eip155:8453', 'eip155:1'])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,14 @@ export const createApprovalNamespaces = (
}
}

return approvedNamespaces
// dApp connectors such as AppKit read the approved chains from here rather than from the accounts
return Object.fromEntries(
Object.entries(approvedNamespaces).map(([key, namespace]) => [
key,
{
...namespace,
chains: uniq(namespace.accounts.map(account => account.split(':').slice(0, 2).join(':'))),
},
]),
)
}
Loading