Skip to content

Commit

Permalink
fixup! feat(suite): minimal walletconnect implementation for evm
Browse files Browse the repository at this point in the history
  • Loading branch information
martykan committed Jan 23, 2025
1 parent d0e2f45 commit e8a05a2
Show file tree
Hide file tree
Showing 20 changed files with 636 additions and 383 deletions.
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
5 changes: 1 addition & 4 deletions packages/suite-walletconnect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@
"@reduxjs/toolkit": "1.9.5",
"@reown/walletkit": "^1.1.1",
"@suite-common/redux-utils": "workspace:*",
"@suite-common/suite-types": "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"
},
"devDependencies": {
"redux-thunk": "^2.4.2"
}
}
152 changes: 152 additions & 0 deletions packages/suite-walletconnect/src/adapters/ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint-disable no-console */
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';

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 [transaction] = event.params.request.params;
const chainId = Number(event.params.chainId.replace('eip155:', ''));
const account = getAccount(transaction.from, chainId);
if (account.networkType !== 'ethereum') {
throw new Error('Account is not Ethereum');
}
if (!transaction.gasPrice) {
throw new Error('Gas price is not set');
}
const signResponse = await dispatch(
trezorConnectPopupActions.connectPopupCallThunk({
id: 0,
method: 'ethereumSignTransaction',
payload: {
path: account.path,
transaction: {
...transaction,
gasLimit: transaction.gas,
nonce: account.misc.nonce,
chainId,
push: true,
},
device,
useEmptyPassphrase: device?.useEmptyPassphrase,
},
processName: 'WalletConnect',
origin: event.verifyContext.verified.origin,
}),
).unwrap();
if (!signResponse.success) {
console.error('eth_sendTransaction error', signResponse);
throw new Error('eth_sendTransaction error');
}

console.log('pushTransaction', {
tx: signResponse.payload.serializedTx,
coin: account.symbol,
});
const pushResponse = await TrezorConnect.pushTransaction({
tx: signResponse.payload.serializedTx,
coin: account.symbol,
});
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';

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 };
};
2 changes: 2 additions & 0 deletions packages/suite-walletconnect/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './walletConnectActions';
export * from './walletConnectThunks';
export * from './walletConnectMiddleware';
export * from './walletConnectReducer';
46 changes: 46 additions & 0 deletions packages/suite-walletconnect/src/walletConnectActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 updateSession = createAction(
`${ACTION_PREFIX}/updateSession`,
(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,
updateSession,
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'],
};
21 changes: 17 additions & 4 deletions packages/suite-walletconnect/src/walletConnectMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { accountsActions } from '@suite-common/wallet-core';
import { createMiddlewareWithExtraDeps } from '@suite-common/redux-utils';

import * as walletConnectActions from './walletConnectThunks';
import * as walletConnectThunks from './walletConnectThunks';
import { walletConnectActions } from './walletConnectActions';

export const prepareWalletConnectMiddleware = createMiddlewareWithExtraDeps(
async (action, { dispatch, next }) => {
async (action, { dispatch, next, extra }) => {
await next(action);

if (accountsActions.updateSelectedAccount.match(action) && action.payload.account) {
dispatch(
walletConnectActions.switchSelectedAccountThunk({
walletConnectThunks.switchSelectedAccountThunk({
account: action.payload.account,
}),
);
Expand All @@ -19,7 +20,19 @@ export const prepareWalletConnectMiddleware = createMiddlewareWithExtraDeps(
accountsActions.createAccount.match(action) ||
accountsActions.removeAccount.match(action)
) {
dispatch(walletConnectActions.updateAccountsThunk());
dispatch(walletConnectThunks.updateAccountsThunk());
}

if (walletConnectActions.createSessionProposal.match(action)) {
dispatch(
extra.actions.openModal({
type: 'walletconnect-proposal',
eventId: action.payload.eventId,
}),
);
}
if (walletConnectActions.clearSessionProposal.match(action)) {
dispatch(extra.actions.onModalCancel());
}

return action;
Expand Down
53 changes: 53 additions & 0 deletions packages/suite-walletconnect/src/walletConnectReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';

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

export type WalletConnectState = {
sessions: WalletConnectSession[];
pendingProposal: PendingConnectionProposal | undefined;
};

type WalletConnectStateRootState = {
wallet: { walletConnect: WalletConnectState };
};

const walletConnectInitialState: WalletConnectState = {
sessions: [],
pendingProposal: undefined,
};

export const prepareWalletConnectReducer = createReducerWithExtraDeps(
walletConnectInitialState,
(builder, _extra) => {
builder
.addCase(walletConnectActions.saveSession, (state, { payload }) => {
state.sessions.push(payload);
})
.addCase(walletConnectActions.updateSession, (state, { payload }) => {
const { topic, ...rest } = payload;
state.sessions = state.sessions.map(session =>
session.topic === topic ? { ...session, ...rest } : session,
);
})
.addCase(walletConnectActions.removeSession, (state, { payload }) => {
const { topic } = payload;
state.sessions = state.sessions.filter(session => session.topic !== topic);
})
.addCase(walletConnectActions.createSessionProposal, (state, { payload }) => {
state.pendingProposal = payload;
})
.addCase(walletConnectActions.clearSessionProposal, state => {
state.pendingProposal = undefined;
})
.addCase(walletConnectActions.expireSessionProposal, state => {
if (state.pendingProposal) state.pendingProposal.expired = true;
});
},
);

export const selectSessions = (state: WalletConnectStateRootState) =>
state.wallet.walletConnect.sessions;

export const selectPendingProposal = (state: WalletConnectStateRootState) =>
state.wallet.walletConnect.pendingProposal;
Loading

0 comments on commit e8a05a2

Please sign in to comment.