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

View File

@ -1,10 +1,11 @@
{
"name": "clear-wallet",
"version": "1.2.8",
"version": "1.2.9",
"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": {
"dev": "vite",
"inject": "tsc --outFile src/extension/inject.js src/extension/inject.ts",
"inject": "tsc --downlevelIteration --outFile src/extension/inject.js src/extension/inject.ts",
"content": "tsc --outFile src/extension/content.js src/extension/content.ts",
"post-build": "ts-node ./release-scripts/post-build.ts",
"build": "yarn inject && yarn content && vue-tsc --noEmit && vite build && yarn post-build",
@ -53,6 +54,5 @@
"vite": "^4.4.9",
"vue-tsc": "^1.8.8",
"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>
<html lang="en" style="width:400px;height:450px">
<html lang="en" style="width:400px;height:500px">
<head>
<meta charset="utf-8" />
<title>Clear Wallet</title>

View File

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

View File

@ -6,9 +6,11 @@
<script lang="ts">
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 { getSettings } from "@/utils/platform";
import { getSelectedAddress } from "@/utils/wallet";
import type { RequestArguments } from "@/extension/types";
export default defineComponent({
name: "App",
@ -21,6 +23,51 @@ export default defineComponent({
const router = useRouter();
const { param, rid } = route.query;
const pageListener = (
message: RequestArguments,
sender: any,
sendResponse: (a: any) => any
) => {
if (chrome.runtime.lastError) {
console.info("Error receiving message:", chrome.runtime.lastError);
}
if (message?.type !== "CLWALLET_PAGE_MSG") {
return true;
}
console.info("page listener:", message);
(async () => {
if (!message?.method) {
sendResponse({
code: 500,
message: "Invalid request method",
});
} else {
// ETH API
switch (message.method) {
case "paste": {
const currentAddress = (await getSelectedAddress()) as string[];
if (currentAddress.length > 0) {
document.execCommand("insertText", false, currentAddress[0]);
}
sendResponse(true);
break;
}
default: {
sendResponse({
error: true,
message:
"ClearWallet: Invalid PAGE request method " + message?.method ?? "",
});
break;
}
}
}
})();
return true;
};
onBeforeMount(() => {
getSettings().then((settings) => {
if (settings.theme !== "system") {
@ -28,6 +75,17 @@ export default defineComponent({
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(() => {

View File

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

View File

@ -4,6 +4,42 @@ interface RequestArguments {
params?: unknown[] | object;
}
interface EIP6963ProviderInfo {
uuid: string;
name: string;
icon: string;
rdns: string;
}
const ProviderInfo: EIP6963ProviderInfo = {
uuid: '1fa914a1-f8c9-4c74-8d84-4aa93dc90eec',
name: 'Clear Wallet',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAGN0lEQVRoge1ZfUgbZxj/3eXLRK112gmDwWYjo9gVin8UN509aTsGG26ywUb/WrWTVtgK1lLGPurGRoe6rmtBN+r219wfhdaJUFw/ImgrMtuO0NrSIhuibU3RJqaa5HK5G8/rJWh6l1yStVLwBw9JLu897/P9PO8dVrGKVTzd4LSk37p1a/RrDYA9AF4BEFHpScCk0jCADgA98XsODAywT3MCYVqLior2NzU1obS0FLIsM3oS4Hme0djY2I6jR4/umJycbHO5XM2CIDyyu54Htq1bt+7swYMH4XQ68ePHl2HLssBs5R+7+AoAKRQBx3No7qhAMBhEQ0MDJiYmtgM4F12XzAN7Nm3ahPz8fHz1wbCqZ/ixCx+P5jfPorVvO+rr69Ha2nrW7/cXAJhdukzPpJUk/Mn2CXC8GRxvWhECx+O7jy5hy5YtqKiogJqTy6DnASvFuyQSIyUt63EcYM8xIyfPAimSZu4oCnwzYVitVhYNAIqMKiApigKrzQw+RQV4nkPdoZfx/Et2jI+PY3Z2FsSL4zTTLYn8Cmw2B7xeLx4+fEiXvEYVYOB4ftGVKQj/zclyTE1N4cCBL3Hz5k0mBFWUdEGK0/2iKBKHX1NSgOdNSEF+vNtYjDt37qCurg6hUKgJwM9qYUm4TxJEXRcAEEpJAbI+WdXQLhywuaoQLS0tJPx+l8v1vSAIrwJ4QxUi0ybCqzwGAfxpWAHOoPufKbJhYWEBbrebfv5GwjudzqHKykqYTKaMmyCFEfEg/qOjo69HlTAQQsaSeE2BlcUpNR4A8wDKcnNzUV5ejuLiYpYLmYByQZIkxmF0dLTSkAKLIWRsVzmy2P7J2ipfSyAQgMfjwdVRLq0qtBwK3nvfCYvFQldt0b8SK8BRCBljT2vjIEYiEWb5YDDzLm618swD4TDjFUvm5CFkMIl5nXJFCoQlbQUc2RbIcgSBBQlcEkvxJm1RE5c31gcMuiDBOtVqMWzYUIAq4QXcvn0bd+/eh9lsxr//AHM+my4PvYaaJISMK5DIgmoTYti48Vkm/LFjx9Df388qS0lJCaqrq8FxBbjv0fYkx2tXscTSkVCpkJ4CYTFG1dvW4/jx4zh16lTH/Px8YSAQKHS73R19fX0oKFxAMBRctj5K8V40pABVDuYFQ6SfK6IYZpSdbWG94vTp03R5n8vlmiEC8AnNTRMTE8jK4mPr40kLiXOAhDJa/hIqsFg0JGnZdktv4MgA1Dd83nBs/VKYTOmE0P+EaBhMe7xwOByora0lxkcEQSggAvBDWVkZ6BA1MzunGUKiTggl9ICikhEkWrfU/T09V9HY2Egm39Pf308PDFBVVYWamhoMDk5CFCVtQc3aHs5kSjQMsmAUF1zXWV/Yu3cvdu3axa6GQiF0/z6CGzc8uiwt2no9KQ+Iy36fP38NFy5cR36+A7KswOud13u+EIPFkoYHFCiQDaqgaKyLHkb0SuD0tKh5XQuiqJ2uiRVQFCiKsTFYY9q0kvDsOMnLMJsyqxfUH6hjpzTMLSpgzAM0hFJXpQGOKiZNEOQBmkgPffFWjF+6IF6UKykNc2R92eCefp8Eq9WOrKwsalbZAC7Pzc3h4sWLuHLlyv9yHsjLy2MPCtRTWXIFZEWBbDCEZu6H4HAUsVo+MDCw0+VytQuCUDE1NbVyR0qWwoqxTkyRc+2veezevRvDw8NtgiAo6qH+7xU71C+GkPGT1MkuD1o6X0RXVxdNm+1ut7udQkc9paWF6Iylxn9WvBJJ+4DREAJrWMDnDeOob34Ohw8fZglHZ+RMngvZ7XYmfGdnJ3p7ez8E0GlEATNpLgYlKEpqm4fDCjq+nWSznSPHhNw8EyKR9BKY8t43I+Hrn5zIycmhS2sfEVTnXpGsp/ALkBVHWpuT+/xzMvxzmZ2H16w1s07+4MED+jkd/7+eeQfv3bsH4Z0QZFliubASREXnsyPrMTIygqGhIZLrj3hB9TzQcevWrdqxsTHs3LcZPb8Adkc2zDSPZFbODSEclqHIPD5tK2E5dOLECfj9/u3x7wYSKXDO5/O19fb27qenwq+9XQqLxZ9SZ84UdG44c+YMuru7ob5iOpfKK6bo15V+yXdJfcn3SOhEXzGtYhWreJoB4D9CrzrJ8WeKXgAAAABJRU5ErkJggg==',
rdns: 'clear-wallet.flashsoft.eu/',
}
function loadEIP1193Provider(provider: any) {
function announceProvider() {
const info: EIP6963ProviderInfo = ProviderInfo
window.dispatchEvent(
new CustomEvent("eip6963:announceProvider", {
detail: Object.freeze({ info, provider }),
})
);
}
window.addEventListener(
"eip6963:requestProvider",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(event: any) => {
announceProvider();
}
);
announceProvider();
}
const listners = {
accountsChanged: new Set<(p?: any) => void>(),
connect: new Set<(p?: any) => void>(),
@ -36,13 +72,13 @@ const getListnersCount = (): number => {
const sendMessage = (args: RequestArguments, ping = false) => {
if(Object.values(promResolvers).filter(r=> r).length < 10 ) {
return new Promise((resolve, reject) => {
const resId = crypto.randomUUID()
const resId = [...`${Math.random().toString(16) + Date.now().toString(16)}`].slice(2).join('')
promResolvers.set(resId, { resolve, reject })
const data = { type: "CLWALLET_CONTENT", data: args, resId};
if (ping) {
data.type = 'CLWALLET_PING'
}
// console.log('data in', data)
// console.info('data in', data)
window.postMessage(data, "*");
})
} else {
@ -80,7 +116,7 @@ class MetaMaskAPI {
_events: {}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(4)
}
isConnected() {
return false
return true
}
// for maximum compatibility since is cloning the same API
@ -129,7 +165,7 @@ class MetaMaskAPI {
} else if (typeof arg1 === 'object') {
return sendMessage(arg1 as RequestArguments)
} else {
console.error('Clear Wallet: faulty request')
console.info('ERROR: Clear Wallet: faulty request')
}
}else if( typeof arg1 === 'string' ) {
return sendMessage({
@ -272,37 +308,22 @@ class MetaMaskAPI {
_handleStreamDisconnect() { return true }
_handleUnlockStateChanged() { return true }
_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(), {
// 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 },
})
const listner = function(event: any) {
if (event.source != window) return;
if (event.data.type && (event.data.type === "CLWALLET_PAGE")) {
if (event?.data?.type === "CLWALLET_PAGE") {
try {
if(event?.data?.data?.error){
promResolvers.get(event.data.resId)?.reject(event.data.data);
console.error(event?.data?.data)
console.info('Error: ', event?.data?.data)
}else {
promResolvers.get(event.data.resId)?.resolve(event.data.data);
}
@ -310,7 +331,7 @@ const listner = function(event: any) {
} catch (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 ) {
try {
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).isConnected = () => true;
} 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).chainId = event?.data?.data?.data ?? '0x89';
} else if ( listnerName === 'accountsChanged' ) {
@ -332,7 +353,7 @@ const listner = function(event: any) {
listners.once[listnerName].delete(listner)
});
} catch (e) {
// console.error(e)
// console.info(e)
// ignore
}
}
@ -341,43 +362,46 @@ const listner = function(event: any) {
window.addEventListener("message",listner)
// const proxy1 = new Proxy({
// // on: (event: any, callback:any) => { if (event === 'message') {
// // debugger;
// // callback(true, true)
// // } }
// }, {
// get: function(target, name, receiver) {
// if (typeof (<any>target)[name] == 'function') {
// return function (...args: any) {
// console.dir({ call: [name, ...args] });
// }
// }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const proxy1 = new Proxy(new MetaMaskAPI(), {
get: function (target: any, prop: any) {
// Intercept method calls and log them
if (typeof target[prop] === 'function') {
return function (...args: any[]) {
console.log(`Calling ${prop} with arguments:`, args);
// eslint-disable-next-line prefer-spread
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);
// return undefined
// }
// })
const web3Shim = {
currentProvider: eth,
__isMetaMaskShim__: true
}
// const web3Shim = {
// currentProvider: eth,
// __isMetaMaskShim__: true
// }
const injectWallet = (win: any) => {
Object.defineProperty(win, 'ethereum', {
value: eth,
});
Object.defineProperty(win, 'web3', {
value: web3Shim
value: eth
});
sendMessage({
method: 'wallet_ready'
}, true)
// console.log('Clear wallet injected', (window as any).ethereum, win)
console.log('Clear wallet injected', (window as any).ethereum, win)
}
injectWallet(this);
loadEIP1193Provider(eth)
// setTimeout(() => {
// // console.log('Metamask clone test');

View File

@ -3,8 +3,8 @@
"name": "__MSG_appName__",
"description": "__MSG_appDesc__",
"default_locale": "en",
"version": "1.2.8",
"version_name": "1.2.8",
"version": "1.3.0",
"version_name": "1.3.0",
"icons": {
"16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png",
@ -23,11 +23,14 @@
"minimum_chrome_version": "103",
"permissions": [
"notifications",
"activeTab",
"storage",
"alarms",
"unlimitedStorage",
"clipboardRead",
"clipboardWrite"
"clipboardWrite",
"contextMenus",
"scripting"
],
"host_permissions": [
"*://*/*"
@ -53,12 +56,17 @@
],
"all_frames": true,
"run_at": "document_start",
"js": ["/src/extension/metamask-stub.js"],
"js": ["/src/extension/inject.ts"],
"world": "MAIN"
}
],
"web_accessible_resources": [{
"resources": ["src/extension/inject.js"],
"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 { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest'
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber, evmCall, getTxByHash, getTxReceipt, signTypedData, getCode, getTxCount } from '@/utils/wallet'
import {
CLW_CONTEXT_MENU_ID,
getSelectedAccount,
getSelectedNetwork,
smallRandomString,
getSettings,
clearPk,
openTab,
getUrl,
addToHistory,
getNetworks,
strToHex,
numToHexStr,
enableRightClickVote,
} from '@/utils/platform';
import {
userApprove,
userReject,
rIdWin,
rIdData,
} from '@/extension/userRequest'
import {
signMsg,
getBalance,
getBlockNumber,
estimateGas,
sendTransaction,
getGasPrice,
getBlockByNumber,
evmCall,
getTxByHash,
getTxReceipt,
signTypedData,
getCode,
getTxCount,
getSelectedAddress
} from '@/utils/wallet'
import type { RequestArguments } from '@/extension/types'
import { rpcError } from '@/extension/rpcConstants'
import { updatePrices } from '@/utils/gecko'
@ -9,23 +43,58 @@ import { mainNets, testNets } from '@/utils/networks'
let notificationUrl: string
chrome.runtime.onInstalled.addListener(() => {
console.log('Service worker installed');
enableRightClickVote()
console.info('Service worker installed');
})
chrome.runtime.onStartup.addListener(() => {
console.log('Service worker startup');
console.info('Service worker startup');
enableRightClickVote();
if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message);
}
})
chrome.runtime.onSuspend.addListener(() => {
console.log('Service worker suspend');
console.info('Service worker suspend');
if(chrome.runtime.lastError) {
console.warn("Whoops.. " + chrome.runtime.lastError.message);
}
})
async function pasteAddress() {
const currentAddress = (await (window as any).ethereum?.request({
method: 'eth_accounts',
params: []
}))
if(currentAddress.length > 0) {
document.execCommand("insertText", false, currentAddress[0]);
}
}
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const extensionId = chrome.runtime.id
const isOwnExtension = info?.pageUrl?.startsWith(`chrome-extension://${extensionId}`)
if (info.menuItemId === CLW_CONTEXT_MENU_ID && tab?.id && !isOwnExtension) {
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN',
func: pasteAddress
});
} catch {
// igonre
}
} else if(isOwnExtension) {
chrome.runtime.sendMessage({ method: 'paste', type: 'CLWALLET_PAGE_MSG' }, (r) => {
if (chrome.runtime.lastError) {
console.warn("LOC3: Error sending message:", chrome.runtime.lastError);
}
return r
})
}
})
chrome.alarms.create('updatePrices', {
periodInMinutes: 1
@ -34,7 +103,9 @@ chrome.alarms.create('updatePrices', {
chrome.alarms.onAlarm.addListener((alarm) => {
if(alarm.name === 'updatePrices') {
updatePrices().then(() => {
console.log('Prices updated')
console.info('Prices updated')
}).catch((err) => {
console.warn('Prices update failed', err)
})
}
getSettings().then((settings) => {
@ -77,9 +148,15 @@ if (!chrome.notifications.onButtonClicked.hasListener(viewTxListner)){
}
const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: any) => any) => {
if (chrome.runtime.lastError) {
console.info("Error receiving message:", chrome.runtime.lastError);
}
if(message?.type !== "CLWALLET_CONTENT_MSG") {
return true
}
console.info('main listener', message);
(async () => {
if (!(message?.method)) {
sendResponse({
@ -154,7 +231,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
case 'eth_gasPrice': {
try {
sendResponse(strToHex(String(await getGasPrice() ?? 0)))
sendResponse(numToHexStr(BigInt(Math.trunc(await getGasPrice() * 1e9))))
} catch {
sendResponse({
error: true,
@ -166,7 +243,9 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
case 'eth_getBalance': {
try {
sendResponse(await getBalance())
const balance = await getBalance()
const balanceHex = numToHexStr(balance ?? 0n)
sendResponse(balanceHex)
} catch {
sendResponse({
error: true,
@ -217,7 +296,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
data: params?.data ?? '',
value: params?.value ?? '0x0'
})
const gasHex = strToHex(String(gas ?? 0))
const gasHex = numToHexStr(gas ?? 0n)
sendResponse(gasHex)
} catch(err) {
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_accounts': {
try {
// give only the selected address for better privacy
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse(address)
sendResponse(await getSelectedAddress())
} catch {
sendResponse({
error: true,
@ -303,13 +379,6 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
params.from = account.address
const serializeParams = strToHex(JSON.stringify(params)) ?? ''
const pEstimateGas = estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
})
const pGasPrice = getGasPrice()
let gWin: any
await new Promise((resolve, reject) => {
chrome.windows.create({
@ -327,7 +396,9 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
})
try {
const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) }, pEstimateGas, pGasPrice )
// console.log('waiting for user to approve or reject')
// console.log(rIdData?.[String(gWin?.id ?? 0)])
const tx = await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) } )
sendResponse(tx.hash)
const buttons = {} as any
const network = await getSelectedNetwork()
@ -452,7 +523,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
clearPk()
}
} catch (e) {
// console.error(e)
// console.info(e)
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
@ -570,7 +641,7 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
})
sendResponse(null)
} catch (err) {
console.log('err')
console.error('err')
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
@ -612,7 +683,8 @@ const mainListner = (message: RequestArguments, sender:any, sendResponse: (a: an
}
case 'wallet_send_data': {
if(String(sender.tab?.windowId) in rIdData){
rIdData[String(sender?.tab?.windowId ?? '')] = (message as any)?.data ?? {}
const intData = rIdData[String(sender?.tab?.windowId ?? '')] ?? {}
rIdData[String(sender?.tab?.windowId ?? '')] = {...intData, ...(message?.data ?? {})}
sendResponse(true)
}
break

View File

@ -8,9 +8,12 @@ export interface Network {
explorer?: string
}
export interface Account {
export interface Contact {
name: string
address: string
}
export interface Account extends Contact {
pk: string
encPk: string
}
@ -65,3 +68,15 @@ export interface HistoryItem {
webiste?: string
txHash: string
}
export interface ContractAction {
name: string
contract: string
abi: string
functionName: string
params: any[]
}
export interface ContractActions {
[key: string] : ContractAction
}

View File

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

View File

@ -79,6 +79,19 @@ const routes: Array<RouteRecordRaw> = [
path: 'add-network/edit/:chainId',
component: () => import('@/views/AddNetwork.vue'),
},
{
path: 'send-token',
component: () => import('@/views/SendToken.vue'),
},
{
path: 'read-contract',
component: () => import('@/views/ReadContract.vue'),
},
{
path: 'write-contract',
component: () => import('@/views/WriteContract.vue'),
},
],
},
]

View File

@ -22,7 +22,7 @@ export const mainNets: {[key: number]: Network} = {
},
100: {
name: 'Gnosis',
rpc: 'https://rpc.gnosischain.com/',
rpc: 'https://rpc.gnosischain.com',
chainId: 100,
explorer: 'https://gnosisscan.io',
icon:'xdai.webp',
@ -56,6 +56,15 @@ export const mainNets: {[key: number]: Network} = {
symbol: 'ETH',
priceId: 'ethereum'
},
8453: {
name: 'Base Mainnet',
rpc: 'https://base.publicnode.com',
chainId: 8453,
explorer: 'https://basescan.org',
icon: 'base.webp',
symbol: 'ETH',
priceId: 'ethereum'
}
}
export const testNets = {
@ -77,7 +86,7 @@ export const testNets = {
name: 'TESTNET Polygon',
rpc: 'https://rpc.ankr.com/polygon_mumbai',
chainId: 80001,
explorer: 'https://mumbai.polygonscan.com/',
explorer: 'https://mumbai.polygonscan.com',
icon:'polygon.webp'
},
100100: {
@ -91,21 +100,21 @@ export const testNets = {
name: 'TESTNET Optimism Goreli',
rpc: 'https://goerli.optimism.io/',
chainId: 420,
explorer: 'https://goerli.etherscan.io/',
explorer: 'https://goerli.etherscan.io',
icon: 'optimism.webp'
},
97: {
name: 'TESTNET BSC',
rpc: 'https://bsctestapi.terminet.io/rpc',
chainId: 97,
explorer: 'https://testnet.bscscan.com/',
explorer: 'https://testnet.bscscan.com',
icon: 'binance.webp'
},
421613: {
name: 'TESTNET Arbitrum One',
rpc: 'https://goerli-rollup.arbitrum.io/rpc/',
chainId: 421613,
explorer: 'https://testnet.arbiscan.io/',
explorer: 'https://testnet.arbiscan.io',
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'
const defaultSettings = {
@ -11,6 +11,12 @@ const defaultSettings = {
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> =>{
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[]> => {
return (await storageGet('accounts')).accounts ?? [] as Account[]
}
export const saveAccount = async (account: Account): Promise<void> => {
const savedAccounts = await getAccounts()
await storageSave('accounts', [account, ...savedAccounts])
@ -62,6 +80,7 @@ export const replaceAccounts = async (accounts: Account[]): Promise<void> => {
await storageSave('accounts', accounts)
}
export const getSelectedAccount = async (): Promise<Account> => {
return (await storageGet('selectedAccount'))?.selectedAccount ?? null as unknown as Account
}
@ -106,6 +125,80 @@ export const setSettings = async (settings: Settings): Promise<void> => {
await storageSave('settings', settings )
}
export const getAllAbis = async (): Promise<{ [key: string]: string }> => {
return ((await storageGet('abis'))?.abis) ?? defaultAbis
}
export const getAbis = async (name: string): Promise<string> => {
return (await getAllAbis())?.[name] ?? ''
}
export const setAbi = async ({
name ,
content
}: {
name: string
content: string
}): Promise<void> => {
const abis = await getAllAbis() || defaultAbis
await storageSave('abis', { ...abis, [name]: content })
}
export const setAbis = async (abis: { [key: string]: string }): Promise<void> => {
await storageSave('abis', abis)
}
export const removeAllAbis = async (): Promise<void> => {
await storageSave('abis', defaultAbis)
}
export const readCAGetAll = async (): Promise<ContractActions> => {
return ((await storageGet('read-actions'))?.['read-actions'] ?? {}) as ContractActions
}
export const readCAGet = async (action: string): Promise<ContractAction | undefined> => {
return ((await readCAGetAll())?.[action]) as ContractAction
}
export const readCASet = async (action: ContractAction): Promise<void> => {
const actions = await readCAGetAll()
await storageSave('read-actions', { ...actions, [action.name]: action })
}
export const readCARemove = async (action: string): Promise<void> => {
const actions = await readCAGetAll()
delete actions[action]
await storageSave('read-actions', actions)
}
export const readCAWipe = async (): Promise<void> => {
await storageSave('read-actions', {})
}
export const writeCAGetAll = async (): Promise<ContractActions> => {
return ((await storageGet('write-actions'))?.['write-actions'] ?? {}) as ContractActions
}
export const writeCAGet = async (action: string): Promise<ContractAction | undefined> => {
return ((await writeCAGetAll())?.[action]) as ContractAction
}
export const writeCASet = async (action: ContractAction): Promise<void> => {
const actions = await writeCAGetAll()
await storageSave('write-actions', { ...actions, [action.name]: action })
}
export const writeCARemove = async (action: string): Promise<void> => {
const actions = await writeCAGetAll()
delete actions[action]
await storageSave('write-actions', actions)
}
export const writeCAWipe = async (): Promise<void> => {
await storageSave('write-actions', {})
}
export const blockLockout = async (): Promise<Settings> => {
const settings = await getSettings()
settings.lockOutBlocked = true
@ -128,15 +221,6 @@ export const setBalanceCache = async (balance: string): Promise<void> => {
await storageSave('balance', balance )
}
export const getRandomPk = () => {
const array = new Uint32Array(10);
crypto.getRandomValues(array)
return array.reduce(
(pv, cv) => `${pv}${cv.toString(16)}`,
'0x'
).substring(0, 66)
}
export const smallRandomString = (size = 7) => {
if(size <= 7) {
return (Math.random() + 1).toString(36).substring(0,7);
@ -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 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>) => {
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) => {
chrome.tabs.create({
url

View File

@ -1,7 +1,5 @@
import { getSelectedAccount, getSelectedNetwork } from '@/utils/platform';
import { ethers } from "ethers"
import { strToHex } from '@/utils/platform';
import { ethers} from "ethers"
export const signMsg = async (msg: string) => {
const account = await getSelectedAccount()
@ -35,8 +33,8 @@ export const getGasPrice = async () => {
const network = await getSelectedNetwork()
const provider = new ethers.JsonRpcProvider(network.rpc)
const feed = await provider.getFeeData()
const gasPrice = feed.gasPrice ?? feed.maxFeePerGas
return gasPrice
const gasPrice = feed.maxFeePerGas ?? feed.gasPrice ?? 0n
return Number(gasPrice) / 1e9
}
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'}:
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string},
gasEstimate: Promise<bigint> | null = null, pGasPrice : Promise<bigint | null> | null) => {
export const getRandomPk = () => {
return ethers.Wallet.createRandom().privateKey
}
export const getCurrentProvider = async () => {
const network = await getSelectedNetwork()
return new ethers.JsonRpcProvider(network.rpc)
}
export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='', gasPrice='0x0'}:
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string}) => {
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.JsonRpcProvider(network.rpc))
if(gas === '0x0') {
if(!gasEstimate){
throw new Error('No gas estimate available')
}else {
gas = (await gasEstimate).toString()
}
}
const gasPriceInt = BigInt(gasPrice)
const gasInt = BigInt(gas)
if(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) {
if(gas === '0x0' || gasPrice === '0x0') {
throw new Error('No gas estimate available')
}
return await wallet.sendTransaction({to, from, data, value, gasLimit: gas, gasPrice})
return await wallet.sendTransaction({
to,
from,
data: data ? data : null,
value: value ? value : null,
gasLimit: gasInt,
gasPrice: null,
maxFeePerGas: gasPriceInt,
})
}
export const formatBalance = (balance: string) => {
@ -135,3 +133,10 @@ export const formatBalance = (balance: string) => {
maximumFractionDigits: 6
}).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-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
@ -54,7 +55,12 @@
<ion-item @click="copyAddress(shownPk, getToastRef())" button>
<ion-icon style="margin-right: 0.5rem" :icon="copyOutline" />
<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-content>
</ion-modal>

View File

@ -9,8 +9,7 @@
<ion-content class="ion-padding">
<ion-item>
<ion-label>Name</ion-label>
<ion-input label="name" v-model="name"></ion-input>
<ion-input label="Name" labelPlacement="stacked" v-model="name"></ion-input>
</ion-item>
<ion-item>
<ion-label>Get Random Name</ion-label>
@ -23,8 +22,12 @@
:icon="clipboardOutline"
button
/>
<ion-label button>PK</ion-label>
<ion-input label="pk" id="pastePk" v-model="pk"></ion-input>
<ion-input
label="PK"
labelPlacement="stacked"
id="pastePk"
v-model="pk"
></ion-input>
</ion-item>
<template v-if="!isEdit">
<ion-item>
@ -33,7 +36,7 @@
</ion-item>
<ion-item>
<ion-button @click="mnemonicModal = true" expand="full"
>Extarct From A Mnemonic</ion-button
>Extract From A Mnemonic</ion-button
>
</ion-item>
</template>
@ -67,7 +70,7 @@
<ion-item>
<ion-textarea
style="overflow-y: scroll"
label="Enter mnemonic"
aria-label="Enter mnemonic"
:rows="10"
:cols="10"
v-model="mnemonic"
@ -75,9 +78,12 @@
</ion-item>
<ion-item>
<ion-label>Enter Index (default: 0)</ion-label>
<ion-input label="mnemonic index" v-model="mnemonicIndex"></ion-input>
</ion-item>
<ion-item>
<ion-input aria-label="mnemonic index" v-model="mnemonicIndex"></ion-input>
</ion-item>
<ion-item>
<ion-button @click="mnemonicModal = false" color="light">Close</ion-button>
<ion-button @click="extractMnemonic">Extract</ion-button>
</ion-item>
</ion-content>
@ -111,7 +117,6 @@ import {
saveSelectedAccount,
getAccounts,
saveAccount,
getRandomPk,
smallRandomString,
paste,
getSettings,
@ -123,7 +128,7 @@ import UnlockModal from "@/views/UnlockModal.vue";
import { encrypt, getCryptoParams } from "@/utils/webCrypto";
import { clipboardOutline } from "ionicons/icons";
import { getFromMnemonic } from "@/utils/wallet";
import { getFromMnemonic, getRandomPk } from "@/utils/wallet";
export default defineComponent({
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
>
<ion-item>
<ion-label>Name(*)</ion-label>
<ion-input label="name" v-model="name" placeholder="ex: Polygon"></ion-input>
<ion-input
label="Name(*)"
labelPlacement="stacked"
v-model="name"
placeholder="ex: Polygon"
></ion-input>
</ion-item>
<ion-item>
<ion-label>ChainId(*)</ion-label>
<ion-input
label="chainid"
label="ChainId(*)"
labelPlacement="stacked"
v-model="chainId"
placeholder="137"
type="number"
@ -24,19 +28,18 @@
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>RPC URL(*)</ion-label>
<ion-input
label="rpc"
label="RPC URL(*)"
labelPlacement="stacked"
id="pasteRpc"
placeholder="https://polygon-mainnet.g.alchemy.com/..."
v-model="rpc"
></ion-input>
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>Native Token Symbol(?)</ion-label>
<ion-input
label="native token"
label="Native Token Symbol"
labelPlacement="stacked"
id="native-token"
placeholder="MATIC"
v-model="symbol"
@ -44,9 +47,9 @@
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteExplorer')" />
<ion-label>Explorer(?)</ion-label>
<ion-input
label="explorer"
label="Explorer"
labelPlacement="stacked"
id="pasteExplorer"
placeholder="https://polygonscan.com"
v-model="explorer"

View File

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

View File

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

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-textarea
style="overflow-y: scroll"
label="Error"
aria-label="Error"
:rows="10"
:cols="20"
:value="error"

View File

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

View File

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

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-label>Name:</ion-label>
<ion-input
label="Name"
aria-label="Name"
style="margin-left: 0.5rem"
v-model="name"
readonly
@ -32,7 +32,7 @@
<ion-item>
<ion-label>ChainId: </ion-label>
<ion-input
label="ChainId"
aria-label="ChainId"
style="margin-left: 0.5rem"
v-model="chainId"
readonly
@ -42,7 +42,7 @@
<ion-item button>
<ion-label>RPC URL: </ion-label>
<ion-input
label="RPC URL"
aria-label="RPC URL"
style="margin-left: 0.5rem"
readonly
placeholder="https://polygon-mainnet.g.alchemy.com/..."
@ -52,7 +52,7 @@
<ion-item button>
<ion-label>Native Token Symbol: </ion-label>
<ion-input
label="Native Token Symbol"
aria-label="Native Token Symbol"
style="margin-left: 0.5rem"
readonly
placeholder="MATIC"
@ -62,7 +62,7 @@
<ion-item button>
<ion-label>Explorer: </ion-label>
<ion-input
label="Explorer"
aria-label="Explorer"
style="margin-left: 0.5rem"
readonly
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-label>Enable Storage Encryption</ion-label>
<ion-toggle
aria-label="Enable Storage Encryption"
:key="updateKey"
@ion-change="changeEncryption"
slot="end"
@ -33,6 +34,7 @@
<ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label>
<ion-toggle
aria-label="Enable Auto Lock"
:key="updateKey"
@ion-change="changeAutoLock"
slot="end"
@ -70,6 +72,7 @@
<ion-item>
<ion-label>Permanent Lock</ion-label>
<ion-toggle
aria-label="Permanent Lock"
@ion-change="changePermaLock"
:key="updateKey"
slot="end"
@ -180,6 +183,7 @@
</ion-accordion>
</ion-accordion-group>
<ion-toast
position="top"
:is-open="toastState"
@didDismiss="toastState = false"
:message="toastMsg"
@ -228,7 +232,11 @@
<ion-label>Old Password</ion-label>
</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-list>
<div v-else>
@ -237,7 +245,11 @@
<ion-label>New Password</ion-label>
</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-list>
<ion-list>
@ -246,7 +258,7 @@
</ion-item>
<ion-item>
<ion-input
label="password"
aria-label="password"
v-model="mpConfirm"
type="password"
></ion-input>

View File

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

View File

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

View File

View File

@ -31,7 +31,7 @@
<ion-label>Unlock Password</ion-label>
</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-list>
<ion-item>

View File

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

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

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

View File

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