dev: 1.0.1
29
index.html
Normal 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
73
package.json
@ -3,46 +3,47 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "tsc --out src/extension/inject.js src/extension/inject.ts && vue-tsc --noEmit && vite build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"preview": "vite preview"
|
||||||
"test:e2e": "vue-cli-service test:e2e",
|
|
||||||
"lint": "vue-cli-service lint"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/app": "4.0.1",
|
"@capacitor/app": "^4.0.1",
|
||||||
"@capacitor/core": "4.1.0",
|
"@capacitor/core": "^4.3.0",
|
||||||
"@capacitor/haptics": "4.0.1",
|
"@capacitor/haptics": "^4.0.1",
|
||||||
"@capacitor/keyboard": "4.0.1",
|
"@capacitor/keyboard": "^4.0.1",
|
||||||
"@capacitor/status-bar": "4.0.1",
|
"@capacitor/status-bar": "^4.0.1",
|
||||||
"@ionic/vue": "^6.0.0",
|
"@ionic/vue": "^6.3.0",
|
||||||
"@ionic/vue-router": "^6.0.0",
|
"@ionic/vue-router": "^6.3.0",
|
||||||
"core-js": "^3.6.5",
|
"@types/chrome": "^0.0.197",
|
||||||
"vue": "^3.2.21",
|
"core-js": "^3.25.2",
|
||||||
"vue-router": "^4.0.12"
|
"ethers": "^5.7.1",
|
||||||
|
"vue": "^3.2.39",
|
||||||
|
"vue-router": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "4.1.0",
|
"@capacitor/cli": "^4.3.0",
|
||||||
"@types/jest": "^27.0.2",
|
"@crxjs/vite-plugin": "^1.0.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
"@types/jest": "^29.0.3",
|
||||||
"@typescript-eslint/parser": "^5.6.0",
|
"@types/node": "^18.7.19",
|
||||||
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
|
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||||
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
|
"@typescript-eslint/parser": "^5.38.0",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
|
"@vitejs/plugin-vue": "^3.1.0",
|
||||||
"@vue/cli-plugin-router": "~5.0.0-rc.1",
|
"@vue/eslint-config-typescript": "^11.0.2",
|
||||||
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
|
"eslint": "^8.23.1",
|
||||||
"@vue/cli-plugin-unit-jest": "~5.0.0-rc.1",
|
"eslint-plugin-vue": "^9.5.1",
|
||||||
"@vue/cli-service": "~5.0.0-rc.1",
|
"http-browserify": "^1.7.0",
|
||||||
"@vue/eslint-config-typescript": "^9.1.0",
|
"https-browserify": "^1.0.0",
|
||||||
"@vue/test-utils": "^2.0.0-rc.16",
|
"jest": "^29.0.3",
|
||||||
"@vue/vue3-jest": "^27.0.0-alpha.3",
|
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||||
"babel-jest": "^27.3.1",
|
"sass": "^1.55.0",
|
||||||
"cypress": "^8.7.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"eslint": "^8.4.1",
|
"ts-jest": "^29.0.1",
|
||||||
"eslint-plugin-vue": "^8.2.0",
|
"typescript": "^4.8.3",
|
||||||
"jest": "^27.3.1",
|
"util": "^0.12.4",
|
||||||
"ts-jest": "^27.0.7",
|
"vite": "^3.1.3",
|
||||||
"typescript": "^4.3.5"
|
"vue-tsc": "^0.40.13",
|
||||||
|
"yarn-upgrade-all": "^0.7.1"
|
||||||
},
|
},
|
||||||
"description": "An Ionic project"
|
"description": "An Ionic project"
|
||||||
}
|
}
|
||||||
|
BIN
public/assets/chain-icons/arbitrum.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/chain-icons/binance.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/chain-icons/eth.webp
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
public/assets/chain-icons/optimism.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/assets/chain-icons/polygon.webp
Normal file
After Width: | Height: | Size: 1006 B |
BIN
public/assets/chain-icons/xdai.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/extension-icon/wallet_128.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
public/assets/extension-icon/wallet_16.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
public/assets/extension-icon/wallet_32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/extension-icon/wallet_48.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" style="width:400px;height:450px">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Ionic App</title>
|
<title>Clear Wallet</title>
|
||||||
|
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
|
||||||
@ -14,16 +14,17 @@
|
|||||||
<meta name="format-detection" content="telephone=no" />
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<meta name="msapplication-tap-highlight" content="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 -->
|
<!-- add to homescreen for ios -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<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" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
262
src/App.vue
@ -1,248 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<ion-app>
|
<ion-app>
|
||||||
<ion-split-pane content-id="main-content">
|
<ion-router-outlet />
|
||||||
<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-app>
|
</ion-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IonApp, IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote, IonRouterOutlet, IonSplitPane } from '@ionic/vue';
|
import { IonApp, IonRouterOutlet } from "@ionic/vue";
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent } from "vue";
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter} from "vue-router";
|
||||||
import { archiveOutline, archiveSharp, bookmarkOutline, bookmarkSharp, heartOutline, heartSharp, mailOutline, mailSharp, paperPlaneOutline, paperPlaneSharp, trashOutline, trashSharp, warningOutline, warningSharp } from 'ionicons/icons';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
IonApp,
|
IonApp,
|
||||||
IonContent,
|
|
||||||
IonIcon,
|
|
||||||
IonItem,
|
|
||||||
IonLabel,
|
|
||||||
IonList,
|
|
||||||
IonListHeader,
|
|
||||||
IonMenu,
|
|
||||||
IonMenuToggle,
|
|
||||||
IonNote,
|
|
||||||
IonRouterOutlet,
|
IonRouterOutlet,
|
||||||
IonSplitPane,
|
|
||||||
},
|
},
|
||||||
setup() {
|
setup () {
|
||||||
const selectedIndex = ref(0);
|
const route = useRoute()
|
||||||
const appPages = [
|
const router = useRouter()
|
||||||
{
|
const { param, rid } = route.query;
|
||||||
title: 'Inbox',
|
console.log(route?.query,'zzzzzzzzzzzzzzz')
|
||||||
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();
|
switch (route?.query?.route ?? "") {
|
||||||
|
case "sign-msg": {
|
||||||
return {
|
router.push({
|
||||||
selectedIndex,
|
path: `/sign-msg/${rid}/${param}`
|
||||||
appPages,
|
});
|
||||||
labels,
|
break;
|
||||||
archiveOutline,
|
|
||||||
archiveSharp,
|
|
||||||
bookmarkOutline,
|
|
||||||
bookmarkSharp,
|
|
||||||
heartOutline,
|
|
||||||
heartSharp,
|
|
||||||
mailOutline,
|
|
||||||
mailSharp,
|
|
||||||
paperPlaneOutline,
|
|
||||||
paperPlaneSharp,
|
|
||||||
trashOutline,
|
|
||||||
trashSharp,
|
|
||||||
warningOutline,
|
|
||||||
warningSharp,
|
|
||||||
isSelected: (url: string) => url === route.path ? 'selected' : ''
|
|
||||||
}
|
}
|
||||||
|
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>
|
</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
@ -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
@ -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
@ -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'}))
|
55
src/extension/manifest.json
Normal 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>"]
|
||||||
|
}]
|
||||||
|
}
|
5
src/extension/rpcConstants.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const rpcError ={
|
||||||
|
USER_REJECTED: 4001,
|
||||||
|
INVALID_PARAM: -32602,
|
||||||
|
INTERNAL_ERROR: -32603
|
||||||
|
}
|
289
src/extension/serviceWorker.ts
Normal 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¶m=${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¶m=${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¶m=${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
@ -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
|
||||||
|
}
|
32
src/extension/userRequest.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,19 +1,70 @@
|
|||||||
import { createRouter, createWebHistory } from '@ionic/vue-router';
|
import { createRouter, createWebHistory } from '@ionic/vue-router';
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
import AppTabs from '@/views/AppTabs.vue'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '/',
|
||||||
redirect: '/folder/Inbox'
|
redirect: '/tabs/home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/folder/:id',
|
path: '/sign-msg/:rid/:param',
|
||||||
component: () => import ('../views/FolderPage.vue')
|
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({
|
const router = createRouter({
|
||||||
history: createWebHistory(process.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
4
src/shims-vue.d.ts
vendored
@ -1,6 +1,8 @@
|
|||||||
/* eslint-disable */
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
11
src/utils/gecko.ts
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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>
|
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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 & 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
@ -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
@ -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>
|
@ -1,20 +1,20 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
// import { mount } from '@vue/test-utils'
|
||||||
import FolderPage from '@/views/FolderPage.vue'
|
// import FolderPage from '@/views/FolderPage.vue'
|
||||||
|
|
||||||
describe('FolderPage.vue', () => {
|
// describe('FolderPage.vue', () => {
|
||||||
it('renders folder view', () => {
|
// it('renders folder view', () => {
|
||||||
const mockRoute = {
|
// const mockRoute = {
|
||||||
params: {
|
// params: {
|
||||||
id: 'Outbox'
|
// id: 'Outbox'
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
const wrapper = mount(FolderPage, {
|
// const wrapper = mount(FolderPage, {
|
||||||
global: {
|
// global: {
|
||||||
mocks: {
|
// mocks: {
|
||||||
$route: mockRoute
|
// $route: mockRoute
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
expect(wrapper.text()).toMatch('Explore UI Components')
|
// expect(wrapper.text()).toMatch('Explore UI Components')
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
"types": [
|
||||||
"webpack-env",
|
"chrome",
|
||||||
"jest"
|
"jest"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
47
vite.config.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|