From 72857fe26838d82bdb2f46aff32bed3d96bd70bb Mon Sep 17 00:00:00 2001 From: Andrei O Date: Mon, 17 Oct 2022 01:25:20 +0300 Subject: [PATCH] 1.0.4.1 --- .gitignore | 4 +- LICENSE | 21 ++ PRIVACY_POLICY.md | 13 + README.md | 35 ++ package.json | 11 +- public/{_locale => _locales}/en/messages.json | 0 release-scripts/create-release.ts | 63 ++++ src/App.vue | 1 - src/extension/content.ts | 14 +- src/extension/inject.ts | 134 +++----- src/extension/manifest.json | 7 +- src/extension/serviceWorker.ts | 65 +++- src/utils/platform.ts | 2 +- src/utils/wallet.ts | 31 ++ src/views/HistoryTab.vue | 7 +- src/views/SettingsTab.vue | 2 +- src/views/SignMessage.vue | 12 +- src/views/SignTx.vue | 11 +- src/views/WalletError.vue | 6 +- yarn.lock | 298 +++++++++++++++++- 20 files changed, 586 insertions(+), 151 deletions(-) create mode 100644 LICENSE create mode 100644 PRIVACY_POLICY.md create mode 100644 README.md rename public/{_locale => _locales}/en/messages.json (100%) create mode 100644 release-scripts/create-release.ts diff --git a/.gitignore b/.gitignore index af1fe4f..5eaf8ba 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,4 @@ npm-debug.log* /plugins /www /src/extension/inject.js -README.md -PRIVACY_POLICY.md -LICENSE \ No newline at end of file +releases diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9604672 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Andrei O. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md new file mode 100644 index 0000000..9f652a5 --- /dev/null +++ b/PRIVACY_POLICY.md @@ -0,0 +1,13 @@ +# 𝐏𝐫𝐢𝐯𝐚𝐜𝐲 𝐏𝐨𝐥𝐢𝐜𝐲: + +## Privacy Points: + +- This extension does not collect any data form your device. +- All storage uses chrome.storage.local +- This extension does not use external files, everything is packed into the extension. +- This extension uses the manifest V3 which does not allow any third party scripts to be injected. +- This extension is completely open source, the source is available on Github - [https://github.com/andrei0x309/clear-wallet](https://github.com/andrei0x309/clear-wallet). + +### 𝐂𝐨𝐧𝐭𝐚𝐜𝐭: + +Discord: andrei0x309#6562 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b47e075 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Clear EVM wallet + +![CLW LOGO](/public/assets/extension-icon/wallet_128.png?raw=true "CLW LOGO") + +## Description + +Simple EVM wallet chrome extension implementation using ethers, mv3, ionc, vue. + +[//]: # Here is an extended article abut this repo: + +### FAQ + +Q: Why using Ionic? +A: The main idea is to extend the codebase to try to aditional platforms like Desktop and Mobile, and because Ionic has a simple interface that is ready to use with no additional design work. + +Q: Is released on Chrome webstore? +A: Not yet but will be probably soon + +Q: What are some features? +A: - It assumes some knowlodege about, EVM echosystem it dosen't come with any network, you can add any EVM network you want, and lets you sleect form the templates of some more popular networks. + - You can have the key stored with or without encryption, you can enable or disable autolock, you can force decryption for every message sign or transaction sign & send. + - You can import, export accounts. + - You can wipe the data + - It only uses local chrome storage + - Is a drop-in replacement for metamask, and currently will overwite metamask if you have both enabled + - It will allow sites directly to get your EVM address without prompting + - Prompts only for changing the network, sending/signing transaction, sending message. + +Q: Is this ready to use? +A: Currently is under some development but it has a nice set of features that I used, I developed this pretty fast in my free time, you should always backup your keys, and do your own research and only use what you are confortable to use. The software dosen't come with any gurantees and is released as it is. But I definitely recomand this to use for testnets and playing with any kind of experiments. + +## LINKS + +[LICENSE.md](LICENSE.md) +[PRIVACY_POLICY.md](PRIVACY_POLICY.md) diff --git a/package.json b/package.json index 7bb3ef5..70007ce 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "clear-wallet", - "version": "0.0.1", + "version": "1.0.5", "private": true, "scripts": { "dev": "vite", "build": "tsc --out src/extension/inject.js src/extension/inject.ts && vue-tsc --noEmit && vite build", - "preview": "vite preview" + "preview": "vite preview", + "release": "yarn config set version-tag-prefix clear-wallet@v && yarn config set version-git-message 'clear-wallet@v%s' && yarn version --patch && yarn postversion", + "postversion": "git push", + "pub": "yarn build && yarn release && ts-node ./scripts/create-release.ts" }, "dependencies": { "@capacitor/app": "^4.0.1", @@ -39,11 +42,13 @@ "sass": "^1.55.0", "stream-browserify": "^3.0.0", "ts-jest": "^29.0.1", + "ts-node": "^10.9.1", "typescript": "^4.8.3", "util": "^0.12.4", "vite": "^3.1.3", "vue-tsc": "^0.40.13", - "yarn-upgrade-all": "^0.7.1" + "yarn-upgrade-all": "^0.7.1", + "archiver": "^5.3.1" }, "description": "An Ionic project" } diff --git a/public/_locale/en/messages.json b/public/_locales/en/messages.json similarity index 100% rename from public/_locale/en/messages.json rename to public/_locales/en/messages.json diff --git a/release-scripts/create-release.ts b/release-scripts/create-release.ts new file mode 100644 index 0000000..4688cb7 --- /dev/null +++ b/release-scripts/create-release.ts @@ -0,0 +1,63 @@ +import archiver from 'archiver'; +import fs from 'fs'; +import { spawn } from 'child_process'; + +async function ghRelease(changes) { + const pkg = JSON.parse(fs.readFileSync('package.json').toString()); + + const archive = archiver('zip', { zlib: { level: 9 } }); + const dirPipes = ['dist']; + + const filePipes = ['LICENSE', 'README.md', 'PRIVACY_POLICY.md']; + const outputPath = `releases/${pkg.version}.zip`; + const outputZip = fs.createWriteStream(outputPath); + + await new Promise((resolve, reject) => { + let arch = archive; + dirPipes.forEach((dir) => { + arch = arch.directory(dir, false); + }); + filePipes.forEach((file) => { + arch = arch.file(file, { name: file }); + }); + arch.on('error', (err) => reject(err)).pipe(outputZip); + + outputZip.on('close', () => resolve(true)); + arch.finalize(); + }); + + const changeLogPath = `releases/${pkg.version}.changelog.md`; + + fs.writeFileSync( + changeLogPath, + `# ${pkg.version} \n + ${changes.reduce((acc, change) => { + return acc + `- ${change}\n`; + }, '')}`, + ); + + console.log( + await new Promise((resolve) => { + const p = spawn('gh', ['release', 'create', `v${pkg.version}`, `./${outputPath}`, '-F', `./${changeLogPath}`], { + shell: true, + }); + // const p = spawn('pwd'); + let result = ''; + p.stdout.on('data', (data) => (result += data.toString())); + p.stderr.on('data', (data) => (result += data.toString())); + p.on('close', () => { + resolve(result); + }); + }), + ); +} + +(async () => { + if (!process.argv[2]) { + console.log('No changes provided'); + return; + } + const changes = process.argv[2].split(','); + await ghRelease(changes); + console.log('Release created', changes); +})(); diff --git a/src/App.vue b/src/App.vue index 1fffcaa..abee891 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,7 +20,6 @@ export default defineComponent({ const route = useRoute() const router = useRouter() const { param, rid } = route.query; -console.log(route?.query,'zzzzzzzzzzzzzzz') onBeforeMount( () => { getSettings().then((settings) => { diff --git a/src/extension/content.ts b/src/extension/content.ts index 5eefc9b..317b4d0 100644 --- a/src/extension/content.ts +++ b/src/extension/content.ts @@ -14,7 +14,19 @@ const allowedMethods = { 'eth_sign': true, 'net_version': true, 'eth_sendTransaction': true, - 'wallet_switchEthereumChain': true + 'wallet_switchEthereumChain': true, + 'eth_call': true, + 'eth_getBalance': true, + 'eth_getTransactionByHash': true, + 'eth_getTransactionReceipt': true, + 'signTypedData': true, + 'eth_signTypedData': true, + 'signTypedData_v1': true, + 'eth_signTypedData_v1': true, + 'signTypedData_v3': true, + 'eth_signTypedData_V3': true, + 'signTypedData_v4': true, + 'eth_signTypedData_v4': true, } window.addEventListener("message", (event) => { diff --git a/src/extension/inject.ts b/src/extension/inject.ts index c00bebc..ea0de47 100644 --- a/src/extension/inject.ts +++ b/src/extension/inject.ts @@ -50,85 +50,6 @@ return new Promise((resolve, reject) => { }) } -// chainId -// : -// "0x89" -// enable -// : -// ƒ () -// isMetaMask -// : -// true -// networkVersion -// : -// "137" -// request -// : -// ƒ () -// selectedAddress -// : -// null -// send -// : -// ƒ () -// sendAsync -// : -// ƒ () -// _events -// : -// {connect: ƒ} -// _eventsCount -// : -// 1 -// _handleAccountsChanged -// : -// ƒ () -// _handleChainChanged -// : -// ƒ () -// _handleConnect -// : -// ƒ () -// _handleDisconnect -// : -// ƒ () -// _handleStreamDisconnect -// : -// ƒ () -// _handleUnlockStateChanged -// : -// ƒ () -// _jsonRpcConnection -// : -// {events: s, stream: d, middleware: ƒ} -// _log -// : -// u {name: undefined, levels: {…}, methodFactory: ƒ, getLevel: ƒ, setLevel: ƒ, …} -// _maxListeners -// : -// 100 -// _metamask -// : -// Proxy {isUnlocked: ƒ, requestBatch: ƒ} -// _rpcEngine -// : -// o {_events: {…}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(3)} -// _rpcRequest -// : -// ƒ () -// _sendSync -// : -// ƒ () -// _sentWarnings -// : -// {enable: false, experimentalMethods: false, send: false, events: {…}} -// _state -// : -// {accounts: Array(0), isConnected: true, isUnlocked: true, initialized: true, isPermanentlyDisconnected: false} -// _warnOfDeprecation -// : -// ƒ ( - const eth = new Proxy({ isConnected: () => { return true @@ -142,6 +63,31 @@ const eth = new Proxy({ return sendMessage(args) }, + // Deprecated + sendAsync: (arg1: RequestArguments, arg2: any): void => { + sendMessage(arg1 as RequestArguments).then(result => { + if (typeof arg2 === 'function'){ + (arg2 as (r?: any) => any )(result) + } + }) + }, + // Deprecated + send: (arg1: unknown, arg2: unknown): unknown => { + if( typeof arg1 === 'string' ) { + return sendMessage({ + method: arg1, + params: arg2 as object + }) + } else if (arg2 === undefined) { + console.error('Clear Wallet: Sync calling is deprecated and not supported') + }else { + sendMessage(arg1 as RequestArguments).then(result => { + if (typeof arg2 === 'function'){ + (arg2 as (r?: any) => any )(result) + } + }) + } + }, on: (eventName: string, callback: () => void) => { switch (eventName) { case 'accountsChanged': @@ -156,12 +102,12 @@ const eth = new Proxy({ case 'disconnect': listners.disconnect.add(callback) break; + // Deprecated - chainIdChanged -networkChanged case 'chainChanged': + case 'chainIdChanged': + case 'networkChanged': listners.chainChanged.add(callback) break; - - default: - return } }, removeListener: (eventName: string, callback: () => void) => { @@ -175,24 +121,29 @@ const eth = new Proxy({ case 'disconnect': listners.disconnect.delete(callback) break; + // Deprecated - chainIdChanged -networkChanged case 'chainChanged': + case 'chainIdChanged': + case 'networkChanged': listners.chainChanged.delete(callback) break; default: return } }, - // Simulate Metamask + // Internal Simulate Metamask _warnOfDeprecation: () => null, _state: {}, _sentWarnings: () => null, _rpcRequest: () => null, _handleAccountsChanged: () => null, + // Deprecated - hardcoded for now, websites should not access this directly since is deprecated for a long time chainId: "0x89", + // Deprecated - hardcoded for now, websites should not access this directly since is deprecated for a long time networkVersion: "137", selectedAddress: null, - send: () => null, - sendAsync: async () => null, + autoRefreshOnNetworkChange: false, + // Internal Simulate Metamask _events: {}, _eventsCount: 0, _handleChainChanged: () => null, @@ -205,10 +156,15 @@ const eth = new Proxy({ _maxListeners: 100, _metamask: new Proxy({}, {}), _rpcEngine: {} - - }, { - set: () => { return false }, + set: () => { return true }, + // get: function(target, name, receiver) { + // if (!(name in target)) { + // console.log(`Getting non-existant property '" + ${name.toString()} + "'`); + // return undefined; + // } + // console.log(target, name, receiver) + // }, deleteProperty: () => { return false }, }) @@ -218,7 +174,7 @@ const injectWallet = (win: any) => { return eth }, set: function () { - return + return true } }); // console.log('Clear wallet injected', (window as any).ethereum, win) diff --git a/src/extension/manifest.json b/src/extension/manifest.json index 0d63e8c..ddf7832 100644 --- a/src/extension/manifest.json +++ b/src/extension/manifest.json @@ -1,7 +1,10 @@ { "manifest_version": 3, - "name": "Clear Wallet EVM", - "version": "1.0.0", + "name": "__MSG_appName__", + "description": "__MSG_appDesc__", + "default_locale": "en", + "version": "1.0.5", + "version_name": "1.0.5", "icons": { "16": "assets/extension-icon/wallet_16.png", "32": "assets/extension-icon/wallet_32.png", diff --git a/src/extension/serviceWorker.ts b/src/extension/serviceWorker.ts index 3dec6a6..abc3618 100644 --- a/src/extension/serviceWorker.ts +++ b/src/extension/serviceWorker.ts @@ -1,8 +1,7 @@ -import { getAccounts, getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory } from '@/utils/platform'; +import { getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory } from '@/utils/platform'; import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest' -import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber } from '@/utils/wallet' +import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber, evmCall, getTxByHash, getTxReceipt, signTypedData } from '@/utils/wallet' import type { RequestArguments } from '@/extension/types' -import type { Account } from '@/extension/types' import { rpcError } from '@/extension/rpcConstants' import { updatePrices } from '@/utils/gecko' @@ -93,6 +92,7 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes // ETH API switch (message.method) { case 'eth_call': { + sendResponse(await evmCall(message?.params?.[0])) break } case 'eth_getBlockByNumber': { @@ -105,6 +105,14 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes sendResponse(block) break; } + case 'eth_getTransactionByHash': { + sendResponse(await getTxByHash(message?.params?.[0] as string)) + break + } + case 'eth_getTransactionReceipt':{ + sendResponse(await getTxReceipt(message?.params?.[0] as string)) + break + } case 'eth_gasPrice': { sendResponse((await getGasPrice()).toHexString()) break; @@ -135,13 +143,9 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes })) break } + case 'eth_requestAccounts': case 'eth_accounts': { - const accounts = await getAccounts() - const addresses = accounts.map((a: Account) => a.address) ?? [] - sendResponse(addresses) - break - } - case 'eth_requestAccounts': { + // give only the selected address for better privacy const account = await getSelectedAccount() const address = account?.address ? [account?.address] : [] sendResponse(address) @@ -210,7 +214,7 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes }) try { const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) }, pEstimateGas, pGasPrice) - sendResponse(tx) + sendResponse(tx.hash) const buttons = {} as any const network = await getSelectedNetwork() addToHistory({ @@ -242,6 +246,11 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes ...(buttons) } as any) + const settings = await getSettings() + if(settings.encryptAfterEveryTx) { + clearPk() + } + } catch (err) { sendResponse({ error: true, @@ -271,7 +280,16 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes } break } - case ('personal_sign' || 'eth_sign'): { + 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': { try { const account = await getSelectedAccount() @@ -286,11 +304,22 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes return } + 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] ?? '' ); + await new Promise((resolve, reject) => { chrome.windows.create({ height: 450, width: 400, - url: chrome.runtime.getURL(`index.html?route=sign-msg¶m=${String(message?.params?.[0] ?? '' )}&rid=${String(message?.resId ?? '')}`), + url: chrome.runtime.getURL(`index.html?route=sign-msg¶m=${signMsgData}&rid=${String(message?.resId ?? '')}`), type: 'popup' }).then((win) => { userReject[String(win.id)] = reject @@ -300,16 +329,22 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes }) sendResponse( - await signMsg(String(message?.params?.[0]) ?? '' ) + isTypedSigned ? + await signTypedData(signMsgData): + await signMsg(signMsgData) ) - } catch { + const settings = await getSettings() + if(settings.encryptAfterEveryTx) { + clearPk() + } + } catch (e) { + console.error(e) sendResponse({ error: true, code: rpcError.USER_REJECTED, message: 'User Rejected Signature' }) } - break } // NON Standard metamask API diff --git a/src/utils/platform.ts b/src/utils/platform.ts index 38dcccb..8f497c4 100644 --- a/src/utils/platform.ts +++ b/src/utils/platform.ts @@ -150,7 +150,7 @@ export const clearPk = async (): Promise => { return a }) accounts = await Promise.all(accProm) - await replaceAccounts(accounts) + await Promise.all([replaceAccounts(accounts), saveSelectedAccount(accounts[0])]) } export const hexTostr = (hexStr: string) => diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 51582c3..04d5269 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -7,6 +7,20 @@ export const signMsg = async (msg: string) => { return await wallet.signMessage( msg.startsWith('0x') ? ethers.utils.arrayify(msg): msg) } +export const signTypedData = async (msg: string) => { + const account = await getSelectedAccount() + const wallet = new ethers.Wallet(account.pk) + const parsedMsg = JSON.parse(msg) + if(parsedMsg?.primaryType) { + if(parsedMsg.primaryType in parsedMsg.types){ + parsedMsg.types = { + [parsedMsg.primaryType]: parsedMsg.types[parsedMsg.primaryType] + } + } + } + return await wallet._signTypedData(parsedMsg.domain, parsedMsg.types, parsedMsg.message) +} + export const getBalance = async () =>{ const account = await getSelectedAccount() const network = await getSelectedNetwork() @@ -38,6 +52,23 @@ export const estimateGas = async ({to = '', from = '', data = '', value = '0x0' return await provider.estimateGas({to, from, data, value}) } +export const evmCall = async ({to = '', from = '', data = '', value = '0x0' }: {to: string, from: string, data: string, value: string}) => { + const network = await getSelectedNetwork() + const provider = new ethers.providers.JsonRpcProvider(network.rpc) + return await provider.call({to, from, data, value}) +} + +export const getTxByHash = async (hash: string) => { + const network = await getSelectedNetwork() + const provider = new ethers.providers.JsonRpcProvider(network.rpc) + return await provider.getTransaction(hash) +} + +export const getTxReceipt = async (hash: string) => { + const network = await getSelectedNetwork() + const provider = new ethers.providers.JsonRpcProvider(network.rpc) + return await provider.getTransactionReceipt(hash) +} export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='0x0', gasPrice='0x0'}: {to: string, from: string, data: string, value: string, gas: string, gasPrice: string}, diff --git a/src/views/HistoryTab.vue b/src/views/HistoryTab.vue index 520cf84..6f7e621 100644 --- a/src/views/HistoryTab.vue +++ b/src/views/HistoryTab.vue @@ -19,7 +19,7 @@ ChainId: {{ item.chainId }} Website: {{ item.webiste }} - ViewTx: LINK + ViewTx: LINK @@ -50,7 +50,7 @@