dev: 1.0.1

This commit is contained in:
Andrei O 2022-10-07 20:07:59 +03:00
parent 3e97999011
commit 9932a1a522
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
49 changed files with 8715 additions and 32540 deletions

29
index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en" style="width:400px;height:450px">
<head>
<meta charset="utf-8" />
<title>Clear Wallet</title>
<base href="/" />
<meta name="color-scheme" content="light dark" />
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Clear Wallet" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

32166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,46 +3,47 @@
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint"
"dev": "vite",
"build": "tsc --out src/extension/inject.js src/extension/inject.ts && vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@capacitor/app": "4.0.1",
"@capacitor/core": "4.1.0",
"@capacitor/haptics": "4.0.1",
"@capacitor/keyboard": "4.0.1",
"@capacitor/status-bar": "4.0.1",
"@ionic/vue": "^6.0.0",
"@ionic/vue-router": "^6.0.0",
"core-js": "^3.6.5",
"vue": "^3.2.21",
"vue-router": "^4.0.12"
"@capacitor/app": "^4.0.1",
"@capacitor/core": "^4.3.0",
"@capacitor/haptics": "^4.0.1",
"@capacitor/keyboard": "^4.0.1",
"@capacitor/status-bar": "^4.0.1",
"@ionic/vue": "^6.3.0",
"@ionic/vue-router": "^6.3.0",
"@types/chrome": "^0.0.197",
"core-js": "^3.25.2",
"ethers": "^5.7.1",
"vue": "^3.2.39",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@capacitor/cli": "4.1.0",
"@types/jest": "^27.0.2",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
"@vue/cli-plugin-router": "~5.0.0-rc.1",
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
"@vue/cli-plugin-unit-jest": "~5.0.0-rc.1",
"@vue/cli-service": "~5.0.0-rc.1",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "^2.0.0-rc.16",
"@vue/vue3-jest": "^27.0.0-alpha.3",
"babel-jest": "^27.3.1",
"cypress": "^8.7.0",
"eslint": "^8.4.1",
"eslint-plugin-vue": "^8.2.0",
"jest": "^27.3.1",
"ts-jest": "^27.0.7",
"typescript": "^4.3.5"
"@capacitor/cli": "^4.3.0",
"@crxjs/vite-plugin": "^1.0.14",
"@types/jest": "^29.0.3",
"@types/node": "^18.7.19",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"@vitejs/plugin-vue": "^3.1.0",
"@vue/eslint-config-typescript": "^11.0.2",
"eslint": "^8.23.1",
"eslint-plugin-vue": "^9.5.1",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"jest": "^29.0.3",
"rollup-plugin-polyfill-node": "^0.10.2",
"sass": "^1.55.0",
"stream-browserify": "^3.0.0",
"ts-jest": "^29.0.1",
"typescript": "^4.8.3",
"util": "^0.12.4",
"vite": "^3.1.3",
"vue-tsc": "^0.40.13",
"yarn-upgrade-all": "^0.7.1"
},
"description": "An Ionic project"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" style="width:400px;height:450px">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<title>Clear Wallet</title>
<base href="/" />
@ -14,16 +14,17 @@
<meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" />
<link rel="shortcut icon" type="image/png" href="<%= BASE_URL %>assets/icon/favicon.png" />
<!-- <link rel="shortcut icon" type="image/png" href="<%= BASE_URL %>assets/icon/favicon.png" /> -->
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Ionic App" />
<meta name="apple-mobile-web-app-title" content="Clear Wallet" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,248 +1,50 @@
<template>
<ion-app>
<ion-split-pane content-id="main-content">
<ion-menu content-id="main-content" type="overlay">
<ion-content>
<ion-list id="inbox-list">
<ion-list-header>Inbox</ion-list-header>
<ion-note>hi@ionicframework.com</ion-note>
<ion-menu-toggle auto-hide="false" v-for="(p, i) in appPages" :key="i">
<ion-item @click="selectedIndex = i" router-direction="root" :router-link="p.url" lines="none" detail="false" class="hydrated" :class="{ selected: selectedIndex === i }">
<ion-icon slot="start" :ios="p.iosIcon" :md="p.mdIcon"></ion-icon>
<ion-label>{{ p.title }}</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<ion-list id="labels-list">
<ion-list-header>Labels</ion-list-header>
<ion-item v-for="(label, index) in labels" lines="none" :key="index">
<ion-icon slot="start" :ios="bookmarkOutline" :md="bookmarkSharp"></ion-icon>
<ion-label>{{ label }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
<ion-router-outlet />
</ion-app>
</template>
<script lang="ts">
import { IonApp, IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote, IonRouterOutlet, IonSplitPane } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
import { useRoute } from 'vue-router';
import { archiveOutline, archiveSharp, bookmarkOutline, bookmarkSharp, heartOutline, heartSharp, mailOutline, mailSharp, paperPlaneOutline, paperPlaneSharp, trashOutline, trashSharp, warningOutline, warningSharp } from 'ionicons/icons';
import { IonApp, IonRouterOutlet } from "@ionic/vue";
import { defineComponent } from "vue";
import { useRoute, useRouter} from "vue-router";
export default defineComponent({
name: 'App',
name: "App",
components: {
IonApp,
IonContent,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenu,
IonMenuToggle,
IonNote,
IonRouterOutlet,
IonSplitPane,
IonApp,
IonRouterOutlet,
},
setup() {
const selectedIndex = ref(0);
const appPages = [
{
title: 'Inbox',
url: '/folder/Inbox',
iosIcon: mailOutline,
mdIcon: mailSharp
},
{
title: 'Outbox',
url: '/folder/Outbox',
iosIcon: paperPlaneOutline,
mdIcon: paperPlaneSharp
},
{
title: 'Favorites',
url: '/folder/Favorites',
iosIcon: heartOutline,
mdIcon: heartSharp
},
{
title: 'Archived',
url: '/folder/Archived',
iosIcon: archiveOutline,
mdIcon: archiveSharp
},
{
title: 'Trash',
url: '/folder/Trash',
iosIcon: trashOutline,
mdIcon: trashSharp
},
{
title: 'Spam',
url: '/folder/Spam',
iosIcon: warningOutline,
mdIcon: warningSharp
}
];
const labels = ['Family', 'Friends', 'Notes', 'Work', 'Travel', 'Reminders'];
const path = window.location.pathname.split('folder/')[1];
if (path !== undefined) {
selectedIndex.value = appPages.findIndex(page => page.title.toLowerCase() === path.toLowerCase());
}
const route = useRoute();
return {
selectedIndex,
appPages,
labels,
archiveOutline,
archiveSharp,
bookmarkOutline,
bookmarkSharp,
heartOutline,
heartSharp,
mailOutline,
mailSharp,
paperPlaneOutline,
paperPlaneSharp,
trashOutline,
trashSharp,
warningOutline,
warningSharp,
isSelected: (url: string) => url === route.path ? 'selected' : ''
}
setup () {
const route = useRoute()
const router = useRouter()
const { param, rid } = route.query;
console.log(route?.query,'zzzzzzzzzzzzzzz')
switch (route?.query?.route ?? "") {
case "sign-msg": {
router.push({
path: `/sign-msg/${rid}/${param}`
});
break;
}
case "sign-tx": {
router.push({
path: `/sign-tx/${rid}/${param}`
});
break;
}
case "switch-network": {
router.push({
path: `/switch-network/${rid}/${param}`
});
break;
}
default: {
router.push({ path: "/", })
}
}
}
});
</script>
<style scoped>
ion-menu ion-content {
--background: var(--ion-item-background, var(--ion-background-color, #fff));
}
ion-menu.md ion-content {
--padding-start: 8px;
--padding-end: 8px;
--padding-top: 20px;
--padding-bottom: 20px;
}
ion-menu.md ion-list {
padding: 20px 0;
}
ion-menu.md ion-note {
margin-bottom: 30px;
}
ion-menu.md ion-list-header,
ion-menu.md ion-note {
padding-left: 10px;
}
ion-menu.md ion-list#inbox-list {
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}
ion-menu.md ion-list#inbox-list ion-list-header {
font-size: 22px;
font-weight: 600;
min-height: 20px;
}
ion-menu.md ion-list#labels-list ion-list-header {
font-size: 16px;
margin-bottom: 18px;
color: #757575;
min-height: 26px;
}
ion-menu.md ion-item {
--padding-start: 10px;
--padding-end: 10px;
border-radius: 4px;
}
ion-menu.md ion-item.selected {
--background: rgba(var(--ion-color-primary-rgb), 0.14);
}
ion-menu.md ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.md ion-item ion-icon {
color: #616e7e;
}
ion-menu.md ion-item ion-label {
font-weight: 500;
}
ion-menu.ios ion-content {
--padding-bottom: 20px;
}
ion-menu.ios ion-list {
padding: 20px 0 0 0;
}
ion-menu.ios ion-note {
line-height: 24px;
margin-bottom: 20px;
}
ion-menu.ios ion-item {
--padding-start: 16px;
--padding-end: 16px;
--min-height: 50px;
}
ion-menu.ios ion-item.selected ion-icon {
color: var(--ion-color-primary);
}
ion-menu.ios ion-item ion-icon {
font-size: 24px;
color: #73849a;
}
ion-menu.ios ion-list#labels-list ion-list-header {
margin-bottom: 8px;
}
ion-menu.ios ion-list-header,
ion-menu.ios ion-note {
padding-left: 16px;
padding-right: 16px;
}
ion-menu.ios ion-note {
margin-bottom: 8px;
}
ion-note {
display: inline-block;
font-size: 16px;
color: var(--ion-color-medium-shade);
}
ion-item.selected {
--color: var(--ion-color-primary);
}
</style>

33
src/extension/content.ts Normal file
View File

@ -0,0 +1,33 @@
const allowedMethods = {
'eth_accounts': true,
'eth_requestAccounts' : true,
'eth_chainId': true,
'personal_sign' : true,
'wallet_requestPermissions': true
}
window.addEventListener("message", (event) => {
if (event.source != window)
return;
if (event.data.type && (event.data.type == "CLWALLET_CONTENT")) {
event.data.data.resId = event.data.resId
console.log('data in', event?.data)
if(event?.data?.data?.method ?? 'x' in allowedMethods)
chrome.runtime.sendMessage(event.data.data, (res) => {
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId };
console.log('data back', data)
window.postMessage(data, "*");
})
}
});
(function() {
const script = document.createElement('script')
script.src = chrome.runtime.getURL('src/extension/inject.js')
document.documentElement.appendChild(script)
})()

227
src/extension/inject.js Normal file
View File

@ -0,0 +1,227 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var _this = this;
var listners = {
accountsChanged: new Set(),
connect: new Set(),
disconnect: new Set,
chainChanged: new Set()
};
var promResolvers = {};
var listner = function (event) {
var _a, _b;
if (event.source != window)
return;
if (event.data.type && (event.data.type == "CLWALLET_PAGE")) {
if ((_b = (_a = event === null || event === void 0 ? void 0 : event.data) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) {
promResolvers[event.data.resId].reject(event.data.data);
}
else {
promResolvers[event.data.resId].resolve(event.data.data);
}
promResolvers[event.data.resId] = undefined;
}
};
window.addEventListener("message", listner);
var sendMessage = function (args) {
return new Promise(function (resolve, reject) {
var resId = crypto.randomUUID();
promResolvers[resId] = { resolve: resolve, reject: reject };
var data = { type: "CLWALLET_CONTENT", data: args, resId: resId };
window.postMessage(data, "*");
});
};
// chainId
// :
// "0x89"
// enable
// :
// ƒ ()
// isMetaMask
// :
// true
// networkVersion
// :
// "137"
// request
// :
// ƒ ()
// selectedAddress
// :
// null
// send
// :
// ƒ ()
// sendAsync
// :
// ƒ ()
// _events
// :
// {connect: ƒ}
// _eventsCount
// :
// 1
// _handleAccountsChanged
// :
// ƒ ()
// _handleChainChanged
// :
// ƒ ()
// _handleConnect
// :
// ƒ ()
// _handleDisconnect
// :
// ƒ ()
// _handleStreamDisconnect
// :
// ƒ ()
// _handleUnlockStateChanged
// :
// ƒ ()
// _jsonRpcConnection
// :
// {events: s, stream: d, middleware: ƒ}
// _log
// :
// u {name: undefined, levels: {…}, methodFactory: ƒ, getLevel: ƒ, setLevel: ƒ, …}
// _maxListeners
// :
// 100
// _metamask
// :
// Proxy {isUnlocked: ƒ, requestBatch: ƒ}
// _rpcEngine
// :
// o {_events: {…}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(3)}
// _rpcRequest
// :
// ƒ ()
// _sendSync
// :
// ƒ ()
// _sentWarnings
// :
// {enable: false, experimentalMethods: false, send: false, events: {…}}
// _state
// :
// {accounts: Array(0), isConnected: true, isUnlocked: true, initialized: true, isPermanentlyDisconnected: false}
// _warnOfDeprecation
// :
// ƒ (
var eth = new Proxy({
isConnected: function () {
return true;
},
// for maximum compatibility since is cloning the same API
isMetaMask: true,
enable: function () {
return sendMessage({ method: 'eth_requestAccounts', params: Array(0) });
},
request: function (args) {
return sendMessage(args);
},
on: function (eventName, callback) {
switch (eventName) {
case 'accountsChanged':
listners.accountsChanged.add(callback);
break;
case 'connect':
listners.connect.add(callback);
break;
case 'disconnect':
listners.disconnect.add(callback);
break;
case 'chainChanged':
listners.chainChanged.add(callback);
break;
default:
return;
}
},
// Simulate Metamask
_warnOfDeprecation: function () { return null; },
_state: {},
_sentWarnings: function () { return null; },
_rpcRequest: function () { return null; },
_handleAccountsChanged: function () { return null; },
chainId: "0x89",
networkVersion: "137",
selectedAddress: null,
send: function () { return null; },
sendAsync: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
return [2 /*return*/, null];
}); }); },
_events: {},
_eventsCount: 0,
_handleChainChanged: function () { return null; },
_handleConnect: function () { return null; },
_handleDisconnect: function () { return null; },
_handleStreamDisconnect: function () { return null; },
_handleUnlockStateChanged: function () { return null; },
_jsonRpcConnection: {},
_log: {},
_maxListeners: 100,
_metamask: new Proxy({}, {}),
_rpcEngine: {}
}, {
set: function () { return false; },
deleteProperty: function () { return false; }
});
var injectWallet = function (win) {
Object.defineProperty(win, 'ethereum', {
get: function () {
return eth;
},
set: function () {
return;
}
});
window.tttest = 'test';
// Object.defineProperty(window, 'ethereum', 444)
console.log('Clear wallet injected', window.ethereum, win);
};
injectWallet(this);
// setTimeout(() => {
// console.log('Metamask clone test');
// // (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, '111111111')});
// }, 3500)
// console.log( (window as any).ethereum.request({method: 'eth_chainId'}))

208
src/extension/inject.ts Normal file
View File

@ -0,0 +1,208 @@
interface RequestArguments {
method: string;
params?: unknown[] | object;
}
const listners = {
accountsChanged: new Set<() => void>(),
connect: new Set<() => void>(),
disconnect: new Set<() => void>,
chainChanged: new Set<() => void>(),
}
const promResolvers = {} as any
const listner = function(event: any) {
if (event.source != window)
return;
if (event.data.type && (event.data.type == "CLWALLET_PAGE")) {
if(event?.data?.data?.error){
promResolvers[event.data.resId].reject(event.data.data)
}else {
promResolvers[event.data.resId].resolve(event.data.data);
}
promResolvers[event.data.resId] = undefined;
}
}
window.addEventListener("message",listner)
const sendMessage = (args: RequestArguments) => {
return new Promise((resolve, reject) => {
const resId = crypto.randomUUID()
promResolvers[resId] = { resolve, reject }
const data = { type: "CLWALLET_CONTENT", data: args, resId};
window.postMessage(data, "*");
})
}
// chainId
// :
// "0x89"
// enable
// :
// ƒ ()
// isMetaMask
// :
// true
// networkVersion
// :
// "137"
// request
// :
// ƒ ()
// selectedAddress
// :
// null
// send
// :
// ƒ ()
// sendAsync
// :
// ƒ ()
// _events
// :
// {connect: ƒ}
// _eventsCount
// :
// 1
// _handleAccountsChanged
// :
// ƒ ()
// _handleChainChanged
// :
// ƒ ()
// _handleConnect
// :
// ƒ ()
// _handleDisconnect
// :
// ƒ ()
// _handleStreamDisconnect
// :
// ƒ ()
// _handleUnlockStateChanged
// :
// ƒ ()
// _jsonRpcConnection
// :
// {events: s, stream: d, middleware: ƒ}
// _log
// :
// u {name: undefined, levels: {…}, methodFactory: ƒ, getLevel: ƒ, setLevel: ƒ, …}
// _maxListeners
// :
// 100
// _metamask
// :
// Proxy {isUnlocked: ƒ, requestBatch: ƒ}
// _rpcEngine
// :
// o {_events: {…}, _eventsCount: 0, _maxListeners: undefined, _middleware: Array(3)}
// _rpcRequest
// :
// ƒ ()
// _sendSync
// :
// ƒ ()
// _sentWarnings
// :
// {enable: false, experimentalMethods: false, send: false, events: {…}}
// _state
// :
// {accounts: Array(0), isConnected: true, isUnlocked: true, initialized: true, isPermanentlyDisconnected: false}
// _warnOfDeprecation
// :
// ƒ (
const eth = new Proxy({
isConnected: () => {
return true
},
// for maximum compatibility since is cloning the same API
isMetaMask: true,
enable: () => {
return sendMessage({ method: 'eth_requestAccounts', params: Array(0)})
},
request: (args: RequestArguments): Promise<unknown> => {
return sendMessage(args)
},
on: (eventName: string, callback: () => void) => {
switch (eventName) {
case 'accountsChanged':
listners.accountsChanged.add(callback)
break
case 'connect':
listners.connect.add(callback)
break;
case 'disconnect':
listners.disconnect.add(callback)
break;
case 'chainChanged':
listners.chainChanged.add(callback)
break;
default:
return
}
},
// Simulate Metamask
_warnOfDeprecation: () => null,
_state: {},
_sentWarnings: () => null,
_rpcRequest: () => null,
_handleAccountsChanged: () => null,
chainId: "0x89",
networkVersion: "137",
selectedAddress: null,
send: () => null,
sendAsync: async () => null,
_events: {},
_eventsCount: 0,
_handleChainChanged: () => null,
_handleConnect: () => null,
_handleDisconnect: () => null,
_handleStreamDisconnect: () => null,
_handleUnlockStateChanged: () => null,
_jsonRpcConnection: {},
_log: {},
_maxListeners: 100,
_metamask: new Proxy({}, {}),
_rpcEngine: {}
}, {
set: () => { return false },
deleteProperty: () => { return false },
})
const injectWallet = (win: any) => {
Object.defineProperty(win, 'ethereum', {
get: function () {
return eth
},
set: function () {
return
}
});
(window as any).tttest = 'test'
// Object.defineProperty(window, 'ethereum', 444)
console.log('Clear wallet injected', (window as any).ethereum, win)
}
injectWallet(this)
// setTimeout(() => {
// console.log('Metamask clone test');
// // (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, '111111111')});
// }, 3500)
// console.log( (window as any).ethereum.request({method: 'eth_chainId'}))

View File

@ -0,0 +1,55 @@
{
"manifest_version": 3,
"name": "Clear Wallet EVM",
"version": "1.0.0",
"icons": {
"16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png",
"48": "assets/extension-icon/wallet_48.png",
"128": "assets/extension-icon/wallet_128.png"
},
"action": {
"default_popup": "index.html",
"default_icon": {
"16": "assets/extension-icon/wallet_16.png",
"32": "assets/extension-icon/wallet_32.png",
"48": "assets/extension-icon/wallet_48.png",
"128": "assets/extension-icon/wallet_128.png"
}
},
"minimum_chrome_version": "93",
"permissions": [
"scripting",
"webNavigation",
"tabs",
"storage",
"alarms",
"unlimitedStorage",
"clipboardRead",
"clipboardWrite"
],
"host_permissions": [
"http://*/",
"https://*/"
],
"background": {
"service_worker": "/src/extension/serviceWorker.ts",
"type": "module"
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"all_frames": true,
"run_at": "document_start",
"js": ["/src/extension/content.ts"]
}
],
"web_accessible_resources": [{
"resources": ["src/extension/inject.js"],
"matches": ["<all_urls>"]
}]
}

View File

@ -0,0 +1,5 @@
export const rpcError ={
USER_REJECTED: 4001,
INVALID_PARAM: -32602,
INTERNAL_ERROR: -32603
}

View File

@ -0,0 +1,289 @@
import { getAccounts, getSelectedAccount, getSelectedNetwork, smallRandomString, storageSave} from '@/utils/platform';
import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest'
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice } from '@/utils/wallet'
import type { RequestArguments } from '@/extension/types'
import type { Account } from '@/extension/types'
import { rpcError } from '@/extension/rpcConstants'
import { updatePrices } from '@/utils/gecko'
chrome.runtime.onInstalled.addListener(() => {
console.log('Service worker installed');
chrome.runtime.connect(null as unknown as string, {
name:'sw-connection'
})
})
chrome.runtime.onConnect.addListener(port => port.onDisconnect.addListener((a) =>
{
console.log('Service worker connected', storageSave('test-d', a));
}))
chrome.runtime.onStartup.addListener(() => {
console.log('Service worker startup');
})
chrome.runtime.onSuspend.addListener(() => {
console.log('Service worker suspend');
})
chrome.alarms.create('updatePrices', {
periodInMinutes: 1
})
chrome.alarms.onAlarm.addListener((alarm) => {
if(alarm.name === 'updatePrices') {
updatePrices().then(() => {
console.log('Prices updated')
})
}
})
chrome.windows.onRemoved.addListener((winId) => {
if (winId in (userReject ?? {})){
userReject[winId]?.()
}
userReject[winId] = undefined
userApprove[winId] = undefined
rIdWin[winId] = undefined
rIdData[winId] = undefined
chrome.windows.getAll().then((wins) => {
if(wins.length === 0) {
storageSave('test-p', 'browser-closed')
}
})
})
chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendResponse) => {
console.log(message);
(async () => {
if (!('method' in message)) {
sendResponse({
code: 500,
message: 'Invalid request method'
})
} else {
// ETH API
switch (message.method) {
case 'eth_call': {
break
}
case 'eth_getBalance': {
sendResponse(await getBalance())
break
}
case 'eth_blockNumber': {
sendResponse(await getBlockNumber())
break
}
case 'eth_estimateGas': {
const params = message?.params?.[0] as any
if(!params) {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'Invalid param for gas estimate'
})
break
}
sendResponse(await estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
}))
break
}
case 'eth_accounts': {
const accounts = await getAccounts()
const addresses = accounts.map((a: Account) => a.address) ?? []
sendResponse(addresses)
break
}
case 'eth_requestAccounts': {
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse(address)
break
}
case 'eth_chainId': {
const network = await getSelectedNetwork()
console.log(network, 'network')
const chainId = network?.chainId ?? 0
sendResponse(`0x${chainId.toString(16)}`)
break
}
case 'eth_sendTransaction': {
try {
const params = message?.params?.[0] as any
if(!params) {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'Invalid param for send transaction'
})
break
}
const [account, network] = await Promise.all([getSelectedAccount(), getSelectedNetwork()])
if(!account || !network) {
return
}
const serializeParams = encodeURIComponent(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({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-tx&param=${serializeParams}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
}).then((win) => {
gWin = win
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
rIdData[String(win.id)] = {}
})
})
sendResponse(
await sendTransaction({...params, ...(rIdData?.[String(gWin?.id ?? 0)] ?? {}) }, pEstimateGas, pGasPrice)
)
} catch(err) {
console.error(err)
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected Signature'
})
}
break
}
case ('personal_sign' || 'eth_sign'): {
try {
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-msg&param=${String(message?.params?.[0] ?? '' )}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
}).then((win) => {
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
})
})
sendResponse(
await signMsg(String(message?.params?.[0]) ?? '' )
)
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected Signature'
})
}
break
}
// NON Standard metamask API
case 'wallet_requestPermissions': {
const account = await getSelectedAccount()
const address = account?.address ? [account?.address] : []
sendResponse([{
caveats: {
type:'',
value: address
},
invoker: '',
date: Date.now(),
id: smallRandomString(),
parentCapability: Object.keys(message?.params?.[0] ?? {})?.[0] ?? 'unknown'
}])
break
}
case 'net_version': {
const network = await getSelectedNetwork()
const chainId = network?.chainId ?? 0
sendResponse(chainId)
break
}
case 'wallet_switchEthereumChain': {
try {
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=switch-network&param=${String(message?.params?.[0] ?? '' )}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
}).then((win) => {
userReject[String(win.id)] = reject
userApprove[String(win.id)] = resolve
rIdWin[String(win.id)] = String(message.resId)
})
})
sendResponse(
await signMsg(String(message?.params?.[0]) ?? '' )
)
} catch {
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'User Rejected Signature'
})
}
break
}
// internal messeges
case 'wallet_approve': {
if(String(sender.tab?.windowId) in rIdWin){
userApprove[String(sender.tab?.windowId)]?.(true)
}
try {
chrome.windows.remove(sender.tab?.windowId ?? 0)
}catch{
// ignore
}
break
}
case 'wallet_send_data': {
if(String(sender.tab?.windowId) in rIdData){
rIdData[String(sender?.tab?.windowId ?? '')] = (message as any)?.data ?? {}
sendResponse(true)
}
break
}
case 'wallet_get_data': {
if(String(sender.tab?.windowId) in rIdData){
sendResponse( rIdData[String(sender?.tab?.windowId ?? '')] ?? {})
}
break
}
case 'wallet_ping': {
sendResponse(true)
break
}
default: {
sendResponse({
error: true,
code: rpcError.INVALID_PARAM,
message: 'Invalid request method'
})
break
}
}
}
}
)();
return true;
});

53
src/extension/types.ts Normal file
View File

@ -0,0 +1,53 @@
export interface Network {
name: string
chainId: number
rpc: string
symbol?: string
icon?: string
priceId?: string
explorer?: string
}
export interface Account {
name: string
address: string
pk: string
encPk: string
}
export interface Accounts {
[key: string]: Account
}
export interface Networks {
[key: number]: Network
}
export interface RequestArguments {
method: string;
params?: unknown[];
resId?: string
}
export interface ProviderRpcError extends Error {
message: string;
code: number;
data?: unknown;
}
export interface Price {
[key: string]: number
}
export interface Prices {
[key: string]: Price
}
export interface Settings {
enableStorageEnctyption: boolean
encryptAfterEveryTx: boolean
lockOutPeriod: number
lockOutEnabled: boolean
theme: 'system' | 'light' | 'dark'
MP: string
}

View File

@ -0,0 +1,32 @@
export const userReject = {} as Record<string, (() => any) | undefined>
export const userApprove = {} as Record<string, ((a: unknown) => any) | undefined>
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 })
}
export const walletSendData = (rId: string, data: any) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_send_data', resId: rId, data}, (r) => {
resolve(r)
})
})
}
export const walletGetData = (rId: string) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_get_data', resId: rId }, (r) => {
resolve(r)
})
})
}
export const walletPing = () => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_ping' }, (r) => {
resolve(r)
})
})
}

View File

@ -26,7 +26,7 @@ import './theme/variables.css';
const app = createApp(App)
.use(IonicVue)
.use(router);
router.isReady().then(() => {
app.mount('#app');
});

View File

@ -1,19 +1,70 @@
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import AppTabs from '@/views/AppTabs.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '',
redirect: '/folder/Inbox'
path: '/',
redirect: '/tabs/home',
},
{
path: '/folder/:id',
component: () => import ('../views/FolderPage.vue')
}
path: '/sign-msg/:rid/:param',
component: () => import('@/views/SignMessage.vue'),
},
{
path: '/sign-tx/:rid/:param',
component: () => import('@/views/SignTx.vue'),
},
{
path: '/switch-network/:rid/:param',
component: () => import('@/views/SwitchNetwork.vue'),
},
{
path: '/tabs/',
component: AppTabs,
children: [
{
path: '',
redirect: 'home',
},
{
path: 'home',
component: () => import('@/views/HomeTab.vue'),
},
{
path: 'networks',
component: () => import('@/views/NetworksTab.vue'),
},
{
path: 'settings',
component: () => import('@/views/SettingsTab.vue'),
},
{
path: 'assets',
component: () => import('@/views/AssetsTab.vue'),
},
{
path: 'accounts',
component: () => import('@/views/AccountsTab.vue'),
},
{
path: 'history',
component: () => import('@/views/HistoryTab.vue'),
},
{
path: 'add-account',
component: () => import('@/views/AddAccount.vue'),
},
{
path: 'add-network',
component: () => import('@/views/AddNetwork.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
history: createWebHistory(import.meta.env.BASE_URL),
routes
})

4
src/shims-vue.d.ts vendored
View File

@ -1,6 +1,8 @@
/* eslint-disable */
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

11
src/utils/gecko.ts Normal file
View File

@ -0,0 +1,11 @@
import { setPrices } from '@/utils/platform'
import { mainNets } from '@/utils/networks'
const coinGekoApiEndpoint = 'https://api.coingecko.com/api/v3/'
export const updatePrices = async() : Promise<void> => {
const priceIds = Object.values(mainNets).map(n => n.priceId).join(',')
const req = await fetch(`${coinGekoApiEndpoint}/simple/price?ids=${priceIds}&vs_currencies=usd`)
if (req.ok) {
await setPrices(await req.json())
}
}

111
src/utils/networks.ts Normal file
View File

@ -0,0 +1,111 @@
import type { Network } from '@/extension/types'
export const mainNets: {[key: number]: Network} = {
1: {
name: 'Ethereum Main',
rpc: 'https://eth-mainnet.public.blastapi.io',
chainId: 1,
explorer: '',
icon: 'eth.webp',
symbol: 'ETH',
priceId: 'ethereum'
},
137: {
name: 'Polygon Mainnet',
rpc: 'https://polygon-rpc.com',
chainId: 137,
explorer: '',
icon:'polygon.webp',
symbol: 'MATIC',
priceId: 'matic-network'
},
100: {
name: 'Gnosis',
rpc: 'https://rpc.gnosischain.com/',
chainId: 100,
explorer: '',
icon:'xdai.webp',
symbol: 'xDAI',
priceId: 'xdai'
},
10: {
name: 'Optimism',
rpc: 'https://mainnet.optimism.io',
chainId: 10,
explorer: '',
icon: 'optimism.webp',
symbol: 'ETH',
priceId: 'ethereum'
},
56: {
name: 'BSC Main',
rpc: 'https://bsc-dataseed2.binance.org',
chainId: 56,
explorer: '',
icon: 'binance.webp',
symbol: 'BNB',
priceId: 'binancecoin'
},
42161: {
name: 'Arbitrum One',
rpc: 'https://rpc.ankr.com/arbitrum',
chainId: 42161,
explorer: '',
icon: 'arbitrum.webp',
symbol: 'ETH',
priceId: 'ethereum'
},
}
export const testNets = {
1: {
name: 'TESTNET Ethereum oerli',
rpc: 'https://rpc.ankr.com/eth_goerli',
chainId: 5,
explorer: 'https://goerli.etherscan.io',
icon: 'eth.webp'
},
4: {
name: 'TESTNET Ethereum Rinkeby',
rpc: 'https://rpc.ankr.com/eth_rinkeby',
chainId: 5,
explorer: 'https://goerli.etherscan.io',
icon: 'eth.webp'
},
80001: {
name: 'TESTNET Polygon',
rpc: 'https://rpc.ankr.com/polygon_mumbai',
chainId: 80001,
explorer: '',
icon:'polygon.webp'
},
100100: {
name: 'TESTNET Gnosis Chiado',
rpc: 'https://gnosis-mainnet.public.blastapi.io',
chainId: 100100,
explorer: '',
icon:'xdai.webp'
},
420: {
name: 'TESTNET Optimism Goreli',
rpc: 'https://goerli.optimism.io/',
chainId: 420,
explorer: '',
icon: 'optimism.webp'
},
97: {
name: 'TESTNET BSC Main',
rpc: 'https://bsctestapi.terminet.io/rpc',
chainId: 97,
explorer: '',
icon: 'binance.webp'
},
421613: {
name: 'TESTNET Arbitrum One',
rpc: 'https://goerli-rollup.arbitrum.io/rpc/',
chainId: 421613,
explorer: '',
icon: 'arbitrum.webp'
},
}

146
src/utils/platform.ts Normal file
View File

@ -0,0 +1,146 @@
import type { Network, Account, Prices, Settings, Networks } from '@/extension/types'
import type { Ref } from 'vue'
const defaultSettings = {
enableStorageEnctyption: false,
encryptAfterEveryTx: false,
lockOutEnabled: false,
lockOutPeriod: 12e4,
lockOutBlocked: false,
theme: 'system',
MP: ''
}
export const storageSave = async (key: string, value: any): Promise<void> =>{
await chrome.storage.local.set({ [key]: value })
}
export const storageGet = async (key: string): Promise<{ [key: string]: any }> => {
return await chrome.storage.local.get(key)
}
export const storageWipe = async (): Promise<void> => {
await chrome.storage.local.clear()
}
export const getNetworks = async (): Promise<Networks> => {
return (await storageGet('networks'))?.networks ?? {} as Networks
}
export const replaceNetworks = async (networks: Networks): Promise<void> => {
await storageSave('networks', networks)
}
export const saveNetwork = async (network: Network): Promise<void> => {
const saveNetworks = await getNetworks()
saveNetworks[network.chainId] = network
await storageSave('networks', saveNetworks)
}
export const getSelectedNetwork = async (): Promise<Network > => {
return (await storageGet('selectedNetwork'))?.selectedNetwork ?? null as unknown as Network
}
export const saveSelectedNetwork = async (selectedNetwork: Network ): Promise<void> => {
await storageSave('selectedNetwork', selectedNetwork )
}
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])
}
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
}
export const saveSelectedAccount = async (selectedAccount: Account): Promise<void> => {
await storageSave('selectedAccount', selectedAccount )
}
export const setPrices = async (prices: Prices): Promise<void> => {
await storageSave('prices', prices )
}
export const getPrices = async (): Promise<void> => {
return (await storageGet('prices'))?.prices ?? {} as unknown as Prices
}
export const getSettings = async (): Promise<Settings> => {
return (await storageGet('settings'))?.settings ?? defaultSettings as unknown as Settings
}
export const setSettings = async (settings: Settings): Promise<void> => {
await storageSave('settings', settings )
}
export const getBalanceCache = async (): Promise<string> => {
return (await storageGet('balance'))?.balance ?? '0x0'
}
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 = () => {
return (Math.random() + 1).toString(36).substring(7);
}
export const hexTostr = (hexStr: string) =>
{
if(hexStr.substring(0,2) === '0x') {
const chunks = [];
const hexCodes = hexStr.substring(2)
for (let i = 0, charsLength = hexCodes.length; i < charsLength; i += 2) {
chunks.push(hexCodes.substring(i, i + 2));
}
return chunks.reduce(
(pv, cv) => `${pv}${String.fromCharCode(parseInt(cv, 16))}`,
''
).substring(0, 66)
}
return hexStr
}
export const copyAddress = async (address: string, toastRef: Ref<boolean>) => {
await navigator.clipboard.writeText(address)
toastRef.value = true
}
export const getUrl = (url: string) => chrome.runtime.getURL(url)
export const paste = (id: string) => {
const el = document.getElementById(id)
if(el){
el.focus();
(document as any)?.execCommand('paste')
}
}
export const openTab = (url: string) => {
chrome.tabs.create({
url
});
}

64
src/utils/wallet.ts Normal file
View File

@ -0,0 +1,64 @@
import { getSelectedAccount, getSelectedNetwork } from '@/utils/platform';
import { BigNumber, ethers } from "ethers"
export const signMsg = async (msg: string) => {
const account = await getSelectedAccount()
const wallet = new ethers.Wallet(account.pk)
return await wallet.signMessage( msg.startsWith('0x') ? ethers.utils.arrayify(msg): msg)
}
export const getBalance = async () =>{
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.providers.JsonRpcProvider(network.rpc))
return await wallet.getBalance()
}
export const getGasPrice = async () => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.getGasPrice()
}
export const getBlockNumber = async () => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.getBlockNumber()
}
export const estimateGas = async ({to = '', from = '', data = '', value = '0x0' }: {to: string, from: string, data: string, value: string}) => {
const network = await getSelectedNetwork()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.estimateGas({to, from, data, value})
}
export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='0x0', gasPrice='0x0'}:
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string},
gasEstimate: Promise<BigNumber> | null = null, pGasPrice : Promise<BigNumber> | null) => {
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.providers.JsonRpcProvider(network.rpc))
if(gas === '0x0') {
if(!gasEstimate){
throw new Error('No gas estimate available')
}else {
gas = (await gasEstimate).toString()
}
}
if(gasPrice === '0x0') {
if(!pGasPrice){
throw new Error('No gas estimate available')
}else {
gasPrice = (await pGasPrice).toString()
}
}
return await wallet.sendTransaction({to, from, data, value, gasLimit: gas, gasPrice})
}
export const formatBalance = (balance: string) => {
Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 6
}).format(Number(ethers.utils.parseEther(balance)))
}

90
src/utils/webCrypto.ts Normal file
View File

@ -0,0 +1,90 @@
import { storageGet, storageSave } from '@/utils/platform'
const getIv = async() => {
let iv = (await storageGet('iv'))?.iv
if(!iv){
iv = crypto.getRandomValues(new Uint8Array(16));
const jsonIv = JSON.stringify(iv)
await storageSave('iv', jsonIv)
return iv
}else {
iv = new Uint8Array(Object.values(JSON.parse(iv)));
}
return iv
}
const getSalt = async() => {
let salt = (await storageGet('salt'))?.salt
if(!salt){
salt = crypto.getRandomValues(new Uint8Array(16));
const jsonSalt = JSON.stringify(salt)
await storageSave('salt', jsonSalt)
return salt
}else {
salt = new Uint8Array(Object.values(JSON.parse(salt)));
}
return salt
}
async function getKey(passwordBytes: Uint8Array) {
const salt = await getSalt()
const initialKey = await crypto.subtle.importKey(
"raw",
passwordBytes,
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 50000, hash: "SHA-256" },
initialKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
export const encrypt = async (password: string, data:string) => {
const enc = new TextEncoder()
const encData = enc.encode(data)
console.log(encData)
const encKey = enc.encode(password)
console.log(encKey)
const key = await getKey(encKey)
console.log(key)
const iv = await getIv()
console.log(iv)
console.log({
name: "AES-GCM",
iv,
},
key,
encData)
const encResult = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
encData,
)
console.log(JSON.stringify(new Uint8Array(encResult)))
return JSON.stringify(new Uint8Array(encResult))
}
export const decrypt = async (encryptedData: string, password: string) => {
const enc = new TextEncoder()
const encKey = enc.encode(password)
const key = await getKey(encKey)
const iv = await getIv()
const encryptedUint= new Uint8Array(Object.values(JSON.parse(encryptedData)));
console.log(encryptedUint)
const contentBytes = new Uint8Array(
await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedUint)
);
return new TextDecoder().decode(contentBytes)
}

126
src/views/AccountsTab.vue Normal file
View File

@ -0,0 +1,126 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<router-link to="/tabs/add-account">
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-title>Accounts</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-toast
:is-open="toastState"
@didDismiss="toastState=false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
<ion-list v-for="account of accounts" :key="account.address">
<ion-item>
<ion-label>
{{ account.name }}
</ion-label>
</ion-item>
<ion-item @click="copyAddress(account.address, getToastRef())">
<p style="font-size:0.7rem">{{ account.address }}</p><ion-icon :icon="copyOutline"></ion-icon>
</ion-item>
<ion-item>
<ion-chip>View Pk</ion-chip>
<ion-chip @click="deleteAccount(account.address)">Delete</ion-chip>
<ion-chip @click="editName(account.address)">Edit Name</ion-chip>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import { getAccounts, copyAddress, replaceAccounts } from "@/utils/platform"
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonIcon,
IonList,
IonItem,
IonLabel,
IonChip,
IonButtons,
IonButton,
onIonViewWillEnter,
IonToast
} from "@ionic/vue";
import { addCircleOutline, copyOutline } from "ionicons/icons";
import type { Account } from '@/extension/types'
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonIcon,
IonList,
IonItem,
IonLabel,
IonChip,
IonButtons,
IonButton,
IonToast
},
setup () {
const accounts = ref({}) as Ref<Account[]>
const loading = ref(true)
const toastState = ref(false)
const getToastRef = () => toastState
const loadData = () => {
const pAccounts = getAccounts()
Promise.all([pAccounts]).then(( res ) => {
accounts.value = res[0]
loading.value = false
})
}
const deleteAccount = async (address: string) => {
loading.value = true
const findIndex = accounts.value.findIndex(a => a.address === address)
if (findIndex !== -1) {
accounts.value.splice(findIndex, 1)
}
await replaceAccounts([...accounts.value])
loading.value = false
}
const editName = async (name: string) => {
// do nothing
}
onIonViewWillEnter(() => {
loadData()
})
return {
accounts,
addCircleOutline,
copyOutline,
toastState,
copyAddress,
getToastRef,
deleteAccount,
editName
}
}
});
</script>

128
src/views/AddAccount.vue Normal file
View File

@ -0,0 +1,128 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Add Account</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>Name</ion-label>
<ion-input v-model="name"></ion-input>
</ion-item>
<ion-item>
<ion-label>Get Random Name</ion-label>
<ion-button @click="getRandomName" >Generate</ion-button>
</ion-item>
<ion-item>
<ion-icon style="margin-right: 0.5rem;" @click="paste('pasteRpc')" :icon="clipboardOutline" button /><ion-label>PK</ion-label>
<ion-input id="pastePk" v-model="pk"></ion-input>
</ion-item>
<ion-item>
<ion-label>Get Random PK</ion-label>
<ion-button @click="generateRandomPk" >Generate</ion-button>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onAddAccount">Add Account</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, IonLabel, IonInput, IonButton, IonAlert, IonIcon } from "@ionic/vue";
import { ethers } from "ethers"
import { saveSelectedAccount, getAccounts, saveAccount, getRandomPk, smallRandomString, paste } from "@/utils/platform";
import router from "@/router";
import { clipboardOutline } from "ionicons/icons";
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonAlert, IonIcon },
setup: () => {
const name = ref('')
const pk = ref('')
const alertOpen = ref(false)
const alertMsg = ref('')
const resetFields = () => {
name.value = ''
pk.value = ''
}
const onAddAccount = async () => {
let p1 = Promise.resolve()
if(pk.value.length === 64){
pk.value = `0x${pk.value.trim()}`
}
if(pk.value.length !== 66) {
alertMsg.value = "Provided private key is invalid."
alertOpen.value = true
return
}
const wallet = new ethers.Wallet(pk.value)
const accounts = await getAccounts()
if((accounts.length ?? 0) < 1 ){
p1 = saveSelectedAccount({
address: wallet.address,
name: name.value,
pk: pk.value,
encPk: ''
})
} else {
if(accounts.find(account => account.address === wallet.address)){
alertMsg.value = "Account already exists."
return alertOpen.value = true
}
}
const p2 = saveAccount({
address: wallet.address,
name: name.value,
pk: pk.value,
encPk: ''
})
await Promise.all([p1, p2])
router.push('/')
resetFields()
}
const generateRandomPk = () => {
pk.value = getRandomPk()
}
const getRandomName = () => {
name.value = smallRandomString()
}
const onCancel = () => {
router.push('/')
}
return {
name,
pk,
onAddAccount,
onCancel,
alertOpen,
alertMsg,
generateRandomPk,
getRandomName,
clipboardOutline,
paste
}
}
});
</script>

196
src/views/AddNetwork.vue Normal file
View File

@ -0,0 +1,196 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Add Network</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button @click="templateModal=true" expand="block">Add from popular chain list</ion-button>
<ion-item>
<ion-label>Name</ion-label>
<ion-input v-model="name"></ion-input>
</ion-item>
<ion-item>
<ion-label>ChainId</ion-label>
<ion-input v-model="chainId" type="number"></ion-input>
</ion-item>
<ion-item button>
<ion-icon :icon="clipboardOutline" @click="paste('pasteRpc')" />
<ion-label>RPC URL</ion-label>
<ion-input id="pasteRpc" v-model="rpc" ></ion-input>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onAddNetwork">Add Network</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
header="Error"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen=false"
></ion-alert>
</ion-content>
<ion-modal :is-open="templateModal" @will-dismiss="templateModal=false">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="templateModal=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-list-header>
<ion-label>Networks</ion-label>
</ion-list-header>
<ion-segment :value="currentSegment" @ion-change="segmentChange">
<ion-segment-button value="mainnets">
<ion-label>Main Networks</ion-label>
</ion-segment-button>
<ion-segment-button value="testnets">
<ion-label>Test Networks</ion-label>
</ion-segment-button>
</ion-segment>
<div v-if="currentSegment==='mainnets'" >
<ion-list class="ion-padding" v-for="network of Object.values(mainNets)" :key="network.chainId">
<ion-item button style="cursor:pointer;" @click="fillTemplate(network)">
<ion-avatar style="margin-right: 1rem;">
<img :alt="network.name" :src="getUrl('assets/chain-icons/' + network.icon)" />
</ion-avatar><ion-label>{{network.name}}</ion-label>
</ion-item>
</ion-list>
</div>
<div v-else>
<ion-list class="ion-padding" v-for="network of Object.values(testNets)" :key="network.chainId">
<ion-item button style="cursor:pointer;" @click="fillTemplate(network)">
<ion-avatar style="margin-right: 1rem;">
<img :alt="network.name" :src="getUrl('assets/chain-icons/' + network.icon)" />
</ion-avatar><ion-label>{{network.name}}</ion-label>
</ion-item>
</ion-list>
</div>
</ion-list>
</ion-content>
</ion-modal>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonIcon,
IonModal, IonList, IonSegment, IonSegmentButton, IonListHeader, IonButtons, IonAvatar, modalController, IonAlert
} from "@ionic/vue";
import { getNetworks, saveSelectedNetwork, saveNetwork, getUrl, paste } from "@/utils/platform";
import router from "@/router";
import { mainNets, testNets } from "@/utils/networks"
import { clipboardOutline } from "ionicons/icons";
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonIcon,
IonModal, IonList, IonSegment, IonSegmentButton, IonListHeader, IonButtons, IonAvatar, IonAlert },
setup: () => {
const name = ref('')
const chainId = ref(0)
const rpc = ref('')
const templateModal = ref(false)
const currentSegment = ref('mainnets')
const alertOpen = ref(false)
const alertMsg = ref('')
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()
const networks = await getNetworks()
if( (Object.keys(networks).length ?? 0) < 1 ){
p1 = saveSelectedNetwork({
name: name.value,
chainId: chainId.value,
rpc: rpc.value
})
} else {
if(chainId.value in networks){
alertMsg.value = "Network already exists."
return alertOpen.value = true
}
}
const p2 = saveNetwork({
name: name.value,
chainId: chainId.value,
rpc: rpc.value
})
await Promise.all([p1, p2])
router.push('/')
resetFields()
}
const segmentChange = (value: any) => {
currentSegment.value = value.detail.value
}
const onCancel = () => {
router.push('/')
}
const fillTemplate = (network: typeof mainNets[1] ) =>{
name.value = network.name
chainId.value = network.chainId
rpc.value = network.rpc
modalController.dismiss(null, 'cancel')
}
return {
name,
chainId,
onAddNetwork,
rpc,
onCancel,
paste,
clipboardOutline,
templateModal,
currentSegment,
mainNets,
testNets,
segmentChange,
getUrl,
fillTemplate,
alertOpen,
alertMsg
}
}
});
</script>

89
src/views/AppTabs.vue Normal file
View File

@ -0,0 +1,89 @@
<template>
<ion-page>
<ion-content>
<ion-tabs @ionTabsWillChange="beforeTabChange" @ionTabsDidChange="afterTabChange">
<ion-router-outlet />
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home" href="/tabs/home">
<ion-icon :icon="walletOutline"></ion-icon>
<ion-label>Wallet</ion-label>
</ion-tab-button>
<ion-tab-button tab="accounts" href="/tabs/accounts">
<ion-icon :icon="personCircle"></ion-icon>
<ion-label>Accounts</ion-label>
</ion-tab-button>
<ion-tab-button tab="networks" href="/tabs/networks">
<ion-icon :icon="gitNetworkOutline"></ion-icon>
<ion-label>Networks</ion-label>
</ion-tab-button>
</ion-tab-bar>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="history" href="/tabs/history">
<ion-icon :icon="receiptOutline"></ion-icon>
<ion-label>History</ion-label>
</ion-tab-button>
<ion-tab-button tab="assets" href="/tabs/assets">
<ion-icon :icon="diamondOutline"></ion-icon>
<ion-label>Assets</ion-label>
</ion-tab-button>
<ion-tab-button tab="settings" href="/tabs/settings">
<ion-icon :icon="cogOutline"></ion-icon>
<ion-label>Settings</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import {
IonPage,
IonContent,
IonRouterOutlet,
IonTabs,
IonTabBar,
IonTabButton,
IonLabel,
IonIcon
} from "@ionic/vue";
import { personCircle, walletOutline, diamondOutline, cogOutline, receiptOutline, gitNetworkOutline } from "ionicons/icons";
export default defineComponent({
components: {
IonPage,
IonContent,
IonRouterOutlet,
IonTabs,
IonTabBar,
IonTabButton,
IonLabel,
IonIcon
},
name: "AppTabs",
setup() {
const beforeTabChange = () => {
// do something before tab change
};
const afterTabChange = () => {
// do something after tab change
};
return {
walletOutline,
personCircle,
diamondOutline,
cogOutline,
receiptOutline,
gitNetworkOutline,
beforeTabChange,
afterTabChange,
};
},
});
</script>

20
src/views/AssetsTab.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Assets</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Schedule Tab</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from "@ionic/vue";
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
});
</script>

View File

@ -1,70 +0,0 @@
<template>
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button color="primary"></ion-menu-button>
</ion-buttons>
<ion-title>{{ $route.params.id }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">{{ $route.params.id }}</ion-title>
</ion-toolbar>
</ion-header>
<div id="container">
<strong class="capitalize">{{ $route.params.id }}</strong>
<p>Explore <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar } from '@ionic/vue';
export default defineComponent({
name: 'FolderPage',
components: {
IonButtons,
IonContent,
IonHeader,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar
}
});
</script>
<style scoped>
#container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
#container strong {
font-size: 20px;
line-height: 26px;
}
#container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
#container a {
text-decoration: none;
}
</style>

20
src/views/HistoryTab.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>History</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Schedule Tab</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from "@ionic/vue";
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
});
</script>

295
src/views/HomeTab.vue Normal file
View File

@ -0,0 +1,295 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Wallet</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item v-if="loading || accounts.length < 1">
<ion-label>No EVM accounts found</ion-label>
<ion-button @click="goToAddAccount">Add Account</ion-button>
</ion-item>
<ion-list v-else>
<ion-item>
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
<ion-button @click="accountsModal = true">Select</ion-button>
</ion-item>
<ion-item button @click="copyAddress(selectedAccount.address, getToastRef())">
<p style="font-size: 0.7rem">{{ selectedAccount?.address }}</p>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-item>
</ion-list>
<ion-item v-if="loading || Object.keys(networks).length < 1">
<ion-label>No EVM Networks found</ion-label>
<ion-button @click="goToAddNetwork">Add Network</ion-button>
</ion-item>
<ion-item v-else>
<ion-avatar
v-if="(mainNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="selectedNetwork?.name"
: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-item>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
<ion-toast
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
</ion-content>
<ion-modal
:is-open="accountsModal"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="accountsModal=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-radio-group :value="selectedAccount.address">
<ion-list-header>
<ion-label>Accounts</ion-label>
</ion-list-header>
<ion-list
@click="changeSelectedAccount(account.address)"
class="ion-padding"
v-for="account of accounts"
:key="account.address"
button
>
<ion-item>
<ion-radio slot="start" :value="account.address" />
<ion-label>{{ account.name }}</ion-label>
</ion-item>
<ion-item>
<ion-text style="font-size:0.8rem;">{{ account.address }}</ion-text>
</ion-item>
</ion-list>
</ion-radio-group>
</ion-list>
</ion-content>
</ion-modal>
<ion-modal
:is-open="networksModal"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="networksModal=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-radio-group :value="selectedNetwork.chainId">
<ion-list-header>
<ion-label>Networks</ion-label>
</ion-list-header>
<ion-list
class="ion-padding"
v-for="network of networks"
:key="network.chainId"
>
<ion-item>
<ion-radio
@click="changeSelectedNetwork(network.chainId)"
slot="start"
:value="network.chainId"
/>
<ion-label>{{ network.name }}</ion-label>
</ion-item>
<ion-item>
<ion-text>{{ network.rpc }}</ion-text>
</ion-item>
</ion-list>
</ion-radio-group>
</ion-list>
</ion-content>
</ion-modal>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, Ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonLoading,
IonItem,
IonLabel,
IonButton,
onIonViewWillEnter,
IonModal,
IonRadioGroup,
IonRadio,
IonButtons,
IonList,
IonListHeader,
IonText,
IonToast,
IonIcon,
IonAvatar,
} from "@ionic/vue";
import {
getAccounts,
getNetworks,
getSelectedAccount,
saveSelectedAccount,
replaceAccounts,
getSelectedNetwork,
copyAddress,
replaceNetworks,
getUrl,
saveSelectedNetwork,
} from "@/utils/platform";
import type { Network, Account, Networks } from "@/extension/types";
import { mainNets } from "@/utils/networks";
import router from "@/router";
import { copyOutline } from "ionicons/icons";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonLoading,
IonItem,
IonLabel,
IonButton,
IonModal,
IonRadioGroup,
IonRadio,
IonButtons,
IonList,
IonListHeader,
IonText,
IonToast,
IonIcon,
IonAvatar,
},
setup: () => {
const loading = ref(true);
const accounts = ref([]) as Ref<Account[]>;
const networks = ref({}) as Ref<Networks>;
const accountsModal = ref(false) as Ref<boolean>;
const networksModal = ref(false) as Ref<boolean>;
const selectedAccount = (ref(null) as unknown) as Ref<Account>;
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
const toastState = ref(false);
const getToastRef = () => toastState;
const loadData = () => {
const pAccounts = getAccounts();
const pNetworks = getNetworks();
const pSelectedAccount = getSelectedAccount();
const pSelectedNetwork = getSelectedNetwork();
Promise.all([pAccounts, pNetworks, pSelectedAccount, pSelectedNetwork]).then(
(res) => {
accounts.value = res[0];
networks.value = res[1];
selectedAccount.value = res[2];
selectedNetwork.value = res[3];
loading.value = false;
}
);
};
onIonViewWillEnter(() => {
loadData();
});
onMounted(() => {
// nothing
});
const goToAddAccount = () => {
router.push("/tabs/add-account");
};
const goToAddNetwork = () => {
router.push("/tabs/add-network");
};
const changeSelectedAccount = async (address: string) => {
loading.value = true;
const findIndex = accounts.value.findIndex(a => a.address == address)
if (findIndex > -1) {
selectedAccount.value = accounts.value[findIndex]
await saveSelectedAccount(selectedAccount.value)
// console.log(({ [address]: accounts.value[address], ...accounts.value}))
accounts.value.splice(findIndex, 1);
accounts.value.splice(0,0,selectedAccount.value)
await replaceAccounts([...accounts.value])
}
accountsModal.value = false;
loading.value = false;
};
const changeSelectedNetwork = async (chainId: number) => {
loading.value = true;
if (chainId in networks.value) {
await saveSelectedNetwork(networks.value[chainId]);
await replaceNetworks(
Object.assign({ [chainId]: networks.value[chainId] }, networks.value)
);
selectedNetwork.value = networks.value[chainId];
}
networksModal.value = false;
loading.value = false;
};
return {
loading,
accounts,
networks,
accountsModal,
goToAddAccount,
goToAddNetwork,
selectedAccount,
selectedNetwork,
changeSelectedAccount,
changeSelectedNetwork,
copyAddress,
copyOutline,
toastState,
getToastRef,
networksModal,
mainNets,
getUrl,
};
},
});
</script>

109
src/views/NetworksTab.vue Normal file
View File

@ -0,0 +1,109 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<router-link to="/tabs/add-network">
<ion-button>
<ion-icon slot="icon-only" :icon="addCircleOutline"></ion-icon>
</ion-button>
</router-link>
</ion-buttons>
<ion-title>Networks</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list v-for="network of networks" :key="network.chainId">
<ion-item>
<ion-avatar v-if="(mainNets as any)[network.chainId].icon" style="margin-right: 1rem; width: 1.8rem; height:1.8rem;">
<img :alt="network.name" :src="getUrl('assets/chain-icons/' + (mainNets as any)[network.chainId].icon)" />
</ion-avatar>
<ion-label>
{{ network.name }}
</ion-label>
<ion-label>
ID: {{ network.chainId }}
</ion-label>
</ion-item>
<ion-item>
<ion-chip>Edit</ion-chip>
<ion-chip>Delete</ion-chip>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import { getNetworks, copyAddress, getUrl } from "@/utils/platform"
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonIcon,
IonList,
IonItem,
IonLabel,
IonChip,
IonButtons,
IonButton,
onIonViewWillEnter,
IonAvatar
} from "@ionic/vue";
import { mainNets } from "@/utils/networks"
import { addCircleOutline, copyOutline } from "ionicons/icons";
import type { Networks } from '@/extension/types'
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonIcon,
IonList,
IonItem,
IonLabel,
IonChip,
IonButtons,
IonButton,
IonAvatar
},
setup () {
const networks = ref([]) as Ref<Networks>
const loading = ref(true)
const toastState = ref(false)
const getToastRef = () => toastState
const loadData = () => {
const pAccounts = getNetworks()
Promise.all([pAccounts]).then(( res ) => {
networks.value = res[0]
loading.value = false
})
}
onIonViewWillEnter(() => {
loadData()
})
return {
networks,
addCircleOutline,
copyOutline,
toastState,
copyAddress,
getToastRef,
getUrl,
mainNets
}
}
});
</script>

375
src/views/SettingsTab.vue Normal file
View File

@ -0,0 +1,375 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Settings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-accordion-group v-if="!loading">
<ion-accordion value="1">
<ion-item slot="header" color="light">
<ion-label>Security</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<ion-list>
<ion-item>
<ion-label>Enable Storage Encryption</ion-label>
<ion-toggle :key="updateKey" @ion-change="changeEncryption" slot="end" :checked="settings.s.enableStorageEnctyption"></ion-toggle>
</ion-item>
<ion-item>
This will require to input an encrypto key when storage is locked.
</ion-item>
</ion-list>
<ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label>
<ion-toggle :key="updateKey" slot="end" :checked="settings.s.lockOutEnabled"></ion-toggle>
</ion-item>
<ion-list>
<ion-item :disabled="!settings.s.enableStorageEnctyption || settings.s.lockOutEnabled">
<ion-label>Auto-lock Period: (2-120) minutes</ion-label>
</ion-item>
<ion-item :disabled="!settings.s.enableStorageEnctyption || settings.s.lockOutEnabled">
<ion-input :key="updateKey" v-model="settings.s.lockOutPeriod" type="number"></ion-input>
</ion-item>
<ion-item :disabled="!settings.s.enableStorageEnctyption || settings.s.lockOutEnabled">
<ion-button @click="setTime">Set Auto-lock</ion-button>
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>Permanent Lock</ion-label>
<ion-toggle :key="updateKey" slot="end" :disabled="!settings.s.enableStorageEnctyption" :checked="settings.s.encryptAfterEveryTx"></ion-toggle>
</ion-item>
<ion-item>Will require decrypt pass before any sign or transaction</ion-item>
</ion-list>
</ion-list>
</div>
</ion-accordion>
<ion-accordion value="2">
<ion-item slot="header" color="light">
<ion-label>Theme</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<ion-radio-group :value="settings.s.theme">
<ion-item>
<ion-radio
slot="start"
value="system"
/>
<ion-label>System Default</ion-label>
</ion-item>
<ion-item>
<ion-radio
slot="start"
value="dark"
/>
<ion-label>Dark</ion-label>
</ion-item>
<ion-item>
<ion-radio
slot="start"
value="light"
/>
<ion-label>Light</ion-label>
</ion-item>
</ion-radio-group>
</ion-list>
</div>
</ion-accordion>
<ion-accordion value="3">
<ion-item slot="header" color="light">
<ion-label>About</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
About text
</div>
</ion-accordion>
<ion-accordion value="4">
<ion-item slot="header" color="light">
<ion-label>Danger</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-item>
<ion-label>WIPE All DATA</ion-label>
<ion-button color="danger" @click="wipeStorage">PERMA WIPE</ion-button>
</ion-item>
</div>
</ion-accordion>
</ion-accordion-group>
<ion-toast
:is-open="toastState"
@didDismiss="toastState = false"
:message="toastMsg"
:duration="1500"
></ion-toast>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
<ion-modal
:is-open="mpModal"
@did-dismiss="mpModal=false;modalDismiss()"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="mpModal=false">Close</ion-button>
</ion-buttons>
<ion-title>Create Encryption Password</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list v-if="settings.s.enableStorageEnctyption">
<ion-item>
<ion-label>Old Passord</ion-label>
</ion-item> <ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
<div v-else>
<ion-list>
<ion-item>
<ion-label>New Password</ion-label>
</ion-item> <ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>Confirm</ion-label>
</ion-item> <ion-item>
<ion-input v-model="mpConfirm" type="password"></ion-input>
</ion-item>
</ion-list>
</div>
<ion-item>
<ion-button @click="confirmModal">Confirm</ion-button>
</ion-item>
</ion-content>
</ion-modal>
<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, reactive } from "vue";
import { storageWipe, getSettings, setSettings, getAccounts, saveSelectedAccount, replaceAccounts } from "@/utils/platform";
import { decrypt, encrypt } from "@/utils/webCrypto"
// import { Account } from '@/extension/type'
import type { Settings } from "@/extension/types"
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonLoading,
onIonViewWillEnter,
IonList,
IonToggle,
IonModal,
IonInput,
IonAccordion,
IonAccordionGroup,
IonRadioGroup,
IonRadio,
IonButtons,
IonAlert,
IonToast
} from "@ionic/vue";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonLoading,
IonList,
IonToggle,
IonModal,
IonInput,
IonAccordion,
IonAccordionGroup,
IonRadioGroup,
IonRadio,
IonButtons,
IonAlert,
IonToast
},
setup() {
const loading = ref(true);
const mpModal = ref(false);
const mpPass = ref('');
const mpConfirm = ref('');
const updateKey = ref(0);
const alertOpen = ref(false);
const alertMsg = ref('');
const toastState = ref(false);
const toastMsg = ref('');
const wipeStorage = async () => {
loading.value = true;
await storageWipe();
loading.value = false;
};
const settings = reactive({
s: null as unknown as Settings
}) as { s: Settings}
const saveSettings = async () => {
loading.value = true
settings.s.lockOutPeriod = settings.s.lockOutPeriod * 6e4
await setSettings(settings.s)
loading.value = false
}
const setEncryptToggle = (state: boolean) => {
settings.s.enableStorageEnctyption = state
updateKey.value++
}
const changeEncryption = async () => {
loading.value = true
mpModal.value = true
loading.value = false
}
const confirmModal = async () => {
loading.value = true
if(mpPass.value.length < 3) {
loading.value = false
alertMsg.value = 'Password is too short. More than 3 characters are required.';
alertOpen.value = true
setEncryptToggle(settings.s.enableStorageEnctyption)
return
}
if (!settings.s.enableStorageEnctyption) {
if (mpPass.value !== mpConfirm.value) {
loading.value = false
alertMsg.value = 'Password and confirm password do not match';
alertOpen.value = true
setEncryptToggle(settings.s.enableStorageEnctyption)
return
}
let accounts = await getAccounts()
const accProm = accounts.map(async a => {
a.encPk = await encrypt(mpPass.value, a.pk)
a.pk = ''
console.log(a)
return a
})
accounts = await Promise.all(accProm)
console.log(accounts)
await replaceAccounts(accounts)
await saveSelectedAccount(accounts[0])
setEncryptToggle(true)
await setSettings(settings.s)
mpPass.value = ''
mpConfirm.value = ''
mpModal.value = false
} else {
try {
let accounts = await getAccounts()
const accProm = accounts.map(async a => {
if(a.encPk) {
a.pk = await decrypt(a.encPk, mpPass.value)
}
return a
})
accounts = await Promise.all(accProm)
await replaceAccounts(accounts)
await saveSelectedAccount(accounts[0])
setEncryptToggle(false)
settings.s.lockOutEnabled = false
settings.s.encryptAfterEveryTx = false
await setSettings(settings.s)
mpPass.value = ''
mpConfirm.value = ''
mpModal.value = false
} catch {
loading.value = false
alertMsg.value = 'Decryption failed, password is not correct.';
alertOpen.value = true
setEncryptToggle(settings.s.enableStorageEnctyption)
return
}
}
// settings.s.enableStorageEnctyption = true;
loading.value = false
}
onIonViewWillEnter( () => {
getSettings().then((storeSettings) =>
{
settings.s = storeSettings
settings.s.lockOutPeriod = (settings.s.lockOutPeriod / 6e4)
loading.value = false
})
})
const setTime = async () => {
loading.value = true
if ( settings.s.lockOutPeriod < 2 || settings.s.lockOutPeriod > 120){
loading.value = false
alertMsg.value = 'Auto-lock period must be between 2 and 120';
alertOpen.value = true
return
}
settings.s.lockOutPeriod = Math.trunc(settings.s.lockOutPeriod)
await saveSettings()
loading.value = false
toastMsg.value = 'Auto-lock period was set';
toastState.value = true
}
const modalDismiss = () => {
setEncryptToggle(settings.s.enableStorageEnctyption)
}
return {
wipeStorage,
loading,
mpModal,
settings,
saveSettings,
changeEncryption,
mpPass,
mpConfirm,
confirmModal,
updateKey,
alertOpen,
alertMsg,
modalDismiss,
setTime,
toastState,
toastMsg
};
},
});
</script>

130
src/views/SignMessage.vue Normal file
View File

@ -0,0 +1,130 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Sign Message</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>Message to Sign</ion-label>
</ion-item>
<ion-item>
<ion-text>{{ signMsg }}</ion-text>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onSign">Sign</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"
@didDismiss="loading = false"
/>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonAlert,
IonText,
IonLoading,
modalController
} from "@ionic/vue";
// import { ethers } from "ethers";
import {
hexTostr,
} from "@/utils/platform";
import { approve } from "@/extension/userRequest";
import { useRoute } from 'vue-router';
import { getSelectedAccount } from '@/utils/platform'
import UnlockModal from '@/views/UnlockModal.vue'
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonAlert,
IonText,
IonLoading
},
setup: () => {
const route = useRoute()
const loading = ref(false)
const rid = route?.params?.rid as string ?? '';
const signMsg = ref(hexTostr(route?.params?.param as string ?? ''));
const alertOpen = ref(false);
const alertMsg = ref("");
const onCancel = () => {
window.close()
};
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: 'message'
}
});
modal.present();
const { role } = await modal.onWillDismiss();
if(role === 'confirm') return true
return false
}
const onSign = async () => {
loading.value = true;
const selectedAccount = await getSelectedAccount()
if ((selectedAccount.pk ?? '').length !== 66) {
const modalResult = await openModal()
if(modalResult) {
approve(rid)
}else {
onCancel()
}
}else {
approve(rid)
}
loading.value = false
}
return {
signMsg,
onCancel,
alertOpen,
alertMsg,
onSign,
loading
};
},
});
</script>

259
src/views/SignTx.vue Normal file
View File

@ -0,0 +1,259 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Send Transaction</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item><ion-label>Network Name: {{ selectedNetwork?.name }}</ion-label></ion-item>
<ion-item>
<ion-avatar
v-if="(mainNets as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (mainNets as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ selectedNetwork?.chainId }}</ion-label>
</ion-item>
<ion-item>
<ion-label>Transaction to Sign &amp; Send</ion-label>
</ion-item>
<ion-item>
Last Balance: {{ userBalance }}
</ion-item>
<ion-item>
Contract: {{ contract }}
</ion-item>
<ion-item>
Tx Total Cost: {{ totalCost }}
</ion-item>
<ion-item>
Gas Fee: {{ gasFee }}
</ion-item>
<ion-item>
Tx value: {{ txValue }}
</ion-item>
<ion-item>
Gas Limit: {{ gasLimit }} <ion-button @click="setGasLimit">Set manually</ion-button>
</ion-item>
<ion-item>
Gas Price: {{ gasPrice}} <ion-button @click="setGasPrice">Set manually</ion-button>
</ion-item>
<ion-item>
<ion-label>Raw TX:</ion-label>
<ion-textarea :rows="10" :cols="20" :value="signTxData" readonly></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button :disabled="insuficientBalance" @click="onSign">{{ insuficientBalance ? "Insuficient Balance": "Send" }}</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
header="Error"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
<ion-list>
<ion-item>Auto-reject Timer: {{ timerReject }}</ion-item>
</ion-list>
<ion-list v-if="gasPriceReFetch">
<ion-item>New Fee price Timer: {{ timerFee }}</ion-item>
</ion-list>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@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,
IonButton,
IonAlert,
IonTextarea,
onIonViewWillEnter,
IonList,
IonLoading
} from "@ionic/vue";
import { ethers } from "ethers";
import { approve, walletPing, walletSendData } from "@/extension/userRequest";
import { useRoute } from "vue-router";
import { getSelectedNetwork, getUrl } from '@/utils/platform'
import { getBalance, getGasPrice, estimateGas } from '@/utils/wallet'
import type { Network } from '@/extension/types'
import { mainNets } from "@/utils/networks";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonAlert,
IonTextarea,
IonList,
IonLoading
},
setup: () => {
const route = useRoute();
const rid = (route?.params?.rid as string) ?? "";
let isError = false
const decodedParam = decodeURIComponent(route.params?.param as string ?? '')
const params = JSON.parse(decodedParam)
const signTxData = ref('');
const alertOpen = ref(false);
const alertMsg = ref('');
const loading = ref(true)
const contract = params.to
const gasPrice = ref(0);
const gasLimit = ref(0);
const totalCost = ref(0)
const gasFee = ref(0);
const userBalance = ref(0)
const txValue = ref(0)
const timerReject = ref(140)
const timerFee = ref(20)
const insuficientBalance = ref(false)
const gasPriceReFetch = ref(true)
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
let interval = 0
const bars = ref(0)
if(!rid){
isError = true;
}
if(!decodedParam){
isError = true
} else {
signTxData.value = JSON.stringify( params, null, 2)
}
const onSign = () => {
approve(rid);
};
const onCancel = () => {
window.close();
if(interval) {
try {
clearInterval(interval)
} catch {
// ignore
}
}
};
onIonViewWillEnter(async () => {
console.log(params.value);
(window as any)?.resizeTo?.(600, 800)
const pEstimateGas = estimateGas({
to: params?.to ?? '',
from: params?.from ?? '',
data: params?.data ?? '',
value: params?.value ?? '0x0'
})
const pGasPrice = getGasPrice()
const pBalance = getBalance()
selectedNetwork.value = await getSelectedNetwork()
userBalance.value = Number(ethers.utils.formatEther((await pBalance).toString()))
console.log(userBalance.value)
gasPrice.value = parseInt(ethers.utils.formatUnits((await pGasPrice).toString(), "gwei"), 10)
console.log(gasPrice.value)
gasLimit.value = parseInt((await pEstimateGas).toString(), 10)
gasFee.value = Number(ethers.utils.formatUnits(String(gasLimit.value * gasPrice.value), "gwei"))
txValue.value = Number(ethers.utils.formatEther(params?.value ?? '0x0'))
totalCost.value = gasFee.value + txValue.value
if(userBalance.value < totalCost.value){
insuficientBalance.value = true
}
loading.value=false
interval = setInterval(async () => {
if(timerReject.value <= 0) {
onCancel()
return;
}
if( gasPriceReFetch.value ) {
timerFee.value -= 1
if(timerFee.value <= 0) {
timerFee.value = 20
loading.value=true
gasPrice.value = parseInt(ethers.utils.formatUnits((await getGasPrice()).toString(), "gwei"), 10)
gasFee.value = Number(ethers.utils.formatUnits(String(gasLimit.value * gasPrice.value), "gwei"))
txValue.value = Number(ethers.utils.formatEther(params?.value ?? '0x0'))
loading.value=false
}
}
timerReject.value -= 1
bars.value++
walletPing()
}, 1000) as any
})
const setGasLimit = () => {
// TODO
}
const setGasPrice = () => {
// TODO
}
return {
signTxData,
onCancel,
alertOpen,
alertMsg,
onSign,
isError,
contract,
txValue,
gasPrice,
gasLimit,
totalCost,
gasFee,
timerReject,
timerFee,
insuficientBalance,
gasPriceReFetch,
userBalance,
bars,
loading,
selectedNetwork,
mainNets,
getUrl,
setGasLimit,
setGasPrice
};
},
});
</script>

203
src/views/SwitchNetwork.vue Normal file
View File

@ -0,0 +1,203 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Switch Network</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-text>Website requested network switch</ion-text>
</ion-item>
<ion-list>
<ion-item v-if="networkCase === 'exists' || networkCase === 'inTemplates'">
<ion-list>
<ion-item><b>Switch</b></ion-item>
<ion-item>From:</ion-item>
<ion-item>
<ion-list>
<ion-item>Network Name: {{ selectedNetwork?.name }}</ion-item>
<ion-item>
<ion-avatar
v-if="(templateNetworks as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (templateNetworks as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ selectedNetwork?.chainId }}</ion-label>
</ion-item>
</ion-list>
</ion-item>
<ion-item>To</ion-item>
<ion-item>
<ion-list>
<ion-item>Network Name: {{ (templateNetworks as any)[selectedNetwork?.chainId]?.name }}</ion-item>
<ion-item>
<ion-avatar
v-if="(templateNetworks as any)[selectedNetwork?.chainId]?.icon"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="selectedNetwork?.name"
:src="getUrl('assets/chain-icons/' + (templateNetworks as any)[selectedNetwork?.chainId]?.icon)"
/>
</ion-avatar>
<ion-label>Network ID: {{ (templateNetworks as any)[selectedNetwork?.chainId]?.chainId }}</ion-label>
</ion-item>
</ion-list>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button v-if="networkCase === 'exists'" @click="onSwitchExists">Switch</ion-button>
<ion-button v-else @click="onSwitchTemplates">Add Network and Switch</ion-button>
</ion-item>
</ion-list>
</ion-item>
<ion-item v-else>
<ion-list>
<ion-item>Request to change to unknown network ID: {{ }}</ion-item>
<ion-item>Do you want to go to {{ }}</ion-item>
<ion-item>To add it manually.</ion-item>
<ion-item>
<ion-button @click="onCancel">No</ion-button>
<ion-button @click="onSwitchTemplates">Yes</ion-button>
</ion-item>
</ion-list>
</ion-item>
</ion-list>
<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"
@didDismiss="loading = false"
/>
</ion-content>
</ion-page>
</template>
<script lang="ts">
const chainListPage = "https://chainlist.org/chain/";
import { defineComponent, ref, Ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonAlert,
IonText,
IonLoading,
onIonViewWillEnter,
IonList,
} from "@ionic/vue";
// import { ethers } from "ethers";
import { hexTostr } from "@/utils/platform";
import { useRoute } from "vue-router";
import { getSelectedNetwork, getNetworks, getUrl } from "@/utils/platform";
import type { Network } from "@/extension/types";
import { mainNets, testNets } from "@/utils/networks";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonAlert,
IonText,
IonLoading,
IonList,
},
setup: () => {
const route = useRoute();
const loading = ref(true);
const rid = (route?.params?.rid as string) ?? "";
const networkId = ref(hexTostr((route?.params?.param as string) ?? ""));
const alertOpen = ref(false);
const selectedNetwork = (ref(null) as unknown) as Ref<Network>;
const alertMsg = ref("");
const networkCase = ref("");
let pnetworks;
const templateNetworks = Object.assign({}, mainNets, testNets) ?? {};
const onCancel = () => {
window.close();
};
onIonViewWillEnter(async () => {
pnetworks = getNetworks();
selectedNetwork.value = await getSelectedNetwork();
const chainId = parseInt(networkId.value, 16);
const existingNetworks = await pnetworks;
if ((chainId ?? "0") in existingNetworks ?? {}) {
networkCase.value = "exists";
} else if ((chainId ?? "0") in templateNetworks) {
networkCase.value = "inTemplates";
} else {
networkCase.value = "doesNotExist";
}
loading.value = false;
});
const onSwitchExists = async () => {
loading.value = true;
// const selectedAccount = await getSelectedAccount();
// if ((selectedAccount.pk ?? "").length !== 66) {
// const modalResult = await openModal();
// if (modalResult) {
// approve(rid);
// } else {
// onCancel();
// }
// } else {
// approve(rid);
// }
// loading.value = false;
};
const onSwitchTemplates = async () => {
loading.value = true;
};
const onSwitchNotExisting = async () => {
loading.value = true;
};
return {
networkId,
onCancel,
alertOpen,
alertMsg,
onSwitchExists,
loading,
networkCase,
selectedNetwork,
templateNetworks,
getUrl,
onSwitchTemplates,
onSwitchNotExisting
};
},
});
</script>

138
src/views/UnlockModal.vue Normal file
View File

@ -0,0 +1,138 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="close">Close</ion-button>
</ion-buttons>
<ion-title>Unlock to Proceed</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-list v-if="unlockType === 'message'">
<ion-item>To continue signing the message, unlock wallet.</ion-item>
<ion-item>Closing will reject sigining the message</ion-item>
</ion-list>
<ion-list v-else>
<ion-item>To continue sending the transaction, unlock wallet.</ion-item>
<ion-item>Closing will reject sending the tranzaction.</ion-item>
</ion-list>
<ion-item>
<ion-label>Unlock Password</ion-label>
</ion-item> <ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
<ion-item>
<ion-button @click="unlock">Confirm</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"
@didDismiss="loading = false"
/>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonList,
IonButtons,
modalController,
IonLoading
} from "@ionic/vue";
import {
getAccounts,
replaceAccounts,
saveSelectedAccount
} from "@/utils/platform";
import { decrypt } from "@/utils/webCrypto"
export default defineComponent({
props: {
unlockType: {
type: String,
default: 'message'
}
},
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonInput,
IonButton,
IonAlert,
IonList,
IonButtons,
IonLoading
},
setup: () => {
const mpPass = ref('');
const loading = ref(false);
const alertOpen = ref(false);
const alertMsg = ref('');
const close = () => {
return modalController.dismiss(null, 'cancel');
}
const unlock = async () => {
try {
loading.value = true
let accounts = await getAccounts()
const accProm = accounts.map(async a => {
a.pk = await decrypt(a.encPk, mpPass.value)
return a
})
accounts = await Promise.all(accProm)
await replaceAccounts(accounts)
await saveSelectedAccount(accounts[0])
loading.value = false
return modalController.dismiss(null, 'confirm');
} catch {
loading.value = false
alertMsg.value = 'Decryption failed, password is not correct, try again.';
alertOpen.value = true
return
}
}
return {
loading,
unlock,
mpPass,
alertOpen,
alertMsg,
close,
};
},
});
</script>

View File

@ -1,20 +1,20 @@
import { mount } from '@vue/test-utils'
import FolderPage from '@/views/FolderPage.vue'
// import { mount } from '@vue/test-utils'
// import FolderPage from '@/views/FolderPage.vue'
describe('FolderPage.vue', () => {
it('renders folder view', () => {
const mockRoute = {
params: {
id: 'Outbox'
}
}
const wrapper = mount(FolderPage, {
global: {
mocks: {
$route: mockRoute
}
}
})
expect(wrapper.text()).toMatch('Explore UI Components')
})
})
// describe('FolderPage.vue', () => {
// it('renders folder view', () => {
// const mockRoute = {
// params: {
// id: 'Outbox'
// }
// }
// const wrapper = mount(FolderPage, {
// global: {
// mocks: {
// $route: mockRoute
// }
// }
// })
// expect(wrapper.text()).toMatch('Explore UI Components')
// })
// })

View File

@ -12,7 +12,7 @@
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"chrome",
"jest"
],
"paths": {
@ -34,7 +34,7 @@
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"exclude": [
"node_modules"
]
}

47
vite.config.ts Normal file
View File

@ -0,0 +1,47 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'url'
import nodePolyfills from 'rollup-plugin-polyfill-node'
import { crx } from '@crxjs/vite-plugin'
import manifest from './src/extension/manifest.json'
const production = process.env.NODE_ENV === 'production'
// https://vitejs.dev/config/
export default defineConfig({
publicDir: './public',
define: {
'process.env': {}
},
resolve: {
extensions: ['.ts', '.js', '.vue'],
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
stream: 'stream-browserify',
http: 'http-browserify',
https: 'https-browserify',
util: 'util'
}
},
build: {
rollupOptions: {
plugins: [nodePolyfills()]
},
sourcemap: false,
chunkSizeWarningLimit: 1000,
commonjsOptions: {
transformMixedEsModules: true
}
},
plugins: [
!production &&
nodePolyfills({
include: ['node_modules/**/*.js', new RegExp('node_modules/.vite/.*js')]
}),
crx({ manifest }),
vue()
],
server: {
port: 4766
}
})

5066
yarn.lock Normal file

File diff suppressed because it is too large Load Diff