-
-
Notifications
You must be signed in to change notification settings - Fork 273
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixup! feat(suite): minimal walletconnect implementation for evm
- Loading branch information
Showing
20 changed files
with
636 additions
and
383 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
packages/suite-walletconnect/src/walletConnectConstants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.