Skip to content

Commit e8a05a2

Browse files
committed
fixup! feat(suite): minimal walletconnect implementation for evm
1 parent d0e2f45 commit e8a05a2

File tree

20 files changed

+636
-383
lines changed

20 files changed

+636
-383
lines changed

packages/suite-desktop-core/src/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const allowedDomains = [
2828
'eth-api-b2c-stage.everstake.one', // staking endpoint for Holesky testnet, works only with VPN
2929
'eth-api-b2c.everstake.one', // staking endpoint for Ethereum mainnet
3030
'dashboard-api.everstake.one', // staking enpoint for Solana
31+
'verify.walletconnect.org', // WalletConnect
3132
];
3233

3334
export const cspRules = [

packages/suite-walletconnect/package.json

+1-4
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,12 @@
1313
"@reduxjs/toolkit": "1.9.5",
1414
"@reown/walletkit": "^1.1.1",
1515
"@suite-common/redux-utils": "workspace:*",
16-
"@suite-common/suite-types": "workspace:*",
1716
"@suite-common/wallet-config": "workspace:*",
1817
"@suite-common/wallet-core": "workspace:*",
1918
"@suite-common/wallet-types": "workspace:*",
2019
"@trezor/connect": "workspace:*",
20+
"@trezor/suite-desktop-api": "workspace:*",
2121
"@walletconnect/core": "^2.17.2",
2222
"@walletconnect/utils": "^2.17.2"
23-
},
24-
"devDependencies": {
25-
"redux-thunk": "^2.4.2"
2623
}
2724
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* eslint-disable no-console */
2+
import { WalletKitTypes } from '@reown/walletkit';
3+
4+
import { createThunk } from '@suite-common/redux-utils';
5+
import { getNetwork } from '@suite-common/wallet-config';
6+
import { selectAccounts, selectSelectedDevice } from '@suite-common/wallet-core';
7+
import * as trezorConnectPopupActions from '@trezor/suite-desktop-connect-popup';
8+
import TrezorConnect from '@trezor/connect';
9+
10+
import { WALLETCONNECT_MODULE } from '../walletConnectConstants';
11+
import { WalletConnectAdapter } from '../walletConnectTypes';
12+
13+
const ethereumRequestThunk = createThunk<
14+
void,
15+
{
16+
event: WalletKitTypes.SessionRequest;
17+
}
18+
>(`${WALLETCONNECT_MODULE}/ethereumRequest`, async ({ event }, { dispatch, getState }) => {
19+
const device = selectSelectedDevice(getState());
20+
const getAccount = (address: string, chainId?: number) => {
21+
const account = selectAccounts(getState()).find(
22+
a =>
23+
a.descriptor.toLowerCase() === address.toLowerCase() &&
24+
a.networkType === 'ethereum' &&
25+
(!chainId || getNetwork(a.symbol).chainId === chainId),
26+
);
27+
if (!account) {
28+
throw new Error('Account not found');
29+
}
30+
31+
return account;
32+
};
33+
34+
switch (event.params.request.method) {
35+
case 'personal_sign': {
36+
const [message, address] = event.params.request.params;
37+
const account = getAccount(address);
38+
const response = await dispatch(
39+
trezorConnectPopupActions.connectPopupCallThunk({
40+
id: 0,
41+
method: 'ethereumSignMessage',
42+
payload: {
43+
path: account.path,
44+
message,
45+
hex: true,
46+
device,
47+
useEmptyPassphrase: device?.useEmptyPassphrase,
48+
},
49+
processName: 'WalletConnect',
50+
origin: event.verifyContext.verified.origin,
51+
}),
52+
).unwrap();
53+
if (!response.success) {
54+
console.error('personal_sign error', response);
55+
throw new Error('personal_sign error');
56+
}
57+
58+
return response.payload.signature;
59+
}
60+
case 'eth_signTypedData_v4': {
61+
const [address, data] = event.params.request.params;
62+
const account = getAccount(address);
63+
const response = await dispatch(
64+
trezorConnectPopupActions.connectPopupCallThunk({
65+
id: 0,
66+
method: 'ethereumSignTypedData',
67+
payload: {
68+
path: account.path,
69+
data: JSON.parse(data),
70+
metamask_v4_compat: true,
71+
device,
72+
useEmptyPassphrase: device?.useEmptyPassphrase,
73+
},
74+
processName: 'WalletConnect',
75+
origin: event.verifyContext.verified.origin,
76+
}),
77+
).unwrap();
78+
if (!response.success) {
79+
console.error('eth_signTypedData_v4 error', response);
80+
throw new Error('eth_signTypedData_v4 error');
81+
}
82+
83+
return response.payload.signature;
84+
}
85+
case 'eth_sendTransaction': {
86+
const [transaction] = event.params.request.params;
87+
const chainId = Number(event.params.chainId.replace('eip155:', ''));
88+
const account = getAccount(transaction.from, chainId);
89+
if (account.networkType !== 'ethereum') {
90+
throw new Error('Account is not Ethereum');
91+
}
92+
if (!transaction.gasPrice) {
93+
throw new Error('Gas price is not set');
94+
}
95+
const signResponse = await dispatch(
96+
trezorConnectPopupActions.connectPopupCallThunk({
97+
id: 0,
98+
method: 'ethereumSignTransaction',
99+
payload: {
100+
path: account.path,
101+
transaction: {
102+
...transaction,
103+
gasLimit: transaction.gas,
104+
nonce: account.misc.nonce,
105+
chainId,
106+
push: true,
107+
},
108+
device,
109+
useEmptyPassphrase: device?.useEmptyPassphrase,
110+
},
111+
processName: 'WalletConnect',
112+
origin: event.verifyContext.verified.origin,
113+
}),
114+
).unwrap();
115+
if (!signResponse.success) {
116+
console.error('eth_sendTransaction error', signResponse);
117+
throw new Error('eth_sendTransaction error');
118+
}
119+
120+
console.log('pushTransaction', {
121+
tx: signResponse.payload.serializedTx,
122+
coin: account.symbol,
123+
});
124+
const pushResponse = await TrezorConnect.pushTransaction({
125+
tx: signResponse.payload.serializedTx,
126+
coin: account.symbol,
127+
});
128+
if (!pushResponse.success) {
129+
console.error('eth_sendTransaction push error', pushResponse);
130+
throw new Error('eth_sendTransaction push error');
131+
}
132+
133+
return pushResponse.payload.txid;
134+
}
135+
case 'wallet_switchEthereumChain': {
136+
const [chainId] = event.params.request.params;
137+
138+
return chainId;
139+
}
140+
}
141+
});
142+
143+
export const ethereumAdapter = {
144+
methods: [
145+
'eth_sendTransaction',
146+
'eth_signTypedData_v4',
147+
'personal_sign',
148+
'wallet_switchEthereumChain',
149+
],
150+
networkType: 'ethereum',
151+
requestThunk: ethereumRequestThunk,
152+
} satisfies WalletConnectAdapter;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Account } from '@suite-common/wallet-types';
2+
import { getNetwork } from '@suite-common/wallet-config';
3+
4+
import { ethereumAdapter } from './ethereum';
5+
import { WalletConnectAdapter, WalletConnectNamespace } from '../walletConnectTypes';
6+
7+
export const adapters: WalletConnectAdapter[] = [
8+
ethereumAdapter,
9+
// TODO: solanaAdapter
10+
// TODO: bitcoinAdapter
11+
];
12+
13+
export const getAdapterByMethod = (method: string) =>
14+
adapters.find(adapter => adapter.methods.includes(method));
15+
16+
export const getAdapterByNetwork = (networkType: string) =>
17+
adapters.find(adapter => adapter.networkType === networkType);
18+
19+
export const getAllMethods = () => adapters.flatMap(adapter => adapter.methods);
20+
21+
export const getNamespaces = (accounts: Account[]) => {
22+
const eip155 = {
23+
chains: [],
24+
accounts: [],
25+
methods: getAllMethods(),
26+
events: ['accountsChanged', 'chainChanged'],
27+
} as WalletConnectNamespace;
28+
29+
accounts.forEach(account => {
30+
const network = getNetwork(account.symbol);
31+
const { chainId, networkType } = network;
32+
33+
if (!account.visible || !getAdapterByNetwork(networkType)) return;
34+
35+
const walletConnectChainId = `eip155:${chainId}`;
36+
if (!eip155.chains.includes(walletConnectChainId)) {
37+
eip155.chains.push(walletConnectChainId);
38+
}
39+
eip155.accounts.push(`${walletConnectChainId}:${account.descriptor}`);
40+
});
41+
42+
return { eip155 };
43+
};
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
export * from './walletConnectActions';
12
export * from './walletConnectThunks';
23
export * from './walletConnectMiddleware';
4+
export * from './walletConnectReducer';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createAction } from '@reduxjs/toolkit';
2+
3+
import { PendingConnectionProposal, WalletConnectSession } from './walletConnectTypes';
4+
5+
export const ACTION_PREFIX = '@trezor/suite-walletconnect';
6+
7+
const saveSession = createAction(
8+
`${ACTION_PREFIX}/saveSession`,
9+
(payload: WalletConnectSession) => ({
10+
payload,
11+
}),
12+
);
13+
14+
const updateSession = createAction(
15+
`${ACTION_PREFIX}/updateSession`,
16+
(payload: WalletConnectSession) => ({
17+
payload,
18+
}),
19+
);
20+
21+
const removeSession = createAction(
22+
`${ACTION_PREFIX}/removeSession`,
23+
(payload: { topic: string }) => ({
24+
payload,
25+
}),
26+
);
27+
28+
const createSessionProposal = createAction(
29+
`${ACTION_PREFIX}/createSessionProposal`,
30+
(payload: PendingConnectionProposal) => ({
31+
payload,
32+
}),
33+
);
34+
35+
const clearSessionProposal = createAction(`${ACTION_PREFIX}/clearSessionProposal`);
36+
37+
const expireSessionProposal = createAction(`${ACTION_PREFIX}/expireSessionProposal`);
38+
39+
export const walletConnectActions = {
40+
saveSession,
41+
updateSession,
42+
removeSession,
43+
createSessionProposal,
44+
clearSessionProposal,
45+
expireSessionProposal,
46+
} as const;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const WALLETCONNECT_MODULE = '@suite/walletconnect';
2+
3+
export const PROJECT_ID = '203549d0480d0f24d994780f34889b03';
4+
5+
export const WALLETCONNECT_METADATA = {
6+
name: 'Trezor Suite',
7+
description: 'Manage your Trezor device',
8+
url: 'https://suite.trezor.io',
9+
icons: ['https://trezor.io/favicon/apple-touch-icon.png'],
10+
};

packages/suite-walletconnect/src/walletConnectMiddleware.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { accountsActions } from '@suite-common/wallet-core';
22
import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils';
33

4-
import * as walletConnectActions from './walletConnectThunks';
4+
import * as walletConnectThunks from './walletConnectThunks';
5+
import { walletConnectActions } from './walletConnectActions';
56

67
export const prepareWalletConnectMiddleware = createMiddlewareWithExtraDeps(
7-
async (action, { dispatch, next }) => {
8+
async (action, { dispatch, next, extra }) => {
89
await next(action);
910

1011
if (accountsActions.updateSelectedAccount.match(action) && action.payload.account) {
1112
dispatch(
12-
walletConnectActions.switchSelectedAccountThunk({
13+
walletConnectThunks.switchSelectedAccountThunk({
1314
account: action.payload.account,
1415
}),
1516
);
@@ -19,7 +20,19 @@ export const prepareWalletConnectMiddleware = createMiddlewareWithExtraDeps(
1920
accountsActions.createAccount.match(action) ||
2021
accountsActions.removeAccount.match(action)
2122
) {
22-
dispatch(walletConnectActions.updateAccountsThunk());
23+
dispatch(walletConnectThunks.updateAccountsThunk());
24+
}
25+
26+
if (walletConnectActions.createSessionProposal.match(action)) {
27+
dispatch(
28+
extra.actions.openModal({
29+
type: 'walletconnect-proposal',
30+
eventId: action.payload.eventId,
31+
}),
32+
);
33+
}
34+
if (walletConnectActions.clearSessionProposal.match(action)) {
35+
dispatch(extra.actions.onModalCancel());
2336
}
2437

2538
return action;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
2+
3+
import { walletConnectActions } from './walletConnectActions';
4+
import { PendingConnectionProposal, WalletConnectSession } from './walletConnectTypes';
5+
6+
export type WalletConnectState = {
7+
sessions: WalletConnectSession[];
8+
pendingProposal: PendingConnectionProposal | undefined;
9+
};
10+
11+
type WalletConnectStateRootState = {
12+
wallet: { walletConnect: WalletConnectState };
13+
};
14+
15+
const walletConnectInitialState: WalletConnectState = {
16+
sessions: [],
17+
pendingProposal: undefined,
18+
};
19+
20+
export const prepareWalletConnectReducer = createReducerWithExtraDeps(
21+
walletConnectInitialState,
22+
(builder, _extra) => {
23+
builder
24+
.addCase(walletConnectActions.saveSession, (state, { payload }) => {
25+
state.sessions.push(payload);
26+
})
27+
.addCase(walletConnectActions.updateSession, (state, { payload }) => {
28+
const { topic, ...rest } = payload;
29+
state.sessions = state.sessions.map(session =>
30+
session.topic === topic ? { ...session, ...rest } : session,
31+
);
32+
})
33+
.addCase(walletConnectActions.removeSession, (state, { payload }) => {
34+
const { topic } = payload;
35+
state.sessions = state.sessions.filter(session => session.topic !== topic);
36+
})
37+
.addCase(walletConnectActions.createSessionProposal, (state, { payload }) => {
38+
state.pendingProposal = payload;
39+
})
40+
.addCase(walletConnectActions.clearSessionProposal, state => {
41+
state.pendingProposal = undefined;
42+
})
43+
.addCase(walletConnectActions.expireSessionProposal, state => {
44+
if (state.pendingProposal) state.pendingProposal.expired = true;
45+
});
46+
},
47+
);
48+
49+
export const selectSessions = (state: WalletConnectStateRootState) =>
50+
state.wallet.walletConnect.sessions;
51+
52+
export const selectPendingProposal = (state: WalletConnectStateRootState) =>
53+
state.wallet.walletConnect.pendingProposal;

0 commit comments

Comments
 (0)