diff --git a/packages/chain-adapters/src/error/ErrorHandler.ts b/packages/chain-adapters/src/error/ErrorHandler.ts index ebed120575d..af56ae2cf8e 100644 --- a/packages/chain-adapters/src/error/ErrorHandler.ts +++ b/packages/chain-adapters/src/error/ErrorHandler.ts @@ -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 @@ -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 diff --git a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts index e0fa540f79d..52de9abaaa1 100644 --- a/packages/chain-adapters/src/evm/EvmBaseAdapter.ts +++ b/packages/chain-adapters/src/evm/EvmBaseAdapter.ts @@ -799,7 +799,11 @@ export abstract class EvmBaseAdapter 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') diff --git a/packages/chain-adapters/src/evm/ethereum/EthereumChainAdapter.test.ts b/packages/chain-adapters/src/evm/ethereum/EthereumChainAdapter.test.ts index 26e8a7408af..642df891159 100644 --- a/packages/chain-adapters/src/evm/ethereum/EthereumChainAdapter.test.ts +++ b/packages/chain-adapters/src/evm/ethereum/EthereumChainAdapter.test.ts @@ -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', () => { diff --git a/packages/hdwallet-integration/src/wallets/trezor.ts b/packages/hdwallet-integration/src/wallets/trezor.ts index 56ab0fb16c9..9dbdbe74c1a 100644 --- a/packages/hdwallet-integration/src/wallets/trezor.ts +++ b/packages/hdwallet-integration/src/wallets/trezor.ts @@ -113,6 +113,7 @@ export class MockTransport extends trezor.TrezorTransport { { path: [2147483692, 2147483708, 2147483648, 0, 0], message: '0x48656c6c6f20576f726c64', // "Hello World" + hex: true, }, { success: true, diff --git a/packages/hdwallet-trezor/src/ethereum.ts b/packages/hdwallet-trezor/src/ethereum.ts index 3d769077bd0..2100b735aab 100644 --- a/packages/hdwallet-trezor/src/ethereum.ts +++ b/packages/hdwallet-trezor/src/ethereum.ts @@ -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, diff --git a/src/plugins/walletConnectToDapps/WalletConnectModalManager.tsx b/src/plugins/walletConnectToDapps/WalletConnectModalManager.tsx index 20741d47756..0dd08b95cd7 100644 --- a/src/plugins/walletConnectToDapps/WalletConnectModalManager.tsx +++ b/src/plugins/walletConnectToDapps/WalletConnectModalManager.tsx @@ -207,21 +207,34 @@ export const WalletConnectModalManager: FC = ({ 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)) + } handleClose() }, diff --git a/src/plugins/walletConnectToDapps/components/modals/SessionAuthenticateConfirmation.tsx b/src/plugins/walletConnectToDapps/components/modals/SessionAuthenticateConfirmation.tsx index 5b2908ea262..f4a08336242 100644 --- a/src/plugins/walletConnectToDapps/components/modals/SessionAuthenticateConfirmation.tsx +++ b/src/plugins/walletConnectToDapps/components/modals/SessionAuthenticateConfirmation.tsx @@ -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, @@ -71,13 +72,10 @@ export const SessionAuthenticateConfirmation: FC1024 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]) @@ -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], @@ -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) @@ -140,5 +165,6 @@ export const useWalletConnectEventsManager = ( sessionRequestListener, sessionUpdateListener, pairingPingListener, + supportsSessionAuthenticate, ]) } diff --git a/src/plugins/walletConnectToDapps/utils/SessionAuthRequestHandlerUtil.ts b/src/plugins/walletConnectToDapps/utils/SessionAuthRequestHandlerUtil.ts index 2718acdcc0b..365efed7ce9 100644 --- a/src/plugins/walletConnectToDapps/utils/SessionAuthRequestHandlerUtil.ts +++ b/src/plugins/walletConnectToDapps/utils/SessionAuthRequestHandlerUtil.ts @@ -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' @@ -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, @@ -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: 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, diff --git a/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.test.ts b/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.test.ts new file mode 100644 index 00000000000..45d42b146a8 --- /dev/null +++ b/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.test.ts @@ -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']) + }) +}) diff --git a/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.ts b/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.ts index 6943be2babc..60ef9245229 100644 --- a/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.ts +++ b/src/plugins/walletConnectToDapps/utils/createApprovalNamespaces.ts @@ -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(':'))), + }, + ]), + ) }