clear-wallet/src/extension/serviceWorker.ts

605 lines
26 KiB
TypeScript
Raw Normal View History

import { getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory, getNetworks, strToHex } from '@/utils/platform';
2022-10-07 17:07:59 +00:00
import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest'
2022-10-16 22:25:20 +00:00
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber, evmCall, getTxByHash, getTxReceipt, signTypedData } from '@/utils/wallet'
2022-10-07 17:07:59 +00:00
import type { RequestArguments } from '@/extension/types'
import { rpcError } from '@/extension/rpcConstants'
import { updatePrices } from '@/utils/gecko'
import { mainNets, testNets } from '@/utils/networks'
2022-10-07 17:07:59 +00:00
2022-10-10 00:52:23 +00:00
let notificationUrl: string
2022-10-07 17:07:59 +00:00
chrome.runtime.onInstalled.addListener(() => {
console.log('Service worker installed');
chrome.runtime.onConnect.addListener(port => port.onDisconnect.addListener(() =>
{
console.log('Service worker connected');
}))
2022-10-07 17:07:59 +00:00
chrome.runtime.connect(null as unknown as string, {
name:'sw-connection'
})
})
chrome.runtime.onStartup.addListener(() => {
console.log('Service worker startup');
})
chrome.runtime.onSuspend.addListener(() => {
console.log('Service worker suspend');
})
chrome.alarms.create('updatePrices', {
periodInMinutes: 1
})
chrome.alarms.onAlarm.addListener((alarm) => {
if(alarm.name === 'updatePrices') {
updatePrices().then(() => {
console.log('Prices updated')
})
}
2022-10-10 23:01:14 +00:00
getSettings().then((settings) => {
if( ((settings.lastLock + settings.lockOutPeriod * 6e4) < Date.now()) && settings.lockOutEnabled && !settings.lockOutBlocked ) {
settings.lastLock = Date.now()
clearPk()
}
})
2022-10-07 17:07:59 +00:00
})
2022-10-10 00:52:23 +00:00
chrome.windows.onRemoved.addListener(async (winId) => {
2022-10-07 17:07:59 +00:00
if (winId in (userReject ?? {})){
userReject[winId]?.()
}
userReject[winId] = undefined
userApprove[winId] = undefined
rIdWin[winId] = undefined
rIdData[winId] = undefined
2022-10-10 00:52:23 +00:00
const wins = await chrome.windows.getAll()
2022-10-07 17:07:59 +00:00
if(wins.length === 0) {
2022-10-10 00:52:23 +00:00
const s = await getSettings()
if(s.enableStorageEnctyption) {
await clearPk()
}
2022-10-07 17:07:59 +00:00
}
})
2022-10-10 00:52:23 +00:00
const viewTxListner = async (id: string) => {
try {
const url = new URL(notificationUrl)
openTab(url.href)
chrome.notifications.clear(id)
} catch {
// ignore
}
}
if (!chrome.notifications.onButtonClicked.hasListener(viewTxListner)){
chrome.notifications.onButtonClicked.addListener(viewTxListner)
}
const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: any) => any) => {
2022-10-15 20:20:33 +00:00
if(message?.type !== "CLWALLET_CONTENT_MSG") {
return true
}
2022-10-07 17:07:59 +00:00
(async () => {
if (!(message?.method)) {
2022-10-07 17:07:59 +00:00
sendResponse({
code: 500,
message: 'Invalid request method'
})
} else {
// ETH API
switch (message.method) {
case 'eth_call': {
2022-10-16 22:25:20 +00:00
sendResponse(await evmCall(message?.params?.[0]))
2022-10-07 17:07:59 +00:00
break
}
2022-10-10 00:52:23 +00:00
case 'eth_getBlockByNumber': {
try {
2022-10-10 00:52:23 +00:00
const params = message?.params?.[0] as any
2022-10-10 23:01:14 +00:00
const block = await getBlockByNumber(params) as any
block.gasLimit = block.gasLimit.toHexString()
block.gasUsed = block.gasUsed.toHexString()
block.baseFeePerGas = block.baseFeePerGas.toHexString()
block._difficulty = block._difficulty.toHexString()
sendResponse(block)
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-10 00:52:23 +00:00
break;
}
2022-10-16 22:25:20 +00:00
case 'eth_getTransactionByHash': {
try {
2022-10-16 22:25:20 +00:00
sendResponse(await getTxByHash(message?.params?.[0] as string))
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-16 22:25:20 +00:00
break
}
case 'eth_getTransactionReceipt':{
try {
2022-10-16 22:25:20 +00:00
sendResponse(await getTxReceipt(message?.params?.[0] as string))
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-16 22:25:20 +00:00
break
}
2022-10-10 00:52:23 +00:00
case 'eth_gasPrice': {
try {
2022-10-10 23:01:14 +00:00
sendResponse((await getGasPrice()).toHexString())
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-10 00:52:23 +00:00
break;
}
2022-10-07 17:07:59 +00:00
case 'eth_getBalance': {
try {
2022-10-07 17:07:59 +00:00
sendResponse(await getBalance())
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-07 17:07:59 +00:00
break
}
case 'eth_blockNumber': {
try {
2022-10-07 17:07:59 +00:00
sendResponse(await getBlockNumber())
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
2022-10-07 17:07:59 +00:00
break
}
case 'eth_estimateGas': {
try {
2022-10-07 17:07:59 +00:00
const params = message?.params?.[0] as any
if(!params) {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'Invalid param for gas estimate'
})
break
}
sendResponse(await estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
}))
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
break
2022-10-07 17:07:59 +00:00
}
2022-10-16 22:25:20 +00:00
case 'eth_requestAccounts':
2022-10-07 17:07:59 +00:00
case 'eth_accounts': {
2022-10-18 15:04:44 +00:00
try {
2022-10-16 22:25:20 +00:00
// give only the selected address for better privacy
2022-10-07 17:07:59 +00:00
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse(address)
2022-10-18 15:04:44 +00:00
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
break
2022-10-07 17:07:59 +00:00
}
case 'eth_chainId': {
2022-10-18 15:04:44 +00:00
try {
2022-10-07 17:07:59 +00:00
const network = await getSelectedNetwork()
const chainId = network?.chainId ?? 0
sendResponse(`0x${chainId.toString(16)}`)
2022-10-18 15:04:44 +00:00
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
}
break
2022-10-07 17:07:59 +00:00
}
case 'eth_sendTransaction': {
try {
const params = message?.params?.[0] as any
if(!params) {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'Invalid param for send transaction'
})
break
}
const [account, network] = await Promise.all([getSelectedAccount(), getSelectedNetwork()])
2022-10-13 20:48:07 +00:00
if(!account || !('address' in account)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=wallet-error&param=${strToHex('No account is selected you need to have an account selected before trying to make a transaction')}&rid=${String(message?.resId ?? '')}`),
2022-10-13 20:48:07 +00:00
type: 'popup'
})
return
}
if(!network || !('chainId' in network)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=wallet-error&param=${strToHex('No network is selected you need to have a network selected before trying to make a transaction')}&rid=${String(message?.resId ?? '')}`),
2022-10-13 20:48:07 +00:00
type: 'popup'
})
2022-10-07 17:07:59 +00:00
return
}
2022-10-10 23:01:14 +00:00
params.from = account.address
const serializeParams = strToHex(JSON.stringify(params)) ?? ''
2022-10-07 17:07:59 +00:00
const pEstimateGas = estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
})
const pGasPrice = getGasPrice()
let gWin: any
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-tx&param=${serializeParams}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
}).then((win) => {
gWin = win
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
rIdData[String(win.id)] = {}
})
2022-10-10 23:01:14 +00:00
2022-10-07 17:07:59 +00:00
})
2022-10-10 00:52:23 +00:00
try {
const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) }, pEstimateGas, pGasPrice)
2022-10-16 22:25:20 +00:00
sendResponse(tx.hash)
2022-10-10 00:52:23 +00:00
const buttons = {} as any
const network = await getSelectedNetwork()
2022-10-13 20:48:07 +00:00
addToHistory({
date: Date.now(),
txHash: tx.hash,
chainId: network.chainId,
...(network.explorer ? {txUrl: `${network.explorer}/tx/${tx.hash}`.replace('//', '/') } : {}),
webiste: (message?.website)
})
2022-10-10 00:52:23 +00:00
const notificationId = crypto.randomUUID()
if(network?.explorer) {
notificationUrl = `${network.explorer}/tx/${tx.hash}`.replace('//', '/')
buttons.buttons = [{
title: 'View Transaction',
}]
setTimeout(() => {
try {
chrome.notifications.clear(notificationId)
} catch {
// ignore
}
}, 6e4)
}
2022-10-10 23:01:14 +00:00
chrome.notifications.create(notificationId,{
2022-10-10 00:52:23 +00:00
message: 'Transaction Confirmed',
title: 'Success',
2022-10-10 23:01:14 +00:00
iconUrl: getUrl('assets/extension-icon/wallet_128.png'),
type: 'basic',
2022-10-10 00:52:23 +00:00
...(buttons)
} as any)
2022-10-16 22:25:20 +00:00
const settings = await getSettings()
if(settings.encryptAfterEveryTx) {
clearPk()
}
2022-10-10 23:01:14 +00:00
} catch (err) {
2022-10-10 00:52:23 +00:00
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'TX Failed'
})
2022-10-15 20:20:33 +00:00
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=wallet-error&param=${strToHex(String(err))}&rid=${String(message?.resId ?? '')}`),
2022-10-15 20:20:33 +00:00
type: 'popup'
})
2022-10-10 00:52:23 +00:00
chrome.notifications.create({
message: 'Transaction Failed',
title: 'Error',
2022-10-10 23:01:14 +00:00
iconUrl: getUrl('assets/extension-icon/wallet_128.png'),
type: 'basic'
2022-10-10 00:52:23 +00:00
} as any)
}
2022-10-07 17:07:59 +00:00
} catch(err) {
2022-10-10 23:01:14 +00:00
// console.log(err)
2022-10-07 17:07:59 +00:00
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected Signature'
})
}
break
}
2022-10-16 22:25:20 +00:00
case 'signTypedData':
case 'eth_signTypedData':
case 'signTypedData_v1':
case 'eth_signTypedData_v1':
case 'signTypedData_v3':
case 'eth_signTypedData_v3':
case 'signTypedData_v4':
case 'eth_signTypedData_v4':
case 'personal_sign':
case 'eth_sign': {
2022-10-07 17:07:59 +00:00
try {
2022-10-13 20:48:07 +00:00
const account = await getSelectedAccount()
if(!account || !('address' in account)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=wallet-error&param=${strToHex('No account is selected you need to have an account selected before trying sign a message')}&rid=${String(message?.resId ?? '')}`),
2022-10-13 20:48:07 +00:00
type: 'popup'
})
return
}
2022-10-16 22:25:20 +00:00
const isTypedSigned = [
'signTypedData',
'eth_signTypedData',
'signTypedData_v1',
'eth_signTypedData_v1',
'signTypedData_v3',
'eth_signTypedData_v3',
'signTypedData_v4',
'eth_signTypedData_v4'].includes(message?.method);
const signMsgData = isTypedSigned ? String(message?.params?.[1] ?? '' ) : String(message?.params?.[0] ?? '' );
2022-10-07 17:07:59 +00:00
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-msg&param=${strToHex(signMsgData)}&rid=${String(message?.resId ?? '')}`),
2022-10-07 17:07:59 +00:00
type: 'popup'
}).then((win) => {
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
})
})
sendResponse(
2022-10-16 22:25:20 +00:00
isTypedSigned ?
await signTypedData(signMsgData):
await signMsg(signMsgData)
2022-10-07 17:07:59 +00:00
)
2022-10-16 22:25:20 +00:00
const settings = await getSettings()
if(settings.encryptAfterEveryTx) {
clearPk()
}
} catch (e) {
console.error(e)
2022-10-07 17:07:59 +00:00
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected Signature'
})
}
break
}
// NON Standard / metamask API
case 'eth_coinbase': {
const account = await getSelectedAccount()
const address = account?.address ?? null
sendResponse(address)
break
}
case 'net_listening': {
sendResponse(true)
break
}
case 'web3_clientVersion': {
sendResponse("MetaMask/v10.20.0")
break
}
case 'wallet_getPermissions':
2022-10-07 17:07:59 +00:00
case 'wallet_requestPermissions': {
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse([{
id: smallRandomString(21),
parentCapability: 'eth_accounts',
invoker: message?.website?.split('/').slice(0,3).join('/') ?? '',
caveats: [{
type:'restrictReturnedAccounts',
2022-10-07 17:07:59 +00:00
value: address
}],
2022-10-07 17:07:59 +00:00
date: Date.now(),
}])
break
}
case 'net_version': {
const network = await getSelectedNetwork()
const chainId = network?.chainId ?? 0
sendResponse(chainId)
break
}
case 'wallet_switchEthereumChain': {
try {
const currentChainId = `0x${((await getSelectedNetwork())?.chainId ?? 0).toString(16)}`
if(currentChainId === String(message?.params?.[0]?.chainId ?? '' )) {
sendResponse(null)
}else {
2022-10-07 17:07:59 +00:00
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
2022-10-10 00:52:23 +00:00
url: chrome.runtime.getURL(`index.html?route=switch-network&param=${String(message?.params?.[0]?.chainId ?? '' )}&rid=${String(message?.resId ?? '')}`),
2022-10-07 17:07:59 +00:00
type: 'popup'
}).then((win) => {
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
})
})
2022-10-10 00:52:23 +00:00
sendResponse(null)
}
2022-10-07 17:07:59 +00:00
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected chain switch'
2022-10-07 17:07:59 +00:00
})
}
break
}
case 'wallet_addEthereumChain': {
const userNetworks = await getNetworks()
const networks = {...mainNets, ...testNets, ...userNetworks}
const chainId = Number(message?.params?.[0]?.chainId ?? '0')
if(!chainId) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'Invalid Network'
})
}
if( chainId in networks ) {
mainListner({...message, method:'wallet_switchEthereumChain', params: [{
chainId: `0x${(chainId).toString(16)}`
}] }, sender, sendResponse)
} else {
if ( !message?.params?.[0]?.chainId ||
!message?.params?.[0]?.chainName ||
!message?.params?.[0]?.rpcUrls ||
!message?.params?.[0]?.blockExplorerUrls ||
!message?.params?.[0]?.nativeCurrency?.symbol
){
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'Invalid Network params chainId, chainName, rpcUrls, blockExplorerUrls, and nativeCurrency are required'
})
}else {
try {
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=request-network&param=${strToHex(JSON.stringify({...{website: message?.website ?? ''}, ...(message?.params?.[0] ?? {})}) ?? '')}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
}).then((win) => {
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
})
})
sendResponse(null)
} catch (err) {
console.log('err')
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected adding network'
})
}
}
}
break
}
2022-10-07 17:07:59 +00:00
// internal messeges
case 'wallet_connect': {
const pNetwork = getSelectedNetwork()
const pAccount = getSelectedAccount()
const [network, account] = await Promise.all([pNetwork, pAccount])
const address = account?.address ? [account?.address] : []
const chainId = `0x${(network?.chainId ?? 0).toString(16)}`
const data = { type: "CLWALLET_PAGE_LISTENER", data: {
listner: 'connect',
data: {
chainId
},
address
}};
sendResponse(data)
break
}
2022-10-07 17:07:59 +00:00
case 'wallet_approve': {
if(String(sender.tab?.windowId) in rIdWin){
userApprove[String(sender.tab?.windowId)]?.(true)
}
try {
chrome.windows.remove(sender.tab?.windowId ?? 0)
2022-10-15 20:20:33 +00:00
}catch (e) {
console.log(e)
2022-10-07 17:07:59 +00:00
// ignore
}
break
}
case 'wallet_send_data': {
if(String(sender.tab?.windowId) in rIdData){
rIdData[String(sender?.tab?.windowId ?? '')] = (message as any)?.data ?? {}
sendResponse(true)
}
break
}
case 'wallet_get_data': {
if(String(sender.tab?.windowId) in rIdData){
sendResponse( rIdData[String(sender?.tab?.windowId ?? '')] ?? {})
}
break
}
case 'wallet_ping': {
sendResponse(true)
break
}
default: {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'ClearWallet: Invalid request method ' + message?.method ?? ''
2022-10-07 17:07:59 +00:00
})
break
}
}
}
}
)();
return true;
}
chrome.runtime.onMessage.addListener(mainListner);