Compare commits

...

25 Commits
v1.2.7 ... main

Author SHA1 Message Date
Andrei O 7d978eada1
'clear-wallet@v1.3.7' 2024-05-03 12:55:12 +03:00
Andrei O 0c33477741
chore: changes for 1.3.7 2024-05-03 12:53:24 +03:00
Andrei O b654910e13
'clear-wallet@v1.3.6' 2024-04-12 19:40:24 +03:00
Andrei O f05c652b0a
chore: changes for `1.3.6` release 2024-04-12 19:39:26 +03:00
Andrei O 0f00a2426b
'clear-wallet@v1.3.5' 2024-03-30 13:41:14 +02:00
Andrei O 6ed73738b7
chore: changes for new release 2024-03-30 13:39:58 +02:00
Andrei O f398cebf2c
'clear-wallet@v1.3.4' 2024-03-13 04:19:26 +02:00
Andrei O 5b9d04a66b
chore: changes for new release 2024-03-13 04:17:57 +02:00
Andrei O c88cfc22b8
'clear-wallet@v1.3.3' 2024-02-11 23:08:24 +02:00
Andrei O 97d37021f0
chore: other changes added in changelog 2024-02-11 21:21:16 +02:00
Andrei O 7a187cd0cf
chore: cleanup 2024-02-08 21:59:03 +02:00
Andrei O 498629d073
chore: changes for a new release 2024-02-08 20:08:06 +02:00
Andrei O 73b3d6e26d
'clear-wallet@v1.3.2' 2024-01-30 22:04:47 +02:00
Andrei O 3102f620b2
chore: changes for 1.3.2 2024-01-30 21:37:35 +02:00
Andrei O 874dd545ab
chore: changes for 1.3.1 2024-01-27 00:50:55 +02:00
Andrei O 5279fe10ee
'clear-wallet@v1.3.1' 2024-01-24 02:55:18 +02:00
Andrei O a6340ae936
chore: set version to `1.3.1` 2024-01-24 02:54:10 +02:00
Andrei O c77a675e43
'clear-wallet@v1.2.10' 2024-01-24 02:52:47 +02:00
Andrei O ec12c64ce4
Merge pull request #3 from andrei0x309/main-dev
Main dev
2024-01-24 02:49:16 +02:00
Andrei O 78c98dd54b
chore: disable sourcemaps 2024-01-24 02:48:42 +02:00
Andrei O 5dabddc28e
chore: remove comment 2024-01-24 02:46:47 +02:00
Andrei O fe8e4c273b
chore: changes for `1.3.0` 2024-01-24 02:41:31 +02:00
Andrei O b52ddd02f0
migrate: to new version WIP 2023-08-14 11:10:52 +03:00
Andrei O 2638ea94f4
'clear-wallet@v1.2.8' 2023-04-27 05:39:12 +03:00
Andrei O 42a6434713
chore: changes for `1.2.8` 2023-04-27 05:32:26 +03:00
57 changed files with 5294 additions and 2307 deletions

View File

@ -1,5 +1,75 @@
# Changelog
## Manifest Version 1.3.7
- improved add Network pages
- upgraded and optimized some dependencies including vite
- optimized vite config
- added condition to not reinject wallet if already injected for websites that reload injected scripts
- optimized throttle fuffilment of requests in case of too many requests
- removed uneeded mobile native code
## Manifest Version 1.3.6
- better display of blockchain explorer button
- updated ethers dependency to latest 6.11.1
- better handling of type sigining
- changed the password input for unlock to not lose focus
- activated focus on password input for unlock on view enter
- disabled integration of fire wallet(in cause user has it installed) with type signing due to incompatibility
- other misc improvements
- added a check when sending native token to check if internet / RPC or Blockchain and show a message to the user
- customize testNets icons to show a small dev icon on the top right corner
- updated testNets templates to include newer networks
- show icons for testNets too in most places
## Manifest Version 1.3.5
- added copy button to chainId for easier development
- added settings to be able to transfrom address to lower case when copying
- added a check in get recepit to return null if hash is missing
- added version display to wallet first page
## Manifest Version 1.3.4
- bump fake Metamask version signature to 11.0.0
- improved compatibility with older deprecated websites
- improved mimicking of Metamask API
- made the wallet compatible with fire extension on sending transaction( by mimicking new Metamask API)
## Manifest Version 1.3.3
- improved eth_call and eth_blockNumber to be more compatible with older websites
- better error internal handling
- modify the receipt returned to resamble more the one from Metamask
- change some notes in about
- refactored account name edit to be more user friendly
## Manifest Version 1.3.2
- added button to open non kyc exchange, no referral is used to maximize privacy
## Manifest Version 1.3.1
- refactored the wallet to use etheres V6
- implemented EIP6963Provider
- updated all dependencies
- added ability to send native tokens
- added ability to manage ABIs
- added ability to perfrom arbitrary read calls to contracts
- added ability to perfrom arbitrary write calls to contracts
- added ability to save read or write calls for later use
- added sandbox to be able to evaluate JS code in order to pass complex parameters to read or write calls
- added base Network to templates class
- added Icon for base network
- added ability to add contacts and load them in Read contract and Write and Send token pages
- added ability to paste current selected address to both webpages and insde wallet itself
## Manifest Version 1.2.8
- better support for estimate gas
- added support for deprecated .send method to support more websites
## Manifest Version 1.2.7
- improve compatibility with ionic 7

20
eval-sandbox.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Eval Sandbox</h1>
<script>
// console.log('sandbox loaded');
window.addEventListener('message', function (event) {
// console.log('message received', event);
const data = event.data;
const execFunc = new Function(
'return ' + data.code
);
const result = execFunc();
event.source.postMessage({ result }, event.origin);
});
</script>
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" style="width:400px;height:450px">
<html lang="en" style="width:400px;height:500px">
<head>
<meta charset="utf-8" />
<title>Clear Wallet</title>

View File

@ -1,58 +1,61 @@
{
"name": "clear-wallet",
"version": "1.2.7",
"version": "1.3.7",
"private": true,
"description": "Clear Wallet (CLW) is a wallet that helps you manage your Ethereum assets and interact with Ethereum dApps and contracts with the main focus on absolute privacy.",
"type": "module",
"scripts": {
"dev": "vite",
"inject": "tsc --outFile src/extension/inject.js src/extension/inject.ts",
"inject": "tsc --downlevelIteration --outFile src/extension/inject.js src/extension/inject.ts",
"content": "tsc --outFile src/extension/content.js src/extension/content.ts",
"post-build": "ts-node ./release-scripts/post-build.ts",
"post-build": "yarn tsx ./release-scripts/post-build.ts",
"build": "yarn inject && yarn content && vue-tsc --noEmit && vite build && yarn post-build",
"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 ./release-scripts/create-release.ts"
"pub": "yarn build && yarn release && yarn tsx ./release-scripts/create-release.ts"
},
"dependencies": {
"@capacitor/app": "^4.1.1",
"@capacitor/core": "^4.7.1",
"@capacitor/haptics": "^4.1.0",
"@capacitor/keyboard": "^4.1.1",
"@capacitor/status-bar": "^4.1.1",
"@ionic/vue": "^7.0.0",
"@ionic/vue-router": "^7.0.0",
"@types/chrome": "^0.0.227",
"core-js": "^3.29.1",
"ethers": "^5.7.2",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"@ionic/vue": "^7.2.3",
"@ionic/vue-router": "^7.2.3",
"core-js": "^3.32.0",
"ethers": "^6.11.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@capacitor/cli": "^4.7.1",
"@crxjs/vite-plugin": "^1.0.14",
"@capacitor/cli": "^5.2.3",
"@crxjs/vite-plugin": "^2.0.0-beta.23",
"@types/archiver": "^5.3.2",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@types/chrome": "^0.0.243",
"@types/jest": "^29.5.3",
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^11.0.3",
"archiver": "^5.3.1",
"eslint": "^8.37.0",
"eslint-plugin-vue": "^9.10.0",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"jest": "^29.5.0",
"jest": "^29.6.2",
"rollup-plugin-polyfill-node": "^0.12.0",
"sass": "^1.60.0",
"sass": "^1.65.1",
"stream-browserify": "^3.0.0",
"ts-jest": "^29.0.5",
"ts-node": "^10.9.1",
"typescript": "^5.0.2",
"ts-jest": "^29.1.1",
"tsx": "^4.8.0",
"typescript": "^5.1.6",
"util": "^0.12.5",
"vite": "^4.2.1",
"vue-tsc": "^1.2.0",
"vite": "^5.2.10",
"vue-tsc": "^1.8.8",
"yarn-upgrade-all": "^0.7.2"
},
"description": "An Ionic project"
"disabledNativeDependencies": {
"@capacitor/app": "^5.0.6",
"@capacitor/core": "^5.2.3",
"@capacitor/haptics": "^5.0.6",
"@capacitor/keyboard": "^5.0.6",
"@capacitor/status-bar": "^5.0.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -1 +0,0 @@
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" style="width:400px;height:450px">
<html lang="en" style="width:400px;height:500px">
<head>
<meta charset="utf-8" />
<title>Clear Wallet</title>

View File

@ -1,16 +1,15 @@
(async () => {
const CONTENT_BUILD_PATH = 'src/extension/content.js'
const METAMASK_STUB_PATH = 'src/extension/metamask-stub.js'
const METAMASK_INJECT_PATH = 'src/extension/inject.js'
const fs = (await import('fs')).default
const path = (await import('path')).default
const pkg = JSON.parse(fs.readFileSync('dist/manifest.json').toString());
pkg.content_scripts[0].js[0] = CONTENT_BUILD_PATH
pkg.content_scripts[1].js[0] = METAMASK_STUB_PATH
pkg.content_scripts[1].js[0] = METAMASK_INJECT_PATH
fs.writeFileSync('dist/manifest.json', JSON.stringify(pkg, null, 2))
// fs.writeFileSync('dist/rules.js', fs.readFileSync('rules.json').toString())
fs.writeFileSync('dist/'+ CONTENT_BUILD_PATH, fs.readFileSync('src/extension/content.js').toString())
fs.writeFileSync('dist/'+ METAMASK_STUB_PATH, fs.readFileSync('src/extension/metamask-stub.js').toString())
fs.writeFileSync('dist/'+ METAMASK_INJECT_PATH, fs.readFileSync('src/extension/inject.js').toString())
const directory = 'dist/assets/';
fs.readdir(directory, (err, files) => {
files.forEach(file => {

View File

@ -9,6 +9,8 @@ import { IonApp, IonRouterOutlet } from "@ionic/vue";
import { defineComponent, onBeforeMount, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getSettings } from "@/utils/platform";
import { getSelectedAddress } from "@/utils/wallet";
import type { RequestArguments } from "@/extension/types";
export default defineComponent({
name: "App",
@ -21,6 +23,56 @@ export default defineComponent({
const router = useRouter();
const { param, rid } = route.query;
const pageListener = (
message: RequestArguments,
sender: any,
sendResponse: (a: any) => any
) => {
if (chrome.runtime.lastError) {
console.info("Error receiving message:", chrome.runtime.lastError);
}
if (message?.type !== "CLWALLET_PAGE_MSG") {
return true;
}
console.info("page listener:", message);
(async () => {
if (!message?.method) {
sendResponse({
code: 500,
message: "Invalid request method",
});
} else {
// ETH API
switch (message.method) {
case "paste": {
const currentAddress = (await getSelectedAddress()) as string[];
if (currentAddress.length > 0) {
document.execCommand("insertText", false, currentAddress[0]);
}
sendResponse(true);
break;
}
default: {
sendResponse({
error: true,
message:
"ClearWallet: Invalid PAGE request method " + (message?.method ?? ""),
});
break;
}
}
}
})();
return true;
};
if (chrome?.runtime?.onMessage) {
chrome.runtime.onMessage.addListener(pageListener);
console.info("page listener set");
}
onBeforeMount(() => {
getSettings().then((settings) => {
if (settings.theme !== "system") {
@ -30,6 +82,13 @@ export default defineComponent({
});
});
// onUnmounted(() => {
// if (chrome?.runtime?.onMessage) {
// chrome.runtime.onMessage.removeListener(pageListener);
// console.info("page listener removed");
// }
// });
onMounted(() => {
switch (route?.query?.route ?? "") {
case "sign-msg": {

View File

@ -1,28 +1,17 @@
(() =>{
try {
const container = document.documentElement;
const script = document.createElement('script');
script.setAttribute('async', "false")
script.setAttribute('fetchpriority', "high")
script.src = chrome.runtime.getURL('src/extension/inject.js')
container.prepend(script)
script.addEventListener('load', () => { container.removeChild(script) } )
} catch (error) {
console.error('MetaMask: Provider injection failed.', error);
}
})()
const allowedMethods = {
'eth_accounts': true,
'eth_requestAccounts' : true,
'eth_requestAccounts': true,
'eth_chainId': true,
'personal_sign' : true,
'personal_sign': true,
'wallet_requestPermissions': true,
'wallet_registerOnboarding': true,
'wallet_revokePermissions': true,
'eth_gasPrice': true,
'eth_getBlockByNumber': true,
'eth_blockNumber': true,
'eth_estimateGas': true,
'eth_syncing': true,
'eth_sign': true,
'net_version': true,
'eth_sendTransaction': true,
@ -50,42 +39,72 @@ const allowedMethods = {
window.addEventListener("message", (event) => {
if (event.source != window)
return;
// console.log(event)
if (event.data.type && (event.data.type === "CLWALLET_CONTENT")) {
event.data.data.resId = event.data.resId
event.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.website = document?.location?.href ?? ''
if((event?.data?.data?.method ?? 'x') in allowedMethods) {
chrome.runtime.sendMessage(event.data.data, (res) => {
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId };
// console.log('data back', data)
window.postMessage(data, "*");
})
}
else {
const data = { type: "CLWALLET_PAGE", data: { error: true, message: 'ClearWallet: Unknown method requested ' + event?.data?.data?.method ?? ''}, resId: event.data.resId };
window.postMessage(data, "*");
}
} else if (event.data.type && (event.data.type === "CLWALLET_PING")) {
event.data.data.resId = event.data.resId
event.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.method = "wallet_connect"
event.data.data.params = Array(0)
chrome.runtime.sendMessage(event.data.data , async (res) => {
window.postMessage(res, "*");
return;
if (event?.data?.type === "CLWALLET_CONTENT") {
event.data.data.data.resId = event.data.resId
event.data.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.data.website = document?.location?.href ?? ''
if ((event?.data?.data?.method ?? 'x') in allowedMethods) {
event.data.data.data.method = event?.data?.data?.method ?? ''
chrome?.runtime?.sendMessage(event.data.data.data, (res) => {
if (chrome.runtime.lastError) {
console.warn("LOC1: Error sending message:", chrome.runtime.lastError);
}
const id = Number(event.data.resId.replace(/[A-Za-z]/g, '').slice(0, 10))
const data = {
target: 'metamask-inpage',
type: "CLWALLET_PAGE",
resId: event.data.resId,
data: { name: 'metamask-provider', data: {
jsonrpc: '2.0',
id,
result: res,
},
id,
method: event?.data?.data?.data?.method ?? '',
params: event?.data?.data?.data?.params ?? [],
},
}
// console.info('data out', data)
window.postMessage(data, "*");
})
}
else {
const data = {
type: "CLWALLET_PAGE",
data: {
data: {
result: { error: true, message: 'ClearWallet: Unknown method requested ' + (event?.data?.data?.data?.method ?? '') }
} }
, resId: event.data.resId };
window.postMessage(data, "*");
}
} else if (event?.data?.type === "CLWALLET_PING") {
event.data.data.data.resId = event.data.resId
event.data.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.data.method = "wallet_connect"
event.data.data.data.params = Array(0)
chrome.runtime.sendMessage(event.data.data.data, async (res) => {
if (chrome.runtime.lastError) {
console.warn("LOC2: Error sending message:", chrome.runtime.lastError);
}
window.postMessage(res, "*");
})
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
chrome.runtime.onMessage.addListener((message: any , sender, sendResponse) => {
if(message.type === "CLWALLET_EXT_LISTNER") {
const data = { type: "CLWALLET_PAGE_LISTENER", data: message.data };
chrome.runtime.onMessage.addListener((message: any, sender, sendResponse) => {
if (chrome.runtime.lastError) {
console.warn("Error receiving message:", chrome.runtime.lastError);
}
if (message.type === "CLWALLET_EXT_LISTNER") {
const data = { type: "CLWALLET_PAGE_LISTENER", data: message.data };
// console.log('data listner', data)
window.postMessage(data, "*");
}
return true
});

View File

@ -4,6 +4,46 @@ interface RequestArguments {
params?: unknown[] | object;
}
interface EIP6963ProviderInfo {
uuid: string;
name: string;
icon: string;
rdns: string;
}
const ProviderInfo: EIP6963ProviderInfo = {
uuid: '1fa914a1-f8c9-4c74-8d84-4aa93dc90eec',
name: 'Clear Wallet',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGN0lEQVRoge1ZfUgbZxj/3eXLRK112gmDwWYjo9gVin8UN509aTsGG26ywUb/WrWTVtgK1lLGPurGRoe6rmtBN+r219wfhdaJUFw/ImgrMtuO0NrSIhuibU3RJqaa5HK5G8/rJWh6l1yStVLwBw9JLu897/P9PO8dVrGKVTzd4LSk37p1a/RrDYA9AF4BEFHpScCk0jCADgA98XsODAywT3MCYVqLior2NzU1obS0FLIsM3oS4Hme0djY2I6jR4/umJycbHO5XM2CIDyyu54Htq1bt+7swYMH4XQ68ePHl2HLssBs5R+7+AoAKRQBx3No7qhAMBhEQ0MDJiYmtgM4F12XzAN7Nm3ahPz8fHz1wbCqZ/ixCx+P5jfPorVvO+rr69Ha2nrW7/cXAJhdukzPpJUk/Mn2CXC8GRxvWhECx+O7jy5hy5YtqKiogJqTy6DnASvFuyQSIyUt63EcYM8xIyfPAimSZu4oCnwzYVitVhYNAIqMKiApigKrzQw+RQV4nkPdoZfx/Et2jI+PY3Z2FsSL4zTTLYn8Cmw2B7xeLx4+fEiXvEYVYOB4ftGVKQj/zclyTE1N4cCBL3Hz5k0mBFWUdEGK0/2iKBKHX1NSgOdNSEF+vNtYjDt37qCurg6hUKgJwM9qYUm4TxJEXRcAEEpJAbI+WdXQLhywuaoQLS0tJPx+l8v1vSAIrwJ4QxUi0ybCqzwGAfxpWAHOoPufKbJhYWEBbrebfv5GwjudzqHKykqYTKaMmyCFEfEg/qOjo69HlTAQQsaSeE2BlcUpNR4A8wDKcnNzUV5ejuLiYpYLmYByQZIkxmF0dLTSkAKLIWRsVzmy2P7J2ipfSyAQgMfjwdVRLq0qtBwK3nvfCYvFQldt0b8SK8BRCBljT2vjIEYiEWb5YDDzLm618swD4TDjFUvm5CFkMIl5nXJFCoQlbQUc2RbIcgSBBQlcEkvxJm1RE5c31gcMuiDBOtVqMWzYUIAq4QXcvn0bd+/eh9lsxr//AHM+my4PvYaaJISMK5DIgmoTYti48Vkm/LFjx9Df388qS0lJCaqrq8FxBbjv0fYkx2tXscTSkVCpkJ4CYTFG1dvW4/jx4zh16lTH/Px8YSAQKHS73R19fX0oKFxAMBRctj5K8V40pABVDuYFQ6SfK6IYZpSdbWG94vTp03R5n8vlmiEC8AnNTRMTE8jK4mPr40kLiXOAhDJa/hIqsFg0JGnZdktv4MgA1Dd83nBs/VKYTOmE0P+EaBhMe7xwOByora0lxkcEQSggAvBDWVkZ6BA1MzunGUKiTggl9ICikhEkWrfU/T09V9HY2Egm39Pf308PDFBVVYWamhoMDk5CFCVtQc3aHs5kSjQMsmAUF1zXWV/Yu3cvdu3axa6GQiF0/z6CGzc8uiwt2no9KQ+Iy36fP38NFy5cR36+A7KswOud13u+EIPFkoYHFCiQDaqgaKyLHkb0SuD0tKh5XQuiqJ2uiRVQFCiKsTFYY9q0kvDsOMnLMJsyqxfUH6hjpzTMLSpgzAM0hFJXpQGOKiZNEOQBmkgPffFWjF+6IF6UKykNc2R92eCefp8Eq9WOrKwsalbZAC7Pzc3h4sWLuHLlyv9yHsjLy2MPCtRTWXIFZEWBbDCEZu6H4HAUsVo+MDCw0+VytQuCUDE1NbVyR0qWwoqxTkyRc+2veezevRvDw8NtgiAo6qH+7xU71C+GkPGT1MkuD1o6X0RXVxdNm+1ut7udQkc9paWF6Iylxn9WvBJJ+4DREAJrWMDnDeOob34Ohw8fZglHZ+RMngvZ7XYmfGdnJ3p7ez8E0GlEATNpLgYlKEpqm4fDCjq+nWSznSPHhNw8EyKR9BKY8t43I+Hrn5zIycmhS2sfEVTnXpGsp/ALkBVHWpuT+/xzMvxzmZ2H16w1s07+4MED+jkd/7+eeQfv3bsH4Z0QZFliubASREXnsyPrMTIygqGhIZLrj3hB9TzQcevWrdqxsTHs3LcZPb8Adkc2zDSPZFbODSEclqHIPD5tK2E5dOLECfj9/u3x7wYSKXDO5/O19fb27qenwq+9XQqLxZ9SZ84UdG44c+YMuru7ob5iOpfKK6bo15V+yXdJfcn3SOhEXzGtYhWreJoB4D9CrzrJ8WeKXgAAAABJRU5ErkJggg==',
rdns: 'clear-wallet.flashsoft.eu/',
}
const THROTTLE_LEVEL = 20;
const THROTTLE_TIMEOUT = 500;
const MAX_PROMISES = 50
function loadEIP1193Provider(provider: any) {
function announceProvider() {
const info: EIP6963ProviderInfo = ProviderInfo
window.dispatchEvent(
new CustomEvent("eip6963:announceProvider", {
detail: Object.freeze({ info, provider }),
})
);
}
window.addEventListener(
"eip6963:requestProvider",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(event: any) => {
announceProvider();
}
);
announceProvider();
}
const listners = {
accountsChanged: new Set<(p?: any) => void>(),
connect: new Set<(p?: any) => void>(),
@ -33,27 +73,43 @@ const getListnersCount = (): number => {
return count
}
const sendMessage = (args: RequestArguments, ping = false) => {
if(Object.values(promResolvers).filter(r=> r).length < 10 ) {
return new Promise((resolve, reject) => {
const resId = crypto.randomUUID()
const sendMessage = (args: RequestArguments, ping = false, from = 'request'): Promise<unknown> => {
return new Promise(async (resolve, reject) => {
if(promResolvers.size < MAX_PROMISES && promResolvers.size > THROTTLE_LEVEL) {
await new Promise((res) => setTimeout(res, THROTTLE_TIMEOUT))
} else if(promResolvers.size > MAX_PROMISES) {
reject({code: -32000, message: 'ClearWallet: Too many requests', error: true })
}
const resId = [...`${Math.random().toString(16) + Date.now().toString(16)}`].slice(2).join('')
promResolvers.set(resId, { resolve, reject })
const data = { type: "CLWALLET_CONTENT", data: args, resId};
const p = [ "eth_signTypedData", "eth_signTypedData_v3", "eth_signTypedData_v4"]
const method = args.method
if (p.includes(args.method)) {
args.method = undefined as any
}
const data = {
type: "CLWALLET_CONTENT",
target: 'metamask-contentscript',
data: {
method,
name: 'metamask-provider', data: args, jsonrpc: '2.0', id: Number(resId.replace(/[A-Za-z]/g, '').slice(0, 10)) },
resId,
from,
}
if (ping) {
data.type = 'CLWALLET_PING'
}
// console.log('data in', data)
// console.info('data in', data)
window.postMessage(data, "*");
})
} else {
return new Promise((resolve, reject) => {
reject(new Error("You have reached the maximum number of concurent wallet messeges."))
})
}
})
}
class MetaMaskAPI {
isMetaMask = true
isClWallet = true
_state = {accounts: Array(1), isConnected: true, isUnlocked: true, initialized: true, isPermanentlyDisconnected: false}
_sentWarnings = {enable: false, experimentalMethods: false, send: false, events: {}}
// Deprecated - hardcoded for now, websites should not access this directly since is deprecated for a long time
@ -80,7 +136,7 @@ class MetaMaskAPI {
_events: {}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(4)
}
isConnected() {
return false
return true
}
// for maximum compatibility since is cloning the same API
@ -89,25 +145,26 @@ class MetaMaskAPI {
}
request(args: RequestArguments): Promise<unknown> {
return sendMessage(args)
return sendMessage(args) as Promise<unknown>
}
// Deprecated
sendAsync (arg1: any, arg2: any): void | Promise<unknown> {
// return this.send(arg1, arg2) as any
if( typeof arg1 === 'string' ) {
return sendMessage({
method: arg1,
params: arg2 as object
})
}, false , 'sendAsync') as Promise<unknown>
}else if (typeof arg2 === 'function'){
sendMessage(arg1 as RequestArguments).then(result => {
((sendMessage(arg1 as RequestArguments, false, 'sendAsync') as Promise<unknown>).then(result => {
(arg2 as (e?: any, r?: any) => any )(undefined, {
id: (arg1 as RequestArguments)?.id,
jsonrpc: '2.0',
method: (arg1 as RequestArguments).method,
result
}
)
}).catch( e => {
)
}) as Promise<unknown>).catch( e => {
(arg2 as (er?: any, r?: any) => any )(new Error(e), {
id: (arg1 as RequestArguments)?.id,
jsonrpc: '2.0',
@ -116,36 +173,44 @@ class MetaMaskAPI {
}
)
})
}
} else {
return sendMessage(arg1 as RequestArguments, false, 'sendAsync') as Promise<unknown>
}
}
// Deprecated
send (arg1: unknown, arg2: unknown): unknown {
const resultFmt = async (result: Promise<any>) => {
return {
"id": 0,
"jsonrpc": "2.0",
result: await result
}
}
if (arg2 === undefined) {
console.error('Clear Wallet: Sync calling is deprecated and not supported')
if( typeof arg1 === 'string' ) {
return resultFmt(sendMessage({
method: arg1,
params: undefined
}, false, 'send'))
} else {
return resultFmt(sendMessage(arg1 as RequestArguments, false, 'send'))
}
} else if (typeof arg1 === 'object') {
if( typeof arg1 === 'string' ) {
return resultFmt(sendMessage(arg1 as RequestArguments, false, 'send'))
} else {
return resultFmt(sendMessage(arg1 as RequestArguments, false, 'send'))
}
}else if( typeof arg1 === 'string' ) {
return sendMessage({
return resultFmt( sendMessage({
method: arg1,
params: arg2 as object
})
}, false, 'send'))
}else if (typeof arg2 === 'function'){
sendMessage(arg1 as RequestArguments).then(result => {
(arg2 as (e?: any, r?: any) => any )(undefined, {
id: (arg1 as RequestArguments)?.id,
jsonrpc: '2.0',
method: (arg1 as RequestArguments).method,
result
}
)
}).catch( e => {
(arg2 as (er?: any, r?: any) => any )(new Error(e), {
id: (arg1 as RequestArguments)?.id,
jsonrpc: '2.0',
method: (arg1 as RequestArguments).method,
error: new Error(e)
}
)
})
}
return resultFmt( sendMessage(arg1 as RequestArguments, false, 'send'))
} else {
return resultFmt(sendMessage(arg1 as RequestArguments , false, 'send'))
}
}
on (eventName: string, callback: () => void) {
this.addListener(eventName, callback)
@ -263,92 +328,77 @@ class MetaMaskAPI {
_handleStreamDisconnect() { return true }
_handleUnlockStateChanged() { return true }
_sendSync () {
console.error('Clear Wallet: Sync calling is deprecated and not supported')
console.warn('ERROR: Clear Wallet: Sync calling is deprecated and not supported')
}
}
const eth = new Proxy( new MetaMaskAPI(), {
// set: () => { return true },
// get: function(target, name, receiver) {
// if (typeof (<any>target)[name] == 'function') {
// return function (...args: any) {
// console.dir({ call: [name, ...args] });
// return undefined;
// }
// }
// let check = true
// setTimeout(() => check = false, 400)
// while(check){
// // igmore
// }
// },
deleteProperty: () => { return true },
// set(obj, prop, value) {
// // Reflect.set(obj, prop, value);
// return true;
// }
})
const listner = function(event: any) {
if (event.source != window) return;
if (event.data.type && (event.data.type === "CLWALLET_PAGE")) {
if(!['CLWALLET_PAGE', 'CLWALLET_PAGE_LISTENER'].includes(event?.data?.type)) return;
const eventData = event?.data
const eventDataData = event?.data?.data
const eventDataDataData = event?.data?.data?.data
const resId = eventData?.resId
const result = eventDataDataData?.result
if (eventData?.type === "CLWALLET_PAGE") {
try {
if(event?.data?.data?.error){
promResolvers.get(event.data.resId)?.reject(event.data.data);
// console.error(event?.data?.data)
if(result?.error){
promResolvers.get(resId).reject(result);
}else {
promResolvers.get(event.data.resId)?.resolve(event.data.data);
promResolvers.get(resId).resolve(result);
}
promResolvers.delete(event.data.resId)
} catch (e) {
// console.log('Failed to connect resolve msg', e)
// console.error('Failed to connect resolve msg', e)
promResolvers.get(resId)?.reject({code: -32000, message: 'Failed to connect resolve msg', error: true });
}
} else if( event.data.type && (event.data.type === "CLWALLET_PAGE_LISTENER")) {
if((event?.data?.data?.listner ?? 'x') in listners ) {
} else if(eventData?.type === "CLWALLET_PAGE_LISTENER") {
if((eventDataData?.listner ?? 'x') in listners ) {
try {
const listnerName = event?.data?.data?.listner as ('accountsChanged' | 'connect' | 'disconnect' | 'chainChanged')
if( listnerName === 'connect' && event?.data?.data?.data) {
(<any>eth).networkVersion = event?.data?.data?.data?.chainId?.toString(10) ?? '137';
(<any>eth).chainId = event?.data?.data?.data?.chainId ?? '0x89';
(<any>eth).selectedAddress = event?.data?.data?.address ?? null;
const listnerName = eventDataData.listner as ('accountsChanged' | 'connect' | 'disconnect' | 'chainChanged')
if( listnerName === 'connect' && eventDataData) {
(<any>eth).networkVersion = String(parseInt(eventDataDataData?.chainId ?? "0x89", 16));
(<any>eth).chainId = eventDataDataData?.chainId ?? '0x89';
(<any>eth).selectedAddress = eventDataData?.address?.[0] ?? null;
(<any>eth).accounts = eventDataData.address?.[0] ? [eventDataData.address?.[0]] : [];
(<any>eth).isConnected = () => true;
} else if( listnerName === 'chainChanged' ) {
// console.log(event?.data?.data?.data);
(<any>eth).networkVersion = event?.data?.data?.data.toString(10) ?? '137';
(<any>eth).chainId = event?.data?.data?.data ?? '0x89';
(<any>eth).networkVersion = String(parseInt(eventDataDataData ?? "0x89", 16));
(<any>eth).chainId = eventDataData ?? '0x89';
} else if ( listnerName === 'accountsChanged' ) {
(<any>eth).selectedAddress = event?.data?.data?.data?.address ?? 'dummy-string';
(<any>eth).accounts = eventDataData?.[0] ? [eventDataData?.[0]] : [];
(<any>eth).selectedAddress = eventDataData?.[0] ?? '';
}
listners[listnerName].forEach(listner => listner(event?.data?.data?.data));
listners[listnerName].forEach(listner => listner(eventDataDataData));
listners.once[listnerName].forEach(listner => {
listner(event?.data?.data?.data)
listner(eventDataData)
listners.once[listnerName].delete(listner)
});
} catch (e) {
// console.error(e)
// console.info(e)
// ignore
}
}
}
}
if(promResolvers.has(resId)) {
promResolvers.delete(resId)
}
}
window.addEventListener("message",listner)
// const proxy1 = new Proxy({
// // on: (event: any, callback:any) => { if (event === 'message') {
// // debugger;
// // callback(true, true)
// // } }
// }, {
// get: function(target, name, receiver) {
// if (typeof (<any>target)[name] == 'function') {
// return function (...args: any) {
// console.dir({ call: [name, ...args] });
// }
// }
// console.log('ETH', name.toString() , target, receiver);
// return undefined
// }
// })
Object.defineProperties(eth, {
selectedAddress: { enumerable: false },
chainId: { enumerable: false },
networkVersion: { enumerable: false },
});
const web3Shim = {
currentProvider: eth,
@ -356,7 +406,12 @@ const web3Shim = {
}
const injectWallet = (win: any) => {
Object.defineProperty(win, 'ethereum', {
const ethKey = 'ethereum'
if (win[ethKey]?.isClWallet) {
return;
}
Object.defineProperty(win, ethKey, {
value: eth,
});
Object.defineProperty(win, 'web3', {
@ -365,28 +420,81 @@ Object.defineProperty(win, 'web3', {
sendMessage({
method: 'wallet_ready'
}, true)
// console.log('Clear wallet injected', (window as any).ethereum, win)
}
injectWallet(this);
loadEIP1193Provider(eth)
// HELPERS TO CLONE METAMASK API
// window.addEventListener("message" , (event) => {
// console.log('event', JSON.stringify(event?.data?.data, null, 2), JSON.stringify(event?.data, null, 2))
// })
// setTimeout(() => {
// // console.log('Metamask clone test');
// // (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x89"}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum2.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x89"}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.on('connect', ((a: any, b: any) => console.log('connect', a, b)));
// // (<any>window).ethereum.on('accountsChanged', ((a: any, b: any) => console.log('accountsChanged', a, b)));
// // (<any>window).ethereum.on('chainChanged', ((a: any) => console.log('chainChanged', a, typeof a)));
// // console.log((<any>window).ethereum.on('message', (a: any, b:any) => console.log(a,b)))
// console.log((<any>window).ethereum.toString())
// console.log((<any>window).ethereum2.toString())
// console.log((<any>window).ethereum.Symbold)
// console.log('Metamask clone test');
// console.log((<any>window).ethereum.send({
// "jsonrpc": "2.0",
// "method": "eth_accounts",
// "params": [],
// "id": 0
// }))
// console.log((<any>window).ethereum.request({
// "jsonrpc": "2.0",
// "method": "eth_accounts",
// "params": [],
// "id": 0
// }))
// }, 5000)
// setTimeout(async () => {
// console.log('Metamask clone test');
// (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, 'MT: eth_requestAccounts')});
// (<any>window).ethereum2.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, 'CW: eth_requestAccounts')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, 'MT: eth_accounts')});
// (<any>window).ethereum2.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, 'CW: eth_accounts')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, 'MT: eth_chainId')});
// (<any>window).ethereum2.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, 'CW: eth_chainId')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'eth_blockNumber', params: Array(0)}).then((res: any) => { console.log(res, 'MT: eth_chainId')});
// (<any>window).ethereum2.request({method: 'eth_blockNumber', params: Array(0)}).then((res: any) => { console.log(res, 'CW: eth_chainId')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, 'MT: wallet_requestPermissions')});
// (<any>window).ethereum2.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, 'CW: wallet_requestPermissions')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, 'MT: net_version')});
// (<any>window).ethereum2.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, 'CW: net_version')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x89"}]}).then((res: any) => { console.log(res, 'MT: wallet_switchEthereumChain')});
// (<any>window).ethereum2.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x89"}]}).then((res: any) => { console.log(res, 'CW: wallet_switchEthereumChain')});
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum.on('connect', ((a: any, b: any) => console.log('connect MT', a, b)));
// (<any>window).ethereum.on('accountsChanged', ((a: any, b: any) => console.log('accountsChanged MT', a, b)));
// (<any>window).ethereum.on('chainChanged', ((a: any) => console.log('chainChanged MT', a, typeof a)));
// await new Promise((resolve) => setTimeout(resolve, 1000));
// (<any>window).ethereum2.on('connect', ((a: any, b: any) => console.log('connect CW', a, b)));
// (<any>window).ethereum2.on('accountsChanged', ((a: any, b: any) => console.log('accountsChanged CW', a, b)));
// (<any>window).ethereum2.on('chainChanged', ((a: any) => console.log('chainChanged CW', a, typeof a)));
// }, 3500)
// console.log( (window as any).ethereum.request({method: 'eth_chainId'}))

View File

@ -2,10 +2,15 @@ import type { listnerType } from '@/extension/types'
export const triggerListner = ( type: listnerType, listnerData: any ) => {
const data = { type: "CLWALLET_EXT_LISTNER", data: { listner: type, data: listnerData } }
chrome.tabs.query({}, (tabs) => tabs.forEach( tab =>
{
chrome.tabs.query({}, (tabs) => tabs.forEach( async tab =>
{
if (tab?.id) {
chrome.tabs.sendMessage(tab.id, data)
try {
await chrome.tabs.sendMessage(tab.id, data)
} catch
{
// ignore
}
}
}
));

View File

@ -3,8 +3,8 @@
"name": "__MSG_appName__",
"description": "__MSG_appDesc__",
"default_locale": "en",
"version": "1.2.7",
"version_name": "1.2.7",
"version": "1.3.7",
"version_name": "1.3.7",
"icons": {
"16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png",
@ -23,11 +23,14 @@
"minimum_chrome_version": "103",
"permissions": [
"notifications",
"activeTab",
"storage",
"alarms",
"unlimitedStorage",
"clipboardRead",
"clipboardWrite"
"clipboardWrite",
"contextMenus",
"scripting"
],
"host_permissions": [
"*://*/*"
@ -44,7 +47,7 @@
],
"all_frames": true,
"run_at": "document_start",
"js": ["/src/extension/content.ts"]
"js": ["/src/extension/content.js"]
},
{
"matches": [
@ -53,12 +56,17 @@
],
"all_frames": true,
"run_at": "document_start",
"js": ["/src/extension/metamask-stub.js"],
"js": ["/src/extension/inject.js"],
"world": "MAIN"
}
],
"web_accessible_resources": [{
"resources": ["src/extension/inject.js"],
"matches": ["<all_urls>"]
}]
}],
"sandbox": {
"pages": [
"eval-sandbox.html"
]
}
}

View File

@ -1,31 +1,102 @@
import { getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory, getNetworks, strToHex, numToHexStr } from '@/utils/platform';
import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest'
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber, evmCall, getTxByHash, getTxReceipt, signTypedData, getCode, getTxCount } from '@/utils/wallet'
import {
CLW_CONTEXT_MENU_ID,
getSelectedAccount,
getSelectedNetwork,
smallRandomString,
getSettings,
clearPk,
openTab,
getUrl,
addToHistory,
getNetworks,
strToHex,
numToHexStr,
enableRightClickVote,
} from '@/utils/platform';
import {
userApprove,
userReject,
rIdWin,
rIdData,
} from '@/extension/userRequest'
import {
signMsg,
getBalance,
getBlockNumber,
estimateGas,
sendTransaction,
getGasPrice,
getBlockByNumber,
evmCall,
getTxByHash,
getTxReceipt,
signTypedData,
getCode,
getTxCount,
getSelectedAddress
} from '@/utils/wallet'
import type { RequestArguments } from '@/extension/types'
import { rpcError } from '@/extension/rpcConstants'
import { updatePrices } from '@/utils/gecko'
import { mainNets, testNets } from '@/utils/networks'
import { allTemplateNets } from '@/utils/networks'
// const METAMAKS_EXTENSION_ID = 'nkbihfbeogaeaoehlefnkodbefgpgknn'
let notificationUrl: string
chrome.runtime.onInstalled.addListener(() => {
console.log('Service worker installed');
enableRightClickVote()
console.info('Service worker installed');
})
chrome.runtime.onStartup.addListener(() => {
console.log('Service worker startup');
console.info('Service worker startup');
enableRightClickVote();
if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message);
}
})
chrome.runtime.onSuspend.addListener(() => {
console.log('Service worker suspend');
console.info('Service worker suspend');
if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message);
}
})
async function pasteAddress() {
const currentAddress = (await (window as any).ethereum?.request({
method: 'eth_accounts',
params: []
}))
if(currentAddress.length > 0) {
document.execCommand("insertText", false, currentAddress[0]);
}
}
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const extensionId = chrome.runtime.id
const isOwnExtension = info?.pageUrl?.startsWith(`chrome-extension://${extensionId}`)
if (info.menuItemId === CLW_CONTEXT_MENU_ID && tab?.id && !isOwnExtension) {
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN',
func: pasteAddress
});
} catch {
// igonre
}
} else if(isOwnExtension) {
chrome.runtime.sendMessage({ method: 'paste', type: 'CLWALLET_PAGE_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC3: Error sending message:", chrome.runtime.lastError);
}
return r
})
}
})
chrome.alarms.create('updatePrices', {
periodInMinutes: 1
@ -34,7 +105,9 @@ chrome.alarms.create('updatePrices', {
chrome.alarms.onAlarm.addListener((alarm) => {
if(alarm.name === 'updatePrices') {
updatePrices().then(() => {
console.log('Prices updated')
console.info('Prices updated')
}).catch((err) => {
console.warn('Prices update failed', err)
})
}
getSettings().then((settings) => {
@ -77,9 +150,13 @@ if (!chrome.notifications.onButtonClicked.hasListener(viewTxListner)){
}
const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: any) => any) => {
if (chrome.runtime.lastError) {
console.info("Error receiving message:", chrome.runtime.lastError);
}
if(message?.type !== "CLWALLET_CONTENT_MSG") {
return true
}
(async () => {
if (!(message?.method)) {
sendResponse({
@ -90,24 +167,36 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
// ETH API
switch (message.method) {
case 'eth_call': {
sendResponse(await evmCall(message?.params?.[0]))
try {
sendResponse(await evmCall(message?.params ?? []))
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_call', e)
}
break
}
case 'eth_getBlockByNumber': {
try {
const params = message?.params?.[0] as any
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 {
const newBlock = {...block}
newBlock.gasLimit = numToHexStr(block.gasLimit)
newBlock.gasUsed = numToHexStr(block.gasUsed)
newBlock.baseFeePerGas = numToHexStr(block.baseFeePerGas)
newBlock._difficulty = numToHexStr(block.difficulty)
newBlock.difficulty = block._difficulty
sendResponse(newBlock)
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getBlockByNumber', e)
}
break;
}
@ -118,85 +207,93 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}else {
sendResponse(numToHexStr(Number(await getTxCount(message?.params?.[0] as string))))
}
} catch {
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getTransactionCount', e)
}
break
}
case 'eth_getTransactionByHash': {
try {
sendResponse(await getTxByHash(message?.params?.[0] as string))
} catch {
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getTransactionByHash', e)
}
break
}
case 'eth_getTransactionReceipt':{
try {
sendResponse(await getTxReceipt(message?.params?.[0] as string))
} catch {
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getTransactionReceipt', e)
}
break
}
case 'eth_gasPrice': {
try {
sendResponse((await getGasPrice()).toHexString())
} catch {
sendResponse(numToHexStr(BigInt(Math.trunc(await getGasPrice() * 1e9))))
} catch(e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_gasPrice', e)
}
break;
}
case 'eth_getBalance': {
try {
sendResponse(await getBalance())
} catch {
const balance = await getBalance()
const balanceHex = numToHexStr(balance ?? 0n)
sendResponse(balanceHex)
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getBalance', e)
}
break
}
case 'eth_getCode': {
try {
sendResponse(await getCode(message?.params?.[0] as string))
} catch {
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_getCode', e)
}
break
}
case 'eth_blockNumber': {
try {
sendResponse(await getBlockNumber())
} catch {
sendResponse(numToHexStr(await getBlockNumber()))
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_blockNumber', e)
}
break
}
@ -211,12 +308,14 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
})
break
}
sendResponse(await estimateGas({
const gas = await estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
}))
})
const gasHex = numToHexStr(gas ?? 0n)
sendResponse(gasHex)
} catch(err) {
if(String(err).includes('UNPREDICTABLE_GAS_LIMIT')) {
chrome.notifications.create({
@ -230,28 +329,28 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
code: rpcError.USER_REJECTED,
message: 'Gas estimate failed'
})
}
} else {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_estimateGas', err)
}
}
break
}
case 'eth_requestAccounts':
case 'eth_accounts': {
try {
// give only the selected address for better privacy
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse(address)
} catch {
sendResponse(await getSelectedAddress())
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_accounts', e)
}
break
}
@ -260,12 +359,13 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
const network = await getSelectedNetwork()
const chainId = network?.chainId ?? 0
sendResponse(`0x${chainId.toString(16)}`)
} catch {
} catch (e) {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'No network or user selected'
})
console.warn('Error: eth_chainId', e)
}
break
}
@ -301,13 +401,6 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
params.from = account.address
const serializeParams = strToHex(JSON.stringify(params)) ?? ''
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({
@ -325,7 +418,9 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
})
try {
const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) }, pEstimateGas, pGasPrice)
// console.log('waiting for user to approve or reject')
// console.log(rIdData?.[String(gWin?.id ?? 0)])
const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) } )
sendResponse(tx.hash)
const buttons = {} as any
const network = await getSelectedNetwork()
@ -383,7 +478,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
} as any)
}
} catch(err) {
// console.log(err)
console.warn('Error: eth_sendTransaction', err)
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
@ -450,7 +545,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
clearPk()
}
} catch (e) {
// console.error(e)
console.warn('Error: signTypedData', e)
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
@ -471,7 +566,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
break
}
case 'web3_clientVersion': {
sendResponse("MetaMask/v10.20.0")
sendResponse("MetaMask/v11.0.0")
break
}
case 'wallet_getPermissions':
@ -490,9 +585,21 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}])
break
}
case 'wallet_revokePermissions': {
sendResponse(null)
break
}
case 'wallet_registerOnboarding': {
sendResponse(true)
break
}
case 'eth_syncing': {
sendResponse(false)
break
}
case 'net_version': {
const network = await getSelectedNetwork()
const chainId = network?.chainId ?? 0
const chainId = String(network?.chainId ?? 1)
sendResponse(chainId)
break
}
@ -527,7 +634,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
case 'wallet_addEthereumChain': {
const userNetworks = await getNetworks()
const networks = {...mainNets, ...testNets, ...userNetworks}
const networks = {...allTemplateNets, ...userNetworks}
const chainId = Number(message?.params?.[0]?.chainId ?? '0')
if(!chainId) {
sendResponse({
@ -568,7 +675,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
})
sendResponse(null)
} catch (err) {
console.log('err')
console.error('err')
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
@ -586,7 +693,8 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
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: {
const data = {
type: "CLWALLET_PAGE_LISTENER", data: {
listner: 'connect',
data: {
chainId
@ -610,7 +718,8 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
case 'wallet_send_data': {
if(String(sender.tab?.windowId) in rIdData){
rIdData[String(sender?.tab?.windowId ?? '')] = (message as any)?.data ?? {}
const intData = rIdData[String(sender?.tab?.windowId ?? '')] ?? {}
rIdData[String(sender?.tab?.windowId ?? '')] = {...intData, ...(message?.data ?? {})}
sendResponse(true)
}
break
@ -629,12 +738,13 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'ClearWallet: Invalid request method ' + message?.method ?? ''
message: 'ClearWallet: Invalid request method ' + (message?.method ?? '')
})
break
}
}
}
}
)();
return true;

View File

@ -8,9 +8,12 @@ export interface Network {
explorer?: string
}
export interface Account {
export interface Contact {
name: string
address: string
}
export interface Account extends Contact {
pk: string
encPk: string
}
@ -54,6 +57,7 @@ export interface Settings {
theme: 'system' | 'light' | 'dark'
lastLock: number
lockOutBlocked: boolean
copyLowerCaseAddress?: boolean
}
export type listnerType = 'accountsChanged' | 'connect' | 'disconnect' | 'chainChanged'
@ -65,3 +69,15 @@ export interface HistoryItem {
webiste?: string
txHash: string
}
export interface ContractAction {
name: string
contract: string
abi: string
functionName: string
params: any[]
}
export interface ContractActions {
[key: string] : ContractAction
}

View File

@ -4,12 +4,20 @@ export const rIdWin = {} as Record<string, string | undefined>
export const rIdData = {} as Record<string, any | undefined>
export const approve = (rId: string) => {
chrome.runtime.sendMessage({ method: 'wallet_approve', resId: rId, type: 'CLWALLET_CONTENT_MSG' })
chrome.runtime.sendMessage({ method: 'wallet_approve', resId: rId, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC4: Error sending message:", chrome.runtime.lastError);
}
return r
})
}
export const walletSendData = (rId: string, data: any) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_send_data', resId: rId, data, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC5: Error sending message:", chrome.runtime.lastError);
}
resolve(r)
})
})
@ -18,6 +26,25 @@ export const walletSendData = (rId: string, data: any) => {
export const walletGetData = (rId: string) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_get_data', resId: rId, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC6: Error sending message:", chrome.runtime.lastError);
}
resolve(r)
})
})
}
export const walletPromptSendTx = (tx: any) => {
const rId = [...`${Math.random().toString(16) + Date.now().toString(16)}`].slice(2).join('')
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'eth_sendTransaction', resId: rId,
params: [
tx
]
, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC7: Error sending message:", chrome.runtime.lastError);
}
resolve(r)
})
})
@ -26,6 +53,9 @@ export const walletGetData = (rId: string) => {
export const walletPing = () => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_ping', type: 'CLWALLET_CONTENT_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC8: Error sending message:", chrome.runtime.lastError);
}
resolve(r)
})
})

View File

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import AppTabs from '@/views/AppTabs.vue'
import HomeTab from '@/views/HomeTab.vue'
const routes: Array<RouteRecordRaw> = [
{
@ -41,7 +42,7 @@ const routes: Array<RouteRecordRaw> = [
},
{
path: 'home',
component: () => import('@/views/HomeTab.vue'),
component: HomeTab,
},
{
path: 'networks',
@ -79,6 +80,19 @@ const routes: Array<RouteRecordRaw> = [
path: 'add-network/edit/:chainId',
component: () => import('@/views/AddNetwork.vue'),
},
{
path: 'send-token',
component: () => import('@/views/SendToken.vue'),
},
{
path: 'read-contract',
component: () => import('@/views/ReadContract.vue'),
},
{
path: 'write-contract',
component: () => import('@/views/WriteContract.vue'),
},
],
},
]

View File

@ -8,4 +8,4 @@ export const exportFile = (fileName: string, content: string, type = 'json') =>
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}

View File

@ -22,7 +22,7 @@ export const mainNets: {[key: number]: Network} = {
},
100: {
name: 'Gnosis',
rpc: 'https://rpc.gnosischain.com/',
rpc: 'https://rpc.gnosischain.com',
chainId: 100,
explorer: 'https://gnosisscan.io',
icon:'xdai.webp',
@ -56,60 +56,78 @@ export const mainNets: {[key: number]: Network} = {
symbol: 'ETH',
priceId: 'ethereum'
},
8453: {
name: 'Base Mainnet',
rpc: 'https://base.publicnode.com',
chainId: 8453,
explorer: 'https://basescan.org',
icon: 'base.webp',
symbol: 'ETH',
priceId: 'ethereum'
}
}
export const testNets = {
5: {
name: 'TESTNET Ethereum Goerli',
rpc: 'https://rpc.ankr.com/eth_goerli',
chainId: 5,
explorer: 'https://goerli.etherscan.io',
icon: 'eth.webp'
11155111: {
name: 'TESTNET Ethereum Sepolia',
rpc: 'https://ethereum-sepolia-rpc.publicnode.com',
chainId: 11155111,
explorer: 'https://sepolia.etherscan.io',
icon: 'eth_t.webp'
},
4: {
name: 'TESTNET Ethereum Rinkeby',
rpc: 'https://rpc.ankr.com/eth_rinkeby',
chainId: 4,
explorer: 'https://rinkeby.etherscan.io',
icon: 'eth.webp'
84532: {
name: 'TESTNET Base Sepolia',
rpc: 'https://sepolia.base.org',
chainId: 84532,
explorer: 'https://sepolia.basescan.org/',
icon: 'base_t.webp'
},
80001: {
name: 'TESTNET Polygon',
rpc: 'https://rpc.ankr.com/polygon_mumbai',
chainId: 80001,
explorer: 'https://mumbai.polygonscan.com/',
icon:'polygon.webp'
80002: {
name: 'TESTNET Polygon Amoy',
rpc: 'https://rpc-amoy.polygon.technology',
chainId: 80002,
explorer: 'https://oklink.com/amoy',
icon:'polygon_t.webp'
},
100100: {
100200: {
name: 'TESTNET Gnosis Chiado',
rpc: 'https://gnosis-mainnet.public.blastapi.io',
chainId: 100100,
explorer: '',
icon:'xdai.webp'
rpc: 'https://rpc.chiadochain.net',
chainId: 100200,
explorer: 'https://gnosis-chiado.blockscout.com',
icon:'xdai_t.webp'
},
420: {
name: 'TESTNET Optimism Goreli',
rpc: 'https://goerli.optimism.io/',
chainId: 420,
explorer: 'https://goerli.etherscan.io/',
icon: 'optimism.webp'
explorer: 'https://goerli.etherscan.io',
icon: 'optimism_t.webp'
},
11155420 : {
name: 'TESTNET Optimism Sepolia',
rpc: 'https://sepolia.optimism.io',
chainId: 11155420 ,
explorer: 'https://sepolia-optimistic.etherscan.io/',
icon: 'optimism_t.webp'
},
97: {
name: 'TESTNET BSC',
rpc: 'https://bsctestapi.terminet.io/rpc',
rpc: 'https://bsc-testnet-rpc.publicnode.com',
chainId: 97,
explorer: 'https://testnet.bscscan.com/',
icon: 'binance.webp'
explorer: 'https://testnet.bscscan.com',
icon: 'binance_t.webp'
},
421613: {
name: 'TESTNET Arbitrum One',
rpc: 'https://goerli-rollup.arbitrum.io/rpc/',
chainId: 421613,
explorer: 'https://testnet.arbiscan.io/',
icon: 'arbitrum.webp'
421614: {
name: 'TESTNET Arbitrum Sepolia',
rpc: 'https://sepolia-rollup.arbitrum.io/rpc',
chainId: 421614,
explorer: 'https://sepolia.arbiscan.io/',
icon: 'arbitrum_t.webp'
},
}
export const allTemplateNets = {...mainNets, ...testNets}
export const chainIdToPriceId = (chainId: number): string => {
return mainNets?.[chainId]?.priceId ?? 'x'
}

View File

@ -1,6 +1,8 @@
import type { Network, Account, Prices, Settings, Networks, HistoryItem } from '@/extension/types'
import type { Network, Account, Prices, Settings, Networks, HistoryItem, ContractActions, ContractAction, Contact } from '@/extension/types'
import type { Ref } from 'vue'
const pottentialMissingSettings = ['copyLowerCaseAddress']
const defaultSettings = {
enableStorageEnctyption: false,
encryptAfterEveryTx: false,
@ -8,9 +10,16 @@ const defaultSettings = {
lockOutPeriod: 2,
lockOutBlocked: false,
theme: 'system',
lastLock: Date.now()
lastLock: Date.now(),
copyLowerCaseAddress: false
}
const defaultAbis = {} as {
[key: string]: string
}
export const CLW_CONTEXT_MENU_ID = 'clw-paste-address'
export const storageSave = async (key: string, value: any): Promise<void> =>{
await chrome.storage.local.set({ [key]: value })
}
@ -39,6 +48,7 @@ export const saveNetwork = async (network: Network): Promise<void> => {
export const getSelectedNetwork = async (): Promise<Network > => {
console.info('network', (await (storageGet('selectedNetwork')))?.selectedNetwork)
return (await storageGet('selectedNetwork'))?.selectedNetwork ?? null as unknown as Network
}
@ -48,11 +58,23 @@ export const saveSelectedNetwork = async (selectedNetwork: Network ): Promise<v
}
export const getContacts = async (): Promise<Contact[]> => {
return (await storageGet('contacts')).contacts ?? [] as Contact[]
}
export const saveContact = async (contact: Contact): Promise<void> => {
const savedContacts = await getContacts()
await storageSave('contacts', [contact, ...savedContacts])
}
export const replaceContacts = async (contacts: Contact[]): Promise<void> => {
await storageSave('contacts', contacts)
}
export const getAccounts = async (): Promise<Account[]> => {
return (await storageGet('accounts')).accounts ?? [] as Account[]
}
export const saveAccount = async (account: Account): Promise<void> => {
const savedAccounts = await getAccounts()
await storageSave('accounts', [account, ...savedAccounts])
@ -62,6 +84,7 @@ export const replaceAccounts = async (accounts: Account[]): Promise<void> => {
await storageSave('accounts', accounts)
}
export const getSelectedAccount = async (): Promise<Account> => {
return (await storageGet('selectedAccount'))?.selectedAccount ?? null as unknown as Account
}
@ -99,13 +122,93 @@ export const wipeHistory = async (): Promise<void> => {
}
export const getSettings = async (): Promise<Settings> => {
return (await storageGet('settings'))?.settings ?? defaultSettings as unknown as Settings
const settings = (await storageGet('settings'))?.settings ?? defaultSettings as unknown as Settings
pottentialMissingSettings.forEach( (s: string) => {
if(settings[s] === undefined) {
settings[s as keyof Settings] = defaultSettings[s as keyof Settings]
}
})
return settings
}
export const setSettings = async (settings: Settings): Promise<void> => {
await storageSave('settings', settings )
}
export const getAllAbis = async (): Promise<{ [key: string]: string }> => {
return ((await storageGet('abis'))?.abis) ?? defaultAbis
}
export const getAbis = async (name: string): Promise<string> => {
return (await getAllAbis())?.[name] ?? ''
}
export const setAbi = async ({
name ,
content
}: {
name: string
content: string
}): Promise<void> => {
const abis = await getAllAbis() || defaultAbis
await storageSave('abis', { ...abis, [name]: content })
}
export const setAbis = async (abis: { [key: string]: string }): Promise<void> => {
await storageSave('abis', abis)
}
export const removeAllAbis = async (): Promise<void> => {
await storageSave('abis', defaultAbis)
}
export const readCAGetAll = async (): Promise<ContractActions> => {
return ((await storageGet('read-actions'))?.['read-actions'] ?? {}) as ContractActions
}
export const readCAGet = async (action: string): Promise<ContractAction | undefined> => {
return ((await readCAGetAll())?.[action]) as ContractAction
}
export const readCASet = async (action: ContractAction): Promise<void> => {
const actions = await readCAGetAll()
await storageSave('read-actions', { ...actions, [action.name]: action })
}
export const readCARemove = async (action: string): Promise<void> => {
const actions = await readCAGetAll()
delete actions[action]
await storageSave('read-actions', actions)
}
export const readCAWipe = async (): Promise<void> => {
await storageSave('read-actions', {})
}
export const writeCAGetAll = async (): Promise<ContractActions> => {
return ((await storageGet('write-actions'))?.['write-actions'] ?? {}) as ContractActions
}
export const writeCAGet = async (action: string): Promise<ContractAction | undefined> => {
return ((await writeCAGetAll())?.[action]) as ContractAction
}
export const writeCASet = async (action: ContractAction): Promise<void> => {
const actions = await writeCAGetAll()
await storageSave('write-actions', { ...actions, [action.name]: action })
}
export const writeCARemove = async (action: string): Promise<void> => {
const actions = await writeCAGetAll()
delete actions[action]
await storageSave('write-actions', actions)
}
export const writeCAWipe = async (): Promise<void> => {
await storageSave('write-actions', {})
}
export const blockLockout = async (): Promise<Settings> => {
const settings = await getSettings()
settings.lockOutBlocked = true
@ -128,15 +231,6 @@ export const setBalanceCache = async (balance: string): Promise<void> => {
await storageSave('balance', balance )
}
export const getRandomPk = () => {
const array = new Uint32Array(10);
crypto.getRandomValues(array)
return array.reduce(
(pv, cv) => `${pv}${cv.toString(16)}`,
'0x'
).substring(0, 66)
}
export const smallRandomString = (size = 7) => {
if(size <= 7) {
return (Math.random() + 1).toString(36).substring(0,7);
@ -176,11 +270,11 @@ export const hexTostr = (hexStr: string) =>
return hexStr
}
export const strToHex = (str: string) => `0x${str.split('').map( s => s.charCodeAt(0).toString(16) ).join('')}`
export const strToHex = (str: string) => `0x${str.split('').map( s => s.charCodeAt(0).toString(16)).join('')}`
export const numToHexStr = (num: number) => `0x${num.toString(16)}`
export const numToHexStr = (num: number | bigint) => `0x${num.toString(16)}`
export const copyAddress = async (address: string, toastRef: Ref<boolean>) => {
export const copyText = async (address: string, toastRef: Ref<boolean>) => {
await navigator.clipboard.writeText(address)
toastRef.value = true
}
@ -195,8 +289,31 @@ export const paste = (id: string) => {
}
}
export const enableRightClickVote = async () => {
try {
await chrome.contextMenus.removeAll();
await chrome.contextMenus.create({
id: CLW_CONTEXT_MENU_ID,
title: "Paste Current Address",
contexts: ["editable"],
});
} catch (error) {
// ignore
}
}
export const pasteToFocused = () => {
const el = document.activeElement as HTMLInputElement
if(el){
el?.focus();
(document as any)?.execCommand('paste')
}
}
export const openTab = (url: string) => {
chrome.tabs.create({
url
});
}
export const getVersion = () => chrome?.runtime?.getManifest()?.version ?? ''

View File

@ -1,90 +1,141 @@
import { getSelectedAccount, getSelectedNetwork } from '@/utils/platform';
import { BigNumber, ethers } from "ethers"
import { getSelectedAccount, getSelectedNetwork, numToHexStr } from '@/utils/platform';
import { ethers } from "ethers"
const convertReceipt = (receipt: ethers.TransactionReceipt | null) => {
if(!receipt) return null
const newReceipt = {...receipt} as any
newReceipt.transactionHash = newReceipt.hash
newReceipt.blockNumber = numToHexStr(newReceipt.blockNumber)
newReceipt.index = numToHexStr(newReceipt.index)
newReceipt.transactionIndex = newReceipt.index
newReceipt.cumulativeGasUsed = numToHexStr(newReceipt.cumulativeGasUsed)
newReceipt.gasUsed = numToHexStr(newReceipt.gasUsed)
newReceipt.gasPrice = numToHexStr(newReceipt.gasPrice)
newReceipt.type = "0x2"
newReceipt.status = numToHexStr(newReceipt.status)
newReceipt.logs = receipt?.logs?.map((log: any) => {
return {
...log,
blockNumber: numToHexStr(log.blockNumber),
logIndex: numToHexStr(log.index),
transactionIndex: numToHexStr(log.transactionIndex),
removed: false
}
})
return newReceipt
}
export const signMsg = async (msg: string) => {
const account = await getSelectedAccount()
const wallet = new ethers.Wallet(account.pk)
return await wallet.signMessage( msg.startsWith('0x') ? ethers.utils.arrayify(msg): msg)
return await wallet.signMessage( msg.startsWith('0x') ? ethers.getBytes(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]
}
const types = {} as Record<string, any>
for (const key in parsedMsg.types) {
if (key !== 'EIP712Domain') {
types[key] = parsedMsg.types[key]
}
}
return await wallet._signTypedData(parsedMsg.domain, parsedMsg.types, parsedMsg.message)
parsedMsg.types = types
const args = [parsedMsg.domain, parsedMsg.types, parsedMsg.message]
return await wallet.signTypedData(args[0], args[1], args[2])
}
export const getBalance = async () =>{
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
return await provider.getBalance(account.address)
}
export const getGasPrice = async () => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.getGasPrice()
const provider = new ethers.JsonRpcProvider(network.rpc)
const feed = await provider.getFeeData()
const gasPrice = feed.maxFeePerGas ?? feed.gasPrice ?? 0n
return Number(gasPrice) / 1e9
}
export const getBlockNumber = async () => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
return await provider.getBlockNumber()
}
export const getBlockByNumber = async (blockNum: number) => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
return await provider.getBlock(blockNum)
}
export const estimateGas = 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)
const provider = new ethers.JsonRpcProvider(network.rpc)
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}) => {
export const evmCall = async (params: any[]) => {
const tx = {} as {to: string, from: string, data: string, value: string, blockTag: string}
const param1 = params[0] as any
if(param1.to) tx.to = param1.to
if(param1.from) tx.from = param1.from
if(param1.data) tx.data = param1.data
if(param1.value) tx.value = param1.value
const param2 = params[1] as string
if (param2.startsWith('0x')) {
tx.blockTag = param2
} else {
tx.blockTag = 'latest'
}
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.call({to, from, data, value})
const provider = new ethers.JsonRpcProvider(network.rpc)
const result = await provider.call(tx)
return result
}
export const getTxByHash = async (hash: string) => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
return await provider.getTransaction(hash)
}
export const getTxReceipt = async (hash: string) => {
try {
if (!hash) return null
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.getTransactionReceipt(hash)
const provider = new ethers.JsonRpcProvider(network.rpc)
const receipt = await provider.getTransactionReceipt(hash)
return convertReceipt(receipt)
} catch (e) {
console.error(e)
return null
}
}
export const getCode = async (addr: string) => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
return await provider.getCode(addr)
}
export const getFromMemonic = (memonic: string, index: number) => {
export const getFromMnemonic = (mnemonic: string, index: number) => {
const path = `m/44'/60'/0'/0/${index}`
const wallet = ethers.Wallet.fromMnemonic(memonic, path)
const mnemonicInst = ethers.Mnemonic.fromPhrase(mnemonic)
const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonicInst, path)
return wallet.privateKey
}
export const getTxCount = async (addr: string, block: null | string = null) => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
const provider = new ethers.JsonRpcProvider(network.rpc)
if(block){
return await provider.getTransactionCount(addr, block)
} else {
@ -92,33 +143,47 @@ export const getTxCount = async (addr: string, block: null | string = null) => {
}
}
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},
gasEstimate: Promise<BigNumber> | null = null, pGasPrice : Promise<BigNumber> | null) => {
export const getRandomPk = () => {
return ethers.Wallet.createRandom().privateKey
}
export const getCurrentProvider = async () => {
const network = await getSelectedNetwork()
return new ethers.JsonRpcProvider(network.rpc)
}
export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='', gasPrice='0x0'}:
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string}) => {
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.providers.JsonRpcProvider(network.rpc))
if(gas === '0x0') {
if(!gasEstimate){
throw new Error('No gas estimate available')
}else {
gas = (await gasEstimate).toString()
}
}
const wallet = new ethers.Wallet(account.pk, new ethers.JsonRpcProvider(network.rpc))
const gasPriceInt = BigInt(gasPrice)
const gasInt = BigInt(gas)
if(gasPrice === '0x0') {
if(!pGasPrice){
throw new Error('No gas estimate available')
}else {
gasPrice = (await pGasPrice).toString()
}
if(gas === '0x0' || gasPrice === '0x0') {
throw new Error('No gas estimate available')
}
return await wallet.sendTransaction({to, from, data, value, gasLimit: gas, gasPrice})
return await wallet.sendTransaction({
to,
from,
data: data ? data : null,
value: value ? value : null,
gasLimit: gasInt,
gasPrice: null,
maxFeePerGas: gasPriceInt,
})
}
export const formatBalance = (balance: string) => {
Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 6
}).format(Number(ethers.utils.parseEther(balance)))
}).format(Number(ethers.parseEther(balance)))
}
export const getSelectedAddress = async () => {
// give only the selected address for better privacy
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
return address
}

178
src/views/AbiAdd.vue Normal file
View File

@ -0,0 +1,178 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="onCancel">Close</ion-button>
</ion-buttons>
<ion-title v-if="!isEdit">Add Abi</ion-title>
<ion-title v-else>Edit Abi</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-input
label="Abi Name(*)"
labelPlacement="stacked"
v-model="name"
:readonly="isEdit"
:style="`${isEdit ? 'opacity: 0.6;' : ''}}`"
></ion-input>
</ion-item>
<ion-item>
<ion-textarea
label="Content:"
labelPlacement="stacked"
style="overflow-y: scroll"
:rows="10"
:cols="40"
v-model="content"
></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onAddAbi">{{ isEdit ? "Edit ABI" : "Add ABI" }}</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
header="Error"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
// IonLabel,
IonInput,
IonButton,
IonAlert,
// IonIcon,
onIonViewWillEnter,
// modalController,
// IonModal,
IonButtons,
IonLoading,
IonTextarea,
modalController,
} from "@ionic/vue";
// import { ethers } from "ethers";
import { paste, setAbi } from "@/utils/platform";
// import { useRoute } from "vue-router";
import { clipboardOutline } from "ionicons/icons";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
// IonLabel,
IonInput,
IonButton,
IonAlert,
// IonIcon,
// IonModal,
IonButtons,
IonTextarea,
IonLoading,
},
props: {
abi: {
type: Object as PropType<{ [key: string]: string } | null>,
default: null,
},
},
setup: (props) => {
const isEdit = ref(props.abi !== null);
const name = ref(props.abi?.name ?? "");
const content = ref(props.abi?.content ?? "");
const alertOpen = ref(false);
const alertMsg = ref("");
const loading = ref(false);
onIonViewWillEnter(async () => {});
const onAddAbi = async () => {
if (!name.value) {
alertMsg.value = "Please input name.";
alertOpen.value = true;
return;
}
if (!content.value) {
alertMsg.value = "Please input content.";
alertOpen.value = true;
return;
}
try {
JSON.parse(content.value);
} catch (e) {
alertMsg.value = "Invalid content, must be json format.";
alertOpen.value = true;
return;
}
loading.value = true;
await setAbi({
name: name.value,
content: content.value,
});
modalController.dismiss(
{
name: name.value,
content: content.value,
},
"confirm"
);
loading.value = false;
};
const onCancel = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
return {
name,
content,
onAddAbi,
onCancel,
alertOpen,
alertMsg,
clipboardOutline,
paste,
isEdit,
loading,
};
},
});
</script>

146
src/views/AbiList.vue Normal file
View File

@ -0,0 +1,146 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Select Abi</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button expand="block" @click="openModal">Add New ABI</ion-button>
<ion-radio-group :value="selectedAbi">
<ion-list-header>
<ion-label>Saved ABIs</ion-label>
</ion-list-header>
<ion-list class="ion-padding" v-for="item of Object.keys(abis ?? {})" :key="item">
<ion-item>
<ion-radio
@click="changeSelected(item)"
slot="start"
:value="item"
:aria-label="item"
>
{{ item }}
</ion-radio>
<ion-button @click="onEdit(item)">Edit</ion-button>
<ion-button @click="onDelete(item)">Delete</ion-button>
</ion-item>
</ion-list>
</ion-radio-group>
<ion-list v-if="!!!Object.keys(abis ?? {}).length">
<ion-item class="ion-padding">
<ion-label>No Abis found, please add at least one</ion-label>
</ion-item>
</ion-list>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts" setup>
import {
IonButton,
IonContent,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonItem,
modalController,
IonRadio,
IonButtons,
IonListHeader,
IonRadioGroup,
IonLabel,
IonLoading,
} from "@ionic/vue";
import AbiAdd from "./AbiAdd.vue";
import { ref, onMounted, Ref } from "vue";
import {
getAllAbis,
setAbis,
// removeAllAbis
} from "@/utils/platform";
const loading = ref(false);
const abis = ref() as Ref<{ [key: string]: string }>;
const selectedAbi = ref("");
onMounted(async () => {
loading.value = true;
// await removeAllAbis();
abis.value = (await getAllAbis()) ?? {};
loading.value = false;
});
const changeSelected = (item: string) => {
selectedAbi.value = item;
modalController.dismiss(
{
name: item,
content: abis.value[item],
},
"confirm"
);
};
const openModal = async (id = "") => {
const modal = await modalController.create({
component: AbiAdd,
componentProps: {
abi: abis.value?.[id] ? { name: id, content: abis.value?.[id] } : null,
},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
selectedAbi.value = data.name;
abis.value = {
...(abis.value ?? {}),
[data.name]: data.content,
};
if (Object.keys(abis.value ?? {}).length === 1) {
changeSelected(data.name);
}
}
};
const onEdit = (id: string) => {
openModal(id);
};
const onDelete = async (id: string) => {
loading.value = true;
if (abis.value?.[id]) {
delete abis.value[id];
await setAbis({ ...(abis.value ?? {}) });
}
loading.value = false;
};
const close = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
</script>

View File

@ -0,0 +1,115 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Select Function</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-searchbar placeholder="Search" @ionInput="onSearch"></ion-searchbar>
</ion-item>
<ion-radio-group :value="selectedAbi">
<ion-list-header>
<ion-label>Functions</ion-label>
</ion-list-header>
<ion-list class="ion-padding" v-for="(item, index) in refFunctions" :key="item">
<ion-item>
<ion-radio
@click="changeSelected(Number(index))"
slot="start"
:value="item"
:aria-label="item"
>
{{ item }}
</ion-radio>
</ion-item>
</ion-list>
</ion-radio-group>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts" setup>
import {
IonContent,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonItem,
modalController,
IonRadio,
IonListHeader,
IonRadioGroup,
IonLabel,
IonLoading,
IonSearchbar,
IonButtons,
IonButton,
} from "@ionic/vue";
import { ref, onMounted, Ref, PropType } from "vue";
// import {
// getAllAbis,
// setAbis,
// // removeAllAbis
// } from "@/utils/platform";
const props = defineProps({
functions: {
type: Array as PropType<string[]>,
required: true,
},
});
const refFunctions = ref(props.functions) as Ref<string[]>;
const loading = ref(false);
const selectedAbi = ref("");
const onSearch = (e: any) => {
const text = e.target.value;
if (text) {
refFunctions.value = props.functions.filter((item) =>
item.toLowerCase().includes(text.toLowerCase())
);
} else {
refFunctions.value = props.functions;
}
};
onMounted(async () => {
loading.value = true;
// await removeAllAbis();
loading.value = false;
});
const changeSelected = (item: number) => {
modalController.dismiss(refFunctions.value[item], "confirm");
};
const close = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
</script>

View File

@ -4,72 +4,81 @@
<ion-toolbar>
<ion-buttons slot="end">
<router-link to="/tabs/add-account">
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-title>Accounts</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-toast
:is-open="toastState"
@didDismiss="toastState=false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
<ion-item v-if="loading || accounts.length < 1">
<ion-label>No EVM accounts found</ion-label>
<ion-button @click="goToAddAccount">Add Account</ion-button>
</ion-item>
<ion-list v-for="account of accounts" :key="account.address">
<ion-item>
<ion-label>
<ion-list v-for="account of accounts" :key="account.address">
<ion-item>
<ion-label>
{{ account.name }}
</ion-label>
</ion-item>
<ion-item @click="copyAddress(account.address, getToastRef())">
<p style="font-size:0.7rem">{{ account.address }}</p><ion-icon :icon="copyOutline"></ion-icon>
</ion-label>
</ion-item>
<ion-item @click="copyText(account.address, getToastRef())">
<p style="font-size: 0.7rem">{{ account.address }}</p>
<ion-icon :icon="copyOutline"></ion-icon>
</ion-item>
<ion-item>
<ion-chip @click="viewPk(account.address)">View Pk</ion-chip>
<ion-chip @click="deleteAccount(account.address)">Delete</ion-chip>
<ion-chip @click="editAccount(account.address)">Edit Name</ion-chip>
<ion-chip @click="viewPk(account.address)">View Pk</ion-chip>
<ion-chip @click="deleteAccount(account.address)">Delete</ion-chip>
<ion-chip @click="editAccount(account.address)">Edit Name</ion-chip>
</ion-item>
</ion-list>
</ion-list>
<ion-modal
:is-open="pkModal"
@didDismiss="shownPk=''"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="pkModal=false">Close</ion-button>
</ion-buttons>
<ion-title>View PK</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item @click="copyAddress(shownPk, getToastRef())" button>
<ion-icon style="margin-right: 0.5rem;" :icon="copyOutline" />
<ion-label button>PK</ion-label>
<ion-input id="pastePk" v-model="shownPk" readonly></ion-input>
</ion-item>
</ion-content>
<ion-modal :is-open="pkModal" @didDismiss="shownPk = ''">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="pkModal = false">Close</ion-button>
</ion-buttons>
<ion-title>View PK</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item @click="copyText(shownPk, getToastRef())" button>
<ion-icon style="margin-right: 0.5rem" :icon="copyOutline" />
<ion-label button>PK</ion-label>
<ion-input
aria-label="pk"
id="pastePk"
v-model="shownPk"
readonly
></ion-input>
</ion-item>
</ion-content>
</ion-modal>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import { getAccounts, copyAddress, replaceAccounts, getSettings, clearPk, getSelectedAccount, saveSelectedAccount } from "@/utils/platform"
import {
getAccounts,
copyText,
replaceAccounts,
getSettings,
clearPk,
getSelectedAccount,
saveSelectedAccount,
} from "@/utils/platform";
import {
IonContent,
IonHeader,
@ -87,14 +96,14 @@ import {
IonToast,
modalController,
IonInput,
IonModal
IonModal,
} from "@ionic/vue";
import { addCircleOutline, copyOutline } from "ionicons/icons";
import router from "@/router";
import UnlockModal from '@/views/UnlockModal.vue'
import UnlockModal from "@/views/UnlockModal.vue";
import type { Account, Settings } from '@/extension/types'
import type { Account, Settings } from "@/extension/types";
export default defineComponent({
components: {
@ -109,120 +118,118 @@ export default defineComponent({
IonLabel,
IonChip,
IonButtons,
IonButton,
IonToast,
IonInput,
IonModal
IonButton,
IonToast,
IonInput,
IonModal,
},
setup () {
const accounts = ref([]) as Ref<Account[]>
const loading = ref(true)
const toastState = ref(false)
const shownPk = ref('')
const pkModal = ref(false)
const settings = ref({}) as Ref<Settings>
setup() {
const accounts = ref([]) as Ref<Account[]>;
const loading = ref(true);
const toastState = ref(false);
const shownPk = ref("");
const pkModal = ref(false);
const settings = ref({}) as Ref<Settings>;
const getToastRef = () => toastState;
const getToastRef = () => toastState
const loadData = () => {
const pAccounts = getAccounts()
const pGetSettings = getSettings()
Promise.all([pAccounts, pGetSettings]).then(( res ) => {
accounts.value = res[0]
settings.value = res[1]
loading.value = false
})
}
const pAccounts = getAccounts();
const pGetSettings = getSettings();
Promise.all([pAccounts, pGetSettings]).then((res) => {
accounts.value = res[0];
settings.value = res[1];
loading.value = false;
});
};
const deleteAccount = async (address: string) => {
loading.value = true
if(settings.value.enableStorageEnctyption) {
const modalR = await openModal('delAcc')
if(!modalR){
return
}
loading.value = true;
if (settings.value.enableStorageEnctyption) {
const modalR = await openModal("delAcc");
if (!modalR) {
return;
}
const findIndex = accounts.value.findIndex(a => a.address === address)
const selectedAccount = await getSelectedAccount()
const pArr: Array<Promise<void>> = []
if (findIndex !== -1) {
accounts.value.splice(findIndex, 1)
pArr.push(replaceAccounts([...accounts.value]))
}
if(selectedAccount.address === address) {
pArr.push(saveSelectedAccount({ name: '', pk: '', encPk: '', address: ''}))
}
await Promise.all(pArr)
loading.value = false
}
}
const findIndex = accounts.value.findIndex((a) => a.address === address);
const selectedAccount = await getSelectedAccount();
const pArr: Array<Promise<void>> = [];
if (findIndex !== -1) {
accounts.value.splice(findIndex, 1);
pArr.push(replaceAccounts([...accounts.value]));
}
if (selectedAccount.address === address) {
pArr.push(saveSelectedAccount({ name: "", pk: "", encPk: "", address: "" }));
}
await Promise.all(pArr);
loading.value = false;
};
const editAccount = (address: string) => {
router.push(`add-account/edit/${address}`)
}
router.push(`add-account/edit/${address}`);
};
const goToAddAccount = () => {
router.push("/tabs/add-account");
};
onIonViewWillEnter(() => {
loadData()
})
loadData();
});
const openModal = async (type: string) => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: type
const openModal = async (type: string) => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: type,
},
});
modal.present();
const { role } = await modal.onWillDismiss();
if (role === "confirm") return true;
return false;
};
const viewPk = async (addr: string) => {
let pk = "";
const account = accounts.value.find((a) => a.address === addr);
if (settings.value.enableStorageEnctyption) {
if (account?.encPk) {
const modalR = await openModal("viewPk");
if (modalR) {
const account = (await getAccounts()).find((a) => a.address === addr);
pk = account?.pk ?? "";
}
});
modal.present();
const { role } = await modal.onWillDismiss();
if(role === 'confirm') return true
return false
}
const viewPk = async (addr: string) => {
let pk = ''
const account = accounts.value.find(a => a.address === addr)
if(settings.value.enableStorageEnctyption) {
if(account?.encPk) {
const modalR = await openModal('viewPk')
if(modalR){
const account = (await getAccounts()).find(a => a.address === addr)
pk = account?.pk ?? ''
}
}else {
pk = account?.pk ?? ''
}
}else {
pk = account?.pk ?? ''
} else {
pk = account?.pk ?? "";
}
if(pk) {
shownPk.value = pk
if(settings.value.encryptAfterEveryTx) {
clearPk()
}
pkModal.value = true
} else {
pk = account?.pk ?? "";
}
if (pk) {
shownPk.value = pk;
if (settings.value.encryptAfterEveryTx) {
clearPk();
}
pkModal.value = true;
}
};
return {
accounts,
addCircleOutline,
copyOutline,
toastState,
copyAddress,
getToastRef,
deleteAccount,
editAccount,
loading,
goToAddAccount,
viewPk,
pkModal,
shownPk
}
}
return {
accounts,
addCircleOutline,
copyOutline,
toastState,
copyText,
getToastRef,
deleteAccount,
editAccount,
loading,
goToAddAccount,
viewPk,
pkModal,
shownPk,
};
},
});
</script>

View File

@ -9,8 +9,7 @@
<ion-content class="ion-padding">
<ion-item>
<ion-label>Name</ion-label>
<ion-input v-model="name"></ion-input>
<ion-input label="Name" labelPlacement="stacked" v-model="name"></ion-input>
</ion-item>
<ion-item>
<ion-label>Get Random Name</ion-label>
@ -23,8 +22,12 @@
:icon="clipboardOutline"
button
/>
<ion-label button>PK</ion-label>
<ion-input id="pastePk" v-model="pk"></ion-input>
<ion-input
label="PK"
labelPlacement="stacked"
id="pastePk"
v-model="pk"
></ion-input>
</ion-item>
<template v-if="!isEdit">
<ion-item>
@ -33,15 +36,22 @@
</ion-item>
<ion-item>
<ion-button @click="mnemonicModal = true" expand="full"
>Extarct From A Mnemonic</ion-button
>Extract From A Mnemonic</ion-button
>
</ion-item>
</template>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onAddAccount">{{
isEdit ? "Edit Account" : "Add Account"
}}</ion-button>
<ion-button
@click="
() => {
isEdit ? onEditAccount() : onAddAccount();
}
"
expand="full"
color="primary"
>{{ isEdit ? "Edit Account" : "Add Account" }}</ion-button
>
</ion-item>
<ion-alert
:is-open="alertOpen"
@ -67,6 +77,7 @@
<ion-item>
<ion-textarea
style="overflow-y: scroll"
aria-label="Enter mnemonic"
:rows="10"
:cols="10"
v-model="mnemonic"
@ -74,9 +85,12 @@
</ion-item>
<ion-item>
<ion-label>Enter Index (default: 0)</ion-label>
<ion-input v-model="mnemonicIndex"></ion-input>
</ion-item>
<ion-item>
<ion-input aria-label="mnemonic index" v-model="mnemonicIndex"></ion-input>
</ion-item>
<ion-item>
<ion-button @click="mnemonicModal = false" color="light">Close</ion-button>
<ion-button @click="extractMnemonic">Extract</ion-button>
</ion-item>
</ion-content>
@ -110,10 +124,10 @@ import {
saveSelectedAccount,
getAccounts,
saveAccount,
getRandomPk,
smallRandomString,
paste,
getSettings,
replaceAccounts,
} from "@/utils/platform";
import router from "@/router";
import { useRoute } from "vue-router";
@ -122,7 +136,7 @@ import UnlockModal from "@/views/UnlockModal.vue";
import { encrypt, getCryptoParams } from "@/utils/webCrypto";
import { clipboardOutline } from "ionicons/icons";
import { getFromMemonic } from "@/utils/wallet";
import { getFromMnemonic, getRandomPk } from "@/utils/wallet";
export default defineComponent({
components: {
@ -186,8 +200,48 @@ export default defineComponent({
}
});
const deleteAccount = async (address: string, accounts: Account[]) => {
const findIndex = accounts.findIndex((a) => a.address === address);
const pArr: Array<Promise<void>> = [];
if (findIndex !== -1) {
accounts.splice(findIndex, 1);
pArr.push(replaceAccounts([...accounts]));
}
await Promise.all(pArr);
};
const onEditAccount = async () => {
if (name.value.length < 1) {
alertMsg.value = "Name cannot be empty.";
alertOpen.value = true;
return;
}
const accounts = (await accountsProm) as Account[];
const account = accounts.find((acc) => acc.address === paramAddress);
if (!account) {
alertMsg.value = "Account not found.";
alertOpen.value = true;
return;
}
const savedAcc = {
address: account.address,
name: name.value,
pk: account.pk,
encPk: account.encPk,
};
await deleteAccount(account.address, accounts);
await saveAccount(savedAcc);
router.push("/tabs/accounts");
};
const onAddAccount = async () => {
let p1 = Promise.resolve();
if (name.value.length < 1) {
alertMsg.value = "Name cannot be empty.";
alertOpen.value = true;
return;
}
if (pk.value.length === 64) {
pk.value = `0x${pk.value.trim()}`;
}
@ -294,7 +348,7 @@ export default defineComponent({
alertOpen.value = true;
return;
}
pk.value = getFromMemonic(mnemonic.value, mnemonicIndex.value);
pk.value = getFromMnemonic(mnemonic.value, mnemonicIndex.value);
mnemonicModal.value = false;
};
@ -314,6 +368,7 @@ export default defineComponent({
mnemonic,
mnemonicIndex,
extractMnemonic,
onEditAccount,
};
},
});

153
src/views/AddContact.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title v-if="!isEdit">Add Contact</ion-title>
<ion-title v-else>Edit Contact</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-input label="Name" labelPlacement="stacked" v-model="localName"></ion-input>
</ion-item>
<ion-item>
<ion-icon
style="margin-right: 0.5rem"
@click="paste('address')"
:icon="clipboardOutline"
button
/>
<ion-input
label="Address"
labelPlacement="stacked"
id="address"
v-model="localAddress"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="close">Cancel</ion-button>
<ion-button @click="onAddContact">{{
isEdit ? "Edit Contact" : "Add Contact"
}}</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
header="Error"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonInput,
IonButton,
IonAlert,
IonIcon,
onIonViewWillEnter,
modalController,
} from "@ionic/vue";
import { paste, saveContact } from "@/utils/platform";
import { clipboardOutline } from "ionicons/icons";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonInput,
IonButton,
IonAlert,
IonIcon,
},
props: {
address: {
type: String,
default: "",
},
name: {
type: String,
default: "",
},
isEdit: {
type: Boolean,
default: false,
},
},
setup: (props) => {
const localName = ref(props.name);
const localAddress = ref(props.address);
const localIsEdit = ref(props.isEdit);
const alertOpen = ref(false);
const alertMsg = ref("");
const resetFields = () => {
localName.value = "";
localAddress.value = "";
};
onIonViewWillEnter(async () => {});
const onAddContact = async () => {
if (!localName.value) {
alertMsg.value = "Name is required.";
alertOpen.value = true;
return;
}
if (!localAddress.value) {
alertMsg.value = "Address is required.";
alertOpen.value = true;
return;
}
await saveContact({
name: localName.value,
address: localAddress.value,
});
resetFields();
modalController.dismiss(
{
name: localName.value,
address: localAddress.value,
},
"confirm"
);
};
const close = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
return {
localName,
localAddress,
onAddContact,
close,
alertOpen,
alertMsg,
clipboardOutline,
paste,
localIsEdit,
};
},
});
</script>

View File

@ -10,31 +10,46 @@
>Add from popular chain list</ion-button
>
<ion-item>
<ion-label>Name(*)</ion-label>
<ion-input v-model="name" placeholder="ex: Polygon"></ion-input>
<ion-input
label="Name(*)"
labelPlacement="stacked"
v-model="name"
placeholder="ex: Polygon"
></ion-input>
</ion-item>
<ion-item>
<ion-label>ChainId(*)</ion-label>
<ion-input v-model="chainId" placeholder="137" type="number"></ion-input>
<ion-input
label="ChainId(*)"
labelPlacement="stacked"
v-model="chainId"
placeholder="137"
type="number"
></ion-input>
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>RPC URL(*)</ion-label>
<ion-input
label="RPC URL(*)"
labelPlacement="stacked"
id="pasteRpc"
placeholder="https://polygon-mainnet.g.alchemy.com/..."
v-model="rpc"
></ion-input>
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>Native Token Symbol(?)</ion-label>
<ion-input id="pasteRpc" placeholder="MATIC" v-model="symbol"></ion-input>
<ion-input
label="Native Token Symbol"
labelPlacement="stacked"
id="native-token"
placeholder="MATIC"
v-model="symbol"
></ion-input>
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteExplorer')" />
<ion-label>Explorer(?)</ion-label>
<ion-input
label="Explorer"
labelPlacement="stacked"
id="pasteExplorer"
placeholder="https://polygonscan.com"
v-model="explorer"
@ -144,6 +159,7 @@ import {
import {
getNetworks,
saveSelectedNetwork,
getSelectedNetwork,
getUrl,
paste,
replaceNetworks,
@ -188,7 +204,6 @@ export default defineComponent({
const route = useRoute();
const isEdit = route.path.includes("/edit");
const paramChainId = route.params.chainId ?? "";
let networksProm: Promise<Networks | undefined>;
const fillNetworkInputs = (network: Network) => {
name.value = network.name;
@ -200,8 +215,7 @@ export default defineComponent({
onIonViewWillEnter(async () => {
if (isEdit && paramChainId) {
networksProm = getNetworks();
const networks = (await networksProm) as Networks;
const networks = (await getNetworks()) as Networks;
fillNetworkInputs(networks[Number(paramChainId)]);
}
});
@ -234,10 +248,13 @@ export default defineComponent({
}
}
let p1 = Promise.resolve();
if (!networksProm) {
networksProm = getNetworks();
}
const networks = (await networksProm) as Networks;
const networksProm = getNetworks();
const selectedNetworkProm = getSelectedNetwork();
const allNetworks = await Promise.all([networksProm, selectedNetworkProm]);
const networks = allNetworks[0] as Networks;
const selectedNetwork = allNetworks[1] as Network;
const network = {
name: name.value,
chainId: chainId.value,
@ -245,7 +262,10 @@ export default defineComponent({
...(symbol.value ? { symbol: symbol.value } : {}),
...(explorer.value ? { explorer: explorer.value } : {}),
};
if ((Object.keys(networks).length ?? 0) < 1) {
if (
(Object.keys(networks).length ?? 0) < 1 ||
selectedNetwork.chainId === chainId.value
) {
p1 = saveSelectedNetwork(network);
} else {
if (chainId.value in networks && !isEdit) {

View File

@ -35,6 +35,23 @@
<ion-label>Settings</ion-label>
</ion-tab-button>
</ion-tab-bar>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="send-token" href="/tabs/send-token">
<ion-icon :icon="sendOutline"></ion-icon>
<ion-label>Send Tokens</ion-label>
</ion-tab-button>
<ion-tab-button tab="read-contract" href="/tabs/read-contract">
<ion-icon :icon="glassesOutline"></ion-icon>
<ion-label>Read Contract</ion-label>
</ion-tab-button>
<ion-tab-button tab="write-contract" href="/tabs/write-contract">
<ion-icon :icon="pushOutline"></ion-icon>
<ion-label>Write Contracts</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
</ion-page>
@ -50,9 +67,19 @@ import {
IonTabBar,
IonTabButton,
IonLabel,
IonIcon
IonIcon,
} from "@ionic/vue";
import { personCircle, walletOutline, diamondOutline, cogOutline, receiptOutline, gitNetworkOutline } from "ionicons/icons";
import {
personCircle,
walletOutline,
diamondOutline,
cogOutline,
receiptOutline,
gitNetworkOutline,
sendOutline,
glassesOutline,
pushOutline,
} from "ionicons/icons";
export default defineComponent({
components: {
@ -63,7 +90,7 @@ export default defineComponent({
IonTabBar,
IonTabButton,
IonLabel,
IonIcon
IonIcon,
},
name: "AppTabs",
setup() {
@ -81,8 +108,11 @@ export default defineComponent({
cogOutline,
receiptOutline,
gitNetworkOutline,
sendOutline,
beforeTabChange,
afterTabChange,
glassesOutline,
pushOutline,
};
},
});

View File

@ -16,6 +16,7 @@
>
</ion-loading>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
@ -25,7 +26,7 @@
<ion-item>
<ion-label>Assests for Account: {{ selectedAccount?.name }}</ion-label>
</ion-item>
<ion-item button @click="copyAddress(selectedAccount?.address, getToastRef())">
<ion-item button @click="copyText(selectedAccount?.address, getToastRef())">
<p style="font-size: 0.7rem">{{ selectedAccount?.address }}</p>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-item>
@ -33,7 +34,11 @@
Assets info could not be retrieved because of an http error, API down or
conectivity issues.
</template>
<template v-else-if="noAssets"> No assets found for this wallet address. </template>
<template v-else-if="noAssets">
<p class="padding: 1rem;">
No know assets found for this wallet address.
</p></template
>
<template v-else>
<template v-if="ethTokens.length || polyTokens.length">
<template v-if="ethTokens.length">
@ -42,12 +47,12 @@
<ion-item v-for="token of ethTokens" :key="token.address">
<ion-avatar
v-if="token?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="token?.name"
:src="token?.image"
@error="token.image = getUrl('assets/randomGrad.svg')"
@error="token.image = getUrl('assets/chain-icons/eth.webp')"
/>
</ion-avatar>
<ion-label
@ -66,7 +71,7 @@
<ion-item v-for="token of polyTokens" :key="token.address">
<ion-avatar
v-if="token?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="token?.name"
@ -91,7 +96,7 @@
<ion-item v-for="nft of ethNfts" :key="nft.address">
<ion-avatar
v-if="nft?.imageURI"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="nft?.collectionName"
@ -115,7 +120,7 @@
<ion-item v-for="nft of polyNfts" :key="nft.address">
<ion-avatar
v-if="nft?.imageURI"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="nft?.collectionName"
@ -139,7 +144,7 @@
<ion-item v-for="nft of poaps" :key="nft.eventId">
<ion-avatar
v-if="nft?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="nft?.title"
@ -179,7 +184,7 @@ import {
IonLoading,
IonIcon,
} from "@ionic/vue";
import { getSelectedAccount, copyAddress, getUrl } from "@/utils/platform";
import { getSelectedAccount, copyText, getUrl } from "@/utils/platform";
import type { Account } from "@/extension/types";
import { copyOutline } from "ionicons/icons";
@ -274,7 +279,7 @@ export default defineComponent({
return null;
}
} catch (error) {
console.error("Failed to fetch web3 profiles", error);
console.info("ERROR: Failed to fetch web3 profiles", error);
return null;
}
};
@ -559,7 +564,7 @@ export default defineComponent({
isError,
noAssets,
getToastRef,
copyAddress,
copyText,
copyOutline,
ethTokens,
polyTokens,

View File

@ -0,0 +1,173 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Select Contact</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-button @click="openModalAddContact()" expand="block">Add contact</ion-button>
</ion-item>
<ion-item>
<ion-searchbar placeholder="Search" @ionInput="onSearch"></ion-searchbar>
</ion-item>
<ion-radio-group :value="selectedContact">
<ion-list-header>
<ion-label>Contacts</ion-label>
</ion-list-header>
<ion-list class="ion-padding" v-for="(item, index) in contacts" :key="index">
<ion-item>
<ion-radio
@click="changeSelected(item.address)"
slot="start"
:value="item"
:aria-label="item"
>
<ion-list>
<ion-item>
<ion-label>{{ item.name }}</ion-label>
</ion-item>
<ion-item>
<ion-label style="font-size: 0.75rem">{{ item.address }}</ion-label>
</ion-item>
</ion-list>
</ion-radio>
</ion-item>
<ion-item>
<ion-button @click="openModalAddContact(item.address)" expand="block"
>Edit contact</ion-button
>
<ion-button @click="deleteContact(item.address)" expand="block"
>Delete contact</ion-button
>
</ion-item>
</ion-list>
<ion-list v-if="!!!contacts.length">
<ion-item class="ion-padding">
<ion-label>No contacts found, please add at least one</ion-label>
</ion-item>
</ion-list>
</ion-radio-group>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts" setup>
import {
IonContent,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonItem,
modalController,
IonRadio,
IonListHeader,
IonRadioGroup,
IonLabel,
IonLoading,
IonSearchbar,
IonButtons,
IonButton,
} from "@ionic/vue";
import { ref, onMounted, Ref } from "vue";
import AddContact from "@/views/AddContact.vue";
import { getContacts, replaceContacts } from "@/utils/platform";
import type { Contact } from "@/extension/types";
const loading = ref(false);
let intialContacts = [] as Contact[];
const contacts = ref([]) as Ref<Contact[]>;
const selectedContact = ref(null) as Ref<Contact | null>;
const onSearch = (e: any) => {
const text = e.target.value;
if (text) {
contacts.value = contacts.value.filter(
(item) =>
item.name.toLowerCase().includes(text.toLowerCase()) ||
item.address.toLowerCase().includes(text.toLowerCase())
);
} else {
contacts.value = intialContacts;
}
};
const loadContacts = async () => {
loading.value = true;
intialContacts = await getContacts();
contacts.value = intialContacts;
loading.value = false;
};
onMounted(async () => {
loadContacts();
});
const openModalAddContact = async (address = "") => {
let modal: Awaited<ReturnType<typeof modalController.create>>;
if (address) {
const contact = contacts.value.find((item) => item.address === address);
modal = await modalController.create({
component: AddContact,
componentProps: {
name: contact?.name,
address: contact?.address,
isEdit: true,
},
});
} else {
modal = await modalController.create({
component: AddContact,
componentProps: {},
});
}
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
selectedContact.value = data;
loadContacts();
}
};
const deleteContact = async (address: string) => {
loading.value = true;
const newContacts = contacts.value.filter((item) => item.address !== address) ?? [];
await replaceContacts(newContacts);
contacts.value = newContacts;
loading.value = false;
};
const changeSelected = (address: string) => {
const contact = contacts.value.find((item) => item.address === address);
modalController.dismiss(contact, "confirm");
};
const close = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
</script>

View File

@ -12,12 +12,12 @@
>
<ion-item>
<ion-avatar
v-if="(mainNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
v-if="(allTemplateNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)"
:src="getUrl('assets/chain-icons/' + (allTemplateNets as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ selectedNetwork?.chainId }}</ion-label>
@ -33,6 +33,7 @@
<ion-label>Error From Contract:</ion-label>
<ion-textarea
style="overflow-y: scroll"
aria-label="Error"
:rows="10"
:cols="20"
:value="error"
@ -74,7 +75,7 @@ import {
import { useRoute } from "vue-router";
import { getSelectedNetwork, getUrl, hexTostr } from "@/utils/platform";
import type { Network } from "@/extension/types";
import { mainNets } from "@/utils/networks";
import { allTemplateNets } from "@/utils/networks";
export default defineComponent({
components: {
@ -111,7 +112,7 @@ export default defineComponent({
contract,
loading,
selectedNetwork,
mainNets,
allTemplateNets,
getUrl,
error,
};

View File

@ -21,7 +21,7 @@
><b style="margin-right: 0.5rem">Date:</b>
{{ new Date(item.date).toDateString() }}</ion-item
>
<ion-item button @click="copyAddress(item.txHash, getToastRef())">
<ion-item button @click="copyText(item.txHash, getToastRef())">
<p style="font-size: 0.7rem">
<b style="margin-right: 0.5rem"
><ion-icon
@ -62,6 +62,7 @@
>
</ion-loading>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
@ -87,7 +88,7 @@ import {
IonButton,
IonIcon,
} from "@ionic/vue";
import { getHistory, copyAddress, wipeHistory, openTab } from "@/utils/platform";
import { getHistory, copyText, wipeHistory, openTab } from "@/utils/platform";
import type { HistoryItem } from "@/extension/types";
import { copyOutline } from "ionicons/icons";
@ -129,7 +130,7 @@ export default defineComponent({
return {
history,
loading,
copyAddress,
copyText,
getToastRef,
toastState,
copyOutline,

View File

@ -2,7 +2,29 @@
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Wallet</ion-title>
<ion-title>
<ion-avatar
style="margin: 0.3rem; width: 1.6rem; height: 1.6rem; display: inline-flex"
>
<img alt="clw" :src="getUrl('assets/extension-icon/wallet_32.png')" />
</ion-avatar>
<span style="position: absolute; top: 0.45rem; margin-left: 0.3rem"
>CL Wallet</span
>
<span
v-if="version"
style="
position: absolute;
top: 0.3rem;
right: 1.1rem;
margin-left: 0.3rem;
color: coral;
font-weight: bold;
font-size: 0.65rem;
"
>Version: {{ version }}</span
>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
@ -13,10 +35,28 @@
<ion-list v-else>
<ion-item>
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
<ion-button @click="accountsModal = true">Select</ion-button>
<ion-button
@click="
() => {
accountsModal = true;
toastState = false;
}
"
>Select</ion-button
>
</ion-item>
<ion-item button @click="copyAddress(selectedAccount?.address, getToastRef())">
<p style="font-size: 0.7rem">{{ selectedAccount?.address }}</p>
<ion-item
button
@click="
copyText(
settings?.copyLowerCaseAddress
? selectedAccount?.address?.toLowerCase()
: selectedAccount?.address,
getToastRef()
)
"
>
<p style="font-size: 0.7rem; color: coral">{{ selectedAccount?.address }}</p>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-item>
<ion-item
@ -31,10 +71,15 @@
)
)
"
class="ion-text-wrap"
expand="block"
style="margin: auto; width: 98%; font-size: 0.8rem; padding: 0.6rem"
>View Address on
{{
`${selectedNetwork.explorer}`.replace("https://", "").replace("http://", "")
`${selectedNetwork.explorer}`
.replace("https://", "")
.replace("http://", "")
.replace(/\/.*/, "")
}}
</ion-button>
</ion-item>
@ -43,18 +88,50 @@
<ion-label>No EVM Networks found</ion-label>
<ion-button @click="goToAddNetwork">Add Network</ion-button>
</ion-item>
<ion-item v-else>
<ion-item style="font-size: 0.86rem" v-else>
<ion-avatar
v-if="(mainNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
v-if="(allTemplateNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)"
:src="getUrl('assets/chain-icons/' + (allTemplateNets as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Selected Network ID: {{ selectedNetwork?.chainId }}</ion-label>
<ion-button @click="networksModal = true">Select</ion-button>
<ion-label
button
@click="copyText(String(selectedNetwork?.chainId), getToastRef())"
style="cursor: pointer"
>Selected Network ID:
<span style="color: coral; font-weight: bold">{{
selectedNetwork?.chainId
}}</span>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-label>
<ion-button
@click="
() => {
networksModal = true;
toastState = false;
}
"
>Select</ion-button
>
</ion-item>
<ion-item style="margin-top: 0.3rem">
<div class="display: flex; flex-direction: column">
<img
alt="stealthex"
@click="openTab('https://stealthex.io')"
id="exchange-btn"
:src="getUrl('assets/exchange-btn-min.svg')"
class="exchange-btn"
style=""
/>
<p style="font-size: 0.75rem; opacity: 0.8; padding: 0.2rem">
This button does not contain any referral to maximize privacy.
</p>
</div>
</ion-item>
<ion-loading
@ -67,6 +144,7 @@
>
</ion-loading>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
@ -98,11 +176,17 @@
button
>
<ion-item>
<ion-radio slot="start" :value="account.address" />
<ion-label>{{ account.name }}</ion-label>
<ion-radio
:aria-label="account.name"
slot="start"
:value="account.address"
>{{ account.name }}</ion-radio
>
</ion-item>
<ion-item>
<ion-text style="font-size: 0.8rem">{{ account.address }}</ion-text>
<ion-text style="font-size: 0.7rem; color: coral">{{
account.address
}}</ion-text>
</ion-item>
</ion-list>
</ion-radio-group>
@ -135,11 +219,18 @@
@click="changeSelectedNetwork(network.chainId)"
slot="start"
:value="network.chainId"
/>
<ion-label>{{ network.name }}</ion-label>
:aria-label="network.name"
>
<span style="opacity: 0.7; font-size: 0.8rem">
ID: {{ network.chainId }} ->
</span>
{{ network.name }}
</ion-radio>
</ion-item>
<ion-item>
<ion-text>{{ network.rpc }}</ion-text>
<ion-text style="opacity: 0.8; font-size: 0.85rem">{{
network.rpc
}}</ion-text>
</ion-item>
</ion-list>
</ion-radio-group>
@ -180,20 +271,23 @@ import {
saveSelectedAccount,
replaceAccounts,
getSelectedNetwork,
copyAddress,
copyText,
replaceNetworks,
getUrl,
saveSelectedNetwork,
numToHexStr,
openTab,
getSettings,
getVersion,
} from "@/utils/platform";
import type { Network, Account, Networks } from "@/extension/types";
import { mainNets } from "@/utils/networks";
import { allTemplateNets } from "@/utils/networks";
import router from "@/router";
import { triggerListner } from "@/extension/listners";
import { copyOutline } from "ionicons/icons";
const version = getVersion();
export default defineComponent({
components: {
IonContent,
@ -225,6 +319,7 @@ export default defineComponent({
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
const toastState = ref(false);
const settings = ref({}) as Ref<Awaited<ReturnType<typeof getSettings>>>;
const getToastRef = () => toastState;
@ -234,15 +329,21 @@ export default defineComponent({
const pNetworks = getNetworks();
const pSelectedAccount = getSelectedAccount();
const pSelectedNetwork = getSelectedNetwork();
Promise.all([pAccounts, pNetworks, pSelectedAccount, pSelectedNetwork]).then(
(res) => {
accounts.value = res[0];
networks.value = res[1];
selectedAccount.value = res[2];
selectedNetwork.value = res[3];
loading.value = false;
}
);
const pSettings = getSettings();
Promise.all([
pAccounts,
pNetworks,
pSelectedAccount,
pSelectedNetwork,
pSettings,
]).then((res) => {
accounts.value = res[0];
networks.value = res[1];
selectedAccount.value = res[2];
selectedNetwork.value = res[3];
settings.value = res[4];
loading.value = false;
});
};
onIonViewWillEnter(() => {
@ -272,10 +373,7 @@ export default defineComponent({
accounts.value.splice(0, 0, selectedAccount.value);
const newAccounts = [...accounts.value];
await replaceAccounts(newAccounts);
triggerListner(
"accountsChanged",
newAccounts.map((a) => a.address)
);
triggerListner("accountsChanged", [newAccounts.map((a) => a.address)?.[0]]);
}
accountsModal.value = false;
loading.value = false;
@ -306,15 +404,33 @@ export default defineComponent({
selectedNetwork,
changeSelectedAccount,
changeSelectedNetwork,
copyAddress,
copyText,
copyOutline,
toastState,
getToastRef,
networksModal,
mainNets,
allTemplateNets,
getUrl,
openTab,
settings,
version,
};
},
});
</script>
<style scoped>
.exchange-btn {
height: 2rem;
margin-top: 0.3rem;
margin-left: auto;
margin-right: auto;
display: block;
cursor: pointer;
}
.exchange-btn:hover {
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
transform: scale(1.05);
}
</style>

View File

@ -4,11 +4,11 @@
<ion-toolbar>
<ion-buttons slot="end">
<router-link to="/tabs/add-network">
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-title>Networks</ion-title>
</ion-toolbar>
</ion-header>
@ -19,30 +19,34 @@
<ion-button @click="goToAddNetwork">Add Network</ion-button>
</ion-item>
<ion-list v-for="network of networks" :key="network.chainId">
<ion-item>
<ion-avatar v-if="(mainNets as any)[network.chainId]?.icon" style="margin-right: 1rem; width: 1.8rem; height:1.8rem;">
<img :alt="network.name" :src="getUrl('assets/chain-icons/' + (mainNets as any)[network.chainId].icon)" />
</ion-avatar>
<ion-label>
{{ network.name }}
</ion-label>
<ion-label>
ID: {{ network.chainId }}
</ion-label>
</ion-item>
<ion-list v-for="network of networks" :key="network.chainId">
<ion-item>
<ion-chip @click="editNetwork(network.chainId)" button>Edit</ion-chip>
<ion-chip @click="deleteNetwork(network.chainId)" button>Delete</ion-chip>
<ion-avatar
v-if="(allTemplateNets as any)[network.chainId]?.icon"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="network.name"
:src="getUrl('assets/chain-icons/' + (allTemplateNets as any)[network.chainId].icon)"
/>
</ion-avatar>
<ion-label>
{{ network.name }}
</ion-label>
<ion-label> ID: {{ network.chainId }} </ion-label>
</ion-item>
</ion-list>
<ion-item>
<ion-chip @click="editNetwork(network.chainId)" button>Edit</ion-chip>
<ion-chip @click="deleteNetwork(network.chainId)" button>Delete</ion-chip>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import { getNetworks, copyAddress, getUrl, replaceNetworks } from "@/utils/platform"
import { getNetworks, copyText, getUrl, replaceNetworks } from "@/utils/platform";
import {
IonContent,
IonHeader,
@ -57,12 +61,12 @@ import {
IonButtons,
IonButton,
onIonViewWillEnter,
IonAvatar
IonAvatar,
} from "@ionic/vue";
import { mainNets } from "@/utils/networks"
import { allTemplateNets } from "@/utils/networks";
import { addCircleOutline, copyOutline } from "ionicons/icons";
import router from '@/router/index'
import type { Networks } from '@/extension/types'
import router from "@/router/index";
import type { Networks } from "@/extension/types";
export default defineComponent({
components: {
@ -77,60 +81,57 @@ export default defineComponent({
IonLabel,
IonChip,
IonButtons,
IonButton,
IonAvatar
IonButton,
IonAvatar,
},
setup () {
const networks = ref({}) as Ref<Networks>
const loading = ref(true)
const toastState = ref(false)
setup() {
const networks = ref({}) as Ref<Networks>;
const loading = ref(true);
const toastState = ref(false);
const getToastRef = () => toastState;
const getToastRef = () => toastState
const loadData = () => {
const pAccounts = getNetworks()
Promise.all([pAccounts]).then(( res ) => {
networks.value = res[0]
loading.value = false
})
}
const pAccounts = getNetworks();
Promise.all([pAccounts]).then((res) => {
networks.value = res[0];
loading.value = false;
});
};
const deleteNetwork = async (chainId: number) => {
loading.value = true
delete networks.value[chainId]
await replaceNetworks(networks.value)
loading.value = false
}
loading.value = true;
delete networks.value[chainId];
await replaceNetworks(networks.value);
loading.value = false;
};
const editNetwork = (chainId: number) => {
router.push(`add-network/edit/${chainId}`)
}
router.push(`add-network/edit/${chainId}`);
};
const goToAddNetwork = () => {
router.push("/tabs/add-network");
};
onIonViewWillEnter(() => {
loadData()
})
loadData();
});
return {
networks,
addCircleOutline,
copyOutline,
toastState,
copyAddress,
getToastRef,
getUrl,
mainNets,
deleteNetwork,
editNetwork,
loading,
goToAddNetwork
}
}
return {
networks,
addCircleOutline,
copyOutline,
toastState,
copyText,
getToastRef,
getUrl,
allTemplateNets,
deleteNetwork,
editNetwork,
loading,
goToAddNetwork,
};
},
});
</script>

637
src/views/ReadContract.vue Normal file
View File

@ -0,0 +1,637 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Read From Contract</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button @click="selectSavedAction" expand="block"
>Load saved read action</ion-button
>
<ion-item>
<template v-if="selectedAbi">
<p>Selected Abi: {{ selectedAbi }}</p>
</template>
<template v-else>
<p>No Abi selected</p>
</template>
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
</ion-item>
<ion-item>
<ion-icon :icon="clipboardOutline" @click="paste('pasteContract')" />
<ion-input
label="Contract Address(*)"
label-placement="stacked"
v-model="contractAddress"
id="pasteContract"
placeholder="0x..."
type="text"
style="font-size: 0.8rem"
></ion-input>
</ion-item>
<ion-item button>
<ion-button @click="openModalAddContact()">
Load address from contacts
</ion-button>
</ion-item>
<ion-item button>
<template v-if="!functions.length">
<p>Select Abi with functions to enable function selection</p>
</template>
<template v-else>
<template v-if="functionName">
<p>Selected Function: {{ functionName }}</p>
<ion-button @click="selectFunction()" expand="block">Change</ion-button>
</template>
<template v-else>
<p>No Function selected</p>
<ion-button @click="selectFunction()" expand="block">Select</ion-button>
</template>
</template>
<!-- <ion-input
aria-label="function signature"
placeholder="exists(uint256)"
v-model="functionName"
></ion-input> -->
</ion-item>
<template v-if="functionName">
<ion-item>
<ion-label>PARAMS NOTES:</ion-label>
</ion-item>
<ion-list>
<ion-item
>Will be evaluated in sandbox using js eval in order to pass complex types
like [1,2 [1,0x...]]</ion-item
>
<ion-item
>Strings must be passed using qoutes example '0x3...1A2', or ['param1',
'param2'] for multiple params.</ion-item
>
<ion-item
>Params are sent exactly as they are, numbers are not parsed to UINT256
format.
</ion-item>
<ion-item>SET PARAMS: </ion-item>
<ion-list v-for="(param, index) in params" :key="index" class="param-list">
<ion-item>
<ion-label style="font-size: 0.85rem"
>P:{{ Number(index) + 1 }} name: {{ param.name }} type: ({{
param.type
}})</ion-label
>
</ion-item>
<ion-item>
<ion-input
aria-label="value"
v-model="param.value"
placeholder="ex: 1 or 0x22 or 'hello' or [1, 2] etc "
type="text"
></ion-input>
</ion-item>
</ion-list>
<ion-item v-if="!params?.length">
<ion-label>Function has no params</ion-label>
</ion-item>
</ion-list>
</template>
<ion-item>
<ion-textarea
label="Result"
label-placement="stacked"
style="overflow-y: scroll"
:rows="10"
:cols="20"
:value="result"
readonly
></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="saveActionInStorage">Save Action</ion-button>
<ion-button @click="executeAction">Execute Action</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
:header="alertHeader"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<iframe
@load="sandboxLoaded = true"
ref="evalFrame"
src="eval-sandbox.html"
style="display: none"
></iframe>
</ion-content>
<ion-modal :is-open="saveActionModal" @will-dismiss="saveActionModal = false">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="saveActionModal = false">Close</ion-button>
</ion-buttons>
<ion-title>Select</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list style="margin-bottom: 4rem">
<ion-item>
<ion-input
label="Name(*)"
label-placement="stacked"
v-model="name"
placeholder="ex: Get lens hande from id"
type="text"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="saveActionModal = false">Cancel</ion-button>
<ion-button @click="saveAction">Save</ion-button>
</ion-item>
</ion-list>
</ion-content>
</ion-modal>
</ion-page>
</template>
<script lang="ts">
import { Ref, defineComponent, ref, onMounted, onBeforeUnmount } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonIcon,
IonTextarea,
modalController,
IonList,
IonAlert,
IonModal,
IonButtons,
} from "@ionic/vue";
import { paste, readCASet, getAbis } from "@/utils/platform";
import { clipboardOutline } from "ionicons/icons";
import type { ContractAction } from "@/extension/types";
import { ethers } from "ethers";
import { getCurrentProvider } from "@/utils/wallet";
import AbiList from "./AbiList.vue";
import AbiSelectFunction from "./AbiSelectFunction.vue";
import SavedReadWriteActionList from "./SavedReadWriteActionList.vue";
import SelectedContacts from "./ContactsSelect.vue";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonIcon,
IonTextarea,
IonModal,
IonButtons,
IonList,
IonAlert,
},
setup: () => {
const savedModalState = ref(false);
const saveActionModal = ref(false);
const alertOpen = ref(false);
const alertMsg = ref("");
const name = ref("");
const contractAddress = ref("");
const functionName = ref("");
const params = ref([]) as Ref<{ value: string; type: string; name: "" }[]>;
const result = ref("");
const evalFrame = ref() as Ref<HTMLIFrameElement>;
let messagePromiseResolve: (v: unknown) => void = () => {};
const sandboxLoaded = ref(false);
const abiContent = ref("");
const selectedAbi = ref("");
const alertHeader = ref("");
let parsedAbi: any;
const functions = ref([]) as Ref<string[]>;
const openAbiListModal = async () => {
const modal = await modalController.create({
component: AbiList,
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
abiContent.value = data.content;
selectedAbi.value = data.name;
parsedAbi = JSON.parse(abiContent.value);
functions.value = parsedAbi
.filter((fn: any) => fn.type === "function")
.map((fn: any) => fn.name);
}
};
const selectFunction = async () => {
const modal = await modalController.create({
component: AbiSelectFunction,
componentProps: {
functions: functions.value,
},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
functionName.value = data;
params.value = parsedAbi
.find((fn: any) => fn.name === data)
.inputs.map((input: any) => {
return { value: "", type: input.type, name: input.name };
});
}
};
const selectSavedAction = async () => {
const modal = await modalController.create({
component: SavedReadWriteActionList,
componentProps: {
type: "read",
},
});
modal.present();
const { data, role } = (await modal.onWillDismiss()) as {
data: ContractAction;
role: string;
};
if (role === "confirm") {
const content = await getAbis(data.abi);
if (!content) {
alertMsg.value =
"Abi not found in storage, be sure Abi with name " + data.abi + " exists.";
return (alertOpen.value = true);
}
abiContent.value = content;
functionName.value = data.functionName;
params.value = Object.values(data.params);
contractAddress.value = data.contract;
selectedAbi.value = data.abi;
parsedAbi = JSON.parse(abiContent.value);
functions.value = parsedAbi
.filter((fn: any) => fn.type === "function")
.map((fn: any) => fn.name);
}
};
const saveActionInStorage = () => {
if (!functionName.value) {
alertMsg.value = "Function Name is required";
return (alertOpen.value = true);
}
if (!contractAddress.value) {
alertMsg.value = "Contract Address is required";
return (alertOpen.value = true);
}
if (abiContent.value === "") {
alertMsg.value = "Abi is required";
return (alertOpen.value = true);
}
saveActionModal.value = true;
};
const executeAction = async () => {
if (sandboxLoaded.value === false) {
alertMsg.value = "Sandbox for eval not loaded yet, please wait";
return (alertOpen.value = true);
}
if (!contractAddress.value) {
alertMsg.value = "Contract Address is required";
return (alertOpen.value = true);
}
if (!functionName.value) {
alertMsg.value = "Function Name is required";
return (alertOpen.value = true);
}
if (!parsedAbi) {
alertMsg.value = "Abi is required";
return (alertOpen.value = true);
}
alertHeader.value = "Error";
const provider = await getCurrentProvider();
const encodeParamsTypes = [];
let evalParams: any[] = [];
try {
evalParams = await Promise.all(
params.value.map(async (param) => await getEvalValue(param.value))
);
} catch {
alertMsg.value = "Error parsing params, check params types";
return (alertOpen.value = true);
}
try {
if (functionName.value?.includes("(")) {
const paramsTypes = functionName.value
.split("(")[1]
.split(")")[0]
.split(",")
.map((param) => param.trim());
if (paramsTypes.length !== evalParams.length) {
alertMsg.value = "Params count mismatch";
return (alertOpen.value = true);
}
encodeParamsTypes.push(...paramsTypes);
}
} catch {
alertMsg.value =
"Function Siganture wrong format (ex: 'functionName(uint256,string)')";
return (alertOpen.value = true);
}
const fnName = functionName.value.includes("(")
? functionName.value.split("(")[0]
: functionName.value;
const contract = new ethers.Contract(contractAddress.value, parsedAbi, provider);
try {
const res = await contract[fnName](...evalParams);
result.value = res.toString();
alertMsg.value = "Value from contract fetched check result area!";
alertHeader.value = "OK";
return (alertOpen.value = true);
} catch (e) {
alertMsg.value = "Function call failed, check params, contract address and ABI";
return (alertOpen.value = true);
}
};
const saveAction = async () => {
if (!name.value) {
alertMsg.value = "Name is required";
return (alertOpen.value = true);
}
const action = {
name: name.value,
contract: contractAddress.value,
functionName: functionName.value,
params: params.value,
abi: selectedAbi.value,
};
await readCASet(action);
saveActionModal.value = false;
alertMsg.value = "Action saved successfully";
alertHeader.value = "OK";
return (alertOpen.value = true);
};
const messageHandler = (event: any) => {
messagePromiseResolve(event?.data?.result);
};
onMounted(() => {
window.addEventListener("message", messageHandler);
});
onBeforeUnmount(() => {
window.removeEventListener("message", messageHandler);
});
const getEvalValue = (evalString: string) => {
return new Promise((resolve) => {
if (!evalFrame.value?.contentWindow?.postMessage) {
return;
}
messagePromiseResolve = resolve;
evalFrame.value?.contentWindow?.postMessage({ code: evalString }, "*");
});
};
// const rpc = ref("");
// const symbol = ref("");
// const explorer = ref("");
// const templateModal = ref(false);
// const currentSegment = ref("mainnets");
// const route = useRoute();
// const isEdit = route.path.includes("/edit");
// const paramChainId = route.params.chainId ?? "";
// let networksProm: Promise<Networks | undefined>;
// const fillNetworkInputs = (network: Network) => {
// name.value = network.name;
// chainId.value = network.chainId;
// rpc.value = network.rpc;
// symbol.value = network.symbol ?? "";
// explorer.value = network.explorer ?? "";
// };
// onIonViewWillEnter(async () => {
// if (isEdit && paramChainId) {
// networksProm = getNetworks();
// const networks = (await networksProm) as Networks;
// fillNetworkInputs(networks[Number(paramChainId)]);
// }
// });
// const resetFields = () => {
// name.value = "";
// chainId.value = 0;
// rpc.value = "";
// };
// const onAddNetwork = async () => {
// if (Number(chainId.value) < 1) {
// alertMsg.value = "Chain Id must be a valid decimal integer";
// return (alertOpen.value = true);
// }
// if (name.value.length < 2) {
// alertMsg.value = "Name must have at least 2 characters";
// return (alertOpen.value = true);
// }
// if (name.value.length > 99) {
// alertMsg.value = "Name must be less than 100 characters";
// return (alertOpen.value = true);
// }
// if (name.value.length > 99) {
// try {
// new URL(rpc.value);
// } catch {
// alertMsg.value = "RPC must be a valid URL";
// return (alertOpen.value = true);
// }
// }
// let p1 = Promise.resolve();
// if (!networksProm) {
// networksProm = getNetworks();
// }
// const networks = (await networksProm) as Networks;
// const network = {
// name: name.value,
// chainId: chainId.value,
// rpc: rpc.value,
// ...(symbol.value ? { symbol: symbol.value } : {}),
// ...(explorer.value ? { explorer: explorer.value } : {}),
// };
// if ((Object.keys(networks).length ?? 0) < 1) {
// p1 = saveSelectedNetwork(network);
// } else {
// if (chainId.value in networks && !isEdit) {
// alertMsg.value = "Network already exists.";
// return (alertOpen.value = true);
// }
// }
// networks[chainId.value] = network;
// const p2 = replaceNetworks(networks);
// await Promise.all([p1, p2]);
// if (isEdit) {
// router.push("/tabs/networks");
// } else {
// router.push("/tabs/home");
// }
// resetFields();
// };
// const segmentChange = (value: any) => {
// currentSegment.value = value.detail.value;
// };
// const onCancel = () => {
// if (isEdit) {
// router.push("/tabs/networks");
// } else {
// router.push("/tabs/home");
// }
// };
// const fillTemplate = (network: typeof mainNets[1]) => {
// fillNetworkInputs(network);
// modalController?.dismiss(null, "cancel");
// };
// document.addEventListener('DOMContentLoaded', () => {
// document.getElementById('reset').addEventListener('click', function () {
// counter = 0;
// document.querySelector('#result').innerHTML = '';
// });
// document.getElementById('sendMessage').addEventListener('click', function () {
// counter++;
// let message = {
// command: 'render',
// templateName: 'sample-template-' + counter,
// context: { counter: counter }
// };
// document.getElementById('theFrame').contentWindow.postMessage(message, '*');
// });
// // on result from sandboxed frame:
// window.addEventListener('message', function () {
// document.querySelector('#result').innerHTML =
// event.data.result || 'invalid result';
// });
// });
const openModalAddContact = async () => {
const modal = await modalController.create({
component: SelectedContacts,
componentProps: {},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
contractAddress.value = data.address;
}
};
const handleFnChange = (event: any) => {
functionName.value = event.detail.value;
};
return {
saveActionModal,
handleFnChange,
clipboardOutline,
evalFrame,
alertOpen,
alertMsg,
alertHeader,
functionName,
paste,
savedModalState,
name,
contractAddress,
params,
// addParam,
// removeParam,
saveActionInStorage,
executeAction,
result,
sandboxLoaded,
openAbiListModal,
selectedAbi,
functions,
selectFunction,
saveAction,
selectSavedAction,
openModalAddContact,
// onAddNetwork,
// rpc,
// onCancel,
// templateModal,
// currentSegment,
// mainNets,
// testNets,
// segmentChange,
// getUrl,
// fillTemplate,
// symbol,
// explorer,
// isEdit,
};
},
});
</script>
<style>
.param-list {
--border-width: 1px; /* Set your desired border width */
}
.param-list ion-item {
border-top: var(--border-width) solid #0ece6e;
}
.param-list ion-item:last-child {
border-bottom: var(--border-width) solid #0ece6e;
}
</style>

View File

@ -22,6 +22,7 @@
<ion-item>
<ion-label>Name:</ion-label>
<ion-input
aria-label="Name"
style="margin-left: 0.5rem"
v-model="name"
readonly
@ -31,6 +32,7 @@
<ion-item>
<ion-label>ChainId: </ion-label>
<ion-input
aria-label="ChainId"
style="margin-left: 0.5rem"
v-model="chainId"
readonly
@ -40,6 +42,7 @@
<ion-item button>
<ion-label>RPC URL: </ion-label>
<ion-input
aria-label="RPC URL"
style="margin-left: 0.5rem"
readonly
placeholder="https://polygon-mainnet.g.alchemy.com/..."
@ -49,6 +52,7 @@
<ion-item button>
<ion-label>Native Token Symbol: </ion-label>
<ion-input
aria-label="Native Token Symbol"
style="margin-left: 0.5rem"
readonly
placeholder="MATIC"
@ -58,6 +62,7 @@
<ion-item button>
<ion-label>Explorer: </ion-label>
<ion-input
aria-label="Explorer"
style="margin-left: 0.5rem"
readonly
placeholder="https://polygonscan.com"
@ -107,7 +112,6 @@ import {
import { useRoute } from "vue-router";
import { getUrl, saveSelectedNetwork, saveNetwork, hexTostr } from "@/utils/platform";
import type { Network } from "@/extension/types";
import { mainNets, testNets } from "@/utils/networks";
import { approve, walletPing } from "@/extension/userRequest";
import { triggerListner } from "@/extension/listners";
@ -132,7 +136,6 @@ export default defineComponent({
const rid = (route?.params?.rid as string) ?? "";
const networkData = hexTostr((route.params?.param as string) ?? "");
const alertOpen = ref(false);
const templateNetworks = Object.assign({}, mainNets, testNets) ?? {};
const timerReject = ref(140);
let interval: any;
const website = ref("");
@ -203,7 +206,6 @@ export default defineComponent({
onCancel,
alertOpen,
loading,
templateNetworks,
getUrl,
onAddSwitch,
timerReject,

View File

@ -0,0 +1,139 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Select Action</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-searchbar placeholder="Search" @ionInput="onSearch"></ion-searchbar>
</ion-item>
<ion-radio-group :value="selectedAbi">
<ion-list-header>
<ion-label>Actions</ion-label>
</ion-list-header>
<ion-list class="ion-padding" v-for="key in Object.keys(actions)" :key="key">
<ion-item>
<ion-radio
@click="changeSelected(key)"
slot="start"
:value="key"
:aria-label="key"
>
{{ key }} on ABI {{ actions[key].abi }}
</ion-radio>
</ion-item>
<ion-item>
<ion-button @click="onDelete(key)">Delete</ion-button>
</ion-item>
</ion-list>
</ion-radio-group>
<ion-list v-if="!!!Object.keys(actions ?? {}).length">
<ion-item class="ion-padding">
<ion-label>No Actions found, please save at least one</ion-label>
</ion-item>
</ion-list>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts" setup>
import {
IonContent,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonItem,
modalController,
IonRadio,
IonListHeader,
IonRadioGroup,
IonLabel,
IonLoading,
IonSearchbar,
IonButtons,
IonButton,
} from "@ionic/vue";
import { ref, onMounted, Ref } from "vue";
import {
readCAGetAll,
readCARemove,
writeCAGetAll,
writeCARemove,
} from "@/utils/platform";
import { ContractActions } from "@/extension/types";
// import {
// getAllAbis,
// setAbis,
// // removeAllAbis
// } from "@/utils/platform";
const props = defineProps(["type"]);
const type = props?.type ?? "read";
const actions = ref({}) as Ref<ContractActions>;
const intialActions = ref({}) as Ref<ContractActions>;
const loading = ref(false);
const selectedAbi = ref("");
const onSearch = (e: any) => {
const text = e.target.value;
if (text) {
const keys = Object.keys(intialActions.value).filter((key) =>
key.toLowerCase().includes(text.toLowerCase())
);
actions.value = keys.reduce((acc, key) => {
acc[key] = intialActions.value[key];
return acc;
}, {} as ContractActions);
} else {
actions.value = { ...intialActions.value };
}
};
onMounted(async () => {
loading.value = true;
actions.value = type === "read" ? await readCAGetAll() : await writeCAGetAll();
intialActions.value = { ...actions.value };
loading.value = false;
});
const onDelete = async (key: string) => {
delete actions.value[key];
type === "read" ? await readCARemove(key) : await writeCARemove(key);
intialActions.value = { ...actions.value };
};
const changeSelected = (item: string) => {
modalController.dismiss(actions.value?.[item], "confirm");
};
const close = () => {
try {
modalController.dismiss(null, "cancel");
} catch {
// ignore
}
};
</script>

308
src/views/SendToken.vue Normal file
View File

@ -0,0 +1,308 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Send Native Token</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>Current Network</ion-label>
</ion-item>
<template v-if="selectedNetwork?.name">
<ion-item>
Name: <b>{{ selectedNetwork.name }}</b>
</ion-item>
<ion-item>
ID: <b>{{ selectedNetwork.chainId }}</b>
</ion-item>
</template>
<hr />
<ion-item>
<ion-label>Current Address</ion-label>
</ion-item>
<ion-item v-if="selectedAccount?.address">
<b style="font-size: 0.8rem">{{ selectedAccount?.address }}</b>
</ion-item>
<hr />
<ion-item>
<ion-label>Current Balance</ion-label>
</ion-item>
<ion-item v-if="currentBalance">
<b>{{ currentBalance.toFixed(8) }}</b>
</ion-item>
<hr />
<ion-item>
<ion-label>Send To Address:</ion-label>
</ion-item>
<ion-item>
<ion-input
aria-label="address"
style="font-size: 0.8rem"
id="pasteAddress"
v-model="sendTo"
></ion-input>
<ion-icon
style="margin-right: 0.5rem"
@click="paste('pasteAddress')"
:icon="clipboardOutline"
button
/>
</ion-item>
<ion-item button>
<ion-button @click="openModalAddContact()">
Load address from contacts
</ion-button>
</ion-item>
<ion-item>
<ion-label>Amount (e.g. 1.2):</ion-label>
</ion-item>
<ion-item>
<ion-input
aria-label="Amount (e.g. 1.2)"
type="number"
id="amount"
v-model="amount"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="promptTransaction">Prompt Transaction</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
:header="alertTitle"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="loadingSend ? 0 : 4000"
:key="`k${loading}`"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
onIonViewWillEnter,
IonLoading,
modalController,
// IonModal,
// IonButtons,
// IonTextarea,
} from "@ionic/vue";
// import { ethers } from "ethers";
import {
// saveSelectedAccount,
// getAccounts,
// saveAccount,
// getRandomPk,
// smallRandomString,
paste,
getSelectedNetwork,
getSelectedAccount,
// getSettings,
} from "@/utils/platform";
// import router from "@/router";
// import UnlockModal from "@/views/UnlockModal.vue";
// import { encrypt, getCryptoParams } from "@/utils/webCrypto";
import { clipboardOutline } from "ionicons/icons";
import type { Network, Account } from "@/extension/types";
import { walletPromptSendTx } from "@/extension/userRequest";
import { isAddress, formatEther, parseEther } from "ethers";
import { getTxCount, getBalance } from "@/utils/wallet";
import SelectedContacts from "./ContactsSelect.vue";
// import { getFromMnemonic } from "@/utils/wallet";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonIcon,
IonLoading,
// IonModal,
// IonButtons,
// IonTextarea,
},
setup: () => {
// const supportedNetworksIds = [1, 3, 4, 5, 42, 56, 97, 137, 80001];
const name = ref("");
const sendTo = ref("");
const alertOpen = ref(false);
const alertMsg = ref("");
const alertTitle = ref("Error");
const loading = ref(true);
const amount = ref(0);
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const currentBalance = ref(0);
const loadingSend = ref(false);
// let accountsProm: Promise<Account[] | undefined>;
// let settingsProm: Promise<Settings | undefined>;
// const resetFields = () => {
// name.value = "";
// pk.value = "";
// };
// const openModal = async () => {
// const modal = await modalController.create({
// component: UnlockModal,
// componentProps: {
// unlockType: "addAccount",
// },
// });
// modal.present();
// const { role, data } = await modal.onWillDismiss();
// if (role === "confirm") return data;
// return false;
// };
onIonViewWillEnter(async () => {
try {
selectedNetwork.value = await getSelectedNetwork();
selectedAccount.value = await getSelectedAccount();
currentBalance.value = Number(formatEther((await getBalance()).toString()));
} catch (e) {
alertOpen.value = true;
alertMsg.value =
"Error getting network & balance Internet or RPC or blockchain may be down";
}
loading.value = false;
});
const promptTransaction = async () => {
alertTitle.value = "Error";
if (
sendTo.value?.toLocaleLowerCase() ===
selectedAccount.value.address?.toLocaleLowerCase()
) {
alertOpen.value = true;
alertMsg.value = "Cannot send to self";
return;
}
if (!isAddress(sendTo.value)) {
alertOpen.value = true;
alertMsg.value = "Invalid send address";
return;
}
if (amount.value <= 0) {
alertOpen.value = true;
alertMsg.value = "Amount must be greater than 0";
return;
}
const value = parseEther(amount.value.toString()).toString();
if (Number(value) >= Number(parseEther(currentBalance.value.toString()))) {
alertOpen.value = true;
alertMsg.value = "Insufficient balance";
return;
}
const nonce = (await getTxCount(selectedAccount.value.address)) + 1;
loading.value = true;
loadingSend.value = true;
const tx = {
from: selectedAccount.value.address,
to: sendTo.value,
value,
nonce,
gasLimit: "0x0",
gasPrice: "0x0",
};
const result = (await walletPromptSendTx(tx)) as {
error?: string;
};
if (result?.error) {
alertOpen.value = true;
alertMsg.value = "Error sending transaction to chain";
loading.value = false;
return;
} else {
alertTitle.value = "OK";
alertOpen.value = true;
alertMsg.value = "Transaction sent successfully";
}
loadingSend.value = false;
loading.value = false;
};
const openModalAddContact = async () => {
const modal = await modalController.create({
component: SelectedContacts,
componentProps: {},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
sendTo.value = data.address;
}
};
return {
name,
sendTo,
alertOpen,
alertMsg,
alertTitle,
clipboardOutline,
loadingSend,
paste,
loading,
amount,
promptTransaction,
currentBalance,
selectedAccount,
selectedNetwork,
openModalAddContact,
};
},
});
</script>

View File

@ -20,6 +20,7 @@
<ion-item>
<ion-label>Enable Storage Encryption</ion-label>
<ion-toggle
aria-label="Enable Storage Encryption"
:key="updateKey"
@ion-change="changeEncryption"
slot="end"
@ -33,6 +34,7 @@
<ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label>
<ion-toggle
aria-label="Enable Auto Lock"
:key="updateKey"
@ion-change="changeAutoLock"
slot="end"
@ -70,6 +72,7 @@
<ion-item>
<ion-label>Permanent Lock</ion-label>
<ion-toggle
aria-label="Permanent Lock"
@ion-change="changePermaLock"
:key="updateKey"
slot="end"
@ -86,7 +89,7 @@
</ion-accordion>
<ion-accordion value="2">
<ion-item slot="header" color="light">
<ion-label>Theme</ion-label>
<ion-label>Theme & Misc</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
@ -104,6 +107,18 @@
<ion-label>Light</ion-label>
</ion-item>
</ion-radio-group>
<ion-item>
<ion-label style="font-size: 0.7rem"
>Convert Address to lowercase on copy</ion-label
>
<ion-toggle
aria-label="Convert Address to Lowercase on Copy"
@ion-change="changeCopyLowerCaseAddress"
:key="updateKey"
slot="end"
:checked="settings.s.copyLowerCaseAddress"
></ion-toggle>
</ion-item>
</ion-list>
</div>
</ion-accordion>
@ -117,13 +132,12 @@
and Ethers.
</p>
<p>
It emulates Metamask Wallet and can be used as a drop-in replacement, right
now if you have both extensions, CLW will overwrite Metamask.
</p>
<p>
Main philosophy of the wallet is: no trackers, full control, export/import
JSONs with accounts, fast generate new accounts, and wipe everything with
one click.
Unlike most wallets, this wallet has no ads, no analytics, no trackers, no
bloatware, no telemetry, no data collection, no sponsored content, no
sponsored Dapps, no sponsored tokens, no sponsored NFTs, no sponsored
anything. It is a clean wallet with no revenue model, made by a single
developer, if you want to support this project financially you can donate at
andrei0x309.eth.
</p>
<p>
Github Repo:
@ -131,6 +145,10 @@
>LINK</a
>
</p>
<p>
Docs Website:
<a href="#" @click="openTab('https://clear-wallet.flashsoft.eu')">LINK</a>
</p>
<br />
<p style="margin-bottom: 0.2rem">Places you can check me out:</p>
<p>
@ -145,10 +163,6 @@
Blog Flashsoft
<a href="#" @click="openTab('https://blog.flashsoft.eu')">LINK</a>
</p>
<p>
Crypto-Leftists Discord
<a href="#" @click="openTab('https://discord.gg/gzA4bTCdhb')">LINK</a>
</p>
</div>
</ion-accordion>
<ion-accordion value="4">
@ -180,6 +194,7 @@
</ion-accordion>
</ion-accordion-group>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
:message="toastMsg"
@ -228,7 +243,11 @@
<ion-label>Old Password</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
<ion-input
aria-label="password"
v-model="mpPass"
type="password"
></ion-input>
</ion-item>
</ion-list>
<div v-else>
@ -237,7 +256,11 @@
<ion-label>New Password</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
<ion-input
aria-label="password"
v-model="mpPass"
type="password"
></ion-input>
</ion-item>
</ion-list>
<ion-list>
@ -245,7 +268,11 @@
<ion-label>Confirm</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpConfirm" type="password"></ion-input>
<ion-input
aria-label="password"
v-model="mpConfirm"
type="password"
></ion-input>
</ion-item>
</ion-list>
</div>
@ -393,6 +420,12 @@ export default defineComponent({
defaultAccordionOpen.value = "1";
};
const changeCopyLowerCaseAddress = async () => {
settings.s.copyLowerCaseAddress = !settings.s?.copyLowerCaseAddress;
await saveSettings();
defaultAccordionOpen.value = "2";
};
const changeTheme = async (theme: "system" | "light" | "dark") => {
document.body.classList.remove(radioTheme.value);
document.body.classList.add(theme);
@ -682,6 +715,7 @@ export default defineComponent({
openTab,
radioTheme,
changePermaLock,
changeCopyLowerCaseAddress,
};
},
});

View File

@ -12,12 +12,12 @@
>
<ion-item>
<ion-avatar
v-if="(mainNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
v-if="(allTemplateNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)"
:src="getUrl('assets/chain-icons/' + (allTemplateNets as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ selectedNetwork?.chainId }}</ion-label>
@ -66,6 +66,7 @@
<ion-item>
<ion-label>Raw TX:</ion-label>
<ion-textarea
aria-label="raw tx"
style="overflow-y: scroll"
:rows="10"
:cols="20"
@ -119,7 +120,11 @@
<ion-label>Limit in units</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="inGasLimit" type="number"></ion-input>
<ion-input
aria-label="gas limit"
v-model="inGasLimit"
type="number"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="setGasLimit">Set Price</ion-button>
@ -143,7 +148,11 @@
<ion-label>Price in gwei</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="inGasPrice" type="number"></ion-input>
<ion-input
aria-label="price in gwei"
v-model="inGasPrice"
type="number"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="setGasPrice">Set Price</ion-button>
@ -192,7 +201,7 @@ import {
} from "@/utils/platform";
import { getBalance, getGasPrice, estimateGas } from "@/utils/wallet";
import type { Network } from "@/extension/types";
import { mainNets, chainIdToPriceId } from "@/utils/networks";
import { allTemplateNets, chainIdToPriceId } from "@/utils/networks";
import UnlockModal from "@/views/UnlockModal.vue";
import router from "@/router";
@ -252,7 +261,12 @@ export default defineComponent({
if (!decodedParam) {
isError = true;
} else {
signTxData.value = JSON.stringify(params, null, 2);
const paramsWithoutZeros = Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(params).filter(([_, v]) => v !== "0x0")
);
signTxData.value = JSON.stringify(paramsWithoutZeros, null, 2);
}
const openModal = async () => {
@ -300,16 +314,22 @@ export default defineComponent({
}
};
const newGasData = () => {
const newGasData = async () => {
await walletSendData(rid, {
gas: numToHexStr(gasLimit.value),
});
await walletSendData(rid, {
gasPrice: numToHexStr(BigInt(Math.trunc(gasPrice.value * 1e9))),
});
gasFee.value = Number(
ethers.utils.formatUnits(String(gasLimit.value * gasPrice.value), "gwei")
ethers.formatUnits(Math.trunc(gasLimit.value * gasPrice.value), "gwei")
);
txValue.value = Number(ethers.utils.formatEther(params?.value ?? "0x0"));
txValue.value = Number(ethers.formatEther(params?.value ?? "0x0"));
totalCost.value = gasFee.value + txValue.value;
};
onIonViewWillEnter(async () => {
console.log(params.value);
(window as any)?.resizeTo?.(600, 800);
const pEstimateGas = estimateGas({
to: params?.to ?? "",
@ -323,13 +343,11 @@ export default defineComponent({
const pGetPrices = getPrices();
selectedNetwork.value = await getSelectedNetwork();
userBalance.value = Number(
ethers.utils.formatEther((await pBalance).toString() ?? "0x0")
ethers.formatEther((await pBalance).toString() ?? "0x0")
);
gasPrice.value = parseInt(
ethers.utils.formatUnits((await pGasPrice).toString() ?? "0x0", "gwei"),
10
);
gasPrice.value = parseFloat((await pGasPrice).toString() ?? 0.1);
try {
gasLimit.value = parseInt((await pEstimateGas).toString(), 10);
} catch (err) {
@ -341,16 +359,13 @@ export default defineComponent({
inGasPrice.value = gasPrice.value;
inGasLimit.value = gasLimit.value;
// console.log( 'test', ethers.utils.formatUnits((await pGasPrice).toString(), "gwei"), ethers.utils.formatUnits(ethers.utils.parseUnits(gasPrice.value.toString(), "gwei"), "gwei") )
newGasData();
if (userBalance.value < totalCost.value) {
insuficientBalance.value = true;
}
const prices = await pGetPrices;
dollarPrice.value =
prices[chainIdToPriceId(selectedNetwork.value?.chainId ?? 0)]?.usd ?? 0;
await newGasData();
loading.value = false;
interval = setInterval(async () => {
@ -363,11 +378,8 @@ export default defineComponent({
if (timerFee.value <= 0) {
timerFee.value = 20;
loading.value = true;
gasPrice.value = parseInt(
ethers.utils.formatUnits((await getGasPrice()).toString(), "gwei"),
10
);
newGasData();
gasPrice.value = parseFloat((await getGasPrice()).toString() ?? 0.1);
await newGasData();
loading.value = false;
}
}
@ -380,9 +392,6 @@ export default defineComponent({
const setGasLimit = () => {
gasLimit.value = inGasLimit.value;
walletSendData(rid, {
gas: numToHexStr(gasLimit.value),
});
newGasData();
gasLimitModal.value = false;
};
@ -390,9 +399,6 @@ export default defineComponent({
const setGasPrice = () => {
gasPrice.value = inGasPrice.value;
gasPriceReFetch.value = false;
walletSendData(rid, {
gasPrice: ethers.utils.parseUnits(gasPrice.value.toString(), "gwei"),
});
newGasData();
gasPriceModal.value = false;
};
@ -418,7 +424,7 @@ export default defineComponent({
bars,
loading,
selectedNetwork,
mainNets,
allTemplateNets,
getUrl,
setGasLimit,
setGasPrice,

View File

@ -20,12 +20,12 @@
<ion-item>Network Name: {{ selectedNetwork?.name }}</ion-item>
<ion-item>
<ion-avatar
v-if="(templateNetworks as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
v-if="(allTemplateNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (templateNetworks as any)[selectedNetwork?.chainId]?.icon)"
:src="getUrl('assets/chain-icons/' + (allTemplateNets as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ selectedNetwork?.chainId }}</ion-label>
@ -42,7 +42,7 @@
<ion-item>
<ion-avatar
v-if="(existingNetworks as any)[networkId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
style="margin-right: 1rem; width: 1.6rem; height: 1.6rem"
>
<img
:alt="(existingNetworks as any)[networkId]?.name"
@ -132,7 +132,7 @@ import {
numToHexStr,
} from "@/utils/platform";
import type { Network, Networks } from "@/extension/types";
import { mainNets, testNets } from "@/utils/networks";
import { allTemplateNets } from "@/utils/networks";
import { approve, walletPing } from "@/extension/userRequest";
import { triggerListner } from "@/extension/listners";
@ -162,7 +162,6 @@ export default defineComponent({
const alertMsg = ref("");
const networkCase = ref("");
let pnetworks: Promise<Networks>;
const templateNetworks = Object.assign({}, mainNets, testNets) ?? {};
const addChainUrl = `${chainListPage}${networkId.value}`;
const timerReject = ref(140);
let interval: any;
@ -183,12 +182,11 @@ export default defineComponent({
(window as any)?.resizeTo?.(600, 600);
pnetworks = getNetworks();
selectedNetwork.value = await getSelectedNetwork();
console.log(networkId.value);
existingNetworks.value = await pnetworks;
if ((networkId.value ?? "0") in existingNetworks.value ?? {}) {
if ((networkId.value ?? "0") in (existingNetworks?.value ?? {})) {
networkCase.value = "exists";
} else if ((networkId.value ?? "0") in templateNetworks) {
existingNetworks.value = templateNetworks;
} else if ((networkId.value ?? "0") in allTemplateNets) {
existingNetworks.value = allTemplateNets;
networkCase.value = "inTemplates";
} else {
networkCase.value = "doesNotExist";
@ -218,9 +216,10 @@ export default defineComponent({
const onSwitchTemplates = async () => {
loading.value = true;
selectedNetwork.value = templateNetworks[Number(networkId.value)];
await saveNetwork(templateNetworks[Number(networkId.value)]);
await saveSelectedNetwork(templateNetworks[Number(networkId.value)]);
const nId = Number(networkId.value) as keyof typeof allTemplateNets;
selectedNetwork.value = allTemplateNets[nId];
await saveNetwork(allTemplateNets[nId]);
await saveSelectedNetwork(allTemplateNets[nId]);
triggerListner("chainChanged", numToHexStr(selectedNetwork.value?.chainId ?? 0));
approve(rid);
loading.value = false;
@ -241,7 +240,7 @@ export default defineComponent({
loading,
networkCase,
selectedNetwork,
templateNetworks,
allTemplateNets,
getUrl,
onSwitchTemplates,
onSwitchNotExisting,

View File

View File

@ -2,10 +2,10 @@
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Unlock to Proceed</ion-title>
<ion-buttons slot="end">
<ion-button @click="close" color="primary">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
@ -30,9 +30,29 @@
<ion-item>
<ion-label>Unlock Password</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
<ion-list>
<ion-item>
<ion-input
aria-label="password"
placeholder=""
class="password-input"
type="password"
@ion-input="mpPass = String($event.target.value)"
fill="solid"
ref="ionInput"
></ion-input>
<!-- <ion-input
label="Password"
label-placement="floating"
fill="outline"
placeholder=""
type="password"
ref="ionInput"
@ion-input="mpPass = String($event.target.value)"
></ion-input> -->
</ion-item>
</ion-list>
</ion-list>
<ion-item>
<ion-button @click="unlock">Confirm</ion-button>
@ -57,7 +77,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { defineComponent, ref, onMounted } from "vue";
import {
IonContent,
IonHeader,
@ -133,6 +153,14 @@ export default defineComponent({
}
};
onMounted(async () => {
await new Promise((resolve) => setTimeout(resolve, 150));
const el = document?.querySelector(
".password-input .native-input"
) as HTMLInputElement;
el?.focus?.();
});
return {
loading,
unlock,

View File

@ -11,8 +11,9 @@
<ion-label>Operation Aborted</ion-label>
</ion-item>
<ion-item>
<ion-label>Error:</ion-label>
<ion-textarea
label="Error:"
labelPlacement="stacked"
style="overflow-y: scroll"
:rows="10"
:cols="20"

494
src/views/WriteContract.vue Normal file
View File

@ -0,0 +1,494 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Contract Write Action</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button @click="selectSavedAction" expand="block"
>Load saved wite action</ion-button
>
<ion-item>
<template v-if="selectedAbi">
<p>Selected Abi: {{ selectedAbi }}</p>
</template>
<template v-else>
<p>No Abi selected</p>
</template>
<ion-button @click="openAbiListModal()" expand="block">Load Abi</ion-button>
</ion-item>
<ion-item>
<ion-icon :icon="clipboardOutline" @click="paste('pasteContract')" />
<ion-input
label="Contract Address(*)"
label-placement="stacked"
v-model="contractAddress"
id="pasteContract"
placeholder="0x..."
type="text"
style="font-size: 0.8rem"
></ion-input>
</ion-item>
<ion-item button>
<ion-button @click="openModalAddContact()">
Load address from contacts
</ion-button>
</ion-item>
<ion-item button>
<template v-if="!functions.length">
<p>Select Abi with functions to enable function selection</p>
</template>
<template v-else>
<template v-if="functionName">
<p>Selected Function: {{ functionName }}</p>
<ion-button @click="selectFunction()" expand="block">Change</ion-button>
</template>
<template v-else>
<p>No Function selected</p>
<ion-button @click="selectFunction()" expand="block">Select</ion-button>
</template>
</template>
</ion-item>
<template v-if="functionName">
<ion-item>
<ion-label>PARAMS NOTES:</ion-label>
</ion-item>
<ion-list>
<ion-item
>Will be evaluated in sandbox using js eval in order to pass complex types
like [1,2 [1,0x...]]</ion-item
>
<ion-item
>Strings must be passed using qoutes example '0x3...1A2', or ['param1',
'param2'] for multiple params.</ion-item
>
<ion-item
>Params are sent exactly as they are, numbers are not parsed to UINT256
format.
</ion-item>
<ion-item>SET PARAMS: </ion-item>
<ion-list v-for="(param, index) in params" :key="index" class="param-list">
<ion-item>
<ion-label style="font-size: 0.85rem"
>P:{{ Number(index) + 1 }} name: {{ param.name }} type: ({{
param.type
}})</ion-label
>
</ion-item>
<ion-item>
<ion-input
aria-label="value"
v-model="param.value"
placeholder="ex: 1 or 0x22 or 'hello' or [1, 2] etc "
type="text"
></ion-input>
</ion-item>
</ion-list>
<ion-item v-if="!params?.length">
<ion-label>Function has no params</ion-label>
</ion-item>
</ion-list>
</template>
<ion-item>
<ion-button @click="saveActionInStorage">Save Action</ion-button>
<ion-button @click="executeAction">Execute Action</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
:header="alertHeader"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="loadingSend ? 0 : 4000"
:key="`k${loading}`"
@didDismiss="loading = false"
/>
<iframe
@load="sandboxLoaded = true"
ref="evalFrame"
src="eval-sandbox.html"
style="display: none"
></iframe>
</ion-content>
<ion-modal :is-open="saveActionModal" @will-dismiss="saveActionModal = false">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="saveActionModal = false">Close</ion-button>
</ion-buttons>
<ion-title>Select</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list style="margin-bottom: 4rem">
<ion-item>
<ion-input
label="Name(*)"
label-placement="stacked"
v-model="name"
placeholder="ex: Get lens hande from id"
type="text"
></ion-input>
</ion-item>
<ion-item>
<ion-button @click="saveActionModal = false">Cancel</ion-button>
<ion-button @click="saveAction">Save</ion-button>
</ion-item>
</ion-list>
</ion-content>
</ion-modal>
</ion-page>
</template>
<script lang="ts">
import { Ref, defineComponent, ref, onMounted, onBeforeUnmount } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonIcon,
modalController,
IonList,
IonAlert,
IonLoading,
IonButtons,
IonModal,
} from "@ionic/vue";
import { paste, writeCASet, getAbis } from "@/utils/platform";
import { clipboardOutline } from "ionicons/icons";
import type { ContractAction } from "@/extension/types";
import { ethers } from "ethers";
import { getSelectedAddress } from "@/utils/wallet";
import AbiList from "./AbiList.vue";
import AbiSelectFunction from "./AbiSelectFunction.vue";
import SavedReadWriteActionList from "./SavedReadWriteActionList.vue";
import { walletPromptSendTx } from "@/extension/userRequest";
import SelectedContacts from "./ContactsSelect.vue";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonIcon,
IonList,
IonAlert,
IonLoading,
IonButtons,
IonModal,
},
setup: () => {
const savedModalState = ref(false);
const saveActionModal = ref(false);
const alertOpen = ref(false);
const alertMsg = ref("");
const name = ref("");
const loadingSend = ref(false);
const contractAddress = ref("");
const functionName = ref("");
const params = ref([]) as Ref<{ value: string; type: string; name: string }[]>;
const evalFrame = ref() as Ref<HTMLIFrameElement>;
let messagePromiseResolve: (v: unknown) => void = () => {};
const sandboxLoaded = ref(false);
const abiContent = ref("");
const selectedAbi = ref("");
const alertHeader = ref("");
let parsedAbi: any;
const functions = ref([]) as Ref<string[]>;
const loading = ref(false);
const openAbiListModal = async () => {
const modal = await modalController.create({
component: AbiList,
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
abiContent.value = data.content;
selectedAbi.value = data.name;
parsedAbi = JSON.parse(abiContent.value);
functions.value = parsedAbi
.filter((fn: any) => fn.type === "function")
.map((fn: any) => fn.name);
}
};
const selectFunction = async () => {
const modal = await modalController.create({
component: AbiSelectFunction,
componentProps: {
functions: functions.value,
},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
functionName.value = data;
params.value = parsedAbi
.find((fn: any) => fn.name === data)
.inputs.map((input: any) => {
return { value: "", type: input.type, name: input.name };
});
}
};
const selectSavedAction = async () => {
const modal = await modalController.create({
component: SavedReadWriteActionList,
componentProps: {
type: "write",
},
});
modal.present();
const { data, role } = (await modal.onWillDismiss()) as {
data: ContractAction;
role: string;
};
if (role === "confirm") {
const content = await getAbis(data.abi);
if (!content) {
alertMsg.value =
"Abi not found in storage, be sure Abi with name " + data.abi + " exists.";
return (alertOpen.value = true);
}
abiContent.value = content;
functionName.value = data.functionName;
params.value = Object.values(data.params);
contractAddress.value = data.contract;
selectedAbi.value = data.abi;
parsedAbi = JSON.parse(abiContent.value);
functions.value = parsedAbi
.filter((fn: any) => fn.type === "function")
.map((fn: any) => fn.name);
}
};
const saveActionInStorage = () => {
if (!functionName.value) {
alertMsg.value = "Function Name is required";
return (alertOpen.value = true);
}
if (!contractAddress.value) {
alertMsg.value = "Contract Address is required";
return (alertOpen.value = true);
}
if (abiContent.value === "") {
alertMsg.value = "Abi is required";
return (alertOpen.value = true);
}
saveActionModal.value = true;
};
const executeAction = async () => {
if (sandboxLoaded.value === false) {
alertMsg.value = "Sandbox for eval not loaded yet, please wait";
return (alertOpen.value = true);
}
if (!contractAddress.value) {
alertMsg.value = "Contract Address is required";
return (alertOpen.value = true);
}
if (!functionName.value) {
alertMsg.value = "Function Name is required";
return (alertOpen.value = true);
}
if (!parsedAbi) {
alertMsg.value = "Abi is required";
return (alertOpen.value = true);
}
alertHeader.value = "Error";
// const provider = await getCurrentProvider();
const encodeParamsTypes = [];
const evalParams = await Promise.all(
params.value.map(async (param) => await getEvalValue(param.value))
);
try {
if (functionName.value?.includes("(")) {
const paramsTypes = functionName.value
.split("(")[1]
.split(")")[0]
.split(",")
.map((param) => param.trim());
if (paramsTypes.length !== evalParams.length) {
alertMsg.value = "Params count mismatch";
return (alertOpen.value = true);
}
encodeParamsTypes.push(...paramsTypes);
}
} catch {
alertMsg.value =
"Function Siganture wrong format (ex: 'functionName(uint256,string)')";
return (alertOpen.value = true);
}
const fnName = functionName.value.includes("(")
? functionName.value.split("(")[0]
: functionName.value;
const iface = new ethers.Interface(parsedAbi);
try {
loadingSend.value = true;
loading.value = true;
const data = iface.encodeFunctionData(fnName, evalParams);
const tx = {
from: [await getSelectedAddress()][0],
to: contractAddress.value,
data,
gasLimit: "0x0",
gasPrice: "0x0",
};
const result = (await walletPromptSendTx(tx)) as {
error?: string;
};
if (result?.error) {
console.error(result);
alertOpen.value = true;
alertMsg.value = "Error sending transaction to chain";
loading.value = false;
return;
} else {
alertHeader.value = "OK";
alertMsg.value = "Transaction sent successfully";
alertOpen.value = true;
}
} catch (e) {
console.error(e);
alertMsg.value = "Function call failed, check params, contract address and ABI";
loadingSend.value = false;
loading.value = false;
return (alertOpen.value = true);
}
loadingSend.value = false;
loading.value = false;
};
const saveAction = async () => {
if (!name.value) {
alertMsg.value = "Name is required";
return (alertOpen.value = true);
}
const action = {
name: name.value,
contract: contractAddress.value,
functionName: functionName.value,
params: params.value,
abi: selectedAbi.value,
};
await writeCASet(action);
saveActionModal.value = false;
alertMsg.value = "Action saved successfully";
alertHeader.value = "OK";
return (alertOpen.value = true);
};
const messageHandler = (event: any) => {
messagePromiseResolve(event?.data?.result);
};
onMounted(() => {
window.addEventListener("message", messageHandler);
});
onBeforeUnmount(() => {
window.removeEventListener("message", messageHandler);
});
const getEvalValue = (evalString: string) => {
return new Promise((resolve) => {
if (!evalFrame.value?.contentWindow?.postMessage) {
return;
}
messagePromiseResolve = resolve;
evalFrame.value?.contentWindow?.postMessage({ code: evalString }, "*");
});
};
const handleFnChange = (event: any) => {
functionName.value = event.detail.value;
};
const openModalAddContact = async () => {
const modal = await modalController.create({
component: SelectedContacts,
componentProps: {},
});
modal.present();
const { data, role } = await modal.onWillDismiss();
if (role === "confirm") {
contractAddress.value = data.address;
}
};
return {
saveActionModal,
handleFnChange,
clipboardOutline,
evalFrame,
alertOpen,
alertMsg,
alertHeader,
functionName,
paste,
savedModalState,
name,
contractAddress,
params,
saveActionInStorage,
executeAction,
sandboxLoaded,
openAbiListModal,
selectedAbi,
functions,
selectFunction,
saveAction,
selectSavedAction,
loading,
loadingSend,
openModalAddContact,
};
},
});
</script>

View File

@ -9,7 +9,7 @@
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"sourceMap": false,
"baseUrl": ".",
"types": [
"chrome",

View File

@ -1,12 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'url'
import nodePolyfills from 'rollup-plugin-polyfill-node'
import { crx } from '@crxjs/vite-plugin'
import manifest from './src/extension/manifest.json'
const production = process.env.NODE_ENV === 'production'
// https://vitejs.dev/config/
export default defineConfig({
publicDir: './public',
@ -25,7 +22,16 @@ export default defineConfig({
},
build: {
rollupOptions: {
plugins: [nodePolyfills()]
// plugins: [nodePolyfills()],
onwarn: (warning) => {
if (warning.message.includes('comment will be removed')) {
return false;
}
return true;
},
input: {
['eval-sandbox']: 'eval-sandbox.html',
},
},
sourcemap: false,
chunkSizeWarningLimit: 1000,
@ -33,12 +39,11 @@ export default defineConfig({
transformMixedEsModules: true
},
},
esbuild: {
legalComments: 'none',
},
plugins: [
!production &&
nodePolyfills({
include: ['node_modules/**/*.js', new RegExp('node_modules/.vite/.*js')]
}),
crx({ manifest }),
crx({ manifest: manifest as any }),
vue()
],
server: {

2812
yarn.lock

File diff suppressed because it is too large Load Diff