Skip to content

Commit

Permalink
feat(suite): walletconnect implementation for evm
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Jan 31, 2025
1 parent db5132f commit f25b23e
Show file tree
Hide file tree
Showing 31 changed files with 2,445 additions and 87 deletions.
47 changes: 24 additions & 23 deletions packages/suite-desktop-connect-popup/src/connectPopupThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,22 @@ import { createDeferred } from '@trezor/utils';

const CONNECT_POPUP_MODULE = '@common/connect-popup';

export const connectPopupCallThunk = createThunk(
export const connectPopupCallThunk = createThunk<
Promise<{
id: number;
success: boolean;
payload: any;
}>,
{
id: number;
method: string;
payload: any;
processName?: string;
origin?: string;
}
>(
`${CONNECT_POPUP_MODULE}/callThunk`,
async (
{
id,
method,
payload,
processName,
origin,
}: {
id: number;
method: string;
payload: any;
processName?: string;
origin?: string;
},
{ dispatch, getState, extra },
) => {
async ({ id, method, payload, processName, origin }, { dispatch, getState, extra }) => {
try {
const device = selectSelectedDevice(getState());

Expand Down Expand Up @@ -71,17 +69,19 @@ export const connectPopupCallThunk = createThunk(

dispatch(extra.actions.onModalCancel());

desktopApi.connectPopupResponse({
return {
...response,
id,
});
};
} catch (error) {
console.error('connectPopupCallThunk', error);
desktopApi.connectPopupResponse({
dispatch(extra.actions.onModalCancel());

return {
success: false,
payload: serializeError(error),
id,
});
};
}
},
);
Expand All @@ -90,8 +90,9 @@ export const connectPopupInitThunk = createThunk(
`${CONNECT_POPUP_MODULE}/initPopupThunk`,
async (_, { dispatch }) => {
if (desktopApi.available && (await desktopApi.connectPopupEnabled())) {
desktopApi.on('connect-popup/call', params => {
dispatch(connectPopupCallThunk(params));
desktopApi.on('connect-popup/call', async params => {
const response = await dispatch(connectPopupCallThunk(params)).unwrap();
desktopApi.connectPopupResponse(response);
});
desktopApi.connectPopupReady();
}
Expand Down
1 change: 1 addition & 0 deletions packages/suite-desktop-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const allowedDomains = [
'eth-api-b2c-stage.everstake.one', // staking endpoint for Holesky testnet, works only with VPN
'eth-api-b2c.everstake.one', // staking endpoint for Ethereum mainnet
'dashboard-api.everstake.one', // staking enpoint for Solana
'verify.walletconnect.org', // WalletConnect
];

export const cspRules = [
Expand Down
24 changes: 24 additions & 0 deletions packages/suite-walletconnect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@trezor/suite-walletconnect",
"version": "1.0.0",
"private": true,
"license": "See LICENSE.md in repo root",
"sideEffects": false,
"main": "src/index",
"scripts": {
"depcheck": "yarn g:depcheck",
"type-check": "yarn g:tsc --build"
},
"dependencies": {
"@reduxjs/toolkit": "1.9.5",
"@reown/walletkit": "^1.1.1",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
"@suite-common/wallet-core": "workspace:*",
"@suite-common/wallet-types": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/suite-desktop-api": "workspace:*",
"@walletconnect/core": "^2.17.2",
"@walletconnect/utils": "^2.17.2"
}
}
164 changes: 164 additions & 0 deletions packages/suite-walletconnect/src/adapters/ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { WalletKitTypes } from '@reown/walletkit';

import { createThunk } from '@suite-common/redux-utils';
import { getNetwork } from '@suite-common/wallet-config';
import { selectAccounts, selectSelectedDevice } from '@suite-common/wallet-core';
import * as trezorConnectPopupActions from '@trezor/suite-desktop-connect-popup';
import TrezorConnect from '@trezor/connect';

Check warning on line 7 in packages/suite-walletconnect/src/adapters/ethereum.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

`@trezor/connect` import should occur before import of `@trezor/suite-desktop-connect-popup`
import { getAccountIdentity } from '@suite-common/wallet-utils';

Check warning on line 8 in packages/suite-walletconnect/src/adapters/ethereum.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

`@suite-common/wallet-utils` import should occur before import of `@trezor/suite-desktop-connect-popup`

import { WALLETCONNECT_MODULE } from '../walletConnectConstants';
import { WalletConnectAdapter } from '../walletConnectTypes';

const ethereumRequestThunk = createThunk<
void,
{
event: WalletKitTypes.SessionRequest;
}
>(`${WALLETCONNECT_MODULE}/ethereumRequest`, async ({ event }, { dispatch, getState }) => {
const device = selectSelectedDevice(getState());
const getAccount = (address: string, chainId?: number) => {
const account = selectAccounts(getState()).find(
a =>
a.descriptor.toLowerCase() === address.toLowerCase() &&
a.networkType === 'ethereum' &&
(!chainId || getNetwork(a.symbol).chainId === chainId),
);
if (!account) {
throw new Error('Account not found');
}

return account;
};

switch (event.params.request.method) {
case 'personal_sign': {
const [message, address] = event.params.request.params;
const account = getAccount(address);
const response = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignMessage',
payload: {
path: account.path,
message,
hex: true,
device,
useEmptyPassphrase: device?.useEmptyPassphrase,
},
processName: 'WalletConnect',
origin: event.verifyContext.verified.origin,
}),
).unwrap();
if (!response.success) {
console.error('personal_sign error', response);
throw new Error('personal_sign error');
}

return response.payload.signature;
}
case 'eth_signTypedData_v4': {
const [address, data] = event.params.request.params;
const account = getAccount(address);
const response = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignTypedData',
payload: {
path: account.path,
data: JSON.parse(data),
metamask_v4_compat: true,
device,
useEmptyPassphrase: device?.useEmptyPassphrase,
},
processName: 'WalletConnect',
origin: event.verifyContext.verified.origin,
}),
).unwrap();
if (!response.success) {
console.error('eth_signTypedData_v4 error', response);
throw new Error('eth_signTypedData_v4 error');
}

return response.payload.signature;
}
case 'eth_sendTransaction': {
const chainId = Number(event.params.chainId.replace('eip155:', ''));
const transaction = event.params.request.params[0];
const account = getAccount(transaction.from, chainId);
if (account.networkType !== 'ethereum') {
throw new Error('Account is not Ethereum');
}
if (!transaction.gasPrice) {
// Fee not provided, estimate it
const feeLevels = await TrezorConnect.blockchainEstimateFee({
coin: account.symbol,
identity: getAccountIdentity(account),
request: {
blocks: [2],
specific: {
from: account.descriptor,
},
},
});
if (!feeLevels.success) {
throw new Error('eth_sendTransaction cannot estimate fee');
}
transaction.gasPrice = feeLevels.payload.levels[0]?.feePerUnit;
}
const payload = {
path: account.path,
transaction: {
...transaction,
gasLimit: transaction.gas ?? '21000',
nonce: account.misc.nonce,
chainId,
push: true,
},
device,
useEmptyPassphrase: device?.useEmptyPassphrase,
};
const signResponse = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignTransaction',
payload,
processName: 'WalletConnect',
origin: event.verifyContext.verified.origin,
}),
).unwrap();
if (!signResponse.success) {
console.error('eth_sendTransaction error', signResponse);
throw new Error('eth_sendTransaction error');
}

const pushResponse = await TrezorConnect.pushTransaction({
coin: account.symbol,
identity: getAccountIdentity(account),
tx: signResponse.payload.serializedTx,
});
if (!pushResponse.success) {
console.error('eth_sendTransaction push error', pushResponse);
throw new Error('eth_sendTransaction push error');
}

return pushResponse.payload.txid;
}
case 'wallet_switchEthereumChain': {
const [chainId] = event.params.request.params;

return chainId;
}
}
});

export const ethereumAdapter = {
methods: [
'eth_sendTransaction',
'eth_signTypedData_v4',
'personal_sign',
'wallet_switchEthereumChain',
],
networkType: 'ethereum',
requestThunk: ethereumRequestThunk,
} satisfies WalletConnectAdapter;
43 changes: 43 additions & 0 deletions packages/suite-walletconnect/src/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Account } from '@suite-common/wallet-types';
import { getNetwork } from '@suite-common/wallet-config';

Check warning on line 2 in packages/suite-walletconnect/src/adapters/index.ts

View workflow job for this annotation

GitHub Actions / Linting and formatting

`@suite-common/wallet-config` import should occur before import of `@suite-common/wallet-types`

import { ethereumAdapter } from './ethereum';
import { WalletConnectAdapter, WalletConnectNamespace } from '../walletConnectTypes';

export const adapters: WalletConnectAdapter[] = [
ethereumAdapter,
// TODO: solanaAdapter
// TODO: bitcoinAdapter
];

export const getAdapterByMethod = (method: string) =>
adapters.find(adapter => adapter.methods.includes(method));

export const getAdapterByNetwork = (networkType: string) =>
adapters.find(adapter => adapter.networkType === networkType);

export const getAllMethods = () => adapters.flatMap(adapter => adapter.methods);

export const getNamespaces = (accounts: Account[]) => {
const eip155 = {
chains: [],
accounts: [],
methods: getAllMethods(),
events: ['accountsChanged', 'chainChanged'],
} as WalletConnectNamespace;

accounts.forEach(account => {
const network = getNetwork(account.symbol);
const { chainId, networkType } = network;

if (!account.visible || !getAdapterByNetwork(networkType)) return;

const walletConnectChainId = `eip155:${chainId}`;
if (!eip155.chains.includes(walletConnectChainId)) {
eip155.chains.push(walletConnectChainId);
}
eip155.accounts.push(`${walletConnectChainId}:${account.descriptor}`);
});

return { eip155 };
};
4 changes: 4 additions & 0 deletions packages/suite-walletconnect/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './walletConnectActions';
export * from './walletConnectThunks';
export * from './walletConnectMiddleware';
export * from './walletConnectReducer';
38 changes: 38 additions & 0 deletions packages/suite-walletconnect/src/walletConnectActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createAction } from '@reduxjs/toolkit';

import { PendingConnectionProposal, WalletConnectSession } from './walletConnectTypes';

export const ACTION_PREFIX = '@trezor/suite-walletconnect';

const saveSession = createAction(
`${ACTION_PREFIX}/saveSession`,
(payload: WalletConnectSession) => ({
payload,
}),
);

const removeSession = createAction(
`${ACTION_PREFIX}/removeSession`,
(payload: { topic: string }) => ({
payload,
}),
);

const createSessionProposal = createAction(
`${ACTION_PREFIX}/createSessionProposal`,
(payload: PendingConnectionProposal) => ({
payload,
}),
);

const clearSessionProposal = createAction(`${ACTION_PREFIX}/clearSessionProposal`);

const expireSessionProposal = createAction(`${ACTION_PREFIX}/expireSessionProposal`);

export const walletConnectActions = {
saveSession,
removeSession,
createSessionProposal,
clearSessionProposal,
expireSessionProposal,
} as const;
10 changes: 10 additions & 0 deletions packages/suite-walletconnect/src/walletConnectConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const WALLETCONNECT_MODULE = '@suite/walletconnect';

export const PROJECT_ID = '203549d0480d0f24d994780f34889b03';

export const WALLETCONNECT_METADATA = {
name: 'Trezor Suite',
description: 'Manage your Trezor device',
url: 'https://suite.trezor.io',
icons: ['https://trezor.io/favicon/apple-touch-icon.png'],
};
Loading

0 comments on commit f25b23e

Please sign in to comment.