chore: changes for `1.3.0`

This commit is contained in:
Andrei O 2024-01-24 02:41:31 +02:00
parent b52ddd02f0
commit fe8e4c273b
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
43 changed files with 3113 additions and 249 deletions

View File

@ -1,5 +1,21 @@
# Changelog # Changelog
## Manifest Version 1.3.0
- 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 ## Manifest Version 1.2.8
- better support for estimate gas - better support for estimate gas

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> <!DOCTYPE html>
<html lang="en" style="width:400px;height:450px"> <html lang="en" style="width:400px;height:500px">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Clear Wallet</title> <title>Clear Wallet</title>

View File

@ -1,10 +1,11 @@
{ {
"name": "clear-wallet", "name": "clear-wallet",
"version": "1.2.8", "version": "1.2.9",
"private": true, "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.",
"scripts": { "scripts": {
"dev": "vite", "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", "content": "tsc --outFile src/extension/content.js src/extension/content.ts",
"post-build": "ts-node ./release-scripts/post-build.ts", "post-build": "ts-node ./release-scripts/post-build.ts",
"build": "yarn inject && yarn content && vue-tsc --noEmit && vite build && yarn post-build", "build": "yarn inject && yarn content && vue-tsc --noEmit && vite build && yarn post-build",
@ -53,6 +54,5 @@
"vite": "^4.4.9", "vite": "^4.4.9",
"vue-tsc": "^1.8.8", "vue-tsc": "^1.8.8",
"yarn-upgrade-all": "^0.7.2" "yarn-upgrade-all": "^0.7.2"
}, }
"description": "An Ionic project"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

View File

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

View File

@ -1,16 +1,15 @@
(async () => { (async () => {
const CONTENT_BUILD_PATH = 'src/extension/content.js' 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 fs = (await import('fs')).default
const path = (await import('path')).default const path = (await import('path')).default
const pkg = JSON.parse(fs.readFileSync('dist/manifest.json').toString()); const pkg = JSON.parse(fs.readFileSync('dist/manifest.json').toString());
pkg.content_scripts[0].js[0] = CONTENT_BUILD_PATH 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/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/'+ 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/'; const directory = 'dist/assets/';
fs.readdir(directory, (err, files) => { fs.readdir(directory, (err, files) => {
files.forEach(file => { files.forEach(file => {

View File

@ -6,9 +6,11 @@
<script lang="ts"> <script lang="ts">
import { IonApp, IonRouterOutlet } from "@ionic/vue"; import { IonApp, IonRouterOutlet } from "@ionic/vue";
import { defineComponent, onBeforeMount, onMounted } from "vue"; import { defineComponent, onBeforeMount, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { getSettings } from "@/utils/platform"; import { getSettings } from "@/utils/platform";
import { getSelectedAddress } from "@/utils/wallet";
import type { RequestArguments } from "@/extension/types";
export default defineComponent({ export default defineComponent({
name: "App", name: "App",
@ -21,6 +23,51 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
const { param, rid } = route.query; 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;
};
onBeforeMount(() => { onBeforeMount(() => {
getSettings().then((settings) => { getSettings().then((settings) => {
if (settings.theme !== "system") { if (settings.theme !== "system") {
@ -28,6 +75,17 @@ export default defineComponent({
document.body.classList.add(settings.theme); document.body.classList.add(settings.theme);
} }
}); });
if (chrome?.runtime?.onMessage) {
chrome.runtime.onMessage.addListener(pageListener);
console.info("page listener set");
}
});
onUnmounted(() => {
if (chrome?.runtime?.onMessage) {
chrome.runtime.onMessage.removeListener(pageListener);
console.info("page listener removed");
}
}); });
onMounted(() => { onMounted(() => {

View File

@ -1,23 +1,24 @@
(() =>{ (() => {
try { // Not needed anymore since injection is done with MAIN_WORLD context
const container = document.documentElement; // try {
const script = document.createElement('script'); // const container = document.documentElement;
script.setAttribute('async', "false") // const script = document.createElement('script');
script.setAttribute('fetchpriority', "high") // script.setAttribute('async', "false")
script.src = chrome.runtime.getURL('src/extension/inject.js') // script.setAttribute('fetchpriority', "high")
container.prepend(script) // script.src = chrome.runtime.getURL('src/extension/inject.js')
script.addEventListener('load', () => { container.removeChild(script) } ) // container.prepend(script)
} catch (error) { // script.addEventListener('load', () => { container.removeChild(script) })
console.error('MetaMask: Provider injection failed.', error); // } catch (error) {
} // console.info('Error: MetaMask: Provider injection failed.', error);
// }
})() })()
const allowedMethods = { const allowedMethods = {
'eth_accounts': true, 'eth_accounts': true,
'eth_requestAccounts' : true, 'eth_requestAccounts': true,
'eth_chainId': true, 'eth_chainId': true,
'personal_sign' : true, 'personal_sign': true,
'wallet_requestPermissions': true, 'wallet_requestPermissions': true,
'eth_gasPrice': true, 'eth_gasPrice': true,
'eth_getBlockByNumber': true, 'eth_getBlockByNumber': true,
@ -50,42 +51,51 @@ const allowedMethods = {
window.addEventListener("message", (event) => { window.addEventListener("message", (event) => {
if (event.source != window) if (event.source != window)
return; return;
// console.log(event) if (event?.data?.type === "CLWALLET_CONTENT") {
if (event.data.type && (event.data.type === "CLWALLET_CONTENT")) {
event.data.data.resId = event.data.resId event.data.data.resId = event.data.resId
event.data.data.type = "CLWALLET_CONTENT_MSG" event.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.website = document?.location?.href ?? '' event.data.data.website = document?.location?.href ?? ''
if((event?.data?.data?.method ?? 'x') in allowedMethods) { if ((event?.data?.data?.method ?? 'x') in allowedMethods) {
chrome.runtime.sendMessage(event.data.data, (res) => { chrome.runtime.sendMessage(event.data.data, (res) => {
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId }; if (chrome.runtime.lastError) {
// console.log('data back', data) console.warn("LOC1: Error sending message:", chrome.runtime.lastError);
}
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId };
// console.info('data out', 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, "*"); window.postMessage(data, "*");
}) }
} } else if (event?.data?.type === "CLWALLET_PING") {
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.resId = event.data.resId
event.data.data.type = "CLWALLET_CONTENT_MSG" event.data.data.type = "CLWALLET_CONTENT_MSG"
event.data.data.method = "wallet_connect" event.data.data.method = "wallet_connect"
event.data.data.params = Array(0) event.data.data.params = Array(0)
chrome.runtime.sendMessage(event.data.data , async (res) => { chrome.runtime.sendMessage(event.data.data, async (res) => {
window.postMessage(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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
chrome.runtime.onMessage.addListener((message: any , sender, sendResponse) => { chrome.runtime.onMessage.addListener((message: any, sender, sendResponse) => {
if(message.type === "CLWALLET_EXT_LISTNER") { if (chrome.runtime.lastError) {
const data = { type: "CLWALLET_PAGE_LISTENER", data: message.data }; 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) // console.log('data listner', data)
window.postMessage(data, "*"); window.postMessage(data, "*");
} }
return true return true
}); });

View File

@ -4,6 +4,42 @@ interface RequestArguments {
params?: unknown[] | object; 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/',
}
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 = { const listners = {
accountsChanged: new Set<(p?: any) => void>(), accountsChanged: new Set<(p?: any) => void>(),
connect: new Set<(p?: any) => void>(), connect: new Set<(p?: any) => void>(),
@ -36,13 +72,13 @@ const getListnersCount = (): number => {
const sendMessage = (args: RequestArguments, ping = false) => { const sendMessage = (args: RequestArguments, ping = false) => {
if(Object.values(promResolvers).filter(r=> r).length < 10 ) { if(Object.values(promResolvers).filter(r=> r).length < 10 ) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const resId = crypto.randomUUID() const resId = [...`${Math.random().toString(16) + Date.now().toString(16)}`].slice(2).join('')
promResolvers.set(resId, { resolve, reject }) promResolvers.set(resId, { resolve, reject })
const data = { type: "CLWALLET_CONTENT", data: args, resId}; const data = { type: "CLWALLET_CONTENT", data: args, resId};
if (ping) { if (ping) {
data.type = 'CLWALLET_PING' data.type = 'CLWALLET_PING'
} }
// console.log('data in', data) // console.info('data in', data)
window.postMessage(data, "*"); window.postMessage(data, "*");
}) })
} else { } else {
@ -80,7 +116,7 @@ class MetaMaskAPI {
_events: {}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(4) _events: {}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(4)
} }
isConnected() { isConnected() {
return false return true
} }
// for maximum compatibility since is cloning the same API // for maximum compatibility since is cloning the same API
@ -129,7 +165,7 @@ class MetaMaskAPI {
} else if (typeof arg1 === 'object') { } else if (typeof arg1 === 'object') {
return sendMessage(arg1 as RequestArguments) return sendMessage(arg1 as RequestArguments)
} else { } else {
console.error('Clear Wallet: faulty request') console.info('ERROR: Clear Wallet: faulty request')
} }
}else if( typeof arg1 === 'string' ) { }else if( typeof arg1 === 'string' ) {
return sendMessage({ return sendMessage({
@ -272,37 +308,22 @@ class MetaMaskAPI {
_handleStreamDisconnect() { return true } _handleStreamDisconnect() { return true }
_handleUnlockStateChanged() { return true } _handleUnlockStateChanged() { return true }
_sendSync () { _sendSync () {
console.error('Clear Wallet: Sync calling is deprecated and not supported') console.info('ERROR: Clear Wallet: Sync calling is deprecated and not supported')
} }
} }
const eth = new Proxy( new MetaMaskAPI(), { 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 }, deleteProperty: () => { return true },
}) })
const listner = function(event: any) { const listner = function(event: any) {
if (event.source != window) return; if (event.source != window) return;
if (event.data.type && (event.data.type === "CLWALLET_PAGE")) { if (event?.data?.type === "CLWALLET_PAGE") {
try { try {
if(event?.data?.data?.error){ if(event?.data?.data?.error){
promResolvers.get(event.data.resId)?.reject(event.data.data); promResolvers.get(event.data.resId)?.reject(event.data.data);
console.error(event?.data?.data) console.info('Error: ', event?.data?.data)
}else { }else {
promResolvers.get(event.data.resId)?.resolve(event.data.data); promResolvers.get(event.data.resId)?.resolve(event.data.data);
} }
@ -310,7 +331,7 @@ const listner = function(event: any) {
} catch (e) { } catch (e) {
// console.log('Failed to connect resolve msg', e) // console.log('Failed to connect resolve msg', e)
} }
} else if( event.data.type && (event.data.type === "CLWALLET_PAGE_LISTENER")) { } else if(event?.data?.type === "CLWALLET_PAGE_LISTENER") {
if((event?.data?.data?.listner ?? 'x') in listners ) { if((event?.data?.data?.listner ?? 'x') in listners ) {
try { try {
const listnerName = event?.data?.data?.listner as ('accountsChanged' | 'connect' | 'disconnect' | 'chainChanged') const listnerName = event?.data?.data?.listner as ('accountsChanged' | 'connect' | 'disconnect' | 'chainChanged')
@ -320,7 +341,7 @@ const listner = function(event: any) {
(<any>eth).selectedAddress = event?.data?.data?.address ?? null; (<any>eth).selectedAddress = event?.data?.data?.address ?? null;
(<any>eth).isConnected = () => true; (<any>eth).isConnected = () => true;
} else if( listnerName === 'chainChanged' ) { } else if( listnerName === 'chainChanged' ) {
// console.log(event?.data?.data?.data); // console.info(event?.data?.data?.data);
(<any>eth).networkVersion = event?.data?.data?.data.toString(10) ?? '137'; (<any>eth).networkVersion = event?.data?.data?.data.toString(10) ?? '137';
(<any>eth).chainId = event?.data?.data?.data ?? '0x89'; (<any>eth).chainId = event?.data?.data?.data ?? '0x89';
} else if ( listnerName === 'accountsChanged' ) { } else if ( listnerName === 'accountsChanged' ) {
@ -332,7 +353,7 @@ const listner = function(event: any) {
listners.once[listnerName].delete(listner) listners.once[listnerName].delete(listner)
}); });
} catch (e) { } catch (e) {
// console.error(e) // console.info(e)
// ignore // ignore
} }
} }
@ -341,43 +362,46 @@ const listner = function(event: any) {
window.addEventListener("message",listner) window.addEventListener("message",listner)
// const proxy1 = new Proxy({
// // on: (event: any, callback:any) => { if (event === 'message') { // eslint-disable-next-line @typescript-eslint/no-unused-vars
// // debugger; const proxy1 = new Proxy(new MetaMaskAPI(), {
// // callback(true, true) get: function (target: any, prop: any) {
// // } } // Intercept method calls and log them
// }, { if (typeof target[prop] === 'function') {
// get: function(target, name, receiver) { return function (...args: any[]) {
// if (typeof (<any>target)[name] == 'function') { console.log(`Calling ${prop} with arguments:`, args);
// return function (...args: any) { // eslint-disable-next-line prefer-spread
// console.dir({ call: [name, ...args] }); const result = target[prop].apply(target, args);
// } console.log(`${prop} returned:`, result);
// } return result;
};
} else {
console.log(`Reading ${prop}`);
return target[prop];
}
},
})
// console.log('ETH', name.toString() , target, receiver); // const web3Shim = {
// return undefined // currentProvider: eth,
// } // __isMetaMaskShim__: true
// }) // }
const web3Shim = {
currentProvider: eth,
__isMetaMaskShim__: true
}
const injectWallet = (win: any) => { const injectWallet = (win: any) => {
Object.defineProperty(win, 'ethereum', { Object.defineProperty(win, 'ethereum', {
value: eth, value: eth,
}); });
Object.defineProperty(win, 'web3', { Object.defineProperty(win, 'web3', {
value: web3Shim value: eth
}); });
sendMessage({ sendMessage({
method: 'wallet_ready' method: 'wallet_ready'
}, true) }, true)
// console.log('Clear wallet injected', (window as any).ethereum, win) console.log('Clear wallet injected', (window as any).ethereum, win)
} }
injectWallet(this); injectWallet(this);
loadEIP1193Provider(eth)
// setTimeout(() => { // setTimeout(() => {
// // console.log('Metamask clone test'); // // console.log('Metamask clone test');

View File

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

View File

@ -1,6 +1,40 @@
import { getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory, getNetworks, strToHex, numToHexStr } from '@/utils/platform'; import {
import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest' CLW_CONTEXT_MENU_ID,
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber, evmCall, getTxByHash, getTxReceipt, signTypedData, getCode, getTxCount } from '@/utils/wallet' 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 type { RequestArguments } from '@/extension/types'
import { rpcError } from '@/extension/rpcConstants' import { rpcError } from '@/extension/rpcConstants'
import { updatePrices } from '@/utils/gecko' import { updatePrices } from '@/utils/gecko'
@ -9,23 +43,58 @@ import { mainNets, testNets } from '@/utils/networks'
let notificationUrl: string let notificationUrl: string
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
console.log('Service worker installed'); enableRightClickVote()
console.info('Service worker installed');
}) })
chrome.runtime.onStartup.addListener(() => { chrome.runtime.onStartup.addListener(() => {
console.log('Service worker startup'); console.info('Service worker startup');
enableRightClickVote();
if(chrome.runtime.lastError) { if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message); console.warn("Whoops.. " + chrome.runtime.lastError.message);
} }
}) })
chrome.runtime.onSuspend.addListener(() => { chrome.runtime.onSuspend.addListener(() => {
console.log('Service worker suspend'); console.info('Service worker suspend');
if(chrome.runtime.lastError) { if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message); 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', { chrome.alarms.create('updatePrices', {
periodInMinutes: 1 periodInMinutes: 1
@ -34,7 +103,9 @@ chrome.alarms.create('updatePrices', {
chrome.alarms.onAlarm.addListener((alarm) => { chrome.alarms.onAlarm.addListener((alarm) => {
if(alarm.name === 'updatePrices') { if(alarm.name === 'updatePrices') {
updatePrices().then(() => { updatePrices().then(() => {
console.log('Prices updated') console.info('Prices updated')
}).catch((err) => {
console.warn('Prices update failed', err)
}) })
} }
getSettings().then((settings) => { getSettings().then((settings) => {
@ -77,9 +148,15 @@ if (!chrome.notifications.onButtonClicked.hasListener(viewTxListner)){
} }
const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: any) => any) => { 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") { if(message?.type !== "CLWALLET_CONTENT_MSG") {
return true return true
} }
console.info('main listener', message);
(async () => { (async () => {
if (!(message?.method)) { if (!(message?.method)) {
sendResponse({ sendResponse({
@ -154,7 +231,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
} }
case 'eth_gasPrice': { case 'eth_gasPrice': {
try { try {
sendResponse(strToHex(String(await getGasPrice() ?? 0))) sendResponse(numToHexStr(BigInt(Math.trunc(await getGasPrice() * 1e9))))
} catch { } catch {
sendResponse({ sendResponse({
error: true, error: true,
@ -166,7 +243,9 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
} }
case 'eth_getBalance': { case 'eth_getBalance': {
try { try {
sendResponse(await getBalance()) const balance = await getBalance()
const balanceHex = numToHexStr(balance ?? 0n)
sendResponse(balanceHex)
} catch { } catch {
sendResponse({ sendResponse({
error: true, error: true,
@ -217,7 +296,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
data: params?.data ?? '', data: params?.data ?? '',
value: params?.value ?? '0x0' value: params?.value ?? '0x0'
}) })
const gasHex = strToHex(String(gas ?? 0)) const gasHex = numToHexStr(gas ?? 0n)
sendResponse(gasHex) sendResponse(gasHex)
} catch(err) { } catch(err) {
if(String(err).includes('UNPREDICTABLE_GAS_LIMIT')) { if(String(err).includes('UNPREDICTABLE_GAS_LIMIT')) {
@ -244,10 +323,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
case 'eth_requestAccounts': case 'eth_requestAccounts':
case 'eth_accounts': { case 'eth_accounts': {
try { try {
// give only the selected address for better privacy sendResponse(await getSelectedAddress())
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse(address)
} catch { } catch {
sendResponse({ sendResponse({
error: true, error: true,
@ -303,13 +379,6 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
} }
params.from = account.address params.from = account.address
const serializeParams = strToHex(JSON.stringify(params)) ?? '' 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 let gWin: any
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
chrome.windows.create({ chrome.windows.create({
@ -327,7 +396,9 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}) })
try { 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) sendResponse(tx.hash)
const buttons = {} as any const buttons = {} as any
const network = await getSelectedNetwork() const network = await getSelectedNetwork()
@ -452,7 +523,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
clearPk() clearPk()
} }
} catch (e) { } catch (e) {
// console.error(e) // console.info(e)
sendResponse({ sendResponse({
error: true, error: true,
code: rpcError.USER_REJECTED, code: rpcError.USER_REJECTED,
@ -570,7 +641,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}) })
sendResponse(null) sendResponse(null)
} catch (err) { } catch (err) {
console.log('err') console.error('err')
sendResponse({ sendResponse({
error: true, error: true,
code: rpcError.USER_REJECTED, code: rpcError.USER_REJECTED,
@ -612,7 +683,8 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
} }
case 'wallet_send_data': { case 'wallet_send_data': {
if(String(sender.tab?.windowId) in rIdData){ 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) sendResponse(true)
} }
break break

View File

@ -8,9 +8,12 @@ export interface Network {
explorer?: string explorer?: string
} }
export interface Account { export interface Contact {
name: string name: string
address: string address: string
}
export interface Account extends Contact {
pk: string pk: string
encPk: string encPk: string
} }
@ -65,3 +68,15 @@ export interface HistoryItem {
webiste?: string webiste?: string
txHash: 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 rIdData = {} as Record<string, any | undefined>
export const approve = (rId: string) => { 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) => { export const walletSendData = (rId: string, data: any) => {
return new Promise((resolve) => { return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_send_data', resId: rId, data, type: 'CLWALLET_CONTENT_MSG' }, (r) => { 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) resolve(r)
}) })
}) })
@ -18,6 +26,25 @@ export const walletSendData = (rId: string, data: any) => {
export const walletGetData = (rId: string) => { export const walletGetData = (rId: string) => {
return new Promise((resolve) => { return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_get_data', resId: rId, type: 'CLWALLET_CONTENT_MSG' }, (r) => { 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) resolve(r)
}) })
}) })
@ -26,6 +53,9 @@ export const walletGetData = (rId: string) => {
export const walletPing = () => { export const walletPing = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_ping', type: 'CLWALLET_CONTENT_MSG' }, (r) => { 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) resolve(r)
}) })
}) })

View File

@ -79,6 +79,19 @@ const routes: Array<RouteRecordRaw> = [
path: 'add-network/edit/:chainId', path: 'add-network/edit/:chainId',
component: () => import('@/views/AddNetwork.vue'), 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

@ -22,7 +22,7 @@ export const mainNets: {[key: number]: Network} = {
}, },
100: { 100: {
name: 'Gnosis', name: 'Gnosis',
rpc: 'https://rpc.gnosischain.com/', rpc: 'https://rpc.gnosischain.com',
chainId: 100, chainId: 100,
explorer: 'https://gnosisscan.io', explorer: 'https://gnosisscan.io',
icon:'xdai.webp', icon:'xdai.webp',
@ -56,6 +56,15 @@ export const mainNets: {[key: number]: Network} = {
symbol: 'ETH', symbol: 'ETH',
priceId: 'ethereum' 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 = { export const testNets = {
@ -77,7 +86,7 @@ export const testNets = {
name: 'TESTNET Polygon', name: 'TESTNET Polygon',
rpc: 'https://rpc.ankr.com/polygon_mumbai', rpc: 'https://rpc.ankr.com/polygon_mumbai',
chainId: 80001, chainId: 80001,
explorer: 'https://mumbai.polygonscan.com/', explorer: 'https://mumbai.polygonscan.com',
icon:'polygon.webp' icon:'polygon.webp'
}, },
100100: { 100100: {
@ -91,21 +100,21 @@ export const testNets = {
name: 'TESTNET Optimism Goreli', name: 'TESTNET Optimism Goreli',
rpc: 'https://goerli.optimism.io/', rpc: 'https://goerli.optimism.io/',
chainId: 420, chainId: 420,
explorer: 'https://goerli.etherscan.io/', explorer: 'https://goerli.etherscan.io',
icon: 'optimism.webp' icon: 'optimism.webp'
}, },
97: { 97: {
name: 'TESTNET BSC', name: 'TESTNET BSC',
rpc: 'https://bsctestapi.terminet.io/rpc', rpc: 'https://bsctestapi.terminet.io/rpc',
chainId: 97, chainId: 97,
explorer: 'https://testnet.bscscan.com/', explorer: 'https://testnet.bscscan.com',
icon: 'binance.webp' icon: 'binance.webp'
}, },
421613: { 421613: {
name: 'TESTNET Arbitrum One', name: 'TESTNET Arbitrum One',
rpc: 'https://goerli-rollup.arbitrum.io/rpc/', rpc: 'https://goerli-rollup.arbitrum.io/rpc/',
chainId: 421613, chainId: 421613,
explorer: 'https://testnet.arbiscan.io/', explorer: 'https://testnet.arbiscan.io',
icon: 'arbitrum.webp' icon: 'arbitrum.webp'
}, },
} }

View File

@ -1,4 +1,4 @@
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' import type { Ref } from 'vue'
const defaultSettings = { const defaultSettings = {
@ -11,6 +11,12 @@ const defaultSettings = {
lastLock: Date.now() lastLock: Date.now()
} }
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> =>{ export const storageSave = async (key: string, value: any): Promise<void> =>{
await chrome.storage.local.set({ [key]: value }) await chrome.storage.local.set({ [key]: value })
} }
@ -48,11 +54,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[]> => { export const getAccounts = async (): Promise<Account[]> => {
return (await storageGet('accounts')).accounts ?? [] as Account[] return (await storageGet('accounts')).accounts ?? [] as Account[]
} }
export const saveAccount = async (account: Account): Promise<void> => { export const saveAccount = async (account: Account): Promise<void> => {
const savedAccounts = await getAccounts() const savedAccounts = await getAccounts()
await storageSave('accounts', [account, ...savedAccounts]) await storageSave('accounts', [account, ...savedAccounts])
@ -62,6 +80,7 @@ export const replaceAccounts = async (accounts: Account[]): Promise<void> => {
await storageSave('accounts', accounts) await storageSave('accounts', accounts)
} }
export const getSelectedAccount = async (): Promise<Account> => { export const getSelectedAccount = async (): Promise<Account> => {
return (await storageGet('selectedAccount'))?.selectedAccount ?? null as unknown as Account return (await storageGet('selectedAccount'))?.selectedAccount ?? null as unknown as Account
} }
@ -106,6 +125,80 @@ export const setSettings = async (settings: Settings): Promise<void> => {
await storageSave('settings', settings ) 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> => { export const blockLockout = async (): Promise<Settings> => {
const settings = await getSettings() const settings = await getSettings()
settings.lockOutBlocked = true settings.lockOutBlocked = true
@ -128,15 +221,6 @@ export const setBalanceCache = async (balance: string): Promise<void> => {
await storageSave('balance', balance ) 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) => { export const smallRandomString = (size = 7) => {
if(size <= 7) { if(size <= 7) {
return (Math.random() + 1).toString(36).substring(0,7); return (Math.random() + 1).toString(36).substring(0,7);
@ -178,7 +262,7 @@ export const hexTostr = (hexStr: string) =>
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 copyAddress = async (address: string, toastRef: Ref<boolean>) => {
await navigator.clipboard.writeText(address) await navigator.clipboard.writeText(address)
@ -195,6 +279,27 @@ 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) => { export const openTab = (url: string) => {
chrome.tabs.create({ chrome.tabs.create({
url url

View File

@ -1,7 +1,5 @@
import { getSelectedAccount, getSelectedNetwork } from '@/utils/platform'; import { getSelectedAccount, getSelectedNetwork } from '@/utils/platform';
import { ethers } from "ethers" import { ethers} from "ethers"
import { strToHex } from '@/utils/platform';
export const signMsg = async (msg: string) => { export const signMsg = async (msg: string) => {
const account = await getSelectedAccount() const account = await getSelectedAccount()
@ -35,8 +33,8 @@ export const getGasPrice = async () => {
const network = await getSelectedNetwork() const network = await getSelectedNetwork()
const provider = new ethers.JsonRpcProvider(network.rpc) const provider = new ethers.JsonRpcProvider(network.rpc)
const feed = await provider.getFeeData() const feed = await provider.getFeeData()
const gasPrice = feed.gasPrice ?? feed.maxFeePerGas const gasPrice = feed.maxFeePerGas ?? feed.gasPrice ?? 0n
return gasPrice return Number(gasPrice) / 1e9
} }
export const getBlockNumber = async () => { export const getBlockNumber = async () => {
@ -98,35 +96,35 @@ export const getTxCount = async (addr: string, block: null | string = null) => {
} }
} }
export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='0x0', gasPrice='0x0'}: export const getRandomPk = () => {
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string}, return ethers.Wallet.createRandom().privateKey
gasEstimate: Promise<bigint> | null = null, pGasPrice : Promise<bigint | null> | null) => { }
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 account = await getSelectedAccount()
const network = await getSelectedNetwork() const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.JsonRpcProvider(network.rpc)) const wallet = new ethers.Wallet(account.pk, new ethers.JsonRpcProvider(network.rpc))
if(gas === '0x0') { const gasPriceInt = BigInt(gasPrice)
if(!gasEstimate){ const gasInt = BigInt(gas)
throw new Error('No gas estimate available')
}else {
gas = (await gasEstimate).toString()
}
}
if(gasPrice === '0x0') { if(gas === '0x0' || gasPrice === '0x0') {
if(!pGasPrice){
throw new Error('No gas estimate available')
}else {
gasPrice = (await pGasPrice ?? 0).toString()
}
}
console.log('gasPrice', gasPrice)
console.log('gas', gas)
if(gas === '0x0' || gasPrice === '0x0' || 1 === 1) {
throw new Error('No gas estimate available') 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) => { export const formatBalance = (balance: string) => {
@ -135,3 +133,10 @@ export const formatBalance = (balance: string) => {
maximumFractionDigits: 6 maximumFractionDigits: 6
}).format(Number(ethers.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
}

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

@ -0,0 +1,212 @@
<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 route = useRoute();
// const isEdit = route.path.includes("/edit");
// const paramAddress = route.params.address ?? "";
const loading = 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 () => {
// if (isEdit && paramAddress) {
// accountsProm = getAccounts();
// settingsProm = getSettings();
// const accounts = (await accountsProm) as Account[];
// const acc = accounts.find((account) => account.address === paramAddress);
// if (acc) {
// name.value = acc.name;
// }
// }
});
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

@ -15,6 +15,7 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ion-toast <ion-toast
position="top"
:is-open="toastState" :is-open="toastState"
@didDismiss="toastState = false" @didDismiss="toastState = false"
message="Copied to clipboard" message="Copied to clipboard"
@ -54,7 +55,12 @@
<ion-item @click="copyAddress(shownPk, getToastRef())" button> <ion-item @click="copyAddress(shownPk, getToastRef())" button>
<ion-icon style="margin-right: 0.5rem" :icon="copyOutline" /> <ion-icon style="margin-right: 0.5rem" :icon="copyOutline" />
<ion-label button>PK</ion-label> <ion-label button>PK</ion-label>
<ion-input label="pk" id="pastePk" v-model="shownPk" readonly></ion-input> <ion-input
aria-label="pk"
id="pastePk"
v-model="shownPk"
readonly
></ion-input>
</ion-item> </ion-item>
</ion-content> </ion-content>
</ion-modal> </ion-modal>

View File

@ -9,8 +9,7 @@
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<ion-item> <ion-item>
<ion-label>Name</ion-label> <ion-input label="Name" labelPlacement="stacked" v-model="name"></ion-input>
<ion-input label="name" v-model="name"></ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label>Get Random Name</ion-label> <ion-label>Get Random Name</ion-label>
@ -23,8 +22,12 @@
:icon="clipboardOutline" :icon="clipboardOutline"
button button
/> />
<ion-label button>PK</ion-label> <ion-input
<ion-input label="pk" id="pastePk" v-model="pk"></ion-input> label="PK"
labelPlacement="stacked"
id="pastePk"
v-model="pk"
></ion-input>
</ion-item> </ion-item>
<template v-if="!isEdit"> <template v-if="!isEdit">
<ion-item> <ion-item>
@ -33,7 +36,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-button @click="mnemonicModal = true" expand="full" <ion-button @click="mnemonicModal = true" expand="full"
>Extarct From A Mnemonic</ion-button >Extract From A Mnemonic</ion-button
> >
</ion-item> </ion-item>
</template> </template>
@ -67,7 +70,7 @@
<ion-item> <ion-item>
<ion-textarea <ion-textarea
style="overflow-y: scroll" style="overflow-y: scroll"
label="Enter mnemonic" aria-label="Enter mnemonic"
:rows="10" :rows="10"
:cols="10" :cols="10"
v-model="mnemonic" v-model="mnemonic"
@ -75,9 +78,12 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label>Enter Index (default: 0)</ion-label> <ion-label>Enter Index (default: 0)</ion-label>
<ion-input label="mnemonic index" v-model="mnemonicIndex"></ion-input>
</ion-item> </ion-item>
<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-button @click="extractMnemonic">Extract</ion-button>
</ion-item> </ion-item>
</ion-content> </ion-content>
@ -111,7 +117,6 @@ import {
saveSelectedAccount, saveSelectedAccount,
getAccounts, getAccounts,
saveAccount, saveAccount,
getRandomPk,
smallRandomString, smallRandomString,
paste, paste,
getSettings, getSettings,
@ -123,7 +128,7 @@ import UnlockModal from "@/views/UnlockModal.vue";
import { encrypt, getCryptoParams } from "@/utils/webCrypto"; import { encrypt, getCryptoParams } from "@/utils/webCrypto";
import { clipboardOutline } from "ionicons/icons"; import { clipboardOutline } from "ionicons/icons";
import { getFromMnemonic } from "@/utils/wallet"; import { getFromMnemonic, getRandomPk } from "@/utils/wallet";
export default defineComponent({ export default defineComponent({
components: { components: {

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,13 +10,17 @@
>Add from popular chain list</ion-button >Add from popular chain list</ion-button
> >
<ion-item> <ion-item>
<ion-label>Name(*)</ion-label> <ion-input
<ion-input label="name" v-model="name" placeholder="ex: Polygon"></ion-input> label="Name(*)"
labelPlacement="stacked"
v-model="name"
placeholder="ex: Polygon"
></ion-input>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label>ChainId(*)</ion-label>
<ion-input <ion-input
label="chainid" label="ChainId(*)"
labelPlacement="stacked"
v-model="chainId" v-model="chainId"
placeholder="137" placeholder="137"
type="number" type="number"
@ -24,19 +28,18 @@
</ion-item> </ion-item>
<ion-item button> <ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" /> <ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>RPC URL(*)</ion-label>
<ion-input <ion-input
label="rpc" label="RPC URL(*)"
labelPlacement="stacked"
id="pasteRpc" id="pasteRpc"
placeholder="https://polygon-mainnet.g.alchemy.com/..." placeholder="https://polygon-mainnet.g.alchemy.com/..."
v-model="rpc" v-model="rpc"
></ion-input> ></ion-input>
</ion-item> </ion-item>
<ion-item button> <ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>Native Token Symbol(?)</ion-label>
<ion-input <ion-input
label="native token" label="Native Token Symbol"
labelPlacement="stacked"
id="native-token" id="native-token"
placeholder="MATIC" placeholder="MATIC"
v-model="symbol" v-model="symbol"
@ -44,9 +47,9 @@
</ion-item> </ion-item>
<ion-item button> <ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteExplorer')" /> <ion-icon :icon="clipboardOutline" @click="paste('pasteExplorer')" />
<ion-label>Explorer(?)</ion-label>
<ion-input <ion-input
label="explorer" label="Explorer"
labelPlacement="stacked"
id="pasteExplorer" id="pasteExplorer"
placeholder="https://polygonscan.com" placeholder="https://polygonscan.com"
v-model="explorer" v-model="explorer"

View File

@ -35,6 +35,23 @@
<ion-label>Settings</ion-label> <ion-label>Settings</ion-label>
</ion-tab-button> </ion-tab-button>
</ion-tab-bar> </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-tabs>
</ion-content> </ion-content>
</ion-page> </ion-page>
@ -50,9 +67,19 @@ import {
IonTabBar, IonTabBar,
IonTabButton, IonTabButton,
IonLabel, IonLabel,
IonIcon IonIcon,
} from "@ionic/vue"; } 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({ export default defineComponent({
components: { components: {
@ -63,7 +90,7 @@ export default defineComponent({
IonTabBar, IonTabBar,
IonTabButton, IonTabButton,
IonLabel, IonLabel,
IonIcon IonIcon,
}, },
name: "AppTabs", name: "AppTabs",
setup() { setup() {
@ -81,8 +108,11 @@ export default defineComponent({
cogOutline, cogOutline,
receiptOutline, receiptOutline,
gitNetworkOutline, gitNetworkOutline,
sendOutline,
beforeTabChange, beforeTabChange,
afterTabChange, afterTabChange,
glassesOutline,
pushOutline,
}; };
}, },
}); });

View File

@ -16,6 +16,7 @@
> >
</ion-loading> </ion-loading>
<ion-toast <ion-toast
position="top"
:is-open="toastState" :is-open="toastState"
@didDismiss="toastState = false" @didDismiss="toastState = false"
message="Copied to clipboard" message="Copied to clipboard"
@ -33,7 +34,11 @@
Assets info could not be retrieved because of an http error, API down or Assets info could not be retrieved because of an http error, API down or
conectivity issues. conectivity issues.
</template> </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-else>
<template v-if="ethTokens.length || polyTokens.length"> <template v-if="ethTokens.length || polyTokens.length">
<template v-if="ethTokens.length"> <template v-if="ethTokens.length">
@ -274,7 +279,7 @@ export default defineComponent({
return null; return null;
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch web3 profiles", error); console.info("ERROR: Failed to fetch web3 profiles", error);
return null; return null;
} }
}; };

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

@ -33,7 +33,7 @@
<ion-label>Error From Contract:</ion-label> <ion-label>Error From Contract:</ion-label>
<ion-textarea <ion-textarea
style="overflow-y: scroll" style="overflow-y: scroll"
label="Error" aria-label="Error"
:rows="10" :rows="10"
:cols="20" :cols="20"
:value="error" :value="error"

View File

@ -62,6 +62,7 @@
> >
</ion-loading> </ion-loading>
<ion-toast <ion-toast
position="top"
:is-open="toastState" :is-open="toastState"
@didDismiss="toastState = false" @didDismiss="toastState = false"
message="Copied to clipboard" message="Copied to clipboard"

View File

@ -2,7 +2,16 @@
<ion-page> <ion-page>
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title>Wallet</ion-title> <ion-title>
<ion-avatar
style="margin: 0.3rem; width: 1.8rem; height: 1.8rem; 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
>
</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
@ -13,10 +22,18 @@
<ion-list v-else> <ion-list v-else>
<ion-item> <ion-item>
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label> <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>
<ion-item button @click="copyAddress(selectedAccount?.address, getToastRef())"> <ion-item button @click="copyAddress(selectedAccount?.address, getToastRef())">
<p style="font-size: 0.7rem">{{ selectedAccount?.address }}</p> <p style="font-size: 0.7rem; color: coral">{{ selectedAccount?.address }}</p>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon> <ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-item> </ion-item>
<ion-item <ion-item
@ -53,8 +70,21 @@
:src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)" :src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)"
/> />
</ion-avatar> </ion-avatar>
<ion-label>Selected Network ID: {{ selectedNetwork?.chainId }}</ion-label> <ion-label
<ion-button @click="networksModal = true">Select</ion-button> >Selected Network ID:
<span style="color: coral; font-weight: bold">{{
selectedNetwork?.chainId
}}</span></ion-label
>
<ion-button
@click="
() => {
networksModal = true;
toastState = false;
}
"
>Select</ion-button
>
</ion-item> </ion-item>
<ion-loading <ion-loading
@ -67,6 +97,7 @@
> >
</ion-loading> </ion-loading>
<ion-toast <ion-toast
position="top"
:is-open="toastState" :is-open="toastState"
@didDismiss="toastState = false" @didDismiss="toastState = false"
message="Copied to clipboard" message="Copied to clipboard"
@ -98,11 +129,17 @@
button button
> >
<ion-item> <ion-item>
<ion-radio slot="start" :value="account.address" /> <ion-radio
<ion-label>{{ account.name }}</ion-label> :aria-label="account.name"
slot="start"
:value="account.address"
>{{ account.name }}</ion-radio
>
</ion-item> </ion-item>
<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-item>
</ion-list> </ion-list>
</ion-radio-group> </ion-radio-group>
@ -135,11 +172,18 @@
@click="changeSelectedNetwork(network.chainId)" @click="changeSelectedNetwork(network.chainId)"
slot="start" slot="start"
:value="network.chainId" :value="network.chainId"
/> :aria-label="network.name"
<ion-label>{{ network.name }}</ion-label> >
<span style="opacity: 0.7; font-size: 0.8rem">
ID: {{ network.chainId }} ->
</span>
{{ network.name }}
</ion-radio>
</ion-item> </ion-item>
<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-item>
</ion-list> </ion-list>
</ion-radio-group> </ion-radio-group>

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,7 +22,7 @@
<ion-item> <ion-item>
<ion-label>Name:</ion-label> <ion-label>Name:</ion-label>
<ion-input <ion-input
label="Name" aria-label="Name"
style="margin-left: 0.5rem" style="margin-left: 0.5rem"
v-model="name" v-model="name"
readonly readonly
@ -32,7 +32,7 @@
<ion-item> <ion-item>
<ion-label>ChainId: </ion-label> <ion-label>ChainId: </ion-label>
<ion-input <ion-input
label="ChainId" aria-label="ChainId"
style="margin-left: 0.5rem" style="margin-left: 0.5rem"
v-model="chainId" v-model="chainId"
readonly readonly
@ -42,7 +42,7 @@
<ion-item button> <ion-item button>
<ion-label>RPC URL: </ion-label> <ion-label>RPC URL: </ion-label>
<ion-input <ion-input
label="RPC URL" aria-label="RPC URL"
style="margin-left: 0.5rem" style="margin-left: 0.5rem"
readonly readonly
placeholder="https://polygon-mainnet.g.alchemy.com/..." placeholder="https://polygon-mainnet.g.alchemy.com/..."
@ -52,7 +52,7 @@
<ion-item button> <ion-item button>
<ion-label>Native Token Symbol: </ion-label> <ion-label>Native Token Symbol: </ion-label>
<ion-input <ion-input
label="Native Token Symbol" aria-label="Native Token Symbol"
style="margin-left: 0.5rem" style="margin-left: 0.5rem"
readonly readonly
placeholder="MATIC" placeholder="MATIC"
@ -62,7 +62,7 @@
<ion-item button> <ion-item button>
<ion-label>Explorer: </ion-label> <ion-label>Explorer: </ion-label>
<ion-input <ion-input
label="Explorer" aria-label="Explorer"
style="margin-left: 0.5rem" style="margin-left: 0.5rem"
readonly readonly
placeholder="https://polygonscan.com" placeholder="https://polygonscan.com"

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>

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

@ -0,0 +1,302 @@
<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 () => {
selectedNetwork.value = await getSelectedNetwork();
selectedAccount.value = await getSelectedAccount();
currentBalance.value = Number(formatEther((await getBalance()).toString()));
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-item>
<ion-label>Enable Storage Encryption</ion-label> <ion-label>Enable Storage Encryption</ion-label>
<ion-toggle <ion-toggle
aria-label="Enable Storage Encryption"
:key="updateKey" :key="updateKey"
@ion-change="changeEncryption" @ion-change="changeEncryption"
slot="end" slot="end"
@ -33,6 +34,7 @@
<ion-item :disabled="!settings.s.enableStorageEnctyption"> <ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label> <ion-label>Enable Auto Lock</ion-label>
<ion-toggle <ion-toggle
aria-label="Enable Auto Lock"
:key="updateKey" :key="updateKey"
@ion-change="changeAutoLock" @ion-change="changeAutoLock"
slot="end" slot="end"
@ -70,6 +72,7 @@
<ion-item> <ion-item>
<ion-label>Permanent Lock</ion-label> <ion-label>Permanent Lock</ion-label>
<ion-toggle <ion-toggle
aria-label="Permanent Lock"
@ion-change="changePermaLock" @ion-change="changePermaLock"
:key="updateKey" :key="updateKey"
slot="end" slot="end"
@ -180,6 +183,7 @@
</ion-accordion> </ion-accordion>
</ion-accordion-group> </ion-accordion-group>
<ion-toast <ion-toast
position="top"
:is-open="toastState" :is-open="toastState"
@didDismiss="toastState = false" @didDismiss="toastState = false"
:message="toastMsg" :message="toastMsg"
@ -228,7 +232,11 @@
<ion-label>Old Password</ion-label> <ion-label>Old Password</ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input label="password" v-model="mpPass" type="password"></ion-input> <ion-input
aria-label="password"
v-model="mpPass"
type="password"
></ion-input>
</ion-item> </ion-item>
</ion-list> </ion-list>
<div v-else> <div v-else>
@ -237,7 +245,11 @@
<ion-label>New Password</ion-label> <ion-label>New Password</ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input label="password" v-model="mpPass" type="password"></ion-input> <ion-input
aria-label="password"
v-model="mpPass"
type="password"
></ion-input>
</ion-item> </ion-item>
</ion-list> </ion-list>
<ion-list> <ion-list>
@ -246,7 +258,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input <ion-input
label="password" aria-label="password"
v-model="mpConfirm" v-model="mpConfirm"
type="password" type="password"
></ion-input> ></ion-input>

View File

@ -66,6 +66,7 @@
<ion-item> <ion-item>
<ion-label>Raw TX:</ion-label> <ion-label>Raw TX:</ion-label>
<ion-textarea <ion-textarea
aria-label="raw tx"
style="overflow-y: scroll" style="overflow-y: scroll"
:rows="10" :rows="10"
:cols="20" :cols="20"
@ -119,7 +120,11 @@
<ion-label>Limit in units</ion-label> <ion-label>Limit in units</ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input label="gas limit" 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-item> <ion-item>
<ion-button @click="setGasLimit">Set Price</ion-button> <ion-button @click="setGasLimit">Set Price</ion-button>
@ -144,7 +149,7 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input <ion-input
label="price in gwei" aria-label="price in gwei"
v-model="inGasPrice" v-model="inGasPrice"
type="number" type="number"
></ion-input> ></ion-input>
@ -256,7 +261,12 @@ export default defineComponent({
if (!decodedParam) { if (!decodedParam) {
isError = true; isError = true;
} else { } 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 () => { const openModal = async () => {
@ -304,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( gasFee.value = Number(
ethers.formatUnits(String(gasLimit.value * gasPrice.value), "gwei") ethers.formatUnits(Math.trunc(gasLimit.value * gasPrice.value), "gwei")
); );
txValue.value = Number(ethers.formatEther(params?.value ?? "0x0")); txValue.value = Number(ethers.formatEther(params?.value ?? "0x0"));
totalCost.value = gasFee.value + txValue.value; totalCost.value = gasFee.value + txValue.value;
}; };
onIonViewWillEnter(async () => { onIonViewWillEnter(async () => {
console.log(params.value);
(window as any)?.resizeTo?.(600, 800); (window as any)?.resizeTo?.(600, 800);
const pEstimateGas = estimateGas({ const pEstimateGas = estimateGas({
to: params?.to ?? "", to: params?.to ?? "",
@ -330,11 +346,7 @@ export default defineComponent({
ethers.formatEther((await pBalance).toString() ?? "0x0") ethers.formatEther((await pBalance).toString() ?? "0x0")
); );
gasPrice.value = parseInt( gasPrice.value = parseFloat((await pGasPrice).toString() ?? 0.1);
ethers.formatUnits(((await pGasPrice) || 0).toString() ?? "0x0", "gwei"),
10
);
console.log(await pGasPrice);
try { try {
gasLimit.value = parseInt((await pEstimateGas).toString(), 10); gasLimit.value = parseInt((await pEstimateGas).toString(), 10);
@ -347,16 +359,13 @@ export default defineComponent({
inGasPrice.value = gasPrice.value; inGasPrice.value = gasPrice.value;
inGasLimit.value = gasLimit.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) { if (userBalance.value < totalCost.value) {
insuficientBalance.value = true; insuficientBalance.value = true;
} }
const prices = await pGetPrices; const prices = await pGetPrices;
dollarPrice.value = dollarPrice.value =
prices[chainIdToPriceId(selectedNetwork.value?.chainId ?? 0)]?.usd ?? 0; prices[chainIdToPriceId(selectedNetwork.value?.chainId ?? 0)]?.usd ?? 0;
await newGasData();
loading.value = false; loading.value = false;
interval = setInterval(async () => { interval = setInterval(async () => {
@ -369,11 +378,8 @@ export default defineComponent({
if (timerFee.value <= 0) { if (timerFee.value <= 0) {
timerFee.value = 20; timerFee.value = 20;
loading.value = true; loading.value = true;
gasPrice.value = parseInt( gasPrice.value = parseFloat((await getGasPrice()).toString() ?? 0.1);
ethers.formatUnits(((await getGasPrice()) || 0).toString(), "gwei"), await newGasData();
10
);
newGasData();
loading.value = false; loading.value = false;
} }
} }
@ -386,9 +392,6 @@ export default defineComponent({
const setGasLimit = () => { const setGasLimit = () => {
gasLimit.value = inGasLimit.value; gasLimit.value = inGasLimit.value;
walletSendData(rid, {
gas: numToHexStr(gasLimit.value),
});
newGasData(); newGasData();
gasLimitModal.value = false; gasLimitModal.value = false;
}; };
@ -396,9 +399,6 @@ export default defineComponent({
const setGasPrice = () => { const setGasPrice = () => {
gasPrice.value = inGasPrice.value; gasPrice.value = inGasPrice.value;
gasPriceReFetch.value = false; gasPriceReFetch.value = false;
walletSendData(rid, {
gasPrice: numToHexStr(gasPrice.value),
});
newGasData(); newGasData();
gasPriceModal.value = false; gasPriceModal.value = false;
}; };

View File

@ -183,7 +183,6 @@ export default defineComponent({
(window as any)?.resizeTo?.(600, 600); (window as any)?.resizeTo?.(600, 600);
pnetworks = getNetworks(); pnetworks = getNetworks();
selectedNetwork.value = await getSelectedNetwork(); selectedNetwork.value = await getSelectedNetwork();
console.log(networkId.value);
existingNetworks.value = await pnetworks; existingNetworks.value = await pnetworks;
if ((networkId.value ?? "0") in existingNetworks.value ?? {}) { if ((networkId.value ?? "0") in existingNetworks.value ?? {}) {
networkCase.value = "exists"; networkCase.value = "exists";

View File

View File

@ -31,7 +31,7 @@
<ion-label>Unlock Password</ion-label> <ion-label>Unlock Password</ion-label>
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-input label="password" v-model="mpPass" type="password"></ion-input> <ion-input aria-label="password" v-model="mpPass" type="password"></ion-input>
</ion-item> </ion-item>
</ion-list> </ion-list>
<ion-item> <ion-item>

View File

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

@ -25,9 +25,12 @@ export default defineConfig({
}, },
build: { build: {
rollupOptions: { rollupOptions: {
plugins: [nodePolyfills()] plugins: [nodePolyfills()],
input: {
['eval-sandbox']: 'eval-sandbox.html',
},
}, },
sourcemap: false, sourcemap: true,
chunkSizeWarningLimit: 1000, chunkSizeWarningLimit: 1000,
commonjsOptions: { commonjsOptions: {
transformMixedEsModules: true transformMixedEsModules: true